@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
@@ -0,0 +1,368 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.drRunbook
4
+ * @nav Compliance
5
+ * @title DR Runbook
6
+ *
7
+ * @intro
8
+ * Disaster-recovery runbook executor — composes pre-recorded
9
+ * regulatory steps, operator confirmation gates, and the framework's
10
+ * audit chain into a posture-appropriate Markdown runbook a
11
+ * regulator can read alongside `b.audit`.
12
+ *
13
+ * `b.drRunbook.emit` walks the operator's posture (one of `hipaa`,
14
+ * `pci-dss`, `gdpr`, `soc2`, `dora`), pulls breach-disclosure
15
+ * deadlines from the operator-supplied `b.budr` registry, summarizes
16
+ * `b.backup` configuration, captures `b.cluster` topology, and
17
+ * writes a single Markdown file under `outDir`. Each emit also
18
+ * records a `dr.runbook.emitted` audit event with the posture, the
19
+ * output path, and the section count — so the operator
20
+ * confirmation chain has an immutable record of which runbook
21
+ * version was last produced.
22
+ *
23
+ * Posture-driven citations:
24
+ *
25
+ * - hipaa — 45 CFR §164.308(a)(7) contingency plan +
26
+ * §164.310(a)(2)(i) facility-recovery checklist
27
+ * - pci-dss — PCI DSS v4.0.1 Req. 12.10 (containment / eradication /
28
+ * recovery)
29
+ * - gdpr — Regulation (EU) 2016/679 Art. 32, 33 (72h notification),
30
+ * 34
31
+ * - soc2 — AICPA TSC CC7.4 / CC9.1 (recovery objectives + change
32
+ * control)
33
+ * - dora — Regulation (EU) 2022/2554 Art. 11, 12, 24
34
+ *
35
+ * The runbook is plain Markdown — operators commit it under
36
+ * `docs/dr/` and version it with their service. Re-emitting
37
+ * overwrites the file in place via `atomicFile.writeSync`, so an
38
+ * operator-supplied template change ships through git review before
39
+ * the runbook lands.
40
+ *
41
+ * @card
42
+ * Disaster-recovery runbook executor — composes pre-recorded regulatory steps, operator confirmation gates, and the framework's audit chain into a posture-appropriate Markdown runbook a regulator can read alongside `b.audit`.
43
+ */
44
+
45
+ var fs = require("fs");
46
+ var path = require("path");
47
+ var C = require("./constants");
48
+ var atomicFile = require("./atomic-file");
49
+ var lazyRequire = require("./lazy-require");
50
+ var validateOpts = require("./validate-opts");
51
+ var { defineClass } = require("./framework-error");
52
+
53
+ var DrRunbookError = defineClass("DrRunbookError", { alwaysPermanent: true });
54
+
55
+ var audit = lazyRequire(function () { return require("./audit"); });
56
+
57
+ // Posture-specific content blocks. The framework owns the regulatory
58
+ // citations; operators fill in environment-specific commands.
59
+ var POSTURE_BLOCKS = {
60
+ "hipaa": {
61
+ citation: "45 CFR §164.308(a)(7)(ii)(B-D); §164.310(a)(2)(i)",
62
+ summary: "HIPAA Security Rule contingency plan — covers data-backup, " +
63
+ "disaster recovery, emergency-mode operation, testing/revision, " +
64
+ "and applications/data-criticality analysis.",
65
+ rtoLabel: "Maximum Tolerable Downtime",
66
+ requiredSections: ["Backup", "Restore", "Test", "Roles", "Notification"],
67
+ },
68
+ "pci-dss": {
69
+ citation: "PCI DSS v4.0.1 Requirement 12.10",
70
+ summary: "PCI DSS incident-response capability — preserves cardholder " +
71
+ "data integrity through detection, containment, eradication, " +
72
+ "recovery, post-incident review.",
73
+ rtoLabel: "Recovery Time Objective",
74
+ requiredSections: ["Backup", "Restore", "Test", "Roles", "Notification", "Card-Brand-Notification"],
75
+ },
76
+ "gdpr": {
77
+ citation: "Regulation (EU) 2016/679 Articles 32, 33, 34",
78
+ summary: "GDPR security-of-processing + breach-notification timeline. " +
79
+ "Personal-data breach → supervisory authority within 72h, " +
80
+ "data subjects without undue delay where high-risk.",
81
+ rtoLabel: "Recovery Time Objective",
82
+ requiredSections: ["Backup", "Restore", "Test", "Roles", "Notification", "Article-33-Timeline"],
83
+ },
84
+ "soc2": {
85
+ citation: "AICPA TSC CC7.4 / CC9.1",
86
+ summary: "SOC 2 availability commitments — recovery objectives, change " +
87
+ "control around recovery procedures, and continuous monitoring.",
88
+ rtoLabel: "Recovery Time Objective",
89
+ requiredSections: ["Backup", "Restore", "Test", "Roles", "Change-Control"],
90
+ },
91
+ "dora": {
92
+ citation: "Regulation (EU) 2022/2554 Articles 11, 12, 24",
93
+ summary: "DORA operational-resilience plan — covers ICT business " +
94
+ "continuity, response/recovery, third-party-risk, and major-" +
95
+ "incident reporting (Article 17).",
96
+ rtoLabel: "Recovery Time Objective",
97
+ requiredSections: ["Backup", "Restore", "Test", "Roles", "Notification", "Article-17-Timeline"],
98
+ },
99
+ };
100
+
101
+ function _formatMs(ms) {
102
+ if (typeof ms !== "number" || !isFinite(ms) || ms < 0) return "—";
103
+ if (ms >= C.TIME.hours(1)) return (ms / C.TIME.hours(1)).toFixed(2) + "h";
104
+ if (ms >= C.TIME.minutes(1)) return (ms / C.TIME.minutes(1)).toFixed(2) + "m";
105
+ return Math.floor(ms / C.TIME.seconds(1)) + "s";
106
+ }
107
+
108
+ function _section(title, body) {
109
+ return "## " + title + "\n\n" + body + "\n";
110
+ }
111
+
112
+ function _renderRoles(contacts) {
113
+ contacts = contacts || {};
114
+ var keys = Object.keys(contacts);
115
+ if (keys.length === 0) {
116
+ return "_No contacts supplied. Populate `contacts:` opt at emit time " +
117
+ "before this runbook is operator-actionable._";
118
+ }
119
+ var rows = ["| Role | Contact |", "| ---- | ------- |"];
120
+ for (var i = 0; i < keys.length; i++) {
121
+ rows.push("| " + keys[i] + " | " + String(contacts[keys[i]]) + " |");
122
+ }
123
+ return rows.join("\n");
124
+ }
125
+
126
+ function _renderServices(services, postureBlock) {
127
+ if (!services || services.length === 0) {
128
+ return "_No service-level recovery objectives supplied._";
129
+ }
130
+ var lines = [
131
+ "| Service | " + (postureBlock.rtoLabel || "RTO") + " | RPO |",
132
+ "| ------- | ----- | --- |",
133
+ ];
134
+ for (var i = 0; i < services.length; i++) {
135
+ var s = services[i];
136
+ lines.push("| " + (s.name || "—") + " | " + _formatMs(s.rtoMs) +
137
+ " | " + _formatMs(s.rpoMs) + " |");
138
+ }
139
+ return lines.join("\n");
140
+ }
141
+
142
+ function _renderBackup(backup) {
143
+ if (!backup) {
144
+ return "_No backup primitive bound. Wire `backup:` opt or document " +
145
+ "operator-managed backup procedure here._";
146
+ }
147
+ var name = (backup.storage && backup.storage.name) || "custom";
148
+ return "Backup engine: `b.backup` with **" + name + "** storage backend. " +
149
+ "Run `await backup.run()` to take an ad-hoc snapshot; the " +
150
+ "framework's scheduler emits scheduled snapshots on the operator's " +
151
+ "cron expression. Bundle integrity: SHA3-512 plaintext checksum + " +
152
+ "per-blob XChaCha20-Poly1305 AEAD + ML-DSA-87 signed manifest.";
153
+ }
154
+
155
+ function _renderCluster(cluster) {
156
+ if (!cluster) {
157
+ return "_No cluster primitive bound. Single-node deployment: backups + " +
158
+ "audit-chain + restore-bundle are the only recovery surface._";
159
+ }
160
+ // cluster.isClusterMode is the operator-stable accessor; everything
161
+ // else is internal.
162
+ var isCluster = false;
163
+ try { isCluster = !!(cluster.isClusterMode && cluster.isClusterMode()); }
164
+ catch (_e) { /* tolerate unwired cluster handle */ }
165
+ if (!isCluster) {
166
+ return "Cluster module imported but currently in **single-node** mode. " +
167
+ "Recovery procedure is point-in-time restore from the most " +
168
+ "recent backup bundle.";
169
+ }
170
+ return "Cluster mode active. Recovery procedure:\n\n" +
171
+ "1. Verify external-db reachability (`SELECT 1` against the " +
172
+ "operator's pool).\n" +
173
+ "2. Confirm leader-election keepalive — a fresh boot must " +
174
+ "stand-down for an existing leader.\n" +
175
+ "3. On total-cluster loss, restore the framework's local DB on a " +
176
+ "single node FIRST, then bring secondaries online.\n";
177
+ }
178
+
179
+ function _renderBudrDeadlines(budrInstance, posture) {
180
+ if (!budrInstance) {
181
+ return "_No b.budr instance bound. Wire `budr:` opt to surface posture-" +
182
+ "specific breach-disclosure deadlines._";
183
+ }
184
+ // budr exports list() — pull every declaration matching the posture so
185
+ // the runbook surfaces exactly which deadlines fire on a confirmed
186
+ // breach.
187
+ var entries;
188
+ try { entries = budrInstance.list && budrInstance.list({ posture: posture }); }
189
+ catch (_e) { entries = null; }
190
+ if (!entries || entries.length === 0) {
191
+ return "_No breach-disclosure deadlines registered under posture `" +
192
+ posture + "`. Register via `b.budr.declare(...)` for the framework " +
193
+ "to surface them here._";
194
+ }
195
+ var lines = ["| Regulator | Trigger | Deadline |", "| --------- | ------- | -------- |"];
196
+ for (var i = 0; i < entries.length; i++) {
197
+ var e = entries[i];
198
+ lines.push("| " + (e.regulator || "—") + " | " + (e.trigger || "—") +
199
+ " | " + (e.deadline || "—") + " |");
200
+ }
201
+ return lines.join("\n");
202
+ }
203
+
204
+ function _renderTest(posture) {
205
+ return "Backup-restore drills MUST run periodically — register through " +
206
+ "`b.backup.scheduleTest({ cron, restoreTo, verify, posture: '" +
207
+ posture + "' })`. The framework emits `backup.test.passed` / " +
208
+ "`backup.test.failed` so a missed drill is visible in the audit " +
209
+ "chain. Recommended cadence:\n\n" +
210
+ "- HIPAA §164.308(a)(7)(ii)(D): documented periodic tests.\n" +
211
+ "- PCI DSS 12.10.2: annual + after major changes.\n" +
212
+ "- DORA Art. 24: at least annually.\n";
213
+ }
214
+
215
+ /**
216
+ * @primitive b.drRunbook.emit
217
+ * @signature b.drRunbook.emit(opts)
218
+ * @since 0.7.25
219
+ * @status stable
220
+ * @compliance hipaa, pci-dss, gdpr, soc2, dora
221
+ * @related b.budr.declare, b.audit.safeEmit
222
+ *
223
+ * Render and write the posture-specific runbook. Returns
224
+ * `{ paths, posture, sectionCount }` after the file lands at
225
+ * `outDir/<filename>` (default filename: `runbook-<posture>.md`).
226
+ *
227
+ * Service-level recovery objectives are emitted as a Markdown table
228
+ * keyed off `services[].rtoMs` and `services[].rpoMs`; values are
229
+ * formatted via `b.constants.TIME` granularity (hours / minutes /
230
+ * seconds). Optional bindings (`budr`, `cluster`, `backup`) populate
231
+ * matching sections when wired and surface a `_No <X> bound_`
232
+ * placeholder otherwise so the runbook never silently drops a
233
+ * required section.
234
+ *
235
+ * Throws `DrRunbookError("drRunbook/unknown-posture")` when `posture`
236
+ * is not in the supported list.
237
+ *
238
+ * @opts
239
+ * outDir: string, // directory to write into (required)
240
+ * posture: "hipaa"|"pci-dss"|"gdpr"|"soc2"|"dora",
241
+ * services: Array<{name, rtoMs, rpoMs}>,
242
+ * rtoMs: number, // service-level RTO in ms
243
+ * rpoMs: number, // service-level RPO in ms
244
+ * contacts: object, // role -> contact string
245
+ * budr: object, // b.budr-shaped registry
246
+ * cluster: object, // b.cluster handle
247
+ * backup: object, // b.backup handle
248
+ * audit: boolean, // default: true
249
+ * filename: string, // override `runbook-<posture>.md`
250
+ *
251
+ * @example
252
+ * var report = await b.drRunbook.emit({
253
+ * outDir: "/tmp/blamejs-runbook-demo",
254
+ * posture: "hipaa",
255
+ * services: [
256
+ * { name: "api-edge", rtoMs: b.constants.TIME.minutes(15), rpoMs: b.constants.TIME.minutes(5) },
257
+ * ],
258
+ * rtoMs: b.constants.TIME.hours(4),
259
+ * rpoMs: b.constants.TIME.minutes(15),
260
+ * contacts: { incidentCommander: "alice@example.com" },
261
+ * audit: false,
262
+ * });
263
+ * report.posture; // → "hipaa"
264
+ * report.sectionCount; // → 9
265
+ * report.paths.length; // → 1
266
+ */
267
+ async function emit(opts) {
268
+ validateOpts.requireObject(opts, "drRunbook.emit", DrRunbookError);
269
+ validateOpts(opts, [
270
+ "outDir", "posture", "services", "rtoMs", "rpoMs",
271
+ "contacts", "budr", "cluster", "backup", "audit", "filename",
272
+ ], "drRunbook.emit");
273
+
274
+ validateOpts.requireNonEmptyString(opts.outDir,
275
+ "drRunbook.emit: outDir", DrRunbookError, "drRunbook/no-outdir");
276
+ validateOpts.requireNonEmptyString(opts.posture,
277
+ "drRunbook.emit: posture", DrRunbookError, "drRunbook/no-posture");
278
+ if (!POSTURE_BLOCKS[opts.posture]) {
279
+ throw new DrRunbookError("drRunbook/unknown-posture",
280
+ "drRunbook.emit: posture '" + opts.posture + "' not in supported list (" +
281
+ Object.keys(POSTURE_BLOCKS).join(", ") + ")");
282
+ }
283
+ if (opts.services !== undefined && !Array.isArray(opts.services)) {
284
+ throw new DrRunbookError("drRunbook/bad-services",
285
+ "drRunbook.emit: services must be an array of {name, rtoMs, rpoMs}");
286
+ }
287
+ validateOpts.optionalPositiveFinite(opts.rtoMs,
288
+ "drRunbook.emit: rtoMs", DrRunbookError, "drRunbook/bad-rto");
289
+ validateOpts.optionalPositiveFinite(opts.rpoMs,
290
+ "drRunbook.emit: rpoMs", DrRunbookError, "drRunbook/bad-rpo");
291
+
292
+ var auditOn = opts.audit !== false;
293
+ var postureBlock = POSTURE_BLOCKS[opts.posture];
294
+
295
+ var sections = [];
296
+ sections.push("# Disaster Recovery Runbook — " + opts.posture.toUpperCase());
297
+ sections.push("");
298
+ sections.push("**Posture citation:** " + postureBlock.citation);
299
+ sections.push("");
300
+ sections.push("**Generated:** " + new Date().toISOString());
301
+ sections.push("");
302
+ sections.push(postureBlock.summary);
303
+ sections.push("");
304
+ sections.push(_section("Recovery Objectives",
305
+ "**Service-level RTO:** " + _formatMs(opts.rtoMs) + " \n" +
306
+ "**Service-level RPO:** " + _formatMs(opts.rpoMs) + "\n\n" +
307
+ _renderServices(opts.services, postureBlock)));
308
+ sections.push(_section("Roles & Contacts", _renderRoles(opts.contacts)));
309
+ sections.push(_section("Backup", _renderBackup(opts.backup)));
310
+ sections.push(_section("Cluster Topology", _renderCluster(opts.cluster)));
311
+ sections.push(_section("Restore Procedure",
312
+ "1. Identify the most recent verified backup bundle: " +
313
+ "`await backup.list()` → highest bundleId.\n" +
314
+ "2. Verify the manifest signature: " +
315
+ "`await b.backupBundle.verifyManifestSignature(bundle, opts)`.\n" +
316
+ "3. Restore staging dir: `await backup.read(bundleId, '/restore/staging')`.\n" +
317
+ "4. Decrypt + verify: `await b.restoreBundle.extract({ bundleDir, " +
318
+ "passphrase, outDir, vaultKeyJsonOnly: false })`.\n" +
319
+ "5. Verify audit chain on the restored DB: " +
320
+ "`await b.auditChain.verify({ db: restoredDb })`.\n" +
321
+ "6. Resume cluster operation only after the chain verifies.\n"));
322
+ sections.push(_section("Backup-Restore Test", _renderTest(opts.posture)));
323
+ sections.push(_section("Breach-Disclosure Deadlines",
324
+ _renderBudrDeadlines(opts.budr, opts.posture)));
325
+ sections.push(_section("Notification & Reporting",
326
+ "Operator wires the disclosure-channel routing (regulator portals, " +
327
+ "press release, customer email, status-page) outside this primitive. " +
328
+ "The framework's `b.audit` chain is the source-of-truth timeline " +
329
+ "regulators expect; preserve it across the recovery."));
330
+ sections.push(_section("Posture-Specific Required Sections",
331
+ "- " + postureBlock.requiredSections.join("\n- ")));
332
+
333
+ var body = sections.join("\n");
334
+
335
+ // Ensure outDir exists.
336
+ if (!fs.existsSync(opts.outDir)) {
337
+ fs.mkdirSync(opts.outDir, { recursive: true });
338
+ }
339
+ var filename = opts.filename || ("runbook-" + opts.posture + ".md");
340
+ var outPath = path.join(opts.outDir, filename);
341
+ atomicFile.writeSync(outPath, body, { fileMode: 0o644 });
342
+
343
+ if (auditOn) {
344
+ try {
345
+ audit().safeEmit({
346
+ action: "dr.runbook.emitted",
347
+ outcome: "success",
348
+ metadata: {
349
+ posture: opts.posture,
350
+ path: outPath,
351
+ sectionCount: sections.filter(function (s) { return s.indexOf("## ") === 0; }).length,
352
+ },
353
+ });
354
+ } catch (_e) { /* audit best-effort */ }
355
+ }
356
+
357
+ return {
358
+ paths: [outPath],
359
+ posture: opts.posture,
360
+ sectionCount: sections.filter(function (s) { return s.indexOf("## ") === 0; }).length,
361
+ };
362
+ }
363
+
364
+ module.exports = {
365
+ emit: emit,
366
+ POSTURE_BLOCKS: POSTURE_BLOCKS,
367
+ DrRunbookError: DrRunbookError,
368
+ };
package/lib/dsr.js CHANGED
@@ -1,13 +1,39 @@
1
1
  "use strict";
2
2
  /**
3
- * b.dsr — Data Subject Rights workflow primitive.
3
+ * @module b.dsr
4
+ * @nav Compliance
5
+ * @title Dsr
4
6
  *
5
- * Coordinates the operator's response to GDPR Articles 15-22 / CCPA /
6
- * CPRA / LGPD / PIPEDA / UK-GDPR data-subject requests. The framework
7
- * owns the ticket state machine, deadline computation, audit emission,
8
- * and source orchestration. The operator owns the storage backend
9
- * (declares a `ticketStore` that satisfies the `{ insert, get, list,
10
- * update }` shape) and the per-source `query` / `erase` callbacks.
7
+ * @intro
8
+ * Data Subject Rights workflow (GDPR Art 15-22, CCPA opt-out /
9
+ * right-to-know / right-to-delete) ticket lifecycle, deadline
10
+ * tracking, source-by-source export.
11
+ *
12
+ * Coordinates the operator's response to GDPR Articles 15-22 /
13
+ * CCPA / CPRA / LGPD / PIPEDA / UK-GDPR data-subject requests.
14
+ * The framework owns the ticket state machine, deadline
15
+ * computation, audit emission, and source orchestration. The
16
+ * operator owns the storage backend (declares a `ticketStore`
17
+ * that satisfies the `{ insert, get, list, update }` shape) and
18
+ * the per-source `query` / `erase` callbacks.
19
+ *
20
+ * Ticket states: `pending` -> `in_progress` -> (`completed` |
21
+ * `partially_completed` | `cancelled` | `rejected` | `expired`).
22
+ *
23
+ * Posture-aware deadlines: gdpr/uk-gdpr/pipeda-ca = 30 days,
24
+ * ccpa = 45 days, lgpd-br/pipl-cn = 15 days. Operators override
25
+ * per-ticket via `submit({ deadlineMs })`.
26
+ *
27
+ * Verification ladder (GDPR Art 12(6) / CCPA §1798.140(y)):
28
+ * minimal / secondary / strong. Erasure + portability +
29
+ * rectification default to `secondary`; the framework refuses
30
+ * `process()` when the actual level is below the per-type floor.
31
+ *
32
+ * @card
33
+ * Data Subject Rights workflow (GDPR Art 15-22, CCPA opt-out / right-to-know / right-to-delete) — ticket lifecycle, deadline tracking, source-by-source export.
34
+ */
35
+ /*
36
+ * Original prose retained as a compact reference:
11
37
  *
12
38
  * var dsr = b.dsr.create({
13
39
  * ticketStore: dsrTickets, // operator-supplied storage
@@ -193,6 +219,60 @@ function _validateSource(s) {
193
219
  return true;
194
220
  }
195
221
 
222
+ /**
223
+ * @primitive b.dsr.create
224
+ * @signature b.dsr.create(opts)
225
+ * @since 0.8.0
226
+ * @status stable
227
+ * @compliance gdpr, ccpa
228
+ * @related b.dsr.memoryTicketStore, b.dsr.dbTicketStore
229
+ *
230
+ * Build a Data Subject Rights workflow handle. Wires the ticket
231
+ * store, identity resolver, and per-source query/erase callbacks
232
+ * into one coordinator that exposes `submit`, `process`, `cancel`,
233
+ * `reject`, `expireOverdue`, `buildReceipt`, and
234
+ * `buildPortabilityBundle`. Posture (`gdpr`, `ccpa`, `lgpd-br`,
235
+ * `uk-gdpr`, `pipeda-ca`, etc.) sets the default deadline; the
236
+ * framework refuses `process()` when the actual verification level
237
+ * is below the per-type floor.
238
+ *
239
+ * @opts
240
+ * ticketStore: { insert, get, list, update },
241
+ * posture: string, // "gdpr" | "ccpa" | "lgpd-br" | ...
242
+ * identityResolver: async function (input) -> resolvedSubject,
243
+ * sources: [{ name, query?, erase?, eraseExclusions? }],
244
+ * audit: boolean, // default true
245
+ * retentionFloorMs: number, // export TTL; default 30 days
246
+ * deadlineMs: number, // overrides posture default
247
+ * verificationLevel: "minimal" | "secondary" | "strong",
248
+ * minVerificationByType: { erasure: "secondary", ... },
249
+ * receiptSigner: async function (receipt) -> { issuer, algorithm, signature },
250
+ *
251
+ * @example
252
+ * var dsr = b.dsr.create({
253
+ * ticketStore: b.dsr.memoryTicketStore(),
254
+ * posture: "gdpr",
255
+ * identityResolver: async function (input) {
256
+ * return { subjectId: "u-42", email: input.email, phone: null };
257
+ * },
258
+ * sources: [{
259
+ * name: "users",
260
+ * query: async function (subj) { return [{ email: subj.email }]; },
261
+ * erase: async function (subj) { return { deletedIds: [subj.subjectId] }; },
262
+ * }],
263
+ * });
264
+ * var ticket = await dsr.submit({
265
+ * type: "access",
266
+ * subject: { email: "alice@example.com" },
267
+ * reason: "user-initiated",
268
+ * });
269
+ * var processed = await dsr.process(ticket.id, {
270
+ * actor: "compliance@example.com",
271
+ * verificationLevel: "secondary",
272
+ * });
273
+ * processed.status;
274
+ * // → "completed"
275
+ */
196
276
  function create(opts) {
197
277
  validateOpts.requireObject(opts, "dsr", DsrError);
198
278
  validateOpts(opts, [
@@ -739,10 +819,29 @@ function create(opts) {
739
819
  };
740
820
  }
741
821
 
742
- // In-memory ticket store — operator dev / test scaffold. Production
743
- // operators wire their own b.externalDb-backed store. The shape is
744
- // the contract: { insert(ticket), get(id), list(filter), update(id,
745
- // patch) }; this implementation satisfies it.
822
+ /**
823
+ * @primitive b.dsr.memoryTicketStore
824
+ * @signature b.dsr.memoryTicketStore()
825
+ * @since 0.8.0
826
+ * @status stable
827
+ * @related b.dsr.create, b.dsr.dbTicketStore
828
+ *
829
+ * In-memory ticket store — operator dev / test scaffold. Production
830
+ * operators wire `b.dsr.dbTicketStore` (or their own b.externalDb-
831
+ * backed store). The shape is the contract: `{ insert(ticket),
832
+ * get(id), list(filter), update(id, ticket) }`. The returned store
833
+ * also exposes `_size()` for tests.
834
+ *
835
+ * @example
836
+ * var store = b.dsr.memoryTicketStore();
837
+ * await store.insert({ id: "DSR-1", status: "pending", subject: {} });
838
+ * var t = await store.get("DSR-1");
839
+ * t.status;
840
+ * // → "pending"
841
+ * var pending = await store.list({ status: "pending" });
842
+ * pending.length;
843
+ * // → 1
844
+ */
746
845
  function memoryTicketStore() {
747
846
  var byId = new Map();
748
847
  return {
@@ -782,6 +881,38 @@ function memoryTicketStore() {
782
881
  };
783
882
  }
784
883
 
884
+ /**
885
+ * @primitive b.dsr.dbTicketStore
886
+ * @signature b.dsr.dbTicketStore(opts)
887
+ * @since 0.8.0
888
+ * @status stable
889
+ * @compliance gdpr, ccpa
890
+ * @related b.dsr.create, b.dsr.memoryTicketStore
891
+ *
892
+ * Production-grade ticket store backed by `b.db`. Auto-provisions
893
+ * the table on first use, indexes on `subject_email` and `status`,
894
+ * persists the full ticket as a JSON payload column, and exposes
895
+ * `purgeExpired(asOfMs?)` for retention-floor enforcement.
896
+ *
897
+ * @opts
898
+ * db: b.db-shaped handle (`{ runSql, prepare }`),
899
+ * table: string, // SQL identifier; defaults to "dsr_tickets"
900
+ *
901
+ * @example
902
+ * var store = b.dsr.dbTicketStore({ db: b.db.handle(), table: "dsr_tickets" });
903
+ * await store.insert({
904
+ * id: "DSR-1234567-DEADBEEF",
905
+ * type: "erasure",
906
+ * status: "pending",
907
+ * subject: { subjectId: "u-42", email: "alice@example.com" },
908
+ * submittedAt: Date.now(),
909
+ * deadlineAt: Date.now() + 30 * 86400 * 1000,
910
+ * retentionUntil: Date.now() + 30 * 86400 * 1000,
911
+ * });
912
+ * var purged = await store.purgeExpired();
913
+ * typeof purged;
914
+ * // → "number"
915
+ */
785
916
  // b.db-backed ticket store — production operators wire this against
786
917
  // the framework's SQLite engine. The store auto-provisions a single
787
918
  // table (default name `dsr_tickets`) with the canonical column set: