// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The Convex profile's half of `POST /api/v1/orgs/{org}/live` (live.ts): the CLP/1 stream is served by // @conusai-kit/live-server in this process, over the deployment's own functions (convex/live.ts): // // - the log: `live:head` and `live:changes` (internal queries) read with the admin key, and the wake-up // is a reactive subscription to `live:head` per organization a stream reads (`convexLiveLog` // refcounts it), over one websocket client opened by the first stream // - who may stream what: `live:open`, a public query run under the caller's identity (the same claims // every procedure call carries, convex-caller.ts), decides with the kernel's tenancy over the // deployment's rows (`decideLiveGrants`); its refusal comes back as the live problem the Postgres // profile answers (401 signed out, 404 not reached), and each heartbeat asks `live:recheck` // // The admin key stays in this process; the browser holds no Convex credential. import { type ConvexLiveHead, type ConvexLiveRow, type ConvexLiveSource, type LiveAuthorization, type LiveRecheck, type OpenRequest, problems, type ResolvedTopic, } from "@conusai-kit/live-server" import { ConvexClient } from "convex/browser" import { makeFunctionReference, type UserIdentityAttributes } from "convex/server" import { ConvexError } from "convex/values" import { asProblem, convexAdminClient } from "./convex-caller" const OPEN = makeFunctionReference<"query">("live:open") const RECHECK = makeFunctionReference<"query">("live:recheck") const HEAD = makeFunctionReference<"query">("live:head") const CHANGES = makeFunctionReference<"query">("live:changes") /** What the source and the authorization ask of a one-shot client (`ConvexHttpClient`). */ export interface LiveQueryClient { query(reference: never, args: never): Promise } /** What the source asks of a subscribing client (`ConvexClient`, a websocket). */ export interface LiveWatchClient { onUpdate( reference: never, args: never, callback: (value: unknown) => unknown, onError?: (error: Error) => unknown, ): () => void close(): Promise } type AdminAuthWatchClient = ConvexClient & { /** Convex SDK 1.45.0 internal, as `ConvexHttpClient.setAdminAuth` (convex-caller.ts). */ setAdminAuth(token: string, identity?: UserIdentityAttributes): void } /** * The log source over two clients: `admin` reads (it holds the admin key and no identity, so it may call * the internal `live:head` and `live:changes`), and `watcher` opens the subscribing client on the first * `watch`; `close` closes it. */ export function convexLiveSource(clients: { readonly admin: LiveQueryClient readonly watcher: () => LiveWatchClient }): ConvexLiveSource { let watcher: LiveWatchClient | undefined return { head: async (orgId) => (await clients.admin.query(HEAD as never, { orgId } as never)) as ConvexLiveHead, changes: async (orgId, after, limit) => (await clients.admin.query( CHANGES as never, { orgId, after: { txid: after.txid, seq: after.seq }, limit } as never, )) as { rows: readonly ConvexLiveRow[] head: ConvexLiveHead more: boolean }, watch(orgId, onChange, onError) { watcher ??= clients.watcher() // Convex answers the current head first, then every new one: each is a wake-up, and a wake-up // with nothing new is one empty read. return watcher.onUpdate(HEAD as never, { orgId } as never, () => onChange(), onError) }, async close() { const open = watcher watcher = undefined await open?.close() }, } } /** The log source over the linked deployment, with its admin key (env.convex). */ export function deploymentLiveSource(convex: { readonly url: string; readonly adminKey: string }): ConvexLiveSource { return convexLiveSource({ admin: convexAdminClient(convex) as unknown as LiveQueryClient, watcher: () => { const client = new ConvexClient(convex.url, { unsavedChangesWarning: false }) ;(client as AdminAuthWatchClient).setAdminAuth(convex.adminKey) return client as unknown as LiveWatchClient }, }) } /** The verified caller of one stream, as live.ts resolves it from the request. */ export interface LiveCaller { readonly userId: string /** The session (or credential) key of the per-session stream limit. */ readonly session: string /** The identity its Convex calls carry (convex-caller.ts `convexIdentity`). */ readonly identity: UserIdentityAttributes } export interface ConvexLiveAuthorizeOptions { /** The request's caller: signed in (or the live 401) and its CSRF checked. */ caller(request: Request): Promise /** A one-shot client that calls as `identity` (`convexAdminClient`). */ clientFor(identity: UserIdentityAttributes): LiveQueryClient /** The org slug of `/api/v1/orgs/{org}/live`. */ orgSlugOf(request: Request): string /** Where an unexpected re-check failure is reported (it does not end the stream). */ warn(message: string): void } /** The status of a refusal: a live problem's own, or the problem a Convex function threw. */ function statusOf(error: unknown): number | undefined { if (error instanceof ConvexError) { const status = (error.data as { status?: unknown } | null)?.status return typeof status === "number" ? status : undefined } const status = (error as { status?: unknown } | null)?.status return typeof status === "number" ? status : undefined } /** A refusal of `live:open` as the Postgres profile answers it; another Convex problem as the kernel's. */ function refusalOf(error: unknown): unknown { if (!(error instanceof ConvexError)) return error const status = statusOf(error) if (status === 401) return problems.unauthenticated() if (status === 404) return problems.notFound() return asProblem(error) } interface OpenAnswer { readonly org: string readonly topics: readonly ResolvedTopic[] readonly entities: readonly string[] readonly denied: ReadonlyArray<{ readonly entity: string; readonly reason: string }> } /** The authorize function of `createLiveServer` on the Convex profile. */ export function convexLiveAuthorize( options: ConvexLiveAuthorizeOptions, ): (request: Request, open: OpenRequest) => Promise { return async (request, open) => { const caller = await options.caller(request) const slug = options.orgSlugOf(request) // The validator of `live:open` takes these fields only. const topics = open.topics.map((topic) => ({ ws: topic.ws, ...(topic.project ? { project: topic.project } : {}) })) let answer: OpenAnswer try { answer = (await options .clientFor(caller.identity) .query(OPEN as never, { org: slug, topics, entities: [...open.entities] } as never)) as OpenAnswer } catch (error) { throw refusalOf(error) } return { org: answer.org, session: caller.session, granted: { entities: answer.entities, topics: answer.topics }, denied: answer.denied, // Every heartbeat and on a grant change: the credential is still valid, the caller still a member of // every scope, and still holds the read permission of every entity the stream was granted. recheck: async (): Promise => { try { const again = await options.caller(new Request(request.url, { method: "GET", headers: request.headers })) if (again.userId !== caller.userId) return "session" return (await options .clientFor(again.identity) .query(RECHECK as never, { org: slug, topics, granted: [...answer.entities] } as never)) as LiveRecheck } catch (error) { const status = statusOf(error) if (status === 401) return "session" if (status === 403 || status === 404) return "membership" options.warn(`live: the re-check failed: ${error instanceof Error ? error.name : "failed"}`) return null } }, } } }