// ---------------------------------------------------------------------------// The per-isolate MCP auth runtime memo, keyed on the bindings it captures.//// Cloudflare reuses warm isolates across binding-only deployments, and its// docs call out exactly the `client ??= new Client(env.SECRET)` pattern as// retaining stale credentials: a plain `runtime ??= ManagedRuntime.make(...)`// would keep authenticating with the OLD WorkOS credentials after a// WORKOS_API_KEY rotation until the isolate happened to be evicted — an auth// outage once the old key is revoked. So the memo is keyed on a fingerprint// of exactly the binding values the layer captures at build: same// fingerprint -> reuse (one string compare per request), changed fingerprint// -> build fresh. The rebuild also resets the ApiKeyService validation cache// living inside the layer, which is required — a rotated key must not serve// cached validations.//// The superseded runtime is DROPPED, not disposed. `ManagedRuntime.dispose`// tears down the runtime's scope, and requests already in flight may still be// running programs against the old runtime's services — pulling the scope out// from under them is not safe. Nothing is leaked by dropping: the MCP auth// layer registers no finalizers (the WorkOS client is plain config; the// org-auth postgres socket is built and closed per `authorize` call), so an// unreferenced runtime is simply GC'd once the last in-flight request// finishes. Rotations only happen on deploys, and at most one previous// runtime is ever still referenced by in-flight work.//// This lives in its own `cloudflare:workers`-free leaf so the node-pool unit// tests can exercise the rebuild semantics; agent-handler.ts is the one// production consumer.// ---------------------------------------------------------------------------import type { ManagedRuntime } from "effect";/** The env subset the MCP auth layer reads at build time. */export type McpAuthBindings = Pick< Env, "WORKOS_API_KEY" | "WORKOS_CLIENT_ID" | "WORKOS_COOKIE_PASSWORD" | "WORKOS_API_URL">;/** * The exact binding values `cloudMcpAuth` captures when its runtime is built — * `WorkOSClient`'s `make` (auth/workos.ts) reads precisely these four. A * joined string is enough to answer "did any of them change"; it CONTAINS the * live WORKOS_API_KEY, so it must never reach a log, span, or error message. * `WORKOS_API_URL` is genuinely optional (unset in production), so absence is * encoded as the empty string — the same "no override" the client resolves it * to (see `workosApiUrlOptions`). * * JSON-encoded rather than delimiter-joined: bindings may contain any byte * (workerd Text permits embedded NUL), so a raw join is not injective — a * delimiter byte inside one value could make two different binding sets * collide and suppress the rebuild a rotation requires. JSON escapes every * byte, so distinct value tuples always produce distinct fingerprints. * * Not included: the module-scope env reads in mcp/auth.ts (AUTHKIT_DOMAIN, * RESOURCE_ORIGIN, the JWKS cache, the JWT audience). Those are frozen at the * isolate's first module evaluation, so rebuilding the runtime cannot refresh * them — keying on them would force rebuilds that change nothing. */export const mcpAuthBindingsFingerprint = (env: McpAuthBindings): string => JSON.stringify([ env.WORKOS_API_KEY, env.WORKOS_CLIENT_ID, env.WORKOS_COOKIE_PASSWORD, env.WORKOS_API_URL ?? "", ]);/** * Memoize one built runtime per fingerprint value (holding only the latest): * an unchanged fingerprint returns the existing runtime, a changed one drops * it (see the header for why dropping, not disposing, is correct) and builds * a fresh runtime via `build`. */export const makeBindingKeyedRuntime = <R, E>( build: () => ManagedRuntime.ManagedRuntime<R, E>,): ((fingerprint: string) => ManagedRuntime.ManagedRuntime<R, E>) => { let runtime: ManagedRuntime.ManagedRuntime<R, E> | undefined; let fingerprint: string | undefined; return (next) => { if (runtime === undefined || fingerprint !== next) { runtime = build(); fingerprint = next; } return runtime; };};
// ---------------------------------------------------------------------------
// The per-isolate MCP auth runtime memo, keyed on the bindings it captures.
//
// Cloudflare reuses warm isolates across binding-only deployments, and its
// docs call out exactly the `client ??= new Client(env.SECRET)` pattern as
// retaining stale credentials: a plain `runtime ??= ManagedRuntime.make(...)`
// would keep authenticating with the OLD WorkOS credentials after a
// WORKOS_API_KEY rotation until the isolate happened to be evicted — an auth
// outage once the old key is revoked. So the memo is keyed on a fingerprint
// of exactly the binding values the layer captures at build: same
// fingerprint -> reuse (one string compare per request), changed fingerprint
// -> build fresh. The rebuild also resets the ApiKeyService validation cache
// living inside the layer, which is required — a rotated key must not serve
// cached validations.
//
// The superseded runtime is DROPPED, not disposed. `ManagedRuntime.dispose`
// tears down the runtime's scope, and requests already in flight may still be
// running programs against the old runtime's services — pulling the scope out
// from under them is not safe. Nothing is leaked by dropping: the MCP auth
// layer registers no finalizers (the WorkOS client is plain config; the
// org-auth postgres socket is built and closed per `authorize` call), so an
// unreferenced runtime is simply GC'd once the last in-flight request
// finishes. Rotations only happen on deploys, and at most one previous
// runtime is ever still referenced by in-flight work.
//
// This lives in its own `cloudflare:workers`-free leaf so the node-pool unit
// tests can exercise the rebuild semantics; agent-handler.ts is the one
// production consumer.
// ---------------------------------------------------------------------------
import type { ManagedRuntime } from "effect";
/** The env subset the MCP auth layer reads at build time. */
export type McpAuthBindings = Pick<
Env,
"WORKOS_API_KEY" | "WORKOS_CLIENT_ID" | "WORKOS_COOKIE_PASSWORD" | "WORKOS_API_URL"
>;
/**
* The exact binding values `cloudMcpAuth` captures when its runtime is built —
* `WorkOSClient`'s `make` (auth/workos.ts) reads precisely these four. A
* joined string is enough to answer "did any of them change"; it CONTAINS the
* live WORKOS_API_KEY, so it must never reach a log, span, or error message.
* `WORKOS_API_URL` is genuinely optional (unset in production), so absence is
* encoded as the empty string — the same "no override" the client resolves it
* to (see `workosApiUrlOptions`).
*
* JSON-encoded rather than delimiter-joined: bindings may contain any byte
* (workerd Text permits embedded NUL), so a raw join is not injective — a
* delimiter byte inside one value could make two different binding sets
* collide and suppress the rebuild a rotation requires. JSON escapes every
* byte, so distinct value tuples always produce distinct fingerprints.
*
* Not included: the module-scope env reads in mcp/auth.ts (AUTHKIT_DOMAIN,
* RESOURCE_ORIGIN, the JWKS cache, the JWT audience). Those are frozen at the
* isolate's first module evaluation, so rebuilding the runtime cannot refresh
* them — keying on them would force rebuilds that change nothing.
*/
export const mcpAuthBindingsFingerprint = (env: McpAuthBindings): string =>
JSON.stringify([
env.WORKOS_API_KEY,
env.WORKOS_CLIENT_ID,
env.WORKOS_COOKIE_PASSWORD,
env.WORKOS_API_URL ?? "",
]);
/**
* Memoize one built runtime per fingerprint value (holding only the latest):
* an unchanged fingerprint returns the existing runtime, a changed one drops
* it (see the header for why dropping, not disposing, is correct) and builds
* a fresh runtime via `build`.
*/
export const makeBindingKeyedRuntime = <R, E>(
build: () => ManagedRuntime.ManagedRuntime<R, E>,
): ((fingerprint: string) => ManagedRuntime.ManagedRuntime<R, E>) => {
let runtime: ManagedRuntime.ManagedRuntime<R, E> | undefined;
let fingerprint: string | undefined;
return (next) => {
if (runtime === undefined || fingerprint !== next) {
runtime = build();
fingerprint = next;
}
return runtime;
};
};