agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
83 lines • 4.23 kB
JavaScript
;
/**
* hosting/signin/audit — the sign-in door's audit trail: one record per
* sign-in outcome, handed to a sink the app owns AND written as one log line.
*
* const door = signInDoor({
* passwords, store, publicUrl, production,
* onAudit: (record) => securityLog.write(record), // typed, optional
* // log: (line) => logger.info(line), // default: console.info
* });
*
* ── Why a door option and not a typed event ─────────────────────────────────
* Every typed event carries `EventMeta`, whose `runId` and `runtimeStageId`
* are REQUIRED, and is emitted by one agent's dispatcher. A sign-in is never a
* run: no agent exists when a person types their password. The library's own
* answer for door facts that happen before any run is a SINK at the door —
* `standingAgent({ onIngressDecision })` (ingressRecord.ts) — and this is the
* same shape, owned by the one place every sign-in outcome is decided.
*
* ── What a record carries, and what it never does ───────────────────────────
* Classes and identifiers only: the outcome, a reason class, the strategy, the
* ACCOUNT the typed name reaches (the checker's `budgetKey` — `alice`,
* `CORP\alice` and `alice@corp.example` are all `alice`), the id the strategy
* PROVED, the client address after trusted-proxy resolution, the time, and on
* `directory-password` the AD sub-code with its name. Never the password,
* never a cookie or its key, never a token, never an error's message.
*
* ── It is not the audit hash chain ──────────────────────────────────────────
* Like the ingress record it is a STREAM into your own sink, not hashed, not
* sequenced and not verifiable by `verifyAuditBundle`. Chain it in your own
* store if you need tamper evidence.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.signInAuditTrail = exports.signInAuditLine = exports.SIGN_IN_AUDIT_PREFIX = void 0;
/** The prefix of the door's audit log line; the rest is the record as JSON. */
exports.SIGN_IN_AUDIT_PREFIX = '[identity] sign-in';
/**
* The record as ONE log line: the prefix, then a JSON object with `time` (ISO
* 8601, UTC) in place of `at`. JSON so a name holding a quote or a space can
* never forge a second field, and so a log pipeline parses it without a
* grammar of its own.
*/
function signInAuditLine(record) {
const { at, ...rest } = record;
const json = JSON.stringify({ time: new Date(at).toISOString(), ...rest });
// JSON leaves U+2028/U+2029 and the bidi controls raw; a viewer may break a
// line at the first or reorder the text after the second. Escaped, the line
// still parses to the same record.
return `${exports.SIGN_IN_AUDIT_PREFIX} ${json.replace(LINE_UNSAFE, escapeChar)}`;
}
exports.signInAuditLine = signInAuditLine;
/** Line and paragraph separators, and the bidi embedding/override/isolate controls. */
const LINE_UNSAFE = /[\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
function escapeChar(ch) {
return `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`;
}
function signInAuditTrail(options) {
let warned = false;
const contained = (where, run) => {
try {
run();
}
catch (err) {
if (warned)
return;
warned = true;
options.warn(`[hosting] signInDoor: the ${where} threw and a sign-in record was dropped. Sign-in is ` +
`unaffected, but this door's audit trail is not being recorded. Reported once per ` +
`door. Cause: ${err?.name ?? 'unknown'}`);
}
};
return {
file(fields) {
const record = { at: options.now(), ...fields };
const sink = options.sink;
if (sink !== undefined)
contained('onAudit sink', () => sink(record));
contained('audit log', () => options.log(signInAuditLine(record)));
},
};
}
exports.signInAuditTrail = signInAuditTrail;
//# sourceMappingURL=audit.js.map