@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.
- package/CHANGELOG.md +92 -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
|
@@ -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
|
|
3
|
+
* @module b.dsr
|
|
4
|
+
* @nav Compliance
|
|
5
|
+
* @title Dsr
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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:
|