@blamejs/core 0.8.43 → 0.8.50

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/forms.js CHANGED
@@ -1,39 +1,36 @@
1
1
  "use strict";
2
2
  /**
3
- * Forms — CSRF tokens, HTML rendering, server-side validation.
3
+ * @module b.forms
4
+ * @nav HTTP
5
+ * @title Forms
4
6
  *
5
- * Three concerns the framework owns at the form layer:
7
+ * @intro
8
+ * HTML form rendering with CSRF token injection, accessible labels,
9
+ * field-type dispatch, and shared-spec server-side validation.
6
10
  *
7
- * 1. CSRF tokens. Generation is a 32-byte random hex string;
8
- * verification is constant-time. The middleware in
9
- * lib/middleware/csrf-protect.js does the actual gating
10
- * this module provides the primitives the operator (or
11
- * template) calls to issue a token and embed it.
11
+ * `b.forms.render(spec)` emits a complete `<form>` element with
12
+ * auto-escaped attributes, a hidden CSRF input, and per-field
13
+ * markup for text / email / password / number / checkbox / radio /
14
+ * textarea / select / hidden / submit. Every attribute value is
15
+ * forced through `escapeAttribute` so a hostile field name or value
16
+ * can't break out of the double-quoted attribute context.
12
17
  *
13
- * 2. Form HTML rendering. forms.render({ action, fields, csrfToken })
14
- * produces a complete <form> element with auto-escaped attributes,
15
- * a hidden CSRF input, and field-type dispatch (text / email /
16
- * password / number / checkbox / radio / textarea / select /
17
- * hidden / submit). All values pass through escapeAttribute so
18
- * the form can't be hijacked by user-supplied attribute payloads.
18
+ * `b.forms.validate(spec, body)` walks the same field spec the
19
+ * renderer accepts and returns `{ valid, errors, values }` —
20
+ * coerced types, required-field checks, length bounds, regex
21
+ * pattern, enum membership. Sharing the spec is the point: the
22
+ * operator's "this is what the form looks like" and "this is what
23
+ * the form expects" stay in lock-step, eliminating the drift class
24
+ * where a field gets added to the renderer but not the validator.
19
25
  *
20
- * 3. Server-side validation. forms.validate(spec, body) walks the
21
- * same field spec the renderer accepts and returns
22
- * { valid, errors, values } coerced types, required-field
23
- * checks, length bounds, regex pattern, enum membership.
26
+ * CSRF tokens are 32-byte hex strings from
27
+ * `b.crypto.generateToken`; `verifyCsrfToken` is constant-time.
28
+ * The middleware in `b.middleware.csrfProtect` does the actual
29
+ * request-time gating this module supplies the issue / verify
30
+ * primitives.
24
31
  *
25
- * The renderer + validator share their field spec on purpose: an
26
- * operator's "this is what the form looks like" is also "this is
27
- * what the form expects." A change to the spec adjusts both.
28
- *
29
- * Public API:
30
- *
31
- * forms.generateCsrfToken() → "<64 hex chars>"
32
- * forms.verifyCsrfToken(submitted, expected) → boolean (timing-safe)
33
- * forms.render(spec) → string (complete <form>…</form>)
34
- * forms.validate(spec, body) → { valid, errors, values }
35
- * forms.escapeAttribute(value) → string (double-quoted-attr context)
36
- * forms.escapeHtml = template.escapeHtml (re-export for convenience)
32
+ * @card
33
+ * HTML form rendering with CSRF token injection, accessible labels, field-type dispatch, and shared-spec server-side validation.
37
34
  */
38
35
  var C = require("./constants");
39
36
  var { generateToken, timingSafeEqual } = require("./crypto");
@@ -55,10 +52,44 @@ var MAX_EMAIL_LENGTH = 254;
55
52
  // (and what most servers / proxies enforce) is 8 KiB.
56
53
  var MAX_URL_LENGTH = C.BYTES.kib(8);
57
54
 
55
+ /**
56
+ * @primitive b.forms.generateCsrfToken
57
+ * @signature b.forms.generateCsrfToken()
58
+ * @since 0.1.0
59
+ * @status stable
60
+ * @related b.forms.verifyCsrfToken, b.middleware.csrfProtect
61
+ *
62
+ * Returns a 32-byte (64 hex char) random token suitable for embedding
63
+ * in a hidden form field. Entropy comes from `b.crypto.generateToken`,
64
+ * which routes through Node's `crypto.randomBytes`. The token is
65
+ * opaque to the framework — operators store it in the session and
66
+ * compare via `verifyCsrfToken` on submit.
67
+ *
68
+ * @example
69
+ * var token = b.forms.generateCsrfToken();
70
+ * // → "8f3a1c4b...e7d9" (64 hex chars)
71
+ */
58
72
  function generateCsrfToken() {
59
73
  return generateToken(CSRF_TOKEN_BYTES);
60
74
  }
61
75
 
76
+ /**
77
+ * @primitive b.forms.verifyCsrfToken
78
+ * @signature b.forms.verifyCsrfToken(submitted, expected)
79
+ * @since 0.1.0
80
+ * @status stable
81
+ * @related b.forms.generateCsrfToken, b.middleware.csrfProtect
82
+ *
83
+ * Constant-time comparison of a submitted token against the expected
84
+ * value. Returns `false` for any non-string input, mismatched length,
85
+ * or empty submitted token — never throws. Routes through
86
+ * `b.crypto.timingSafeEqual` so an attacker can't probe character
87
+ * positions via response-time differences.
88
+ *
89
+ * @example
90
+ * var ok = b.forms.verifyCsrfToken(req.body.csrf, req.session.csrf);
91
+ * // → true (when both strings are non-empty and byte-identical)
92
+ */
62
93
  function verifyCsrfToken(submitted, expected) {
63
94
  if (typeof submitted !== "string" || typeof expected !== "string") return false;
64
95
  if (submitted.length === 0 || submitted.length !== expected.length) return false;
@@ -82,6 +113,25 @@ var ATTR_ESCAPE_MAP = {
82
113
  };
83
114
  var ATTR_ESCAPE_RE = /[&<>"'`=]/g;
84
115
 
116
+ /**
117
+ * @primitive b.forms.escapeAttribute
118
+ * @signature b.forms.escapeAttribute(value)
119
+ * @since 0.1.0
120
+ * @status stable
121
+ * @related b.forms.render, b.template.escapeHtml
122
+ *
123
+ * Escapes a value for safe interpolation into a double-quoted HTML
124
+ * attribute context. Stricter than `b.template.escapeHtml`: also
125
+ * escapes backtick (some browsers parse `` ` `` as an attribute
126
+ * delimiter under quirks mode) and `=` (defense-in-depth for any
127
+ * unquoted-attribute slips). `null` / `undefined` become the empty
128
+ * string. Used internally by `b.forms.render` for every attribute
129
+ * value; exported for operators rendering their own form fragments.
130
+ *
131
+ * @example
132
+ * var safe = b.forms.escapeAttribute('a"b<c>d');
133
+ * // → "a&quot;b&lt;c&gt;d"
134
+ */
85
135
  function escapeAttribute(value) {
86
136
  if (value === null || value === undefined) return "";
87
137
  var s = typeof value === "string" ? value : String(value);
@@ -214,6 +264,34 @@ function _renderField(field) {
214
264
  return control;
215
265
  }
216
266
 
267
+ /**
268
+ * @primitive b.forms.render
269
+ * @signature b.forms.render(spec)
270
+ * @since 0.1.0
271
+ * @status stable
272
+ * @related b.forms.validate, b.forms.generateCsrfToken, b.template.escapeHtml
273
+ *
274
+ * Renders a complete `<form>` element from a typed spec — method,
275
+ * action, fields, optional CSRF token. Each field's `type` selects
276
+ * the input widget (text / email / password / number / checkbox /
277
+ * radio / textarea / select / hidden / submit). All attribute values
278
+ * pass through `escapeAttribute`; `spec.csrfToken` (if present) is
279
+ * embedded as a hidden `_csrf` input. Throws when `spec.action` is
280
+ * missing / empty or `spec.fields` isn't an array.
281
+ *
282
+ * @example
283
+ * var html = b.forms.render({
284
+ * action: "/login",
285
+ * method: "POST",
286
+ * csrfToken: "8f3a1c4b...e7d9",
287
+ * fields: [
288
+ * { type: "email", name: "email", label: "Email", required: true },
289
+ * { type: "password", name: "password", label: "Password", required: true },
290
+ * { type: "submit", name: "submit", value: "Sign in" },
291
+ * ],
292
+ * });
293
+ * // → "<form method=\"POST\" action=\"/login\">...<input type=\"hidden\" name=\"_csrf\" .../></form>"
294
+ */
217
295
  function render(spec) {
218
296
  if (!spec || typeof spec.action !== "string" || spec.action.length === 0) {
219
297
  throw new Error("forms.render: spec.action is required");
@@ -296,6 +374,31 @@ function _isEmpty(v) {
296
374
  return v === undefined || v === null || v === "";
297
375
  }
298
376
 
377
+ /**
378
+ * @primitive b.forms.validate
379
+ * @signature b.forms.validate(spec, body)
380
+ * @since 0.1.0
381
+ * @status stable
382
+ * @related b.forms.render, b.safeSchema
383
+ *
384
+ * Walks the same spec the renderer accepts and validates a submitted
385
+ * body. Per field: required-field check, type coercion (string /
386
+ * number / boolean / email / url), `minLength` / `maxLength` bounds,
387
+ * regex `pattern`, `enum` membership. Returns
388
+ * `{ valid: boolean, errors: { field: msg, ... }, values: { ... } }`.
389
+ * The `values` object holds coerced values keyed by field name —
390
+ * route handlers consume `result.values` directly without re-parsing.
391
+ *
392
+ * @example
393
+ * var result = b.forms.validate(
394
+ * { fields: [
395
+ * { type: "email", name: "email", required: true },
396
+ * { type: "number", name: "age", minLength: 1 },
397
+ * ] },
398
+ * { email: "ada@example.com", age: "37" }
399
+ * );
400
+ * // → { valid: true, errors: {}, values: { email: "ada@example.com", age: 37 } }
401
+ */
299
402
  function validate(spec, body) {
300
403
  if (!spec || !Array.isArray(spec.fields)) {
301
404
  throw new Error("forms.validate: spec.fields must be an array");
@@ -407,6 +407,152 @@ var A2aError = defineClass("A2aError", { alwaysPermane
407
407
  // missing or malformed router-token, replay (nonce already seen),
408
408
  // unauthorized SDL probe. Permanent.
409
409
  var GraphqlFederationError = defineClass("GraphqlFederationError", { alwaysPermanent: true });
410
+ // Fda21Cfr11Error covers FDA 21 CFR Part 11 §11.10(e) audit-content
411
+ // shape + §11.50/§11.70 electronic-signature shape violations: missing
412
+ // printedName / dateTimeUtc / signatureMeaning / predicateRule / signed
413
+ // record bind, before/after pair missing on a GxP audit row, signature-
414
+ // algorithm allowlist drift, posture interceptor refusal. Permanent —
415
+ // every case is operator-supplied data shape, not transient.
416
+ var Fda21Cfr11Error = defineClass("Fda21Cfr11Error", { alwaysPermanent: true });
417
+ // AuditDailyReviewError covers PCI DSS 4.0 Req 10.4.1.1 daily-review
418
+ // misconfiguration: bad cron / lookback / severity threshold, missing
419
+ // notify callback under threshold-bearing posture, audit-source not
420
+ // queryable. Permanent — config-time errors.
421
+ var AuditDailyReviewError = defineClass("AuditDailyReviewError", { alwaysPermanent: true });
422
+ // AuditSegregationError covers SOX §404 / SOC 2 CC1.3 actor-binding
423
+ // violations: bound-actor mismatch on emit, missing db-role context,
424
+ // trigger-installation failure when sox-404 / soc2 posture demands it.
425
+ // Permanent — operator-misconfig or in-flight identity mismatch.
426
+ var AuditSegregationError = defineClass("AuditSegregationError", { alwaysPermanent: true });
427
+ // DdlChangeControlError covers SOX §404 / PCI-DSS DDL change-control
428
+ // violations: insufficient approvers, approval window violation,
429
+ // signature-mismatch on apply, duplicate approval, application of an
430
+ // already-applied or rejected change. Permanent.
431
+ var DdlChangeControlError = defineClass("DdlChangeControlError", { alwaysPermanent: true });
432
+ // LegalHoldError covers subject-level legal-hold registry violations:
433
+ // missing subjectId, malformed reason/citation, duplicate placement,
434
+ // release-without-placement, bad opts. Permanent — config / API
435
+ // shape errors, not transient.
436
+ var LegalHoldError = defineClass("LegalHoldError", { alwaysPermanent: true });
437
+ // WormViolationError covers operator-declared WORM (write-once-read-
438
+ // many) trigger-installation failures and posture-asserted boot
439
+ // gates: declareWorm called on a non-existent table, table requires
440
+ // WORM under sec-17a-4 / finra-4511 / fda-21cfr11 but none declared,
441
+ // operator attempted to drop the WORM trigger outside a sanctioned
442
+ // retention.purge flow. Permanent.
443
+ var WormViolationError = defineClass("WormViolationError", { alwaysPermanent: true });
444
+ // SandboxError covers operator-supplied transform-source isolation
445
+ // failures: bad opts at create() (non-string source, non-finite
446
+ // timeoutMs / maxBytes, allowed-list contains a non-allowlisted
447
+ // global), worker-thread spawn failure, timeout exceeded, peak-bytes
448
+ // overrun, non-allowlisted-global access, output-shape-too-large,
449
+ // runtime exceptions inside the transform. Permanent — every case is
450
+ // either operator-misconfig or a transform that the host should
451
+ // refuse rather than retry. Operator decides at the call site whether
452
+ // to surface the refusal as a 4xx or to fall back to a default value.
453
+ var SandboxError = defineClass("SandboxError", { alwaysPermanent: true });
454
+ // DlpError — outbound DLP scanner refusal raised by
455
+ // b.redact.installOutboundDlp's interceptors when the classifier verdict
456
+ // is "refuse". Permanent; the request body must be operator-corrected
457
+ // before re-attempt rather than retried as-is.
458
+ var DlpError = defineClass("DlpError", { alwaysPermanent: true });
459
+ // AuthBotChallengeError — challenge / escalation refusal raised by
460
+ // b.authBotChallenge when the operator-supplied challengeFn is
461
+ // missing, returns a non-boolean verdict, or throws. Permanent.
462
+ var AuthBotChallengeError = defineClass("AuthBotChallengeError", { alwaysPermanent: true });
463
+ // SessionDeviceBindingError — fingerprint-drift refusal raised by
464
+ // b.sessionDeviceBinding when create-time opts are malformed or the
465
+ // boundKeyResolver returns a non-Buffer. Permanent.
466
+ var SessionDeviceBindingError = defineClass("SessionDeviceBindingError", { alwaysPermanent: true });
467
+ // AcmeError — RFC 8555 ACME + RFC 9773 ACME Renewal Information
468
+ // (ARI) protocol violations raised by b.acme: bad opts at create
469
+ // (non-https directory URL, missing accountKey, malformed audit hook),
470
+ // directory-fetch failure shape, newOrder/finalize/retrieveCert
471
+ // HTTP-status / response-shape errors, ARI window parse failures,
472
+ // retrieveCert returning non-PEM bytes, renewIfDue called before
473
+ // retrieveCert / before ARI URL is reachable. Permanent — every case
474
+ // is operator-misconfig or a CA-side response shape the framework
475
+ // refuses to coerce. withStatusCode so HTTP-shaped failures from the
476
+ // CA surface as a typed status for retry classification.
477
+ var AcmeError = defineClass("AcmeError", { withStatusCode: true });
478
+
479
+ // HpkeError — RFC 9180 Hybrid Public-Key Encryption (lib/crypto-hpke.js).
480
+ // Bad opts at the call site, KEM encap/decap failures, AEAD tag failures.
481
+ var HpkeError = defineClass("HpkeError", { alwaysPermanent: true });
482
+ // TlsExporterError — RFC 9266 TLS-Exporter channel binding
483
+ // (lib/tls-exporter.js). Non-TLS sockets, TLS<1.3 sessions, short
484
+ // exporter outputs.
485
+ var TlsExporterError = defineClass("TlsExporterError", { alwaysPermanent: true });
486
+ // HttpSigError — RFC 9421 HTTP Message Signatures (lib/http-message-
487
+ // signature.js). Bad opts, missing covered components, unsupported alg.
488
+ var HttpSigError = defineClass("HttpSigError", { alwaysPermanent: true });
489
+ // HttpClientError — outbound httpClient streaming primitives
490
+ // (b.httpClient.downloadStream / b.httpClient.uploadMultipartStream).
491
+ // withStatusCode so HTTP-shaped failures (404, 500, 503) carry the
492
+ // upstream status for retry classification. Codes follow the
493
+ // "httpclient/<reason>" shape: hash-mismatch, dest-not-writable,
494
+ // missing-file, http-error, etc.
495
+ var HttpClientError = defineClass("HttpClientError", { withStatusCode: true });
496
+ // KeychainError — b.keychain (lib/keychain.js). Bad opts at config time,
497
+ // native-tool exec failure (security / secret-tool / PowerShell
498
+ // CredentialManager), file-fallback unseal / shape failure, oversized
499
+ // native-tool output. alwaysPermanent — every case is operator-misconfig
500
+ // or a host-environment condition the framework refuses to coerce.
501
+ var KeychainError = defineClass("KeychainError", { alwaysPermanent: true });
502
+ // WatcherError — b.watcher recursive-fs.watch wrapper (lib/watcher.js).
503
+ // Bad opts at create (non-string root, missing root, bad ignore pattern,
504
+ // non-finite debounceMs, non-function hook), recursive-watch unsupported
505
+ // on the host platform/kernel, fs.watch start failure, pending-event
506
+ // queue overflow under runaway-directory pressure. alwaysPermanent —
507
+ // every case is config-misuse or a host-environment refusal the
508
+ // framework will not coerce.
509
+ var WatcherError = defineClass("WatcherError", { alwaysPermanent: true });
510
+ // LocalDbThinError — b.localDb.thin lightweight node:sqlite wrapper
511
+ // (lib/local-db-thin.js). Bad opts at create, node:sqlite unavailable
512
+ // on the host Node build, integrity_check failure under recovery:
513
+ // "refuse", recovery-rename I/O failure, post-close handle reuse, bad
514
+ // SQL passed to prepare/run/query. alwaysPermanent — every case is
515
+ // caller-shape misuse or an irrecoverable on-disk condition.
516
+ var LocalDbThinError = defineClass("LocalDbThinError", { alwaysPermanent: true });
517
+ // RouterError covers operator-shape violations on the router primitive:
518
+ // invalid `allowedRedirectOrigins` opt at create time, and cross-origin
519
+ // `res.redirect()` targets that are not on the allowlist. alwaysPermanent
520
+ // — every case is config-time programming bug or an outbound-redirect
521
+ // shape error that retry will not recover.
522
+ var RouterError = defineClass("RouterError", { alwaysPermanent: true });
523
+ // WorkerPoolError — b.workerPool (lib/worker-pool.js). Bad opts at
524
+ // config (size / maxQueueDepth / taskTimeoutMs out of range, non-
525
+ // absolute scriptPath, non-function onExit), runtime queue-full,
526
+ // per-task timeout, worker spawn / error / non-zero exit, malformed
527
+ // reply envelope, terminate-aborted tasks. alwaysPermanent — every
528
+ // case is operator-misconfig or worker-script bug; retry without a
529
+ // fix would just repeat the failure.
530
+ var WorkerPoolError = defineClass("WorkerPoolError", { alwaysPermanent: true });
531
+ // ArgParserError — b.argParser declarative CLI argument parser
532
+ // (lib/arg-parser.js). Bad opts at create time (unsupported flag type,
533
+ // duplicate flag/alias, malformed flag/command name, prototype-polluting
534
+ // name like __proto__/constructor/prototype), bad parse-time argv (not
535
+ // an array, non-string elements, unknown flag/command, missing required
536
+ // flag, unparseable number/boolean coercion, missing flag value).
537
+ // alwaysPermanent — every case is operator-shape misuse the framework
538
+ // will not coerce; the operator fixes the spec or the argv source.
539
+ var ArgParserError = defineClass("ArgParserError", { alwaysPermanent: true });
540
+ // DaemonError — b.daemon (lib/daemon.js). Bad opts at start/stop, pidfile
541
+ // already held by a live PID, spawn failure for detached-fork mode,
542
+ // log-file open failure, kill() failure outside ESRCH. alwaysPermanent —
543
+ // every case is operator-misconfig or a host-environment condition the
544
+ // framework refuses to coerce; transient-shaped failures (process
545
+ // already exited between read and kill) are surfaced as a non-error
546
+ // "stopped: false, reason: stale" return.
547
+ var DaemonError = defineClass("DaemonError", { alwaysPermanent: true });
548
+ // SelfUpdateError — b.selfUpdate (lib/self-update.js). Bad opts at
549
+ // poll/verify/swap/rollback, non-2xx releases-feed response, malformed
550
+ // JSON, missing tag_name, signature verify mismatch, atomic-swap or
551
+ // rollback failure, cross-device install failure. alwaysPermanent —
552
+ // every case is operator-misconfig or a release-feed shape the
553
+ // framework refuses to coerce. Operators wrap the call in their own
554
+ // retry policy when polling against a flaky CDN.
555
+ var SelfUpdateError = defineClass("SelfUpdateError", { alwaysPermanent: true });
410
556
 
411
557
  module.exports = {
412
558
  FrameworkError: FrameworkError,
@@ -472,4 +618,27 @@ module.exports = {
472
618
  AiInputError: AiInputError,
473
619
  A2aError: A2aError,
474
620
  GraphqlFederationError: GraphqlFederationError,
621
+ Fda21Cfr11Error: Fda21Cfr11Error,
622
+ AuditDailyReviewError: AuditDailyReviewError,
623
+ AuditSegregationError: AuditSegregationError,
624
+ DdlChangeControlError: DdlChangeControlError,
625
+ LegalHoldError: LegalHoldError,
626
+ WormViolationError: WormViolationError,
627
+ SandboxError: SandboxError,
628
+ DlpError: DlpError,
629
+ AuthBotChallengeError: AuthBotChallengeError,
630
+ SessionDeviceBindingError: SessionDeviceBindingError,
631
+ AcmeError: AcmeError,
632
+ HpkeError: HpkeError,
633
+ TlsExporterError: TlsExporterError,
634
+ HttpSigError: HttpSigError,
635
+ HttpClientError: HttpClientError,
636
+ KeychainError: KeychainError,
637
+ WatcherError: WatcherError,
638
+ LocalDbThinError: LocalDbThinError,
639
+ RouterError: RouterError,
640
+ WorkerPoolError: WorkerPoolError,
641
+ ArgParserError: ArgParserError,
642
+ DaemonError: DaemonError,
643
+ SelfUpdateError: SelfUpdateError,
475
644
  };
@@ -1,47 +1,47 @@
1
1
  "use strict";
2
2
  /**
3
- * Framework state schema for cluster-mode external storage.
3
+ * @module b.frameworkSchema
4
+ * @nav Production
5
+ * @title Framework Schema
4
6
  *
5
- * When cluster mode is active, the framework's audit chain + consent
6
- * log + audit checkpoints + audit tip live in the operator's external-db
7
- * (configured via b.externalDb.init). This module owns the DDL for
8
- * those tables and exposes a single idempotent ensureSchema() entry
9
- * point that operators (or the framework's leader-acquire hook in a
10
- * later release) call to create them.
7
+ * @intro
8
+ * Framework-defined SQL schema (audit / sessions / api_keys / cache /
9
+ * break-glass / scheduler-ticks / pubsub / rate-limit / seeders /
10
+ * etc.) declarative, migration-aware, and dialect-portable across
11
+ * Postgres and SQLite.
11
12
  *
12
- * In external-db the framework tables are prefixed with `_blamejs_`
13
- * to avoid collision with the operator's app data:
13
+ * When cluster mode is active the framework's audit chain, consent
14
+ * log, audit checkpoints, audit tip, scheduler ticks, rate-limit
15
+ * counters, pubsub fan-out, sessions, jobs, cache, seeders, and
16
+ * break-glass policies/grants live in the operator's external
17
+ * database (configured via `b.externalDb.init`). This module owns
18
+ * the DDL for those tables and exposes a single idempotent entry
19
+ * point — `b.frameworkSchema.ensureSchema` — that operators (or the
20
+ * framework's leader-acquire hook in a later release) call to create
21
+ * them at boot.
14
22
  *
15
- * audit_log local-SQLite name
16
- * _blamejs_audit_log external-db name
23
+ * External-db tables are prefixed with `_blamejs_` so they never
24
+ * collide with the operator's application tables:
17
25
  *
18
- * The mapping is exposed via tableName(local) so write-dispatch code
19
- * (cluster-storage.js) uses a single name reference and the
20
- * dialect-aware ensureSchema fans out the DDL to either the local-
21
- * SQLite (db.js's FRAMEWORK_SCHEMA) or the external-db backend.
26
+ * audit_log — local-SQLite name
27
+ * _blamejs_audit_log — external-db name
22
28
  *
23
- * Dialects: Postgres + SQLite. Both support CREATE TABLE IF NOT EXISTS,
24
- * CREATE INDEX IF NOT EXISTS, and the same column types modulo
25
- * INTEGER/BIGINT and BLOB/BYTEA differences. MySQL is not currently
26
- * supported — operators on MySQL must use one of the supported
27
- * dialects until a MySQL adapter ships.
29
+ * `b.frameworkSchema.tableName` exposes the mapping so write-
30
+ * dispatch code (`cluster-storage.js`) can use a single name
31
+ * reference. `b.frameworkSchema.LOCAL_TO_EXTERNAL` is the frozen
32
+ * read-only mapping object.
28
33
  *
29
- * What ensureSchema does NOT do:
30
- * - Migrate existing audit_log rows from local SQLite into external-db.
31
- * That migration belongs to a separate operator-driven tool.
32
- * - Verify chain integrity in external-db. That happens at boot via
33
- * the audit module's regular verify() path on every read.
34
- * - Install append-only triggers. The framework's tamper-evidence
35
- * model is the audit chain's hash linkage + SLH-DSA-signed
36
- * checkpoints — triggers would add a defense-in-depth layer
37
- * but they're not load-bearing for the threat model. Operators
38
- * who want triggers add them per their dialect's syntax.
34
+ * Append-only WORM enforcement: `ensureSchema` installs BEFORE
35
+ * DELETE / BEFORE UPDATE triggers on `audit_log`, `consent_log`,
36
+ * and `audit_checkpoints` Postgres via plpgsql RAISE EXCEPTION
37
+ * functions, SQLite via `RAISE(ABORT, ...)`. Idempotent across
38
+ * reboots; any operator-applied DROP TRIGGER is restored on the
39
+ * next ensureSchema pass. MySQL is not currently supported —
40
+ * operators on MySQL must run on Postgres or SQLite until a MySQL
41
+ * adapter ships.
39
42
  *
40
- * Public API:
41
- * await frameworkSchema.ensureSchema({ externalDbBackend, dialect })
42
- * frameworkSchema.tableName(localName) external table name lookup
43
- * frameworkSchema.LOCAL_TO_EXTERNAL mapping (read-only)
44
- * frameworkSchema.FrameworkSchemaError error class
43
+ * @card
44
+ * Framework-defined SQL schema (audit / sessions / api_keys / cache / break-glass / scheduler-ticks / pubsub / rate-limit / seeders / etc.) — declarative, migration-aware, and dialect-portable across Postgres and SQLite.
45
45
  */
46
46
 
47
47
  var externalDb = require("./external-db");
@@ -138,6 +138,29 @@ var LOCAL_TO_EXTERNAL = Object.freeze({
138
138
  _blamejs_break_glass_grants: "_blamejs_break_glass_grants",
139
139
  });
140
140
 
141
+ /**
142
+ * @primitive b.frameworkSchema.tableName
143
+ * @signature b.frameworkSchema.tableName(localName)
144
+ * @since 0.5.0
145
+ * @status stable
146
+ * @related b.frameworkSchema.ensureSchema
147
+ *
148
+ * Translate a local-SQLite table name into the external-db name. The
149
+ * mapping is the frozen `LOCAL_TO_EXTERNAL` object — tables that already
150
+ * carry the `_blamejs_` prefix locally pass through unchanged. Cluster
151
+ * write-dispatch code uses this lookup so the same SQL works against
152
+ * both backends without per-call branching.
153
+ *
154
+ * @example
155
+ * b.frameworkSchema.tableName("audit_log");
156
+ * // → "_blamejs_audit_log"
157
+ *
158
+ * b.frameworkSchema.tableName("_blamejs_sessions");
159
+ * // → "_blamejs_sessions"
160
+ *
161
+ * b.frameworkSchema.tableName("operator_app_table");
162
+ * // → "operator_app_table"
163
+ */
141
164
  function tableName(localName) {
142
165
  if (Object.prototype.hasOwnProperty.call(LOCAL_TO_EXTERNAL, localName)) {
143
166
  return LOCAL_TO_EXTERNAL[localName];
@@ -660,6 +683,42 @@ function _breakGlassGrantsDDL(dialect) {
660
683
 
661
684
  // ---- ensureSchema ----
662
685
 
686
+ /**
687
+ * @primitive b.frameworkSchema.ensureSchema
688
+ * @signature b.frameworkSchema.ensureSchema(opts)
689
+ * @since 0.5.0
690
+ * @status stable
691
+ * @related b.frameworkSchema.tableName, b.externalDb.init, b.audit
692
+ *
693
+ * Create every framework-owned table + index in the operator's
694
+ * external database, then install append-only WORM triggers on
695
+ * `_blamejs_audit_log`, `_blamejs_consent_log`, and
696
+ * `_blamejs_audit_checkpoints`. Idempotent: every DDL uses
697
+ * `IF NOT EXISTS` and re-running is safe across reboots.
698
+ *
699
+ * Returns `{ tables }` with the set of CREATE TABLE names emitted
700
+ * so the operator can confirm the expected surface landed.
701
+ *
702
+ * Throws `FrameworkSchemaError("framework-schema/invalid-config")`
703
+ * when `externalDbBackend` is missing and
704
+ * `FrameworkSchemaError("framework-schema/unsupported-dialect")`
705
+ * when `dialect` is anything other than `postgres` or `sqlite`.
706
+ *
707
+ * @opts
708
+ * externalDbBackend: string, // backend name registered with b.externalDb (required)
709
+ * dialect: "postgres"|"sqlite", // default: "postgres"
710
+ *
711
+ * @example
712
+ * try {
713
+ * var report = await b.frameworkSchema.ensureSchema({
714
+ * externalDbBackend: "primary",
715
+ * dialect: "postgres",
716
+ * });
717
+ * report.tables[0]; // → "_blamejs_audit_log"
718
+ * } catch (e) {
719
+ * e.code; // → "framework-schema/unsupported-dialect"
720
+ * }
721
+ */
663
722
  async function ensureSchema(opts) {
664
723
  if (!opts || !opts.externalDbBackend) {
665
724
  throw new FrameworkSchemaError(
@@ -706,9 +765,78 @@ async function ensureSchema(opts) {
706
765
  }
707
766
  created.push(d.create.match(/CREATE TABLE IF NOT EXISTS\s+(\S+)/)[1]);
708
767
  }
768
+
769
+ // D-M11 — append-only WORM enforcement on audit_log / consent_log /
770
+ // audit_checkpoints in cluster mode. Local-SQLite path already
771
+ // installs CREATE TRIGGER IF NOT EXISTS via lib/db.js's
772
+ // _installAppendOnlyTriggers; Postgres needs equivalent rules
773
+ // (BEFORE-row triggers raising an exception) so a privileged
774
+ // cluster-side actor with the framework role can't DELETE / UPDATE
775
+ // a row out from under the chain. The chain integrity check still
776
+ // catches it at next boot, but the trigger is the in-band defense.
777
+ await _installWormTriggers(opts.externalDbBackend, dialect);
778
+
709
779
  return { tables: created };
710
780
  }
711
781
 
782
+ // D-M11 — WORM enforcement helper. Idempotent: rebuilding triggers
783
+ // per boot is cheap and any operator-applied DROP TRIGGER is restored
784
+ // at the next ensureSchema pass.
785
+ async function _installWormTriggers(backend, dialect) {
786
+ var wormTables = [
787
+ LOCAL_TO_EXTERNAL.audit_log,
788
+ LOCAL_TO_EXTERNAL.consent_log,
789
+ LOCAL_TO_EXTERNAL.audit_checkpoints,
790
+ ];
791
+ for (var i = 0; i < wormTables.length; i++) {
792
+ var t = wormTables[i];
793
+ if (dialect === "postgres") {
794
+ // Per-table trigger function. Postgres rejects the statement
795
+ // with a SQLSTATE that bubbles up as a query-failure audit row.
796
+ var fnName = t + "_worm_block";
797
+ await externalDb.query(
798
+ "CREATE OR REPLACE FUNCTION " + fnName + "() RETURNS trigger AS $$ " +
799
+ "BEGIN RAISE EXCEPTION '" + t + " is append-only — % prohibited', TG_OP " +
800
+ "USING ERRCODE = '0A000'; END; $$ LANGUAGE plpgsql",
801
+ [], { backend: backend }
802
+ );
803
+ await externalDb.query(
804
+ "DROP TRIGGER IF EXISTS no_delete_" + t + " ON " + t,
805
+ [], { backend: backend }
806
+ );
807
+ await externalDb.query(
808
+ "CREATE TRIGGER no_delete_" + t + " BEFORE DELETE ON " + t +
809
+ " FOR EACH ROW EXECUTE FUNCTION " + fnName + "()",
810
+ [], { backend: backend }
811
+ );
812
+ await externalDb.query(
813
+ "DROP TRIGGER IF EXISTS no_update_" + t + " ON " + t,
814
+ [], { backend: backend }
815
+ );
816
+ await externalDb.query(
817
+ "CREATE TRIGGER no_update_" + t + " BEFORE UPDATE ON " + t +
818
+ " FOR EACH ROW EXECUTE FUNCTION " + fnName + "()",
819
+ [], { backend: backend }
820
+ );
821
+ } else {
822
+ // SQLite cluster path. CREATE TRIGGER IF NOT EXISTS matches the
823
+ // local-SQLite shape installed by lib/db.js.
824
+ await externalDb.query(
825
+ 'CREATE TRIGGER IF NOT EXISTS "no_delete_' + t + '" ' +
826
+ 'BEFORE DELETE ON "' + t + '" ' +
827
+ "BEGIN SELECT RAISE(ABORT, '" + t + " is append-only — DELETE prohibited'); END",
828
+ [], { backend: backend }
829
+ );
830
+ await externalDb.query(
831
+ 'CREATE TRIGGER IF NOT EXISTS "no_update_' + t + '" ' +
832
+ 'BEFORE UPDATE ON "' + t + '" ' +
833
+ "BEGIN SELECT RAISE(ABORT, '" + t + " is append-only — UPDATE prohibited'); END",
834
+ [], { backend: backend }
835
+ );
836
+ }
837
+ }
838
+ }
839
+
712
840
  module.exports = {
713
841
  ensureSchema: ensureSchema,
714
842
  tableName: tableName,