@blamejs/core 0.8.43 → 0.8.50
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/restore-bundle.js
CHANGED
|
@@ -1,47 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.restoreBundle
|
|
4
|
+
* @nav Production
|
|
5
|
+
* @title Restore Bundle
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* swaps into place. The bundle directory itself is read-only throughout.
|
|
7
|
+
* @intro
|
|
8
|
+
* Backup-bundle reader — verify the manifest signature, list bundle
|
|
9
|
+
* contents without decrypting, and cherry-pick a restore subset to a
|
|
10
|
+
* staging directory the caller atomically swaps into place.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* The mirror of `b.backupBundle`. `b.restoreBundle.inspect` reads
|
|
13
|
+
* `manifest.json` and returns the parsed object — useful for
|
|
14
|
+
* dashboards and pre-flight UI that want to list files, sizes,
|
|
15
|
+
* timestamps, and kinds before prompting the operator for the
|
|
16
|
+
* passphrase. `b.restoreBundle.extract` decrypts each per-file blob
|
|
17
|
+
* via `b.backup/crypto`, verifies the SHA3-512 plaintext checksum
|
|
18
|
+
* against the manifest, and writes the recovered files into a
|
|
19
|
+
* fresh `stagingDir`. The bundle directory itself stays read-only
|
|
20
|
+
* throughout.
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* `extract` always recovers the wrapped vault key (decrypted JSON
|
|
23
|
+
* returned on `vaultKeyJson`) so the operator can unseal columns
|
|
24
|
+
* from a partial restore. The `filter` predicate lets the caller
|
|
25
|
+
* pull a subset — only the DB, only TLS keys, only the consent
|
|
26
|
+
* log — without producing every blob.
|
|
26
27
|
*
|
|
27
|
-
*
|
|
28
|
-
* the DB, only the TLS keys, etc.). The vault key is always recovered
|
|
29
|
-
* regardless of filter so the operator can read sealed values from a
|
|
30
|
-
* partial restore.
|
|
28
|
+
* Defense surface:
|
|
31
29
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* restore-bundle/decrypt-failed (no plaintext leaked, no staging
|
|
30
|
+
* - Wrong passphrase / tampered blob → AEAD tag failure →
|
|
31
|
+
* `restore-bundle/decrypt-failed` (no plaintext leak, no staging
|
|
35
32
|
* left behind)
|
|
36
|
-
* -
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* -
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
33
|
+
* - Pre-decrypt `encryptedSize` mismatch → `restore-bundle/
|
|
34
|
+
* size-mismatch`
|
|
35
|
+
* - Post-decrypt SHA3-512 ≠ manifest checksum →
|
|
36
|
+
* `restore-bundle/checksum-mismatch`
|
|
37
|
+
* - Missing blob file → `restore-bundle/missing-blob`
|
|
38
|
+
* - Bad manifest signature → `restore-bundle/bad-signature`;
|
|
39
|
+
* `requireSignature: true` upgrades a missing signature to
|
|
40
|
+
* `restore-bundle/missing-signature`
|
|
41
|
+
* - On any failure the partially-built `stagingDir` is removed so a
|
|
42
|
+
* subsequent retry is not blocked by a stale directory
|
|
43
|
+
*
|
|
44
|
+
* @card
|
|
45
|
+
* Backup-bundle reader — verify the manifest signature, list bundle contents without decrypting, and cherry-pick a restore subset to a staging directory the caller atomically swaps into place.
|
|
45
46
|
*/
|
|
46
47
|
|
|
47
48
|
var fs = require("fs");
|
|
@@ -68,6 +69,57 @@ function _cleanupStaging(stagingDir) {
|
|
|
68
69
|
catch (_e) { /* best-effort */ }
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* @primitive b.restoreBundle.extract
|
|
74
|
+
* @signature b.restoreBundle.extract(opts)
|
|
75
|
+
* @since 0.5.0
|
|
76
|
+
* @status stable
|
|
77
|
+
* @related b.restoreBundle.inspect, b.backupBundle.create, b.vault.init
|
|
78
|
+
*
|
|
79
|
+
* Decrypt every blob the manifest references (or the subset
|
|
80
|
+
* `opts.filter` accepts), verify each plaintext's checksum, and write
|
|
81
|
+
* the recovered files into `opts.stagingDir`. Returns
|
|
82
|
+
* `{ manifest, vaultKeyJson, fileCount, totalBytes, stagingDir,
|
|
83
|
+
* durationMs }`.
|
|
84
|
+
*
|
|
85
|
+
* `stagingDir` MUST NOT exist — extract refuses to merge into an
|
|
86
|
+
* existing directory so a half-finished prior restore can never get
|
|
87
|
+
* silently overlaid. On any failure the partial `stagingDir` is
|
|
88
|
+
* removed.
|
|
89
|
+
*
|
|
90
|
+
* Signature handling: when the manifest carries a signature it is
|
|
91
|
+
* verified with `b.backup/manifest`'s public-key check. Pass
|
|
92
|
+
* `verifySignature: false` for cold restores from an org whose
|
|
93
|
+
* audit-sign keypair the framework cannot reach; pass
|
|
94
|
+
* `requireSignature: true` to fail-closed on bundles missing a
|
|
95
|
+
* signature; pass `expectedFingerprint` to pin a specific signing
|
|
96
|
+
* key.
|
|
97
|
+
*
|
|
98
|
+
* @opts
|
|
99
|
+
* bundleDir: string, // read-only bundle dir (required)
|
|
100
|
+
* stagingDir: string, // fresh output dir (required, must not exist)
|
|
101
|
+
* passphrase: Buffer | string, // unwrap key (required)
|
|
102
|
+
* filter: function (entry): boolean,// subset predicate
|
|
103
|
+
* progressCallback: function (ev): void, // phase events: read_manifest / decrypt / done
|
|
104
|
+
* verifySignature: boolean, // default: true
|
|
105
|
+
* requireSignature: boolean, // fail-closed on missing signature
|
|
106
|
+
* expectedFingerprint: string, // pin specific signing key
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* try {
|
|
110
|
+
* var report = await b.restoreBundle.extract({
|
|
111
|
+
* bundleDir: "/srv/backups/2026-04-27.bundle",
|
|
112
|
+
* stagingDir: "/srv/restore/data.staging",
|
|
113
|
+
* passphrase: Buffer.from("operator-passphrase"),
|
|
114
|
+
* requireSignature: true,
|
|
115
|
+
* filter: function (entry) { return entry.kind === "db"; },
|
|
116
|
+
* });
|
|
117
|
+
* report.fileCount; // → 1
|
|
118
|
+
* typeof report.vaultKeyJson; // → "string"
|
|
119
|
+
* } catch (e) {
|
|
120
|
+
* e.code; // → "restore-bundle/decrypt-failed"
|
|
121
|
+
* }
|
|
122
|
+
*/
|
|
71
123
|
async function extract(opts) {
|
|
72
124
|
var t0 = Date.now();
|
|
73
125
|
opts = opts || {};
|
|
@@ -107,6 +159,27 @@ async function extract(opts) {
|
|
|
107
159
|
"extract: manifest could not be parsed: " + ((e && e.message) || String(e)));
|
|
108
160
|
}
|
|
109
161
|
|
|
162
|
+
// Verify the manifest signature when present. Operators can pass
|
|
163
|
+
// `requireSignature: true` to fail-closed on missing signatures
|
|
164
|
+
// (HIPAA/PCI-DSS), `expectedFingerprint` to pin a specific signing
|
|
165
|
+
// key, or pass `verifySignature: false` to skip verification when
|
|
166
|
+
// the signing key is genuinely unavailable (cold-restore from a
|
|
167
|
+
// separate org with their own audit-sign keypair the framework
|
|
168
|
+
// can't reach).
|
|
169
|
+
var verifySig = opts.verifySignature !== false;
|
|
170
|
+
if (verifySig && manifest.signature) {
|
|
171
|
+
var sigResult = backupManifest.verifySignature(manifest, {
|
|
172
|
+
expectedFingerprint: opts.expectedFingerprint || undefined,
|
|
173
|
+
});
|
|
174
|
+
if (!sigResult.ok) {
|
|
175
|
+
throw new RestoreBundleError("restore-bundle/bad-signature",
|
|
176
|
+
"extract: manifest signature invalid: " + sigResult.reason);
|
|
177
|
+
}
|
|
178
|
+
} else if (opts.requireSignature === true && !manifest.signature) {
|
|
179
|
+
throw new RestoreBundleError("restore-bundle/missing-signature",
|
|
180
|
+
"extract: manifest has no signature but opts.requireSignature=true");
|
|
181
|
+
}
|
|
182
|
+
|
|
110
183
|
// 2. Recover the vault key (always, regardless of filter — the
|
|
111
184
|
// operator may need it to unseal post-restore even on partial
|
|
112
185
|
// restores)
|
|
@@ -213,9 +286,39 @@ async function extract(opts) {
|
|
|
213
286
|
};
|
|
214
287
|
}
|
|
215
288
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
289
|
+
/**
|
|
290
|
+
* @primitive b.restoreBundle.inspect
|
|
291
|
+
* @signature b.restoreBundle.inspect(opts)
|
|
292
|
+
* @since 0.5.0
|
|
293
|
+
* @status stable
|
|
294
|
+
* @related b.restoreBundle.extract, b.backupBundle.create
|
|
295
|
+
*
|
|
296
|
+
* Read `manifest.json` from `opts.bundleDir` and return the parsed
|
|
297
|
+
* object — files, sizes, timestamps, kinds, signature presence —
|
|
298
|
+
* without prompting for the passphrase or decrypting anything. Useful
|
|
299
|
+
* for dashboards, pre-flight UI, and "what's in this bundle?" checks
|
|
300
|
+
* before kicking off a long extract.
|
|
301
|
+
*
|
|
302
|
+
* Throws `RestoreBundleError("restore-bundle/no-bundle")` when
|
|
303
|
+
* `bundleDir` is missing, and
|
|
304
|
+
* `RestoreBundleError("restore-bundle/missing-manifest")` when the
|
|
305
|
+
* directory exists but has no `manifest.json` (the bundle is
|
|
306
|
+
* incomplete or not a blamejs bundle).
|
|
307
|
+
*
|
|
308
|
+
* @opts
|
|
309
|
+
* bundleDir: string, // bundle directory (required, must exist)
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* try {
|
|
313
|
+
* var manifest = b.restoreBundle.inspect({
|
|
314
|
+
* bundleDir: "/srv/backups/2026-04-27.bundle",
|
|
315
|
+
* });
|
|
316
|
+
* manifest.files.length; // → 12
|
|
317
|
+
* typeof manifest.signature; // → "string"
|
|
318
|
+
* } catch (e) {
|
|
319
|
+
* e.code; // → "restore-bundle/missing-manifest"
|
|
320
|
+
* }
|
|
321
|
+
*/
|
|
219
322
|
function inspect(opts) {
|
|
220
323
|
opts = opts || {};
|
|
221
324
|
if (typeof opts.bundleDir !== "string" || !fs.existsSync(opts.bundleDir)) {
|
package/lib/restore-rollback.js
CHANGED
|
@@ -1,56 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.restoreRollback
|
|
4
|
+
* @nav Other
|
|
5
|
+
* @title Restore Rollback
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Backup-restore safety net — atomic dataDir swap with a versioned
|
|
9
|
+
* rollback path. The primitive `b.restore` calls to put a
|
|
10
|
+
* freshly-decrypted bundle into place: filesystem rename is atomic
|
|
11
|
+
* on POSIX (and on Windows when nothing has the dir open), so the
|
|
12
|
+
* swap either fully completes or the previous `dataDir` is
|
|
13
|
+
* recoverable through `rollback`.
|
|
9
14
|
*
|
|
10
|
-
*
|
|
15
|
+
* Three steps frame every restore: pre-restore snapshot (the
|
|
16
|
+
* existing `dataDir` is renamed into `<root>/<timestamp>/` before
|
|
17
|
+
* the new bundle moves in), post-restore verify (operator runs
|
|
18
|
+
* integrity / audit-chain checks against the live framework), and
|
|
19
|
+
* rollback on failure (a single `rollback({ rollbackPath })` call
|
|
20
|
+
* reverses the swap). A marker JSON file carries operator-supplied
|
|
21
|
+
* metadata (`bundleId`, `reason`, timestamps) so `list` and `purge`
|
|
22
|
+
* are informative without rifling through directory contents.
|
|
11
23
|
*
|
|
12
|
-
*
|
|
13
|
-
* stagingDir: "./data.staging",
|
|
14
|
-
* dataDir: "./data",
|
|
15
|
-
* rollbackRoot: "./data.rollbacks", // optional; defaults to <dataDir>.rollbacks
|
|
16
|
-
* marker: { bundleId: "...", reason: "scheduled-restore" },
|
|
17
|
-
* });
|
|
18
|
-
* // → { rollbackPath, markerPath, swappedAt }
|
|
19
|
-
*
|
|
20
|
-
* // Reverse the most recent swap (or a specific one by path)
|
|
21
|
-
* await rb.rollback({ dataDir: "./data", rollbackPath: r.rollbackPath });
|
|
22
|
-
* // → { restoredFrom, discardedAt }
|
|
23
|
-
*
|
|
24
|
-
* rb.list({ rollbackRoot: "./data.rollbacks" });
|
|
25
|
-
* // → [{ rollbackPath, swappedAt, marker }] (newest first)
|
|
26
|
-
*
|
|
27
|
-
* rb.purge({ rollbackRoot: "./data.rollbacks", keep: 3 });
|
|
28
|
-
* // → { kept, deleted: [paths] }
|
|
24
|
+
* Layout after a successful swap:
|
|
29
25
|
*
|
|
30
|
-
*
|
|
26
|
+
* ./data <- freshly-restored bundle
|
|
27
|
+
* ./data.rollbacks/
|
|
28
|
+
* 2026-04-27T17-46-36-075Z/ <- previous dataDir
|
|
29
|
+
* 2026-04-27T17-46-36-075Z.marker.json
|
|
31
30
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
31
|
+
* Stop-framework-first contract: this primitive does NOT close the
|
|
32
|
+
* framework's open file handles. On Linux a directory rename
|
|
33
|
+
* succeeds with handles open, but the running process keeps reading
|
|
34
|
+
* stale data. Operators run restore as `stop framework -> swap ->
|
|
35
|
+
* start framework`, same shape as a database restore. Concurrency
|
|
36
|
+
* guard: `swap` refuses if another rollback for the same
|
|
37
|
+
* millisecond timestamp already exists — collisions are
|
|
38
|
+
* vanishingly rare but the check keeps a double-fire from
|
|
39
|
+
* corrupting state.
|
|
37
40
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* over rollback dirs is informative without rifling through their
|
|
41
|
-
* contents.
|
|
42
|
-
*
|
|
43
|
-
* Concurrency: swap() refuses to operate if another rollback dir for
|
|
44
|
-
* the same timestamp already exists — collisions are vanishingly rare
|
|
45
|
-
* because the timestamp has millisecond precision plus the framework
|
|
46
|
-
* never runs two restores in parallel on the same dataDir, but the
|
|
47
|
-
* check makes a corrupted state impossible if an operator fires twice.
|
|
48
|
-
*
|
|
49
|
-
* Operator stop-framework-first contract: this primitive does NOT
|
|
50
|
-
* close the framework's open file handles. On Linux a directory
|
|
51
|
-
* rename succeeds even with handles open, but the running framework
|
|
52
|
-
* process will see stale data. Operators run restore as: stop
|
|
53
|
-
* framework → swap → start framework. Same as a database restore.
|
|
41
|
+
* @card
|
|
42
|
+
* Backup-restore safety net — atomic dataDir swap with a versioned rollback path.
|
|
54
43
|
*/
|
|
55
44
|
|
|
56
45
|
var fs = require("fs");
|
|
@@ -76,6 +65,33 @@ function _resolveRollbackRoot(opts) {
|
|
|
76
65
|
}
|
|
77
66
|
|
|
78
67
|
|
|
68
|
+
/**
|
|
69
|
+
* @primitive b.restoreRollback.swap
|
|
70
|
+
* @signature b.restoreRollback.swap(opts)
|
|
71
|
+
* @since 0.1.89
|
|
72
|
+
* @status stable
|
|
73
|
+
* @related b.restoreRollback.rollback, b.restoreRollback.list, b.restore.applyBundle
|
|
74
|
+
*
|
|
75
|
+
* Pre-restore snapshot + atomic swap. Renames the existing `dataDir`
|
|
76
|
+
* into `<rollbackRoot>/<timestamp>/`, then renames `stagingDir` into
|
|
77
|
+
* `dataDir`. If step two fails, step one is undone so the operator's
|
|
78
|
+
* dataDir is intact. Writes a `<timestamp>.marker.json` carrying
|
|
79
|
+
* operator metadata for later `list` / `rollback` discovery.
|
|
80
|
+
*
|
|
81
|
+
* @opts
|
|
82
|
+
* stagingDir: string, // pre-decrypted bundle, must exist
|
|
83
|
+
* dataDir: string, // live data dir to replace
|
|
84
|
+
* rollbackRoot: string, // optional; defaults to "<dataDir>.rollbacks"
|
|
85
|
+
* marker: object, // operator metadata for the marker file
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* var r = b.restoreRollback.swap({
|
|
89
|
+
* stagingDir: "./data.staging",
|
|
90
|
+
* dataDir: "./data",
|
|
91
|
+
* marker: { bundleId: "bk-2026-05-09", reason: "scheduled-restore" },
|
|
92
|
+
* });
|
|
93
|
+
* // → { rollbackPath: "./data.rollbacks/2026-05-09T...", markerPath, swappedAt, marker }
|
|
94
|
+
*/
|
|
79
95
|
function swap(opts) {
|
|
80
96
|
opts = opts || {};
|
|
81
97
|
if (typeof opts.stagingDir !== "string" || !fs.existsSync(opts.stagingDir)) {
|
|
@@ -143,6 +159,34 @@ function swap(opts) {
|
|
|
143
159
|
};
|
|
144
160
|
}
|
|
145
161
|
|
|
162
|
+
/**
|
|
163
|
+
* @primitive b.restoreRollback.rollback
|
|
164
|
+
* @signature b.restoreRollback.rollback(opts)
|
|
165
|
+
* @since 0.1.89
|
|
166
|
+
* @status stable
|
|
167
|
+
* @related b.restoreRollback.swap, b.restoreRollback.list
|
|
168
|
+
*
|
|
169
|
+
* Reverse a prior swap. Moves the current `dataDir` aside as
|
|
170
|
+
* `discarded-<timestamp>/` (so the rename target is empty), then
|
|
171
|
+
* renames the named `rollbackPath` back into `dataDir`. The marker
|
|
172
|
+
* JSON is removed best-effort. Operator must have stopped the
|
|
173
|
+
* framework first — open file handles on the live dataDir on Windows
|
|
174
|
+
* cause the rename to fail.
|
|
175
|
+
*
|
|
176
|
+
* @opts
|
|
177
|
+
* dataDir: string, // live dataDir to replace
|
|
178
|
+
* rollbackPath: string, // must exist; from swap() return
|
|
179
|
+
* rollbackRoot: string, // optional; defaults to "<dataDir>.rollbacks"
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* var r = b.restoreRollback.swap({
|
|
183
|
+
* stagingDir: "./data.staging", dataDir: "./data",
|
|
184
|
+
* marker: { reason: "test" },
|
|
185
|
+
* });
|
|
186
|
+
* // post-restore verify failed:
|
|
187
|
+
* await b.restoreRollback.rollback({ dataDir: "./data", rollbackPath: r.rollbackPath });
|
|
188
|
+
* // → { restoredFrom: "./data.rollbacks/2026-05-09T...", discardedAt: "..." }
|
|
189
|
+
*/
|
|
146
190
|
async function rollback(opts) {
|
|
147
191
|
opts = opts || {};
|
|
148
192
|
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
@@ -188,6 +232,29 @@ async function rollback(opts) {
|
|
|
188
232
|
};
|
|
189
233
|
}
|
|
190
234
|
|
|
235
|
+
/**
|
|
236
|
+
* @primitive b.restoreRollback.list
|
|
237
|
+
* @signature b.restoreRollback.list(opts)
|
|
238
|
+
* @since 0.1.89
|
|
239
|
+
* @status stable
|
|
240
|
+
* @related b.restoreRollback.swap, b.restoreRollback.purge
|
|
241
|
+
*
|
|
242
|
+
* Enumerate available rollback points, newest first. Reads each
|
|
243
|
+
* marker file (capped at 64 KiB via `b.safeJson` to bound a
|
|
244
|
+
* tampered-marker DoS). Skips `discarded-*` directories — those are
|
|
245
|
+
* sweep-only and never restore points.
|
|
246
|
+
*
|
|
247
|
+
* @opts
|
|
248
|
+
* dataDir: string, // optional, used to derive rollbackRoot
|
|
249
|
+
* rollbackRoot: string, // optional; defaults to "<dataDir>.rollbacks"
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* var points = b.restoreRollback.list({ dataDir: "./data" });
|
|
253
|
+
* points.forEach(function (p) {
|
|
254
|
+
* console.log(p.swappedAt, p.marker && p.marker.operator);
|
|
255
|
+
* });
|
|
256
|
+
* // → [{ rollbackPath, swappedAt, marker }, ...]
|
|
257
|
+
*/
|
|
191
258
|
function list(opts) {
|
|
192
259
|
opts = opts || {};
|
|
193
260
|
var rollbackRoot = _resolveRollbackRoot(opts);
|
|
@@ -218,6 +285,30 @@ function list(opts) {
|
|
|
218
285
|
return out;
|
|
219
286
|
}
|
|
220
287
|
|
|
288
|
+
/**
|
|
289
|
+
* @primitive b.restoreRollback.purge
|
|
290
|
+
* @signature b.restoreRollback.purge(opts)
|
|
291
|
+
* @since 0.1.89
|
|
292
|
+
* @status stable
|
|
293
|
+
* @related b.restoreRollback.list, b.restoreRollback.swap
|
|
294
|
+
*
|
|
295
|
+
* Sweep stale rollback directories. Always removes every directory
|
|
296
|
+
* named `discarded-<timestamp>` (those are never restore points),
|
|
297
|
+
* then keeps the newest `keep` rollback points and removes the rest
|
|
298
|
+
* along with their marker files. `opts.keep` defaults to 0; pass a
|
|
299
|
+
* positive integer to retain a sliding window. Best-effort: a
|
|
300
|
+
* per-path unlink failure is logged via the deleted-list omission
|
|
301
|
+
* rather than thrown.
|
|
302
|
+
*
|
|
303
|
+
* @opts
|
|
304
|
+
* dataDir: string,
|
|
305
|
+
* rollbackRoot: string,
|
|
306
|
+
* keep: number, // non-negative integer, default 0
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* var r = b.restoreRollback.purge({ dataDir: "./data", keep: 3 });
|
|
310
|
+
* // → { kept: 3, deleted: ["./data.rollbacks/2026-04-...", ...] }
|
|
311
|
+
*/
|
|
221
312
|
function purge(opts) {
|
|
222
313
|
opts = opts || {};
|
|
223
314
|
nb.requireNonNegativeFiniteIntIfPresent(opts.keep,
|