prae-cli 0.2.1 → 0.3.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/lib/bin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { t as main } from "./cli-6hDu8-nF.js";
3
- import "./views-BU0sOciR.js";
2
+ import { t as main } from "./cli-DZ0FPdSr.js";
3
+ import "./src-DGWTvmRZ.js";
4
+ import "./views-CLLfJPWB.js";
4
5
 
5
6
  //#region src/bin.ts
6
7
  main(process.argv.slice(2)).catch((error) => {
@@ -1,8 +1,8 @@
1
- import { a as renderPacks, c as renderShell, i as renderOverview, l as VERDICT_LABEL$1, n as renderApprovals, o as esc, r as renderCompliance, s as renderNav, t as navFor } from "./views-BU0sOciR.js";
2
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { p as seal, t as Ledger } from "./src-DGWTvmRZ.js";
2
+ import { a as renderPacks, c as renderShell, i as renderOverview, l as VERDICT_LABEL$1, n as renderApprovals, o as esc, r as renderCompliance, s as renderNav, t as navFor } from "./views-CLLfJPWB.js";
3
+ import { existsSync, readFileSync } from "node:fs";
3
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
4
- import { dirname, extname, join, normalize, resolve, sep } from "node:path";
5
- import { createHash } from "node:crypto";
5
+ import { extname, join, normalize, resolve, sep } from "node:path";
6
6
  import { homedir } from "node:os";
7
7
  import { composePatchFile, preflightOps, revisionOf, takeSnapshot } from "dsh-overlay-check";
8
8
  import { createServer } from "node:http";
@@ -774,165 +774,6 @@ function parsePack(source) {
774
774
  };
775
775
  }
776
776
 
777
- //#endregion
778
- //#region ../ledger/src/entry.ts
779
- /** The hash a chain starts from. */
780
- const GENESIS = "0".repeat(64);
781
- /**
782
- * Deterministic serialization: object keys sorted at every depth.
783
- *
784
- * A hash over `JSON.stringify(entry)` would depend on key insertion order,
785
- * which means a re-serialized entry could hash differently while saying
786
- * exactly the same thing — and a verifier that reports tampering on a
787
- * faithful copy trains its operators to ignore it.
788
- */
789
- function canonical(value) {
790
- if (value === null || typeof value !== "object") return JSON.stringify(value);
791
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
792
- return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
793
- }
794
- /** Hash one entry's content together with its position and predecessor. */
795
- function hashEntry(record, seq, prev) {
796
- return createHash("sha256").update(canonical({
797
- ...record,
798
- seq,
799
- prev
800
- })).digest("hex");
801
- }
802
- /** Chain a record onto a predecessor. */
803
- function chain(record, seq, prev) {
804
- return {
805
- ...record,
806
- seq,
807
- prev,
808
- hash: hashEntry(record, seq, prev)
809
- };
810
- }
811
- /** The short form an auditor quotes from a screenshot. */
812
- function seal(entry) {
813
- return `${entry.hash.slice(0, 4)}…${entry.hash.slice(-4)}`;
814
- }
815
-
816
- //#endregion
817
- //#region ../ledger/src/ledger.ts
818
- /**
819
- * Read a ledger held as text.
820
- *
821
- * Split out from the class because a ledger is not always a file being
822
- * appended to: a build step renders one it imported, and a reader pastes one
823
- * in. Those callers need the same parsing and the same verification the CLI
824
- * runs, and a second implementation of either is how two answers to "does this
825
- * chain hold" come to exist.
826
- */
827
- function readEntries(text) {
828
- return text.split("\n").filter((line) => line.trim() !== "").map((line) => JSON.parse(line));
829
- }
830
- /** Re-derive every hash and confirm each entry names its predecessor. */
831
- function verifyEntries(text) {
832
- const problems = [];
833
- let expectedPrev = GENESIS;
834
- let expectedSeq = 0;
835
- let count = 0;
836
- const lines = text.split("\n").filter((line) => line.trim() !== "");
837
- for (const [index, line] of lines.entries()) {
838
- const at = index + 1;
839
- count += 1;
840
- let entry;
841
- try {
842
- entry = JSON.parse(line);
843
- } catch {
844
- problems.push({
845
- line: at,
846
- message: "not valid JSON"
847
- });
848
- return {
849
- ok: false,
850
- entries: count,
851
- problems
852
- };
853
- }
854
- if (entry.seq !== expectedSeq) problems.push({
855
- line: at,
856
- message: `expected seq ${expectedSeq}, found ${entry.seq}`
857
- });
858
- if (entry.prev !== expectedPrev) problems.push({
859
- line: at,
860
- message: "does not name the previous entry; a row was removed or reordered"
861
- });
862
- const { seq, prev, hash,...record } = entry;
863
- if (hashEntry(record, seq, prev) !== hash) problems.push({
864
- line: at,
865
- message: "contents do not match the sealed hash"
866
- });
867
- expectedPrev = entry.hash;
868
- expectedSeq = entry.seq + 1;
869
- }
870
- return {
871
- ok: problems.length === 0,
872
- entries: count,
873
- problems
874
- };
875
- }
876
- var Ledger = class Ledger {
877
- path;
878
- seq;
879
- last;
880
- constructor(path, seq, last) {
881
- this.path = path;
882
- this.seq = seq;
883
- this.last = last;
884
- }
885
- /**
886
- * Open a ledger, reading the tail to continue an existing chain.
887
- *
888
- * A corrupt or truncated final line is a hard error rather than something to
889
- * skip past: appending onto a chain whose end we could not read would bury
890
- * the damage under valid-looking entries.
891
- */
892
- static open(path) {
893
- mkdirSync(dirname(path), { recursive: true });
894
- let text = "";
895
- try {
896
- text = readFileSync(path, "utf8");
897
- } catch (error) {
898
- if (error.code !== "ENOENT") throw error;
899
- writeFileSync(path, "", { flag: "wx" });
900
- return new Ledger(path, 0, GENESIS);
901
- }
902
- const tail = text.split("\n").filter((line) => line.trim() !== "").at(-1);
903
- if (tail === void 0) return new Ledger(path, 0, GENESIS);
904
- let entry;
905
- try {
906
- entry = JSON.parse(tail);
907
- } catch {
908
- throw new Error(`prae ledger: last line of ${path} is not readable; refusing to append onto a chain that cannot be continued`);
909
- }
910
- return new Ledger(path, entry.seq + 1, entry.hash);
911
- }
912
- /** Append one decision and return the entry as written. */
913
- append(record) {
914
- const entry = chain(record, this.seq, this.last);
915
- appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
916
- this.seq += 1;
917
- this.last = entry.hash;
918
- return entry;
919
- }
920
- /** Every entry, in order. */
921
- read() {
922
- return readEntries(readFileSync(this.path, "utf8"));
923
- }
924
- /**
925
- * Re-derive every hash and confirm each entry names its predecessor.
926
- *
927
- * Catches a deleted row, an edited field, and a reordering. Does not catch a
928
- * wholesale rewrite of the file by someone holding the same code — see the
929
- * note in `entry.ts`.
930
- */
931
- verify() {
932
- return verifyEntries(readFileSync(this.path, "utf8"));
933
- }
934
- };
935
-
936
777
  //#endregion
937
778
  //#region src/console/ledger.ts
938
779
  function ago(iso, now) {
@@ -1381,8 +1222,16 @@ const USAGE = `prae — operator policy for agent harnesses
1381
1222
  prae explain <pack.json> <tool> [key=value ...]
1382
1223
  Judge one call against the pack and print the trace. Nothing is executed.
1383
1224
 
1384
- prae verify <ledger.jsonl>
1385
- Re-derive every hash in a decision log and report any break.
1225
+ prae verify <ledger.jsonl> [--key <public.pem>]
1226
+ Re-derive every hash in a decision log and report any break. With
1227
+ --key, also verify each row's signature against that pinned key.
1228
+
1229
+ prae keygen [--out <dir>]
1230
+ Generate an Ed25519 keypair for signing ledger rows. Prints the
1231
+ fingerprint a counterparty pins.
1232
+
1233
+ prae receipt <ledger.jsonl> <seq> --key <private.pem> [--out <file>]
1234
+ Export one decision as a self-contained signed receipt.
1386
1235
 
1387
1236
  prae report <ledger.jsonl> [--out <file.html>]
1388
1237
  Render the decision record as a printable evidence report.
@@ -1472,7 +1321,52 @@ async function explain(path, tool, pairs) {
1472
1321
  console.log(`\n${pack.pack} ${pack.version} · ${decision.evaluated} rules considered`);
1473
1322
  if (capability === void 0) console.log(`\nnote: no capability claims \`${tool}\`, so capability rules did not apply`);
1474
1323
  }
1475
- async function verify(path) {
1324
+ /**
1325
+ * Keys for signing rows.
1326
+ *
1327
+ * Written 0600 and never printed. What is printed is the fingerprint — the
1328
+ * thing a counterparty pins out-of-band, because a signature checked against
1329
+ * a key that arrived beside it is checked against nothing.
1330
+ */
1331
+ async function keygen(dir) {
1332
+ const { generateKeypair, fingerprint } = await import("./src-Cinsv7Wq.js");
1333
+ const { writeFileSync: writeFileSync$1 } = await import("node:fs");
1334
+ const keys = generateKeypair();
1335
+ const priv = join(dir, "prae-signing.pem");
1336
+ const pub = join(dir, "prae-signing.pub.pem");
1337
+ writeFileSync$1(priv, keys.privateKey, { mode: 384 });
1338
+ writeFileSync$1(pub, keys.publicKey);
1339
+ console.log(`private ${priv} (0600 — this signs; guard it)`);
1340
+ console.log(`public ${pub}`);
1341
+ console.log(`fingerprint ${fingerprint(keys.publicKey)}`);
1342
+ console.log("");
1343
+ console.log("Give the PUBLIC key and fingerprint to whoever will verify");
1344
+ console.log("receipts, through a channel they trust. Point the plugin at the");
1345
+ console.log("private key with `signingKey` to sign every row.");
1346
+ }
1347
+ async function receipt(path, seq, keyPath, out) {
1348
+ const { Ledger: Ledger$1, makeReceipt, fingerprint } = await import("./src-Cinsv7Wq.js");
1349
+ const { readFileSync: readFileSync$1 } = await import("node:fs");
1350
+ const { createPublicKey } = await import("node:crypto");
1351
+ const privateKey = readFileSync$1(keyPath, "utf8");
1352
+ const publicKey = createPublicKey(privateKey).export({
1353
+ type: "spki",
1354
+ format: "pem"
1355
+ }).toString();
1356
+ const entry = Ledger$1.open(path).read().find((row$1) => row$1.seq === seq);
1357
+ if (entry === void 0) return fail(`no row with seq ${seq} in ${path}`);
1358
+ const packaged = makeReceipt(entry, privateKey, publicKey);
1359
+ const text = `${JSON.stringify(packaged, null, 2)}\n`;
1360
+ if (out === void 0) console.log(text);
1361
+ else {
1362
+ await writeFile(out, text);
1363
+ console.log(`${out} · seq ${seq} · ${entry.verdict} ${entry.tool} ${entry.subject ?? ""}`);
1364
+ }
1365
+ console.log(`fingerprint ${fingerprint(publicKey)}`);
1366
+ console.log("The verifier compares that fingerprint against the one they");
1367
+ console.log("pinned — a receipt is only as good as the key they trust.");
1368
+ }
1369
+ async function verify(path, keyPath) {
1476
1370
  let ledger;
1477
1371
  try {
1478
1372
  ledger = Ledger.open(path);
@@ -1481,8 +1375,26 @@ async function verify(path) {
1481
1375
  }
1482
1376
  const result = ledger.verify();
1483
1377
  if (result.ok) {
1484
- const last = ledger.read().at(-1);
1378
+ const entries = ledger.read();
1379
+ const last = entries.at(-1);
1485
1380
  console.log(`${result.entries} entries · chain intact${last === void 0 ? "" : ` · seal ${seal(last)}`}`);
1381
+ if (keyPath !== void 0) {
1382
+ const { verifySignature, fingerprint } = await import("./src-Cinsv7Wq.js");
1383
+ const { readFileSync: readFileSync$1 } = await import("node:fs");
1384
+ const publicKey = readFileSync$1(keyPath, "utf8");
1385
+ let signed = 0;
1386
+ const bad = [];
1387
+ for (const entry of entries) {
1388
+ if (entry.sig === void 0) continue;
1389
+ signed += 1;
1390
+ if (!verifySignature(entry, entry.sig, publicKey)) bad.push(entry.seq);
1391
+ }
1392
+ if (bad.length > 0) {
1393
+ console.error(`signatures: ${bad.length} of ${signed} FAIL against ${fingerprint(publicKey)} — rows ${bad.join(", ")}`);
1394
+ process.exitCode = 1;
1395
+ } else if (signed === 0) console.log("signatures: none present — this ledger is not signed");
1396
+ else console.log(`signatures: ${signed}/${entries.length} rows verify against ${fingerprint(publicKey)}`);
1397
+ }
1486
1398
  console.log("tamper-evident: a removed, edited, or reordered row breaks this chain.");
1487
1399
  console.log("It does not prove against a rewrite of the whole file by someone holding this code.");
1488
1400
  return;
@@ -1543,7 +1455,7 @@ async function report(path, out) {
1543
1455
  * that step is a proposal a human approves, and it starts from this list.
1544
1456
  */
1545
1457
  async function instructions(from, asJson) {
1546
- const { ingest } = await import("./src-BrZXeEez.js");
1458
+ const { ingest } = await import("./src-2pGbYeGU.js");
1547
1459
  const result = ingest(from);
1548
1460
  if (asJson) {
1549
1461
  console.log(JSON.stringify(result, null, 2));
@@ -1579,7 +1491,7 @@ async function instructions(from, asJson) {
1579
1491
  * on purpose rather than something that happened to their policy.
1580
1492
  */
1581
1493
  async function propose(from, options) {
1582
- const [{ ingest }, { extract }] = await Promise.all([import("./src-BrZXeEez.js"), import("./src-Dd2_8m2g.js")]);
1494
+ const [{ ingest }, { extract }] = await Promise.all([import("./src-2pGbYeGU.js"), import("./src-CuHszf3f.js")]);
1583
1495
  const read = ingest(from);
1584
1496
  if (read.files.length === 0) {
1585
1497
  console.log(`No instruction files found at or above ${from}.`);
@@ -1639,9 +1551,9 @@ async function consoleOut(path, argv) {
1639
1551
  const instructionsDir = flag(argv, "instructions");
1640
1552
  if (instructionsDir !== void 0) {
1641
1553
  const [{ ingest }, { extract }, { renderInstructions }] = await Promise.all([
1642
- import("./src-BrZXeEez.js"),
1643
- import("./src-Dd2_8m2g.js"),
1644
- import("./instructions-ijuj4IP1.js")
1554
+ import("./src-2pGbYeGU.js"),
1555
+ import("./src-CuHszf3f.js"),
1556
+ import("./instructions-C6jwClPO.js")
1645
1557
  ]);
1646
1558
  const read = ingest(instructionsDir);
1647
1559
  instructionsView = renderInstructions(data, read, extract(read.statements));
@@ -1740,10 +1652,17 @@ async function main(argv) {
1740
1652
  if (path === void 0 || tool === void 0) fail("explain needs a pack file and a tool name");
1741
1653
  return explain(path, tool, pairs);
1742
1654
  }
1655
+ case "keygen": return keygen(flag(rest, "out") ?? ".");
1656
+ case "receipt": {
1657
+ const [path, seq] = rest;
1658
+ const key = flag(rest, "key");
1659
+ if (path === void 0 || seq === void 0 || key === void 0) fail("receipt needs <ledger.jsonl> <seq> --key <private.pem>");
1660
+ return receipt(path, Number(seq), key, flag(rest, "out"));
1661
+ }
1743
1662
  case "verify": {
1744
1663
  const path = rest[0];
1745
1664
  if (path === void 0) fail("verify needs a ledger file");
1746
- return verify(path);
1665
+ return verify(path, flag(rest, "key"));
1747
1666
  }
1748
1667
  case "report": {
1749
1668
  const path = rest[0];
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-6hDu8-nF.js";
2
- import "./views-BU0sOciR.js";
1
+ import { a as planWrite, i as dshHome, n as applyWrite, o as praeRow, r as defaultProfile, s as profileAt, t as main } from "./cli-DZ0FPdSr.js";
2
+ import "./src-DGWTvmRZ.js";
3
+ import "./views-CLLfJPWB.js";
3
4
 
4
5
  export { applyWrite, defaultProfile, dshHome, main, planWrite, praeRow, profileAt };
@@ -1,4 +1,4 @@
1
- import { c as renderShell, o as esc, s as renderNav, t as navFor } from "./views-BU0sOciR.js";
1
+ import { c as renderShell, o as esc, s as renderNav, t as navFor } from "./views-CLLfJPWB.js";
2
2
 
3
3
  //#region src/console/instructions.ts
4
4
  const STYLE = `
@@ -0,0 +1,3 @@
1
+ import { a as generateKeypair, c as verifySignature, d as chain, f as hashEntry, i as fingerprint, l as GENESIS, n as readEntries, o as makeReceipt, p as seal, r as verifyEntries, s as signEntry, t as Ledger, u as canonical } from "./src-DGWTvmRZ.js";
2
+
3
+ export { GENESIS, Ledger, canonical, chain, fingerprint, generateKeypair, hashEntry, makeReceipt, readEntries, seal, signEntry, verifyEntries, verifySignature };
@@ -0,0 +1,225 @@
1
+ import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
4
+
5
+ //#region ../ledger/src/entry.ts
6
+ /** The hash a chain starts from. */
7
+ const GENESIS = "0".repeat(64);
8
+ /**
9
+ * Deterministic serialization: object keys sorted at every depth.
10
+ *
11
+ * A hash over `JSON.stringify(entry)` would depend on key insertion order,
12
+ * which means a re-serialized entry could hash differently while saying
13
+ * exactly the same thing — and a verifier that reports tampering on a
14
+ * faithful copy trains its operators to ignore it.
15
+ */
16
+ function canonical(value) {
17
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
18
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
19
+ return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
20
+ }
21
+ /** Hash one entry's content together with its position and predecessor. */
22
+ function hashEntry(record, seq, prev) {
23
+ return createHash("sha256").update(canonical({
24
+ ...record,
25
+ seq,
26
+ prev
27
+ })).digest("hex");
28
+ }
29
+ /** Chain a record onto a predecessor. */
30
+ function chain(record, seq, prev) {
31
+ return {
32
+ ...record,
33
+ seq,
34
+ prev,
35
+ hash: hashEntry(record, seq, prev)
36
+ };
37
+ }
38
+ /** The short form an auditor quotes from a screenshot. */
39
+ function seal(entry) {
40
+ return `${entry.hash.slice(0, 4)}…${entry.hash.slice(-4)}`;
41
+ }
42
+
43
+ //#endregion
44
+ //#region ../ledger/src/receipt.ts
45
+ function generateKeypair() {
46
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
47
+ return {
48
+ publicKey: publicKey.export({
49
+ type: "spki",
50
+ format: "pem"
51
+ }).toString(),
52
+ privateKey: privateKey.export({
53
+ type: "pkcs8",
54
+ format: "pem"
55
+ }).toString()
56
+ };
57
+ }
58
+ /** Sign a row's seal. The seal already covers the whole canonical record. */
59
+ function signEntry(entry, privateKeyPem) {
60
+ const key = createPrivateKey(privateKeyPem);
61
+ return sign(null, Buffer.from(entry.hash, "hex"), key).toString("base64");
62
+ }
63
+ function verifySignature(entry, signature, publicKeyPem) {
64
+ try {
65
+ const key = createPublicKey(publicKeyPem);
66
+ return verify(null, Buffer.from(entry.hash, "hex"), key, Buffer.from(signature, "base64"));
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+ function makeReceipt(entry, privateKeyPem, publicKeyPem) {
72
+ return {
73
+ receipt: "prae/1",
74
+ entry,
75
+ signature: signEntry(entry, privateKeyPem),
76
+ publicKey: publicKeyPem
77
+ };
78
+ }
79
+ /**
80
+ * A short identifier for a public key, for pinning by eye.
81
+ *
82
+ * A counterparty checks a receipt against a key they already trust; comparing
83
+ * the whole PEM by hand invites skimming, and eight grouped hex bytes do not.
84
+ */
85
+ function fingerprint(publicKeyPem) {
86
+ const der = createPublicKey(publicKeyPem).export({
87
+ type: "spki",
88
+ format: "der"
89
+ });
90
+ return createHash("sha256").update(der).digest("hex").slice(0, 16).replace(/(.{4})/g, "$1 ").trim();
91
+ }
92
+
93
+ //#endregion
94
+ //#region ../ledger/src/ledger.ts
95
+ /**
96
+ * Read a ledger held as text.
97
+ *
98
+ * Split out from the class because a ledger is not always a file being
99
+ * appended to: a build step renders one it imported, and a reader pastes one
100
+ * in. Those callers need the same parsing and the same verification the CLI
101
+ * runs, and a second implementation of either is how two answers to "does this
102
+ * chain hold" come to exist.
103
+ */
104
+ function readEntries(text) {
105
+ return text.split("\n").filter((line) => line.trim() !== "").map((line) => JSON.parse(line));
106
+ }
107
+ /** Re-derive every hash and confirm each entry names its predecessor. */
108
+ function verifyEntries(text) {
109
+ const problems = [];
110
+ let expectedPrev = GENESIS;
111
+ let expectedSeq = 0;
112
+ let count = 0;
113
+ const lines = text.split("\n").filter((line) => line.trim() !== "");
114
+ for (const [index, line] of lines.entries()) {
115
+ const at = index + 1;
116
+ count += 1;
117
+ let entry;
118
+ try {
119
+ entry = JSON.parse(line);
120
+ } catch {
121
+ problems.push({
122
+ line: at,
123
+ message: "not valid JSON"
124
+ });
125
+ return {
126
+ ok: false,
127
+ entries: count,
128
+ problems
129
+ };
130
+ }
131
+ if (entry.seq !== expectedSeq) problems.push({
132
+ line: at,
133
+ message: `expected seq ${expectedSeq}, found ${entry.seq}`
134
+ });
135
+ if (entry.prev !== expectedPrev) problems.push({
136
+ line: at,
137
+ message: "does not name the previous entry; a row was removed or reordered"
138
+ });
139
+ const { seq, prev, hash, sig: _sig,...record } = entry;
140
+ if (hashEntry(record, seq, prev) !== hash) problems.push({
141
+ line: at,
142
+ message: "contents do not match the sealed hash"
143
+ });
144
+ expectedPrev = entry.hash;
145
+ expectedSeq = entry.seq + 1;
146
+ }
147
+ return {
148
+ ok: problems.length === 0,
149
+ entries: count,
150
+ problems
151
+ };
152
+ }
153
+ var Ledger = class Ledger {
154
+ path;
155
+ seq;
156
+ last;
157
+ constructor(path, seq, last) {
158
+ this.path = path;
159
+ this.seq = seq;
160
+ this.last = last;
161
+ }
162
+ /**
163
+ * Open a ledger, reading the tail to continue an existing chain.
164
+ *
165
+ * A corrupt or truncated final line is a hard error rather than something to
166
+ * skip past: appending onto a chain whose end we could not read would bury
167
+ * the damage under valid-looking entries.
168
+ */
169
+ /** Sign every appended row with this Ed25519 private key (PEM). */
170
+ signWith;
171
+ static open(path, options = {}) {
172
+ const ledger = Ledger.openUnsigned(path);
173
+ ledger.signWith = options.signWith;
174
+ return ledger;
175
+ }
176
+ static openUnsigned(path) {
177
+ mkdirSync(dirname(path), { recursive: true });
178
+ let text = "";
179
+ try {
180
+ text = readFileSync(path, "utf8");
181
+ } catch (error) {
182
+ if (error.code !== "ENOENT") throw error;
183
+ writeFileSync(path, "", { flag: "wx" });
184
+ return new Ledger(path, 0, GENESIS);
185
+ }
186
+ const tail = text.split("\n").filter((line) => line.trim() !== "").at(-1);
187
+ if (tail === void 0) return new Ledger(path, 0, GENESIS);
188
+ let entry;
189
+ try {
190
+ entry = JSON.parse(tail);
191
+ } catch {
192
+ throw new Error(`prae ledger: last line of ${path} is not readable; refusing to append onto a chain that cannot be continued`);
193
+ }
194
+ return new Ledger(path, entry.seq + 1, entry.hash);
195
+ }
196
+ /** Append one decision and return the entry as written. */
197
+ append(record) {
198
+ const sealed = chain(record, this.seq, this.last);
199
+ const entry = this.signWith === void 0 ? sealed : {
200
+ ...sealed,
201
+ sig: signEntry(sealed, this.signWith)
202
+ };
203
+ appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
204
+ this.seq += 1;
205
+ this.last = entry.hash;
206
+ return entry;
207
+ }
208
+ /** Every entry, in order. */
209
+ read() {
210
+ return readEntries(readFileSync(this.path, "utf8"));
211
+ }
212
+ /**
213
+ * Re-derive every hash and confirm each entry names its predecessor.
214
+ *
215
+ * Catches a deleted row, an edited field, and a reordering. Does not catch a
216
+ * wholesale rewrite of the file by someone holding the same code — see the
217
+ * note in `entry.ts`.
218
+ */
219
+ verify() {
220
+ return verifyEntries(readFileSync(this.path, "utf8"));
221
+ }
222
+ };
223
+
224
+ //#endregion
225
+ export { generateKeypair as a, verifySignature as c, chain as d, hashEntry as f, fingerprint as i, GENESIS as l, readEntries as n, makeReceipt as o, seal as p, verifyEntries as r, signEntry as s, Ledger as t, canonical as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prae-cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Operator policy for agent harnesses: validate packs, explain a decision, verify a decision log, and provision a governed DSH profile.",
5
5
  "keywords": [
6
6
  "dsh-plugin",
@@ -42,9 +42,9 @@
42
42
  "@prae/controls": "0.1.0",
43
43
  "@prae/extract": "0.1.0",
44
44
  "@prae/instructions": "0.1.0",
45
+ "@prae/kernel": "0.1.0",
45
46
  "@prae/ledger": "0.1.0",
46
- "@prae/tokens": "0.1.0",
47
- "@prae/kernel": "0.1.0"
47
+ "@prae/tokens": "0.1.0"
48
48
  },
49
49
  "scripts": {
50
50
  "test": "vitest run",
File without changes
File without changes
File without changes