@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.
- package/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/subject.js
CHANGED
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* subject
|
|
15
|
-
*
|
|
16
|
-
* subject
|
|
17
|
-
* subject
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* (subject data is erased)
|
|
23
|
-
* cryptographic
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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,
|