@blamejs/core 0.8.43 → 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 +92 -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
@@ -1,49 +1,39 @@
1
1
  "use strict";
2
2
  /**
3
- * config-drift — boot-time config-baseline capture + signed sidecar +
4
- * next-boot drift detection.
3
+ * @module b.configDrift
4
+ * @nav Observability
5
+ * @title Config Drift
5
6
  *
6
- * The framework's audit chain captures DATA writes (every row, every
7
- * key change). It does NOT capture RUNTIME CONFIG (the operator
8
- * silently changed `allowedOrigins` 3 weeks ago and we have no
9
- * signal). config-drift fills that gap: at every boot, the operator
10
- * passes the baseline config snapshot they want tracked. The
11
- * primitive hashes it with SHA3-512, signs the digest with the audit-
12
- * signing key, and writes the result to a sidecar at
13
- * `<dataDir>/config-baseline.sig`. On the next boot, the sidecar is
14
- * loaded + verified + diffed against the new snapshot. Drift surfaces
15
- * as an audit event (`config.drift.detected`); no boot block — the
16
- * operator may have a legitimate reason to change config and the
17
- * framework's job is to make the change auditable, not to refuse to
18
- * start.
7
+ * @intro
8
+ * Monitor + alert when runtime config diverges from a declared
9
+ * baseline.
19
10
  *
20
- * var configDrift = b.configDrift.create({
21
- * dataDir: "/data",
22
- * audit: b.audit,
23
- * });
11
+ * The framework's audit chain captures DATA writes (every row, every
12
+ * key change). It does NOT capture RUNTIME CONFIG — an operator who
13
+ * silently changes `allowedOrigins` three weeks ago leaves no signal.
14
+ * `b.configDrift` fills that gap: at every boot the operator passes
15
+ * the baseline snapshot they want tracked. The primitive hashes it
16
+ * with SHA3-512, signs the digest with the audit-signing key, and
17
+ * writes the result to a sidecar at `<dataDir>/config-baseline.sig`.
18
+ * On the next boot the sidecar is loaded, verified, and diffed
19
+ * against the new snapshot. Drift surfaces as an audit event
20
+ * (`config.drift.detected`); no boot block — the operator may have a
21
+ * legitimate reason to change config, and the framework's job is to
22
+ * make the change auditable, not to refuse to start.
24
23
  *
25
- * await configDrift.checkpoint({
26
- * // operator decides what's tracked. JSON-stringifiable.
27
- * allowedOrigins: ["https://app.example.com"],
28
- * csp: "default-src 'self'",
29
- * auditMode: b.audit.getMode(),
30
- * vaultMode: b.vault.getMode(),
31
- * dbAtRest: b.db.getAtRestMode(),
32
- * });
33
- * // → { signed: true, drifted: false, previousAt: 1730000000000 }
24
+ * The signed sidecar uses `b.auditSign` — same SLH-DSA-SHAKE-256f
25
+ * keypair the audit chain anchors on. An attacker who flips a config
26
+ * value would also need to forge the signing key to update the
27
+ * sidecar cleanly; otherwise next-boot verify catches the tamper.
34
28
  *
35
- * The signed sidecar uses b.auditSign same SLH-DSA-SHAKE-256f keypair
36
- * the audit chain anchors on. An attacker who flips a config value
37
- * would also need to forge the signing key to update the sidecar
38
- * cleanly; otherwise next-boot verify catches the tamper.
29
+ * `b.configDrift.verifyVendorIntegrity` is a sibling primitive that
30
+ * re-hashes every file under `lib/vendor/` against the manifest's
31
+ * sha256 digests at boot catches a half-applied vendor refresh,
32
+ * a corrupted install, or a vendored cjs modified without a manifest
33
+ * update.
39
34
  *
40
- * Validation:
41
- * - create() opts: throw at boot on bad shape
42
- * - checkpoint() snapshot: must be a JSON-serialisable object
43
- * - sidecar verify failure (tampered, key rotated, missing pubkey)
44
- * surfaces as `config.baseline.tamper` audit event AND the call
45
- * returns { tamper: true } so the operator can decide whether
46
- * to refuse boot
35
+ * @card
36
+ * Monitor + alert when runtime config diverges from a declared baseline.
47
37
  */
48
38
  var fs = require("node:fs");
49
39
  var path = require("node:path");
@@ -96,6 +86,52 @@ function _diffShallow(prev, next) {
96
86
  return { changed: changed, added: added, removed: removed };
97
87
  }
98
88
 
89
+ /**
90
+ * @primitive b.configDrift.create
91
+ * @signature b.configDrift.create(opts)
92
+ * @since 0.6.0
93
+ * @status stable
94
+ * @related b.configDrift.verifyVendorIntegrity, b.auditSign.sign, b.audit.safeEmit
95
+ *
96
+ * Build a per-baseline drift detector bound to a `dataDir`. Returns a
97
+ * handle exposing `.checkpoint(snapshot)` (write or compare against the
98
+ * signed sidecar), `.read()` (load + verify the sidecar without
99
+ * mutating it), and `.sidecarPath` (absolute path of the sidecar file).
100
+ *
101
+ * Drift on a key listed in `opts.criticalKeys` surfaces as
102
+ * `config.drift.detected` with `severity: "high"` and outcome
103
+ * `failure`; drift in any other key surfaces as `severity: "low"` and
104
+ * outcome `success`. When `criticalKeys` is omitted every key is
105
+ * treated as high severity. Keys listed in `opts.ignoreKeys` are
106
+ * captured in the snapshot but never raise a drift event.
107
+ *
108
+ * @opts
109
+ * dataDir: string, // directory holding the baseline sidecar (required)
110
+ * audit: object, // b.audit instance; pass false to disable
111
+ * baseline: string, // baseline name — sidecar is `config-baseline-<name>.sig`
112
+ * criticalKeys: string[], // keys whose drift raises severity to "high"
113
+ * ignoreKeys: string[], // keys excluded from drift detection
114
+ *
115
+ * @example
116
+ * var fakeAudit = { safeEmit: function () {} };
117
+ * var detector = b.configDrift.create({
118
+ * dataDir: "/tmp/blamejs-drift-demo",
119
+ * audit: fakeAudit,
120
+ * criticalKeys: ["allowedOrigins", "csp"],
121
+ * ignoreKeys: ["bootCount"],
122
+ * });
123
+ * detector.sidecarPath;
124
+ * // → "/tmp/blamejs-drift-demo/config-baseline.sig"
125
+ *
126
+ * var first = await detector.checkpoint({
127
+ * allowedOrigins: ["https://app.example.com"],
128
+ * csp: "default-src 'self'",
129
+ * bootCount: 1,
130
+ * });
131
+ * first.signed; // → true
132
+ * first.drifted; // → false
133
+ * first.previousAt; // → null
134
+ */
99
135
  function create(opts) {
100
136
  opts = opts || {};
101
137
  validateOpts(opts, [
@@ -292,13 +328,42 @@ function create(opts) {
292
328
  };
293
329
  }
294
330
 
295
- // verifyVendorIntegrity — at-boot integrity check over `lib/vendor/*`.
296
- // MANIFEST.json carries a sha256 digest per bundled file; we re-hash
297
- // each one and refuse on mismatch. Catches a half-applied vendor
298
- // refresh, a corrupted install, or an attacker who modified a
299
- // vendored cjs without updating the manifest. Returns
300
- // `{ ok, mismatches: [{ path, expected, actual }] }` and emits
301
- // `vendor.integrity.{verified,tampered}` audit on each call.
331
+ /**
332
+ * @primitive b.configDrift.verifyVendorIntegrity
333
+ * @signature b.configDrift.verifyVendorIntegrity(opts)
334
+ * @since 0.7.39
335
+ * @status stable
336
+ * @related b.configDrift.create, b.audit.safeEmit
337
+ *
338
+ * At-boot integrity check over `lib/vendor/*`. MANIFEST.json carries a
339
+ * sha256 digest per bundled file; the call re-hashes each one and
340
+ * surfaces mismatches without throwing. Returns
341
+ * `{ ok, checkedCount, mismatches }` and emits
342
+ * `vendor.integrity.verified` (success) or `vendor.integrity.tampered`
343
+ * (failure) on every invocation so a corrupted install lands in the
344
+ * audit chain at boot, not later.
345
+ *
346
+ * Throws `ConfigDriftError("VENDOR_MANIFEST_MISSING")` when MANIFEST.json
347
+ * is absent and `ConfigDriftError("VENDOR_MANIFEST_SHAPE")` when its
348
+ * top-level `packages` map is missing — operators see a hard fail
349
+ * instead of a silent zero-files-checked pass.
350
+ *
351
+ * @opts
352
+ * libVendorDir: string, // absolute path to lib/vendor (defaults to cwd-relative)
353
+ * manifestPath: string, // absolute path to MANIFEST.json (defaults under libVendorDir)
354
+ *
355
+ * @example
356
+ * try {
357
+ * var result = b.configDrift.verifyVendorIntegrity({
358
+ * libVendorDir: "/srv/app/lib/vendor",
359
+ * });
360
+ * result.ok; // → true
361
+ * result.checkedCount; // → 42
362
+ * result.mismatches; // → []
363
+ * } catch (e) {
364
+ * e.code; // → "VENDOR_MANIFEST_MISSING"
365
+ * }
366
+ */
302
367
  function verifyVendorIntegrity(opts) {
303
368
  opts = opts || {};
304
369
  var libVendorDir = opts.libVendorDir || path.join(process.cwd(), "lib", "vendor");
package/lib/config.js CHANGED
@@ -1,35 +1,27 @@
1
1
  "use strict";
2
2
  /**
3
- * config — schema-validated environment configuration.
3
+ * @module b.config
4
+ * @nav Tools
5
+ * @title Config
4
6
  *
5
- * Operators read process.env throughout their app code. A typo in the
6
- * key name OR a value in the wrong shape (port="abc", flag="yas")
7
- * surfaces three days later as a mysterious 500. This primitive validates
8
- * env at boot via b.safeSchema so the app refuses to start with broken
9
- * config.
7
+ * @intro
8
+ * Schema-validated environment configuration. Operators read
9
+ * `process.env` throughout their app; a typo in the key name OR a
10
+ * value in the wrong shape (`port="abc"`, `flag="yas"`) surfaces
11
+ * three days later as a mysterious 500. `b.config.create` validates
12
+ * env at boot through `b.safeSchema` so the app refuses to start
13
+ * with broken config — the throw happens at `create()` time, not
14
+ * at the first request that touches the broken value.
10
15
  *
11
- * var config = b.config.create({
12
- * schema: b.safeSchema.object({
13
- * NODE_ENV: b.safeSchema.enum_(["development", "test", "production"]),
14
- * PORT: b.config.coerce.number().default(3000),
15
- * LOG_LEVEL: b.safeSchema.enum_(["debug", "info", "warn", "error"]).default("info"),
16
- * SESSION_SECRET: b.safeSchema.string().min(32),
17
- * DATABASE_URL: b.safeSchema.string().url(),
18
- * REDIS_URL: b.safeSchema.string().url().optional(),
19
- * FEATURE_X: b.config.coerce.boolean().default(false),
20
- * }),
21
- * // env: process.env (default) — operators in tests pass a fake object
22
- * // redactKeys: ["SESSION_SECRET", "DATABASE_URL"] — never log these
23
- * });
24
- *
25
- * config.value → the validated, typed value object
26
- * config.value.PORT → 3000 (Number, not "3000")
27
- * config.boot() → throws ConfigError on validation failure;
28
- * returns the validated value otherwise
16
+ * `b.config.coerce.number()` and `b.config.coerce.boolean()` wrap
17
+ * schema leaves with the env-friendly preprocessors most operators
18
+ * want (env values are always strings at the source). `loadDbBacked`
19
+ * composes `create` with periodic DB-row polling so a row update in
20
+ * `_blamejs_config_overrides` surfaces without restart, and falls
21
+ * back to the last-good value on validation failure.
29
22
  *
30
- * The factory immediately runs validation — the throw at create() time
31
- * is intentional. Operators want config errors at app boot, not at
32
- * the first request that touches the broken value.
23
+ * @card
24
+ * Schema-validated environment configuration.
33
25
  */
34
26
  var safeSchema = require("./safe-schema");
35
27
  var validateOpts = require("./validate-opts");
@@ -64,6 +56,41 @@ var coerce = {
64
56
  },
65
57
  };
66
58
 
59
+ /**
60
+ * @primitive b.config.create
61
+ * @signature b.config.create(opts)
62
+ * @since 0.8.0
63
+ * @status stable
64
+ * @related b.config.loadDbBacked, b.safeSchema.object
65
+ *
66
+ * Validate env against a `b.safeSchema` shape and return a frozen
67
+ * config handle (`value` / `get` / `has` / `redacted` / `subscribe` /
68
+ * `reload`). Throws `ConfigError` synchronously when validation fails
69
+ * — the operator sees broken config at boot rather than at the first
70
+ * request that touches the value. The handle's `reload(overlay)`
71
+ * applies a new env-shaped overlay on top of the validated baseline,
72
+ * notifies subscribers on success, and falls back to the prior value
73
+ * on failure.
74
+ *
75
+ * @opts
76
+ * schema: b.safeSchema instance (required; built via b.safeSchema.object({...})),
77
+ * env: object (env bag; default process.env),
78
+ * redactKeys: Array<string> (keys masked by `.redacted()` for log output),
79
+ *
80
+ * @example
81
+ * var s = b.safeSchema;
82
+ * var cfg = b.config.create({
83
+ * schema: s.object({
84
+ * NODE_ENV: s.enum_(["development", "test", "production"]),
85
+ * PORT: b.config.coerce.number().default(3000),
86
+ * }),
87
+ * env: { NODE_ENV: "production", PORT: "8080" },
88
+ * redactKeys: [],
89
+ * });
90
+ * cfg.value.NODE_ENV; // → "production"
91
+ * cfg.value.PORT; // → 8080 (Number, not "8080")
92
+ * cfg.has("PORT"); // → true
93
+ */
67
94
  function create(opts) {
68
95
  if (!opts || typeof opts !== "object") {
69
96
  throw new ConfigError("config/bad-opts",
@@ -168,19 +195,46 @@ function create(opts) {
168
195
  };
169
196
  }
170
197
 
171
- // loadDbBacked — composes b.config.create with a periodic DB-row
172
- // fetch. Operators put canonical config values in
173
- // `_blamejs_config_overrides(key TEXT PRIMARY KEY, value TEXT)`;
174
- // this helper polls every `intervalMs`, applies the rows as an
175
- // overlay via cfg.reload(), and re-validates. Reload failures emit
176
- // a `config.reload.failed` audit row but do NOT clobber the
177
- // previous value (the running app stays on the last-good config).
178
- //
179
- // var cfg = await b.config.loadDbBacked({
180
- // schema: mySchema,
181
- // fetchRows: async () => await db.query("SELECT key, value FROM _blamejs_config_overrides"),
182
- // intervalMs: C.TIME.minutes(1),
183
- // });
198
+ /**
199
+ * @primitive b.config.loadDbBacked
200
+ * @signature b.config.loadDbBacked(opts)
201
+ * @since 0.8.0
202
+ * @status stable
203
+ * @related b.config.create, b.safeAsync.repeating
204
+ *
205
+ * Compose `b.config.create` with a periodic DB-row fetch. Operators
206
+ * keep canonical config values in
207
+ * `_blamejs_config_overrides(key TEXT PRIMARY KEY, value TEXT)`; this
208
+ * helper polls every `intervalMs`, applies the rows as an overlay
209
+ * via the underlying handle's `reload`, and re-validates. Reload
210
+ * failures emit a `config.reload.failed` audit row but DO NOT
211
+ * clobber the previous value — the running app stays on the
212
+ * last-good config. The returned handle is the same shape as
213
+ * `create()` plus a `.stop()` method that halts the poller.
214
+ *
215
+ * @opts
216
+ * schema: b.safeSchema instance (required),
217
+ * env: object (env baseline; default process.env),
218
+ * redactKeys: Array<string>,
219
+ * fetchRows: async () => Array<{ key: string, value: string }> (required),
220
+ * intervalMs: number (positive finite poll interval),
221
+ * audit: boolean (default true; reserved for future per-poll audit),
222
+ *
223
+ * @example
224
+ * var s = b.safeSchema;
225
+ * var cfg = b.config.loadDbBacked({
226
+ * schema: s.object({
227
+ * FEATURE_X: b.config.coerce.boolean().default(false),
228
+ * }),
229
+ * env: { FEATURE_X: "false" },
230
+ * fetchRows: async function () {
231
+ * return [{ key: "FEATURE_X", value: "true" }];
232
+ * },
233
+ * intervalMs: 60 * 1000,
234
+ * });
235
+ * cfg.value.FEATURE_X; // → false (until first poll tick lands)
236
+ * cfg.stop(); // halt the poller on shutdown
237
+ */
184
238
  function loadDbBacked(opts) {
185
239
  opts = opts || {};
186
240
  validateOpts(opts, ["schema", "env", "redactKeys", "fetchRows", "intervalMs", "audit"],
package/lib/consent.js CHANGED
@@ -1,23 +1,39 @@
1
1
  "use strict";
2
2
  /**
3
- * Consent log — GDPR Art. 6/7 lawful-basis tracking with hash chain.
4
- *
5
- * consent_log is baked into db.js's schema runner alongside audit_log.
6
- * Same tamper-evidence design: per-row SHA3-512 hash chain, append-only,
7
- * verified at boot.
8
- *
9
- * Lawful-basis consistency: any audit-recorded operation declared under
10
- * `lawfulBasis: 'consent'` should reference a current consent_log entry.
11
- * The framework records consent grants/withdrawals here; enforcement of
12
- * consent before processing is the app's responsibility — query
13
- * `consent.isActive(subjectId, purpose)` at the trust boundary.
14
- *
15
- * Public API:
16
- * consent.grant({ subjectId, purpose, lawfulBasis, scope?, channel, evidenceRef? })
17
- * consent.withdraw({ subjectId, purpose, reason? })
18
- * consent.isGranted({ subjectId, purpose }) boolean
19
- * consent.history(subjectId) array of consent_log rows (decrypted)
20
- * consent.verify() → { ok, rowsVerified, breakAt? }
3
+ * @module b.consent
4
+ * @nav Identity
5
+ * @title Consent
6
+ *
7
+ * @intro
8
+ * Consent-record chain — every grant / withdrawal / expiry / supersede
9
+ * for a (subjectId, purpose) pair lands in `consent_log` as one
10
+ * append-only, hash-chained row. Same tamper-evidence design as
11
+ * `audit_log`: per-row SHA3-512 hash chain over the sealed payload,
12
+ * verified at boot, refuse-to-boot on a break.
13
+ *
14
+ * GDPR Art. 7 demands controllers be able to demonstrate the data
15
+ * subject consented; CCPA / CPRA Title 1.81.5 requires evidence the
16
+ * consumer exercised opt-out. consent_log carries `subjectId` (sealed)
17
+ * + `purpose` + `lawfulBasis` + `channel` + operator-supplied
18
+ * `evidenceRef` so a regulator request resolves to a specific row,
19
+ * tied to the audit chain via shared chain-writer primitives.
20
+ *
21
+ * Lawful-basis vocabulary tracks the GDPR Art. 6(1) enumeration:
22
+ * `consent`, `contract`, `legal_obligation`, `vital_interests`,
23
+ * `public_task`, `legitimate_interests`. Any audit event declaring
24
+ * `lawfulBasis: 'consent'` should reference a current consent_log
25
+ * entry — the framework records the grants and withdrawals here;
26
+ * enforcement at the trust boundary is the app's call (typical shape:
27
+ * `if (!b.consent.isGranted({ subjectId, purpose })) return 403`).
28
+ *
29
+ * Cluster mode keeps `_blamejs_consent_tip` current with a fenced
30
+ * `INSERT … ON CONFLICT DO UPDATE … WHERE fencingToken <= EXCLUDED`
31
+ * so a partitioned old leader cannot rewrite the tip even if its
32
+ * application-layer leader gate let the call through. Followers
33
+ * refuse `grant` / `withdraw` with `NotLeaderError`.
34
+ *
35
+ * @card
36
+ * Consent-record chain — every grant / withdrawal / expiry / supersede for a (subjectId, purpose) pair lands in `consent_log` as one append-only, hash-chained row.
21
37
  */
22
38
  var auditChain = require("./audit-chain");
23
39
  var cluster = require("./cluster");
@@ -64,6 +80,40 @@ var _chainWriter = chainWriter.create({
64
80
 
65
81
  // ---- Public API ----
66
82
 
83
+ /**
84
+ * @primitive b.consent.grant
85
+ * @signature b.consent.grant(opts)
86
+ * @since 0.1.0
87
+ * @status stable
88
+ * @compliance gdpr, ccpa, hipaa
89
+ * @related b.consent.withdraw, b.consent.isGranted, b.consent.history
90
+ *
91
+ * Append a "granted" row to consent_log for a (subjectId, purpose) pair.
92
+ * Refuses on a follower (cluster mode). Lawful-basis must come from the
93
+ * GDPR Art. 6(1) enumeration; an unknown value throws synchronously
94
+ * before touching the chain.
95
+ *
96
+ * @opts
97
+ * subjectId: string, // sealed at rest
98
+ * purpose: string, // e.g. "marketing"
99
+ * lawfulBasis: "consent" | "contract" | "legal_obligation"
100
+ * | "vital_interests" | "public_task"
101
+ * | "legitimate_interests",
102
+ * scope: object, // optional, JSON-serialized
103
+ * channel: string, // "web-banner" / "api" / ...
104
+ * evidenceRef: string, // optional pointer to UI snapshot
105
+ *
106
+ * @example
107
+ * await b.consent.grant({
108
+ * subjectId: "u-42",
109
+ * purpose: "marketing",
110
+ * lawfulBasis: "consent",
111
+ * scope: { channels: ["email", "sms"] },
112
+ * channel: "web-banner-v3",
113
+ * evidenceRef: "snapshot-2026-05-09T14:00:00Z",
114
+ * });
115
+ * // → { _id, monotonicCounter, rowHash, prevHash, ... }
116
+ */
67
117
  function grant(opts) {
68
118
  cluster.requireLeader();
69
119
  if (!opts || !opts.subjectId || !opts.purpose || !opts.lawfulBasis || !opts.channel) {
@@ -83,6 +133,34 @@ function grant(opts) {
83
133
  });
84
134
  }
85
135
 
136
+ /**
137
+ * @primitive b.consent.withdraw
138
+ * @signature b.consent.withdraw(opts)
139
+ * @since 0.1.0
140
+ * @status stable
141
+ * @compliance gdpr, ccpa
142
+ * @related b.consent.grant, b.consent.isGranted
143
+ *
144
+ * Append a "withdrawn" row to consent_log. After this lands,
145
+ * `isGranted` returns `false` for the same (subjectId, purpose). Pair
146
+ * with a downstream sweep over data-classes that depended on the
147
+ * lawful basis (typical pattern: cascade into `b.retention` or
148
+ * `b.subject.erase`).
149
+ *
150
+ * @opts
151
+ * subjectId: string,
152
+ * purpose: string,
153
+ * reason: string, // optional, recorded as evidenceRef
154
+ * channel: string, // optional, defaults to "api"
155
+ *
156
+ * @example
157
+ * await b.consent.withdraw({
158
+ * subjectId: "u-42",
159
+ * purpose: "marketing",
160
+ * reason: "user-self-service-portal",
161
+ * });
162
+ * // → { _id, monotonicCounter, rowHash, ... }
163
+ */
86
164
  function withdraw(opts) {
87
165
  cluster.requireLeader();
88
166
  if (!opts || !opts.subjectId || !opts.purpose) {
@@ -99,6 +177,30 @@ function withdraw(opts) {
99
177
  });
100
178
  }
101
179
 
180
+ /**
181
+ * @primitive b.consent.isGranted
182
+ * @signature b.consent.isGranted(opts)
183
+ * @since 0.1.0
184
+ * @status stable
185
+ * @compliance gdpr, ccpa
186
+ * @related b.consent.grant, b.consent.withdraw, b.consent.history
187
+ *
188
+ * Returns `true` when the most recent consent_log row for the
189
+ * (subjectId, purpose) pair has action `granted`. Lookups go through
190
+ * the derived `subjectIdHash` so the sealed `subjectId` column never
191
+ * needs to be unsealed for the query. Safe on followers (read-only).
192
+ *
193
+ * @opts
194
+ * subjectId: string,
195
+ * purpose: string,
196
+ *
197
+ * @example
198
+ * if (!b.consent.isGranted({ subjectId: "u-42", purpose: "marketing" })) {
199
+ * res.statusCode = 403;
200
+ * return res.end("consent required");
201
+ * }
202
+ * // → true / false
203
+ */
102
204
  function isGranted(opts) {
103
205
  if (!opts || !opts.subjectId || !opts.purpose) {
104
206
  throw new Error("consent.isGranted requires { subjectId, purpose }");
@@ -117,6 +219,26 @@ function isGranted(opts) {
117
219
  return row.action === "granted";
118
220
  }
119
221
 
222
+ /**
223
+ * @primitive b.consent.history
224
+ * @signature b.consent.history(subjectId)
225
+ * @since 0.1.0
226
+ * @status stable
227
+ * @compliance gdpr, ccpa
228
+ * @related b.consent.grant, b.consent.withdraw, b.subject.export
229
+ *
230
+ * Returns every consent_log row for `subjectId`, oldest first, decrypted
231
+ * by the framework's row reader. Composes into `b.subject.export` for
232
+ * GDPR Art. 15 / CCPA §1798.110 right-of-access responses without the
233
+ * caller having to walk the chain manually.
234
+ *
235
+ * @example
236
+ * var rows = b.consent.history("u-42");
237
+ * rows.forEach(function (r) {
238
+ * console.log(r.recordedAt, r.purpose, r.action, r.lawfulBasis);
239
+ * });
240
+ * // → [{ recordedAt, purpose, action, lawfulBasis, channel, ... }]
241
+ */
120
242
  function history(subjectId) {
121
243
  if (!subjectId) throw new Error("consent.history requires a subjectId");
122
244
  var hash = db().hashFor("consent_log", "subjectId", subjectId);
@@ -130,6 +252,31 @@ function history(subjectId) {
130
252
  return rows;
131
253
  }
132
254
 
255
+ /**
256
+ * @primitive b.consent.verify
257
+ * @signature b.consent.verify(opts)
258
+ * @since 0.1.0
259
+ * @status stable
260
+ * @compliance gdpr, soc2
261
+ * @related b.audit.verify, b.consent.grant
262
+ *
263
+ * Verify the consent_log hash chain end-to-end. Recomputes each row's
264
+ * `rowHash` from the sealed-form columns + nonce + `prevHash`, walking
265
+ * from genesis to tip. Returns `{ ok, rowsVerified, breakAt? }` — a
266
+ * regulator-ready integrity check that auditors can run without
267
+ * holding the vault key.
268
+ *
269
+ * @opts
270
+ * from: number, // optional monotonicCounter floor
271
+ * to: number, // optional monotonicCounter ceiling
272
+ *
273
+ * @example
274
+ * var report = await b.consent.verify();
275
+ * if (!report.ok) {
276
+ * console.error("consent chain break at row", report.breakAt);
277
+ * }
278
+ * // → { ok: true, rowsVerified: 1024 }
279
+ */
133
280
  async function verify(opts) {
134
281
  return await auditChain.verifyChain(
135
282
  function (sql, params) { return clusterStorage.executeAll(sql, params || []); },
package/lib/constants.js CHANGED
@@ -171,6 +171,7 @@ var PQC_GROUPS = Object.freeze({
171
171
  var TLS_GROUP_PREFERENCE = Object.freeze([
172
172
  "SecP384r1MLKEM1024",
173
173
  "X25519MLKEM768",
174
+ "SecP256r1MLKEM768",
174
175
  ]);
175
176
 
176
177
  var TLS_GROUP_CURVE_STR = TLS_GROUP_PREFERENCE.join(":");