UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

191 lines • 10.2 kB
"use strict"; /** * hosting/identityVerification — the door checks the badge, instead of reading * the name off it. * * Until 9.26 the hosting port had exactly one honest sentence about WHO is * calling: `HostRequest.userId` is "what the transport said", and how much that * is worth is the transport's answer. A managed runtime that authenticates in * front of the container can fill it in; a container you expose yourself cannot, * because a header there is a string anybody can send. That sentence is still * true. This module is the other half of it: a way to make the door itself the * thing that knows, so a deployment does not have to choose between "trust a * header" and "no identity at all". * * ── The shape: one strategy, one method ────────────────────────────────────── * {@link IdentityVerifier} takes the bearer token off the wire and hands back * {@link VerifiedIdentity} — `{ userId, roles?, claims? }` — or throws. It is * vendor-neutral by construction: a JWT from any IdP, an opaque token * introspected against a server, a mutual-TLS thumbprint an edge proxy already * checked. `jwksIdentity({ jwksUrl, issuer, audience })` is the one adapter * this release ships, and it covers cloud IdPs and on-prem ones alike because * JWKS is the same protocol in both. * * ── Where it sits, exactly ─────────────────────────────────────────────────── * BEFORE `identityForRequest` composes the run's scope. That ordering is the * whole feature: the composed scope is what memory namespaces on, what * artifacts isolate on, and what a credential provider scopes a token vault on. * Verifying after any of that would be checking the badge on the way out. * * ── The refusal law, and why it is not optional ────────────────────────────── * With a verifier configured, a request that names a `userId` and cannot prove * it is REFUSED — never quietly downgraded to anonymous, and never served under * the name it claimed. Both alternatives are the accepted-and-silently-wrong * failure wearing different clothes: one produces an audit trail that names the * wrong party, the other produces a door that anybody opens by omitting a * field. * * ── Secrets never travel ───────────────────────────────────────────────────── * A bearer token is a credential. It never appears in an error message, an * event payload, a log line or a reply body — not truncated, not fingerprinted, * not "just the first 8 characters". What travels is the CLASS of the failure * ({@link IdentityFailureClass}) and nothing else, which is exactly what an * operator needs to act (`expired` → the client should refresh; * `wrong-audience` → the client is pointed at the wrong API; * `keys-unavailable` → your IdP is unreachable and this is not the caller's * fault). That is the `sdkFailure` law, applied to the front door. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyRequestIdentity = exports.presentedToken = exports.bearerToken = void 0; const errors_js_1 = require("./errors.js"); /** * Pull the bearer token out of the delivered transport headers. * * ONE extraction, so every dialect's tokens are read the same way. The header * is `Authorization: Bearer <token>` — the vocabulary every transport this * package speaks already normalizes onto: * * - `httpHost` (and therefore `nodeHost` / `jsonWire`) hands the handler the * request's lower-cased headers verbatim, so the token is simply there; * - the managed-runtime dialect maps its own bearer SUBPROTOCOL onto * `headers.authorization` before the port ever sees it, which is why that * mapping was written the way it was; * - a custom `HttpWire` that carries the credential somewhere else supplies * the header itself, or wraps its own verifier. * * The scheme match is case-insensitive (RFC 7235 says it is), and a header with * a different scheme returns `undefined` rather than being half-read — handing * a Basic credential to a JWT verifier would produce a confusing refusal about * the wrong thing. */ function bearerToken(headers) { const raw = headers?.authorization ?? headers?.Authorization; if (typeof raw !== 'string' || raw.length === 0) return undefined; const space = raw.indexOf(' '); if (space < 0) return undefined; if (raw.slice(0, space).toLowerCase() !== 'bearer') return undefined; const token = raw.slice(space + 1).trim(); return token.length > 0 ? token : undefined; } exports.bearerToken = bearerToken; /** * The token a request presents under `options.tokenHeader` — `bearerToken` for * the default `authorization`, the raw (trimmed) value for any other header. */ function presentedToken(headers, tokenHeader) { if (tokenHeader === undefined || tokenHeader.toLowerCase() === 'authorization') { return bearerToken(headers); } const raw = headers?.[tokenHeader.toLowerCase()]; if (typeof raw !== 'string') return undefined; const token = raw.replace(/^bearer\s+/i, '').trim(); return token.length > 0 ? token : undefined; } exports.presentedToken = presentedToken; /** * Run one request's verification and hand back the proven identity — or refuse. * * Shared by every door on the composer (a turn, an artifact redemption, a * session-history op) so all three inherit one answer to "who is this", and a * later door cannot accidentally be the lenient one. * * @param options the configured verifier, or `undefined` — with `undefined` * this returns `undefined` immediately and NOTHING about the * request changes (the zero-delta path). * @param headers the delivered transport headers. * @param claimedUserId what the transport put on `HostRequest.userId`. * @param signInKey what the transport put on `HostRequest.signInKey` — the * sign-in cookie's key, the cookie itself already stripped. * * One credential per request (rule 13): a token AND a sign-in together are * refused as `'two-credentials'` — which one to believe is not a question a * door should answer. A sign-in that ended, expired or never existed is * `'expired'`, one answer for all three. A sign-in store that cannot answer is * {@link VerifierUnavailableError} (503), never a signed-out caller. */ async function verifyRequestIdentity(options, headers, claimedUserId, signInKey) { if (options === undefined) return undefined; const token = presentedToken(headers, options.tokenHeader); if (signInKey !== undefined && token !== undefined) { throw new errors_js_1.IdentityNotVerifiedError('two-credentials', claimedUserId !== undefined); } if (signInKey !== undefined && options.signIn !== undefined) { return accepted(await signedIn(options.signIn, signInKey), claimedUserId); } if (token === undefined) { // No badge. Whether that is allowed is the operator's call — but a request // that NAMED a user without one is refused either way, because that is the // header this whole module exists to stop being trusted. if (claimedUserId !== undefined) throw new errors_js_1.IdentityNotVerifiedError('no-token', true); if (options.allowAnonymous === true) return undefined; throw new errors_js_1.IdentityNotVerifiedError('no-token', false); } // A token at a door that takes sign-ins only: nothing here can check it. if (typeof options.verify !== 'function') throw new errors_js_1.IdentityNotVerifiedError('unverifiable', false); let verified; try { verified = await options.verify(token); } catch (err) { // A verifier that named its failure class keeps it. Anything else is // `'unverifiable'` — including a bug in the verifier, because a verifier // that failed in a way it did not describe has proven nothing. if (err instanceof errors_js_1.IdentityNotVerifiedError || err instanceof errors_js_1.VerifierUnavailableError) throw err; throw new errors_js_1.IdentityNotVerifiedError('unverifiable', false); } return accepted(verified, claimedUserId); } exports.verifyRequestIdentity = verifyRequestIdentity; /** The person behind a sign-in key, or the refusal. A store fault is an outage. */ async function signedIn(source, key) { let who; try { who = await source.identify(key); } catch (err) { if (err instanceof errors_js_1.IdentityNotVerifiedError || err instanceof errors_js_1.VerifierUnavailableError) throw err; throw new errors_js_1.VerifierUnavailableError('sign-in', 'its sign-in store could not answer'); } if (who === undefined) throw new errors_js_1.IdentityNotVerifiedError('expired', false); return who; } /** The checks every proven identity passes, whichever credential proved it. */ function accepted(verified, claimedUserId) { if (verified === null || typeof verified !== 'object' || typeof verified.userId !== 'string' || verified.userId.length === 0) { // A verifier that resolved without naming a subject has not identified // anybody. Serving that as "verified" is the silent failure. throw new errors_js_1.IdentityNotVerifiedError('unverifiable', false); } if (claimedUserId !== undefined && claimedUserId !== verified.userId) { // The token proves one person and the request signs another's name. Never // resolved in favour of either: one of the two facts is a lie, and this // door cannot know which. throw new errors_js_1.IdentityNotVerifiedError('claimed-another-user', true); } return verified; } //# sourceMappingURL=identityVerification.js.map