// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // Server-only procedure transport for the Convex profile. The browser still uses the same Hono // BFF and session cookie as SQL profiles; the linked admin key never crosses that boundary. // // A write (a generated tenant mutation) is called with `LIVE_TXID_ARG`, so it answers its live log // position with its value; the position is recorded with the router's capture and answered as // `ConusAI-Txid`, as a Postgres transaction id is (umbrella TS6.3). import type { AccountCredential } from "@app/modules/account" import type { Principal } from "@conusai-kit/auth" import type { CallOptions, TransportCaller } from "@conusai-kit/core" import { AppError, internal } from "@conusai-kit/core" import { LIVE_TXID_ARG, type LiveTxidEnvelope } from "@conusai-kit/data-convex/runtime" import { recordTxid } from "@conusai-kit/data-kysely" import { type FileScope, PartGuard, platformObjectStore } from "@conusai-kit/files" import type { ObjectStore as PlatformObjectStore } from "@conusai-kit/platform" import { ConvexHttpClient } from "convex/browser" import { makeFunctionReference, type UserIdentityAttributes } from "convex/server" import { ConvexError } from "convex/values" import { CONVEX_PROCEDURES } from "../../../../convex/generated/procedures" import { CONVEX_BFF_ISSUER } from "../../../../convex/lib/runtime" const unavailableProcedure = (name: string): AppError => new AppError({ type: "/problems/not_found", title: "Not found", status: 404, code: "not_found", detail: `no procedure ${name}`, }) type AdminAuthClient = ConvexHttpClient & { /** Convex SDK 1.45.0 internal server transport, also used by `convex run --identity`. */ setAdminAuth(token: string, identity?: UserIdentityAttributes): void } /** A Convex refusal as the kernel's problem; anything else is an opaque internal error. */ export const asProblem = (error: unknown): AppError => { if (error instanceof AppError) return error if (error instanceof ConvexError && typeof error.data === "object" && error.data !== null) { const value = error.data as Record if ( typeof value.type === "string" && typeof value.title === "string" && typeof value.status === "number" && typeof value.code === "string" && (value.detail === undefined || typeof value.detail === "string") ) { return new AppError(value as unknown as ConstructorParameters[0]) } } return internal() } /** The account credential as identity claims: only for a person signed in to this app. */ function credentialClaims(credential: AccountCredential | undefined): Record { if (!credential?.interactive) return {} return { conusaiCredential: "interactive", ...(credential.sessionId ? { conusaiSessionId: credential.sessionId } : {}), ...(credential.signedInAt ? { conusaiSignedInAt: credential.signedInAt } : {}), } } /** * An account event of the app's identity provider (Better Auth's deletion or ban, from its revocation * relay) on the Convex profile: the account module's flow runs in the backend (convex/account.ts * `providerEvent`), called with the admin key and no identity. A failure is thrown, so the relay keeps * the event and delivers it again. */ export async function deliverConvexAccountEvent( convex: { url: string; adminKey: string }, event: { readonly reason: "account-deleted" | "user-disabled"; readonly userId: string }, ): Promise { const client = convexAdminClient(convex) try { await client.action(makeFunctionReference<"action">("account:providerEvent") as never, event as never) } catch (error) { throw asProblem(error) } } /** Who a request's Convex calls are made as (request.ts): the facts the BFF verified. */ export interface ConvexIdentityOptions { readonly principal: Principal | null readonly user?: { readonly email: string; readonly displayName: string } /** What the account module asks of the credential (request.ts), carried as identity claims (convex/lib/runtime.ts). */ readonly credential?: AccountCredential /** The request's app session (kit-auth `sessionRefKey`): a derived service session is minted for it. */ readonly identitySession?: string } /** * The identity a request's Convex calls carry (convex/lib/runtime.ts `callContextOf` reads it), or * undefined for a request with no principal: its calls are anonymous. */ export function convexIdentity(options: ConvexIdentityOptions): UserIdentityAttributes | undefined { if (!options.principal) return undefined const subject = options.principal.actor.userId return { issuer: CONVEX_BFF_ISSUER, subject, tokenIdentifier: `${CONVEX_BFF_ISSUER}|${subject}`, ...(options.user?.email ? { email: options.user.email } : {}), ...(options.user?.displayName ? { name: options.user.displayName } : {}), ...(options.principal.orgId ? { conusaiOrgId: options.principal.orgId } : {}), ...credentialClaims(options.credential), ...(options.identitySession ? { conusaiIdentitySession: options.identitySession } : {}), } } /** * A one-shot client holding the deployment's admin key, server-side only: with an identity it calls * as that person; without one it may call the deployment's internal functions too. */ export function convexAdminClient( convex: { readonly url: string; readonly adminKey: string }, identity?: UserIdentityAttributes, ): ConvexHttpClient { const client = new ConvexHttpClient(convex.url) ;(client as AdminAuthClient).setAdminAuth(convex.adminKey, identity) return client } /** * A tenant mutation's answer when it was called with `LIVE_TXID_ARG`: the procedure's value, with * the write's live log position recorded for the router's `ConusAI-Txid` (none when it wrote no * collection row). */ export function unwrapLiveTxid(answer: unknown): unknown { const envelope = answer as Partial | null if (typeof envelope !== "object" || envelope === null || !("value" in envelope) || !("txid" in envelope)) { throw internal("a tenant mutation did not answer its live txid envelope") } if (typeof envelope.txid === "string") recordTxid(envelope.txid) return envelope.value } export function createConvexCaller(options: { url: string adminKey: string principal: Principal | null objectStore: PlatformObjectStore /** The request's identity (`convexIdentity`); absent for a request with no principal. */ identity?: UserIdentityAttributes }): TransportCaller { const client = convexAdminClient(options, options.identity) const callByName = async (name: string, input: unknown, callOptions?: CallOptions): Promise => { if (name === "files.file.download") { if (!options.principal) throw unavailableProcedure(name) const value = input as { org: string; workspace: string; project: string; fileId: string } try { const info = (await client.query( makeFunctionReference("files:downloadInfo") as never, { input: value, } as never, )) as { storageKey: string filename: string contentType: string size: number scope: { orgId: string; workspaceId: string; projectId: string } } const scope: FileScope = { level: "project", actor: options.principal.actor, ...info.scope } const object = await platformObjectStore({ blob: options.objectStore }).get(scope, info.storageKey) if (!object) { throw new AppError({ type: "/problems/not_found", title: "Not found", status: 404, code: "not_found", detail: "no such file", }) } const fallback = info.filename.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "download" return new Response(object.body, { status: 200, headers: { "content-type": info.contentType, "content-length": String(info.size), "content-disposition": `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(info.filename)}`, "x-content-type-options": "nosniff", "cache-control": "private, no-store", }, }) } catch (error) { throw asProblem(error) } } if (name === "files.file.putPart") { if (!options.principal) throw unavailableProcedure(name) const value = input as { org: string workspace: string project: string fileId: string part: number body: ReadableStream | Uint8Array } try { const info = (await client.query( makeFunctionReference("files:partInfo") as never, { input: { org: value.org, workspace: value.workspace, project: value.project, fileId: value.fileId, part: value.part, }, } as never, )) as { uploadId: string storageKey: string part: number size: number contentType?: string scope: { orgId: string; workspaceId: string; projectId: string } } const scope: FileScope = { level: "project", actor: options.principal.actor, ...info.scope, } const guard = new PartGuard({ expectedBytes: info.size, ...(info.contentType ? { contentType: info.contentType } : {}), }) const store = platformObjectStore({ blob: options.objectStore }) try { const sent = await store.putUploadPart(scope, { key: info.storageKey, uploadId: info.uploadId, number: info.part, size: info.size, body: guard.wrap(value.body), }) return { part: info.part, etag: sent.etag } } catch (error) { throw guard.violation ?? error } } catch (error) { throw asProblem(error) } } const fileAction = { "files.file.beginUpload": "filesActions:begin", "files.file.completeUpload": "filesActions:complete", "files.file.delete": "filesActions:remove", }[name] if (fileAction) { const fn = makeFunctionReference(fileAction) as never try { return await client.action(fn, { input } as never) } catch (error) { throw asProblem(error) } } const definition = CONVEX_PROCEDURES[name as keyof typeof CONVEX_PROCEDURES] if (!definition) throw unavailableProcedure(name) const fn = makeFunctionReference(definition.reference) as never const args = { input, ...(callOptions?.idempotencyKey ? { idempotencyKey: callOptions.idempotencyKey } : {}) } try { // Widened: an app without guides and support generates no effect procedure at all. switch (definition.kind as "read" | "write" | "effect") { case "read": return await client.query(fn, args as never) // Every write is a generated tenant mutation, the one kind of function that takes LIVE_TXID_ARG. case "write": return unwrapLiveTxid(await client.mutation(fn, { ...args, [LIVE_TXID_ARG]: true } as never)) // An effect of CONVEX_ACTION_EFFECT_MODULES (guides, support): a generated action that runs // the kernel with the module's platform port (convex/lib/effects.ts). case "effect": return await client.action(fn, args as never) } } catch (error) { throw asProblem(error) } } return { call: (contract, input, options) => callByName(contract.name, input, options), callByName, } as TransportCaller }