@blamejs/core 0.8.42 → 0.8.49

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 (222) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/retention.js CHANGED
@@ -1,59 +1,46 @@
1
1
  "use strict";
2
2
  /**
3
- * retention — operator-declared data retention rules with periodic sweep.
3
+ * @module b.retention
4
+ * @nav Compliance
5
+ * @title Retention
4
6
  *
5
- * GDPR / HIPAA / PCI / industry-specific compliance regimes all share
6
- * the shape: "data of class X stored beyond TTL Y must be either
7
- * deleted or anonymized". The framework provides the building blocks
8
- * (b.cryptoField.eraseRow for crypto-erasure of sealed columns,
9
- * b.scheduler for the wake cadence, b.audit for the chain). retention
10
- * ties them into one operator-facing primitive.
7
+ * @intro
8
+ * Row-level retention floors per regulatory regime. GDPR Art. 17,
9
+ * HIPAA 45 CFR §164.530(j), PCI-DSS Req. 3.1, SOX §802 and friends
10
+ * all share the shape: "data of class X stored beyond TTL Y must be
11
+ * either deleted or anonymized". `b.retention` ties the framework's
12
+ * building blocks (`b.cryptoField.eraseRow` for crypto-erasure of
13
+ * sealed columns, `b.scheduler` for cadence, `b.audit` for the
14
+ * chain, `b.legalHold` for per-subject holds) into one
15
+ * operator-facing primitive that emits delete / erase / soft-delete
16
+ * jobs at expiry.
11
17
  *
12
- * var rules = b.retention.create({
13
- * db: b.db,
14
- * audit: b.audit,
15
- * });
16
- *
17
- * rules.declare({
18
- * name: "users.notes-ttl",
19
- * table: "users",
20
- * ageField: "createdAt", // milliseconds-since-epoch column
21
- * ttlMs: C.TIME.days(90),
22
- * action: "erase", // "erase" (b.cryptoField.eraseRow) | "delete"
23
- * batchSize: 500, // rows-per-sweep iteration; default 500
24
- * });
25
- *
26
- * // Operator wires the sweep cadence:
27
- * scheduler.schedule({
28
- * name: "retention.sweep",
29
- * every: C.TIME.hours(1),
30
- * run: function () { return rules.runAll(); },
31
- * });
32
- *
33
- * // Or run on demand (operator CLI / one-shot):
34
- * var summary = await rules.run("users.notes-ttl");
35
- * // → { name, scanned, processed, action, durationMs, errors: [] }
18
+ * Action vocabulary per row: `"erase"` (sealed columns + derived
19
+ * hashes go to NULL, `__erasedAt` set, row remains for FK / audit
20
+ * reference — cleartext is unrecoverable even with a vault key);
21
+ * `"delete"` (full row DELETE — for tables with no FK / audit
22
+ * reference); `"soft-delete"` (writes a deletion timestamp into
23
+ * `softDeleteField` — typical "trash bin" pattern); `"warn"` (audit
24
+ * only, no row write — used as an early stage in multi-stage
25
+ * schedules); `function(row)` (escape hatch for joined / conditional
26
+ * retention). Cascades follow `rule.cascade[]` foreign-key edges so
27
+ * a parent erase fans out into child rows in the same sweep.
36
28
  *
37
- * Audit posture (audit namespace "retention"):
38
- * - retention.rule.declared — once per declare() call
39
- * - retention.sweep.started — at the top of each runAll()/run()
40
- * - retention.row.processed — per row, with metadata.action
41
- * - retention.sweep.completed — at the end with row counts
42
- * - retention.sweep.failed — when the rule's SQL throws
29
+ * `b.compliance.set(posture)` cascades into `applyPosture` here, so
30
+ * the active posture's `audit_log` minimum-retention floor becomes
31
+ * the default `ttlMs` for any rule the operator declares without an
32
+ * explicit value. `complianceFloor(posture, candidateTtlMs)`
33
+ * surfaces those minimums for app-side conditional logic.
43
34
  *
44
- * Erase vs delete:
45
- * - "erase" (default): sealed columns + derived hashes go to NULL,
46
- * `__erasedAt` is set. Row stays for FK / audit reference. Per
47
- * GDPR Art. 17 the cleartext is unrecoverable even with a vault
48
- * key (no ciphertext to decrypt).
49
- * - "delete": full row DELETE. Use when no FK / audit reference
50
- * blocks the row from going.
35
+ * Audit events (namespace `retention`): `rule.declared`,
36
+ * `sweep.started`, `row.processed` (with `action`), `row.warned`,
37
+ * `row.legal_hold_skipped`, `sweep.completed`, `sweep.failed`,
38
+ * `sweep.skipped_concurrent`. Each sweep is single-flighted per
39
+ * rule name so a slow run cannot be re-entered by the next
40
+ * scheduler tick.
51
41
  *
52
- * Operators with COMPLEX retention (multi-table joins, conditional
53
- * rules) use action: function(row) async — the framework calls back
54
- * with each candidate row and the operator's function performs the
55
- * write. This is the escape hatch; the table+ageField+ttlMs shape
56
- * covers the common case.
42
+ * @card
43
+ * Row-level retention floors per regulatory regime.
57
44
  */
58
45
  var C = require("./constants");
59
46
  var lazyRequire = require("./lazy-require");
@@ -63,6 +50,7 @@ var { defineClass } = require("./framework-error");
63
50
 
64
51
  var audit = lazyRequire(function () { return require("./audit"); });
65
52
  var cryptoField = require("./crypto-field");
53
+ var legalHold = lazyRequire(function () { return require("./legal-hold"); });
66
54
 
67
55
  var RetentionError = defineClass("RetentionError", { alwaysPermanent: true });
68
56
  var _err = RetentionError.factory;
@@ -127,6 +115,13 @@ function _validateRule(rule) {
127
115
  if (rule.legalHoldField !== undefined) {
128
116
  _validateRuleIdentifier(rule.legalHoldField, "rule.legalHoldField");
129
117
  }
118
+ if (rule.subjectField !== undefined &&
119
+ (typeof rule.subjectField !== "string" || rule.subjectField.length === 0)) {
120
+ throw _err("BAD_RULE", "rule.subjectField must be a non-empty string");
121
+ }
122
+ if (rule.subjectField !== undefined) {
123
+ _validateRuleIdentifier(rule.subjectField, "rule.subjectField");
124
+ }
130
125
  if (rule.cascade !== undefined) {
131
126
  if (!Array.isArray(rule.cascade) || rule.cascade.length === 0) {
132
127
  throw _err("BAD_RULE", "rule.cascade must be a non-empty array of { table, foreignKey } entries");
@@ -163,6 +158,37 @@ function _validateRule(rule) {
163
158
  }
164
159
  }
165
160
 
161
+ /**
162
+ * @primitive b.retention.create
163
+ * @signature b.retention.create(opts)
164
+ * @since 0.6.14
165
+ * @status stable
166
+ * @compliance gdpr, hipaa, pci-dss, sox-404, soc2, dora, nis2
167
+ * @related b.retention.complianceFloor, b.retention.applyPosture, b.cryptoField.eraseRow, b.legalHold
168
+ *
169
+ * Build a retention controller bound to a database handle. Returns an
170
+ * object with `declare(rule)`, `run(name, runOpts?)`, `runAll(runOpts?)`,
171
+ * `preview(name)`, and `list()`. Audit emit is on by default; pass
172
+ * `audit: false` for a quiet controller in tests.
173
+ *
174
+ * @opts
175
+ * db: object, // b.db handle, must expose .prepare(sql)
176
+ * audit: boolean | object, // true | false | a b.audit instance
177
+ *
178
+ * @example
179
+ * var rules = b.retention.create({ db: b.db, audit: true });
180
+ * rules.declare({
181
+ * name: "users.notes-ttl",
182
+ * table: "users",
183
+ * ageField: "createdAt",
184
+ * ttlMs: C.TIME.days(90),
185
+ * action: "erase",
186
+ * batchSize: 500,
187
+ * legalHoldField: "__legalHold",
188
+ * });
189
+ * var summary = await rules.run("users.notes-ttl");
190
+ * // → { name, scanned, processed, action: "erase", durationMs, errors: [] }
191
+ */
166
192
  function create(opts) {
167
193
  opts = opts || {};
168
194
  validateOpts(opts, ["db", "audit"], "retention");
@@ -384,9 +410,25 @@ function create(opts) {
384
410
  if (rule.legalHoldField && row[rule.legalHoldField]) {
385
411
  summary.legalHoldsHonored++;
386
412
  _emit("retention.row.legal_hold_skipped",
387
- { name: name, table: rule.table, rowId: row._id }, "warning");
413
+ { name: name, table: rule.table, rowId: row._id,
414
+ source: "per-row-field" }, "warning");
388
415
  continue;
389
416
  }
417
+ // Subject-level legal-hold registry consult. When the rule
418
+ // names a subjectField (typical for user-keyed retention
419
+ // tables), the central registry is authoritative. Honors
420
+ // the same skip semantics as the per-row field.
421
+ if (rule.subjectField && row[rule.subjectField]) {
422
+ var holdsRegistry = legalHold._getSingleton();
423
+ if (holdsRegistry && holdsRegistry.isHeld(row[rule.subjectField])) {
424
+ summary.legalHoldsHonored++;
425
+ _emit("retention.row.legal_hold_skipped",
426
+ { name: name, table: rule.table, rowId: row._id,
427
+ source: "subject-registry",
428
+ subjectId: row[rule.subjectField] }, "warning");
429
+ continue;
430
+ }
431
+ }
390
432
  var action = _stageForRow(rule, row, startedAt);
391
433
  if (!action) { summary.skipped++; continue; }
392
434
  var actionLabel = typeof action === "function" ? "custom" : action;
@@ -487,6 +529,29 @@ var COMPLIANCE_RETENTION_FLOOR_MS = Object.freeze({
487
529
  // Operator passes a posture name + a candidate ttlMs; returns the
488
530
  // effective ttl that meets-or-exceeds the floor. Throws if posture is
489
531
  // unknown so typos surface at config time.
532
+ /**
533
+ * @primitive b.retention.complianceFloor
534
+ * @signature b.retention.complianceFloor(posture, candidateTtlMs)
535
+ * @since 0.7.24
536
+ * @status stable
537
+ * @compliance pci-dss, hipaa, sox-404, soc2, dora, nis2, cra
538
+ * @related b.retention.applyPosture, b.retention.create, b.compliance
539
+ *
540
+ * Take a regulatory posture name and a candidate TTL; return the
541
+ * effective TTL that meets-or-exceeds the regime's minimum-retention
542
+ * floor. Floors come from `COMPLIANCE_RETENTION_FLOOR_MS` (PCI-DSS
543
+ * §10.7.1: 12 months online; HIPAA 45 CFR §164.316(b)(2)(i): 6 years;
544
+ * SOX §802: 7 years; DORA Art. 17: 5 years; NIS2 Art. 23: 3 years;
545
+ * CRA Art. 14: 5 years; LGPD-BR / APPI-JP / PDPA-SG / UK-GDPR variants
546
+ * matched). Throws on an unknown posture so config-time typos surface.
547
+ *
548
+ * @example
549
+ * var ttl = b.retention.complianceFloor("hipaa", b.C.TIME.days(180));
550
+ * // → 189216000000 (HIPAA's 6-year floor wins over the 180-day candidate)
551
+ *
552
+ * var sox = b.retention.complianceFloor("sox", 0);
553
+ * // → 220752000000 (Sarbanes-Oxley §802 — 7 years)
554
+ */
490
555
  function complianceFloor(posture, candidateTtlMs) {
491
556
  if (typeof posture !== "string") {
492
557
  throw new RetentionError("retention/bad-posture",
@@ -504,9 +569,72 @@ function complianceFloor(posture, candidateTtlMs) {
504
569
  return candidateTtlMs > floor ? candidateTtlMs : floor;
505
570
  }
506
571
 
572
+ // applyPosture — F-POSTURE-1 cascade hook. b.compliance.set(posture)
573
+ // calls this to merge posture defaults into retention's state. The
574
+ // retention module itself doesn't carry per-instance global defaults;
575
+ // the cascade's job here is to surface the posture's audit-log
576
+ // retention floor as the value rules.declare() uses when an operator
577
+ // hasn't passed an explicit ttlMs. Returns the recognized floor (ms)
578
+ // or null when the posture has no retention floor.
579
+ /**
580
+ * @primitive b.retention.applyPosture
581
+ * @signature b.retention.applyPosture(posture)
582
+ * @since 0.7.24
583
+ * @status stable
584
+ * @compliance pci-dss, hipaa, sox-404, soc2, dora, nis2, cra
585
+ * @related b.retention.complianceFloor, b.retention.activePosture, b.compliance
586
+ *
587
+ * Cascade hook called by `b.compliance.set(posture)`. Records the
588
+ * posture name and its `audit_log` retention floor as module state so
589
+ * subsequent `complianceFloor` callers without an explicit posture
590
+ * argument inherit the active value. Returns `null` for an empty
591
+ * input or a posture with no retention floor; otherwise returns
592
+ * `{ posture, floorMs }`.
593
+ *
594
+ * @example
595
+ * b.compliance.set("hipaa");
596
+ * b.retention.applyPosture("hipaa");
597
+ * // → { posture: "hipaa", floorMs: 189216000000 }
598
+ * b.retention.activePosture();
599
+ * // → "hipaa"
600
+ */
601
+ function applyPosture(posture) {
602
+ if (typeof posture !== "string" || posture.length === 0) return null;
603
+ var floor = COMPLIANCE_RETENTION_FLOOR_MS[posture];
604
+ STATE.activePosture = posture;
605
+ STATE.activeFloorMs = (typeof floor === "number") ? floor : null;
606
+ return { posture: posture, floorMs: STATE.activeFloorMs };
607
+ }
608
+
609
+ // Module-level state — read by complianceFloor() callers that omit the
610
+ // posture argument (lookup falls back to the active cascade-set value).
611
+ var STATE = { activePosture: null, activeFloorMs: null };
612
+
613
+ /**
614
+ * @primitive b.retention.activePosture
615
+ * @signature b.retention.activePosture()
616
+ * @since 0.7.24
617
+ * @status stable
618
+ * @related b.retention.applyPosture, b.compliance.current
619
+ *
620
+ * Read the posture name set by the most recent `applyPosture` call,
621
+ * or `null` if `b.compliance.set` has never run on this process.
622
+ * Used by audit-dashboard tooling to surface "this deployment is
623
+ * pinned to <posture>" without crossing into `b.compliance` directly.
624
+ *
625
+ * @example
626
+ * var p = b.retention.activePosture();
627
+ * if (p === null) console.log("no compliance posture pinned");
628
+ * else console.log("active posture:", p);
629
+ * // → "hipaa"
630
+ */
631
+ function activePosture() { return STATE.activePosture; }
632
+
507
633
  module.exports = {
508
634
  create: create,
509
635
  complianceFloor: complianceFloor,
636
+ applyPosture: applyPosture,
637
+ activePosture: activePosture,
510
638
  COMPLIANCE_RETENTION_FLOOR_MS: COMPLIANCE_RETENTION_FLOOR_MS,
511
639
  RetentionError: RetentionError,
512
640
  };
package/lib/retry.js CHANGED
@@ -1,35 +1,40 @@
1
1
  "use strict";
2
2
  /**
3
- * b.retry — exponential-backoff retry + circuit breaker.
3
+ * @module b.retry
4
+ * @nav Production
5
+ * @title Retry
4
6
  *
5
- * Two layers of resilience for any operation that may fail transiently:
7
+ * @intro
8
+ * Retry plus circuit-breaker primitives — exponential backoff with
9
+ * jitter, half-open probe, and built-in classification of OS network
10
+ * error codes plus retryable HTTP status codes.
6
11
  *
7
- * 1. PER-CALL retry (withRetry) exponential backoff with jitter.
8
- * Default classifier targets HTTP 408/425/429/5xx and Node net-layer
9
- * error codes. Caller can override `opts.isRetryable` for non-network
10
- * semantics (e.g. operator-defined flush() in handlers).
12
+ * `b.retry.withRetry(fn, opts)` wraps a single call: exponential
13
+ * backoff (`baseDelayMs * 2^(attempt-1)`, capped at `maxDelayMs`)
14
+ * with cryptographic jitter so a thundering-herd of retrying clients
15
+ * does not realign on the same boundary. The default classifier
16
+ * targets HTTP 408 / 425 / 429 / 5xx and the Node net-layer codes
17
+ * (`ECONNRESET`, `ECONNREFUSED`, `ECONNABORTED`, `ETIMEDOUT`,
18
+ * `EPIPE`, `EAGAIN`, `ENOTFOUND`, `ENETUNREACH`); callers with
19
+ * non-network semantics override via `opts.isRetryable`. The retry
20
+ * loop honors `opts.signal` (AbortSignal) so a caller who aborts
21
+ * mid-retry is unblocked immediately rather than waiting out the
22
+ * backoff.
11
23
  *
12
- * 2. PER-TARGET circuit breaker (CircuitBreaker) N consecutive
13
- * failures opens the circuit (fast-fail for the cooldown window).
14
- * After cooldown the breaker enters half-open: probe successes close
15
- * it, a probe failure reopens it.
24
+ * `b.retry.CircuitBreaker` is the per-target sibling: N consecutive
25
+ * failures opens the circuit, opening fast-fails subsequent calls
26
+ * for `cooldownMs`, then a half-open state lets one probe call
27
+ * through. M consecutive probe successes close the circuit; one
28
+ * probe failure reopens it. Permanent errors (`err.permanent`) do
29
+ * not trip the breaker — those are caller bugs, not backend health
30
+ * issues. Both primitives are side-effect-free on success and
31
+ * compose with `b.safeAsync.withTimeout` and any caller-side
32
+ * instrumentation. HTTP-client auto-retry is intentionally not
33
+ * provided here so timeout, idempotency, and body-replay decisions
34
+ * stay explicit at the call site.
16
35
  *
17
- * Both are intentionally side-effect-free in the success path — they
18
- * compose freely with `safeAsync.withTimeout`, AbortSignals, and any
19
- * caller-side instrumentation.
20
- *
21
- * Validation policy:
22
- *
23
- * - withRetry opts at first call → throw at call site
24
- * - CircuitBreaker constructor opts → throw at call site
25
- * - backoffDelay(attempt) attempt argument → throw at call site
26
- * - isRetryable(err) defensive read → tolerant (return defaults)
27
- * - onRetry callback throw → drop silent (hot-path sink)
28
- * - breaker internal _onSuccess/_onFailure → drop silent (hot-path sink)
29
- *
30
- * HTTP-client auto-retry is intentionally NOT provided here. Callers
31
- * wrap their own outbound calls in `b.retry.withRetry(...)` to keep
32
- * timeout/idempotency/body-replay decisions explicit.
36
+ * @card
37
+ * Retry plus circuit-breaker primitives — exponential backoff with jitter, half-open probe, and built-in classification of OS network error codes plus retryable HTTP status codes.
33
38
  */
34
39
 
35
40
  var C = require("./constants");
@@ -187,6 +192,30 @@ function _validateBreakerOpts(name, opts) {
187
192
 
188
193
  // ---- Public surface ----
189
194
 
195
+ /**
196
+ * @primitive b.retry.isRetryable
197
+ * @signature b.retry.isRetryable(err)
198
+ * @since 0.5.0
199
+ * @related b.retry.withRetry, b.retry.backoffDelay
200
+ *
201
+ * Default classifier — returns `true` when an error looks transient
202
+ * and worth retrying, `false` otherwise. Honors `err.permanent` and
203
+ * `err.isObjectStoreError && err.permanent`; recognizes retryable HTTP
204
+ * status codes (408 / 425 / 429 / 5xx) and Node net-layer codes
205
+ * (`ECONNRESET`, `ECONNREFUSED`, `ECONNABORTED`, `ETIMEDOUT`, `EPIPE`,
206
+ * `EAGAIN`, `ENOTFOUND`, `ENETUNREACH`). Defensive read — missing
207
+ * fields return `false` rather than throwing, so a malformed error
208
+ * never crashes the retry loop.
209
+ *
210
+ * @example
211
+ * var transient = new Error("timeout");
212
+ * transient.code = "ETIMEDOUT";
213
+ * b.retry.isRetryable(transient); // → true
214
+ *
215
+ * var fatal = new Error("bad request");
216
+ * fatal.statusCode = 400;
217
+ * b.retry.isRetryable(fatal); // → false
218
+ */
190
219
  // Tolerant read of err shape; missing fields → false.
191
220
  function isRetryable(err) {
192
221
  if (!err) return false;
@@ -202,13 +231,30 @@ function isRetryable(err) {
202
231
  return false; // default: not retryable (avoid masking bugs)
203
232
  }
204
233
 
205
- // Throw on bad input: attempt must be a positive int; opts (when supplied)
206
- // must have non-neg-finite baseDelayMs/maxDelayMs and finite jitterFactor
207
- // in [0,1]. We don't full-validate opts here every call (hot path)
208
- // defaults are frozen, so the only way a bad opts reaches here is via
209
- // withRetry which already validated, OR a caller using backoffDelay
210
- // directly. For that
211
- // direct case we still validate the attempt arg loudly.
234
+ /**
235
+ * @primitive b.retry.backoffDelay
236
+ * @signature b.retry.backoffDelay(attempt, opts)
237
+ * @since 0.5.0
238
+ * @related b.retry.withRetry, b.retry.isRetryable
239
+ *
240
+ * Compute the backoff in milliseconds for a given (1-based) `attempt`
241
+ * number. Exponential growth `baseDelayMs * 2^(attempt-1)` capped at
242
+ * `maxDelayMs`, then subtract a cryptographic jitter sample scaled by
243
+ * `jitterFactor` so retrying clients do not realign on the same
244
+ * boundary. Throws TypeError when `attempt` is not a positive integer.
245
+ * `opts` defaults to `b.retry.DEFAULT_RETRY` when absent.
246
+ *
247
+ * @opts
248
+ * baseDelayMs: number, // initial backoff (default 100)
249
+ * maxDelayMs: number, // cap between attempts (default 10s)
250
+ * jitterFactor: number, // 0..1; 0 = no jitter, 1 = full jitter (default 0.5)
251
+ *
252
+ * @example
253
+ * var d1 = b.retry.backoffDelay(1, { baseDelayMs: 100, maxDelayMs: 1000, jitterFactor: 0 });
254
+ * d1; // → 100
255
+ * var d3 = b.retry.backoffDelay(3, { baseDelayMs: 100, maxDelayMs: 1000, jitterFactor: 0 });
256
+ * d3; // → 400
257
+ */
212
258
  function backoffDelay(attempt, opts) {
213
259
  if (!_isPositiveInt(attempt)) {
214
260
  throw new TypeError("retry.backoffDelay: attempt must be a positive integer, got " +
@@ -224,6 +270,43 @@ function backoffDelay(attempt, opts) {
224
270
  return Math.floor(capped - jitter);
225
271
  }
226
272
 
273
+ /**
274
+ * @primitive b.retry.withRetry
275
+ * @signature b.retry.withRetry(fn, opts)
276
+ * @since 0.5.0
277
+ * @related b.retry.isRetryable, b.retry.backoffDelay
278
+ *
279
+ * Run `fn(attempt)` with exponential backoff plus jitter. Retries up
280
+ * to `maxAttempts` while the classifier reports the failure transient;
281
+ * on a non-retryable error or after the final attempt the underlying
282
+ * error rethrows. Honors `opts.signal` so an AbortSignal cancels the
283
+ * backoff sleep. The `opts.onRetry` hook is invoked between attempts
284
+ * with `{ attempt, delay, error }`; throws inside the hook are
285
+ * captured and surfaced as `retry.onRetry.threw` observability events
286
+ * — the retry loop itself never crashes.
287
+ *
288
+ * @opts
289
+ * maxAttempts: number, // total tries incl. first (default 5)
290
+ * baseDelayMs: number, // initial backoff (default 100)
291
+ * maxDelayMs: number, // cap between attempts (default 10s)
292
+ * jitterFactor: number, // 0..1 (default 0.5)
293
+ * isRetryable: function, // override classifier (default b.retry.isRetryable)
294
+ * onRetry: function, // ({ attempt, delay, error }) -> void
295
+ * signal: object, // AbortSignal — cancels the backoff sleep
296
+ *
297
+ * @example
298
+ * var attempts = 0;
299
+ * var result = await b.retry.withRetry(async function () {
300
+ * attempts += 1;
301
+ * if (attempts < 2) {
302
+ * var err = new Error("nope");
303
+ * err.code = "ECONNRESET";
304
+ * throw err;
305
+ * }
306
+ * return "ok";
307
+ * }, { maxAttempts: 3, baseDelayMs: 1, maxDelayMs: 1, jitterFactor: 0 });
308
+ * result; // → "ok"
309
+ */
227
310
  async function withRetry(fn, opts) {
228
311
  if (typeof fn !== "function") {
229
312
  throw new TypeError("retry.withRetry: fn must be a function, got " + typeof fn);