@blamejs/core 0.13.0 → 0.13.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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.13.x
10
10
 
11
+ - v0.13.1 (2026-05-26) — **`b.worm` — write-once-read-many retention.** Store records that cannot be altered or deleted before a retention period elapses — the immutable-storage discipline regulators require (SEC 17a-4(f), CFTC 1.31, FINRA 4511). b.worm.create(opts) returns a WORM store that enforces, on every mutating call, that a record is not overwritten or deleted while it is within its retainUntil window or under a legal hold. Two modes mirror cloud Object-Lock: compliance (the default — no one, including the operator, can delete before expiry) and governance (a privileged caller may override with an audited reason). Retention can only be extended, never shortened; every record carries a SHA3-512 digest that get verifies, so tampering with the underlying bytes is detected on read; every allow/refuse decision is audited. Storage is pluggable via a synchronous store adapter, so the policy layer sits over a sealed DB table, a filesystem, or any non-S3 backend — the store-agnostic, application-level companion to b.objectStore's S3 Object Lock, with content-integrity verification that native Object Lock does not provide. **Added:** *`b.worm.create` — write-once-read-many retention* — Returns a store with `put` / `get` / `delete` / `extendRetention` / `placeLegalHold` / `releaseLegalHold` / `list`. `put` is write-once (an overwrite of a retained or held record is refused); `delete` is gated by the retention window, legal holds, and the mode (`compliance` refuses any early delete; `governance` allows a privileged override with a required, audited reason); `extendRetention` is extend-only; `get` verifies the stored SHA3-512 digest and throws `worm/tampered` on a mismatch. Storage is a pluggable synchronous adapter (`get` / `set` / `delete` / `has` / `keys`), defaulting to in-memory for tests. Use it for SEC 17a-4 / CFTC / FINRA immutable records on backends without native Object Lock; `b.objectStore` remains the path for S3 Object Lock.
12
+
11
13
  - v0.13.0 (2026-05-26) — **`b.crypto.oprf` — RFC 9497 Oblivious PRFs.** Compute F(serverKey, input) without the server learning the input and without the client learning the key — the Oblivious PRF primitive behind password hardening (the server peppers a password it never sees), private set intersection, and Privacy Pass. b.crypto.oprf.suite(name) returns an RFC 9497 ciphersuite — ristretto255-sha512, p256-sha256, p384-sha384, or p521-sha512 — each exposing the base oprf mode and the verifiable voprf mode (a DLEQ proof lets the client confirm the server used the key committed in its public key). The client blinds its input, the server blind-evaluates with its secret key, and the client finalizes by un-blinding and hashing; because un-blinding cancels the blind, the output depends only on key and input. Validated byte-for-byte against the RFC 9497 Appendix-A test vectors. Group and hash-to-curve operations come from the newly vendored @noble/curves (Paul Miller, MIT) — the same maintainer as the framework's existing vendored @noble/post-quantum and @noble/ciphers, with no added npm runtime dependency. **Added:** *`b.crypto.oprf` — RFC 9497 OPRF / VOPRF* — `suite(name)` returns `{ name, oprf, voprf }` for one of the four RFC 9497 ciphersuites (ristretto255-SHA512 / P-256-SHA256 / P-384-SHA384 / P-521-SHA512). The `oprf` (base) mode provides `deriveKeyPair` / `generateKeyPair` / `blind` / `blindEvaluate` / `finalize` / `evaluate`; `voprf` (verifiable) adds a DLEQ proof so the client can prove the server used the committed key. Use it for password hardening, private set intersection, and OPRF-based tokens. Verified against the RFC 9497 Appendix-A vectors. The partially-oblivious `poprf` mode is not yet exposed (the vendored `@noble/curves` does not implement it) and will follow upstream. · *Vendored `@noble/curves`* — `@noble/curves` 2.2.0 (Paul Miller, MIT) is vendored under `lib/vendor/` (no npm runtime dependency), supplying the ristretto255 / NIST-curve group and hash-to-curve operations behind `b.crypto.oprf`. It joins the existing vendored `@noble/post-quantum` and `@noble/ciphers` from the same maintainer; tracked in the SBOM and the vendor-currency gate.
12
14
 
13
15
  ## v0.12.x
package/README.md CHANGED
@@ -89,6 +89,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
89
89
  - **Workflow gates** — break-glass column gates with second-factor + audit (`b.breakGlass`); two-person-rule m-of-n approval with cooling-off lock + cancellation (`b.dualControl`)
90
90
  - **Financial / Open Banking** — FAPI 2.0 Final composite posture (PAR + PKCE-S256 + DPoP-or-mTLS + RFC 9207); runtime enforcement helpers `b.fapi2.assertCallback` (refuses missing iss + bare-param under message-signing) and `b.fapi2.assertAuthzRequest` (refuses non-JAR); CFPB §1033 / FDX 6.0 consumer-financial-data-sharing wrapper (`b.fdx`)
91
91
  - **Data-subject coordination** — cross-table export / rectify / erase / restrict / objection (`b.subject`, `b.subject.eraseHard`); subject-level legal-hold registry consulted by erase + retention paths (FRCP Rule 26/37(e), GDPR Art 17(3)(e), SEC Rule 17a-4, HIPAA §164.530(j)(2)) (`b.legalHold`)
92
+ - **WORM retention** — write-once-read-many records over any backing store (`b.worm.create`): `compliance` / `governance` Object-Lock modes, extend-only `retainUntil`, legal holds, and a tamper-evident SHA3-512 digest verified on read — the store-agnostic application-level companion to `b.objectStore`'s S3 Object Lock, for sealed-DB / filesystem / non-S3 backends (SEC 17a-4(f), CFTC 1.31, FINRA 4511)
92
93
  - **Account safety** — adaptive bot-challenge staircase (`b.authBotChallenge`); session-to-device-posture binding with fail-closed verify (`b.sessionDeviceBinding`)
93
94
  - **Anonymous authorization** — Privacy Pass origin side (RFC 9577/9578 — `b.privacyPass`): issue a `WWW-Authenticate: PrivateToken` challenge and verify a presented Blind-RSA (type 0x0002) token against the issuer public key, with no issuer callback and no client identity
94
95
  - **Oblivious PRF** — RFC 9497 OPRF / VOPRF (`b.crypto.oprf.suite`): learn `F(serverKey, input)` without the server seeing the input — the primitive behind password hardening (pepper a password the server never sees), private set intersection, and Privacy Pass; `oprf` (base) + `voprf` (verifiable, DLEQ-proof) modes over ristretto255-SHA512 / P-256 / P-384 / P-521; validated against the RFC 9497 Appendix-A vectors
package/index.js CHANGED
@@ -372,6 +372,7 @@ var fileUpload = require("./lib/file-upload");
372
372
  var dualControl = require("./lib/dual-control");
373
373
  var retention = require("./lib/retention");
374
374
  var legalHold = require("./lib/legal-hold");
375
+ var worm = require("./lib/worm");
375
376
  var network = require("./lib/network");
376
377
  var cloudEvents = require("./lib/cloud-events");
377
378
  var dsr = require("./lib/dsr");
@@ -710,6 +711,7 @@ module.exports = {
710
711
  dualControl: dualControl,
711
712
  retention: retention,
712
713
  legalHold: legalHold,
714
+ worm: worm,
713
715
  network: network,
714
716
  cloudEvents: cloudEvents,
715
717
  dsr: dsr,
package/lib/worm.js ADDED
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.worm
4
+ * @nav Compliance
5
+ * @title WORM Retention
6
+ *
7
+ * @intro
8
+ * Write-once-read-many records with retention-until immutability — the
9
+ * storage discipline regulators require for records that must not be
10
+ * altered or deleted before a retention period elapses (SEC 17a-4(f),
11
+ * CFTC 1.31, FINRA 4511, and the "immutable storage" controls in many
12
+ * sectoral postures). A WORM store enforces, on every mutating call, that
13
+ * a stored record cannot be overwritten or deleted while it is within its
14
+ * retention window or under a legal hold.
15
+ *
16
+ * Two modes mirror the cloud Object-Lock model: in <code>compliance</code>
17
+ * mode (the default) a record cannot be deleted before its
18
+ * <code>retainUntil</code> time by anyone, including the operator; in
19
+ * <code>governance</code> mode a privileged caller may override with an
20
+ * explicit reason, which is audited. Retention can only be
21
+ * <em>extended</em>, never shortened. Every record carries a SHA3-512
22
+ * content digest, so <code>get</code> detects tampering of the underlying
23
+ * bytes. Every allow/refuse decision is audited.
24
+ *
25
+ * Storage is pluggable: <code>create</code> takes a synchronous
26
+ * <code>store</code> adapter (<code>get</code> / <code>set</code> /
27
+ * <code>delete</code> / <code>has</code> / <code>keys</code>) so the WORM
28
+ * policy layer sits over an operator's durable backend (a sealed DB
29
+ * table, an S3 Object-Lock bucket, a filesystem); the default in-memory
30
+ * adapter is for tests and ephemeral use.
31
+ *
32
+ * @card
33
+ * Write-once-read-many retention (`b.worm.create`) — SEC 17a-4 / CFTC-style
34
+ * immutable records with compliance / governance Object-Lock modes,
35
+ * extend-only retention, legal holds, and a tamper-evident SHA3-512 digest
36
+ * over any pluggable store.
37
+ */
38
+
39
+ var nodeCrypto = require("node:crypto");
40
+ var lazyRequire = require("./lazy-require");
41
+ var validateOpts = require("./validate-opts");
42
+ var { timingSafeEqual } = require("./crypto");
43
+ var { defineClass } = require("./framework-error");
44
+ var audit = lazyRequire(function () { return require("./audit"); });
45
+
46
+ var WormError = defineClass("WormError", { alwaysPermanent: true });
47
+
48
+ var MODES = { compliance: 1, governance: 1 };
49
+
50
+ function _now(clock) { return typeof clock === "function" ? clock() : Date.now(); }
51
+ function _toBytes(data) {
52
+ // Always return a fresh copy — the WORM record owns its bytes. If we kept
53
+ // the caller's Buffer, a later mutation of their reference would silently
54
+ // change stored bytes and break the digest, defeating immutability.
55
+ if (Buffer.isBuffer(data)) return Buffer.from(data);
56
+ if (data instanceof Uint8Array) return Buffer.from(data);
57
+ if (typeof data === "string") return Buffer.from(data, "utf8");
58
+ return Buffer.from(JSON.stringify(data), "utf8"); // structured value → canonical-ish JSON
59
+ }
60
+ function _digest(bytes) { return nodeCrypto.createHash("sha3-512").update(bytes).digest(); }
61
+
62
+ // Default in-memory store adapter (tests / ephemeral). Operators pass a
63
+ // durable adapter with the same five synchronous methods.
64
+ function _memStore() {
65
+ var m = new Map();
66
+ return {
67
+ get: function (id) { return m.get(id); },
68
+ set: function (id, rec) { m.set(id, rec); },
69
+ delete: function (id) { m.delete(id); },
70
+ has: function (id) { return m.has(id); },
71
+ keys: function () { return Array.from(m.keys()); },
72
+ };
73
+ }
74
+
75
+ /**
76
+ * @primitive b.worm.create
77
+ * @signature b.worm.create(opts?)
78
+ * @since 0.13.0
79
+ * @status stable
80
+ * @compliance sox-404, soc2
81
+ * @related b.retention.create, b.legalHold, b.retention.complianceFloor
82
+ *
83
+ * Create a WORM store that enforces write-once-read-many retention over a
84
+ * backing store. The returned instance has <code>put</code>,
85
+ * <code>get</code>, <code>delete</code>, <code>extendRetention</code>,
86
+ * <code>placeLegalHold</code>, <code>releaseLegalHold</code>, and
87
+ * <code>list</code>. Throws <code>WormError</code> on policy violations.
88
+ *
89
+ * @opts
90
+ * store: object, // adapter: get/set/delete/has/keys (default: in-memory)
91
+ * mode: string, // "compliance" (default) | "governance"
92
+ * defaultRetentionMs: number, // applied when put() gives no retain time
93
+ * clock: function, // () => epoch ms (default Date.now; for tests)
94
+ *
95
+ * @example
96
+ * var w = b.worm.create({ mode: "compliance" });
97
+ * w.put("invoice-42", pdfBytes, { retentionMs: b.C.TIME.days(2555) }); // 7y
98
+ * w.get("invoice-42").data; // → pdfBytes (digest verified)
99
+ * w.delete("invoice-42"); // throws worm/retained until 2033
100
+ */
101
+ function create(opts) {
102
+ opts = opts || {};
103
+ validateOpts(opts, ["store", "mode", "defaultRetentionMs", "clock"], "worm.create");
104
+ var mode = opts.mode || "compliance";
105
+ if (!MODES[mode]) throw new WormError("worm/bad-mode", "worm.create: mode must be 'compliance' or 'governance'");
106
+ var store = opts.store || _memStore();
107
+ ["get", "set", "delete", "has", "keys"].forEach(function (m) {
108
+ if (typeof store[m] !== "function") throw new WormError("worm/bad-store", "worm.create: store adapter must implement " + m + "()");
109
+ });
110
+ var clock = opts.clock;
111
+ var defaultRetentionMs = opts.defaultRetentionMs;
112
+ if (defaultRetentionMs != null && (typeof defaultRetentionMs !== "number" || !isFinite(defaultRetentionMs) || defaultRetentionMs < 0)) {
113
+ throw new WormError("worm/bad-opt", "worm.create: defaultRetentionMs must be a non-negative finite number");
114
+ }
115
+
116
+ function _emit(action, outcome, id, metadata) {
117
+ try {
118
+ audit().safeEmit({ action: "worm." + action, actor: { type: "system" }, outcome: outcome, metadata: Object.assign({ id: id, mode: mode }, metadata || {}) });
119
+ } catch (_e) { /* audit is drop-silent by design */ }
120
+ }
121
+
122
+ function _resolveRetainUntil(now, putOpts) {
123
+ if (putOpts.retainUntil != null) {
124
+ if (typeof putOpts.retainUntil !== "number" || !isFinite(putOpts.retainUntil)) throw new WormError("worm/bad-opt", "worm.put: retainUntil must be an epoch-ms number");
125
+ return putOpts.retainUntil;
126
+ }
127
+ var ms = putOpts.retentionMs != null ? putOpts.retentionMs : defaultRetentionMs;
128
+ if (ms == null) throw new WormError("worm/no-retention", "worm.put: a retention is required — pass retainUntil, retentionMs, or set defaultRetentionMs at create()");
129
+ if (typeof ms !== "number" || !isFinite(ms) || ms < 0) throw new WormError("worm/bad-opt", "worm.put: retentionMs must be a non-negative finite number");
130
+ return now + ms;
131
+ }
132
+
133
+ function put(id, data, putOpts) {
134
+ putOpts = putOpts || {};
135
+ if (typeof id !== "string" || id.length === 0) throw new WormError("worm/bad-id", "worm.put: id must be a non-empty string");
136
+ var now = _now(clock);
137
+ var existing = store.get(id);
138
+ if (existing) {
139
+ // Write-once: an existing record may not be overwritten while it is
140
+ // still retained or held — that is the whole guarantee.
141
+ if (existing.legalHolds.length > 0 || now < existing.retainUntil) {
142
+ _emit("put-refused", "denied", id, { reason: "write-once" });
143
+ throw new WormError("worm/already-exists", "worm.put: '" + id + "' exists and is still retained/held — WORM records are write-once");
144
+ }
145
+ }
146
+ var bytes = _toBytes(data);
147
+ var retainUntil = _resolveRetainUntil(now, putOpts);
148
+ var holds = [];
149
+ if (putOpts.legalHold != null) holds.push(String(putOpts.legalHold));
150
+ var rec = {
151
+ bytes: bytes,
152
+ digest: _digest(bytes),
153
+ createdAt: now,
154
+ retainUntil: retainUntil,
155
+ legalHolds: holds,
156
+ mode: mode,
157
+ };
158
+ store.set(id, rec);
159
+ _emit("put", "allowed", id, { retainUntil: retainUntil, bytes: bytes.length });
160
+ return { id: id, digest: rec.digest.toString("hex"), createdAt: now, retainUntil: retainUntil };
161
+ }
162
+
163
+ function _require(id) {
164
+ var rec = store.get(id);
165
+ if (!rec) throw new WormError("worm/not-found", "worm: no record '" + id + "'");
166
+ return rec;
167
+ }
168
+
169
+ function get(id) {
170
+ var rec = _require(id);
171
+ // Tamper-evidence: the stored digest must still match the stored bytes.
172
+ if (!timingSafeEqual(rec.digest, _digest(rec.bytes))) {
173
+ _emit("tamper-detected", "denied", id, {});
174
+ throw new WormError("worm/tampered", "worm.get: stored bytes for '" + id + "' do not match their digest");
175
+ }
176
+ // Hand back a copy so a consumer cannot mutate the stored bytes through
177
+ // the read API and corrupt the record behind the policy checks.
178
+ return { id: id, data: Buffer.from(rec.bytes), digest: rec.digest.toString("hex"), createdAt: rec.createdAt, retainUntil: rec.retainUntil, legalHolds: rec.legalHolds.slice(), mode: rec.mode };
179
+ }
180
+
181
+ function del(id, delOpts) {
182
+ delOpts = delOpts || {};
183
+ var rec = _require(id);
184
+ var now = _now(clock);
185
+ if (rec.legalHolds.length > 0) {
186
+ _emit("delete-refused", "denied", id, { reason: "legal-hold", holds: rec.legalHolds.length });
187
+ throw new WormError("worm/legal-hold", "worm.delete: '" + id + "' is under " + rec.legalHolds.length + " legal hold(s)");
188
+ }
189
+ if (now < rec.retainUntil) {
190
+ if (mode === "governance" && delOpts.override === true) {
191
+ if (typeof delOpts.reason !== "string" || delOpts.reason.length === 0) throw new WormError("worm/override-reason", "worm.delete: a governance override requires a non-empty reason");
192
+ store.delete(id);
193
+ _emit("delete-override", "allowed", id, { retainUntil: rec.retainUntil, reason: delOpts.reason });
194
+ return true;
195
+ }
196
+ _emit("delete-refused", "denied", id, { reason: "retained", retainUntil: rec.retainUntil });
197
+ throw new WormError("worm/retained", "worm.delete: '" + id + "' is retained until " + new Date(rec.retainUntil).toISOString() + (mode === "compliance" ? " (compliance mode — no override)" : " (pass { override: true, reason } in governance mode)"));
198
+ }
199
+ store.delete(id);
200
+ _emit("delete", "allowed", id, {});
201
+ return true;
202
+ }
203
+
204
+ function extendRetention(id, newRetainUntil) {
205
+ var rec = _require(id);
206
+ if (typeof newRetainUntil !== "number" || !isFinite(newRetainUntil)) throw new WormError("worm/bad-opt", "worm.extendRetention: newRetainUntil must be an epoch-ms number");
207
+ if (newRetainUntil < rec.retainUntil) {
208
+ _emit("extend-refused", "denied", id, { current: rec.retainUntil, requested: newRetainUntil });
209
+ throw new WormError("worm/retention-shorten", "worm.extendRetention: retention can only be extended, never shortened");
210
+ }
211
+ rec.retainUntil = newRetainUntil;
212
+ store.set(id, rec);
213
+ _emit("extend", "allowed", id, { retainUntil: newRetainUntil });
214
+ return newRetainUntil;
215
+ }
216
+
217
+ function placeLegalHold(id, holdId) {
218
+ var rec = _require(id);
219
+ if (typeof holdId !== "string" || holdId.length === 0) throw new WormError("worm/bad-opt", "worm.placeLegalHold: holdId must be a non-empty string");
220
+ if (rec.legalHolds.indexOf(holdId) === -1) { rec.legalHolds.push(holdId); store.set(id, rec); }
221
+ _emit("legal-hold-placed", "allowed", id, { holdId: holdId });
222
+ return rec.legalHolds.slice();
223
+ }
224
+
225
+ function releaseLegalHold(id, holdId) {
226
+ var rec = _require(id);
227
+ var i = rec.legalHolds.indexOf(String(holdId));
228
+ if (i !== -1) { rec.legalHolds.splice(i, 1); store.set(id, rec); }
229
+ _emit("legal-hold-released", "allowed", id, { holdId: holdId });
230
+ return rec.legalHolds.slice();
231
+ }
232
+
233
+ function list() { return store.keys(); }
234
+
235
+ return {
236
+ put: put, get: get, delete: del, extendRetention: extendRetention,
237
+ placeLegalHold: placeLegalHold, releaseLegalHold: releaseLegalHold,
238
+ list: list, mode: mode,
239
+ };
240
+ }
241
+
242
+ module.exports = {
243
+ create: create,
244
+ MODES: Object.keys(MODES),
245
+ WormError: WormError,
246
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:d8196be4-b27f-4e74-86fc-32557c5f1ac1",
5
+ "serialNumber": "urn:uuid:867201c0-c1e7-42ae-8956-c09389ae0095",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-26T20:49:46.612Z",
8
+ "timestamp": "2026-05-26T22:27:34.169Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.13.0",
22
+ "bom-ref": "@blamejs/core@0.13.1",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.13.0",
25
+ "version": "0.13.1",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.13.0",
29
+ "purl": "pkg:npm/%40blamejs/core@0.13.1",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.13.0",
57
+ "ref": "@blamejs/core@0.13.1",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]