@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
package/lib/subject.js CHANGED
@@ -1,28 +1,40 @@
1
1
  "use strict";
2
2
  /**
3
- * Data subject rights primitives — GDPR Articles 15–22, AU Privacy Act
4
- * Privacy Act Review (right to erasure), and HIPAA §164.524.
5
- *
6
- * App schema declares per-table `subjectField` (the column that points to
7
- * the subject) and `personalDataCategories` (semantic tag for RoPA). This
8
- * module then walks every table that knows about a given subject without
9
- * the app having to plumb subject IDs through repository code.
10
- *
11
- * Public API:
12
- * subject.export(subjectId, opts) → { tableName: [unsealed rows] }
13
- * subject.rectify(subjectId, { table, id, changes, reason })
14
- * subject.erase(subjectId, { reason, acknowledgements })
15
- * subject.restrict(subjectId, { on, reason })
16
- * subject.recordObjection(subjectId, { purpose, reason })
17
- * subject.isRestricted(subjectId) boolean
18
- *
19
- * Erasure model: physical row deletion with the audit chain preserved
20
- * (the subject's data rows are gone; the audit_log entries about them
21
- * remain hash-linked). This satisfies GDPR Art. 17 in the strict sense
22
- * (subject data is erased) at the cost of the more granular
23
- * cryptographic-erasure property (per-subject key destruction) the spec
24
- * aspires to — that variant requires per-subject derivation keys, which
25
- * the framework does not currently maintain.
3
+ * @module b.subject
4
+ * @nav Identity
5
+ * @title Subject
6
+ *
7
+ * @intro
8
+ * Data subject (user) lifecycle + DSR (Data Subject Rights) helpers
9
+ * register / lookup / export / erase. Tied to GDPR (Articles 15-22)
10
+ * and CCPA workflows; also covers AU Privacy Act review (right to
11
+ * erasure) and HIPAA §164.524 access requests.
12
+ *
13
+ * App schema declares per-table `subjectField` (the column that points
14
+ * to the subject) and `personalDataCategories` (semantic tag for the
15
+ * Record of Processing Activities). This module then walks every
16
+ * table that knows about a given subject without the app having to
17
+ * plumb subject IDs through repository code.
18
+ *
19
+ * Erasure model: physical row deletion with the audit chain preserved
20
+ * (the subject's data rows are gone; the audit_log entries about them
21
+ * remain hash-linked). `b.subject.erase` satisfies GDPR Art. 17 in
22
+ * the strict sense (subject data is erased). `b.subject.eraseHard`
23
+ * layers cryptographic erasure on top — destroys per-row K_row keys
24
+ * for tables that opted into per-row keying, leaving any residual
25
+ * ciphertext in WAL / replica / backup storage undecryptable even if
26
+ * the operator's vault key is later recovered.
27
+ *
28
+ * Every mutating call routes through `cluster.requireLeader` and
29
+ * writes a structured audit event (`subject.export` / `subject.rectify`
30
+ * / `subject.erase` / `subject.erase_hard` / `subject.restrict` /
31
+ * `subject.objection`). Erasure is additionally gated by the central
32
+ * legal-hold registry (FRCP Rule 26/37(e), GDPR Art 17(3)(e), SEC
33
+ * Rule 17a-4, HIPAA §164.530(j)(2)) — a stale operator attestation
34
+ * cannot override an active hold.
35
+ *
36
+ * @card
37
+ * Data subject (user) lifecycle + DSR (Data Subject Rights) helpers — register / lookup / export / erase.
26
38
  */
27
39
  var { sha3Hash } = require("./crypto");
28
40
  var cryptoField = require("./crypto-field");
@@ -31,6 +43,7 @@ var cluster = require("./cluster");
31
43
  var lazyRequire = require("./lazy-require");
32
44
 
33
45
  var db = lazyRequire(function () { return require("./db"); });
46
+ var legalHold = lazyRequire(function () { return require("./legal-hold"); });
34
47
 
35
48
  // Required acknowledgements before subject.erase will run. Operator must
36
49
  // explicitly attest each one to confirm no statutory retention or active
@@ -39,6 +52,67 @@ var REQUIRED_ERASE_ACKS = ["no-litigation-hold", "no-statutory-retention-require
39
52
 
40
53
  // ---- Export (Art. 15, Art. 20) ----
41
54
 
55
+ /**
56
+ * @primitive b.subject.export
57
+ * @signature b.subject.export(subjectId, opts?)
58
+ * @since 0.1.0
59
+ * @status stable
60
+ * @compliance gdpr, ccpa, hipaa
61
+ * @related b.subject.exportData, b.subject.rectify, b.subject.erase
62
+ *
63
+ * GDPR Art. 15 (right of access) + Art. 20 (data portability) +
64
+ * HIPAA §164.524 access request. Walks every table whose schema
65
+ * declared a `subjectField` pointing at the subject identifier and
66
+ * returns `{ tableName: [unsealedRows] }`. Sealed columns are
67
+ * unsealed in-memory for the export; derived-hash columns are used
68
+ * for predicate lookup so plaintext subject IDs never need to land
69
+ * in a query string. Writes a `subject.export` audit event listing
70
+ * the tables touched.
71
+ *
72
+ * @opts
73
+ * include: "all" | string[], // table allowlist; "all" exports every subjectField-tagged table
74
+ * reason: string, // ticket reference recorded in the audit event
75
+ *
76
+ * @example
77
+ * var dump = b.subject.export("user-4471", {
78
+ * include: "all",
79
+ * reason: "GDPR Art. 15 access request 2026-05-08 ticket #4471",
80
+ * });
81
+ * Object.keys(dump);
82
+ * // → ["users", "orders", "audit_log"]
83
+ *
84
+ * var ordersOnly = b.subject.export("user-4471", {
85
+ * include: ["orders"],
86
+ * reason: "GDPR Art. 20 portability subset",
87
+ * });
88
+ */
89
+
90
+ /**
91
+ * @primitive b.subject.exportData
92
+ * @signature b.subject.exportData(subjectId, opts?)
93
+ * @since 0.1.0
94
+ * @status stable
95
+ * @compliance gdpr, ccpa, hipaa
96
+ * @related b.subject.export
97
+ *
98
+ * Identical behaviour to `b.subject.export`. Shipped as a non-reserved
99
+ * alias because some downstream toolchains (older bundlers, TypeScript
100
+ * `import { export }` parsing, JSON-serialised method lists) trip on
101
+ * the `export` keyword. New code should prefer `b.subject.export`;
102
+ * `exportData` is kept for tool-friendliness.
103
+ *
104
+ * @opts
105
+ * include: "all" | string[], // table allowlist
106
+ * reason: string, // ticket reference recorded in the audit event
107
+ *
108
+ * @example
109
+ * var dump = b.subject.exportData("user-4471", {
110
+ * include: "all",
111
+ * reason: "GDPR Art. 15 access request",
112
+ * });
113
+ * Array.isArray(dump.users || []);
114
+ * // → true
115
+ */
42
116
  function exportData(subjectId, opts) {
43
117
  if (!subjectId) throw new Error("subject.export requires a subjectId");
44
118
  opts = opts || {};
@@ -91,6 +165,36 @@ function _getDerivedFieldName(tableName, sourceField) {
91
165
 
92
166
  // ---- Rectify (Art. 16) ----
93
167
 
168
+ /**
169
+ * @primitive b.subject.rectify
170
+ * @signature b.subject.rectify(subjectId, opts)
171
+ * @since 0.1.0
172
+ * @status stable
173
+ * @compliance gdpr, ccpa, hipaa
174
+ * @related b.subject.export, b.subject.erase
175
+ *
176
+ * GDPR Art. 16 (right to rectification). Updates a single row in a
177
+ * single table on behalf of the subject and emits an audit event
178
+ * carrying the before/after values for the changed fields. Leader-only
179
+ * in cluster mode (`cluster.requireLeader`). Throws when the row
180
+ * cannot be located or `opts` is missing required keys.
181
+ *
182
+ * @opts
183
+ * table: string, // table name (must declare subjectField in schema)
184
+ * id: string, // _id of the row to update
185
+ * changes: object, // { fieldName: newValue, ... }
186
+ * reason: string, // ticket reference recorded in the audit event
187
+ *
188
+ * @example
189
+ * var ok = b.subject.rectify("user-4471", {
190
+ * table: "users",
191
+ * id: "row-9912",
192
+ * changes: { email: "new@example.com", displayName: "Jane Roe" },
193
+ * reason: "GDPR Art. 16 rectification ticket #5512",
194
+ * });
195
+ * ok;
196
+ * // → true
197
+ */
94
198
  function rectify(subjectId, opts) {
95
199
  cluster.requireLeader();
96
200
  if (!subjectId) throw new Error("subject.rectify requires a subjectId");
@@ -132,6 +236,49 @@ function rectify(subjectId, opts) {
132
236
 
133
237
  // ---- Erase (Art. 17 right to be forgotten) ----
134
238
 
239
+ /**
240
+ * @primitive b.subject.erase
241
+ * @signature b.subject.erase(subjectId, opts)
242
+ * @since 0.1.0
243
+ * @status stable
244
+ * @compliance gdpr, ccpa, hipaa
245
+ * @related b.subject.eraseHard, b.subject.export, b.subject.restrict
246
+ *
247
+ * GDPR Art. 17 (right to be forgotten). Physical row deletion across
248
+ * every subjectField-tagged table; the audit chain remains intact
249
+ * (entries about the subject stay hash-linked even after the subject's
250
+ * data rows are gone). Leader-only.
251
+ *
252
+ * Two gates layer in front of the deletion: every operator-supplied
253
+ * acknowledgement in `REQUIRED_ERASE_ACKS` must be present
254
+ * (`no-litigation-hold`, `no-statutory-retention-required`), AND the
255
+ * central legal-hold registry must report no active hold for the
256
+ * subject. The registry is authoritative — a stale attestation cannot
257
+ * override an active hold (FRCP Rule 26/37(e), GDPR Art 17(3)(e),
258
+ * SEC Rule 17a-4, HIPAA §164.530(j)(2)).
259
+ *
260
+ * Returns `{ rowsDeleted, perTable }`. Use `b.subject.eraseHard` when
261
+ * residual ciphertext in WAL / replicas / backups must also be made
262
+ * undecryptable.
263
+ *
264
+ * @opts
265
+ * reason: string, // ticket reference recorded in the audit event
266
+ * acknowledgements: string[], // must include every entry in REQUIRED_ERASE_ACKS
267
+ * legalHold: object, // optional override for testing; defaults to the framework registry
268
+ *
269
+ * @example
270
+ * var result = b.subject.erase("user-4471", {
271
+ * reason: "GDPR Art. 17 request 2026-05-08 ticket #4471",
272
+ * acknowledgements: [
273
+ * "no-litigation-hold",
274
+ * "no-statutory-retention-required",
275
+ * ],
276
+ * });
277
+ * result.rowsDeleted;
278
+ * // → 12
279
+ * Object.keys(result.perTable);
280
+ * // → ["users", "orders", "preferences"]
281
+ */
135
282
  function erase(subjectId, opts) {
136
283
  cluster.requireLeader();
137
284
  if (!subjectId) throw new Error("subject.erase requires a subjectId");
@@ -150,6 +297,28 @@ function erase(subjectId, opts) {
150
297
  }
151
298
  }
152
299
 
300
+ // Authoritative legal-hold gate. Even when the operator passed the
301
+ // "no-litigation-hold" attestation, the central registry is the
302
+ // source of truth — a stale attestation cannot override an active
303
+ // hold. Per FRCP Rule 26/37(e), GDPR Art 17(3)(e), SEC Rule 17a-4,
304
+ // HIPAA §164.530(j)(2).
305
+ var holds = (opts && opts.legalHold) || legalHold()._getSingleton();
306
+ if (holds && holds.isHeld(subjectId)) {
307
+ var holdInfo = holds.get(subjectId) || {};
308
+ _writeAudit("subject.erase", subjectId, "denied", {
309
+ requestReason: opts.reason,
310
+ reason: "legal-hold-active",
311
+ heldSince: holdInfo.placedAt,
312
+ citation: holdInfo.citation,
313
+ });
314
+ throw new Error(
315
+ "subject.erase: subject '" + subjectId + "' is on legal hold (" +
316
+ (holdInfo.citation || "operator-defined") + "; placed " +
317
+ new Date(holdInfo.placedAt).toISOString() +
318
+ "). Release the hold before erasure."
319
+ );
320
+ }
321
+
153
322
  var tables = db()._getSubjectTables();
154
323
  var totalDeleted = 0;
155
324
  var perTable = {};
@@ -186,8 +355,174 @@ function erase(subjectId, opts) {
186
355
  return { rowsDeleted: totalDeleted, perTable: perTable };
187
356
  }
188
357
 
358
+ // ---- Crypto-shred erase (Art. 17 + WAL/replica residual closure) ----
359
+ //
360
+ // F-RTBF-3 — when a table opts into per-row keying via
361
+ // b.cryptoField.declarePerRowKey, this primitive deletes the
362
+ // per-row K_row entries from _blamejs_per_row_keys, leaving any
363
+ // residual ciphertext in WAL / replica / backup storage
364
+ // undecryptable even if the operator's vault key is later
365
+ // recovered. Combined with a row DELETE + REINDEX, this is the
366
+ // strongest GDPR Art. 17 erasure shape the framework offers.
367
+
368
+ /**
369
+ * @primitive b.subject.eraseHard
370
+ * @signature b.subject.eraseHard(subjectId, opts)
371
+ * @since 0.8.44
372
+ * @status stable
373
+ * @compliance gdpr, ccpa, hipaa
374
+ * @related b.subject.erase, b.cryptoField.declarePerRowKey
375
+ *
376
+ * Cryptographic erasure on top of `b.subject.erase`. For tables that
377
+ * opted into per-row keying via `b.cryptoField.declarePerRowKey`, the
378
+ * call destroys each row's K_row entry from `_blamejs_per_row_keys`
379
+ * before the row DELETE, then runs `REINDEX` on the table so B-tree
380
+ * pages holding the deleted index entries are rebuilt. Residual
381
+ * ciphertext in WAL / replicas / backup archives stays undecryptable
382
+ * even if the operator's vault key is later recovered — the strongest
383
+ * Art. 17 erasure shape the framework offers.
384
+ *
385
+ * Same legal-hold + acknowledgement gates as `b.subject.erase`.
386
+ * Leader-only. Returns `{ rowsDeleted, perRowKeysDestroyed, perTable }`.
387
+ *
388
+ * @opts
389
+ * reason: string, // ticket reference recorded in the audit event
390
+ * acknowledgements: string[], // must include every entry in REQUIRED_ERASE_ACKS
391
+ * legalHold: object, // optional override for testing
392
+ *
393
+ * @example
394
+ * var result = b.subject.eraseHard("user-4471", {
395
+ * reason: "GDPR Art. 17 cryptographic erasure ticket #4471",
396
+ * acknowledgements: [
397
+ * "no-litigation-hold",
398
+ * "no-statutory-retention-required",
399
+ * ],
400
+ * });
401
+ * result.rowsDeleted;
402
+ * // → 12
403
+ * result.perRowKeysDestroyed;
404
+ * // → 8
405
+ */
406
+ function eraseHard(subjectId, opts) {
407
+ cluster.requireLeader();
408
+ if (!subjectId) throw new Error("subject.eraseHard requires a subjectId");
409
+ opts = opts || {};
410
+ if (!opts.reason) {
411
+ throw new Error("subject.eraseHard requires { reason } — e.g. 'GDPR Art. 17 ticket #4471'");
412
+ }
413
+ if (!Array.isArray(opts.acknowledgements)) {
414
+ throw new Error("subject.eraseHard requires { acknowledgements: [...] } — see REQUIRED_ERASE_ACKS");
415
+ }
416
+ for (var i = 0; i < REQUIRED_ERASE_ACKS.length; i++) {
417
+ if (opts.acknowledgements.indexOf(REQUIRED_ERASE_ACKS[i]) === -1) {
418
+ throw new Error(
419
+ "subject.eraseHard: missing required acknowledgement '" +
420
+ REQUIRED_ERASE_ACKS[i] + "'");
421
+ }
422
+ }
423
+ // Authoritative legal-hold gate.
424
+ var holds = (opts && opts.legalHold) || legalHold()._getSingleton();
425
+ if (holds && holds.isHeld(subjectId)) {
426
+ var holdInfo = holds.get(subjectId) || {};
427
+ _writeAudit("subject.erase_hard", subjectId, "denied", {
428
+ requestReason: opts.reason,
429
+ reason: "legal-hold-active",
430
+ heldSince: holdInfo.placedAt,
431
+ citation: holdInfo.citation,
432
+ });
433
+ throw new Error(
434
+ "subject.eraseHard: subject '" + subjectId + "' is on legal hold (" +
435
+ (holdInfo.citation || "operator-defined") + "). Release the hold first.");
436
+ }
437
+
438
+ var tables = db()._getSubjectTables();
439
+ var perTable = {};
440
+ var perRowKeysDestroyed = 0;
441
+ var totalDeleted = 0;
442
+
443
+ db().transaction(function () {
444
+ for (var t = 0; t < tables.length; t++) {
445
+ var spec = tables[t];
446
+ var hash = db().hashFor(spec.name, spec.subjectField, subjectId);
447
+ var pred;
448
+ if (hash) {
449
+ var derivedField = _getDerivedFieldName(spec.name, spec.subjectField);
450
+ if (derivedField) {
451
+ pred = {}; pred[derivedField] = hash;
452
+ } else {
453
+ pred = {}; pred[spec.subjectField] = subjectId;
454
+ }
455
+ } else {
456
+ pred = {}; pred[spec.subjectField] = subjectId;
457
+ }
458
+ // Find rows so we can destroy their per-row keys before delete.
459
+ var rows = db().from(spec.name).where(pred).all();
460
+ if (cryptoField.hasPerRowKey(spec.name)) {
461
+ for (var r = 0; r < rows.length; r++) {
462
+ var rowId = rows[r]._id;
463
+ if (rowId) {
464
+ var dr = cryptoField.destroyPerRowKey(spec.name, rowId, db());
465
+ perRowKeysDestroyed += (dr && dr.destroyed) || 0;
466
+ }
467
+ }
468
+ }
469
+ var deleted = db().from(spec.name).where(pred).deleteMany();
470
+ totalDeleted += deleted;
471
+ perTable[spec.name] = deleted;
472
+ // REINDEX the table so B-tree pages holding the deleted row's
473
+ // index entries are rebuilt — closes the F-RTBF-2 residual class.
474
+ try { db().runSql('REINDEX "' + spec.name + '"'); } // allow:identifier-from-schema — table name comes from FRAMEWORK_SCHEMA
475
+ catch (_e) { /* cluster mode / unsupported dialect */ }
476
+ }
477
+ _markErased(subjectId);
478
+ });
479
+
480
+ _writeAudit("subject.erase_hard", subjectId, "success", {
481
+ requestReason: opts.reason,
482
+ rowsDeleted: totalDeleted,
483
+ perRowKeysDestroyed: perRowKeysDestroyed,
484
+ perTable: perTable,
485
+ acknowledgements: opts.acknowledgements,
486
+ });
487
+ return {
488
+ rowsDeleted: totalDeleted,
489
+ perRowKeysDestroyed: perRowKeysDestroyed,
490
+ perTable: perTable,
491
+ };
492
+ }
493
+
189
494
  // ---- Restrict (Art. 18) ----
190
495
 
496
+ /**
497
+ * @primitive b.subject.restrict
498
+ * @signature b.subject.restrict(subjectId, opts)
499
+ * @since 0.1.0
500
+ * @status stable
501
+ * @compliance gdpr
502
+ * @related b.subject.isRestricted, b.subject.recordObjection
503
+ *
504
+ * GDPR Art. 18 (right to restriction of processing). Toggles a flag in
505
+ * `_blamejs_subject_restrictions` keyed by the subject-id hash;
506
+ * downstream code consults `b.subject.isRestricted` before processing.
507
+ * Leader-only. The subject ID is hashed before storage so the table
508
+ * carries no plaintext subject identifiers.
509
+ *
510
+ * @opts
511
+ * on: boolean, // true to apply restriction, false to lift
512
+ * reason: string, // ticket reference recorded in the audit event
513
+ *
514
+ * @example
515
+ * b.subject.restrict("user-4471", {
516
+ * on: true,
517
+ * reason: "GDPR Art. 18 contested-accuracy hold ticket #6612",
518
+ * });
519
+ * b.subject.isRestricted("user-4471");
520
+ * // → true
521
+ *
522
+ * b.subject.restrict("user-4471", { on: false, reason: "dispute resolved" });
523
+ * b.subject.isRestricted("user-4471");
524
+ * // → false
525
+ */
191
526
  function restrict(subjectId, opts) {
192
527
  cluster.requireLeader();
193
528
  if (!subjectId) throw new Error("subject.restrict requires a subjectId");
@@ -217,6 +552,26 @@ function restrict(subjectId, opts) {
217
552
  return true;
218
553
  }
219
554
 
555
+ /**
556
+ * @primitive b.subject.isRestricted
557
+ * @signature b.subject.isRestricted(subjectId)
558
+ * @since 0.1.0
559
+ * @status stable
560
+ * @compliance gdpr
561
+ * @related b.subject.restrict
562
+ *
563
+ * Cheap read-side check — returns `true` when the subject currently
564
+ * has an active GDPR Art. 18 restriction. Safe to call on any node
565
+ * (no leader gate); reads from `_blamejs_subject_restrictions` via
566
+ * the indexed subject-id hash.
567
+ *
568
+ * @example
569
+ * if (b.subject.isRestricted("user-4471")) {
570
+ * throw new Error("processing paused under GDPR Art. 18");
571
+ * }
572
+ * b.subject.isRestricted("user-9999");
573
+ * // → false
574
+ */
220
575
  function isRestricted(subjectId) {
221
576
  if (!subjectId) return false;
222
577
  var row = db().prepare(
@@ -227,6 +582,32 @@ function isRestricted(subjectId) {
227
582
 
228
583
  // ---- Object (Art. 21) ----
229
584
 
585
+ /**
586
+ * @primitive b.subject.recordObjection
587
+ * @signature b.subject.recordObjection(subjectId, opts)
588
+ * @since 0.1.0
589
+ * @status stable
590
+ * @compliance gdpr
591
+ * @related b.subject.restrict
592
+ *
593
+ * GDPR Art. 21 (right to object). Records a structured audit event
594
+ * (`subject.objection`) naming the processing purpose the subject is
595
+ * objecting to plus an optional free-form reason. The framework does
596
+ * not enforce the objection automatically — operators wire the
597
+ * downstream consequence (suppress marketing send, exclude from
598
+ * profiling, etc.) into their own pipeline. Leader-only.
599
+ *
600
+ * @opts
601
+ * purpose: string, // e.g. "marketing", "profiling", "automated-decisioning"
602
+ * reason: string, // optional free-form ticket reference
603
+ *
604
+ * @example
605
+ * b.subject.recordObjection("user-4471", {
606
+ * purpose: "marketing",
607
+ * reason: "GDPR Art. 21 opt-out ticket #7780",
608
+ * });
609
+ * // → true
610
+ */
230
611
  function recordObjection(subjectId, opts) {
231
612
  cluster.requireLeader();
232
613
  if (!subjectId) throw new Error("subject.recordObjection requires a subjectId");
@@ -273,6 +654,7 @@ module.exports = {
273
654
  exportData: exportData, // alias — `export` is a reserved word in some toolchains
274
655
  rectify: rectify,
275
656
  erase: erase,
657
+ eraseHard: eraseHard,
276
658
  restrict: restrict,
277
659
  isRestricted: isRestricted,
278
660
  recordObjection: recordObjection,