prae-cli 0.9.2 → 0.10.1

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
@@ -178,13 +178,16 @@ function unsealPrivateKey(sealed, passphrase) {
178
178
  if (match === null) throw new Error("not a PRAE sealed key (an older format?): make a new one with prae keygen --role");
179
179
  const box = JSON.parse(Buffer.from((match[1] ?? "").replace(/\s/g, ""), "base64").toString("utf8"));
180
180
  if (box.v !== 1 || box.kdf !== "scrypt") throw new Error("unknown sealing");
181
- 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, {
182
182
  N: Number(box.N),
183
183
  r: Number(box.r),
184
184
  p: Number(box.p),
185
185
  maxmem: SCRYPT.maxmem
186
- }), Buffer.from(String(box.iv), "base64"));
187
- 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);
188
191
  return createPrivateKey({
189
192
  key: Buffer.concat([decipher.update(Buffer.from(String(box.data), "base64")), decipher.final()]),
190
193
  format: "der",
@@ -207,14 +210,15 @@ function readEntries(text) {
207
210
  return text.split("\n").filter((line) => line.trim() !== "").map((line) => JSON.parse(line));
208
211
  }
209
212
  /** Re-derive every hash and confirm each entry names its predecessor. */
210
- function verifyEntries(text) {
213
+ function verifyEntries(text, from) {
211
214
  const problems = [];
212
- let expectedPrev = GENESIS;
213
- let expectedSeq = 0;
215
+ let expectedPrev = from?.prev ?? GENESIS;
216
+ let expectedSeq = from?.seq ?? 0;
214
217
  let count = 0;
218
+ const offset = from?.line ?? 0;
215
219
  const lines = text.split("\n").filter((line) => line.trim() !== "");
216
220
  for (const [index, line] of lines.entries()) {
217
- const at = index + 1;
221
+ const at = offset + index + 1;
218
222
  count += 1;
219
223
  let entry;
220
224
  try {
@@ -301,6 +305,16 @@ const STALE_LOCK_MS = 1e4;
301
305
  /** Run `fn` holding an exclusive lock file; synchronous, like the append. */
302
306
  function withLock(lock, fn) {
303
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) {
304
318
  for (;;) {
305
319
  let fd;
306
320
  try {
@@ -313,9 +327,7 @@ function withLock(lock, fn) {
313
327
  continue;
314
328
  }
315
329
  } catch {}
316
- if (Date.now() > deadline) throw new Error(`prae ledger: ${lock} has been held for 30 s; refusing to append without it`);
317
- Atomics.wait(SLEEP, 0, 0, 2);
318
- continue;
330
+ return BUSY;
319
331
  }
320
332
  try {
321
333
  return fn();
@@ -361,19 +373,30 @@ var Ledger = class Ledger {
361
373
  }
362
374
  /** Append one decision and return the entry as written. */
363
375
  append(record) {
364
- return withLock(`${this.path}.lock`, () => {
365
- const { seq, last } = tailOf(this.path);
366
- const sealed = chain(record, seq, last);
367
- const entry = this.signer !== void 0 ? {
368
- ...sealed,
369
- sig: this.signer(sealed)
370
- } : this.signWith === void 0 ? sealed : {
371
- ...sealed,
372
- sig: signEntry(sealed, this.signWith)
373
- };
374
- appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
375
- return entry;
376
- });
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;
377
400
  }
378
401
  /** Every entry, in order. */
379
402
  read() {
@@ -396,4 +419,60 @@ var Ledger = class Ledger {
396
419
  const nap = new Int32Array(new SharedArrayBuffer(4));
397
420
 
398
421
  //#endregion
399
- export { hashEntry as _, signBytes as a, fingerprint as c, signEntry as d, verifyReceipt as f, chain as g, canonical as h, sealedKeypair as i, generateKeypair as l, GENESIS as m, readEntries as n, unsealPrivateKey as o, verifySignature as p, verifyEntries as r, verifyBytes as s, Ledger as t, makeReceipt as u, seal as v };
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,4 +1,4 @@
1
- import { t as evaluate } from "./evaluate-BqLxhYiP.js";
1
+ import { i as evaluate } from "./src-CZYvvEn5.js";
2
2
 
3
3
  //#region src/test-run.ts
4
4
  /** The path argument a scenario's call carries, if any. */