UNPKG

agentfootprint

Version:

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

134 lines • 5.35 kB
/** * hosting/signin/cookie — the sign-in cookie comes OFF the headers, and only * its key goes onward. * * The law (rule 10, "secrets never travel"): a handler never sees the sign-in * cookie. The transport strips it from the headers it hands onward * (`HostRequest.headers`, `HostConversation.headers`) and passes the sign-in * KEY instead — the value's SHA-256, which the store is keyed by and which * cannot be turned back into a cookie. One consumer that logs its headers would * otherwise publish an 8-hour credential. * * `readSignIn` is that stripping, exported for doors that read raw Node * headers themselves (an app's own routes beside the host). `withoutCredentials` * is the other half: every credential-bearing header removed, for anything that * logs. */ import { createHash } from 'node:crypto'; import { SIGN_IN_COOKIE } from './types.js'; /** SHA-256 of a cookie value, base64url — the only form a sign-in is keyed or passed by. */ export function signInKeyOf(cookieValue) { return createHash('sha256').update(cookieValue, 'utf8').digest('base64url'); } /** * Take the sign-in cookie off a header bag. Returns the key and the headers * without the cookie; everything else is left exactly as it was (names * lower-cased, repeated values joined the way the hosts join them). * * @example * const { key, headers } = readSignIn(req.headers); * const who = key === undefined ? undefined : await signIns.identify(key); * log.info({ path: req.url, headers: withoutCredentials(headers) }); */ export function readSignIn(headers, cookieName = SIGN_IN_COOKIE) { const out = flatten(headers); const cookie = out.cookie; if (cookie === undefined) return { headers: out }; const kept = []; const values = []; let stripped = false; for (const part of cookie.split(';')) { const trimmed = part.trim(); if (trimmed.length === 0) continue; const eq = trimmed.indexOf('='); const name = eq < 0 ? trimmed : trimmed.slice(0, eq).trim(); if (name === cookieName) values.push(eq < 0 ? '' : trimmed.slice(eq + 1).trim()); // A sign-in attempt's sealed transaction cookie is the door's alone. else if (name.startsWith(`${cookieName}-tx-`)) stripped = true; else kept.push(trimmed); } if (values.length === 0 && !stripped) return { headers: out }; if (kept.length > 0) out.cookie = kept.join('; '); else delete out.cookie; const value = values.length === 1 ? unquote(values[0]) : ''; return value.length > 0 ? { key: signInKeyOf(value), headers: out } : { headers: out }; } /** * The headers minus every one that carries a credential — for a log line, an * error report, anything that leaves the process. Removes `cookie` and * `authorization` whole (any cookie may be a credential), every header in * {@link CREDENTIAL_HEADERS}, and any header named in `also` — pass the * deployment's own `tokenHeader` there. Names are compared lower-cased. */ export function withoutCredentials(headers, options = {}) { const out = flatten(headers); for (const name of CREDENTIAL_HEADERS) delete out[name]; for (const name of options.also ?? []) delete out[name.toLowerCase()]; return out; } /** * Every header name {@link withoutCredentials} removes: * * - the standard ones: `authorization`, `proxy-authorization`, `cookie`, * `set-cookie`; * - `sec-websocket-protocol`, where a browser dialect carries a bearer token * (a browser's WebSocket cannot set `authorization`); * - what authenticating proxies forward a signed token or identity in: * `x-forwarded-access-token`, `x-auth-request-access-token`, * `x-pomerium-jwt-assertion`, `x-amzn-oidc-accesstoken`, `x-amzn-oidc-data`, * `x-amzn-oidc-identity`, `x-goog-iap-jwt-assertion`, `cf-access-jwt-assertion`, * `x-ms-token-aad-access-token`, `x-ms-token-aad-id-token`, * `x-ms-token-aad-refresh-token`, `x-ms-client-principal`; * - `x-api-key`. * * A header this list does not know (a custom token header) is removed only * when named in `also`. */ export const CREDENTIAL_HEADERS = [ 'authorization', 'proxy-authorization', 'cookie', 'set-cookie', 'sec-websocket-protocol', 'x-forwarded-access-token', 'x-auth-request-access-token', 'x-pomerium-jwt-assertion', 'x-amzn-oidc-accesstoken', 'x-amzn-oidc-data', 'x-amzn-oidc-identity', 'x-goog-iap-jwt-assertion', 'cf-access-jwt-assertion', 'x-ms-token-aad-access-token', 'x-ms-token-aad-id-token', 'x-ms-token-aad-refresh-token', 'x-ms-client-principal', 'x-api-key', ]; function flatten(headers) { const out = {}; for (const [name, value] of Object.entries(headers ?? {})) { const key = name.toLowerCase(); if (typeof value === 'string') out[key] = value; else if (Array.isArray(value)) out[key] = value.join(key === 'cookie' ? '; ' : ', '); } return out; } /** RFC 6265 allows a DQUOTE-wrapped value; the quotes are not part of it. */ function unquote(value) { return value.length >= 2 && value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value; } //# sourceMappingURL=cookie.js.map