@kya-os/mcp-i
Version:
The TypeScript MCP framework with identity features built-in
120 lines (119 loc) • 5.15 kB
JavaScript
;
/**
* Outbound Identity Bridge (mcp-i → @kya-os/compute interceptor)
*
* Bridges this package's AsyncLocalStorage request context into the compute
* outbound-identity interceptor so the Layer-2 delegation headers and the
* `KYA-OS-Session-Id` anchor are emitted on the compute (OpenClaw sidecar)
* path — resolving the long-standing `interceptor.ts:183-188` TODO.
*
* Why a global rendezvous (not a direct import): neither package depends on
* the other (compute must not import the SDK; the SDK must not pull in the
* Fly.io sidecar). When the compute interceptor installs it publishes a
* registrar on `Symbol.for("kya-os.outbound-identity.registry")`; this module
* reads that registrar and registers providers backed by `getContext()`. When
* compute is absent (pure-Node / Cloudflare deployments) the registrar is
* missing and this is a clean no-op — those paths inject delegation headers via
* their own fetch wrap (`outbound-delegation.ts`).
*
* Authority model follows ADR-001: the DelegationCredential VC is
* authoritative; `-Chain` / `-Granted-Scopes` are advisory hints.
*
* Related Spec: DIF MCP-I §8 — Outbound Delegation Propagation
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ensureOutboundIdentityBridge = ensureOutboundIdentityBridge;
exports.__resetOutboundIdentityBridgeForTests = __resetOutboundIdentityBridgeForTests;
const mcp_i_core_1 = require("@kya-os/mcp-i-core");
const request_context_1 = require("./request-context");
/**
* Must match `OUTBOUND_IDENTITY_REGISTRY_KEY` in
* `@kya-os/compute`'s `outbound-identity/interceptor.ts`. `Symbol.for` resolves
* the same registry symbol in both packages within one process.
*/
const OUTBOUND_IDENTITY_REGISTRY_KEY = Symbol.for("kya-os.outbound-identity.registry");
// Idempotency guard — the registered providers are closures over getContext(),
// so a single registration covers all subsequent requests in this process.
let bridged = false;
function readRegistry() {
// REASON: globalThis has no index signature for our Symbol key; one localized
// cast reads the typed cross-package slot compute published at install.
return globalThis[OUTBOUND_IDENTITY_REGISTRY_KEY];
}
/**
* Build a compute-compatible delegation provider from the request context.
* Returns undefined per-call when the active context carries no delegation (or
* lacks the signing material), so anonymous / non-delegated calls inject no
* delegation headers.
*/
function delegationProvider() {
const ctx = (0, request_context_1.getContext)();
if (!ctx?.delegationRef ||
!ctx.delegationPrivateKeyJwk ||
!ctx.delegationAgentKid) {
return undefined;
}
const delegationChain = ctx.delegationChain ?? ctx.delegationRef;
const delegationScopes = ctx.delegationScopes ?? [];
const agentDid = ctx.session?.serverDid ?? "";
const userDid = ctx.session?.userDid ?? "";
// Surface the calling client's DID (xmcp-i#413) so the compute interceptor
// can attach it to KTA attestations. Spread conditionally below so the field
// is absent (not `undefined`) when the session carries no client identity.
const clientDid = ctx.session?.clientDid;
const privateKeyJwk = ctx.delegationPrivateKeyJwk;
const kid = ctx.delegationAgentKid;
const delegationRef = ctx.delegationRef;
return {
delegationRef,
delegationChain,
delegationScopes,
agentDid,
userDid,
...(clientDid ? { clientDid } : {}),
kid,
delegationCredential: ctx.delegationCredential,
buildProof: (targetHostname) => (0, mcp_i_core_1.buildDelegationProofJWT)({
agentDid,
userDid,
delegationId: delegationRef,
delegationChain,
scopes: delegationScopes,
privateKeyJwk,
kid,
targetHostname,
}),
};
}
/** Resolve the stable MCP session id from the active request context. */
function sessionIdProvider() {
return (0, request_context_1.getContext)()?.session?.sessionId;
}
/**
* Register the AsyncLocalStorage-backed delegation / session providers with the
* compute outbound-identity interceptor, if it is installed in this process.
*
* Idempotent and best-effort: safe to call from multiple init sites. Returns
* true when the registrar was found and providers were wired, false otherwise.
*/
function ensureOutboundIdentityBridge(logger) {
if (bridged)
return true;
const registry = readRegistry();
if (!registry?.setDelegationProvider) {
logger?.debug("Outbound identity bridge: compute interceptor not present — skipping");
return false;
}
registry.setDelegationProvider(delegationProvider);
registry.setSessionIdProvider?.(sessionIdProvider);
bridged = true;
logger?.debug("Outbound identity bridge: delegation + session providers registered");
return true;
}
/**
* Test-only reset of the idempotency guard. Not part of the public runtime API.
* @internal
*/
function __resetOutboundIdentityBridgeForTests() {
bridged = false;
}