/** * Scoped multi-app client factory. * * `createClient()` returns an isolated, READ-ONLY client bound to one Bounded * app, independent of the module-global `init()` client. It exists so a single * frontend can hold its normal authenticated client for its own app AND a * read-only client for a SECOND Bounded app (e.g. a shared steward/runtime app), * with live subscriptions on both and zero interference between them. * * Each instance carries its OWN fully-resolved config (endpoints + appId) and * routes through the SAME shared transport/subscription implementation as the * global path - it never forks that code. Isolation comes from threading the * instance config (and a read-only marker) through the existing per-request / * per-subscription override channel, plus the websocket layer already keying * connections by appId. * * ## Anonymous by default, authenticated with `getAuthHeaders` * * With no `getAuthHeaders`, a scoped instance is UNAUTHENTICATED. It sends no * auth, never reads or refreshes the global session, and connects to its app's * realtime endpoint anonymously. It is therefore only useful against collections * a Bounded app exposes with a public read rule (`read: true`) - private/per-user * reads will be denied by the target app's policy, exactly as an anonymous caller * would be. * * Supply `getAuthHeaders` and the instance authenticates AS THAT CREDENTIAL, on * both transports, with nothing shared with any other principal: * - HTTP sends exactly the headers the provider returns, alongside this * instance's `X-App-Id`. The provider is the SOLE auth source: the ambient * global session is neither read nor refreshed nor cleared, and the read * cache is keyed by the provider's own opaque credential. * - The websocket gets its OWN connection, keyed by that credential * (`principalKey`/`principalToken`), so it never shares a socket, an auth * frame or cached data with the global client or with an anonymous scoped * instance for the same appId. * - No per-instance session is ever persisted. The global session store (which * deletes a stored session whose appId differs from the global config's) is * only reachable through the global session manager, and a per-instance * provider never calls into it. * - A provider that returns no credential leaves the instance ANONYMOUS for * that call rather than failing it, so a signed-out page degrades to the * public read lane. An already-authenticated socket is never silently * downgraded: it keeps failing closed on a credential that stops resolving. * * Writes and any auth-requiring surface are NOT supported on a scoped instance * and throw a clear error, with or without a credential. */ import { type GetOptions, type SearchOptions, type CountOptions, type AggregateOptions, type QueryAggregateOptions, type RunQueryOptions, type AggregateOperation, type AggregateResult, type AggregateRow, type AggregateSpec, type GetManyResult, type RequestOverrides } from './operations'; import { type ClientConfig } from './config'; import type { SubscriptionOptions } from '../types'; /** * Resolve the auth headers a scoped instance authenticates with. Called per * request and per subscribe, so a rotated credential is picked up without * recreating the client. * * Return `{ Authorization: 'Bearer ' }` for the app this instance is * bound to - typically from `createAppTokenAuthHeaders`, which exchanges the * signed-in parent app's session for a child-app token. Return `{}` (or null) * when there is no credential: the instance stays anonymous for that call. */ export type ScopedAuthHeadersProvider = () => Record | null | undefined | Promise | null | undefined>; /** Config accepted by {@link createClient}. Only `appId` is required; `network` * (or explicit endpoint fields) selects the target Bounded backend. Supply * `getAuthHeaders` to make the instance authenticated instead of anonymous. */ export type CreateClientConfig = Partial & { appId: string; /** Per-instance credential source. See {@link ScopedAuthHeadersProvider}. */ getAuthHeaders?: ScopedAuthHeadersProvider; }; /** * A read-only client scoped to a single Bounded app, isolated from the global * `init()` client. Read methods mirror the global surface; writes and auth throw. */ export interface BoundedClient { /** The app id this instance is bound to. */ readonly appId: string; /** True when this instance was given a `getAuthHeaders` credential source. */ readonly authenticated: boolean; /** A snapshot of this instance's fully-resolved config (endpoints + appId). */ getConfig(): ClientConfig; /** Read a document or collection. See the global `get`. */ get(path: string, opts?: GetOptions): Promise; /** Batch-read up to 30 documents. See the global `getMany`. */ getMany(paths: string[], opts?: { cache?: boolean; bypassCache?: boolean; _overrides?: RequestOverrides; }): Promise; /** Full-text search a collection declared with `search`. */ search(path: string, query: string, opts?: SearchOptions): Promise; /** Structured, grouped aggregation over a collection. */ queryAggregate(path: string, spec: AggregateSpec, opts?: QueryAggregateOptions): Promise; /** Count matching documents (deterministic on Bounded). */ count(path: string, opts?: CountOptions): Promise; /** Single-scalar aggregate (count/sum/avg/min/max/uniqueCount). */ aggregate(path: string, operation: AggregateOperation, opts?: AggregateOptions): Promise; /** Run a named/computed query defined by the app's policy. */ runQuery(absolutePath: string, queryName: string, queryArgs: any, opts?: RunQueryOptions): Promise; /** Batch variant of {@link runQuery}. */ runQueryMany(many: { absolutePath: string; queryName: string; queryArgs: any; }[], opts?: RunQueryOptions): Promise; /** * Subscribe to realtime updates for a path on THIS app's realtime endpoint. * Authenticated when the instance has a credential (its own socket, keyed by * that credential), anonymous otherwise - in which case the collection must be * publicly readable. Returns an unsubscribe function. Subscriptions on this * instance are fully independent of the global client and of any other scoped * instance. */ subscribe(path: string, options: SubscriptionOptions | ((data: any) => void)): Promise<() => Promise>; /** Not supported on a scoped instance - always throws {@link ScopedClientReadOnlyError}. */ set(path: string, document: any): Promise; /** Not supported on a scoped instance - always throws {@link ScopedClientReadOnlyError}. */ setMany(many: { path: string; document: any; }[]): Promise; } /** Error thrown when an unsupported (write) call is made on a scoped client. * Reads may be authenticated per instance; writes still are not supported. */ export declare class ScopedClientReadOnlyError extends Error { constructor(operation: string); } /** * Create a read-only client scoped to a single Bounded app, anonymous by default * and authenticated when given `getAuthHeaders`. * * @example * ```ts * // Global client stays the venue's own authenticated app: * await init({ appId: VENUE_APP, network: 'bounded-production', authMethod: 'email' }); * * // A second, anonymous client for the steward runtime app's public collections: * const steward = createClient({ appId: STEWARD_APP, network: 'bounded-production' }); * const state = await steward.get('runtime/current'); * const stop = await steward.subscribe('runtime/current', (d) => render(d)); * * // The same second app, read AS THE SIGNED-IN HUMAN: the venue session is * // exchanged for a token pinned to the child app (see createAppTokenAuthHeaders). * const mine = createClient({ * appId: ROOT_APP, * network: 'bounded-production', * getAuthHeaders: createAppTokenAuthHeaders({ * authApiUrl: HUMAN_AUTH_URL, * appId: ROOT_APP, * parentToken: () => getIdToken(), * }), * }); * const stopChat = await mine.subscribe(`conversations/${id}/m`, (d) => render(d)); * ``` */ export declare function createClient(config: CreateClientConfig): BoundedClient;