@blamejs/core 0.7.4 → 0.7.18
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 +423 -395
- package/README.md +150 -149
- package/bin/blamejs.js +0 -0
- package/index.js +308 -284
- package/lib/api-key.js +660 -663
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt.js +311 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +594 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/codepoint-class.js +196 -0
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -286
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -421
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/file-upload.js +213 -10
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -215
- package/lib/framework-schema.js +717 -717
- package/lib/gate-contract.js +971 -0
- package/lib/guard-all.js +405 -0
- package/lib/guard-archive.js +739 -0
- package/lib/guard-csv.js +816 -0
- package/lib/guard-email.js +744 -0
- package/lib/guard-filename.js +724 -0
- package/lib/guard-html.js +976 -0
- package/lib/guard-json.js +729 -0
- package/lib/guard-markdown.js +586 -0
- package/lib/guard-svg.js +976 -0
- package/lib/guard-xml.js +405 -0
- package/lib/guard-yaml.js +529 -0
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -362
- package/lib/mail.js +981 -962
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/body-parser.js +1170 -1091
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +79 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -91
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +478 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/static.js +184 -4
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -319
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
package/lib/restore-rollback.js
CHANGED
|
@@ -1,259 +1,259 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* restore-rollback — atomic dataDir swap with a versioned rollback path.
|
|
4
|
-
*
|
|
5
|
-
* The primitive used by lib/restore to put a freshly-decrypted bundle
|
|
6
|
-
* into place. Filesystem-level directory rename is atomic on POSIX
|
|
7
|
-
* (and on Windows when nothing has the dir open) — the swap either
|
|
8
|
-
* fully completes or the previous dataDir is recoverable.
|
|
9
|
-
*
|
|
10
|
-
* var rb = b.restoreRollback;
|
|
11
|
-
*
|
|
12
|
-
* var r = rb.swap({
|
|
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] }
|
|
29
|
-
*
|
|
30
|
-
* Layout after a successful swap:
|
|
31
|
-
*
|
|
32
|
-
* ./data ← the freshly-restored bundle
|
|
33
|
-
* ./data.rollbacks/
|
|
34
|
-
* 2026-04-27T17-46-36-075Z/ ← previous dataDir, renamed atomically
|
|
35
|
-
* (whatever was in dataDir at the time of swap)
|
|
36
|
-
* 2026-04-27T17-46-36-075Z.marker.json
|
|
37
|
-
*
|
|
38
|
-
* The marker file carries operator-supplied metadata (which bundle
|
|
39
|
-
* triggered the swap, what reason was given, when) so a list / audit
|
|
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.
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
var fs = require("fs");
|
|
57
|
-
var path = require("path");
|
|
58
|
-
var atomicFile = require("./atomic-file");
|
|
59
|
-
var C = require("./constants");
|
|
60
|
-
var nb = require("./numeric-bounds");
|
|
61
|
-
var safeJson = require("./safe-json");
|
|
62
|
-
var { defineClass } = require("./framework-error");
|
|
63
|
-
|
|
64
|
-
var RestoreRollbackError = defineClass("RestoreRollbackError", { alwaysPermanent: true });
|
|
65
|
-
|
|
66
|
-
function _resolveRollbackRoot(opts) {
|
|
67
|
-
if (typeof opts.rollbackRoot === "string" && opts.rollbackRoot.length > 0) {
|
|
68
|
-
return opts.rollbackRoot;
|
|
69
|
-
}
|
|
70
|
-
// Default: sibling of dataDir named <dataDir>.rollbacks
|
|
71
|
-
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
72
|
-
throw new RestoreRollbackError("restore-rollback/no-rollback-root",
|
|
73
|
-
"rollbackRoot must be supplied or derivable from opts.dataDir");
|
|
74
|
-
}
|
|
75
|
-
return opts.dataDir + ".rollbacks";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
function swap(opts) {
|
|
80
|
-
opts = opts || {};
|
|
81
|
-
if (typeof opts.stagingDir !== "string" || !fs.existsSync(opts.stagingDir)) {
|
|
82
|
-
throw new RestoreRollbackError("restore-rollback/no-staging",
|
|
83
|
-
"swap: opts.stagingDir is required and must exist");
|
|
84
|
-
}
|
|
85
|
-
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
86
|
-
throw new RestoreRollbackError("restore-rollback/no-datadir",
|
|
87
|
-
"swap: opts.dataDir is required");
|
|
88
|
-
}
|
|
89
|
-
var rollbackRoot = _resolveRollbackRoot(opts);
|
|
90
|
-
atomicFile.ensureDir(rollbackRoot);
|
|
91
|
-
|
|
92
|
-
var swappedAt = atomicFile.pathTimestamp();
|
|
93
|
-
var rollbackPath = path.join(rollbackRoot, swappedAt);
|
|
94
|
-
var markerPath = path.join(rollbackRoot, swappedAt + ".marker.json");
|
|
95
|
-
|
|
96
|
-
if (fs.existsSync(rollbackPath) || fs.existsSync(markerPath)) {
|
|
97
|
-
throw new RestoreRollbackError("restore-rollback/collision",
|
|
98
|
-
"swap: a rollback at " + rollbackPath + " already exists — refusing to overwrite");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
var hadDataDir = fs.existsSync(opts.dataDir);
|
|
102
|
-
|
|
103
|
-
// Step 1: rename current dataDir → rollback path. Skipped on first
|
|
104
|
-
// restore (no existing dataDir).
|
|
105
|
-
if (hadDataDir) {
|
|
106
|
-
try { fs.renameSync(opts.dataDir, rollbackPath); }
|
|
107
|
-
catch (e) {
|
|
108
|
-
throw new RestoreRollbackError("restore-rollback/rename-existing-failed",
|
|
109
|
-
"swap: could not move existing dataDir to rollback: " + ((e && e.message) || String(e)));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Step 2: rename staging → dataDir
|
|
114
|
-
try { fs.renameSync(opts.stagingDir, opts.dataDir); }
|
|
115
|
-
catch (e) {
|
|
116
|
-
// Step 2 failed — try to undo step 1 so the operator's dataDir is back
|
|
117
|
-
if (hadDataDir) {
|
|
118
|
-
try { fs.renameSync(rollbackPath, opts.dataDir); }
|
|
119
|
-
catch (_e) { /* dataDir is now in rollbackPath; operator must recover manually */ }
|
|
120
|
-
}
|
|
121
|
-
throw new RestoreRollbackError("restore-rollback/rename-staging-failed",
|
|
122
|
-
"swap: could not move staging to dataDir: " + ((e && e.message) || String(e)) +
|
|
123
|
-
(hadDataDir ? " (attempted to undo previous rename — verify dataDir state)" : ""));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Step 3: write the marker (best-effort; missing marker is recoverable
|
|
127
|
-
// from the rollback dir's mtime, but operators want it for audit)
|
|
128
|
-
var marker = {
|
|
129
|
-
swappedAt: new Date().toISOString(),
|
|
130
|
-
rollbackPath: rollbackPath,
|
|
131
|
-
dataDir: opts.dataDir,
|
|
132
|
-
operator: opts.marker || null,
|
|
133
|
-
};
|
|
134
|
-
try {
|
|
135
|
-
fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2) + "\n", { mode: 0o600 });
|
|
136
|
-
} catch (_e) { /* marker write is best-effort */ }
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
rollbackPath: hadDataDir ? rollbackPath : null,
|
|
140
|
-
markerPath: markerPath,
|
|
141
|
-
swappedAt: swappedAt,
|
|
142
|
-
marker: marker,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function rollback(opts) {
|
|
147
|
-
opts = opts || {};
|
|
148
|
-
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
149
|
-
throw new RestoreRollbackError("restore-rollback/no-datadir",
|
|
150
|
-
"rollback: opts.dataDir is required");
|
|
151
|
-
}
|
|
152
|
-
if (typeof opts.rollbackPath !== "string" || !fs.existsSync(opts.rollbackPath)) {
|
|
153
|
-
throw new RestoreRollbackError("restore-rollback/no-rollback",
|
|
154
|
-
"rollback: opts.rollbackPath is required and must exist");
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Move the current dataDir aside (so the rollback's rename target is empty)
|
|
158
|
-
var discardedAt = null;
|
|
159
|
-
if (fs.existsSync(opts.dataDir)) {
|
|
160
|
-
var rollbackRoot = _resolveRollbackRoot(opts);
|
|
161
|
-
atomicFile.ensureDir(rollbackRoot);
|
|
162
|
-
discardedAt = atomicFile.pathTimestamp();
|
|
163
|
-
var discardedPath = path.join(rollbackRoot, "discarded-" + discardedAt);
|
|
164
|
-
try { fs.renameSync(opts.dataDir, discardedPath); }
|
|
165
|
-
catch (e) {
|
|
166
|
-
throw new RestoreRollbackError("restore-rollback/rename-existing-failed",
|
|
167
|
-
"rollback: could not move current dataDir aside: " + ((e && e.message) || String(e)));
|
|
168
|
-
}
|
|
169
|
-
discardedAt = discardedPath;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Rename the rollback dir back into dataDir's place
|
|
173
|
-
try { fs.renameSync(opts.rollbackPath, opts.dataDir); }
|
|
174
|
-
catch (e) {
|
|
175
|
-
throw new RestoreRollbackError("restore-rollback/rollback-rename-failed",
|
|
176
|
-
"rollback: could not move rollback into dataDir: " + ((e && e.message) || String(e)) +
|
|
177
|
-
" (current dataDir, if any, was moved to " + discardedAt + ")");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Best-effort: clean up the marker file alongside the rollback path
|
|
181
|
-
var markerPath = opts.rollbackPath + ".marker.json";
|
|
182
|
-
try { if (fs.existsSync(markerPath)) fs.unlinkSync(markerPath); }
|
|
183
|
-
catch (_e) { /* marker cleanup is best-effort */ }
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
restoredFrom: opts.rollbackPath,
|
|
187
|
-
discardedAt: discardedAt,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function list(opts) {
|
|
192
|
-
opts = opts || {};
|
|
193
|
-
var rollbackRoot = _resolveRollbackRoot(opts);
|
|
194
|
-
if (!fs.existsSync(rollbackRoot)) return [];
|
|
195
|
-
var entries = fs.readdirSync(rollbackRoot, { withFileTypes: true });
|
|
196
|
-
var out = [];
|
|
197
|
-
for (var i = 0; i < entries.length; i++) {
|
|
198
|
-
if (!entries[i].isDirectory()) continue;
|
|
199
|
-
var name = entries[i].name;
|
|
200
|
-
if (name.indexOf("discarded-") === 0) continue; // discarded dirs aren't restore points
|
|
201
|
-
var p = path.join(rollbackRoot, name);
|
|
202
|
-
var markerPath = p + ".marker.json";
|
|
203
|
-
var marker = null;
|
|
204
|
-
if (fs.existsSync(markerPath)) {
|
|
205
|
-
try { marker = safeJson.parse(fs.readFileSync(markerPath, "utf8"), { maxBytes: C.BYTES.kib(64) }); }
|
|
206
|
-
catch (_e) { marker = null; }
|
|
207
|
-
}
|
|
208
|
-
var stat;
|
|
209
|
-
try { stat = fs.statSync(p); } catch (_e) { continue; }
|
|
210
|
-
out.push({
|
|
211
|
-
rollbackPath: p,
|
|
212
|
-
swappedAt: (marker && marker.swappedAt) || stat.mtime.toISOString(),
|
|
213
|
-
marker: marker,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
// Newest first
|
|
217
|
-
out.sort(function (a, b) { return a.swappedAt < b.swappedAt ? 1 : -1; });
|
|
218
|
-
return out;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function purge(opts) {
|
|
222
|
-
opts = opts || {};
|
|
223
|
-
nb.requireNonNegativeFiniteIntIfPresent(opts.keep,
|
|
224
|
-
"restore-rollback.purge: opts.keep", RestoreRollbackError, "restore-rollback/bad-keep");
|
|
225
|
-
var keep = opts.keep !== undefined ? opts.keep : 0;
|
|
226
|
-
var rollbackRoot = _resolveRollbackRoot(opts);
|
|
227
|
-
if (!fs.existsSync(rollbackRoot)) return { kept: keep, deleted: [] };
|
|
228
|
-
// Always sweep "discarded-*" dirs — they're never restore points
|
|
229
|
-
var entries = fs.readdirSync(rollbackRoot, { withFileTypes: true });
|
|
230
|
-
var deleted = [];
|
|
231
|
-
for (var i = 0; i < entries.length; i++) {
|
|
232
|
-
if (entries[i].isDirectory() && entries[i].name.indexOf("discarded-") === 0) {
|
|
233
|
-
var p = path.join(rollbackRoot, entries[i].name);
|
|
234
|
-
try { fs.rmSync(p, { recursive: true, force: true }); deleted.push(p); }
|
|
235
|
-
catch (_e) { /* best-effort */ }
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Keep newest `keep`, delete the rest (and their marker files)
|
|
240
|
-
var rb = list(opts);
|
|
241
|
-
var toDelete = rb.slice(keep);
|
|
242
|
-
for (var j = 0; j < toDelete.length; j++) {
|
|
243
|
-
var rbPath = toDelete[j].rollbackPath;
|
|
244
|
-
var mkPath = rbPath + ".marker.json";
|
|
245
|
-
try { fs.rmSync(rbPath, { recursive: true, force: true }); deleted.push(rbPath); }
|
|
246
|
-
catch (_e) { /* best-effort */ }
|
|
247
|
-
try { if (fs.existsSync(mkPath)) fs.unlinkSync(mkPath); }
|
|
248
|
-
catch (_e) { /* best-effort */ }
|
|
249
|
-
}
|
|
250
|
-
return { kept: keep, deleted: deleted };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
module.exports = {
|
|
254
|
-
swap: swap,
|
|
255
|
-
rollback: rollback,
|
|
256
|
-
list: list,
|
|
257
|
-
purge: purge,
|
|
258
|
-
RestoreRollbackError: RestoreRollbackError,
|
|
259
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* restore-rollback — atomic dataDir swap with a versioned rollback path.
|
|
4
|
+
*
|
|
5
|
+
* The primitive used by lib/restore to put a freshly-decrypted bundle
|
|
6
|
+
* into place. Filesystem-level directory rename is atomic on POSIX
|
|
7
|
+
* (and on Windows when nothing has the dir open) — the swap either
|
|
8
|
+
* fully completes or the previous dataDir is recoverable.
|
|
9
|
+
*
|
|
10
|
+
* var rb = b.restoreRollback;
|
|
11
|
+
*
|
|
12
|
+
* var r = rb.swap({
|
|
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] }
|
|
29
|
+
*
|
|
30
|
+
* Layout after a successful swap:
|
|
31
|
+
*
|
|
32
|
+
* ./data ← the freshly-restored bundle
|
|
33
|
+
* ./data.rollbacks/
|
|
34
|
+
* 2026-04-27T17-46-36-075Z/ ← previous dataDir, renamed atomically
|
|
35
|
+
* (whatever was in dataDir at the time of swap)
|
|
36
|
+
* 2026-04-27T17-46-36-075Z.marker.json
|
|
37
|
+
*
|
|
38
|
+
* The marker file carries operator-supplied metadata (which bundle
|
|
39
|
+
* triggered the swap, what reason was given, when) so a list / audit
|
|
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.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
var fs = require("fs");
|
|
57
|
+
var path = require("path");
|
|
58
|
+
var atomicFile = require("./atomic-file");
|
|
59
|
+
var C = require("./constants");
|
|
60
|
+
var nb = require("./numeric-bounds");
|
|
61
|
+
var safeJson = require("./safe-json");
|
|
62
|
+
var { defineClass } = require("./framework-error");
|
|
63
|
+
|
|
64
|
+
var RestoreRollbackError = defineClass("RestoreRollbackError", { alwaysPermanent: true });
|
|
65
|
+
|
|
66
|
+
function _resolveRollbackRoot(opts) {
|
|
67
|
+
if (typeof opts.rollbackRoot === "string" && opts.rollbackRoot.length > 0) {
|
|
68
|
+
return opts.rollbackRoot;
|
|
69
|
+
}
|
|
70
|
+
// Default: sibling of dataDir named <dataDir>.rollbacks
|
|
71
|
+
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
72
|
+
throw new RestoreRollbackError("restore-rollback/no-rollback-root",
|
|
73
|
+
"rollbackRoot must be supplied or derivable from opts.dataDir");
|
|
74
|
+
}
|
|
75
|
+
return opts.dataDir + ".rollbacks";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
function swap(opts) {
|
|
80
|
+
opts = opts || {};
|
|
81
|
+
if (typeof opts.stagingDir !== "string" || !fs.existsSync(opts.stagingDir)) {
|
|
82
|
+
throw new RestoreRollbackError("restore-rollback/no-staging",
|
|
83
|
+
"swap: opts.stagingDir is required and must exist");
|
|
84
|
+
}
|
|
85
|
+
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
86
|
+
throw new RestoreRollbackError("restore-rollback/no-datadir",
|
|
87
|
+
"swap: opts.dataDir is required");
|
|
88
|
+
}
|
|
89
|
+
var rollbackRoot = _resolveRollbackRoot(opts);
|
|
90
|
+
atomicFile.ensureDir(rollbackRoot);
|
|
91
|
+
|
|
92
|
+
var swappedAt = atomicFile.pathTimestamp();
|
|
93
|
+
var rollbackPath = path.join(rollbackRoot, swappedAt);
|
|
94
|
+
var markerPath = path.join(rollbackRoot, swappedAt + ".marker.json");
|
|
95
|
+
|
|
96
|
+
if (fs.existsSync(rollbackPath) || fs.existsSync(markerPath)) {
|
|
97
|
+
throw new RestoreRollbackError("restore-rollback/collision",
|
|
98
|
+
"swap: a rollback at " + rollbackPath + " already exists — refusing to overwrite");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
var hadDataDir = fs.existsSync(opts.dataDir);
|
|
102
|
+
|
|
103
|
+
// Step 1: rename current dataDir → rollback path. Skipped on first
|
|
104
|
+
// restore (no existing dataDir).
|
|
105
|
+
if (hadDataDir) {
|
|
106
|
+
try { fs.renameSync(opts.dataDir, rollbackPath); }
|
|
107
|
+
catch (e) {
|
|
108
|
+
throw new RestoreRollbackError("restore-rollback/rename-existing-failed",
|
|
109
|
+
"swap: could not move existing dataDir to rollback: " + ((e && e.message) || String(e)));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Step 2: rename staging → dataDir
|
|
114
|
+
try { fs.renameSync(opts.stagingDir, opts.dataDir); }
|
|
115
|
+
catch (e) {
|
|
116
|
+
// Step 2 failed — try to undo step 1 so the operator's dataDir is back
|
|
117
|
+
if (hadDataDir) {
|
|
118
|
+
try { fs.renameSync(rollbackPath, opts.dataDir); }
|
|
119
|
+
catch (_e) { /* dataDir is now in rollbackPath; operator must recover manually */ }
|
|
120
|
+
}
|
|
121
|
+
throw new RestoreRollbackError("restore-rollback/rename-staging-failed",
|
|
122
|
+
"swap: could not move staging to dataDir: " + ((e && e.message) || String(e)) +
|
|
123
|
+
(hadDataDir ? " (attempted to undo previous rename — verify dataDir state)" : ""));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Step 3: write the marker (best-effort; missing marker is recoverable
|
|
127
|
+
// from the rollback dir's mtime, but operators want it for audit)
|
|
128
|
+
var marker = {
|
|
129
|
+
swappedAt: new Date().toISOString(),
|
|
130
|
+
rollbackPath: rollbackPath,
|
|
131
|
+
dataDir: opts.dataDir,
|
|
132
|
+
operator: opts.marker || null,
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2) + "\n", { mode: 0o600 });
|
|
136
|
+
} catch (_e) { /* marker write is best-effort */ }
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
rollbackPath: hadDataDir ? rollbackPath : null,
|
|
140
|
+
markerPath: markerPath,
|
|
141
|
+
swappedAt: swappedAt,
|
|
142
|
+
marker: marker,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function rollback(opts) {
|
|
147
|
+
opts = opts || {};
|
|
148
|
+
if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
|
|
149
|
+
throw new RestoreRollbackError("restore-rollback/no-datadir",
|
|
150
|
+
"rollback: opts.dataDir is required");
|
|
151
|
+
}
|
|
152
|
+
if (typeof opts.rollbackPath !== "string" || !fs.existsSync(opts.rollbackPath)) {
|
|
153
|
+
throw new RestoreRollbackError("restore-rollback/no-rollback",
|
|
154
|
+
"rollback: opts.rollbackPath is required and must exist");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Move the current dataDir aside (so the rollback's rename target is empty)
|
|
158
|
+
var discardedAt = null;
|
|
159
|
+
if (fs.existsSync(opts.dataDir)) {
|
|
160
|
+
var rollbackRoot = _resolveRollbackRoot(opts);
|
|
161
|
+
atomicFile.ensureDir(rollbackRoot);
|
|
162
|
+
discardedAt = atomicFile.pathTimestamp();
|
|
163
|
+
var discardedPath = path.join(rollbackRoot, "discarded-" + discardedAt);
|
|
164
|
+
try { fs.renameSync(opts.dataDir, discardedPath); }
|
|
165
|
+
catch (e) {
|
|
166
|
+
throw new RestoreRollbackError("restore-rollback/rename-existing-failed",
|
|
167
|
+
"rollback: could not move current dataDir aside: " + ((e && e.message) || String(e)));
|
|
168
|
+
}
|
|
169
|
+
discardedAt = discardedPath;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Rename the rollback dir back into dataDir's place
|
|
173
|
+
try { fs.renameSync(opts.rollbackPath, opts.dataDir); }
|
|
174
|
+
catch (e) {
|
|
175
|
+
throw new RestoreRollbackError("restore-rollback/rollback-rename-failed",
|
|
176
|
+
"rollback: could not move rollback into dataDir: " + ((e && e.message) || String(e)) +
|
|
177
|
+
" (current dataDir, if any, was moved to " + discardedAt + ")");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Best-effort: clean up the marker file alongside the rollback path
|
|
181
|
+
var markerPath = opts.rollbackPath + ".marker.json";
|
|
182
|
+
try { if (fs.existsSync(markerPath)) fs.unlinkSync(markerPath); }
|
|
183
|
+
catch (_e) { /* marker cleanup is best-effort */ }
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
restoredFrom: opts.rollbackPath,
|
|
187
|
+
discardedAt: discardedAt,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function list(opts) {
|
|
192
|
+
opts = opts || {};
|
|
193
|
+
var rollbackRoot = _resolveRollbackRoot(opts);
|
|
194
|
+
if (!fs.existsSync(rollbackRoot)) return [];
|
|
195
|
+
var entries = fs.readdirSync(rollbackRoot, { withFileTypes: true });
|
|
196
|
+
var out = [];
|
|
197
|
+
for (var i = 0; i < entries.length; i++) {
|
|
198
|
+
if (!entries[i].isDirectory()) continue;
|
|
199
|
+
var name = entries[i].name;
|
|
200
|
+
if (name.indexOf("discarded-") === 0) continue; // discarded dirs aren't restore points
|
|
201
|
+
var p = path.join(rollbackRoot, name);
|
|
202
|
+
var markerPath = p + ".marker.json";
|
|
203
|
+
var marker = null;
|
|
204
|
+
if (fs.existsSync(markerPath)) {
|
|
205
|
+
try { marker = safeJson.parse(fs.readFileSync(markerPath, "utf8"), { maxBytes: C.BYTES.kib(64) }); }
|
|
206
|
+
catch (_e) { marker = null; }
|
|
207
|
+
}
|
|
208
|
+
var stat;
|
|
209
|
+
try { stat = fs.statSync(p); } catch (_e) { continue; }
|
|
210
|
+
out.push({
|
|
211
|
+
rollbackPath: p,
|
|
212
|
+
swappedAt: (marker && marker.swappedAt) || stat.mtime.toISOString(),
|
|
213
|
+
marker: marker,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// Newest first
|
|
217
|
+
out.sort(function (a, b) { return a.swappedAt < b.swappedAt ? 1 : -1; });
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function purge(opts) {
|
|
222
|
+
opts = opts || {};
|
|
223
|
+
nb.requireNonNegativeFiniteIntIfPresent(opts.keep,
|
|
224
|
+
"restore-rollback.purge: opts.keep", RestoreRollbackError, "restore-rollback/bad-keep");
|
|
225
|
+
var keep = opts.keep !== undefined ? opts.keep : 0;
|
|
226
|
+
var rollbackRoot = _resolveRollbackRoot(opts);
|
|
227
|
+
if (!fs.existsSync(rollbackRoot)) return { kept: keep, deleted: [] };
|
|
228
|
+
// Always sweep "discarded-*" dirs — they're never restore points
|
|
229
|
+
var entries = fs.readdirSync(rollbackRoot, { withFileTypes: true });
|
|
230
|
+
var deleted = [];
|
|
231
|
+
for (var i = 0; i < entries.length; i++) {
|
|
232
|
+
if (entries[i].isDirectory() && entries[i].name.indexOf("discarded-") === 0) {
|
|
233
|
+
var p = path.join(rollbackRoot, entries[i].name);
|
|
234
|
+
try { fs.rmSync(p, { recursive: true, force: true }); deleted.push(p); }
|
|
235
|
+
catch (_e) { /* best-effort */ }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Keep newest `keep`, delete the rest (and their marker files)
|
|
240
|
+
var rb = list(opts);
|
|
241
|
+
var toDelete = rb.slice(keep);
|
|
242
|
+
for (var j = 0; j < toDelete.length; j++) {
|
|
243
|
+
var rbPath = toDelete[j].rollbackPath;
|
|
244
|
+
var mkPath = rbPath + ".marker.json";
|
|
245
|
+
try { fs.rmSync(rbPath, { recursive: true, force: true }); deleted.push(rbPath); }
|
|
246
|
+
catch (_e) { /* best-effort */ }
|
|
247
|
+
try { if (fs.existsSync(mkPath)) fs.unlinkSync(mkPath); }
|
|
248
|
+
catch (_e) { /* best-effort */ }
|
|
249
|
+
}
|
|
250
|
+
return { kept: keep, deleted: deleted };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
swap: swap,
|
|
255
|
+
rollback: rollback,
|
|
256
|
+
list: list,
|
|
257
|
+
purge: purge,
|
|
258
|
+
RestoreRollbackError: RestoreRollbackError,
|
|
259
|
+
};
|