/** * Per-callable identity — dispatcher runtime + install-time wiring. * * Three flavors of callable get an identity per the install-time identity * binding, each keyed by its mount-stable semantic sub: Methods on * classes/interfaces (sub = MethodPath), standalone-function MEMBERS * (sub = FunctionPath `/::function.`), and view MEMBERS * (sub = ViewPath `/::view.`). * * At server startup the method dispatcher and the View / RemoteFunction * route mounter pre-compute, for every materialized callable, the * `RemoteIdentityConfig` they sign outbound kernel calls with. The kernel * then matches an existing function identity instead of provisioning a * generic one. * * **Single source of truth:** every helper here is a thin lens over the * canonical `resolveCallables` from `@astrale-os/kernel-core/domain`. The * kernel validates install subs against the SAME function — drift = 0 * by construction. This closes the previous SDK-side gap where * `collectMethodPaths` filtered to `own|override` and silently dropped * `sealed`/`default` interface methods (which DO materialize as nodes). */ import type { BoundMethod, CompiledDomain } from '@astrale-os/kernel-core/domain' import { resolveCallables } from '@astrale-os/kernel-core/domain' import type { RemoteIdentityConfig } from '../auth/identity.js' import type { AnyRemoteHandler } from '../method/single.js' /** * For each materialized method node, emit its `MethodPath` (semantic, * layout-independent) keyed by the method's namespaced ref. Used to * build the install `subs` claim and to look up runtime per-method * subjects in `buildIdentityMap`. * * Includes every method node the kernel materializes — class own/override * AND interface own (sealed/default/override). Inherited (non-overriding) * class methods reuse the interface's node via a `method_of` edge and * are correctly absent here. */ export function collectMethodPaths(compiled: CompiledDomain): Record { const out: Record = {} for (const c of resolveCallables(compiled)) { if (c.kind === 'method') out[c.ref] = c.sub } return out } /** `{ views, remoteFunctions }` buckets keyed by slug — the shared shape * for every per-aux-callable map (paths, identity configs). */ export type AuxBuckets = { views: Record remoteFunctions: Record } /** * slug → mount-stable semantic `sub` for each aux callable MEMBER node: * - standalone-function members (`kind: 'function'`) → FunctionPath; * - view members (`kind: 'view'`) → ViewPath (plus any hand-authored/legacy * core `View` nodes, `kind: 'core'` className `View`, keyed by their AbsolutePath). * The value is `resolveCallables(...).sub` — the exact subject the kernel stamped on * the node at install, so an outbound-signing worker matches its own function identity. */ export type AuxIdentityPaths = AuxBuckets export function collectAuxIdentityPaths(compiled: CompiledDomain): AuxIdentityPaths { const views: Record = {} const remoteFunctions: Record = {} for (const c of resolveCallables(compiled)) { if (!c.slug) continue if (c.kind === 'function') { remoteFunctions[c.slug] = c.sub } else if (c.kind === 'view' || (c.kind === 'core' && c.className === 'View')) { views[c.slug] = c.sub } // Methods (`kind: 'method'`) get their identity via `collectMethodPaths`. } return { views, remoteFunctions } } /** * slug → `RemoteIdentityConfig` for each auto-materialized View / * RemoteFunction. Built once at server startup; consumed by * `mountAuxiliaryRoutes` so each handler signs outbound `kernel.call(...)` * with its own `sub` (its FunctionPath/ViewPath). Mirrors `buildIdentityMap`. */ export type AuxIdentityMap = AuxBuckets export function buildAuxIdentityMap( compiled: CompiledDomain, privateKey: JsonWebKey, issuer: string, ): AuxIdentityMap { const paths = collectAuxIdentityPaths(compiled) const views: Record = {} for (const [slug, subject] of Object.entries(paths.views)) { views[slug] = { issuer, subject, privateKey } } const remoteFunctions: Record = {} for (const [slug, subject] of Object.entries(paths.remoteFunctions)) { remoteFunctions[slug] = { issuer, subject, privateKey } } return { views, remoteFunctions } } /** * Pre-resolve per-method identity configs. Lookup is by BoundMethod instance, * so the dispatcher pays O(1) per call instead of re-constructing the config. * * `iss` = the worker's own **serving URL** (`issuer` arg, e.g. * `https://crm.test.com`) — its cryptographic identity, DECOUPLED from the * domain's addressing `origin` (a graph slug). The kernel stamps the same * value on each function node at install (it pins `iss` to the URL it fetched * the domain at) and verifies inbound credentials against that issuer's JWKS * (OIDC discovery). The function `sub` stays the origin-addressed MethodPath — * `sub` = which function, `iss` = who signs. */ export function buildIdentityMap( compiled: CompiledDomain, methods: BoundMethod[], privateKey: JsonWebKey, issuer: string, ): Map, RemoteIdentityConfig> { const paths = collectMethodPaths(compiled) const out = new Map, RemoteIdentityConfig>() for (const bound of methods) { const subject = paths[bound.ref] if (!subject) { // Under the "drift = 0" invariant every dispatched method's ref is a key // in `paths`. A miss means an install/index drift — fail loudly at boot // rather than signing with `bound.ref` (a namespaced ref, NOT a // MethodPath sub), which would silently fail kernel identity matching at // call time. Mirrors `identityFor` / `requireAuxIdentity`. throw new Error( `buildIdentityMap: method "${bound.ref}" is in the dispatch index but absent ` + `from resolveCallables(compiled) — no identity subject to sign with.`, ) } out.set(bound, { issuer, subject, privateKey }) } return out }