@blamejs/core 0.6.7 → 0.6.12

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,12 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.6.x
10
10
 
11
+ - **0.6.12** (2026-05-02) — `b.safeUrl.parse` now rejects URLs with `user:pass@` userinfo by default (opt in per-call via `allowUserinfo: true`); `b.session.touch({ extendBy })` enforces the same `MAX_TTL_MS` ceiling as `create` / `rotate`; `b.queue.consume({ rateLimit })` rejects negative / zero / `NaN` / `Infinity` / fractional `max`; `b.middleware.requireAuth` no longer treats request `Content-Type: application/json` as a JSON-preference signal (only `Accept` and `X-Requested-With` count); `b.backup.create({ requireFlush: true })` opt-in fails the backup if pre-flush fails instead of producing a stale snapshot; `b.restore.create({ maxPulledBytes, maxPulledFiles })` preflight bounds bundle footprint before and after pull (defaults 4 GiB / 100K files); `b.mail.transports.console({ redactBcc: true })` opt-in prints recipient count instead of addresses; `b.logStream.transports.webhook({ onDrop })` callback fires on overflow + retry-exhausted batch drops. Wiki: admin login wired through `b.auth.lockout` (exponential-backoff after bad-cred attempts) and cookie `Secure` flag now routes through `b.requestHelpers.requestProtocol` with `WIKI_TRUST_PROXY` opt-in instead of trusting raw `x-forwarded-proto`. Wiki README documents the trust model for editable page bodies and the sanitization pattern operators should adopt before expanding the editor surface.
12
+ - **0.6.11** (2026-05-01) — wiki example-execution validator: fixture init no longer reaches across module realms (unblocks the npm-publish workflow's wiki-e2e gate, which `npm install --install-links` copies the framework into the wiki's node_modules — two distinct singletons before this fix)
13
+ - **0.6.10** (2026-05-01) — README / SECURITY / CONTRIBUTING / wiki: removed stale version stamps and an inaccurate vendored-dep list; SECURITY now points at `lib/vendor/MANIFEST.json` for the authoritative vendor list; supported-versions table no longer pins to a specific minor; wiki archive example names the digest variable correctly (was `sha256`, output is SHA3-512 hex)
14
+ - **0.6.9** (2026-05-01) — b.archive.zip().digest() returns a SHA3-512 hex string (was SHA-256); operators reconciling against an external SHA-256 must hash the bytes themselves
15
+ - **0.6.8** (2026-05-01) — wiki primitive-section validator: presence + opts diff + example execution against canonical fixture
16
+ - **0.6.7** (2026-05-01) — db.role.switched audit + per-role metrics + API snapshot baseline
11
17
  - **0.6.6** (2026-05-01) — request-time DB role binding + Postgres RLS migrations
12
18
  - **0.6.5** (2026-05-01) — b.db.declareView + b.externalDb.migrate
13
19
  - **0.6.4** (2026-05-01) — wiki schema docs realigned with the actual lib API
@@ -73,7 +79,7 @@ upgrading across more than a few patches at a time.
73
79
 
74
80
  ## v0.3.x
75
81
 
76
- - **0.3.39** (2026-04-29) — MIGRATING.md generator scans deprecate() calls in lib/
82
+ - **0.3.39** (2026-04-29) — MIGRATING.md generator scans deprecate calls in lib/
77
83
  - **0.3.38** (2026-04-29) — LTS-CALENDAR.md publishes the major-cadence + algorithm posture
78
84
  - **0.3.37** (2026-04-29) — bundled pure-JS mTLS engine + vendor-update.sh + CHANGELOG + thanks page
79
85
  - **0.3.36** (2026-04-29) — prepack guard refuses to publish any path that matches a gitignore rule
@@ -88,15 +94,15 @@ upgrading across more than a few patches at a time.
88
94
  - **0.3.27** (2026-04-29) — Dockerfile pre-builds public/dist in deps stage so runtime container boots cleanly under --cap-drop ALL
89
95
  - **0.3.26** (2026-04-29) — operator-facing env-var surface, GitHub-side ops docs, contribution standards
90
96
  - **0.3.25** (2026-04-29) — env-var surface documented across Dockerfile, docker-compose.prod, DEPLOY.md, README; smoke-test GHCR login fix
91
- - **0.3.24** (2026-04-29) — CI + release-container workflows brought to hermitstash parity (structured output, multi-arch, cosign, post-publish smoke)
97
+ - **0.3.24** (2026-04-29) — CI + release-container workflows brought (structured output, multi-arch, cosign, post-publish smoke)
92
98
  - **0.3.23** (2026-04-29) — _runVault and _runBackup refactored to use b.cliHelpers (sweep complete across the three CLI commands the helper was designed for)
93
99
  - **0.3.22** (2026-04-29) — blamejs api-key CLI; b.cliHelpers shared primitive for headless app + reporter shape
94
100
  - **0.3.21** (2026-04-29) — blamejs backup CLI: inspect / verify / extract; backup encryption format documented
95
101
  - **0.3.20** (2026-04-29) — drop redundant Windows-flaky audit.flush perf assertion
96
102
  - **0.3.19** (2026-04-29) — wiki docs for the unwired primitives surface
97
103
  - **0.3.18** (2026-04-29) — blamejs vault CLI: seal / unseal / rotate / status
98
- - **0.3.17** (2026-04-29) — Tier-A validateOpts coverage extended to remaining operator-facing primitives (i18n, bundler, migrations, seeders, template, jobs, cookies, log, mtlsCa, staticServe, handlers, websocket, pqcGate, tracing, metrics — 15 more factories now reject typo'd opts at boot). Coverage is now ~39/lib primitives with the helper; the remaining handful (app-shutdown, chain-writer, dev, error-page, nonce-store, protocol-dispatcher, render, session, pqc-agent) are either internal-only — operators don't construct them directly — or have a single required-key surface where typo guards add no value.
99
- - **0.3.16** (2026-04-29) — Tier-A config validation, CSRF cookie mode, HTML balance, dynamic Cache-Control, URL canonicalization
104
+ - **0.3.17** (2026-04-29) — validateOpts coverage extended to remaining operator-facing primitives (i18n, bundler, migrations, seeders, template, jobs, cookies, log, mtlsCa, staticServe, handlers, websocket, pqcGate, tracing, metrics — 15 more factories now reject typo'd opts at boot). Coverage is now ~39/lib primitives with the helper; the remaining handful (app-shutdown, chain-writer, dev, error-page, nonce-store, protocol-dispatcher, render, session, pqc-agent) are either internal-only — operators don't construct them directly — or have a single required-key surface where typo guards add no value.
105
+ - **0.3.16** (2026-04-29) — config validation, CSRF cookie mode, HTML balance, dynamic Cache-Control, URL canonicalization
100
106
  - **0.3.15** (2026-04-29) — cors: accept Origin: null when Sec-Fetch-Site is same-origin (browsers send opaque Origin: null on form-navigation POSTs from a page whose response sets Referrer-Policy: no-referrer; the Fetch-metadata Sec-Fetch-Site header is the matching disambiguation signal — same-origin or none means accept, anything else stays refused)
101
107
  - **0.3.14** (2026-04-29) — Dockerfile hotfix + lint as release gate
102
108
  - **0.3.13** (2026-04-29) — CI lint scans + Wolfi container + Trivy two-step + lint cleanup
@@ -133,7 +139,7 @@ upgrading across more than a few patches at a time.
133
139
  - **0.2.25** (2026-04-28) — b.slug
134
140
  - **0.2.24** (2026-04-28) — b.retry primitive
135
141
  - **0.2.23** (2026-04-28) — test helpers consolidate
136
- - **0.2.22** (2026-04-28) — tier-aware input validation
142
+ - **0.2.22** (2026-04-28) — input validation
137
143
  - **0.2.21** (2026-04-28) — defineClass.factory replaces per-module _err wrappers
138
144
  - **0.2.20** (2026-04-28) — b.requestHelpers + metrics _normalizeLabelArg
139
145
  - **0.2.19** (2026-04-28) — audit.safeEmit replaces per-module _emit wrappers
@@ -195,22 +201,22 @@ upgrading across more than a few patches at a time.
195
201
  - **0.1.78** (2026-04-27) — pqc-agent: locked-posture HTTPS agent + http-client refactor — slice 2
196
202
  - **0.1.77** (2026-04-27) — pqc-gate: TCP-level PQC enforcement on ClientHello — slice 1
197
203
  - **0.1.76** (2026-04-27) — bundler: content-hashed asset pipeline + manifest — complete
198
- - **0.1.75** (2026-04-27) — dev: file-watch + child-process restart engine — slice 6 ( complete)
204
+ - **0.1.75** (2026-04-27) — dev: file-watch + child-process restart engine — slice 6 (complete)
199
205
  - **0.1.74** (2026-04-27) — cli: bin/blamejs + migrate up/down/status — slice 5
200
206
  - **0.1.73** (2026-04-27) — migrations: public up/down/status runner — slice 4
201
207
  - **0.1.72** (2026-04-27) — cookies: parse/serialize + sealed-value access gate — slice 3
202
208
  - **0.1.71** (2026-04-27) — errors-page: rich dev page + safe prod page; middleware shim — slice 2
203
209
  - **0.1.70** (2026-04-27) — log: structured JSON logging with request-id correlation — slice 1
204
- - **0.1.69** (2026-04-27) — scheduler: cron + interval over jobs/queue — slice 5 ( complete)
210
+ - **0.1.69** (2026-04-27) — scheduler: cron + interval over jobs/queue — slice 5 (complete)
205
211
  - **0.1.68** (2026-04-27) — mail: generalize http transport, demote resend to thin preset
206
212
  - **0.1.67** (2026-04-27) — mail: contract + console/memory/smtp/resend transports — slice 4
207
213
  - **0.1.66** (2026-04-26) — jobs (define + enqueue + in-process worker) + createApp wiring — slices 2/3
208
214
  - **0.1.65** (2026-04-26) — createApp factory — slice 1
209
- - **0.1.64** (2026-04-26) — forms + csrfProtect — slice 4 ( complete)
215
+ - **0.1.64** (2026-04-26) — forms + csrfProtect — slice 4 (complete)
210
216
  - **0.1.63** (2026-04-26) — staticServe: file serving + ETag + SRI integrity — slice 3
211
217
  - **0.1.62** (2026-04-26) — render: HTTP response helpers — slice 2
212
218
  - **0.1.61** (2026-04-26) — template engine (eval-free interpreter) — slice 1
213
- - **0.1.60** (2026-04-26) — auth.jwt with SLH-DSA-SHAKE-256f default — slice 5/5 ( complete)
219
+ - **0.1.60** (2026-04-26) — auth.jwt with SLH-DSA-SHAKE-256f default — slice 5/5 (complete)
214
220
  - **0.1.59** (2026-04-26) — middleware.attachUser + middleware.requireAuth — slice 4
215
221
  - **0.1.58** (2026-04-26) — auth.passkey (WebAuthn / FIDO2) — slice 3
216
222
  - **0.1.57** (2026-04-26) — TOTP defaults match spec (HMAC-SHA512 / 128-byte secret / 8 digits / 30s) + crypto.random truncation fix
@@ -221,7 +227,7 @@ upgrading across more than a few patches at a time.
221
227
  - **0.1.52** (2026-04-26) — consent_log integrity parity with audit_log (closes the cluster spec storage move)
222
228
  - **0.1.51** (2026-04-26) — queue jobs move to external-db in cluster mode
223
229
  - **0.1.50** (2026-04-26) — sessions move to external-db in cluster mode
224
- - **0.1.49** (2026-04-26) — boot-time vault-key consistency check (closes cluster spec phase 4)
230
+ - **0.1.49** (2026-04-26) — boot-time vault-key consistency check (closes cluster spec)
225
231
  - **0.1.48** (2026-04-26) — boot-time audit-tip rollback detection in cluster mode
226
232
  - **0.1.47** (2026-04-26) — fix handlers.drain unbounded loop on recursive emit (cluster-mode audit hang)
227
233
  - **0.1.46** (2026-04-26) — cluster-mode audit-tip fencing-token guard
@@ -278,7 +284,7 @@ upgrading across more than a few patches at a time.
278
284
  - **0.0.18** (2026-04-26) — strip internal-process narrative from framework comments
279
285
  - **0.0.17** (2026-04-25) — internal adoption sweep + functional scale helpers
280
286
  - **0.0.16** (2026-04-25) — atomic file I/O + safe parsers (XML, CSV)
281
- - **0.0.15** (2026-04-25) — HTTP middleware: request lifecycle hardening ( begin)
287
+ - **0.0.15** (2026-04-25) — HTTP middleware: request lifecycle hardening (begin)
282
288
  - **0.0.14** (2026-04-25) — external DB (Shape B: app-data-only, bring-your-own-client)
283
289
  - **0.0.13** (2026-04-25) — log streaming + redaction + bidirectional command channel
284
290
  - **0.0.12** (2026-04-25) — queue dispatcher + local SQLite-backed protocol
package/README.md CHANGED
@@ -17,7 +17,7 @@ The modern Node app is a 1,200-package supply-chain liability with no LTS calend
17
17
 
18
18
  ## Status
19
19
 
20
- Pre-1.0. Usable end-to-end — operators can build production apps on it today; the surface is still subject to change before 1.0. Recent line is **v0.6.3** ([releases](https://github.com/blamejs/blamejs/releases) · [npm](https://www.npmjs.com/package/@blamejs/core) · [container](https://github.com/blamejs/blamejs/pkgs/container/blamejs-wiki)).
20
+ Pre-1.0. Usable end-to-end — operators can build production apps on it today; the surface is still subject to change before 1.0. The latest release lives on [GitHub](https://github.com/blamejs/blamejs/releases), [npm](https://www.npmjs.com/package/@blamejs/core), and the [container registry](https://github.com/blamejs/blamejs/pkgs/container/blamejs-wiki).
21
21
 
22
22
  ```js
23
23
  var b = require("@blamejs/core");
package/lib/api-key.js CHANGED
@@ -43,14 +43,14 @@
43
43
  * Same dual-storage pattern as sessions — local SQLite in single-node
44
44
  * mode, external-db in cluster mode, dispatched via cluster-storage.
45
45
  *
46
- * Validation tiers:
46
+ * Validation policy:
47
47
  *
48
- * - apiKey.create opts → Tier A (throw)
49
- * - registry.issue opts → Tier A (throw ApiKeyError)
50
- * - registry.rotate(id) on missing/revoked → Tier A (throw)
51
- * - registry.verify(token) on any failure → Tier C (return null)
52
- * - registry.revoke(id) on missing → Tier C (return false)
53
- * - registry.getById(id) on missing → Tier C (return null)
48
+ * - apiKey.create opts → throw at config time
49
+ * - registry.issue opts → throw ApiKeyError at call site
50
+ * - registry.rotate(id) on missing/revoked → throw ApiKeyError at call site
51
+ * - registry.verify(token) on any failure → return null (tolerant read)
52
+ * - registry.revoke(id) on missing → return false (tolerant read)
53
+ * - registry.getById(id) on missing → return null (tolerant read)
54
54
  */
55
55
 
56
56
  var crypto = require("./crypto");
@@ -69,7 +69,7 @@ var observability = lazyRequire(function () { return require("./observability");
69
69
 
70
70
  function _emitEvent(name, value, labels) {
71
71
  try { observability().event(name, value, labels || {}); }
72
- catch (_e) { /* Tier B: hot-path observability sink */ }
72
+ catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
73
73
  }
74
74
 
75
75
  var _err = ApiKeyError.factory;
@@ -127,7 +127,7 @@ var DEFAULTS = Object.freeze({
127
127
  hashAlgo: "shake256",
128
128
  });
129
129
 
130
- // ---- Tier-A validation helpers ----
130
+ // ---- Config-time validation helpers (throw on bad input) ----
131
131
 
132
132
  function _isPositiveInt(n) {
133
133
  return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
package/lib/archive.js CHANGED
@@ -216,11 +216,11 @@ function zip() {
216
216
  }
217
217
 
218
218
  function digest() {
219
- // SHA-256 of the produced archive bytes — useful for operator-side
220
- // integrity logging on exported bundles. Not vendor-locked to any
221
- // particular hash; SHA-256 is universally recognized for content
222
- // addressing.
223
- return nodeCrypto.createHash("sha256").update(toBuffer()).digest("hex");
219
+ // SHA3-512 of the produced archive bytes — useful for operator-side
220
+ // integrity logging on exported bundles. Matches the framework's
221
+ // PQC-first hash posture; operators reconciling against an external
222
+ // SHA-256 can hash the bytes themselves.
223
+ return nodeCrypto.createHash("sha3-512").update(toBuffer()).digest("hex");
224
224
  }
225
225
 
226
226
  return {
package/lib/audit.js CHANGED
@@ -711,8 +711,8 @@ function emit(event) {
711
711
  // audit outage doesn't crash the request handler. This is that helper,
712
712
  // hoisted out so each module can stop redefining it.
713
713
  //
714
- // Tier B (drop-silent): malformed input is dropped without throwing, by
715
- // design. safeEmit is called from request hot paths where throwing on a
714
+ // Drop-silent on malformed input by design. safeEmit is called from
715
+ // request hot paths where throwing on a
716
716
  // missing `action` would mean a malformed audit attempt crashes the
717
717
  // request that triggered it — strictly worse than the missing audit
718
718
  // row. Operators who need a hard guarantee the event landed should call
package/lib/auth/jwt.js CHANGED
@@ -247,10 +247,10 @@ async function verify(token, opts) {
247
247
  if (typeof opts.clockToleranceSec !== "number" ||
248
248
  !isFinite(opts.clockToleranceSec) ||
249
249
  opts.clockToleranceSec < 0) {
250
- // Tier-A: a negative tolerance over-tightens the window (legit
251
- // tokens get rejected as expired or not-yet-valid). A non-finite
252
- // value would NaN-poison the comparison. Catch both at config
253
- // time rather than producing surprise rejections in production.
250
+ // Throw at config time: a negative tolerance over-tightens the
251
+ // window (legit tokens get rejected as expired or not-yet-valid)
252
+ // and a non-finite value would NaN-poison the comparison. Catch
253
+ // both here rather than producing surprise rejections in production.
254
254
  throw new AuthError("auth-jwt/bad-clock-tolerance",
255
255
  "verify: clockToleranceSec must be a non-negative finite number, got " +
256
256
  JSON.stringify(opts.clockToleranceSec));
@@ -238,6 +238,13 @@ function create(opts) {
238
238
  var flushBeforeBackup = typeof opts.flushBeforeBackup === "function"
239
239
  ? opts.flushBeforeBackup
240
240
  : (opts.flushBeforeBackup === false ? null : null);
241
+ // requireFlush — when true, a flush failure FAILS the backup instead
242
+ // of producing a (potentially stale) snapshot. Operators on
243
+ // encrypted-at-rest with hard freshness requirements (compliance,
244
+ // audit, point-in-time recovery) opt in. Default false preserves the
245
+ // long-standing best-effort posture for operators who care about
246
+ // backup completing more than freshness.
247
+ var requireFlush = opts.requireFlush === true;
241
248
  // Default: try b.db.flushToDisk if available. Wired this way so the
242
249
  // backup primitive doesn't take a hard dependency on b.db (operators
243
250
  // running backup against an external db handle still work).
@@ -272,15 +279,24 @@ function create(opts) {
272
279
  var stagingDir = path.join(os.tmpdir(),
273
280
  "blamejs-backup-staging-" + bundleId.replace(/[:.]/g, "-"));
274
281
 
275
- // Flush the live DB to disk so the snapshot is current. Best-effort —
276
- // a flush failure logs but doesn't fail the whole backup; the bundle
277
- // will just snapshot whatever's on disk.
282
+ // Flush the live DB to disk so the snapshot is current. Default
283
+ // posture is best-effort — a flush failure logs but doesn't fail
284
+ // the whole backup. With requireFlush:true the failure aborts the
285
+ // backup so a stale snapshot never lands in storage.
278
286
  if (flushBeforeBackup) {
279
287
  try { await flushBeforeBackup(); }
280
288
  catch (e) {
289
+ var flushReason = (e && e.message) || String(e);
281
290
  _emitAudit("backup.flush.failure",
282
- { bundleId: bundleId, reason: (e && e.message) || String(e) },
283
- "warning");
291
+ { bundleId: bundleId, reason: flushReason },
292
+ requireFlush ? "failure" : "warning");
293
+ if (requireFlush) {
294
+ _emitAudit("backup.failure",
295
+ { bundleId: bundleId, reason: "flush-required-but-failed: " + flushReason },
296
+ "failure");
297
+ throw new BackupError("backup/flush-required-failed",
298
+ "backup flush required but failed: " + flushReason);
299
+ }
284
300
  }
285
301
  }
286
302
 
@@ -17,10 +17,10 @@
17
17
  *
18
18
  * Spec: memory/specs/blamejs-break-glass-spec.md
19
19
  *
20
- * v0.5.0 ships Model A (policy gate) + TOTP factor + Tier-A validation +
21
- * 14 error codes + audit chain integration. Model B (cryptographic
22
- * gate via per-row K_row), passkey factor, service-account bypass,
23
- * and admin tools land in v0.5.1 / v0.5.2 per the phasing plan.
20
+ * Ships Model A (policy gate) + TOTP factor + config-time input
21
+ * validation + 14 error codes + audit chain integration. Model B
22
+ * (cryptographic gate via per-row K_row), passkey factor, service-
23
+ * account bypass, and admin tools land in subsequent patches.
24
24
  *
25
25
  * Public API:
26
26
  *
@@ -59,8 +59,9 @@ var lockout = lazyRequire(function () { return require("./auth/lockout"); });
59
59
  var passkey = lazyRequire(function () { return require("./auth/passkey"); });
60
60
 
61
61
  // Errors — all 14 codes documented in the spec. `permanent: true`
62
- // means caller's input is bad (Tier-A); `permanent: false` means
63
- // transient (factor failed, rate-limited) — caller may retry.
62
+ // means caller's input is bad (config-time / call-site reject);
63
+ // `permanent: false` means transient (factor failed, rate-limited)
64
+ // — caller may retry.
64
65
  var BreakGlassError = defineClass("BreakGlassError", { alwaysPermanent: false });
65
66
 
66
67
  // ---- Defaults (matched to operator-locked decisions) ----
package/lib/cache.js CHANGED
@@ -51,14 +51,14 @@
51
51
  * { get, set, del, clear, size, close } — operator-supplied custom
52
52
  * backend (Redis, Memcached, …). All methods async.
53
53
  *
54
- * Validation tiers:
54
+ * Validation policy:
55
55
  *
56
- * - create() opts → Tier A (throw at boot)
57
- * - get/set/del/has/wrap key arg type → Tier A (throw programming bug)
58
- * - set value type → C (operator decides what to store)
59
- * - per-call ttlMs override → Tier A (throw bad ttl is silent footgun)
60
- * - audit / observability emit failures → Tier B (drop silent)
61
- * - method called after close() → Tier A (throw BAD_STATE)
56
+ * - create() opts → throw at boot
57
+ * - get/set/del/has/wrap key arg type → throw at call site (programming bug)
58
+ * - set value type → tolerant (operator decides what to store)
59
+ * - per-call ttlMs override → throw at call site (bad ttl is silent footgun)
60
+ * - audit / observability emit failures → drop silent (hot-path sink)
61
+ * - method called after close() → throw BAD_STATE at call site
62
62
  *
63
63
  * Security defaults:
64
64
  *
@@ -114,7 +114,7 @@ var DEFAULTS = Object.freeze({
114
114
  auditClear: true,
115
115
  });
116
116
 
117
- // ---- Tier-A validation helpers ----
117
+ // ---- Config-time validation helpers (throw on bad input) ----
118
118
 
119
119
  function _isFiniteNonNegative(n) {
120
120
  return typeof n === "number" && isFinite(n) && n >= 0;
@@ -669,7 +669,7 @@ function create(opts) {
669
669
  try {
670
670
  if (operatorObs) operatorObs.event(name, 1, labels || {});
671
671
  else observability().event(name, 1, labels || {});
672
- } catch (_e) { /* Tier B: hot-path observability sink */ }
672
+ } catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
673
673
  }
674
674
 
675
675
  function emitAudit(action, info) {
package/lib/constants.js CHANGED
@@ -18,10 +18,10 @@ var pkg = require("../package.json");
18
18
  // reader sees the unit at the call site (`C.TIME.minutes(45)` instead of
19
19
  // adding a new FORTY_FIVE_MIN constant). All return integer milliseconds.
20
20
  //
21
- // Tier A validation: these are config-time helpers. Throw on non-finite
22
- // or negative input so a typo (`C.TIME.minutes(opts.x)` where opts.x is
23
- // undefined) surfaces at boot instead of silently becoming `NaN` or `0`
24
- // and shipping a 0ms timeout into production.
21
+ // Throw on non-finite or negative input — these are config-time helpers,
22
+ // so a typo (`C.TIME.minutes(opts.x)` where opts.x is undefined) should
23
+ // surface at boot instead of silently becoming `NaN` or `0` and shipping
24
+ // a 0ms timeout into production.
25
25
  function _validateDuration(unit, n) {
26
26
  if (typeof n !== "number" || !isFinite(n) || n < 0) {
27
27
  throw new TypeError("C.TIME." + unit + ": expected non-negative finite number, got " +
@@ -42,8 +42,8 @@ var TIME = Object.freeze({
42
42
  // (KiB/MiB/GiB), since every existing byte literal in the framework
43
43
  // already uses 1024 multiplication.
44
44
  //
45
- // Tier A validation: same rationale as TIME bad input surfaces at the
46
- // call site, not as a silent NaN cap that disables size limits.
45
+ // Throw on bad input — same rationale as TIME: bad input surfaces at
46
+ // the call site, not as a silent NaN cap that disables size limits.
47
47
  function _validateBytes(unit, n) {
48
48
  if (typeof n !== "number" || !isFinite(n) || n < 0) {
49
49
  throw new TypeError("C.BYTES." + unit + ": expected non-negative finite number, got " +
@@ -61,13 +61,13 @@
61
61
  * - Transparent rehash via needsRehash() drains old algos at the
62
62
  * pace of organic verify traffic.
63
63
  *
64
- * Validation tiers:
64
+ * Validation policy:
65
65
  *
66
- * - hash() opts (algo, params) → Tier A (throw)
67
- * - hash() secret type / length → Tier A (throw)
68
- * - verify() envelope shape unparsable → Tier C (return false)
69
- * - verify() unknown algo ID → Tier C (return false)
70
- * - inspect() bad envelope → Tier C (return null)
66
+ * - hash() opts (algo, params) → throw at call site
67
+ * - hash() secret type / length → throw at call site
68
+ * - verify() envelope shape unparsable → return false (tolerant read)
69
+ * - verify() unknown algo ID → return false (tolerant read)
70
+ * - inspect() bad envelope → return null (tolerant read)
71
71
  */
72
72
 
73
73
  var crypto = require("./crypto");
@@ -79,7 +79,7 @@ var observability = lazyRequire(function () { return require("./observability");
79
79
 
80
80
  function _emitEvent(name, value, labels) {
81
81
  try { observability().event(name, value, labels || {}); }
82
- catch (_e) { /* Tier B: hot-path observability sink */ }
82
+ catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
83
83
  }
84
84
 
85
85
  // Default SHAKE256 output size for newly-issued credentials. 128
@@ -135,7 +135,7 @@ var ID_TO_NAME = Object.freeze({
135
135
  0x02: "argon2id",
136
136
  });
137
137
 
138
- // ---- Tier-A validation ----
138
+ // ---- Call-site validation (throw on bad input) ----
139
139
 
140
140
  function _validateSecret(secret) {
141
141
  if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
@@ -223,7 +223,7 @@ async function hash(secret, opts) {
223
223
  }
224
224
 
225
225
  async function verify(secret, envelope) {
226
- // Tier C: any malformed input → false. Lets operators write
226
+ // Tolerant read: any malformed envelope → false. Lets operators write
227
227
  // if (!await ch.verify(s, row.hash)) return res.status(401);
228
228
  // without try/catch ceremony. We still reject obvious caller bugs
229
229
  // (non-string-or-Buffer secret) loudly because that signals broken
@@ -39,7 +39,7 @@
39
39
  * typos surface as clear errors at the migrate command, not as silent
40
40
  * empty views or grant-to-nonexistent-role footguns.
41
41
  *
42
- * Tier-A validation at declareView() call time:
42
+ * Throw at declareView() call time on:
43
43
  * - schema, name, source segments → safeSql.validateIdentifier
44
44
  * - column names in redactColumns / sealedColumns / hashColumns →
45
45
  * safeSql.validateIdentifier
@@ -303,7 +303,7 @@ function declareView(opts) {
303
303
  var qSource = safeSql.quoteQualified([spec.source.schema, spec.source.name], "postgres");
304
304
 
305
305
  async function up(xdb, ctx) {
306
- // Tier-A boundary: confirm we're on Postgres before any DDL leaves the process.
306
+ // Boundary throw: confirm we're on Postgres before any DDL leaves the process.
307
307
  if (ctx && ctx.externalDb && ctx.backendName) {
308
308
  _ensureBackendIsPostgres(ctx.externalDb, ctx.backendName);
309
309
  }
@@ -208,7 +208,7 @@ function _emit(audit, action, outcome, info, reason) {
208
208
  metadata: info || {},
209
209
  reason: reason || null,
210
210
  });
211
- } catch (_e) { /* Tier B: drop-silent */ }
211
+ } catch (_e) { /* drop-silent — audit emit failure must not crash the migration */ }
212
212
  }
213
213
 
214
214
  // ---- Backend resolution ----
@@ -480,8 +480,8 @@ function request(opts) {
480
480
  return Promise.reject(_makeError(opts && opts.errorClass, "BAD_ARG", "url is required", true));
481
481
  }
482
482
 
483
- // Validate before/after shapes early — Tier-A throw if the operator
484
- // passed something un-callable so the bug surfaces at the call site
483
+ // Validate before/after shapes early — throw at call site if the
484
+ // operator passed something un-callable so the bug surfaces here
485
485
  // rather than inside the request loop.
486
486
  if (opts.before !== undefined) {
487
487
  if (!Array.isArray(opts.before) || !opts.before.every(function (f) { return typeof f === "function"; })) {
@@ -896,7 +896,7 @@ function _requestH1(transport, u, opts) {
896
896
  var bodyBuf = Buffer.isBuffer(opts.body) ? opts.body : Buffer.from(opts.body, "utf8");
897
897
  if (onUploadProgress) {
898
898
  // Chunked write so progress reports land before req.end().
899
- var CHUNK = 64 * 1024;
899
+ var CHUNK = C.BYTES.kib(64);
900
900
  var off = 0;
901
901
  while (off < bodyBuf.length) {
902
902
  var slice = bodyBuf.slice(off, Math.min(off + CHUNK, bodyBuf.length));
package/lib/i18n.js CHANGED
@@ -38,16 +38,16 @@
38
38
  * - {var} interpolation; missing vars render as literal {var} unless
39
39
  * `interpolation.strict: true`.
40
40
  *
41
- * Validation tiers:
41
+ * Validation policy:
42
42
  *
43
- * - create() opts → Tier A (throw at boot)
44
- * - bad locale tag at any boundary → Tier A (throw)
45
- * - t(missingKey) → Tier C (return key + obs)
46
- * - t() with bad locale override → Tier A (programming bug)
47
- * - plural shape missing 'other' → Tier A (throw at load)
48
- * - interpolation missing var → Tier C (literal {var})
49
- * - format* bad input → Tier A
50
- * - middleware Accept-Language parse error → Tier C (defaultLocale)
43
+ * - create() opts → throw at boot
44
+ * - bad locale tag at any boundary → throw at call site
45
+ * - t(missingKey) → return key + observability event
46
+ * - t() with bad locale override → throw at call site (programming bug)
47
+ * - plural shape missing 'other' → throw at load time
48
+ * - interpolation missing var → render literal {var}
49
+ * - format* bad input → throw at call site
50
+ * - middleware Accept-Language parse error → fall back to defaultLocale
51
51
  *
52
52
  * Security stance: translation values come from operator-controlled
53
53
  * files, not user input. {var} interpolation does NOT html-escape;
@@ -97,7 +97,7 @@ var DEFAULTS = Object.freeze({
97
97
  RTL_LANGUAGES: RTL_LANGUAGES,
98
98
  });
99
99
 
100
- // ---- Tier-A validation ----
100
+ // ---- Call-site validation (throw on bad input) ----
101
101
 
102
102
  function _isValidBcp47(tag) {
103
103
  if (typeof tag !== "string" || tag.length === 0) return false;
@@ -476,7 +476,7 @@ function create(opts) {
476
476
  try {
477
477
  if (operatorObs) operatorObs.event(name, 1, labels || {});
478
478
  else observability().event(name, 1, labels || {});
479
- } catch (_e) { /* Tier B */ }
479
+ } catch (_e) { /* drop-silent observability sink must not crash i18n calls */ }
480
480
  }
481
481
 
482
482
  // Cardinal plural-rules instances per locale. `Intl.PluralRules` is
@@ -721,7 +721,7 @@ function create(opts) {
721
721
  // Permit setting a non-configured locale (operators may want to
722
722
  // experiment), but fall the chain through to the configured ones.
723
723
  // Don't throw — i18n.locale is set/observed in many flows; making
724
- // it Tier-A would force operators into try/catch around UI setters.
724
+ // this throw would force operators into try/catch around UI setters.
725
725
  _emitObs("i18n.miss.locale", { requested: newLocale, resolved: defaultLocale });
726
726
  }
727
727
  currentLocale = newLocale;
@@ -97,6 +97,19 @@ function create(config) {
97
97
  errorClass: LogStreamError,
98
98
  });
99
99
  var headers = Object.assign({ "Content-Type": cfg.contentType }, _authHeaders(cfg));
100
+ // onDrop callback: invoked when a batch is dropped, either by buffer
101
+ // overflow ("overflow") or by retry exhaustion ("retry-exhausted").
102
+ // Operator wiring this directly (without the framework's dispatcher
103
+ // wrapping) needs visibility into permanent-drop events; the
104
+ // dispatcher path emits its own audit, but a sink used in isolation
105
+ // would otherwise lose drops silently. The callback is invoked
106
+ // best-effort — a throw inside it is swallowed.
107
+ var onDrop = typeof cfg.onDrop === "function" ? cfg.onDrop : null;
108
+ function _emitDrop(reason, batch, err) {
109
+ if (!onDrop) return;
110
+ try { onDrop({ reason: reason, batch: batch, error: err || null }); }
111
+ catch (_e) { /* drop callback is best-effort by design */ }
112
+ }
100
113
  var buffer = [];
101
114
  var dropCount = 0;
102
115
  var flushTimer = null;
@@ -121,9 +134,13 @@ function create(config) {
121
134
  await retryHelper.withRetry(function () {
122
135
  return _post(cfg.url, body, headers, cfg.timeoutMs, cfg.allowedProtocols, cfg.allowInternal);
123
136
  }, cfg.retry);
124
- } catch {
125
- // Batch permanently rejected — surface via the dropped counter.
126
- // Caller's audit hook recorded the drop already at the dispatcher.
137
+ } catch (e) {
138
+ // Batch permanently rejected — surface via dropCount AND the
139
+ // operator-supplied onDrop callback. The dispatcher path
140
+ // wraps its own audit hook around emit(); operators using
141
+ // this sink directly rely on dropCount + onDrop.
142
+ dropCount += batch.length;
143
+ _emitDrop("retry-exhausted", batch, e);
127
144
  break;
128
145
  }
129
146
  }
@@ -136,8 +153,9 @@ function create(config) {
136
153
  function emit(record) {
137
154
  if (closed) return Promise.resolve({ accepted: false, reason: "sink closed" });
138
155
  if (buffer.length >= cfg.bufferLimit) {
139
- buffer.shift(); // drop oldest
156
+ var dropped = buffer.shift(); // drop oldest
140
157
  dropCount += 1;
158
+ _emitDrop("overflow", [dropped], null);
141
159
  }
142
160
  buffer.push(record);
143
161
  if (buffer.length >= cfg.batchSize) {
package/lib/mail.js CHANGED
@@ -246,6 +246,14 @@ function _toArray(v) {
246
246
  function consoleTransport(opts) {
247
247
  opts = opts || {};
248
248
  var stream = opts.stream || process.stderr;
249
+ // redactBcc: print only the recipient COUNT instead of the addresses.
250
+ // Default false preserves the dev-visibility purpose of this
251
+ // transport. Operators piping dev logs into shared / centralized
252
+ // sinks (Slack, log aggregator, ticket system) opt in to avoid
253
+ // leaking the BCC list — the property exists precisely so a recipient
254
+ // doesn't see who else got the message, and that promise breaks the
255
+ // moment the addresses land in a non-private log.
256
+ var redactBcc = opts.redactBcc === true;
249
257
  return {
250
258
  name: "console",
251
259
  send: async function (message) {
@@ -255,7 +263,14 @@ function consoleTransport(opts) {
255
263
  "[mail.console] Subject: " + (message.subject || ""),
256
264
  ];
257
265
  if (message.cc) lines.push("[mail.console] Cc: " + (Array.isArray(message.cc) ? message.cc.join(", ") : message.cc));
258
- if (message.bcc) lines.push("[mail.console] Bcc: " + (Array.isArray(message.bcc) ? message.bcc.join(", ") : message.bcc));
266
+ if (message.bcc) {
267
+ if (redactBcc) {
268
+ var bccCount = Array.isArray(message.bcc) ? message.bcc.length : 1;
269
+ lines.push("[mail.console] Bcc: <" + bccCount + " recipient" + (bccCount === 1 ? "" : "s") + " — redacted>");
270
+ } else {
271
+ lines.push("[mail.console] Bcc: " + (Array.isArray(message.bcc) ? message.bcc.join(", ") : message.bcc));
272
+ }
273
+ }
259
274
  var body = message.text || (message.html ? "(html body, " + message.html.length + " bytes)" : "");
260
275
  lines.push("");
261
276
  lines.push(body);
@@ -1041,8 +1041,40 @@ async function _parseJsonFromBuf(buf, opts) {
1041
1041
  return parsed;
1042
1042
  }
1043
1043
 
1044
+ // raw — convenience wrapper that returns a middleware which buffers
1045
+ // the request body as a Buffer regardless of Content-Type. Webhook
1046
+ // signature-verification routes use this — the HMAC is computed over
1047
+ // the literal body bytes, so JSON-parsing first would change them.
1048
+ //
1049
+ // router.post("/hooks/in", b.middleware.bodyParser.raw(), function (req, res) {
1050
+ // verifier.verify(req.headers["x-signature"], req.body); // req.body is a Buffer
1051
+ // });
1052
+ //
1053
+ // Accepts the same `raw`-section opts as create() (limit, contentTypes).
1054
+ // contentTypes default expands to `["*/*"]` so any Content-Type lands
1055
+ // as raw bytes.
1056
+ function raw(opts) {
1057
+ opts = opts || {};
1058
+ return create({
1059
+ json: false,
1060
+ urlencoded: false,
1061
+ text: false,
1062
+ multipart: false,
1063
+ raw: {
1064
+ limit: opts.limit != null ? opts.limit : DEFAULTS.raw.limit,
1065
+ contentTypes: opts.contentTypes || ["*/*"],
1066
+ },
1067
+ });
1068
+ }
1069
+
1070
+ // Attach raw onto create so b.middleware.bodyParser.raw() works
1071
+ // (middleware/index.js exports the create function as the namespace
1072
+ // itself, so static helpers hang off it).
1073
+ create.raw = raw;
1074
+
1044
1075
  module.exports = {
1045
1076
  create: create,
1077
+ raw: raw,
1046
1078
  BodyParserError: BodyParserError,
1047
1079
  // Internal helpers exposed for tests + the csrf-protect refactor.
1048
1080
  _contentType: _contentType,
@@ -191,7 +191,7 @@ function create(opts) {
191
191
  }
192
192
  }
193
193
 
194
- // Tier A validation on opts.siteOrigin — must parse as http(s) URL.
194
+ // Throw at create() on bad opts.siteOrigin — must parse as http(s) URL.
195
195
  // Accept string OR array of strings.
196
196
  var siteOrigins = [];
197
197
  if (opts.siteOrigin !== undefined && opts.siteOrigin !== null) {