agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
140 lines • 5.67 kB
JavaScript
;
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CREDENTIAL_HEADERS = exports.withoutCredentials = exports.readSignIn = exports.signInKeyOf = void 0;
const node_crypto_1 = require("node:crypto");
const types_js_1 = require("./types.js");
/** SHA-256 of a cookie value, base64url — the only form a sign-in is keyed or passed by. */
function signInKeyOf(cookieValue) {
return (0, node_crypto_1.createHash)('sha256').update(cookieValue, 'utf8').digest('base64url');
}
exports.signInKeyOf = signInKeyOf;
/**
* 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) });
*/
function readSignIn(headers, cookieName = types_js_1.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 };
}
exports.readSignIn = readSignIn;
/**
* 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.
*/
function withoutCredentials(headers, options = {}) {
const out = flatten(headers);
for (const name of exports.CREDENTIAL_HEADERS)
delete out[name];
for (const name of options.also ?? [])
delete out[name.toLowerCase()];
return out;
}
exports.withoutCredentials = withoutCredentials;
/**
* 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`.
*/
exports.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