@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
@@ -1,41 +1,41 @@
1
1
  "use strict";
2
2
  /**
3
- * break-glass — column-policy / row-enforcement step-up auth.
3
+ * @module b.breakGlass
4
+ * @nav Identity
5
+ * @title Break Glass
4
6
  *
5
- * Operator declares which columns of which tables are GLASS-LOCKED.
6
- * Reading the encrypted value on any row of a glass-locked column
7
- * requires the calling operator to:
7
+ * @intro
8
+ * Column-policy / row-enforcement step-up auth PHI / PCI columns
9
+ * require a fresh second-factor grant + operator-supplied reason;
10
+ * every unseal is audited row-by-row.
8
11
  *
9
- * 1. Prove identity with a second factor (TOTP / passkey).
10
- * 2. Provide an operator-supplied REASON the audit chain captures.
11
- * 3. Hold a short-lived, scope-bounded GRANT.
12
+ * The operator declares which columns of which tables are
13
+ * GLASS-LOCKED. Reading a glass-locked column on any row requires
14
+ * the caller to (1) prove identity with a fresh second factor (TOTP
15
+ * or passkey), (2) supply a reason the audit chain captures, and
16
+ * (3) hold a short-lived scope-bounded grant. Each row read emits a
17
+ * per-row audit event; the default `maxRowsPerGrant: 1` enforces
18
+ * row-by-row auth so every PHI / PCI access is its own discrete
19
+ * authenticated event.
12
20
  *
13
- * Each row read under a grant emits a per-row audit event. Default
14
- * `maxRowsPerGrant: 1` enforces row-by-row auth each row access is
15
- * its own discrete authenticated event, compliance-defensible by
16
- * construction. Operators with batch workflows raise the cap per-table.
21
+ * Two crypto models ship side-by-side. Model A the default is a
22
+ * policy gate: glass-locked columns sit in the regular cryptoField
23
+ * sealed-row pipeline, and break-glass enforces the grant + audit
24
+ * contract on every read path. Model B (`cryptographic: true` on the
25
+ * policy) layers per-cell encryption on top: every (table, rowId,
26
+ * column) triple gets its own key derived `K_cell = SHAKE256(DEK ||
27
+ * table || rowId || column)`, AEAD-bound to AAD = `SHA3-512(table ||
28
+ * rowId || column)` so swapping ciphertexts between rows fails
29
+ * closed. Operators opt into Model B per-policy, then run
30
+ * `b.breakGlass.migrate(table)` to convert existing rows.
17
31
  *
18
- * Spec: memory/specs/blamejs-break-glass-spec.md
32
+ * Service-account bypass (`policy.serviceAccountBypass`) is opt-in
33
+ * per-table — both an apiKey-id allowlist and a required role must
34
+ * match. Admin tools (`listActiveAll`, `revokeAll`) cover security-
35
+ * team dashboards and incident-response offboarding.
19
36
  *
20
- * Ships Model A (policy gate) + TOTP factor + config-time input
21
- * validation + 14 error codes + audit chain integration. Model B
22
- * (cryptographic gate via per-row K_row), passkey factor, service-
23
- * account bypass, and admin tools land in subsequent patches.
24
- *
25
- * Public API:
26
- *
27
- * b.breakGlass.init({ now? }) — boot once
28
- * b.breakGlass.policy.set(table, opts)
29
- * b.breakGlass.policy.get(table) — null if unset
30
- * b.breakGlass.policy.list()
31
- * b.breakGlass.policy.delete(table)
32
- *
33
- * b.breakGlass.grant({ req, table, reason, factor, columns? })
34
- * b.breakGlass.unsealRow(grant, table, rowId)
35
- * b.breakGlass.revoke(grantId, { reason })
36
- * b.breakGlass.listActive({ req })
37
- *
38
- * b.breakGlass.BreakGlassError
37
+ * @card
38
+ * Column-policy / row-enforcement step-up auth PHI / PCI columns require a fresh second-factor grant + operator-supplied reason; every unseal is audited row-by-row.
39
39
  */
40
40
  var audit = require("./audit");
41
41
  var C = require("./constants");
@@ -177,9 +177,41 @@ async function _ensureDek(table) {
177
177
  return dek;
178
178
  }
179
179
 
180
- // Encrypt a single cell value with encryption context binding. Operators
181
- // in cryptographic mode use this at write time INSTEAD of letting
182
- // cryptoField.sealRow seal the column.
180
+ /**
181
+ * @primitive b.breakGlass.encryptCell
182
+ * @signature b.breakGlass.encryptCell(plaintext, ctx)
183
+ * @since 0.5.1
184
+ * @status stable
185
+ * @related b.breakGlass.decryptCell, b.breakGlass.migrate, b.breakGlass.policy.set
186
+ *
187
+ * Encrypt a single glass-locked cell value with encryption-context
188
+ * binding. Operators running a policy in `cryptographic: true` mode
189
+ * call this at write time INSTEAD of letting `cryptoField.sealRow`
190
+ * seal the column. The framework derives a per-cell key from the
191
+ * policy's vault-sealed DEK plus `(table, rowId, column)`, encrypts
192
+ * with XChaCha20-Poly1305, and sets AAD to `SHA3-512(table || rowId
193
+ * || column)` so a ciphertext literally cannot be decrypted under a
194
+ * different row identifier even with the same DEK.
195
+ *
196
+ * Returns a string of the form `bgcell:1:<base64>` ready to write
197
+ * back to the column. Throws `breakglass/policy-not-set` when the
198
+ * table has no policy or the policy isn't in cryptographic mode, and
199
+ * `breakglass/grant-column-mismatch` when the column isn't glass-
200
+ * locked on the policy.
201
+ *
202
+ * @example
203
+ * await b.breakGlass.policy.set("patients", {
204
+ * columns: ["ssn"],
205
+ * factors: ["totp"],
206
+ * cryptographic: true,
207
+ * });
208
+ * var sealed = await b.breakGlass.encryptCell("123-45-6789", {
209
+ * table: "patients",
210
+ * rowId: "patient-001",
211
+ * column: "ssn",
212
+ * });
213
+ * // → "bgcell:1:AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
214
+ */
183
215
  async function encryptCell(plaintext, ctx) {
184
216
  _requireInit();
185
217
  if (!ctx || typeof ctx !== "object" ||
@@ -207,11 +239,32 @@ async function encryptCell(plaintext, ctx) {
207
239
  return "bgcell:1:" + packed.toString("base64");
208
240
  }
209
241
 
210
- // Decrypt a cell value. Caller must hold a valid grant covering the
211
- // (table, column) — caller passes through unsealRow which gates this.
212
- // The encryption context (table, rowId, column) is passed both to the
213
- // key derivation AND the AAD; if the caller passes the wrong rowId
214
- // trying to "swap" ciphertexts between rows, decryption fails closed.
242
+ /**
243
+ * @primitive b.breakGlass.decryptCell
244
+ * @signature b.breakGlass.decryptCell(ciphertext, ctx)
245
+ * @since 0.5.1
246
+ * @status stable
247
+ * @related b.breakGlass.encryptCell, b.breakGlass.unsealRow
248
+ *
249
+ * Decrypt a Model-B `bgcell:1:<base64>` cell value. Internal — the
250
+ * caller must already hold a valid grant covering the (table,
251
+ * column); operator-facing reads route through `b.breakGlass.unsealRow`
252
+ * which gates this call. The encryption context (`table, rowId,
253
+ * column`) is fed into BOTH the per-cell key derivation AND the AEAD
254
+ * AAD, so a caller passing the wrong `rowId` trying to swap
255
+ * ciphertexts between rows fails closed at the AEAD verify step.
256
+ *
257
+ * @example
258
+ * // unsealRow routes here automatically for cryptographic-mode
259
+ * // policies; calling decryptCell directly is rare and only for
260
+ * // operator tooling that's already enforced its own grant gate.
261
+ * var plaintext = await b.breakGlass.decryptCell(sealed, {
262
+ * table: "patients",
263
+ * rowId: "patient-001",
264
+ * column: "ssn",
265
+ * });
266
+ * // → "123-45-6789"
267
+ */
215
268
  async function decryptCell(ciphertext, ctx) {
216
269
  _requireInit();
217
270
  if (typeof ciphertext !== "string" || ciphertext.indexOf("bgcell:1:") !== 0) {
@@ -238,6 +291,38 @@ async function decryptCell(ciphertext, ctx) {
238
291
  // back. The migration is idempotent — a row already in Model B form
239
292
  // (column starts with "bgcell:") is skipped.
240
293
 
294
+ /**
295
+ * @primitive b.breakGlass.migrate
296
+ * @signature b.breakGlass.migrate(table, opts)
297
+ * @since 0.5.1
298
+ * @status stable
299
+ * @related b.breakGlass.encryptCell, b.breakGlass.policy.set
300
+ *
301
+ * One-shot migration that converts every existing row of a glass-
302
+ * locked table from Model A (cryptoField.sealRow only) into Model B
303
+ * per-cell ciphertext. Iterates via `_id`-keyset paging so memory
304
+ * stays bounded; rows already in Model B (column starts with
305
+ * `bgcell:`) are skipped, making the migration idempotent and safe
306
+ * to re-run after a partial failure.
307
+ *
308
+ * Emits a `breakglass.migrate` audit event on completion with totals
309
+ * and skipped counts. Refuses to run when the policy isn't in
310
+ * cryptographic mode — operator must `policy.set({ cryptographic:
311
+ * true })` first.
312
+ *
313
+ * @opts
314
+ * batchSize: number, // _id-keyset page size (default 100)
315
+ * callerOpts: object, // forwarded to audit actor resolution
316
+ *
317
+ * @example
318
+ * await b.breakGlass.policy.set("patients", {
319
+ * columns: ["ssn", "dob"],
320
+ * factors: ["totp"],
321
+ * cryptographic: true,
322
+ * });
323
+ * var summary = await b.breakGlass.migrate("patients", { batchSize: 250 });
324
+ * // → { table: "patients", totalRows: 1200, migratedRows: 1198, skippedRows: 2 }
325
+ */
241
326
  async function migrate(table, opts) {
242
327
  _requireInit();
243
328
  opts = opts || {};
@@ -324,6 +409,29 @@ async function migrate(table, opts) {
324
409
 
325
410
  // ---- init ----
326
411
 
412
+ /**
413
+ * @primitive b.breakGlass.init
414
+ * @signature b.breakGlass.init(opts)
415
+ * @since 0.5.0
416
+ * @status stable
417
+ * @related b.breakGlass.policy.set, b.breakGlass.grant
418
+ *
419
+ * One-shot boot wiring. Clears the in-memory policy cache, resets the
420
+ * factor-lockout counter, and records the framework-wide trustProxy
421
+ * boundary so subsequent `grant()` calls populate the grant row's `ip`
422
+ * field from `X-Forwarded-For` only when proxies are trusted. Operators
423
+ * call this once at boot, before any policy / grant / unseal call —
424
+ * every other primitive throws `breakglass/not-initialized` until init
425
+ * has run.
426
+ *
427
+ * @opts
428
+ * now: number, // testing-only override of Date.now (fixtures)
429
+ * trustProxy: boolean, // honor X-Forwarded-For when populating grant.ip (default false)
430
+ *
431
+ * @example
432
+ * b.breakGlass.init({ trustProxy: true });
433
+ * // → undefined (init returns nothing; throws on bad opts)
434
+ */
327
435
  function init(opts) {
328
436
  opts = opts || {};
329
437
  validateOpts(opts, ["now", "trustProxy"], "breakGlass.init");
@@ -489,6 +597,47 @@ function _validatePolicySet(table, opts) {
489
597
  };
490
598
  }
491
599
 
600
+ /**
601
+ * @primitive b.breakGlass.policy.set
602
+ * @signature b.breakGlass.policy.set(table, opts, callerOpts)
603
+ * @since 0.5.0
604
+ * @status stable
605
+ * @compliance hipaa, pci-dss, gdpr, soc2
606
+ * @related b.breakGlass.policy.get, b.breakGlass.policy.delete, b.breakGlass.grant
607
+ *
608
+ * Declare the column-policy that gates step-up auth on the named
609
+ * table. The listed columns become GLASS-LOCKED — every read of one of
610
+ * those columns on any row requires the caller to hold a fresh
611
+ * second-factor grant whose scope covers the column. Stored
612
+ * cluster-wide in `_blamejs_break_glass_policies` (sealed via
613
+ * cryptoField) so every node honors the same gate. Re-runs UPSERT;
614
+ * the policy cache flushes for the table.
615
+ *
616
+ * @opts
617
+ * columns: Array<string>, // glass-locked column names (required, ≥1)
618
+ * factors: Array<string>, // allowed second factors: "totp" / "passkey"
619
+ * cryptographic: boolean, // opt into Model B per-cell encryption (default false)
620
+ * grantTtl: number, // grant lifetime in ms (default 15 minutes)
621
+ * maxRowsPerGrant: number, // rows a grant may unseal (default 1 — row-by-row)
622
+ * reasonRequired: boolean, // require operator reason on grant (default true)
623
+ * reasonMinLength: number, // minimum reason length in chars (default 12)
624
+ * pinIp: boolean, // bind grant to issuing IP (default true)
625
+ * sessionPin: boolean, // bind grant to issuing session (default true)
626
+ * onLockedAccess: string, // "throw" | "redact" on unauthorized read (default "throw")
627
+ * requireScope: string, // actor scope required before grant mints (e.g. "phi:admin")
628
+ * serviceAccountBypass: object, // { enabled, apiKeyIds, requireRole } — opt-in machine bypass
629
+ * auditReasonStorage: string, // "cleartext" | "hmac" | "both" (default "cleartext")
630
+ *
631
+ * @example
632
+ * await b.breakGlass.policy.set("patients", {
633
+ * columns: ["ssn", "dob"],
634
+ * factors: ["totp"],
635
+ * grantTtl: 600000,
636
+ * maxRowsPerGrant: 1,
637
+ * requireScope: "phi:admin",
638
+ * });
639
+ * // → { applied: true, table: "patients" }
640
+ */
492
641
  async function policySet(table, opts, callerOpts) {
493
642
  _requireInit();
494
643
  var validated = _validatePolicySet(table, opts);
@@ -540,6 +689,26 @@ async function policySet(table, opts, callerOpts) {
540
689
  return { applied: true, table: table };
541
690
  }
542
691
 
692
+ /**
693
+ * @primitive b.breakGlass.policy.get
694
+ * @signature b.breakGlass.policy.get(table)
695
+ * @since 0.5.0
696
+ * @status stable
697
+ * @related b.breakGlass.policy.set, b.breakGlass.policy.list
698
+ *
699
+ * Read the current break-glass policy for `table` from the cluster-
700
+ * shared policies table, with an in-process cache that short-circuits
701
+ * the DB roundtrip on the unsealRow hot path. Returns `null` when the
702
+ * table has no policy declared (a non-glass-locked table). The cache
703
+ * invalidates on `policy.set` / `policy.delete`.
704
+ *
705
+ * @example
706
+ * var policy = await b.breakGlass.policy.get("patients");
707
+ * // → { table: "patients", columns: ["ssn", "dob"], factors: ["totp"], ... }
708
+ *
709
+ * var none = await b.breakGlass.policy.get("posts");
710
+ * // → null
711
+ */
543
712
  async function policyGet(table) {
544
713
  _requireInit();
545
714
  if (typeof table !== "string" || table.length === 0) return null;
@@ -576,6 +745,24 @@ async function policyGet(table) {
576
745
  return policy;
577
746
  }
578
747
 
748
+ /**
749
+ * @primitive b.breakGlass.policy.list
750
+ * @signature b.breakGlass.policy.list()
751
+ * @since 0.5.0
752
+ * @status stable
753
+ * @related b.breakGlass.policy.get, b.breakGlass.policy.set
754
+ *
755
+ * Enumerate every glass-locked table the cluster knows about. Used by
756
+ * compliance dashboards (which tables hold PHI / PCI?) and migration
757
+ * tooling that needs to walk the full set. Returns hydrated policy
758
+ * objects in `tableName` order — no abbreviated row form.
759
+ *
760
+ * @example
761
+ * var policies = await b.breakGlass.policy.list();
762
+ * // → [{ table: "patients", columns: ["ssn", "dob"], ... }, { table: "cards", ... }]
763
+ * policies.length;
764
+ * // → 2
765
+ */
579
766
  async function policyList() {
580
767
  _requireInit();
581
768
  var rows = await clusterStorage.executeAll(
@@ -589,6 +776,23 @@ async function policyList() {
589
776
  return out;
590
777
  }
591
778
 
779
+ /**
780
+ * @primitive b.breakGlass.policy.delete
781
+ * @signature b.breakGlass.policy.delete(table, callerOpts)
782
+ * @since 0.5.0
783
+ * @status stable
784
+ * @related b.breakGlass.policy.set
785
+ *
786
+ * Remove the break-glass policy for `table`. Subsequent reads of the
787
+ * previously glass-locked columns no longer require a grant — operators
788
+ * call this only when a column genuinely stops being PHI / PCI (rare;
789
+ * almost always the operator wants `policy.set` with a revised column
790
+ * list instead). Emits a `breakglass.policy.delete` audit event.
791
+ *
792
+ * @example
793
+ * await b.breakGlass.policy.delete("legacy_patients");
794
+ * // → { deleted: true, table: "legacy_patients" }
795
+ */
592
796
  async function policyDelete(table, callerOpts) {
593
797
  _requireInit();
594
798
  if (typeof table !== "string" || table.length === 0) {
@@ -647,6 +851,39 @@ async function _verifyPasskeyFactor(factor) {
647
851
  }
648
852
  }
649
853
 
854
+ /**
855
+ * @primitive b.breakGlass.grant
856
+ * @signature b.breakGlass.grant(opts)
857
+ * @since 0.5.0
858
+ * @status stable
859
+ * @compliance hipaa, pci-dss, soc2
860
+ * @related b.breakGlass.unsealRow, b.breakGlass.revoke, b.breakGlass.policy.set
861
+ *
862
+ * Mint a short-lived, scope-bounded break-glass grant. The framework
863
+ * verifies the operator's second factor (TOTP code or passkey
864
+ * assertion), records the operator-supplied reason into the audit
865
+ * chain, and issues a grant whose scope covers the named columns of
866
+ * the named table for `policy.grantTtl` ms or `policy.maxRowsPerGrant`
867
+ * row reads — whichever ends first. Failures emit a denied-grant audit
868
+ * row; repeated factor failures trigger the lockout primitive.
869
+ *
870
+ * @opts
871
+ * req: object, // the active request (carries actor identity, ip, session)
872
+ * table: string, // glass-locked table the grant scopes to
873
+ * columns: Array<string>, // optional subset of policy.columns (default = full policy)
874
+ * reason: string, // operator-supplied reason (length-gated by policy.reasonMinLength)
875
+ * factor: object, // { type: "totp", secret, code } or { type: "passkey", response, ... }
876
+ *
877
+ * @example
878
+ * var handle = await b.breakGlass.grant({
879
+ * req: req,
880
+ * table: "patients",
881
+ * columns: ["ssn"],
882
+ * reason: "ER admit verifying identity for patient-001",
883
+ * factor: { type: "totp", secret: req.user.totpSecret, code: "123456" },
884
+ * });
885
+ * // → { id: "bg-...", expiresAt: 1735000000000, rowsRemaining: 1, scopeTable: "patients", scopeColumns: ["ssn"] }
886
+ */
650
887
  async function grant(opts) {
651
888
  _requireInit();
652
889
  if (!opts || typeof opts !== "object") {
@@ -853,6 +1090,30 @@ function _reasonForAudit(reason, mode) {
853
1090
 
854
1091
  // ---- Use a grant ----
855
1092
 
1093
+ /**
1094
+ * @primitive b.breakGlass.unsealRow
1095
+ * @signature b.breakGlass.unsealRow(grantHandle, table, rowId, opts)
1096
+ * @since 0.5.0
1097
+ * @status stable
1098
+ * @compliance hipaa, pci-dss, soc2
1099
+ * @related b.breakGlass.grant, b.breakGlass.decryptCell, b.breakGlass.revoke
1100
+ *
1101
+ * Read one row's glass-locked columns under an active grant. The
1102
+ * framework validates the grant (not revoked, not expired, not
1103
+ * exhausted, scope matches the table), atomically increments
1104
+ * `rowsConsumed`, fetches and unseals the row, and emits a per-row
1105
+ * `breakglass.unsealrow` audit event carrying the reason + actor +
1106
+ * remaining rows. For Model B (cryptographic) policies, glass-locked
1107
+ * columns route through `decryptCell` with encryption-context binding
1108
+ * — a swapped ciphertext from another row fails closed at AEAD verify.
1109
+ *
1110
+ * @opts
1111
+ * req: object, // optional originating request — populates ip / userAgent / sessionId / requestId on the audit row
1112
+ *
1113
+ * @example
1114
+ * var row = await b.breakGlass.unsealRow(handle, "patients", "patient-001", { req: req });
1115
+ * // → { _id: "patient-001", name: "Alice", ssn: "123-45-6789", dob: "1980-04-12", ... }
1116
+ */
856
1117
  async function unsealRow(grantHandle, table, rowId, opts) {
857
1118
  _requireInit();
858
1119
  if (!grantHandle || typeof grantHandle !== "object" || typeof grantHandle.id !== "string") {
@@ -1038,6 +1299,26 @@ async function unsealRow(grantHandle, table, rowId, opts) {
1038
1299
 
1039
1300
  // ---- Revoke ----
1040
1301
 
1302
+ /**
1303
+ * @primitive b.breakGlass.revoke
1304
+ * @signature b.breakGlass.revoke(grantId, opts)
1305
+ * @since 0.5.0
1306
+ * @status stable
1307
+ * @related b.breakGlass.grant, b.breakGlass.revokeAll
1308
+ *
1309
+ * Mark a single grant revoked. Subsequent `unsealRow` calls against the
1310
+ * grant id throw `breakglass/grant-revoked`. Idempotent — already-
1311
+ * revoked grants stay at their original `revokedAt` timestamp because
1312
+ * the UPDATE clause is gated on `revokedAt IS NULL`.
1313
+ *
1314
+ * @opts
1315
+ * reason: string, // operator note recorded into the audit row
1316
+ * callerOpts: object, // forwarded to audit actor resolution
1317
+ *
1318
+ * @example
1319
+ * await b.breakGlass.revoke("bg-abc123", { reason: "operator finished read; releasing" });
1320
+ * // → { revoked: true, grantId: "bg-abc123" }
1321
+ */
1041
1322
  async function revoke(grantId, opts) {
1042
1323
  _requireInit();
1043
1324
  if (typeof grantId !== "string" || grantId.length === 0) {
@@ -1063,6 +1344,27 @@ async function revoke(grantId, opts) {
1063
1344
 
1064
1345
  // ---- listActive ----
1065
1346
 
1347
+ /**
1348
+ * @primitive b.breakGlass.listActive
1349
+ * @signature b.breakGlass.listActive(opts)
1350
+ * @since 0.5.0
1351
+ * @status stable
1352
+ * @related b.breakGlass.listActiveAll, b.breakGlass.revoke
1353
+ *
1354
+ * Enumerate the active (not revoked, not expired, rows remaining)
1355
+ * grants the caller currently holds. Lookup is keyed via cryptoField's
1356
+ * `computeDerived` so the actor's id never appears in cleartext on the
1357
+ * grants table — the framework hashes via the table's namespaced
1358
+ * derivation. Unauthenticated callers (no actorId on `req`) get an
1359
+ * empty array.
1360
+ *
1361
+ * @opts
1362
+ * req: object, // request carrying the actor identity (req.user.id or req.apiKey.id)
1363
+ *
1364
+ * @example
1365
+ * var grants = await b.breakGlass.listActive({ req: req });
1366
+ * // → [{ id: "bg-...", scopeTable: "patients", scopeColumns: ["ssn"], expiresAt: ..., rowsRemaining: 1, factorType: "totp" }]
1367
+ */
1066
1368
  async function listActive(opts) {
1067
1369
  _requireInit();
1068
1370
  opts = opts || {};
@@ -1110,6 +1412,32 @@ async function listActive(opts) {
1110
1412
  // audit row so post-incident review can distinguish operator-initiated
1111
1413
  // from service-initiated reads.
1112
1414
 
1415
+ /**
1416
+ * @primitive b.breakGlass.unsealRowAsService
1417
+ * @signature b.breakGlass.unsealRowAsService(req, table, rowId, opts)
1418
+ * @since 0.5.0
1419
+ * @status stable
1420
+ * @compliance hipaa, pci-dss, soc2
1421
+ * @related b.breakGlass.policy.set, b.breakGlass.unsealRow
1422
+ *
1423
+ * Machine-account read of a glass-locked row. Bypass is gated by the
1424
+ * policy's `serviceAccountBypass` block — both the verified `req.apiKey.id`
1425
+ * must be on the operator-declared allowlist AND the apiKey must
1426
+ * carry the operator-declared role. Both checks must pass; either
1427
+ * failure emits a denied bypass audit row and throws
1428
+ * `breakglass/bypass-unauthorized`. Each successful bypass emits a
1429
+ * distinct `breakglass.grant.bypass` audit row so post-incident review
1430
+ * separates operator-initiated reads from scheduled-job reads.
1431
+ *
1432
+ * @opts
1433
+ * reason: string, // operator-supplied reason recorded into the audit row
1434
+ *
1435
+ * @example
1436
+ * var row = await b.breakGlass.unsealRowAsService(req, "patients", "patient-001", {
1437
+ * reason: "nightly de-identification job",
1438
+ * });
1439
+ * // → { _id: "patient-001", name: "Alice", ssn: "123-45-6789", ... }
1440
+ */
1113
1441
  async function unsealRowAsService(req, table, rowId, opts) {
1114
1442
  _requireInit();
1115
1443
  opts = opts || {};
@@ -1219,6 +1547,29 @@ async function unsealRowAsService(req, table, rowId, opts) {
1219
1547
  // teams when an account is suspected compromised. Both require
1220
1548
  // admin scope (operator wires via opts.requireScope or their own gate).
1221
1549
 
1550
+ /**
1551
+ * @primitive b.breakGlass.listActiveAll
1552
+ * @signature b.breakGlass.listActiveAll(opts)
1553
+ * @since 0.5.0
1554
+ * @status stable
1555
+ * @related b.breakGlass.listActive, b.breakGlass.revokeAll
1556
+ *
1557
+ * Admin variant of `listActive` — returns every active grant across
1558
+ * every actor. Used by security-team dashboards and offboarding
1559
+ * workflows; operators wire their own gate (`requireScope` or a
1560
+ * middleware) on the calling route so non-admins can't enumerate the
1561
+ * full grant pool. Each call emits a `breakglass.admin.listactiveall`
1562
+ * audit row.
1563
+ *
1564
+ * @opts
1565
+ * table: string, // optional filter — only grants scoped to this table
1566
+ * since: number, // optional issuedAt floor (ms epoch)
1567
+ * callerOpts: object, // forwarded to audit actor resolution
1568
+ *
1569
+ * @example
1570
+ * var all = await b.breakGlass.listActiveAll({ table: "patients" });
1571
+ * // → [{ id: "bg-...", issuedToActorId: "user-42", scopeTable: "patients", ... }]
1572
+ */
1222
1573
  async function listActiveAll(opts) {
1223
1574
  _requireInit();
1224
1575
  opts = opts || {};
@@ -1261,6 +1612,31 @@ async function listActiveAll(opts) {
1261
1612
  return out;
1262
1613
  }
1263
1614
 
1615
+ /**
1616
+ * @primitive b.breakGlass.revokeAll
1617
+ * @signature b.breakGlass.revokeAll(criteria, opts)
1618
+ * @since 0.5.0
1619
+ * @status stable
1620
+ * @related b.breakGlass.revoke, b.breakGlass.listActiveAll
1621
+ *
1622
+ * Mass-revoke grants matching a scope predicate. Refuses to run with
1623
+ * empty criteria — IR teams must name at least one of `actorId` or
1624
+ * `table` so the framework never silently revokes every grant in the
1625
+ * cluster. The to-be-revoked grant ids are snapshotted into the audit
1626
+ * row before the UPDATE, so post-incident timelines have the exact
1627
+ * list. Common shape: revoke every active grant held by a suspected-
1628
+ * compromised account.
1629
+ *
1630
+ * @opts
1631
+ * callerOpts: object, // forwarded to audit actor resolution
1632
+ *
1633
+ * @example
1634
+ * var result = await b.breakGlass.revokeAll(
1635
+ * { actorId: "user-42", reason: "account compromise — IR-2026-0042" },
1636
+ * { callerOpts: { actor: { userId: "soc-on-call" } } }
1637
+ * );
1638
+ * // → { revokedCount: 3 }
1639
+ */
1264
1640
  async function revokeAll(criteria, opts) {
1265
1641
  _requireInit();
1266
1642
  if (!criteria || typeof criteria !== "object") {