/** * Remote-domain server entry point. The worker serving URL is the issuer; each * method/view/function signs with its own semantic subject. */ import type { JwksKeys } from '@astrale-os/kernel-server' import { deriveAllowedAlgorithms } from '@astrale-os/kernel-core' import { collectFunctionSubs, domainInstallRequestSchema, hashInstallGraph, } from '@astrale-os/kernel-core/domain' import { createKernelApp } from '@astrale-os/kernel-server' import { Hono } from 'hono' import { importJWK, SignJWT } from 'jose' import type { ViewDef } from '../define/index.js' import type { RemoteServerConfig } from './config.js' import type { RemoteServer, RemoteServerHandle } from './handle.js' import { MetaSchema } from '../deploy/meta.js' import { SdkDispatcher } from '../dispatch/dispatcher.js' import { buildAuxIdentityMap } from '../dispatch/identity.js' import { buildMethodIndex } from '../dispatch/resolve.js' import { buildInstallGraph, buildInstallGraphHash } from '../domain/build-spec.js' import { toSdkContract } from '../domain/contract.js' import { materializeRemoteDomain } from '../domain/define.js' import { mountAuxiliaryRoutes } from './auxiliary-routes.js' import { derivePublicJwk } from './jwks.js' import { canonicalizeServingUrl } from './serving-url.js' export function createRemoteServer(config: RemoteServerConfig): RemoteServer { const methods = buildMethodIndex(config.domain.methods) // Canonical serving URL drives signing, JWKS, /meta, and install credentials. const iss = canonicalizeServingUrl(config.url) // Stamp aux bindings against the same URL used as issuer. const { compiled, auxiliary } = materializeRemoteDomain(config.domain, iss) const dispatcher = new SdkDispatcher({ compiled, methods, deps: config.deps, privateKey: config.privateKey, issuer: iss, // The canonicalized serving URL, NOT the raw config.url: `ctx.env.url` is // documented as the worker's `iss` identity, so the two must be one value. url: iss, }) const publicJwk = derivePublicJwk(config.privateKey) const jwks: JwksKeys = { issuer: iss, loadOwnKeys: async () => [publicJwk], } // Normal /meta schemaHash is the deterministic install-graph hash; explicit overrides win. let cachedSchemaHash: Promise | null = null const resolveSchemaHash = (): Promise => config.meta?.schemaHash !== undefined ? Promise.resolve(config.meta.schemaHash) : (cachedSchemaHash ??= buildInstallGraphHash(config.domain, iss)) // Register `/meta` on the host app before `createKernelApp` mounts its // catch-all routes so the verbatim path wins. const hostApp = config.app ?? new Hono() const metaBase = { iss, sdkCommit: config.meta?.sdkCommit, domainName: config.meta?.domainName ?? compiled.$.origin, ...(config.domain.manifest ? { manifest: config.domain.manifest } : {}), } hostApp.get('/meta', async (c) => c.json(MetaSchema.parse({ ...metaBase, schemaHash: await resolveSchemaHash() })), ) hostApp.post('/_astrale/install-domain', async (c) => { const parsed = domainInstallRequestSchema.safeParse(await c.req.json().catch(() => null)) if (!parsed.success) { return c.json({ error: 'Invalid install request', issues: parsed.error.issues }, 400) } const token = bearerToken(c.req.header('authorization')) try { await config.install?.authorize?.({ c, ...(token ? { token } : {}), kernelIssuer: parsed.data.kernelIssuer, nonce: parsed.data.nonce, deps: config.deps, }) } catch (err) { return c.json({ error: 'Install denied', message: (err as Error).message }, 403) } // The signed graph is built for this serving URL; the kernel verifies it as-is. const graph = buildInstallGraph(config.domain, iss) const graphHash = await hashInstallGraph(graph) const origin = compiled.$.origin // Signed claims and bundle fields must agree exactly. const bundleExtras = { ...(config.postInstall ? { postInstall: config.postInstall } : {}), ...(config.requires && config.requires.length > 0 ? { requires: config.requires } : {}), } // Install credential issuer is the serving URL the kernel fetched. const credential = await signInstallCredential({ privateKey: config.privateKey, issuer: iss, audience: parsed.data.kernelIssuer, nonce: parsed.data.nonce, graphHash, subs: collectFunctionSubs(compiled), ...bundleExtras, }) return c.json({ origin, graph, identity: { credential }, ...bundleExtras, }) }) // Resolved once and shared between the kernel envelope (createKernelApp) and // the aux routes (mountAuxiliaryRoutes) so both honor the same policy. const cors = config.cors ?? { origin: '*' } // Aux routes mount before the kernel catch-all and sign with their member subjects. if (auxiliary) { const auxIdentities = buildAuxIdentityMap(compiled, config.privateKey, iss) mountAuxiliaryRoutes({ app: hostApp, url: auxiliary.url, // oxlint-disable-next-line no-explicit-any views: config.domain.views as Record> | undefined, viewBindings: auxiliary.viewBindings, remoteFunctions: config.domain.remoteFunctions, remoteFunctionBindings: auxiliary.remoteFunctionBindings, deps: config.deps, identities: auxIdentities, cors, }) } const { app } = createKernelApp({ dispatcher, domain: config.domain.methods.map(toSdkContract), host: { url: config.url }, jwks, transports: config.transports, cors, health: config.health, app: hostApp, ws: config.ws, }) return { app, // Expose the canonical issuer, not the raw env URL, for graph identity stamping. iss, async start(port?: number): Promise { const nodeStartModule = './start' const { startNodeServer } = await import(nodeStartModule) return startNodeServer(app, port) }, } } function bearerToken(header: string | undefined): string | undefined { if (!header) return undefined const match = /^Bearer\s+(.+)$/i.exec(header.trim()) return match?.[1] } async function signInstallCredential(args: { privateKey: JsonWebKey issuer: string audience: string nonce: string graphHash: string subs: string[] postInstall?: string requires?: readonly string[] }): Promise { const alg = deriveAllowedAlgorithms(args.privateKey)[0] if (!alg) { throw new Error( `createRemoteServer: cannot derive install signing algorithm from JWK (kty=${args.privateKey.kty}).`, ) } const kid = (args.privateKey as unknown as Record).kid const header = typeof kid === 'string' ? { alg, kid } : { alg } const key = await importJWK(args.privateKey, alg) // postInstall/requires are signed, not just copied into the bundle. return new SignJWT({ subs: args.subs, nonce: args.nonce, graph_hash: args.graphHash, ...(args.postInstall ? { postInstall: args.postInstall } : {}), ...(args.requires ? { requires: args.requires } : {}), }) .setProtectedHeader(header) .setIssuer(args.issuer) .setSubject(args.issuer) .setAudience(args.audience) .setIssuedAt() .setExpirationTime('10m') .sign(key) }