@blamejs/blamejs-shop 0.3.11 → 0.3.13

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.
Files changed (94) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/lib/admin.js +157 -117
  3. package/lib/asset-manifest.json +1 -1
  4. package/lib/currency-rounding.js +2 -14
  5. package/lib/index.js +1 -0
  6. package/lib/text-guard.js +227 -0
  7. package/lib/vendor/MANIFEST.json +2 -2
  8. package/lib/vendor/blamejs/CHANGELOG.md +2 -0
  9. package/lib/vendor/blamejs/README.md +3 -2
  10. package/lib/vendor/blamejs/SECURITY.md +3 -0
  11. package/lib/vendor/blamejs/api-snapshot.json +14 -2
  12. package/lib/vendor/blamejs/lib/agent-event-bus.js +4 -4
  13. package/lib/vendor/blamejs/lib/agent-idempotency.js +6 -6
  14. package/lib/vendor/blamejs/lib/agent-orchestrator.js +9 -9
  15. package/lib/vendor/blamejs/lib/agent-posture-chain.js +10 -10
  16. package/lib/vendor/blamejs/lib/agent-saga.js +6 -7
  17. package/lib/vendor/blamejs/lib/agent-snapshot.js +8 -8
  18. package/lib/vendor/blamejs/lib/agent-stream.js +3 -3
  19. package/lib/vendor/blamejs/lib/agent-tenant.js +4 -4
  20. package/lib/vendor/blamejs/lib/agent-trace.js +5 -5
  21. package/lib/vendor/blamejs/lib/ai-disclosure.js +3 -3
  22. package/lib/vendor/blamejs/lib/app.js +2 -2
  23. package/lib/vendor/blamejs/lib/archive-read.js +1 -1
  24. package/lib/vendor/blamejs/lib/archive-tar-read.js +1 -1
  25. package/lib/vendor/blamejs/lib/archive-wrap.js +5 -5
  26. package/lib/vendor/blamejs/lib/audit-tools.js +65 -5
  27. package/lib/vendor/blamejs/lib/audit.js +2 -2
  28. package/lib/vendor/blamejs/lib/auth/ciba.js +1 -1
  29. package/lib/vendor/blamejs/lib/auth/dpop.js +1 -1
  30. package/lib/vendor/blamejs/lib/auth/fal.js +1 -1
  31. package/lib/vendor/blamejs/lib/auth/fido-mds3.js +2 -3
  32. package/lib/vendor/blamejs/lib/auth/jwt-external.js +2 -2
  33. package/lib/vendor/blamejs/lib/auth/oauth.js +9 -9
  34. package/lib/vendor/blamejs/lib/auth/oid4vci.js +7 -7
  35. package/lib/vendor/blamejs/lib/auth/oid4vp.js +1 -1
  36. package/lib/vendor/blamejs/lib/auth/openid-federation.js +5 -5
  37. package/lib/vendor/blamejs/lib/auth/passkey.js +6 -6
  38. package/lib/vendor/blamejs/lib/auth/saml.js +1 -1
  39. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -6
  40. package/lib/vendor/blamejs/lib/backup/index.js +18 -18
  41. package/lib/vendor/blamejs/lib/cache.js +4 -4
  42. package/lib/vendor/blamejs/lib/calendar.js +5 -5
  43. package/lib/vendor/blamejs/lib/circuit-breaker.js +1 -1
  44. package/lib/vendor/blamejs/lib/cms-codec.js +2 -2
  45. package/lib/vendor/blamejs/lib/compliance.js +14 -14
  46. package/lib/vendor/blamejs/lib/crypto-field.js +58 -21
  47. package/lib/vendor/blamejs/lib/crypto.js +5 -6
  48. package/lib/vendor/blamejs/lib/db-query.js +131 -9
  49. package/lib/vendor/blamejs/lib/db.js +106 -22
  50. package/lib/vendor/blamejs/lib/external-db.js +64 -16
  51. package/lib/vendor/blamejs/lib/framework-schema.js +4 -4
  52. package/lib/vendor/blamejs/lib/guard-list-id.js +2 -2
  53. package/lib/vendor/blamejs/lib/guard-list-unsubscribe.js +1 -2
  54. package/lib/vendor/blamejs/lib/incident-report.js +150 -0
  55. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +1 -1
  56. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -3
  57. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +2 -2
  58. package/lib/vendor/blamejs/lib/mail-server-pop3.js +2 -2
  59. package/lib/vendor/blamejs/lib/mail-store.js +1 -1
  60. package/lib/vendor/blamejs/lib/metrics.js +8 -8
  61. package/lib/vendor/blamejs/lib/middleware/csrf-protect.js +1 -1
  62. package/lib/vendor/blamejs/lib/middleware/dpop.js +5 -5
  63. package/lib/vendor/blamejs/lib/middleware/idempotency-key.js +21 -22
  64. package/lib/vendor/blamejs/lib/middleware/protected-resource-metadata.js +2 -2
  65. package/lib/vendor/blamejs/lib/network-dns-resolver.js +2 -2
  66. package/lib/vendor/blamejs/lib/network-dns.js +1 -2
  67. package/lib/vendor/blamejs/lib/network-tls.js +0 -1
  68. package/lib/vendor/blamejs/lib/outbox.js +1 -1
  69. package/lib/vendor/blamejs/lib/pqc-agent.js +1 -1
  70. package/lib/vendor/blamejs/lib/retention.js +1 -1
  71. package/lib/vendor/blamejs/lib/retry.js +1 -1
  72. package/lib/vendor/blamejs/lib/safe-archive.js +2 -2
  73. package/lib/vendor/blamejs/lib/safe-ical.js +2 -2
  74. package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
  75. package/lib/vendor/blamejs/lib/self-update-standalone-verifier.js +1 -1
  76. package/lib/vendor/blamejs/lib/self-update.js +2 -2
  77. package/lib/vendor/blamejs/lib/static.js +1 -1
  78. package/lib/vendor/blamejs/lib/subject.js +2 -2
  79. package/lib/vendor/blamejs/lib/vault/index.js +64 -1
  80. package/lib/vendor/blamejs/lib/vault/rotate.js +19 -0
  81. package/lib/vendor/blamejs/lib/vendor-data.js +1 -1
  82. package/lib/vendor/blamejs/package.json +1 -1
  83. package/lib/vendor/blamejs/release-notes/v0.14.7.json +77 -0
  84. package/lib/vendor/blamejs/scripts/release.js +28 -3
  85. package/lib/vendor/blamejs/test/layer-0-primitives/audit-tools-dual-control.test.js +115 -0
  86. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +163 -3
  87. package/lib/vendor/blamejs/test/layer-0-primitives/crypto-field-derived-hash.test.js +102 -0
  88. package/lib/vendor/blamejs/test/layer-0-primitives/db-column-gate.test.js +150 -0
  89. package/lib/vendor/blamejs/test/layer-0-primitives/db-key-aad.test.js +109 -0
  90. package/lib/vendor/blamejs/test/layer-0-primitives/external-db-hardening.test.js +53 -0
  91. package/lib/vendor/blamejs/test/layer-0-primitives/incident-report.test.js +65 -0
  92. package/lib/webhook-subscriptions.js +11 -24
  93. package/lib/webhooks.js +12 -33
  94. package/package.json +1 -1
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.textGuard
4
+ * @title Shop input-validation primitives over the framework's codepoint catalog
5
+ *
6
+ * @intro
7
+ * One home for the cross-cutting string validators the shop's
8
+ * handlers reach for at the request boundary: the ISO 4217 currency
9
+ * code, the URL slug label, the outbound host label, and the
10
+ * dangerous-codepoint screen for unconstrained free text. Each was
11
+ * previously open-coded at several call sites with subtly different
12
+ * shapes; centralizing them keeps the discipline identical
13
+ * everywhere and gives the codebase one place to tighten.
14
+ *
15
+ * The dangerous-codepoint surface (bidi overrides, C0 control bytes,
16
+ * null bytes, zero-width / invisible formatting, mixed-script
17
+ * confusables) is NOT reinvented here — it composes the framework's
18
+ * own codepoint catalog, the single source of truth the guard-*
19
+ * family already builds on. The framework does not yet re-export the
20
+ * catalog on its public index; until it does, this module reaches
21
+ * the catalog leaf directly. The leaf has no dependency on any shop
22
+ * module, so requiring it is refresh-safe; an upstream one-line
23
+ * `module.exports` add would let this collapse to `b.codepointClass`
24
+ * with no behavior change.
25
+ *
26
+ * Validators THROW a `TypeError` on bad input. The admin / account
27
+ * request wrappers map a thrown `TypeError` to a clean 400, so a
28
+ * typo or a hostile value surfaces as a bad-request to the caller
29
+ * instead of a 500 — the validators are written to be called at the
30
+ * handler boundary, before the value reaches storage or an outbound
31
+ * dial.
32
+ *
33
+ * Surface:
34
+ * - re-exports of the framework codepoint catalog (BIDI_RE,
35
+ * C0_CTRL_RE, ZERO_WIDTH_RE, NULL_BYTE, scriptFor,
36
+ * detectMixedScripts, detectCharThreats, assertNoCharThreats,
37
+ * applyCharStripPolicies) so consumers grab one import
38
+ * - asciiUpperLetters(s, label, n?) — fixed-length A-Z token
39
+ * - currencyCode(s, label?) — ISO 4217 shape + catalog membership
40
+ * - slugLabel(s, label, opts?) — lowercase URL-slug label
41
+ * - hostLabel(host, label?) — outbound host SSRF + by-name denylist
42
+ * - freeText(s, label, policy?) — dangerous-codepoint screen for
43
+ * unconstrained UTF-8 text fields
44
+ *
45
+ * @primitive textGuard
46
+ * @related b.money, b.ssrfGuard, b.safeUrl, shop.currencyRounding,
47
+ * shop.webhooks, shop.webhookSubscriptions
48
+ */
49
+
50
+ var b = require("./vendor/blamejs");
51
+
52
+ // The framework's codepoint catalog (bidi / control / null / zero-width
53
+ // tables + regexes, the script ranges, and the detect/assert/strip
54
+ // helpers the guard-* family composes). It is not on the framework's
55
+ // public index yet; the catalog leaf has no shop-module dependency, so
56
+ // composing it directly stays refresh-safe and keeps a single source of
57
+ // truth instead of a second copy of the tables.
58
+ // allow:vendor-hand-edit — composing the framework's codepoint catalog leaf (no shop dependency, refresh-safe); collapses to b.codepointClass once upstream re-exports it
59
+ var codepointClass = require("./vendor/blamejs/lib/codepoint-class");
60
+
61
+ // ---- shop validators ----------------------------------------------------
62
+
63
+ // asciiUpperLetters(s, label, n?) — exactly `n` (default 3) ASCII
64
+ // uppercase letters, nothing else. The generalized shape behind every
65
+ // `/^[A-Z]{3}$/` token check. ASCII-allowlist by construction, so bidi
66
+ // / control / zero-width / confusable codepoints are rejected for free
67
+ // (they are not in A-Z). Throws TypeError on a miss.
68
+ function asciiUpperLetters(s, label, n) {
69
+ var name = label || "value";
70
+ var len = n == null ? 3 : n;
71
+ if (typeof len !== "number" || !Number.isInteger(len) || len <= 0) {
72
+ throw new TypeError("textGuard: length must be a positive integer, got " + JSON.stringify(n));
73
+ }
74
+ // allow:dynamic-regex — len is a validated positive integer, not external input
75
+ var re = new RegExp("^[A-Z]{" + len + "}$");
76
+ if (typeof s !== "string" || !re.test(s)) {
77
+ throw new TypeError(
78
+ name + " must be exactly " + len + " uppercase ASCII letter(s), got " + JSON.stringify(s)
79
+ );
80
+ }
81
+ return s;
82
+ }
83
+
84
+ // currencyCode(s, label?) — ISO 4217 currency code: the asciiUpperLetters
85
+ // shape check THEN membership in the framework's catalog (b.money.CURRENCIES,
86
+ // the same surface currency-rounding + currency-display compose). Rejects
87
+ // both malformed codes ("usd", "US") and well-formed-but-nonexistent codes
88
+ // ("ZZZ") that would shape-check past `/^[A-Z]{3}$/` but bind money to a
89
+ // currency the rest of the shop can't price. Throws TypeError on a miss.
90
+ function currencyCode(s, label) {
91
+ var name = label || "currency";
92
+ if (typeof s !== "string" || !/^[A-Z]{3}$/.test(s)) {
93
+ throw new TypeError(
94
+ name + " must be a 3-letter uppercase ISO 4217 code, got " + JSON.stringify(s)
95
+ );
96
+ }
97
+ if (!Object.prototype.hasOwnProperty.call(b.money.CURRENCIES, s)) {
98
+ throw new TypeError(name + " " + JSON.stringify(s) + " is not in the ISO 4217 catalog");
99
+ }
100
+ return s;
101
+ }
102
+
103
+ // slugLabel(s, label, opts?) — a lowercase URL-slug label: starts and
104
+ // ends with [a-z0-9], inner chars [a-z0-9-], no leading / trailing /
105
+ // doubled hyphen, length 1..maxLen (default 80). ASCII-allowlist, so
106
+ // dangerous codepoints are rejected by construction. Throws TypeError.
107
+ function slugLabel(s, label, opts) {
108
+ var name = label || "slug";
109
+ opts = opts || {};
110
+ var maxLen = opts.maxLen == null ? 80 : opts.maxLen;
111
+ if (typeof maxLen !== "number" || !Number.isInteger(maxLen) || maxLen <= 0) {
112
+ throw new TypeError("textGuard: maxLen must be a positive integer, got " + JSON.stringify(opts.maxLen));
113
+ }
114
+ if (typeof s !== "string" || s.length < 1 || s.length > maxLen) {
115
+ throw new TypeError(
116
+ name + " must be a 1.." + maxLen + "-character lowercase slug, got " + JSON.stringify(s)
117
+ );
118
+ }
119
+ // Single char must itself be [a-z0-9]; longer must match start/inner/end.
120
+ var ok = s.length === 1
121
+ ? /^[a-z0-9]$/.test(s)
122
+ : /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(s);
123
+ if (!ok || /--/.test(s)) {
124
+ throw new TypeError(
125
+ name + " must be a lowercase URL slug ([a-z0-9] separated by single hyphens), got " + JSON.stringify(s)
126
+ );
127
+ }
128
+ return s;
129
+ }
130
+
131
+ // Hostnames that name an internal / metadata destination by name rather
132
+ // than by IP literal. The cloud-metadata services answer on a hostname as
133
+ // well as the 169.254.169.254 link-local IP; localhost resolves to
134
+ // loopback. A literal-IP host is classified directly via b.ssrfGuard
135
+ // (no DNS); these names cover the by-name reach of the same targets.
136
+ var BLOCKED_HOSTS = Object.freeze({
137
+ "localhost": 1,
138
+ "metadata.google.internal": 1,
139
+ });
140
+
141
+ // hostLabel(host, label?) — refuse an outbound host that targets an
142
+ // internal / loopback / link-local / reserved / cloud-metadata
143
+ // destination. A literal-IP host is classified via b.ssrfGuard.classify
144
+ // (IPv4 + IPv6 + IPv4-mapped, no DNS); a hostname is matched against the
145
+ // by-name metadata / loopback denylist plus the *.internal suffix. A
146
+ // trailing FQDN dot and IPv6 brackets are stripped first so neither check
147
+ // is evaded. DNS-rebinding (a public name resolving to a private IP) is
148
+ // out of scope at this gate by design — the resolving guard belongs on
149
+ // the delivery dial. Throws TypeError when the host is not allowed.
150
+ // Returns the normalized (bracket / trailing-dot stripped, lowercased)
151
+ // host on success.
152
+ function hostLabel(host, label) {
153
+ var name = label || "host";
154
+ if (typeof host !== "string" || host.length === 0) {
155
+ throw new TypeError(name + " must be a non-empty string");
156
+ }
157
+ var h = host.replace(/^\[|\]$/g, "").toLowerCase().replace(/\.+$/, "");
158
+ if (b.ssrfGuard.classify(h) ||
159
+ BLOCKED_HOSTS[h] ||
160
+ h === "internal" || /\.internal$/.test(h)) {
161
+ throw new TypeError(name + " is not allowed (internal/loopback/metadata address)");
162
+ }
163
+ return h;
164
+ }
165
+
166
+ // freeText(s, label, policy?) — the dangerous-codepoint screen for an
167
+ // UNCONSTRAINED UTF-8 text field (customer note, review body, gift
168
+ // message, product Q&A). Unlike the ASCII-allowlist validators above,
169
+ // these fields legitimately carry arbitrary letters, so the codepoint
170
+ // catalog is the right backing: bidi overrides (CVE-2021-42574 Trojan
171
+ // Source), null bytes, and C0 control characters are refused by default;
172
+ // zero-width / invisible formatting is refused when policy.zeroWidth is
173
+ // "reject"; a mixed-script confusable (Cyrillic 'а' inside an otherwise
174
+ // Latin label) is refused when policy.mixedScript is "reject". Throws
175
+ // TypeError on a refused codepoint. Returns the input on success.
176
+ function freeText(s, label, policy) {
177
+ var name = label || "value";
178
+ if (typeof s !== "string") {
179
+ throw new TypeError(name + " must be a string");
180
+ }
181
+ policy = policy || {};
182
+ var bidiP = policy.bidi || "reject";
183
+ var nullP = policy.nullByte || "reject";
184
+ var controlP = policy.control || "reject";
185
+ if (bidiP === "reject" && codepointClass.BIDI_RE.test(s)) {
186
+ throw new TypeError(name + " contains a Unicode bidi override (CVE-2021-42574 Trojan Source)");
187
+ }
188
+ if (nullP === "reject" && s.indexOf(codepointClass.NULL_BYTE) !== -1) {
189
+ throw new TypeError(name + " contains a null byte");
190
+ }
191
+ if (controlP === "reject" && codepointClass.C0_CTRL_RE.test(s)) {
192
+ throw new TypeError(name + " contains a C0 control character");
193
+ }
194
+ if (policy.zeroWidth === "reject" && codepointClass.ZERO_WIDTH_RE.test(s)) {
195
+ throw new TypeError(name + " contains a zero-width / invisible formatting character");
196
+ }
197
+ if (policy.mixedScript === "reject") {
198
+ var scripts = codepointClass.detectMixedScripts(s, policy.allowedScripts);
199
+ if (scripts) {
200
+ throw new TypeError(
201
+ name + " mixes writing systems (" + scripts.join(", ") + ") — possible confusable / homograph"
202
+ );
203
+ }
204
+ }
205
+ return s;
206
+ }
207
+
208
+ module.exports = {
209
+ // Re-exports of the framework codepoint catalog (single source of truth).
210
+ BIDI_RE: codepointClass.BIDI_RE,
211
+ C0_CTRL_RE: codepointClass.C0_CTRL_RE,
212
+ ZERO_WIDTH_RE: codepointClass.ZERO_WIDTH_RE,
213
+ NULL_BYTE: codepointClass.NULL_BYTE,
214
+ BOM_CHAR: codepointClass.BOM_CHAR,
215
+ scriptFor: codepointClass.scriptFor,
216
+ detectMixedScripts: codepointClass.detectMixedScripts,
217
+ detectCharThreats: codepointClass.detectCharThreats,
218
+ assertNoCharThreats: codepointClass.assertNoCharThreats,
219
+ applyCharStripPolicies: codepointClass.applyCharStripPolicies,
220
+
221
+ // Shop validators.
222
+ asciiUpperLetters: asciiUpperLetters,
223
+ currencyCode: currencyCode,
224
+ slugLabel: slugLabel,
225
+ hostLabel: hostLabel,
226
+ freeText: freeText,
227
+ };
@@ -3,8 +3,8 @@
3
3
  "_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
4
4
  "packages": {
5
5
  "blamejs": {
6
- "version": "0.14.6",
7
- "tag": "v0.14.6",
6
+ "version": "0.14.7",
7
+ "tag": "v0.14.7",
8
8
  "license": "Apache-2.0",
9
9
  "author": "blamejs contributors",
10
10
  "source": "https://github.com/blamejs/blamejs",
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.14.x
10
10
 
11
+ - v0.14.7 (2026-05-30) — **Storage and audit-trail hardening: queries are gated to declared columns, raw SQL refuses embedded literals, the database key is bound to its location, sealed-column lookup hashes gain a keyed mode, audit-chain purges can require dual control, and breach deadlines ship a running clock.** This release tightens the data and audit layers against a set of failure modes that were previously reachable. Database queries are now checked against the columns a table declared in its schema: a reference to an undeclared column fails closed by default instead of silently matching nothing, and the `whereRaw` escape hatch refuses an embedded string literal so values bind through placeholders. The database encryption key is sealed with its purpose, data directory, and key path as additional authenticated data, so a key file cannot be relocated to another deployment and unsealed there; an older key without that binding upgrades itself on first load. Sealed-column equality-lookup hashes can now be computed as a keyed MAC (HMAC-SHAKE256) off a per-deployment key, making the lookup hash unforgeable without that key, while the salted-SHA3 default is unchanged. Purging the tamper-evident audit chain can be placed under a two-authorizer dual-control grant so one operator cannot erase it alone, and database credential-rejection audits now record which relation the rejected credential tried to reach. Finally, breach-notification deadlines get a running clock that raises approaching and passed alerts as each regime's window elapses. One behavior change to note: the column gate defaults to reject — if a service issues queries against columns it did not declare in its schema, set `db.init({ columnGate: "warn" })` (audited, allowed) or `"off"` while the schema is reconciled. **Added:** *Column-membership gate on every query* — `b.db.from(table)` now checks each referenced column against the table's declared schema. The mode is set with `db.init({ columnGate: "reject" | "warn" | "off" })` (default `reject`), and `query.allowedColumns([...])` narrows a single query to an explicit allowlist that is always enforced. `b.db.getDeclaredColumns(table)` returns a table's declared column names (or `null` for an unknown table). This is defense in depth against typo'd or caller-influenced column names reaching the SQL layer (CWE-89). · *Keyed mode for sealed-column lookup hashes* — Equality-lookup ("derived") hashes for sealed columns can be computed as `hmac-shake256` — a keyed MAC over a per-deployment key — instead of the default `salted-sha3`. Set it per table with `cryptoField.registerTable(name, { derivedHashMode: "hmac-shake256" })` or per column with `{ from, mode: "hmac-shake256" }`. The keyed hash is unforgeable and un-correlatable without the deployment's MAC key, which raises the bar against offline lookup-table attacks on low-entropy sealed values (CWE-916). `b.vault.getDerivedHashMacKey()` exposes the 32-byte per-deployment key; it is created on first use and re-sealed across key rotation automatically. · *Dual-control gate on audit-chain purge* — `b.auditTools.purge` accepts `dualControlGrant`. When `audit_log` is placed under dual control, a verified archive and `confirm: true` are no longer sufficient: the purge additionally requires a consumed m-of-n grant whose action is bound to the purge, so a grant minted for another operation cannot be replayed and one operator cannot erase the tamper-evident chain alone (NIST SP 800-53 AU-9, separation of duties). · *Running clock for breach-notification deadlines* — `b.incident.report.createDeadlineClock({ notify, approachThresholds })` tracks open incidents and raises `deadline_approaching` and `deadline_passed` alerts as each regime's window elapses (GDPR 72h, DORA, NIS2, and the rest of the registry). Alerts are deduplicated per incident and stage, suppressed once a submission stage is acknowledged, and the clock can run on an interval or be ticked manually. **Changed:** *Queries against undeclared columns now fail closed by default* — The column gate defaults to `reject`: a query that references a column the table did not declare throws rather than silently matching nothing. A service that intentionally queries undeclared columns can set `db.init({ columnGate: "warn" })` to audit and allow, or `"off"` to disable the gate, while its schema is reconciled. Framework-declared columns (including `_id` and derived-hash columns) are always members. **Security:** *Database encryption key bound to its location* — `db.key.enc` is sealed with additional authenticated data over its purpose, resolved data directory, and resolved key path. A sealed key copied to a different deployment or path no longer unseals there — the AEAD authentication fails — which prevents silent key relocation. A legacy key sealed without this binding is detected and re-sealed in the bound format on first load, with no operator action required. · *`whereRaw` refuses embedded string literals* — `whereRaw(sql, params)` and `WhereBuilder.raw(sql, params)` reject a raw fragment containing a string literal (`'...'`); values must bind through the `params` array. A static, operator-controlled literal can opt in with `{ allowLiterals: true }`. This closes a path where a value concatenated into a raw fragment would reintroduce SQL injection (CWE-89). · *Credential-rejection audits record the attempted relation* — A `db.auth.failed` audit row (SQLSTATE 28000 / 28P01 / 42501) now carries `attemptedTable`, the relation the rejected credential tried to reach, extracted defensively from the statement. Triage can scope the blast radius of a credential-abuse event without correlating back to the raw SQL log (CWE-778). **Detectors:** *Audit-purge dual-control gate* — A new check fails the build if a call to `purgeAuditChain` appears in a file that does not also route through the dual-control gate, so a future caller cannot physically delete chain rows without two-authorizer enforcement. · *Raw-SQL literal/interpolation guard* — A new check fails the build on a `whereRaw` / `.raw` call whose SQL argument is built by template interpolation or string concatenation, keeping the bound-params discipline enforceable in framework code. · *Hand-rolled lookup-hash guard* — A new check fails the build if a sealed-column lookup hash is derived from the per-deployment salt outside the canonical helper, so call sites cannot bypass the keyed-mode and per-column mode policy. · *Auth-audit attempted-relation guard* — A new check fails the build if a `db.auth.failed` audit is emitted in a file that does not name `attemptedTable`, so the forensic field cannot be dropped from a future emitter.
12
+
11
13
  - v0.14.6 (2026-05-30) — **Access-refusal middleware can return RFC 9457 problem+json or a custom response, and several documented-but-uncallable APIs are now reachable.** Every access-refusal middleware — the auth gates (bearer, DPoP, mTLS, AAL, bound-key), CSRF, CORS, rate-limit, bot-guard, age-gate, the host and network allowlists, and the method and content-type gates — now accepts two uniform options: `problemDetails: true` returns an RFC 9457 `application/problem+json` body, and `onDeny(req, res, info)` hands the response to the caller. With neither set the refusal is byte-for-byte what it was, so this is a drop-in change that lets a service standardize one error envelope across its API instead of working around each middleware's hardcoded body. Alongside that: `b.middleware.requireBoundKey` is now exported (it was documented and tested but never wired into the middleware surface), `b.middleware.bearerAuth` accepts `requiredScopes` (previously rejected at construction, which made its scope-enforcement path unreachable), API-key refusals send the RFC 6750 challenge code that matches the failure, two documented call paths that named a missing namespace segment are corrected, and the release flow now flags stale GitHub Actions and vendored bundles — with a ready-to-paste pin — before a dependency PR is needed. **Added:** *Uniform `onDeny` and `problemDetails` options on every access-refusal middleware* — Each request-lifecycle middleware that refuses a request now takes `problemDetails: true` to emit an RFC 9457 `application/problem+json` body (composing `b.problemDetails`) and `onDeny(req, res, info)` to take over the response entirely; `info` carries the status, a machine reason, and the middleware-specific fields. The deny-path response headers (`Allow`, `WWW-Authenticate`, `Retry-After`, `Accept`) survive every mode. When neither option is set the response is unchanged. Covers `requireAuth`, `requireAal`, `requireMethods`, `requireContentType`, `requireMtls`, `requireBoundKey`, `bearerAuth`, `dpop`, `csrfProtect`, `fetchMetadata`, `botGuard`, `ageGate`, `hostAllowlist`, `networkAllowlist`, `cors`, `rateLimit`, and `dailyByteQuota` (whose existing `onExceeded` keeps working as an alias of `onDeny`). **Fixed:** *`b.middleware.requireBoundKey` is now callable* — The Bearer-API-key middleware was documented (with examples and tests) but never exported on `b.middleware`, so `b.middleware.requireBoundKey(...)` threw `undefined is not a function`. It is now wired into the middleware surface. · *`b.middleware.bearerAuth` accepts `requiredScopes`* — The RFC 6750 scope-enforcement path read `opts.requiredScopes`, but the option was rejected at construction with `unknown option`, making the 403 `insufficient_scope` behavior unreachable. `requiredScopes` is now an accepted option. · *RFC 6750 challenge codes on API-key refusals* — `b.middleware.requireBoundKey` now sends the `WWW-Authenticate` error code that matches the failure: `insufficient_scope` on a 403 missing-scope, `invalid_token` on an unknown or revoked token, and no error code on a 401 that presented no credentials (RFC 6750 §3). It previously sent `invalid_request` for every refusal. · *Corrected two documented call paths* — The compliance and network references named a path that dropped a namespace segment: the conformity-assessment scaffold is at `b.cra.report.conformityAssessment` (not `b.cra.conformityAssessment`), and the per-socket tuning helper is at `b.network.socket.applyToSocket` (not `b.network.applyToSocket`). The documented signatures now match the callable paths. · *GitHub Actions pins refreshed* — `github/codeql-action` 4.35.5 to 4.36.0, and `docker/login-action`, `docker/setup-buildx-action`, and `docker/setup-qemu-action` to their latest releases. **Detectors:** *`@primitive` reachability gate* — A new check resolves every documented `b.X.Y` primitive against the actual public surface and fails the build when a documented path is not callable (factory-instance shorthands excluded). This is the gate that would have caught the `requireBoundKey` and call-path issues above. · *Deny-path composition gate* — A new check requires every access-refusal middleware to route its refusal through the shared deny-response writer, so a future middleware cannot reintroduce a hardcoded body that locks callers out of `onDeny` / `problemDetails`. · *Actions and vendor currency in the release flow* — The release flow now fails the cut when a SHA-pinned GitHub Action or a vendored bundle is behind its latest upstream release. The actions report prints a ready-to-paste `owner/repo@<sha> # vX.Y.Z` pin and every file and line that uses it, so the bump is copy-paste rather than an after-the-fact dependency PR. Transient registry or API errors stay advisory so a flaky network response does not block an unrelated release.
12
14
 
13
15
  - v0.14.5 (2026-05-30) — **Finished cleaning up the mislabeled byte-literal lint suppressions, with no API or behavior changes.** A follow-up to the byte-literal lint tightening. The remaining suppression comments that named the byte-literal check on values that are not byte sizes — JSON-RPC error codes, HTTP status codes, octet ranges, day-in-milliseconds constants — are removed, keeping their explanatory text and any correctly-named companion suppression. Every byte-literal suppression that remains is now on genuine 1024-scale byte arithmetic. Source-comment hygiene only. **Changed:** *Remaining mislabeled byte-literal suppressions removed* — The byte-literal lint was previously a check on any multiple-of-8 integer, so suppression comments naming it were scattered across non-byte values. The last of those (in a handful of files, in mixed comment formats) are now removed — their explanatory text is retained as plain comments, and any correctly-named companion suppression is kept. The only byte-literal suppressions that remain are on genuine 1024-scale byte arithmetic. No change to any exported API, error code, wire format, or runtime behavior.
@@ -61,7 +61,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
61
61
  ### Data layer
62
62
 
63
63
  - **SQLite with sealed-by-default columns** — `b.db`, migrations, seeders, atomic-file writes
64
- - **Chainable query builder** — atomic `.increment(col, delta)`, closure-form `.whereGroup` / top-level `.orWhere` OR composition, `.search(fields, term)` LIKE-OR with safe `%`/`_` ESCAPE handling, `.paginate(opts)` returning `{ items, total, page, totalPages }`
64
+ - **Chainable query builder** — atomic `.increment(col, delta)`, closure-form `.whereGroup` / top-level `.orWhere` OR composition, `.search(fields, term)` LIKE-OR with safe `%`/`_` ESCAPE handling, `.paginate(opts)` returning `{ items, total, page, totalPages }`; a column-membership gate (`db.init({ columnGate })`, default reject) fails a query closed when it names a column the table never declared, and `whereRaw` refuses an embedded string literal so values bind through placeholders
65
65
  - **Mongo-style document-store facade** — `b.db.collection(name, opts?)` with `$set` / `$inc` / `$unset` / `$eq` / `$ne` / `$gt` / `$gte` / `$lt` / `$lte` / `$in` / `$like`; schemaless-document opts via `overflow: "<col>"` (folds unknown fields into a JSON-text column; rewrites `WHERE` on virtual fields to `JSON_EXTRACT`), `jsonColumns: [...]` (auto-stringify on write + parse via `b.safeJson` on read), `sealedFields: { email: "emailHash" }` (co-locates a `b.cryptoField` sealed-column / derived-hash declaration so plaintext lookups auto-rewrite to hash-column lookups)
66
66
  - **DB lifecycle** — in-memory encrypted snapshot via `b.db.snapshot()`; standalone encrypted-DB-file lifecycle (`b.db.fileLifecycle({ dataDir, vault })` — decrypt-to-tmpfs, periodic re-encrypt flush, graceful shutdown — same envelope as `b.db`, no schema/audit-chain coupling); `db.init` opt-outs `frameworkTables: false` / `auditSigning: false` and path overrides `encryptedDbPath` / `encryptedDbName` / `dbKeyPath`
67
67
  - **External RDBMS** — bring-your-own Postgres / MySQL with pool tuning + role-aware connect + read-replica routing (`b.externalDb`); declarative role-narrowed views and Postgres row-level-security migrations (`b.db.declareView`, `b.db.declareRowPolicy`)
@@ -98,7 +98,8 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
98
98
  - **At-rest envelope** — envelope-versioned PQC (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256); vault sealing (`b.crypto`, `b.vault`)
99
99
  - **Power-on self-test** — `b.crypto.selfTest()` runs FIPS 140-3-style integrity checks: NIST FIPS 202 known-answer tests (SHA3-256/512, SHAKE256), AEAD round-trip + tamper-detect, and ML-KEM-1024 / ML-DSA-87 / SLH-DSA-SHAKE-256f pairwise-consistency + negative tests; fails closed (throws) on any mismatch
100
100
  - **Field-level + crypto-shred** — `b.cryptoField.eraseRow`; per-column data residency tagging + per-row keys (`K_row = HKDF(K_table, rowId)`) so erasing the per-row key makes WAL / replica residuals undecryptable (`b.cryptoField.declareColumnResidency`, `b.cryptoField.declarePerRowKey`)
101
- - **AAD-bound sealed columns** — AEAD tag tied to `(table, rowId, column, schemaVersion)`; copy-paste between rows or schema-version replay surfaces as refused decrypt (`b.vault.aad`)
101
+ - **AAD-bound sealed columns** — AEAD tag tied to `(table, rowId, column, schemaVersion)`; copy-paste between rows or schema-version replay surfaces as refused decrypt (`b.vault.aad`). The database encryption key is sealed the same way — bound to its purpose, data directory, and key path — so a relocated key file fails to unseal; an older unbound key upgrades itself on first load
102
+ - **Keyed lookup hashes** — sealed-column equality-lookup hashes default to salted SHA3-512 and can opt into a keyed `hmac-shake256` MAC off a per-deployment key (`cryptoField.registerTable({ derivedHashMode })`, `b.vault.getDerivedHashMacKey`), making the lookup hash unforgeable and un-correlatable across deployments
102
103
  - **Signed webhooks + API encryption** — SLH-DSA-SHAKE-256f default; ML-DSA-65 opt-in; ECIES API encryption (`b.webhook`, `b.crypto`)
103
104
  - **HPKE / HTTP signatures** — RFC 9180 HPKE with ML-KEM-1024 + HKDF-SHA3-512 + ChaCha20-Poly1305 (`b.crypto.hpke`); RFC 9421 HTTP Message Signatures with derived components and ed25519 / ML-DSA-65 (`b.crypto.httpSig`); RFC 9530 Content-Digest / Repr-Digest body-integrity fields (SHA-256 / SHA-512, legacy algorithms refused — `b.contentDigest`) to sign the digest rather than the whole body
104
105
  - **X-Wing hybrid KEM** — `b.crypto.xwing` (draft-connolly-cfrg-xwing-kem, experimental): ML-KEM-768 + X25519 bound by SHA3-256, secure if either component holds — the conservative key-encapsulation shape for migrating off classical ECDH. `keygen` / `encapsulate` / `decapsulate` with a 1216-byte public key, 1120-byte ciphertext, and 32-byte shared secret
@@ -194,6 +194,8 @@ What blamejs defends against, by design:
194
194
  - **Disk theft of an offline data dir** — `vault.key.sealed` (wrapped mode) + sealed columns + audit chain mean the data dir alone is opaque without the vault passphrase. Plaintext mode is dev-only and prints a `WARNING:` on every boot.
195
195
  - **Future quantum decrypt of currently-stored ciphertext** — every encrypted-at-rest blob uses ML-KEM-1024 + P-384 hybrid KEM and XChaCha20-Poly1305. There's no classical-only fallback to harvest now and decrypt later.
196
196
  - **Audit-chain tampering** — every audit row carries `prevHash` + `rowHash` + `nonce` + `fencingToken`; the chain is verified at boot via `auditChain.verifyChain` and any mismatch refuses subsequent appends. Checkpoints are signed with SLH-DSA-SHAKE-256f. An attacker rewriting history needs to rewrite every subsequent hash AND forge the signing key.
197
+ - **Single-operator erasure of the audit chain** — physically purging archived rows already requires a verified archive bundle plus `confirm: true`. Placing `audit_log` under dual control (`b.db.declareRequireDualControl`) additionally requires `b.auditTools.purge({ dualControlGrant })` to carry a consumed m-of-n grant whose action is bound to the purge, so a single compromised operator cannot truncate the tamper-evident chain alone and a grant minted for another operation cannot be replayed.
198
+ - **Stolen / relocated database key file** — `db.key.enc` is sealed with additional authenticated data over its purpose, resolved data directory, and resolved key path. A key file copied into a different deployment or path no longer unseals there (the AEAD authentication fails), so an attacker who exfiltrates the key cannot point it at a different data dir to decrypt. An older key sealed without the binding upgrades to the bound format on first load, with no operator action.
197
199
  - **Cross-site request forgery on state-changing routes** — `csrfProtect` cookie-mode (double-submit pattern) + `SameSite=Lax` cookie + `Origin` / `Sec-Fetch-Site` checks in CORS.
198
200
  - **Drive-by scrapers / low-effort bots** — `botGuard` middleware fingerprints `User-Agent` + `Sec-Fetch-*` + `Accept-Language`.
199
201
  - **Online brute-force against credentials** — `b.auth.lockout` tracks failed attempts per account (or any operator-chosen key) and engages an exponential-backoff lockout (1m → 5m → 15m → 1h → 6h+, clamped). State lives in `b.cache` so it shares across cluster nodes when the cluster backend is wired. Operator-driven `unlock(key, { req, reason })` audits with the admin's 5 W's. Backend errors fail open (the framework's job is to slow attackers, not to lock operators out of their own admin accounts when Redis dies).
@@ -287,6 +289,7 @@ This is the minimum-viable security posture for a production deployment. The fra
287
289
  - [ ] Rotate the audit signing key annually (or per compliance schedule)
288
290
  - [ ] Archive old audit rows monthly: `blamejs audit archive --before <date> --out ./audit-archives/`
289
291
  - [ ] Back up the audit-archive bundles to a separate location with a different passphrase
292
+ - [ ] Place `audit_log` under dual control (`b.db.declareRequireDualControl`) so a physical purge requires a two-authorizer m-of-n grant in addition to a verified archive — one operator cannot erase the chain alone
290
293
 
291
294
  **Backups**
292
295
  - [ ] Schedule nightly backups via the framework's `b.backup` primitive (encrypted with `BLAMEJS_BACKUP_PASSPHRASE`, separate from vault passphrase)
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 1,
3
- "frameworkVersion": "0.14.6",
4
- "createdAt": "2026-05-30T16:00:48.364Z",
3
+ "frameworkVersion": "0.14.7",
4
+ "createdAt": "2026-05-30T20:25:42.630Z",
5
5
  "exports": {
6
6
  "a2a": {
7
7
  "type": "object",
@@ -14518,6 +14518,10 @@
14518
14518
  "type": "function",
14519
14519
  "arity": 0
14520
14520
  },
14521
+ "getDeclaredColumns": {
14522
+ "type": "function",
14523
+ "arity": 1
14524
+ },
14521
14525
  "getMode": {
14522
14526
  "type": "function",
14523
14527
  "arity": 0
@@ -37103,6 +37107,10 @@
37103
37107
  "create": {
37104
37108
  "type": "function",
37105
37109
  "arity": 1
37110
+ },
37111
+ "createDeadlineClock": {
37112
+ "type": "function",
37113
+ "arity": 1
37106
37114
  }
37107
37115
  }
37108
37116
  }
@@ -49732,6 +49740,10 @@
49732
49740
  "type": "function",
49733
49741
  "arity": 0
49734
49742
  },
49743
+ "getDerivedHashMacKey": {
49744
+ "type": "function",
49745
+ "arity": 0
49746
+ },
49735
49747
  "getDerivedHashSalt": {
49736
49748
  "type": "function",
49737
49749
  "arity": 0
@@ -136,7 +136,7 @@ function _registerTopic(topics, name, topicOpts, auditImpl) {
136
136
  throw new AgentEventBusError("agent-event-bus/bad-schema",
137
137
  "registerTopic: schema required (flat key→type map)");
138
138
  }
139
- // BUG-12 — `kind` is now captured on register so listTopics's kind
139
+ // `kind` is now captured on register so listTopics's kind
140
140
  // filter actually matches. Prior shape never set entry.kind, so the
141
141
  // filter at args.kind was dead. Default value derives from the
142
142
  // dotted topic name's first segment ("mail.scan.x" → "mail"), giving
@@ -164,7 +164,7 @@ function _registerTopic(topics, name, topicOpts, auditImpl) {
164
164
  });
165
165
  }
166
166
 
167
- // SUBSTRATE-22 — operators reloading a module (test runners between
167
+ // Operators reloading a module (test runners between
168
168
  // runs, hot-reload tools, multi-tenant onboarding flows that
169
169
  // register-deregister topics) need a clean unregister path; without
170
170
  // it the second register throws topic-duplicate and the operator is
@@ -183,7 +183,7 @@ function _unregisterTopic(topics, name, auditImpl) {
183
183
  function _listTopics(topics, args, permissions) {
184
184
  // Permission gate: list-topics requires no special scope by default;
185
185
  // operator can wrap with their own permissions instance for stricter.
186
- // BUG-12 — kind filter now matches because register captures kind.
186
+ // Kind filter now matches because register captures kind.
187
187
  var out = [];
188
188
  topics.forEach(function (entry) {
189
189
  if (args.kind && entry.kind !== args.kind) return;
@@ -229,7 +229,7 @@ async function _publish(topics, pubsub, name, payload, pOpts, permissions, audit
229
229
  }
230
230
  // Schema validation.
231
231
  guardEventBusPayload.validate(payload, entry.schema);
232
- // SUBSTRATE-6 — when a topic is tenant-scoped, require the publisher
232
+ // When a topic is tenant-scoped, require the publisher
233
233
  // to declare a tenantId BEFORE the event reaches the durable bus
234
234
  // backend. Prior shape allowed `wrapped._tenantId: null` to land on
235
235
  // the bus, and the receive-side drop only fired AFTER persistence —
@@ -158,7 +158,7 @@ function create(opts) {
158
158
  return {
159
159
  get: function (method, actorId, key) { return _get(store, method, actorId, key, auditImpl, ttlMs, maxResultBytes); },
160
160
  put: function (method, actorId, key, result, putOpts) { return _put(store, method, actorId, key, result, putOpts || {}, ttlMs, maxResultBytes, fingerprintArgs, auditImpl); },
161
- // SUBSTRATE-4 — putIfAbsent gates concurrent retries at the cache
161
+ // putIfAbsent gates concurrent retries at the cache
162
162
  // boundary so only one consumer runs the handler. Operator wraps:
163
163
  // var claim = await idem.putIfAbsent(method, actor, key, args);
164
164
  // if (claim.alreadyClaimed) return claim.result; // another retry won
@@ -173,7 +173,7 @@ function create(opts) {
173
173
  };
174
174
  }
175
175
 
176
- // SUBSTRATE-4 — atomic claim/check/run pattern. Returns one of:
176
+ // Atomic claim/check/run pattern. Returns one of:
177
177
  // { alreadyClaimed: false, fingerprint } — caller runs the handler
178
178
  // { alreadyClaimed: true, pending: true } — another in-flight claim holds the slot
179
179
  // { alreadyClaimed: true, result: <cached> } — prior handler completed; cached result
@@ -299,7 +299,7 @@ async function _get(store, method, actorId, key, auditImpl, ttlMs, maxResultByte
299
299
  throw new AgentIdempotencyError("agent-idempotency/corrupt-result",
300
300
  "get: cached result failed to parse — " + (e && e.message ? e.message : String(e)));
301
301
  }
302
- // SUBSTRATE-12 — atomic replayCount increment. The prior shape
302
+ // Atomic replayCount increment. The prior shape
303
303
  // (read row → mutate → put row) raced two concurrent retries: each
304
304
  // saw replayCount=N, both wrote replayCount=N+1, so the counter
305
305
  // missed bumps and the put-with-fresh-result race-clobbered prior
@@ -439,7 +439,7 @@ function _fingerprintArgs(args) {
439
439
  // unique fingerprint and defeat the args-mismatch defense. Strip
440
440
  // _traceContext (varies per-hop, doesn't change result).
441
441
  //
442
- // SUBSTRATE-11 — DO NOT strip _postureChain. The prior shape ignored
442
+ // DO NOT strip _postureChain. The prior shape ignored
443
443
  // _postureChain.postureSet, so a request made under
444
444
  // postureSet:["hipaa","pci-dss"] cached the same result that a
445
445
  // downgrade attempt under postureSet:["pci-dss"] would replay
@@ -493,7 +493,7 @@ function _inMemoryBackend(maxEntries) {
493
493
  map.set(_k(method, actorId, hash), row);
494
494
  return Promise.resolve();
495
495
  },
496
- // SUBSTRATE-4 — atomic insert. Map.set is synchronous so the
496
+ // Atomic insert. Map.set is synchronous so the
497
497
  // get+set pair below is naturally race-free within the in-memory
498
498
  // backend (V8 single-threaded). Returns true when inserted, false
499
499
  // when the row already exists.
@@ -503,7 +503,7 @@ function _inMemoryBackend(maxEntries) {
503
503
  map.set(k, row);
504
504
  return Promise.resolve(true);
505
505
  },
506
- // SUBSTRATE-12 — atomic replayCount increment. Operators wiring
506
+ // Atomic replayCount increment. Operators wiring
507
507
  // a SQL backend implement this with `UPDATE ... SET
508
508
  // replay_count = replay_count + 1 WHERE keyHash = $1 RETURNING *`
509
509
  // — read-modify-write race-free. In-memory backend is naturally
@@ -119,7 +119,7 @@ function _unsealRegistryRow(row) {
119
119
  var DEFAULT_DRAIN_TIMEOUT_MS = C.TIME.minutes(2);
120
120
  var STREAM_ID_RAND_BYTES = 8; // stream-id random-suffix byte length, not a size cap
121
121
  var DEFAULT_PER_CONSUMER_STOP_MS = C.TIME.seconds(5);
122
- // SUBSTRATE-20 — FNV-1a offset basis salted with the first 32 bits of
122
+ // FNV-1a offset basis salted with the first 32 bits of
123
123
  // SHA3-512(vault master). Attackers who don't have read access to the
124
124
  // vault keypair can't compute the salt, so they can't engineer
125
125
  // tenantIds that all map to one shard. Cached per-process; rotation
@@ -183,7 +183,7 @@ function create(opts) {
183
183
  // operator-supplied metadata (kind / tenantId / posture / ...);
184
184
  // every consuming process holds its own runtime map of name → agent.
185
185
  liveAgents: new Map(),
186
- // SUBSTRATE-8 — drain quiesce wiring. Operator passes
186
+ // Drain quiesce wiring. Operator passes
187
187
  // { outbox, sagaInFlightCount, pubsubFlush } via create() so the
188
188
  // drain phase can quiesce real in-flight work, not just stop
189
189
  // consumers. Optional — operators with no outbox / saga / pubsub
@@ -192,7 +192,7 @@ function create(opts) {
192
192
  sagaInFlightCount: typeof opts.sagaInFlightCount === "function" ? opts.sagaInFlightCount : null,
193
193
  pubsubFlush: typeof opts.pubsubFlush === "function" ? opts.pubsubFlush : null,
194
194
  perConsumerStopMs: typeof opts.perConsumerStopMs === "number" ? opts.perConsumerStopMs : DEFAULT_PER_CONSUMER_STOP_MS,
195
- // SUBSTRATE-9 — onTransition handler invalidates election cache
195
+ // onTransition handler invalidates election cache
196
196
  // on lease-lost / acquired / released. Operator opts out via
197
197
  // { cacheElections: false } to always re-query b.cluster.
198
198
  cacheElections: opts.cacheElections !== false,
@@ -205,7 +205,7 @@ function create(opts) {
205
205
  });
206
206
  }
207
207
 
208
- // SUBSTRATE-9 — subscribe to cluster lease transitions so cached
208
+ // Subscribe to cluster lease transitions so cached
209
209
  // election state can't go stale after a partition. b.cluster
210
210
  // .onTransition fires for every lease-acquired / lease-lost / lease-
211
211
  // released event; we invalidate the affected resource's cached
@@ -254,7 +254,7 @@ function create(opts) {
254
254
  * @status stable
255
255
  * @related b.agent.orchestrator.create
256
256
  *
257
- * SUBSTRATE-3 — attach an in-process live agent reference to a row
257
+ * Attach an in-process live agent reference to a row
258
258
  * that already exists in the persistent registry backend. The
259
259
  * canonical boot-phase contract: the *first* process to start a new
260
260
  * agent calls `register()` (writes the backend row + holds the live
@@ -499,7 +499,7 @@ function _spawnSingleConsumer(ctx, agent, queue, topic, maxConcurrency) {
499
499
  * // → integer in [0, 8)
500
500
  */
501
501
  function _saltedFnvBasis() {
502
- // SUBSTRATE-20 — salt FNV-1a offset basis with the vault master so
502
+ // Salt FNV-1a offset basis with the vault master so
503
503
  // an attacker can't engineer tenantIds that all hash to one shard.
504
504
  // Vault-less path (single-process tests / dev) falls back to the
505
505
  // standard FNV offset basis; production deployments with vault
@@ -561,7 +561,7 @@ async function _elect(ctx, args) {
561
561
  });
562
562
  return elec;
563
563
  }
564
- // SUBSTRATE-9 — cluster mode: ALWAYS query truth from b.cluster.
564
+ // Cluster mode: ALWAYS query truth from b.cluster.
565
565
  // The onTransition handler installed in create() invalidates the
566
566
  // cache on every lease event, so a cache hit here is safe (it
567
567
  // means no lease event has fired since the last query). But the
@@ -600,10 +600,10 @@ async function _drain(ctx, args) {
600
600
  var drained = 0;
601
601
  var startedAt = Date.now();
602
602
  var perConsumerMs = ctx.perConsumerStopMs;
603
- // SUBSTRATE-8 — drain phases:
603
+ // Drain phases:
604
604
  // 1. set ctx.draining so streams emit drain-markers + new task
605
605
  // dispatches refuse (consumers re-check on every envelope).
606
- // 2. stop each consumer with BUG-6 per-consumer timeout race —
606
+ // 2. stop each consumer with a per-consumer timeout race —
607
607
  // one hung consumer can't block the full drain budget.
608
608
  // 3. quiesce in-flight: poll outbox.pendingCount + sagaInFlightCount
609
609
  // until 0 OR remaining-budget-ms elapses.
@@ -65,7 +65,7 @@ var AgentPostureChainError = defineClass("AgentPostureChainError", { alwaysPerma
65
65
 
66
66
  var BUILTIN_REGIMES = Object.freeze(["hipaa", "pci-dss", "gdpr", "soc2"]);
67
67
 
68
- // SUBSTRATE-10 — envelope MAC vocabulary. Cross-process envelope
68
+ // Envelope MAC vocabulary. Cross-process envelope
69
69
  // integrity: an attacker with queue / event-bus write access who
70
70
  // strips postureSet to [] and re-sends a saga / sub-agent envelope
71
71
  // can bypass the downgrade refusal in _validate (which only checks
@@ -73,7 +73,7 @@ var BUILTIN_REGIMES = Object.freeze(["hipaa", "pci-dss", "gdpr", "soc2"]);
73
73
  // envelope bytes, computed at appendHop and verified at validate.
74
74
  var ENVELOPE_MAC_LABEL = "blamejs.agent.postureChain/v1";
75
75
  var ENVELOPE_MAC_KEY_BYTES = 32; // HMAC-SHA3-512 keyed bytes
76
- // SUBSTRATE-21 — hop count cap defends infinite recursion across
76
+ // Hop count cap defends infinite recursion across
77
77
  // agent delegation. 16 is the spec default; operators can lower via
78
78
  // opts.maxHopCount but never raise (audit fan-out without a cap is a
79
79
  // DoS class).
@@ -111,7 +111,7 @@ function _resolveMacKey() {
111
111
 
112
112
  function _envelopeMacBytes(envelope) {
113
113
  // Sign every field that downstream consumers verify off the wire,
114
- // except the `_mac` field itself. SUBSTRATE-21 — also includes
114
+ // except the `_mac` field itself. Also includes
115
115
  // hopCount + chainTrail so a hostile rewriter can't roll back the
116
116
  // trail to evade the cap.
117
117
  var payload = {
@@ -163,8 +163,8 @@ function create(opts) {
163
163
  opts.maxHopCount <= DEFAULT_MAX_HOP_COUNT
164
164
  ? Math.floor(opts.maxHopCount)
165
165
  : DEFAULT_MAX_HOP_COUNT;
166
- // SUBSTRATE-10 escape hatch — only operator-confirmed single-process
167
- // unit tests should opt out of envelope MAC. Production / multi-
166
+ // Escape hatch — only single-process unit tests should opt out of
167
+ // envelope MAC. Production / multi-
168
168
  // process / queue-spanning deployments leave the default on; the
169
169
  // gate audit-emits when bypassed so the posture is visible.
170
170
  var requireMac = opts.requireMac !== false;
@@ -257,7 +257,7 @@ function _appendHop(ctx, envelope, hopName) {
257
257
  "appendHop: hopName must be a non-empty string");
258
258
  }
259
259
  var trail = Array.isArray(envelope.chainTrail) ? envelope.chainTrail.slice() : [];
260
- // SUBSTRATE-21 — cap enforced BEFORE the push so the hop-cap throw
260
+ // Cap enforced BEFORE the push so the hop-cap throw
261
261
  // fires consistently regardless of whether the operator inspects
262
262
  // trail.length first. Cap is a hard refusal (no truncation) because
263
263
  // a silently-dropped hop loses audit provenance for the call.
@@ -279,8 +279,8 @@ function _appendHop(ctx, envelope, hopName) {
279
279
  hopCount: trail.length,
280
280
  });
281
281
  guardPostureChain.validate(newEnvelope);
282
- // SUBSTRATE-10 — sign at every hop. Verify-side enforces requireMac.
283
- // ctx.requireMac=false (operator-confirmed test escape hatch) skips
282
+ // Sign at every hop. Verify-side enforces requireMac.
283
+ // ctx.requireMac=false (test escape hatch) skips
284
284
  // the sign so a vault-less test path still works.
285
285
  if (ctx.requireMac) {
286
286
  try {
@@ -301,7 +301,7 @@ function _appendHop(ctx, envelope, hopName) {
301
301
 
302
302
  function _validate(ctx, envelope, agentPostureSet) {
303
303
  guardPostureChain.validate(envelope);
304
- // SUBSTRATE-10 — MAC verification BEFORE any field-based decision so
304
+ // MAC verification BEFORE any field-based decision so
305
305
  // the wire-rewrite attack (postureSet:[] downgrade with valid SHAPE
306
306
  // but no integrity binding) is refused. ctx.requireMac=false skips
307
307
  // verification and emits an audit so the bypass is visible.
@@ -326,7 +326,7 @@ function _validate(ctx, envelope, agentPostureSet) {
326
326
  chainTrail: envelope.chainTrail,
327
327
  });
328
328
  }
329
- // SUBSTRATE-21 — hop cap also enforced at validate-time. A hostile
329
+ // Hop cap also enforced at validate-time. A hostile
330
330
  // envelope might arrive with hopCount > cap if a prior hop's
331
331
  // requireMac was off; refuse here regardless.
332
332
  if (Array.isArray(envelope.chainTrail) && envelope.chainTrail.length > ctx.maxHopCount) {