# foundation-sdk

TypeScript SDK for the Foundation platform. Handles authentication, data, files, integrations, and account management with one client.

## Installation

```bash
npm install foundation-sdk
```

If you're using Cognito or Auth0 (most apps), install the corresponding peer dependency:

```bash
npm install aws-amplify              # for Cognito
npm install @auth0/auth0-spa-js      # for Auth0
```

## Quick Start

```typescript
import { createFoundation } from 'foundation-sdk'
import { cognitoAuth } from 'foundation-sdk/cognito'  // or 'foundation-sdk/auth0'

const foundation = await createFoundation({
  configUrl: 'https://backend.example.com/api/v1/public/init',
  tenantId: 'your-tenant-id',
  appId: 'your-app-id',
  auth: cognitoAuth
})

// Now use it
await foundation.auth.signIn('user@example.com', 'password')
const { items: projects } = await foundation.db.list('projects')
```

In production deployments, the SDK reads `/foundation-env.json` to discover the config URL and identifiers, so the call is just:

```typescript
const foundation = await createFoundation({ auth: cognitoAuth })
```

## Configuration

```typescript
interface FoundationConfig {
  /** Public bootstrap endpoint URL. If omitted, reads from /foundation-env.json */
  configUrl?: string
  /** Tenant identifier */
  tenantId?: string
  /** Application identifier */
  appId?: string
  /** Override API base URL — useful with a dev proxy ('/api') */
  baseUrl?: string
  /** Auth provider factory or instance */
  auth?: AuthClient | AuthProvider
}
```

When `/foundation-env.json` is present, its values override `configUrl`, `tenantId`, `appId`, and clear `baseUrl` (the proxy is dev-only).

`configUrl` is fetched exactly as provided. It should already be the public bootstrap URL, usually `https://.../api/v1/public/init`. After auth, the SDK separately fetches authenticated backend config from the API base URL.

`createFoundation(...)` resolves with a fully initialized instance. You do not need to await `foundation.ready` separately.

## Authentication

The SDK supports Cognito, Auth0, or no auth. Pick a provider entry point based on what your Foundation app uses:

```typescript
// Cognito
import { cognitoAuth } from 'foundation-sdk/cognito'
const f = await createFoundation({ auth: cognitoAuth })

// Auth0
import { auth0Auth } from 'foundation-sdk/auth0'
const f = await createFoundation({ auth: auth0Auth })
```

Or auto-register via side-effect import:

```typescript
import 'foundation-sdk/cognito'   // registers cognito as a known provider
const f = await createFoundation({})  // SDK reads provider from config and uses it
```

### Sign in / sign up

`signIn` and `signUp` return result objects so you can branch on the outcome:

```typescript
const result = await foundation.auth.signIn(email, password)

if (result.isSignedIn) {
  router.push('/dashboard')
} else if (result.nextStep?.signInStep === 'CONFIRM_SIGN_UP') {
  router.push({ path: '/confirm', query: { email } })
} else {
  console.error(`Sign-in needs additional step: ${result.nextStep?.signInStep}`)
}

const signUpResult = await foundation.auth.signUp(email, password, { name })
if (signUpResult.isSignUpComplete) {
  // verification email sent — user can sign in
} else if (signUpResult.nextStep?.signUpStep === 'CONFIRM_SIGN_UP') {
  // unconfigured Cognito — user needs to enter a code
  const confirmResult = await foundation.auth.confirmSignUp(email, code)
  if (confirmResult.isSignedIn) {
    router.push('/dashboard')   // already signed in — no trip back to the login form
  } else {
    router.push('/login')       // check confirmResult.nextStep for why (e.g. MFA)
  }
}
```

`confirmSignUp` signs the user in for you: the password passed to `signUp` is held in
memory (never in storage) for 15 minutes so the confirmation code — which already proves
the user owns the address — completes the session instead of dead-ending at a login form.
The password is dropped as soon as it is used, and on `signIn`, `logout`, `resetPassword`,
or TTL expiry. Pass `autoSignInAfterConfirm: false` to `createFoundation` to turn it off.

Result types:

```typescript
interface SignInResult {
  isSignedIn: boolean
  nextStep?: { signInStep: 'DONE' | 'CONFIRM_SIGN_UP' | string; [key: string]: unknown }
}

interface SignUpResult {
  isSignUpComplete: boolean
  userId?: string
  nextStep?: { signUpStep: 'DONE' | 'CONFIRM_SIGN_UP' | string; [key: string]: unknown }
}

// confirmSignUp returns SignInResult: isSignedIn true means the session is ready.
```

### Other auth methods

```typescript
foundation.auth.user                  // User | null
foundation.auth.isAuthenticated       // boolean
foundation.auth.getToken()            // current JWT
foundation.auth.login()               // hosted login redirect
foundation.auth.logout()
foundation.auth.handleCallback()      // call on your /callback route after OAuth redirect
foundation.auth.confirmSignUp(email, code)   // -> SignInResult (auto signs in)
foundation.auth.resendSignUpCode(email)
foundation.auth.forgotPassword(email)
foundation.auth.resetPassword(email, code, newPassword)
foundation.auth.onChange(user => { ... })
```

`auth.resendSignUpCode` is provider-aware: Cognito sends a new code; Auth0 hits the backend `resend-verification` endpoint to retrigger the email link.

## Data (`foundation.db`)

Generic CRUD for any entity defined in your Foundation app:

```typescript
const { items, nextCursor } = await foundation.db.list<Project>('projects', {
  filters: { status: 'active' },
  limit: 20,
  orderBy: 'createdAt',
  orderDir: 'desc'
})

// Run a named list template (from the entity's methods.list.templates).
// Sent on the wire as the reserved `query` param; pass the template's key
// fields via `filters`. If `filters` also contains a `query` key, the
// explicit `template` option wins.
const { items: matches } = await foundation.db.list<Company>('companies', {
  template: 'by-domain',
  filters: { domain: 'acme.com' },
  limit: 1
})
// GET /api/v1/core/companies?domain=acme.com&query=by-domain&limit=1

const project = await foundation.db.get<Project>('projects', id)
const created = await foundation.db.create<Project>('projects', { name: 'New' })
const updated = await foundation.db.update<Project>('projects', id, { name: 'Renamed' })
await foundation.db.save<Project>('projects', data)   // create-or-update
await foundation.db.delete('projects', id)
```

## Realtime (`foundation.on`)

Subscribe to entity-change events pushed over WebSocket:

```typescript
const off = foundation.on('entity.changed', (event) => {
  // event: EntityChangeEvent
  // {
  //   changeType: 'created' | 'modified' | 'deleted'
  //   entityId?: string    // entity record id
  //   resource?: string    // entity resource name, e.g. 'companies'
  //   id?: string
  //   namespace?: string
  //   timestamp?: number
  // }
})

off() // unsubscribe
```

No client-side configuration is needed — the SDK discovers the realtime endpoint from the
auth token. The socket opens lazily on the first subscriber, is shared by all subscribers,
heartbeats and reconnects automatically (server-side idle timeouts are normal), and closes
on the last unsubscribe.

**Backend prerequisite:** events fire only for entities whose backend definition enables
them — `websocket: { enabled: true }` with scope `namespace` or `user`. If the backend has
no realtime support (or there is no `WebSocket` global, e.g. SSR), `foundation.on` logs a
single warning and the listener simply never fires — it does not throw.

## Files (`foundation.files`)

```typescript
// Upload (handles initiate + S3 PUT; sha256 is computed automatically)
const { id, name } = await foundation.files.upload({
  name: file.name,
  contentType: file.type,
  file                               // ArrayBuffer | Blob | File
})

// Or get a presigned URL yourself. sha256 is optional here; if you pass it,
// it must be the BASE64-encoded digest (not hex) and S3 will verify the
// uploaded bytes against it.
const { signedUrl, signedData } = await foundation.files.initiate({ ... })

const file = await foundation.files.get(id)
const { items } = await foundation.files.list({ limit: 20 })
await foundation.files.delete(id)
```

## Integrations (`foundation.integration`)

```typescript
// Get available connectors merged with the user's current connections
const all = await foundation.integration.all()
// [{ id, name, connections: [...], connected: true, connectionCount: 2, ... }]

// Or fetch them separately
const catalog = await foundation.integration.list()
const connections = await foundation.integration.connections()

// Status for a single source
const { connected, connections } = await foundation.integration.status('github-oauth')

// Connect (OAuth flow returns auth URL — open it in a popup or redirect)
const result = await foundation.integration.connect('github-oauth')
if (result.url) window.open(result.url, 'oauth')

await foundation.integration.disconnect('github-oauth', configurationId)
```

## Account (`foundation.account`)

```typescript
const { user, account } = await foundation.account.get()
await foundation.account.update({ name: 'New Name' })
const usage = await foundation.account.usage()
await foundation.account.resendVerification()
```

Pass type parameters for extended user/account shapes:

```typescript
const { user, account } = await foundation.account.get<MyUser, MyAccount>()
```

## Billing (`foundation.billing`)

```typescript
// List purchasable plans. plan.id is the Stripe price id checkout() takes;
// the account's current plan comes back disabled: true ("Current Plan").
const plans = await foundation.billing.plans()

// Start a subscription checkout (returns a plan-change portal flow instead
// when the account already has a plan). Redirect the user to the URL.
const { url } = await foundation.billing.checkout('price_pro_monthly')
window.location.href = url

// Open the Stripe customer portal
const { url: portalUrl } = await foundation.billing.portal()

// On your success page: the redirect can beat the Stripe webhook, so poll
// until the session is complete and paid before treating the plan as active.
const sessionId = new URLSearchParams(location.search).get('checkoutSessionId')!
const session = await foundation.billing.lookupSession(sessionId)
const paid = session.status === 'complete' && session.payment_status === 'paid'

// The account's subscriptions: status, card summary, renewal date, subjects
const subscriptions = await foundation.billing.subscriptions()
```

`checkout`, `portal`, and `subscriptions` are account-owner only — other
members get a 403 with `code: 'OWNER_REQUIRED'`. `plans` and `lookupSession`
work for any authenticated user.

By default the backend derives redirect URLs from your app's origin
(`{origin}/callback/stripe`, `{origin}/plan`, `{origin}/settings/profile`).
Apps with different routes override them — once at init, or per call:

```typescript
const foundation = await createFoundation({
  configUrl: '...',
  billing: {
    successUrl: 'https://app.example.com/billing/done', // after completed checkout
    cancelUrl: 'https://app.example.com/pricing',       // abandoned checkout
    returnUrl: 'https://app.example.com/settings'       // customer portal return
  }
})

// Per-call options win over the init defaults, field by field
await foundation.billing.checkout('price_pro_monthly', {
  successUrl: 'https://app.example.com/onboarding/complete'
})
```

URLs must be absolute http(s). The backend appends
`checkoutSessionId={CHECKOUT_SESSION_ID}` to a custom `successUrl` unless you
already placed the `{CHECKOUT_SESSION_ID}` placeholder yourself.

## OAuth (`foundation.oauth`)

For building consent screens when your app acts as an OAuth authorization server:

```typescript
// Read OAuth params from URL (set by /oauth/authorize redirect)
const params = new URLSearchParams(location.search)
const clientId = params.get('client_id')!

// Get client info to display
const client = await foundation.oauth.getClient(clientId)
// { id, name, description, redirectUris, allowedScopes, ... }

// User clicks Allow → generate code, redirect back
const { code } = await foundation.oauth.authorizeConsent({
  client_id: clientId,
  redirect_uri: params.get('redirect_uri')!,
  scope: params.get('scope')!,
  state: params.get('state')!,
  code_challenge: params.get('code_challenge')!,
  code_challenge_method: params.get('code_challenge_method')!
})

const url = new URL(params.get('redirect_uri')!)
url.searchParams.set('code', code)
url.searchParams.set('state', params.get('state')!)
window.location.href = url.toString()
```

## Config (`foundation.config`)

The full config from your Foundation backend:

```typescript
foundation.config.app          // { id, name, version, environment }
foundation.config.features     // feature flag definitions
foundation.config.plans        // ConfigPlan[] — static plan defs (Foundation plan ids); empty pre-auth; billing.plans() has the Stripe prices
foundation.config.theme        // { colors, dark, defaultColorScheme }
foundation.config.connectors   // available integration connectors
foundation.config.resources    // service endpoints
foundation.config.auth         // auth provider config
foundation.config.raw          // full config object for anything not surfaced above
```

## Logging (`foundation.log`)

```typescript
foundation.log.info('Page loaded', { path: '/dashboard' })
foundation.log.warn('Slow query', { duration: 1234 })
foundation.log.error('Failed to save', { error })
foundation.log.event('button_clicked', { id: 'submit' })
```

## OpenAPI (`foundation.openapi`)

```typescript
const spec = await foundation.openapi.get()
```

## Web Components

The SDK ships pre-built custom elements that handle complex flows. They work in any framework.

```typescript
import 'foundation-sdk/components'   // registers all custom elements
```

```html
<!-- OAuth integration connect modal — handles the popup, completion state, errors -->
<foundation-connect integration-id="github-oauth"></foundation-connect>
```

The component reads the SDK instance from `window.__foundation`, so set it after creating:

```typescript
const foundation = await createFoundation({ ... })
;(window as any).__foundation = foundation
```

If your framework warns about unknown elements, configure it to recognize `foundation-*` tags. For Vue:

```typescript
// vite.config.ts
vue({
  template: {
    compilerOptions: {
      isCustomElement: tag => tag.startsWith('foundation-')
    }
  }
})
```

## Browser Builds (Script Tag)

For apps without a bundler, two pre-bundled IIFE files are available:

```html
<!-- Cognito auth bundled in -->
<script src="https://unpkg.com/foundation-sdk/dist/foundation-sdk.browser.global.js"></script>

<!-- Auth0 auth bundled in -->
<script src="https://unpkg.com/foundation-sdk/dist/foundation-sdk.browser.auth0.global.js"></script>

<script>
  FoundationSDK.createFoundation({
    configUrl: '...',
    tenantId: '...',
    appId: '...'
  }).then(f => {
    window.__foundation = f
  })
</script>

<!-- Optional: web components -->
<script src="https://unpkg.com/foundation-sdk/dist/foundation-sdk.components.global.js"></script>
```

## Type Exports

```typescript
import type {
  Foundation,
  FoundationConfig,
  FullConfig,
  User,
  AuthClient,
  AuthService,
  AuthProvider,
  SignInResult,
  SignUpResult,
  DbService,
  FilesService,
  FileMetadata,
  AccountService,
  BillingService,
  BillingDefaults,
  IntegrationService,
  Integration,
  IntegrationConnection,
  IntegrationDetail,
  OAuthService,
  OAuthClient,
  OAuthConsentParams,
  OpenApiService,
  LogService,
  EntityChangeEvent
} from 'foundation-sdk'
```

## Dev Tip — local proxy

When developing against a remote Foundation backend, configure a Vite proxy and point `baseUrl` at it. This avoids CORS and gives you the network panel:

```typescript
// vite.config.ts
server: {
  proxy: {
    '/api': { target: 'https://backend.your-app.com', changeOrigin: true, secure: true }
  }
}
```

```typescript
const foundation = await createFoundation({
  configUrl: '/api/v1/public/init',
  baseUrl: '/api',
  tenantId: '...',
  appId: '...',
  auth: cognitoAuth
})
```

`baseUrl` is automatically ignored when `/foundation-env.json` is present, so this only takes effect in dev.

## License

MIT
