@ktlsr/aer-verify 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ktlesr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,44 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @ktlsr/aer-verify
2
+
3
+ Offline verifier for [Agent Evidence Recorder](https://aer.ktlsr.com) audit packets. Reads local
4
+ files only — no network request, no AER account, no database. Zero runtime dependencies (Node ≥ 18).
5
+
6
+ ```sh
7
+ npx @ktlsr/aer-verify packet.json --receipt receipt.json --trusted-key aer-key.json
8
+ ```
9
+
10
+ - `packet.json` — a JSON packet exported from AER (schema 1.1–2.0).
11
+ - `--receipt` — the receipt **you kept** when the run was sealed (the `complete` response, or its
12
+ `receipt` field). This is what makes a later rewrite detectable.
13
+ - `--trusted-key` (repeatable) — an Ed25519 public key you obtained **out of band**:
14
+ `{ "key_id", "public_key" }`, a list of them, or a saved `GET /api/v1/receipt-keys` response.
15
+ A key carried inside the packet is never used.
16
+ - `--require-anchor` — exit 1 unless the result is tied to your receipt or trusted key.
17
+ - `--json` — machine-readable report.
18
+
19
+ ## What it reports, separately
20
+
21
+ | Check | Meaning |
22
+ | --- | --- |
23
+ | Schema | Known packet version? Unknown versions are never "verified". |
24
+ | Event chain | Each event's hash and link (`aer-chain/1`); `consistent_unsealed` while the run is open. |
25
+ | Seal | Whether the run was sealed (completed/failed). |
26
+ | Run manifest | Agent, status, tenant, times, policies, capture claims, lineage, sourced blocks — frozen at seal (`aer-manifest/1`). |
27
+ | Content hash | Whole-file consistency. Not tamper evidence by itself: an editor can recompute it. |
28
+ | Receipt in packet / signature | The server's receipt, and its signature checked **only** against your trusted keys. |
29
+ | Your kept receipt | Whether this packet is the run you hold a receipt for. |
30
+
31
+ It also lists the fields no integrity check covers (counters, export-time views).
32
+
33
+ **Anchored: NO** means the file is internally consistent — whoever produced it could have rewritten
34
+ and recomputed everything. Anchoring needs a receipt you kept or a signature by a key you trust.
35
+
36
+ Exit codes: `0` verified · `2` verified with limits (legacy schema, unsealed, pre-manifest run) ·
37
+ `1` failed · `4` unsupported or unreadable input.
38
+
39
+ ## Limits
40
+
41
+ AER verifies what was **recorded**: not that the events happened, that sources are correct, or that
42
+ nothing went unreported. `issued_at` in a receipt is the AER server's clock, not an independent
43
+ timestamp. Schema 1.x packets carry no manifest; their content hash covers the writer's key order and
44
+ may not be reproducible after storage.
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.main = main;
5
+ // aer-verify — check an AER audit packet offline. Reads local files only; makes no network request.
6
+ //
7
+ // aer-verify packet.json [--receipt receipt.json] [--trusted-key key.json ...] [--require-anchor] [--json]
8
+ //
9
+ // Exit codes: 0 verified · 2 verified with limits (partial / unsealed / legacy) · 1 failed · 4 unsupported/unreadable.
10
+ const node_fs_1 = require("node:fs");
11
+ const index_1 = require("../../evidence-chain/src/index");
12
+ const USAGE = "usage: aer-verify <packet.json> [--receipt <receipt.json>] [--trusted-key <key.json>]... [--require-anchor] [--json]";
13
+ /** Fixed messages only: a parser/OS error message can quote file contents (keys, payloads). */
14
+ class InputError extends Error {
15
+ }
16
+ function readJson(path, what) {
17
+ let text;
18
+ try {
19
+ text = (0, node_fs_1.readFileSync)(path, "utf8");
20
+ }
21
+ catch {
22
+ throw new InputError(`cannot read the ${what} file`);
23
+ }
24
+ try {
25
+ return JSON.parse(text);
26
+ }
27
+ catch {
28
+ throw new InputError(`the ${what} file is not valid JSON`);
29
+ }
30
+ }
31
+ /** Accepts the receipt itself or the whole `complete` response ({ run, receipt }). */
32
+ function toReceipt(v) {
33
+ const o = v;
34
+ if (o && typeof o === "object" && o.statement)
35
+ return o;
36
+ if (o && typeof o === "object" && o.receipt)
37
+ return o.receipt;
38
+ throw new InputError("the receipt file has no `statement`");
39
+ }
40
+ /** Accepts one key {key_id, public_key}, an array of them, or the /api/v1/receipt-keys response. */
41
+ function toKeys(v) {
42
+ const list = Array.isArray(v) ? v : v && typeof v === "object" && "keys" in v ? v.keys : [v];
43
+ return list.map((k) => {
44
+ const o = k;
45
+ if (!o || typeof o.key_id !== "string" || typeof o.public_key !== "string")
46
+ throw new InputError("a trusted key needs key_id and public_key");
47
+ return { key_id: o.key_id, public_key: o.public_key };
48
+ });
49
+ }
50
+ const LINES = [
51
+ ["schema", "Schema"],
52
+ ["chain", "Event chain"],
53
+ ["seal", "Seal"],
54
+ ["manifest", "Run manifest"],
55
+ ["content_hash", "Content hash"],
56
+ ["packet_receipt", "Receipt in packet"],
57
+ ["signature", "Receipt signature"],
58
+ ["trusted_receipt", "Your kept receipt"],
59
+ ["trusted_receipt_signature", "Kept receipt signature"],
60
+ ];
61
+ function render(r) {
62
+ const out = [`AER packet verification — ${r.overall.toUpperCase()}`, ""];
63
+ for (const [key, label] of LINES) {
64
+ const c = r[key];
65
+ const extra = [c.version && `v${c.version}`, c.key_id && `key ${c.key_id}`, c.detail, c.reason].filter(Boolean).join(" · ");
66
+ out.push(` ${label.padEnd(24)} ${c.status}${extra ? ` (${extra})` : ""}`);
67
+ }
68
+ out.push("", ` Versions: canonical ${r.versions.canonicalization}, chain ${r.versions.chain}, manifest ${r.versions.manifest ?? "—"}, content ${r.versions.content_hash}`);
69
+ out.push(r.anchored
70
+ ? " Anchored: yes — tied to a receipt you kept or a key you trust."
71
+ : " Anchored: NO — this only shows the file is internally consistent. Whoever produced it could have");
72
+ if (!r.anchored)
73
+ out.push(" rewritten and recomputed everything. Pass --receipt and/or --trusted-key to anchor it.");
74
+ if (r.not_integrity_bound.length) {
75
+ out.push("", " Not covered by chain or manifest (editable without detection):");
76
+ for (const f of r.not_integrity_bound)
77
+ out.push(` - ${f}`);
78
+ }
79
+ out.push("", " AER verifies what was recorded, not that the recorded events happened or that sources are correct.");
80
+ return out.join("\n");
81
+ }
82
+ function main(argv) {
83
+ const args = [...argv];
84
+ let packetPath;
85
+ let receiptPath;
86
+ const keyPaths = [];
87
+ let json = false;
88
+ let requireAnchor = false;
89
+ while (args.length) {
90
+ const a = args.shift();
91
+ if (a === "--receipt")
92
+ receiptPath = args.shift();
93
+ else if (a === "--trusted-key")
94
+ keyPaths.push(args.shift() ?? "");
95
+ else if (a === "--json")
96
+ json = true;
97
+ else if (a === "--require-anchor")
98
+ requireAnchor = true;
99
+ else if (a === "-h" || a === "--help") {
100
+ console.log(USAGE);
101
+ return 0;
102
+ }
103
+ else if (!packetPath && !a.startsWith("--"))
104
+ packetPath = a;
105
+ else {
106
+ console.error(USAGE);
107
+ return 4;
108
+ }
109
+ }
110
+ if (!packetPath) {
111
+ console.error(USAGE);
112
+ return 4;
113
+ }
114
+ let report;
115
+ try {
116
+ report = (0, index_1.verifyPacket)(readJson(packetPath, "packet"), {
117
+ trustedReceipt: receiptPath ? toReceipt(readJson(receiptPath, "receipt")) : undefined,
118
+ trustedKeys: keyPaths.flatMap((p) => toKeys(readJson(p, "trusted key"))),
119
+ });
120
+ }
121
+ catch (err) {
122
+ // Unreadable input: our own fixed message, never the underlying error text.
123
+ console.error(`aer-verify: ${err instanceof InputError ? err.message : "input could not be processed"}`);
124
+ return 4;
125
+ }
126
+ console.log(json ? JSON.stringify(report, null, 2) : render(report));
127
+ if (report.overall === "unsupported")
128
+ return 4;
129
+ if (report.overall === "failed")
130
+ return 1;
131
+ if (requireAnchor && !report.anchored)
132
+ return 1;
133
+ return report.overall === "verified" ? 0 : 2;
134
+ }
135
+ if (require.main === module)
136
+ process.exit(main(process.argv.slice(2)));
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ // Deterministic serialization. The writer and every verifier MUST produce identical bytes for the
3
+ // same logical value, regardless of object key order. Leaves are int | string | boolean | null
4
+ // (this domain has no floats; costMicroUsd is integer micro-USD). undefined is normalized to null.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.canonicalize = canonicalize;
7
+ function sortValue(v) {
8
+ if (v === undefined || v === null)
9
+ return null;
10
+ if (Array.isArray(v))
11
+ return v.map(sortValue);
12
+ if (typeof v === "object") {
13
+ const out = {};
14
+ for (const k of Object.keys(v).sort()) {
15
+ out[k] = sortValue(v[k]);
16
+ }
17
+ return out;
18
+ }
19
+ return v;
20
+ }
21
+ function canonicalize(value) {
22
+ return JSON.stringify(sortValue(value));
23
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeChain = computeChain;
4
+ const hash_1 = require("./hash");
5
+ /** Build the append-only chain for a run: genesis → … → head, in seq order. */
6
+ function computeChain(runId, cores) {
7
+ const ordered = [...cores].sort((a, b) => a.seq - b.seq);
8
+ const links = [];
9
+ let prev = (0, hash_1.genesisHash)(runId);
10
+ for (const core of ordered) {
11
+ const hash = (0, hash_1.hashEvent)(prev, core);
12
+ links.push({ ...core, prevHash: prev, hash });
13
+ prev = hash;
14
+ }
15
+ return links;
16
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.genesisHash = genesisHash;
4
+ exports.hashEvent = hashEvent;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const canonicalize_1 = require("./canonicalize");
7
+ const sha256hex = (s) => (0, node_crypto_1.createHash)("sha256").update(s, "utf8").digest("hex");
8
+ /** Chain root, bound to the run so a chain copied wholesale from another run fails verification. */
9
+ function genesisHash(runId) {
10
+ return sha256hex("AER-CHAIN-v1:" + runId);
11
+ }
12
+ /** The canonical, hashed projection of an event. Findings are sorted so storage order can't change the hash. */
13
+ function coreView(c) {
14
+ return {
15
+ seq: c.seq,
16
+ type: c.type,
17
+ title: c.title,
18
+ occurredAt: c.occurredAt,
19
+ inputRedacted: c.inputRedacted ?? null,
20
+ outputRedacted: c.outputRedacted ?? null,
21
+ riskLevel: c.riskLevel ?? null,
22
+ costMicroUsd: c.costMicroUsd ?? null,
23
+ metadata: c.metadata ?? null,
24
+ findings: [...c.findings]
25
+ .map((f) => ({
26
+ findingType: f.findingType,
27
+ severity: f.severity,
28
+ fieldPath: f.fieldPath,
29
+ originalHash: f.originalHash,
30
+ }))
31
+ .sort((a, b) => `${a.fieldPath}|${a.originalHash}|${a.findingType}`.localeCompare(`${b.fieldPath}|${b.originalHash}|${b.findingType}`)),
32
+ };
33
+ }
34
+ /** hash = sha256(prevHash + "\n" + canonical(core)). The delimiter keeps prev/core unambiguous. */
35
+ function hashEvent(prevHash, core) {
36
+ return sha256hex(prevHash + "\n" + (0, canonicalize_1.canonicalize)(coreView(core)));
37
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.unrepresentableKeyPath = exports.packetContentHash = exports.manifestInputFromPacket = exports.packetChainLinks = exports.verifyPacket = exports.checkReceiptSignature = exports.publicKeyOf = exports.signReceipt = exports.manifestHash = exports.buildManifest = exports.verifyChain = exports.computeChain = exports.hashEvent = exports.genesisHash = exports.canonicalize = void 0;
18
+ var canonicalize_1 = require("./canonicalize");
19
+ Object.defineProperty(exports, "canonicalize", { enumerable: true, get: function () { return canonicalize_1.canonicalize; } });
20
+ var hash_1 = require("./hash");
21
+ Object.defineProperty(exports, "genesisHash", { enumerable: true, get: function () { return hash_1.genesisHash; } });
22
+ Object.defineProperty(exports, "hashEvent", { enumerable: true, get: function () { return hash_1.hashEvent; } });
23
+ var chain_1 = require("./chain");
24
+ Object.defineProperty(exports, "computeChain", { enumerable: true, get: function () { return chain_1.computeChain; } });
25
+ var verify_1 = require("./verify");
26
+ Object.defineProperty(exports, "verifyChain", { enumerable: true, get: function () { return verify_1.verifyChain; } });
27
+ var manifest_1 = require("./manifest");
28
+ Object.defineProperty(exports, "buildManifest", { enumerable: true, get: function () { return manifest_1.buildManifest; } });
29
+ Object.defineProperty(exports, "manifestHash", { enumerable: true, get: function () { return manifest_1.manifestHash; } });
30
+ var receipt_1 = require("./receipt");
31
+ Object.defineProperty(exports, "signReceipt", { enumerable: true, get: function () { return receipt_1.signReceipt; } });
32
+ Object.defineProperty(exports, "publicKeyOf", { enumerable: true, get: function () { return receipt_1.publicKeyOf; } });
33
+ Object.defineProperty(exports, "checkReceiptSignature", { enumerable: true, get: function () { return receipt_1.checkReceiptSignature; } });
34
+ var packet_1 = require("./packet");
35
+ Object.defineProperty(exports, "verifyPacket", { enumerable: true, get: function () { return packet_1.verifyPacket; } });
36
+ Object.defineProperty(exports, "packetChainLinks", { enumerable: true, get: function () { return packet_1.packetChainLinks; } });
37
+ Object.defineProperty(exports, "manifestInputFromPacket", { enumerable: true, get: function () { return packet_1.manifestInputFromPacket; } });
38
+ Object.defineProperty(exports, "packetContentHash", { enumerable: true, get: function () { return packet_1.packetContentHash; } });
39
+ __exportStar(require("./versions"), exports);
40
+ var representable_1 = require("./representable");
41
+ Object.defineProperty(exports, "unrepresentableKeyPath", { enumerable: true, get: function () { return representable_1.unrepresentableKeyPath; } });
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildManifest = buildManifest;
4
+ exports.manifestHash = manifestHash;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const canonicalize_1 = require("./canonicalize");
7
+ const hash_1 = require("./hash");
8
+ const versions_1 = require("./versions");
9
+ const sha256hex = (s) => (0, node_crypto_1.createHash)("sha256").update(s, "utf8").digest("hex");
10
+ /**
11
+ * The run manifest: binds run context (agent, status, tenant, policies, lineage) and each event's
12
+ * out-of-chain fields (capture claim, policy, sourced block — the latter by digest) to the chain
13
+ * head. Frozen at seal; its hash is what a receipt/signature commits to.
14
+ */
15
+ function buildManifest(input) {
16
+ const nullable = (v) => (v === undefined ? null : v);
17
+ return {
18
+ manifest_version: versions_1.MANIFEST,
19
+ canonicalization: versions_1.CANONICALIZATION,
20
+ hash_algorithm: "sha256",
21
+ chain: {
22
+ version: versions_1.CHAIN,
23
+ genesis: (0, hash_1.genesisHash)(input.run.id),
24
+ head: input.seal,
25
+ event_count: input.events.length,
26
+ },
27
+ run: {
28
+ id: input.run.id,
29
+ organization_id: input.run.organizationId,
30
+ project_id: input.run.projectId,
31
+ agent_name: input.run.agentName,
32
+ status: input.run.status,
33
+ risk_level: input.run.riskLevel,
34
+ started_at: input.run.startedAt,
35
+ ended_at: input.run.endedAt,
36
+ cost_micro_usd: input.run.costMicroUsd,
37
+ redaction_policy: input.run.redactionPolicy,
38
+ capture_declared: nullable(input.run.captureDeclared),
39
+ environment: input.run.environment,
40
+ conversation_ref: input.run.conversationRef,
41
+ turn_ref: input.run.turnRef,
42
+ parent_run_id: input.run.parentRunId,
43
+ },
44
+ events: [...input.events]
45
+ .sort((a, b) => a.seq - b.seq)
46
+ .map((e) => ({
47
+ seq: e.seq,
48
+ hash: e.hash,
49
+ redaction_policy: e.redactionPolicy,
50
+ capture_declared: nullable(e.captureDeclared),
51
+ sourced_digest: e.sourced === null || e.sourced === undefined ? null : sha256hex((0, canonicalize_1.canonicalize)(e.sourced)),
52
+ })),
53
+ };
54
+ }
55
+ /** sha256 over the manifest's canonical bytes (raw hex). */
56
+ function manifestHash(manifest) {
57
+ return sha256hex((0, canonicalize_1.canonicalize)(manifest));
58
+ }
@@ -0,0 +1,419 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.packetChainLinks = packetChainLinks;
4
+ exports.manifestInputFromPacket = manifestInputFromPacket;
5
+ exports.packetContentHash = packetContentHash;
6
+ exports.packetShapeError = packetShapeError;
7
+ exports.verifyPacket = verifyPacket;
8
+ // Offline packet verification. Input: a parsed packet (JSON) plus what the VERIFIER trusts (a receipt
9
+ // it kept, public keys it obtained out of band). No network, no DB, no AER account.
10
+ const node_crypto_1 = require("node:crypto");
11
+ const canonicalize_1 = require("./canonicalize");
12
+ const manifest_1 = require("./manifest");
13
+ const receipt_1 = require("./receipt");
14
+ const hash_1 = require("./hash");
15
+ const representable_1 = require("./representable");
16
+ const verify_1 = require("./verify");
17
+ const versions_1 = require("./versions");
18
+ const sha256hex = (s) => (0, node_crypto_1.createHash)("sha256").update(s, "utf8").digest("hex");
19
+ const strip = (v) => v.replace(/^sha256:/, "");
20
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
21
+ const str = (v) => (typeof v === "string" ? v : null);
22
+ /** Packet events → chain links, `sha256:` presentation prefix removed (the bytes the writer hashed). */
23
+ function packetChainLinks(packet) {
24
+ const events = Array.isArray(packet.events) ? packet.events : [];
25
+ return events.map((e) => ({
26
+ seq: e.seq,
27
+ type: e.type,
28
+ title: e.title,
29
+ occurredAt: e.occurredAt,
30
+ inputRedacted: e.inputRedacted ?? null,
31
+ outputRedacted: e.outputRedacted ?? null,
32
+ riskLevel: e.riskLevel ?? null,
33
+ costMicroUsd: e.costMicroUsd ?? null,
34
+ metadata: e.metadata ?? null,
35
+ findings: (Array.isArray(e.findings) ? e.findings : []).map((f) => ({
36
+ findingType: f.findingType,
37
+ severity: f.severity,
38
+ fieldPath: f.fieldPath,
39
+ originalHash: typeof f.originalHash === "string" ? strip(f.originalHash) : "",
40
+ })),
41
+ hash: typeof e.hash === "string" ? strip(e.hash) : "",
42
+ prevHash: typeof e.prevHash === "string" ? strip(e.prevHash) : "",
43
+ }));
44
+ }
45
+ /** A capture view {layer, ...declared} back to the stored declaration. */
46
+ function declared(capture) {
47
+ const d = isObj(capture) ? capture.client_declared : null;
48
+ if (!isObj(d))
49
+ return null;
50
+ const { layer: _layer, ...rest } = d;
51
+ return rest;
52
+ }
53
+ const serverPolicy = (capture) => {
54
+ const s = isObj(capture) ? capture.server_applied : null;
55
+ return isObj(s) ? str(s.policy) : null;
56
+ };
57
+ /** Rebuild the manifest input from a 2.x packet — must match what the server froze at seal. */
58
+ function manifestInputFromPacket(packet) {
59
+ const run = packet.run;
60
+ const links = isObj(run.links) ? run.links : {};
61
+ const parent = isObj(links.parentRun) ? links.parentRun : null;
62
+ return {
63
+ run: {
64
+ id: run.id,
65
+ organizationId: run.organizationId,
66
+ projectId: run.projectId,
67
+ agentName: run.agentName,
68
+ status: run.status,
69
+ riskLevel: run.riskLevel,
70
+ startedAt: run.startedAt,
71
+ endedAt: str(run.endedAt),
72
+ costMicroUsd: run.costMicroUsd,
73
+ redactionPolicy: serverPolicy(run.capture),
74
+ captureDeclared: declared(run.capture),
75
+ environment: str(run.environment),
76
+ conversationRef: str(links.conversationRef),
77
+ turnRef: str(links.turnRef),
78
+ parentRunId: parent ? str(parent.id) : null,
79
+ },
80
+ events: packet.events.map((e) => {
81
+ let sourced = null;
82
+ if (isObj(e.sourced)) {
83
+ // `not_recorded` is a reading aid added at export time, not part of the stored block.
84
+ const { not_recorded: _gaps, ...rest } = e.sourced;
85
+ sourced = rest;
86
+ }
87
+ return {
88
+ seq: e.seq,
89
+ hash: typeof e.hash === "string" ? strip(e.hash) : "",
90
+ redactionPolicy: serverPolicy(e.capture),
91
+ captureDeclared: declared(e.capture),
92
+ sourced,
93
+ };
94
+ }),
95
+ seal: typeof packet.seal === "string" ? strip(packet.seal) : "",
96
+ };
97
+ }
98
+ /** content_hash as the writer computed it, per schema generation. */
99
+ function packetContentHash(packet) {
100
+ const exp = isObj(packet.export) ? packet.export : {};
101
+ const { content_hash: _h, ...restExport } = exp;
102
+ const base = { ...packet, export: restExport };
103
+ return String(packet.schema_version).startsWith("1.")
104
+ ? { algorithm: versions_1.CONTENT_HASH_LEGACY, hex: sha256hex(JSON.stringify(base)) }
105
+ : { algorithm: versions_1.CONTENT_HASH, hex: sha256hex((0, canonicalize_1.canonicalize)(base)) };
106
+ }
107
+ /** Fields only the content_hash covers — anyone who edits the file can recompute that hash too. */
108
+ const NOT_INTEGRITY_BOUND = {
109
+ "1": [
110
+ "run.* (agentName, status, riskLevel, times, counters)",
111
+ "run.capture / events[].capture",
112
+ "run.links / events[].sourced",
113
+ "redaction_findings (copy of events[].findings)",
114
+ "export.*",
115
+ ],
116
+ "2": [
117
+ "run.durationMs, run.eventCount, run.redactionCount (counters)",
118
+ "run.links.parentRun.status / agentName (resolved at export time)",
119
+ "events[].sourced.not_recorded (derived at export time)",
120
+ "redaction_findings (copy of events[].findings)",
121
+ "views, coverage (descriptive)",
122
+ "export.* (generatedAt, type)",
123
+ ],
124
+ };
125
+ const HASH = /^sha256:[0-9a-f]{64}$/;
126
+ const isInt = (v) => Number.isInteger(v);
127
+ const isStrOrNull = (v) => v === null || typeof v === "string";
128
+ const isHashOrNull = (v) => v === null || (typeof v === "string" && HASH.test(v));
129
+ /**
130
+ * The packet's shape, checked BEFORE anything is hashed or compared: a wrong type must never be
131
+ * normalised into a valid one (e.g. `sourced: []` read as null). Free-form JSON (input/output/
132
+ * metadata, sourced/declaration contents) is only required to be present with the right outer type.
133
+ */
134
+ function packetShapeError(p, version) {
135
+ const minor = version.startsWith("1.") ? Number(version.slice(2)) : 99;
136
+ const run = p.run;
137
+ const top = [
138
+ ["run.id", typeof run.id === "string"],
139
+ ["run.agentName", typeof run.agentName === "string"],
140
+ ["run.status", typeof run.status === "string"],
141
+ ["run.riskLevel", typeof run.riskLevel === "string"],
142
+ ["run.startedAt", typeof run.startedAt === "string"],
143
+ ["run.endedAt", isStrOrNull(run.endedAt)],
144
+ ["run.costMicroUsd", isInt(run.costMicroUsd)],
145
+ ["seal", isHashOrNull(p.seal)],
146
+ ["chain", isObj(p.chain) && typeof p.chain.algorithm === "string" && typeof p.chain.genesis === "string"],
147
+ ["export", isObj(p.export) && typeof p.export.generatedAt === "string"],
148
+ ["redaction_findings", Array.isArray(p.redaction_findings)],
149
+ ];
150
+ if (minor >= 2)
151
+ top.push(["run.capture", captureOk(run.capture)]);
152
+ if (minor >= 3) {
153
+ const l = run.links;
154
+ top.push([
155
+ "run.links",
156
+ isObj(l) && isStrOrNull(l.conversationRef) && isStrOrNull(l.turnRef) &&
157
+ (l.parentRun === null || (isObj(l.parentRun) && typeof l.parentRun.id === "string")),
158
+ ]);
159
+ }
160
+ if (minor === 99) {
161
+ top.push(["run.organizationId", typeof run.organizationId === "string"], ["run.projectId", typeof run.projectId === "string"], ["run.environment", isStrOrNull(run.environment)], ["manifest_hash", isHashOrNull(p.manifest_hash)], ["receipt", p.receipt === null || receiptShapeOk(p.receipt)], ["versions", isObj(p.versions)]);
162
+ }
163
+ for (const [field, ok] of top)
164
+ if (!ok)
165
+ return `${field} has the wrong type or is missing`;
166
+ const events = p.events;
167
+ for (let i = 0; i < events.length; i++) {
168
+ const e = events[i];
169
+ if (!isObj(e))
170
+ return `events[${i}] is not an object`;
171
+ const checks = [
172
+ ["seq", isInt(e.seq) && e.seq > 0],
173
+ ["type", typeof e.type === "string"],
174
+ ["title", typeof e.title === "string"],
175
+ ["occurredAt", typeof e.occurredAt === "string"],
176
+ ["inputRedacted", "inputRedacted" in e],
177
+ ["outputRedacted", "outputRedacted" in e],
178
+ ["metadata", "metadata" in e],
179
+ ["riskLevel", isStrOrNull(e.riskLevel)],
180
+ ["costMicroUsd", e.costMicroUsd === null || isInt(e.costMicroUsd)],
181
+ ["hash", isHashOrNull(e.hash)],
182
+ ["prevHash", isHashOrNull(e.prevHash)],
183
+ [
184
+ "findings",
185
+ Array.isArray(e.findings) &&
186
+ e.findings.every((f) => isObj(f) && typeof f.findingType === "string" && typeof f.severity === "string" &&
187
+ typeof f.fieldPath === "string" && isHashOrNull(f.originalHash)),
188
+ ],
189
+ ];
190
+ if (minor >= 2)
191
+ checks.push(["capture", captureOk(e.capture)]);
192
+ if (minor >= 3)
193
+ checks.push(["sourced", e.sourced === null || isObj(e.sourced)]);
194
+ for (const [field, ok] of checks)
195
+ if (!ok)
196
+ return `events[${i}].${field} has the wrong type or is missing`;
197
+ }
198
+ return null;
199
+ }
200
+ function captureOk(c) {
201
+ if (!isObj(c))
202
+ return false;
203
+ const s = c.server_applied;
204
+ const d = c.client_declared;
205
+ return ((s === null || (isObj(s) && s.layer === "server" && typeof s.policy === "string")) &&
206
+ (d === null || (isObj(d) && d.layer === "client")));
207
+ }
208
+ function receiptShapeOk(r) {
209
+ if (!isObj(r) || !isObj(r.statement))
210
+ return false;
211
+ const s = r.signature;
212
+ return s === null || (isObj(s) && typeof s.alg === "string" && typeof s.key_id === "string" && typeof s.value === "string");
213
+ }
214
+ /** Declared algorithm versions this verifier implements; anything else is unsupported, never assumed. */
215
+ function versionError(p, version, runId) {
216
+ const chain = p.chain;
217
+ if (chain.algorithm !== "sha256")
218
+ return `chain.algorithm ${String(chain.algorithm)} not supported`;
219
+ if (chain.genesis !== `sha256:${(0, hash_1.genesisHash)(runId)}`)
220
+ return "chain.genesis is not the aer-chain/1 genesis for this run";
221
+ if (version.startsWith("1."))
222
+ return null; // 1.x predates declared versions: implicitly canonical/1 + chain/1
223
+ const v = p.versions;
224
+ if (v.canonicalization !== versions_1.CANONICALIZATION)
225
+ return `canonicalization ${String(v.canonicalization)} not supported`;
226
+ if (v.chain !== versions_1.CHAIN)
227
+ return `chain ${String(v.chain)} not supported`;
228
+ if (v.content_hash !== versions_1.CONTENT_HASH)
229
+ return `content_hash ${String(v.content_hash)} not supported`;
230
+ if (!(v.manifest === null || v.manifest === versions_1.MANIFEST))
231
+ return `manifest ${String(v.manifest)} not supported`;
232
+ if (p.manifest_hash !== null && v.manifest !== versions_1.MANIFEST)
233
+ return "manifest_hash present without a supported manifest version";
234
+ return null;
235
+ }
236
+ /** Every commitment in a receipt statement, checked against the packet; issued_at stays a claim. */
237
+ function receiptMismatch(s, ctx) {
238
+ if (!isObj(s))
239
+ return "receipt has no statement";
240
+ if (s.receipt_version !== versions_1.RECEIPT)
241
+ return `receipt_version ${String(s.receipt_version)} not supported`;
242
+ if (s.manifest_version !== versions_1.MANIFEST)
243
+ return `manifest_version ${String(s.manifest_version)} not supported`;
244
+ const checks = [
245
+ ["run_id", s.run_id === ctx.runId],
246
+ ["organization_id", s.organization_id === ctx.run.organizationId],
247
+ ["project_id", s.project_id === ctx.run.projectId],
248
+ ["manifest_hash", typeof s.manifest_hash === "string" && ctx.manifest !== null && strip(s.manifest_hash) === ctx.manifest],
249
+ ["chain_head", typeof s.chain_head === "string" && ctx.seal !== null && strip(s.chain_head) === ctx.seal],
250
+ ["event_count", s.event_count === ctx.events],
251
+ ["clock", s.clock === "aer-server"],
252
+ ["issued_at", typeof s.issued_at === "string"],
253
+ ];
254
+ for (const [field, ok] of checks)
255
+ if (!ok)
256
+ return `receipt ${field} does not match this packet`;
257
+ return null;
258
+ }
259
+ /**
260
+ * Verify a packet offline. Each check is reported on its own; `overall` is "verified" only when the
261
+ * schema is current, the chain is sealed and intact, the content hash and the manifest reproduce,
262
+ * and nothing that was checked failed. Never throws on bad input — reports it.
263
+ */
264
+ function verifyPacket(input, opts = {}) {
265
+ const trusted = opts.trustedKeys ?? [];
266
+ const report = {
267
+ overall: "failed",
268
+ anchored: false,
269
+ schema: { status: "malformed", version: null },
270
+ versions: { canonicalization: versions_1.CANONICALIZATION, chain: versions_1.CHAIN, manifest: null, content_hash: versions_1.CONTENT_HASH },
271
+ chain: { status: "not_run" },
272
+ content_hash: { status: "not_run" },
273
+ manifest: { status: "not_run" },
274
+ seal: { status: "not_run" },
275
+ signature: { status: "not_run" },
276
+ packet_receipt: { status: "not_run" },
277
+ trusted_receipt: { status: "not_run" },
278
+ trusted_receipt_signature: { status: "not_run" },
279
+ not_integrity_bound: [],
280
+ };
281
+ if (!isObj(input) || !isObj(input.run) || typeof input.run.id !== "string" || !Array.isArray(input.events)) {
282
+ report.schema.detail = "not an AER packet (run.id / events missing)";
283
+ return report;
284
+ }
285
+ const packet = input;
286
+ const version = str(packet.schema_version);
287
+ report.schema.version = version;
288
+ if (!version || !versions_1.SUPPORTED_PACKET_SCHEMAS.includes(version)) {
289
+ // Never "verify" a format this verifier does not know: its hash scopes may differ.
290
+ report.schema = { status: "unsupported", version, detail: `supported: ${versions_1.SUPPORTED_PACKET_SCHEMAS.join(", ")}` };
291
+ report.overall = "unsupported";
292
+ return report;
293
+ }
294
+ const legacySchema = version.startsWith("1.");
295
+ const runId = packet.run.id;
296
+ // Shape first: a wrong type is never coerced into a valid one before hashing/comparison.
297
+ const shape = packetShapeError(packet, version);
298
+ const proto = shape ? null : (0, representable_1.unrepresentableKeyPath)(packet);
299
+ if (shape || proto) {
300
+ report.schema = {
301
+ status: "malformed",
302
+ version,
303
+ detail: shape ?? `${proto}: own "__proto__" keys are not representable in ${versions_1.CANONICALIZATION} (not covered by any hash)`,
304
+ };
305
+ return report;
306
+ }
307
+ // Declared algorithms are reported as declared and checked — never assumed to be this build's.
308
+ if (!legacySchema) {
309
+ const v = packet.versions;
310
+ report.versions = {
311
+ canonicalization: String(v.canonicalization),
312
+ chain: String(v.chain),
313
+ manifest: v.manifest === null ? null : String(v.manifest),
314
+ content_hash: String(v.content_hash),
315
+ };
316
+ }
317
+ else {
318
+ report.versions.content_hash = versions_1.CONTENT_HASH_LEGACY;
319
+ }
320
+ const unsupported = versionError(packet, version, runId);
321
+ if (unsupported) {
322
+ report.schema = { status: "unsupported", version, detail: unsupported };
323
+ report.overall = "unsupported";
324
+ return report;
325
+ }
326
+ report.schema.status = "supported";
327
+ report.not_integrity_bound = NOT_INTEGRITY_BOUND[legacySchema ? "1" : "2"];
328
+ const seal = typeof packet.seal === "string" ? strip(packet.seal) : null;
329
+ // 1. Chain.
330
+ try {
331
+ const v = (0, verify_1.verifyChain)(packetChainLinks(packet), seal, runId);
332
+ report.chain = v.ok
333
+ ? { status: v.status, detail: `${v.checkedCount} events` }
334
+ : v.status === "legacy"
335
+ ? { status: "legacy", detail: v.reason }
336
+ : { status: "broken", detail: v.reason };
337
+ }
338
+ catch {
339
+ report.chain = { status: "broken", detail: "events are not well-formed chain links" };
340
+ }
341
+ report.seal = { status: seal ? "sealed" : "unsealed" };
342
+ // 2. Content hash (whole file consistency — NOT tamper evidence on its own).
343
+ const claimed = isObj(packet.export) ? str(packet.export.content_hash) : null;
344
+ const computed = packetContentHash(packet);
345
+ if (!claimed)
346
+ report.content_hash = { status: "missing" };
347
+ else if (strip(claimed) === computed.hex)
348
+ report.content_hash = { status: "verified" };
349
+ else
350
+ report.content_hash = legacySchema
351
+ ? { status: "not_reproducible", detail: "1.x hashes the writer's key order; storage may have reordered keys" }
352
+ : { status: "mismatch" };
353
+ // 3. Manifest (2.x): rebuilt from the packet, compared with the hash frozen at seal.
354
+ let rebuilt = null;
355
+ if (legacySchema)
356
+ report.manifest = { status: "not_in_schema" };
357
+ else {
358
+ const frozen = str(packet.manifest_hash);
359
+ try {
360
+ rebuilt = seal ? (0, manifest_1.manifestHash)((0, manifest_1.buildManifest)(manifestInputFromPacket(packet))) : null;
361
+ }
362
+ catch {
363
+ rebuilt = null;
364
+ }
365
+ if (!frozen)
366
+ report.manifest = { status: "not_frozen", detail: seal ? "sealed before run manifests existed" : "run not sealed" };
367
+ else if (rebuilt && strip(frozen) === rebuilt)
368
+ report.manifest = { status: "verified" };
369
+ else
370
+ report.manifest = { status: "mismatch", detail: "run context or out-of-chain event fields differ from what was frozen at seal" };
371
+ }
372
+ const receiptCtx = { runId, run: packet.run, seal, manifest: rebuilt, events: packet.events.length };
373
+ // 4. The server's receipt inside the packet: signature only counts against keys the verifier trusts.
374
+ const packetReceipt = isObj(packet.receipt) ? packet.receipt : null;
375
+ if (!packetReceipt) {
376
+ report.packet_receipt = { status: "not_present" };
377
+ report.signature = { status: "not_present" };
378
+ }
379
+ else {
380
+ report.signature = (0, receipt_1.checkReceiptSignature)(packetReceipt, trusted);
381
+ const why = receiptMismatch(packetReceipt.statement, receiptCtx);
382
+ report.packet_receipt = why ? { status: "mismatch", detail: why } : { status: "matches_packet" };
383
+ }
384
+ // 5. A receipt the verifier kept: the anchor that a fully recomputed packet cannot match.
385
+ if (!opts.trustedReceipt) {
386
+ report.trusted_receipt = { status: "not_provided" };
387
+ report.trusted_receipt_signature = { status: "not_provided" };
388
+ }
389
+ else {
390
+ report.trusted_receipt_signature = (0, receipt_1.checkReceiptSignature)(opts.trustedReceipt, trusted);
391
+ const why = rebuilt ? receiptMismatch(opts.trustedReceipt.statement, receiptCtx) : null;
392
+ if (!rebuilt)
393
+ report.trusted_receipt = { status: "cannot_compare", detail: "packet has no reproducible manifest (unsealed or schema 1.x)" };
394
+ else if (why)
395
+ report.trusted_receipt = { status: "mismatch", detail: `packet differs from the run you hold a receipt for: ${why}` };
396
+ else
397
+ report.trusted_receipt = { status: "match" };
398
+ }
399
+ const failed = report.chain.status === "broken" ||
400
+ report.content_hash.status === "mismatch" ||
401
+ report.manifest.status === "mismatch" ||
402
+ report.signature.status === "invalid" ||
403
+ report.packet_receipt.status === "mismatch" ||
404
+ report.trusted_receipt.status === "mismatch" ||
405
+ report.trusted_receipt.status === "cannot_compare" ||
406
+ report.trusted_receipt_signature.status === "invalid";
407
+ report.anchored =
408
+ !failed && (report.signature.status === "valid_trusted" || report.trusted_receipt.status === "match");
409
+ report.overall = failed
410
+ ? "failed"
411
+ : report.chain.status === "legacy"
412
+ ? "legacy_unchained"
413
+ : report.chain.status === "consistent_unsealed"
414
+ ? "consistent_unsealed"
415
+ : legacySchema || report.manifest.status !== "verified" || report.content_hash.status !== "verified"
416
+ ? "verified_partial"
417
+ : "verified";
418
+ return report;
419
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.signReceipt = signReceipt;
4
+ exports.publicKeyOf = publicKeyOf;
5
+ exports.checkReceiptSignature = checkReceiptSignature;
6
+ const node_crypto_1 = require("node:crypto");
7
+ const canonicalize_1 = require("./canonicalize");
8
+ function toPublicKey(material) {
9
+ return material.includes("BEGIN")
10
+ ? (0, node_crypto_1.createPublicKey)(material)
11
+ : (0, node_crypto_1.createPublicKey)({ key: Buffer.from(material, "base64"), format: "der", type: "spki" });
12
+ }
13
+ function toPrivateKey(material) {
14
+ return material.includes("BEGIN")
15
+ ? (0, node_crypto_1.createPrivateKey)(material)
16
+ : (0, node_crypto_1.createPrivateKey)({ key: Buffer.from(material, "base64"), format: "der", type: "pkcs8" });
17
+ }
18
+ const statementBytes = (s) => Buffer.from((0, canonicalize_1.canonicalize)(s), "utf8");
19
+ /** Sign with an Ed25519 private key (base64 PKCS8 DER or PEM). The key never leaves this call. */
20
+ function signReceipt(statement, privateKey, keyId) {
21
+ const value = (0, node_crypto_1.sign)(null, statementBytes(statement), toPrivateKey(privateKey)).toString("base64");
22
+ return { statement, signature: { alg: "Ed25519", key_id: keyId, value } };
23
+ }
24
+ /** base64 SPKI DER public key for a private key — what the server publishes for verifiers. */
25
+ function publicKeyOf(privateKey) {
26
+ return (0, node_crypto_1.createPublicKey)(toPrivateKey(privateKey)).export({ format: "der", type: "spki" }).toString("base64");
27
+ }
28
+ /** Check a receipt's signature against keys the caller trusts. A key carried in the packet is never used. */
29
+ function checkReceiptSignature(receipt, trusted) {
30
+ const sig = receipt.signature;
31
+ if (!sig)
32
+ return { status: "not_present" };
33
+ if (sig.alg !== "Ed25519")
34
+ return { status: "invalid", key_id: sig.key_id, reason: `unsupported alg ${sig.alg}` };
35
+ const key = trusted.find((k) => k.key_id === sig.key_id);
36
+ if (!key)
37
+ return { status: "untrusted_key", key_id: sig.key_id };
38
+ try {
39
+ const ok = (0, node_crypto_1.verify)(null, statementBytes(receipt.statement), toPublicKey(key.public_key), Buffer.from(sig.value, "base64"));
40
+ return ok ? { status: "valid_trusted", key_id: sig.key_id } : { status: "invalid", key_id: sig.key_id, reason: "signature does not match" };
41
+ }
42
+ catch {
43
+ return { status: "invalid", key_id: sig.key_id, reason: "unreadable key or signature" };
44
+ }
45
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.unrepresentableKeyPath = unrepresentableKeyPath;
4
+ /**
5
+ * aer-canonical/1 rebuilds objects by assignment (`out[k] = …`). An OWN "__proto__" key — which
6
+ * JSON.parse creates as plain data — is therefore re-routed to the prototype and silently left out
7
+ * of the hashed bytes: anything under it is not covered by any hash. The algorithm is frozen (old
8
+ * records were hashed that way), so such content is refused instead: at the API boundary (never
9
+ * written) and in the verifier (a packet carrying one is malformed, never "verified").
10
+ * Returns the JSON path of the first such key, or null.
11
+ */
12
+ function unrepresentableKeyPath(value, path = "$") {
13
+ if (Array.isArray(value)) {
14
+ for (let i = 0; i < value.length; i++) {
15
+ const p = unrepresentableKeyPath(value[i], `${path}[${i}]`);
16
+ if (p)
17
+ return p;
18
+ }
19
+ return null;
20
+ }
21
+ if (value !== null && typeof value === "object") {
22
+ if (Object.prototype.hasOwnProperty.call(value, "__proto__"))
23
+ return `${path}.__proto__`;
24
+ for (const k of Object.keys(value)) {
25
+ const p = unrepresentableKeyPath(value[k], `${path}.${k}`);
26
+ if (p)
27
+ return p;
28
+ }
29
+ }
30
+ return null;
31
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ // Shared shapes. Framework-agnostic — no Prisma, no app types leak in here.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyChain = verifyChain;
4
+ const hash_1 = require("./hash");
5
+ /**
6
+ * The single shared verifier. Pure: input is ONLY the links + seal (+ runId for genesis).
7
+ * No DB, no app context, no I/O. Consumers map their source (packet JSON / DB rows) into
8
+ * ChainLink[] and delegate here — never reimplement chain logic.
9
+ */
10
+ function verifyChain(links, seal, runId) {
11
+ const ordered = [...links].sort((a, b) => a.seq - b.seq);
12
+ // A run created before the chain feature carries no hashes — distinct from tampering.
13
+ if (ordered.length > 0 && ordered.every((l) => !l.hash && !l.prevHash)) {
14
+ return { ok: false, status: "legacy", reason: "no chain present (pre-chain run)" };
15
+ }
16
+ let prev = (0, hash_1.genesisHash)(runId);
17
+ let checked = 0;
18
+ for (const link of ordered) {
19
+ if (link.prevHash !== prev) {
20
+ return {
21
+ ok: false,
22
+ status: "broken",
23
+ brokenSeq: link.seq,
24
+ reason: `prevHash mismatch at seq ${link.seq}`,
25
+ checkedCount: checked,
26
+ };
27
+ }
28
+ if (link.hash !== (0, hash_1.hashEvent)(prev, link)) {
29
+ return {
30
+ ok: false,
31
+ status: "broken",
32
+ brokenSeq: link.seq,
33
+ reason: `content hash mismatch at seq ${link.seq}`,
34
+ checkedCount: checked,
35
+ };
36
+ }
37
+ prev = link.hash;
38
+ checked++;
39
+ }
40
+ // seal is null while a run is still running: nothing to compare the head against yet.
41
+ if (seal === null)
42
+ return { ok: true, status: "consistent_unsealed", checkedCount: checked };
43
+ if (seal !== prev) {
44
+ const lastSeq = ordered.length ? ordered[ordered.length - 1].seq : 0;
45
+ return {
46
+ ok: false,
47
+ status: "broken",
48
+ brokenSeq: lastSeq,
49
+ reason: "seal does not match chain head",
50
+ checkedCount: checked,
51
+ };
52
+ }
53
+ return { ok: true, status: "verified", checkedCount: checked };
54
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ // Named, frozen algorithm versions. A version string never changes meaning: any change to what is
3
+ // hashed, or how, gets a NEW identifier and the old one keeps verifying historical records.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.SUPPORTED_PACKET_SCHEMAS = exports.CONTENT_HASH_LEGACY = exports.CONTENT_HASH = exports.RECEIPT = exports.MANIFEST = exports.CHAIN = exports.CANONICALIZATION = void 0;
6
+ /** canonicalize(): sorted object keys, undefined → null, JSON.stringify for leaves (see canonicalize.ts). */
7
+ exports.CANONICALIZATION = "aer-canonical/1";
8
+ /** hashEvent()/genesisHash(): sha256(prevHash + "\n" + canonical(coreView)), genesis "AER-CHAIN-v1:" + runId. */
9
+ exports.CHAIN = "aer-chain/1";
10
+ /** buildManifest(): run context + per-event out-of-chain fields, frozen at seal. */
11
+ exports.MANIFEST = "aer-manifest/1";
12
+ /** Receipt statement + optional Ed25519 signature over its canonical bytes. */
13
+ exports.RECEIPT = "aer-receipt/1";
14
+ /** Packet content_hash for schema >= 2.0: sha256(canonical(packet without export.content_hash)). */
15
+ exports.CONTENT_HASH = "aer-canonical/1+sha256";
16
+ /**
17
+ * Packet content_hash for schema 1.x: sha256(JSON.stringify(packet)) in the writer's key order.
18
+ * Only reproducible while that order survives (it does not through JSONB storage).
19
+ */
20
+ exports.CONTENT_HASH_LEGACY = "emission-order-json+sha256";
21
+ exports.SUPPORTED_PACKET_SCHEMAS = ["1.1", "1.2", "1.3", "2.0"];
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
- {
2
- "name": "@ktlsr/aer-verify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@ktlsr/aer-verify",
3
+ "version": "0.1.0",
4
+ "description": "Offline verifier for Agent Evidence Recorder audit packets: chain, manifest, content hash, seal, receipt and signature — no network, no AER account.",
5
+ "license": "MIT",
6
+ "author": "ktlesr",
7
+ "type": "commonjs",
8
+ "bin": {
9
+ "aer-verify": "dist/aer-verify/src/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "typecheck": "tsc -p tsconfig.json --noEmit",
22
+ "test": "tsc -p tsconfig.json && node --test test/cli.test.js test/review-stage4.test.js",
23
+ "prepublishOnly": "tsc -p tsconfig.json && node --test test/cli.test.js test/review-stage4.test.js"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/ktlesr/aer.git",
31
+ "directory": "packages/aer-verify"
32
+ },
33
+ "homepage": "https://aer.ktlsr.com",
34
+ "devDependencies": {
35
+ "@types/node": "^20.19.43",
36
+ "typescript": "^5"
37
+ }
38
+ }