prae-cli 0.9.1 → 0.10.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.
@@ -1,5 +1,5 @@
1
1
  import { appendFileSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
2
+ import { dirname, join, resolve } from "node:path";
3
3
  import { createCipheriv, createDecipheriv, createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, scryptSync, sign, verify } from "node:crypto";
4
4
 
5
5
  //#region ../ledger/src/entry.ts
@@ -116,6 +116,13 @@ const ANSWER_TTL_MS = 5 * 6e4;
116
116
  function signBytes(bytes, privateKey) {
117
117
  return sign(null, Buffer.from(bytes), priv(privateKey)).toString("base64");
118
118
  }
119
+ function verifyBytes(bytes, signature, publicKeyPem) {
120
+ try {
121
+ return verify(null, Buffer.from(bytes), createPublicKey(publicKeyPem), Buffer.from(signature.trim(), "base64"));
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
119
126
  /**
120
127
  * How a private key is sealed at rest: scrypt (N = 2^17, r = 8, p = 1) over
121
128
  * the passphrase, AES-256-GCM over the key. The sealed file can sit where an
@@ -171,13 +178,16 @@ function unsealPrivateKey(sealed, passphrase) {
171
178
  if (match === null) throw new Error("not a PRAE sealed key (an older format?): make a new one with prae keygen --role");
172
179
  const box = JSON.parse(Buffer.from((match[1] ?? "").replace(/\s/g, ""), "base64").toString("utf8"));
173
180
  if (box.v !== 1 || box.kdf !== "scrypt") throw new Error("unknown sealing");
174
- const decipher = createDecipheriv("aes-256-gcm", scryptSync(passphrase, Buffer.from(String(box.salt), "base64"), 32, {
181
+ const wrap = scryptSync(passphrase, Buffer.from(String(box.salt), "base64"), 32, {
175
182
  N: Number(box.N),
176
183
  r: Number(box.r),
177
184
  p: Number(box.p),
178
185
  maxmem: SCRYPT.maxmem
179
- }), Buffer.from(String(box.iv), "base64"));
180
- decipher.setAuthTag(Buffer.from(String(box.tag), "base64"));
186
+ });
187
+ const tag = Buffer.from(String(box.tag), "base64");
188
+ if (tag.length !== 16) throw new Error("sealed key: authentication tag is not 16 bytes");
189
+ const decipher = createDecipheriv("aes-256-gcm", wrap, Buffer.from(String(box.iv), "base64"), { authTagLength: 16 });
190
+ decipher.setAuthTag(tag);
181
191
  return createPrivateKey({
182
192
  key: Buffer.concat([decipher.update(Buffer.from(String(box.data), "base64")), decipher.final()]),
183
193
  format: "der",
@@ -200,14 +210,15 @@ function readEntries(text) {
200
210
  return text.split("\n").filter((line) => line.trim() !== "").map((line) => JSON.parse(line));
201
211
  }
202
212
  /** Re-derive every hash and confirm each entry names its predecessor. */
203
- function verifyEntries(text) {
213
+ function verifyEntries(text, from) {
204
214
  const problems = [];
205
- let expectedPrev = GENESIS;
206
- let expectedSeq = 0;
215
+ let expectedPrev = from?.prev ?? GENESIS;
216
+ let expectedSeq = from?.seq ?? 0;
207
217
  let count = 0;
218
+ const offset = from?.line ?? 0;
208
219
  const lines = text.split("\n").filter((line) => line.trim() !== "");
209
220
  for (const [index, line] of lines.entries()) {
210
- const at = index + 1;
221
+ const at = offset + index + 1;
211
222
  count += 1;
212
223
  let entry;
213
224
  try {
@@ -294,6 +305,16 @@ const STALE_LOCK_MS = 1e4;
294
305
  /** Run `fn` holding an exclusive lock file; synchronous, like the append. */
295
306
  function withLock(lock, fn) {
296
307
  const deadline = Date.now() + 3e4;
308
+ for (;;) {
309
+ const done = tryLock(lock, fn);
310
+ if (done !== BUSY) return done;
311
+ if (Date.now() > deadline) throw new Error(`prae ledger: ${lock} has been held for 30 s; refusing to append without it`);
312
+ Atomics.wait(SLEEP, 0, 0, 2);
313
+ }
314
+ }
315
+ const BUSY = Symbol("busy");
316
+ /** Run `fn` holding the lock if it is free now (or stale); `BUSY` if not. */
317
+ function tryLock(lock, fn) {
297
318
  for (;;) {
298
319
  let fd;
299
320
  try {
@@ -306,9 +327,7 @@ function withLock(lock, fn) {
306
327
  continue;
307
328
  }
308
329
  } catch {}
309
- if (Date.now() > deadline) throw new Error(`prae ledger: ${lock} has been held for 30 s; refusing to append without it`);
310
- Atomics.wait(SLEEP, 0, 0, 2);
311
- continue;
330
+ return BUSY;
312
331
  }
313
332
  try {
314
333
  return fn();
@@ -354,19 +373,30 @@ var Ledger = class Ledger {
354
373
  }
355
374
  /** Append one decision and return the entry as written. */
356
375
  append(record) {
357
- return withLock(`${this.path}.lock`, () => {
358
- const { seq, last } = tailOf(this.path);
359
- const sealed = chain(record, seq, last);
360
- const entry = this.signer !== void 0 ? {
361
- ...sealed,
362
- sig: this.signer(sealed)
363
- } : this.signWith === void 0 ? sealed : {
364
- ...sealed,
365
- sig: signEntry(sealed, this.signWith)
366
- };
367
- appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
368
- return entry;
369
- });
376
+ return withLock(`${this.path}.lock`, () => this.write(record));
377
+ }
378
+ /**
379
+ * Append only if no other writer holds the lock right now; `undefined`
380
+ * when one does. For a process that must not block while it waits —
381
+ * one that answers other writers' signers (PRA-143), whose writers hold
382
+ * the lock until it answers.
383
+ */
384
+ tryAppend(record) {
385
+ const done = tryLock(`${this.path}.lock`, () => this.write(record));
386
+ return done === BUSY ? void 0 : done;
387
+ }
388
+ write(record) {
389
+ const { seq, last } = tailOf(this.path);
390
+ const sealed = chain(record, seq, last);
391
+ const entry = this.signer !== void 0 ? {
392
+ ...sealed,
393
+ sig: this.signer(sealed)
394
+ } : this.signWith === void 0 ? sealed : {
395
+ ...sealed,
396
+ sig: signEntry(sealed, this.signWith)
397
+ };
398
+ appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
399
+ return entry;
370
400
  }
371
401
  /** Every entry, in order. */
372
402
  read() {
@@ -389,4 +419,60 @@ var Ledger = class Ledger {
389
419
  const nap = new Int32Array(new SharedArrayBuffer(4));
390
420
 
391
421
  //#endregion
392
- export { seal as _, signBytes as a, generateKeypair as c, verifyReceipt as d, verifySignature as f, hashEntry as g, chain as h, sealedKeypair as i, makeReceipt as l, canonical as m, readEntries as n, unsealPrivateKey as o, GENESIS as p, verifyEntries as r, fingerprint as s, Ledger as t, signEntry as u };
422
+ //#region ../ledger/src/verify-cache.ts
423
+ const prefixes = /* @__PURE__ */ new Map();
424
+ const sha = (s) => createHash("sha256").update(s).digest("hex");
425
+ function verifyGrowing(path, publicKey) {
426
+ const raw = readFileSync(path, "utf8");
427
+ const text = raw.slice(0, raw.lastIndexOf("\n") + 1);
428
+ const slot = `${path}\u0000${publicKey ?? ""}`;
429
+ const known = prefixes.get(slot);
430
+ const from = known !== void 0 && known.key === publicKey && text.length >= known.length && sha(text.slice(0, known.length)) === known.sha ? known : void 0;
431
+ const fresh = from === void 0 ? text : text.slice(from.length);
432
+ const verified = verifyEntries(fresh, from === void 0 ? void 0 : {
433
+ prev: from.head,
434
+ seq: from.seq,
435
+ line: from.entries
436
+ });
437
+ let checked = from?.checked ?? 0;
438
+ const failed = [...from?.failed ?? []];
439
+ let rows = [];
440
+ try {
441
+ rows = readEntries(fresh);
442
+ } catch {
443
+ rows = [];
444
+ }
445
+ if (publicKey !== void 0) for (const e of rows) {
446
+ if (e.sig === void 0) continue;
447
+ checked += 1;
448
+ if (!verifySignature(e, e.sig, publicKey)) failed.push(e.seq);
449
+ }
450
+ const entries = (from?.entries ?? 0) + verified.entries;
451
+ const problems = [...from?.problems ?? [], ...verified.problems];
452
+ const last = rows.at(-1);
453
+ if (rows.length === verified.entries) prefixes.set(slot, {
454
+ length: text.length,
455
+ sha: sha(text),
456
+ key: publicKey,
457
+ head: last?.hash ?? from?.head ?? GENESIS,
458
+ seq: last === void 0 ? from?.seq ?? 0 : last.seq + 1,
459
+ entries,
460
+ checked,
461
+ failed,
462
+ problems
463
+ });
464
+ else prefixes.delete(slot);
465
+ return {
466
+ ok: problems.length === 0,
467
+ entries,
468
+ problems,
469
+ signatures: publicKey === void 0 ? null : {
470
+ checked,
471
+ failed
472
+ },
473
+ text
474
+ };
475
+ }
476
+
477
+ //#endregion
478
+ export { canonical as _, verifyEntries as a, seal as b, unsealPrivateKey as c, generateKeypair as d, makeReceipt as f, GENESIS as g, verifySignature as h, tailOf as i, verifyBytes as l, verifyReceipt as m, Ledger as n, sealedKeypair as o, signEntry as p, readEntries as r, signBytes as s, verifyGrowing as t, fingerprint as u, chain as v, hashEntry as y };
@@ -1,3 +1,3 @@
1
- import { a as scan, i as looksBinary, n as MAX_SCAN_BYTES, r as entropy, t as DETECTORS } from "./src-B25SHWcz.js";
1
+ import { a as scan, i as looksBinary, n as MAX_SCAN_BYTES, r as entropy, t as DETECTORS } from "./src-BuaOiL2v.js";
2
2
 
3
3
  export { DETECTORS, MAX_SCAN_BYTES, entropy, looksBinary, scan };
@@ -0,0 +1,3 @@
1
+ import { _ as canonical, a as verifyEntries, b as seal, c as unsealPrivateKey, d as generateKeypair, f as makeReceipt, g as GENESIS, h as verifySignature, i as tailOf, l as verifyBytes, m as verifyReceipt, n as Ledger, o as sealedKeypair, p as signEntry, r as readEntries, s as signBytes, t as verifyGrowing, u as fingerprint, v as chain, y as hashEntry } from "./src-Cy-Acrgj.js";
2
+
3
+ export { GENESIS, Ledger, canonical, chain, fingerprint, generateKeypair, hashEntry, makeReceipt, readEntries, seal, sealedKeypair, signBytes, signEntry, tailOf, unsealPrivateKey, verifyBytes, verifyEntries, verifyGrowing, verifyReceipt, verifySignature };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "./evaluate-wMPkjcIC.js";
1
+ import { t as evaluate } from "./evaluate-DhoGQw0Z.js";
2
2
 
3
3
  //#region src/test-run.ts
4
4
  /** The path argument a scenario's call carries, if any. */