@blamejs/core 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +201 -0
  3. package/LTS-CALENDAR.md +29 -0
  4. package/MIGRATING.md +7 -0
  5. package/NOTICE +59 -0
  6. package/README.md +100 -0
  7. package/bin/blamejs.js +13 -0
  8. package/index.js +253 -0
  9. package/lib/api-key.js +705 -0
  10. package/lib/api-snapshot.js +335 -0
  11. package/lib/app-shutdown.js +381 -0
  12. package/lib/app.js +364 -0
  13. package/lib/atomic-file.js +525 -0
  14. package/lib/audit-chain.js +168 -0
  15. package/lib/audit-sign.js +319 -0
  16. package/lib/audit-tools.js +682 -0
  17. package/lib/audit.js +753 -0
  18. package/lib/auth/jwt.js +280 -0
  19. package/lib/auth/oauth.js +691 -0
  20. package/lib/auth/passkey.js +185 -0
  21. package/lib/auth/password.js +139 -0
  22. package/lib/auth/totp.js +17 -0
  23. package/lib/auth-header.js +81 -0
  24. package/lib/backup/bundle.js +219 -0
  25. package/lib/backup/crypto.js +174 -0
  26. package/lib/backup/index.js +490 -0
  27. package/lib/backup/manifest.js +275 -0
  28. package/lib/bundler.js +295 -0
  29. package/lib/cache.js +819 -0
  30. package/lib/chain-writer.js +234 -0
  31. package/lib/cli-helpers.js +201 -0
  32. package/lib/cli.js +1377 -0
  33. package/lib/cluster-provider-db.js +245 -0
  34. package/lib/cluster-storage.js +166 -0
  35. package/lib/cluster.js +691 -0
  36. package/lib/consent.js +222 -0
  37. package/lib/constants.js +186 -0
  38. package/lib/cookies.js +293 -0
  39. package/lib/credential-hash.js +303 -0
  40. package/lib/crypto-field.js +159 -0
  41. package/lib/crypto.js +250 -0
  42. package/lib/db-query.js +297 -0
  43. package/lib/db-schema.js +250 -0
  44. package/lib/db.js +1054 -0
  45. package/lib/deprecate.js +226 -0
  46. package/lib/dev.js +324 -0
  47. package/lib/error-page.js +424 -0
  48. package/lib/events.js +135 -0
  49. package/lib/external-db.js +422 -0
  50. package/lib/forms.js +378 -0
  51. package/lib/framework-error.js +189 -0
  52. package/lib/framework-schema.js +604 -0
  53. package/lib/handlers.js +350 -0
  54. package/lib/html-balance.js +227 -0
  55. package/lib/http-client.js +615 -0
  56. package/lib/i18n.js +780 -0
  57. package/lib/jobs.js +181 -0
  58. package/lib/lazy-require.js +48 -0
  59. package/lib/log-stream-local.js +137 -0
  60. package/lib/log-stream-webhook.js +170 -0
  61. package/lib/log-stream.js +211 -0
  62. package/lib/log.js +355 -0
  63. package/lib/mail-bounce.js +507 -0
  64. package/lib/mail.js +701 -0
  65. package/lib/metrics.js +647 -0
  66. package/lib/middleware/api-encrypt.js +553 -0
  67. package/lib/middleware/attach-user.js +156 -0
  68. package/lib/middleware/body-parser.js +883 -0
  69. package/lib/middleware/bot-guard.js +148 -0
  70. package/lib/middleware/compression.js +436 -0
  71. package/lib/middleware/cors.js +236 -0
  72. package/lib/middleware/csp-nonce.js +332 -0
  73. package/lib/middleware/csrf-protect.js +275 -0
  74. package/lib/middleware/error-handler.js +46 -0
  75. package/lib/middleware/health.js +358 -0
  76. package/lib/middleware/index.js +52 -0
  77. package/lib/middleware/rate-limit.js +319 -0
  78. package/lib/middleware/request-id.js +53 -0
  79. package/lib/middleware/require-auth.js +95 -0
  80. package/lib/middleware/security-headers.js +91 -0
  81. package/lib/migrations.js +353 -0
  82. package/lib/mtls-ca.js +333 -0
  83. package/lib/mtls-engine-default.js +285 -0
  84. package/lib/nonce-store.js +177 -0
  85. package/lib/notify.js +643 -0
  86. package/lib/ntp-check.js +178 -0
  87. package/lib/object-store/azure-blob.js +467 -0
  88. package/lib/object-store/gcs.js +469 -0
  89. package/lib/object-store/http-put.js +153 -0
  90. package/lib/object-store/index.js +140 -0
  91. package/lib/object-store/local.js +163 -0
  92. package/lib/object-store/retry.js +15 -0
  93. package/lib/object-store/sigv4.js +535 -0
  94. package/lib/observability.js +114 -0
  95. package/lib/pagination.js +371 -0
  96. package/lib/parsers/index.js +64 -0
  97. package/lib/parsers/safe-csv.js +224 -0
  98. package/lib/parsers/safe-env.js +614 -0
  99. package/lib/parsers/safe-toml.js +745 -0
  100. package/lib/parsers/safe-xml.js +379 -0
  101. package/lib/parsers/safe-yaml.js +977 -0
  102. package/lib/permissions.js +430 -0
  103. package/lib/pqc-agent.js +85 -0
  104. package/lib/pqc-gate.js +266 -0
  105. package/lib/protocol-dispatcher.js +144 -0
  106. package/lib/queue-local.js +327 -0
  107. package/lib/queue.js +430 -0
  108. package/lib/redact.js +192 -0
  109. package/lib/render.js +193 -0
  110. package/lib/request-helpers.js +178 -0
  111. package/lib/restore-bundle.js +239 -0
  112. package/lib/restore-rollback.js +254 -0
  113. package/lib/restore.js +301 -0
  114. package/lib/retry.js +329 -0
  115. package/lib/router.js +437 -0
  116. package/lib/safe-async.js +520 -0
  117. package/lib/safe-buffer.js +162 -0
  118. package/lib/safe-json.js +532 -0
  119. package/lib/safe-schema.js +1176 -0
  120. package/lib/safe-sql.js +157 -0
  121. package/lib/safe-url.js +109 -0
  122. package/lib/scheduler.js +680 -0
  123. package/lib/seeders.js +622 -0
  124. package/lib/session.js +304 -0
  125. package/lib/slug.js +243 -0
  126. package/lib/static.js +268 -0
  127. package/lib/storage.js +470 -0
  128. package/lib/subject.js +281 -0
  129. package/lib/template.js +781 -0
  130. package/lib/testing.js +621 -0
  131. package/lib/totp.js +285 -0
  132. package/lib/tracing.js +484 -0
  133. package/lib/validate-opts.js +56 -0
  134. package/lib/vault/index.js +299 -0
  135. package/lib/vault/passphrase-ops.js +311 -0
  136. package/lib/vault/passphrase-source.js +198 -0
  137. package/lib/vault/rotate.js +761 -0
  138. package/lib/vault/wrap.js +289 -0
  139. package/lib/vendor/MANIFEST.json +84 -0
  140. package/lib/vendor/argon2/argon2.cjs +466 -0
  141. package/lib/vendor/argon2/argon2.d.cts +62 -0
  142. package/lib/vendor/argon2/package.json +1 -0
  143. package/lib/vendor/argon2/prebuilds/darwin-arm64/argon2.armv8.glibc.node +0 -0
  144. package/lib/vendor/argon2/prebuilds/darwin-x64/argon2.glibc.node +0 -0
  145. package/lib/vendor/argon2/prebuilds/freebsd-arm64/argon2.armv8.glibc.node +0 -0
  146. package/lib/vendor/argon2/prebuilds/freebsd-x64/argon2.glibc.node +0 -0
  147. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.glibc.node +0 -0
  148. package/lib/vendor/argon2/prebuilds/linux-arm/argon2.armv7.musl.node +0 -0
  149. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.glibc.node +0 -0
  150. package/lib/vendor/argon2/prebuilds/linux-arm64/argon2.armv8.musl.node +0 -0
  151. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.glibc.node +0 -0
  152. package/lib/vendor/argon2/prebuilds/linux-x64/argon2.musl.node +0 -0
  153. package/lib/vendor/argon2/prebuilds/win32-x64/argon2.glibc.node +0 -0
  154. package/lib/vendor/noble-ciphers.cjs +9 -0
  155. package/lib/vendor/pki.cjs +181 -0
  156. package/lib/vendor/simplewebauthn-server.cjs +328 -0
  157. package/lib/webhook.js +632 -0
  158. package/lib/websocket-channels.js +413 -0
  159. package/lib/websocket.js +833 -0
  160. package/package.json +39 -0
@@ -0,0 +1,254 @@
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 { defineClass } = require("./framework-error");
60
+
61
+ var RestoreRollbackError = defineClass("RestoreRollbackError", { alwaysPermanent: true });
62
+
63
+ function _resolveRollbackRoot(opts) {
64
+ if (typeof opts.rollbackRoot === "string" && opts.rollbackRoot.length > 0) {
65
+ return opts.rollbackRoot;
66
+ }
67
+ // Default: sibling of dataDir named <dataDir>.rollbacks
68
+ if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
69
+ throw new RestoreRollbackError("restore-rollback/no-rollback-root",
70
+ "rollbackRoot must be supplied or derivable from opts.dataDir");
71
+ }
72
+ return opts.dataDir + ".rollbacks";
73
+ }
74
+
75
+
76
+ function swap(opts) {
77
+ opts = opts || {};
78
+ if (typeof opts.stagingDir !== "string" || !fs.existsSync(opts.stagingDir)) {
79
+ throw new RestoreRollbackError("restore-rollback/no-staging",
80
+ "swap: opts.stagingDir is required and must exist");
81
+ }
82
+ if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
83
+ throw new RestoreRollbackError("restore-rollback/no-datadir",
84
+ "swap: opts.dataDir is required");
85
+ }
86
+ var rollbackRoot = _resolveRollbackRoot(opts);
87
+ atomicFile.ensureDir(rollbackRoot);
88
+
89
+ var swappedAt = atomicFile.pathTimestamp();
90
+ var rollbackPath = path.join(rollbackRoot, swappedAt);
91
+ var markerPath = path.join(rollbackRoot, swappedAt + ".marker.json");
92
+
93
+ if (fs.existsSync(rollbackPath) || fs.existsSync(markerPath)) {
94
+ throw new RestoreRollbackError("restore-rollback/collision",
95
+ "swap: a rollback at " + rollbackPath + " already exists — refusing to overwrite");
96
+ }
97
+
98
+ var hadDataDir = fs.existsSync(opts.dataDir);
99
+
100
+ // Step 1: rename current dataDir → rollback path. Skipped on first
101
+ // restore (no existing dataDir).
102
+ if (hadDataDir) {
103
+ try { fs.renameSync(opts.dataDir, rollbackPath); }
104
+ catch (e) {
105
+ throw new RestoreRollbackError("restore-rollback/rename-existing-failed",
106
+ "swap: could not move existing dataDir to rollback: " + ((e && e.message) || String(e)));
107
+ }
108
+ }
109
+
110
+ // Step 2: rename staging → dataDir
111
+ try { fs.renameSync(opts.stagingDir, opts.dataDir); }
112
+ catch (e) {
113
+ // Step 2 failed — try to undo step 1 so the operator's dataDir is back
114
+ if (hadDataDir) {
115
+ try { fs.renameSync(rollbackPath, opts.dataDir); }
116
+ catch (_e) { /* dataDir is now in rollbackPath; operator must recover manually */ }
117
+ }
118
+ throw new RestoreRollbackError("restore-rollback/rename-staging-failed",
119
+ "swap: could not move staging to dataDir: " + ((e && e.message) || String(e)) +
120
+ (hadDataDir ? " (attempted to undo previous rename — verify dataDir state)" : ""));
121
+ }
122
+
123
+ // Step 3: write the marker (best-effort; missing marker is recoverable
124
+ // from the rollback dir's mtime, but operators want it for audit)
125
+ var marker = {
126
+ swappedAt: new Date().toISOString(),
127
+ rollbackPath: rollbackPath,
128
+ dataDir: opts.dataDir,
129
+ operator: opts.marker || null,
130
+ };
131
+ try {
132
+ fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2) + "\n", { mode: 0o600 });
133
+ } catch (_e) { /* marker write is best-effort */ }
134
+
135
+ return {
136
+ rollbackPath: hadDataDir ? rollbackPath : null,
137
+ markerPath: markerPath,
138
+ swappedAt: swappedAt,
139
+ marker: marker,
140
+ };
141
+ }
142
+
143
+ async function rollback(opts) {
144
+ opts = opts || {};
145
+ if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
146
+ throw new RestoreRollbackError("restore-rollback/no-datadir",
147
+ "rollback: opts.dataDir is required");
148
+ }
149
+ if (typeof opts.rollbackPath !== "string" || !fs.existsSync(opts.rollbackPath)) {
150
+ throw new RestoreRollbackError("restore-rollback/no-rollback",
151
+ "rollback: opts.rollbackPath is required and must exist");
152
+ }
153
+
154
+ // Move the current dataDir aside (so the rollback's rename target is empty)
155
+ var discardedAt = null;
156
+ if (fs.existsSync(opts.dataDir)) {
157
+ var rollbackRoot = _resolveRollbackRoot(opts);
158
+ atomicFile.ensureDir(rollbackRoot);
159
+ discardedAt = atomicFile.pathTimestamp();
160
+ var discardedPath = path.join(rollbackRoot, "discarded-" + discardedAt);
161
+ try { fs.renameSync(opts.dataDir, discardedPath); }
162
+ catch (e) {
163
+ throw new RestoreRollbackError("restore-rollback/rename-existing-failed",
164
+ "rollback: could not move current dataDir aside: " + ((e && e.message) || String(e)));
165
+ }
166
+ discardedAt = discardedPath;
167
+ }
168
+
169
+ // Rename the rollback dir back into dataDir's place
170
+ try { fs.renameSync(opts.rollbackPath, opts.dataDir); }
171
+ catch (e) {
172
+ throw new RestoreRollbackError("restore-rollback/rollback-rename-failed",
173
+ "rollback: could not move rollback into dataDir: " + ((e && e.message) || String(e)) +
174
+ " (current dataDir, if any, was moved to " + discardedAt + ")");
175
+ }
176
+
177
+ // Best-effort: clean up the marker file alongside the rollback path
178
+ var markerPath = opts.rollbackPath + ".marker.json";
179
+ try { if (fs.existsSync(markerPath)) fs.unlinkSync(markerPath); }
180
+ catch (_e) { /* marker cleanup is best-effort */ }
181
+
182
+ return {
183
+ restoredFrom: opts.rollbackPath,
184
+ discardedAt: discardedAt,
185
+ };
186
+ }
187
+
188
+ function list(opts) {
189
+ opts = opts || {};
190
+ var rollbackRoot = _resolveRollbackRoot(opts);
191
+ if (!fs.existsSync(rollbackRoot)) return [];
192
+ var entries = fs.readdirSync(rollbackRoot, { withFileTypes: true });
193
+ var out = [];
194
+ for (var i = 0; i < entries.length; i++) {
195
+ if (!entries[i].isDirectory()) continue;
196
+ var name = entries[i].name;
197
+ if (name.indexOf("discarded-") === 0) continue; // discarded dirs aren't restore points
198
+ var p = path.join(rollbackRoot, name);
199
+ var markerPath = p + ".marker.json";
200
+ var marker = null;
201
+ if (fs.existsSync(markerPath)) {
202
+ try { marker = JSON.parse(fs.readFileSync(markerPath, "utf8")); }
203
+ catch (_e) { marker = null; }
204
+ }
205
+ var stat;
206
+ try { stat = fs.statSync(p); } catch (_e) { continue; }
207
+ out.push({
208
+ rollbackPath: p,
209
+ swappedAt: (marker && marker.swappedAt) || stat.mtime.toISOString(),
210
+ marker: marker,
211
+ });
212
+ }
213
+ // Newest first
214
+ out.sort(function (a, b) { return a.swappedAt < b.swappedAt ? 1 : -1; });
215
+ return out;
216
+ }
217
+
218
+ function purge(opts) {
219
+ opts = opts || {};
220
+ var keep = typeof opts.keep === "number" && opts.keep >= 0 ? Math.floor(opts.keep) : 0;
221
+ var rollbackRoot = _resolveRollbackRoot(opts);
222
+ if (!fs.existsSync(rollbackRoot)) return { kept: keep, deleted: [] };
223
+ // Always sweep "discarded-*" dirs — they're never restore points
224
+ var entries = fs.readdirSync(rollbackRoot, { withFileTypes: true });
225
+ var deleted = [];
226
+ for (var i = 0; i < entries.length; i++) {
227
+ if (entries[i].isDirectory() && entries[i].name.indexOf("discarded-") === 0) {
228
+ var p = path.join(rollbackRoot, entries[i].name);
229
+ try { fs.rmSync(p, { recursive: true, force: true }); deleted.push(p); }
230
+ catch (_e) { /* best-effort */ }
231
+ }
232
+ }
233
+
234
+ // Keep newest `keep`, delete the rest (and their marker files)
235
+ var rb = list(opts);
236
+ var toDelete = rb.slice(keep);
237
+ for (var j = 0; j < toDelete.length; j++) {
238
+ var rbPath = toDelete[j].rollbackPath;
239
+ var mkPath = rbPath + ".marker.json";
240
+ try { fs.rmSync(rbPath, { recursive: true, force: true }); deleted.push(rbPath); }
241
+ catch (_e) { /* best-effort */ }
242
+ try { if (fs.existsSync(mkPath)) fs.unlinkSync(mkPath); }
243
+ catch (_e) { /* best-effort */ }
244
+ }
245
+ return { kept: keep, deleted: deleted };
246
+ }
247
+
248
+ module.exports = {
249
+ swap: swap,
250
+ rollback: rollback,
251
+ list: list,
252
+ purge: purge,
253
+ RestoreRollbackError: RestoreRollbackError,
254
+ };
package/lib/restore.js ADDED
@@ -0,0 +1,301 @@
1
+ "use strict";
2
+ /**
3
+ * restore — operator-facing restore from a backup bundle in storage.
4
+ *
5
+ * Mirror of lib/backup. Pulls a bundle from a storage backend,
6
+ * decrypts it via lib/restore-bundle into a staging directory, then
7
+ * swaps that staging into place as the new dataDir via
8
+ * lib/restore-rollback (saving the previous dataDir as a versioned
9
+ * rollback point). Operators stop the framework first, run restore,
10
+ * then start the framework — same workflow as a database restore.
11
+ *
12
+ * var restore = b.restore.create({
13
+ * dataDir: "./data",
14
+ * storage: b.backup.localStorage({ root: "./backups" }),
15
+ * passphrase: Buffer.from("operator backup passphrase"),
16
+ * rollbackRoot: "./data.rollbacks", // optional; default <dataDir>.rollbacks
17
+ * audit: true,
18
+ * });
19
+ *
20
+ * await restore.list(); // → [{ bundleId, createdAt, size }]
21
+ * await restore.inspect(bundleId); // → manifest (no decrypt; no passphrase
22
+ * // needed for inspect)
23
+ *
24
+ * await restore.run({
25
+ * bundleId,
26
+ * filter, // optional file filter
27
+ * marker: { reason: "incident-2026-04-27" },
28
+ * });
29
+ * // → { bundleId, fileCount, totalBytes, rollbackPath, vaultKeyJson,
30
+ * // durationMs }
31
+ *
32
+ * await restore.rollback(); // → reverts the most recent restore
33
+ * await restore.listRollbacks(); // → [{ rollbackPath, swappedAt, marker }]
34
+ * await restore.purgeRollbacks({ keep });
35
+ *
36
+ * vaultKeyJson: the manifest's vaultKeyEnc decrypted via the passphrase,
37
+ * returned to the caller as a string. Returned but NOT auto-installed —
38
+ * the caller decides what to do with it (the swap may have already put
39
+ * the bundle's vault.key file into place if the bundle included one;
40
+ * operators with vault-passphrase-wrapped setups handle the placement
41
+ * themselves before re-starting the framework).
42
+ *
43
+ * Failure modes (each cleans up the tmp pull dir + staging dir):
44
+ * - bundle not in storage → restore/bundle-not-found
45
+ * - manifest absent / bad → restore/missing-manifest
46
+ * - wrong passphrase → restore/decrypt-failed
47
+ * - tampered blob → restore/decrypt-failed (AEAD tag check)
48
+ * - checksum mismatch → restore/checksum-mismatch
49
+ * - swap fails after extract → restore/swap-failed (staging is
50
+ * preserved at the path returned in error.stagingDir for operator
51
+ * manual recovery)
52
+ */
53
+
54
+ var fs = require("fs");
55
+ var nodeCrypto = require("node:crypto");
56
+ var os = require("os");
57
+ var path = require("path");
58
+ var restoreBundle = require("./restore-bundle");
59
+ var restoreRollback = require("./restore-rollback");
60
+ var lazyRequire = require("./lazy-require");
61
+ var validateOpts = require("./validate-opts");
62
+ var audit = lazyRequire(function () { return require("./audit"); });
63
+ var { FrameworkError } = require("./framework-error");
64
+
65
+ class RestoreError extends FrameworkError {
66
+ constructor(code, message, permanent) {
67
+ super(message, code);
68
+ this.name = "RestoreError";
69
+ this.permanent = !!permanent;
70
+ this.isRestoreError = true;
71
+ }
72
+ }
73
+
74
+ function _validateStorage(storage) {
75
+ if (!storage || typeof storage !== "object") {
76
+ throw new RestoreError("restore/bad-storage",
77
+ "storage backend is required (use b.backup.localStorage or pass a custom one)");
78
+ }
79
+ var required = ["readBundle", "listBundles", "hasBundle"];
80
+ for (var i = 0; i < required.length; i++) {
81
+ if (typeof storage[required[i]] !== "function") {
82
+ throw new RestoreError("restore/bad-storage",
83
+ "storage backend missing method '" + required[i] + "'");
84
+ }
85
+ }
86
+ }
87
+
88
+ function create(opts) {
89
+ opts = opts || {};
90
+ validateOpts(opts, [
91
+ "dataDir", "storage", "passphrase", "rollbackRoot", "audit",
92
+ ], "restore");
93
+ if (typeof opts.dataDir !== "string" || opts.dataDir.length === 0) {
94
+ throw new RestoreError("restore/no-datadir",
95
+ "create: opts.dataDir is required");
96
+ }
97
+ _validateStorage(opts.storage);
98
+ if (!Buffer.isBuffer(opts.passphrase) && typeof opts.passphrase !== "string") {
99
+ throw new RestoreError("restore/no-passphrase",
100
+ "create: opts.passphrase is required (Buffer or string)");
101
+ }
102
+
103
+ var dataDir = opts.dataDir;
104
+ var storage = opts.storage;
105
+ var passphrase = opts.passphrase;
106
+ var rollbackRoot = opts.rollbackRoot || (dataDir + ".rollbacks");
107
+ var auditOn = opts.audit !== false;
108
+
109
+ function _emitAudit(action, info, outcome) {
110
+ if (!auditOn) return;
111
+ audit().safeEmit({
112
+ action: action,
113
+ outcome: outcome,
114
+ metadata: info || {},
115
+ reason: info && info.reason ? info.reason : null,
116
+ });
117
+ }
118
+
119
+ async function list() { return await storage.listBundles(); }
120
+
121
+ async function inspect(bundleId) {
122
+ if (typeof bundleId !== "string" || bundleId.length === 0) {
123
+ throw new RestoreError("restore/bad-bundle-id", "inspect: bundleId is required");
124
+ }
125
+ var has = await storage.hasBundle(bundleId);
126
+ if (!has) {
127
+ throw new RestoreError("restore/bundle-not-found",
128
+ "inspect: bundle '" + bundleId + "' not in storage");
129
+ }
130
+ var pullDir = path.join(os.tmpdir(),
131
+ "blamejs-restore-inspect-" + nodeCrypto.randomBytes(4).toString("hex"));
132
+ try {
133
+ await storage.readBundle(bundleId, pullDir);
134
+ return restoreBundle.inspect({ bundleDir: pullDir });
135
+ } finally {
136
+ try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
137
+ }
138
+ }
139
+
140
+ async function run(runOpts) {
141
+ runOpts = runOpts || {};
142
+ var t0 = Date.now();
143
+ var bundleId = runOpts.bundleId;
144
+ if (typeof bundleId !== "string" || bundleId.length === 0) {
145
+ throw new RestoreError("restore/bad-bundle-id", "run: opts.bundleId is required");
146
+ }
147
+ var has = await storage.hasBundle(bundleId);
148
+ if (!has) {
149
+ throw new RestoreError("restore/bundle-not-found",
150
+ "run: bundle '" + bundleId + "' not in storage");
151
+ }
152
+
153
+ var pullId = nodeCrypto.randomBytes(4).toString("hex");
154
+ var pullDir = path.join(os.tmpdir(), "blamejs-restore-pull-" + pullId);
155
+ var stagingDir = path.join(os.tmpdir(), "blamejs-restore-staging-" + pullId);
156
+
157
+ function _cleanupTmp() {
158
+ try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
159
+ try { fs.rmSync(stagingDir, { recursive: true, force: true }); } catch (_e) {}
160
+ }
161
+
162
+ // 1. Pull bundle out of storage
163
+ try {
164
+ await storage.readBundle(bundleId, pullDir);
165
+ } catch (e) {
166
+ _cleanupTmp();
167
+ _emitAudit("restore.failure",
168
+ { bundleId: bundleId, reason: "storage.readBundle: " + ((e && e.message) || String(e)) },
169
+ "failure");
170
+ throw new RestoreError("restore/storage-read-failed",
171
+ "pulling bundle from storage failed: " + ((e && e.message) || String(e)));
172
+ }
173
+
174
+ // 2. Decrypt + verify into stagingDir
175
+ var extracted;
176
+ try {
177
+ extracted = await restoreBundle.extract({
178
+ bundleDir: pullDir,
179
+ stagingDir: stagingDir,
180
+ passphrase: passphrase,
181
+ filter: runOpts.filter,
182
+ progressCallback: runOpts.progressCallback,
183
+ });
184
+ } catch (e) {
185
+ _cleanupTmp();
186
+ // Map restore-bundle error codes to restore/* domain so consumer
187
+ // code sees a single error namespace
188
+ var code = e && e.code;
189
+ var mappedCode = "restore/extract-failed";
190
+ if (code === "restore-bundle/decrypt-failed") mappedCode = "restore/decrypt-failed";
191
+ else if (code === "restore-bundle/checksum-mismatch") mappedCode = "restore/checksum-mismatch";
192
+ else if (code === "restore-bundle/missing-manifest") mappedCode = "restore/missing-manifest";
193
+ else if (code === "restore-bundle/missing-blob") mappedCode = "restore/missing-blob";
194
+ else if (code === "restore-bundle/size-mismatch") mappedCode = "restore/size-mismatch";
195
+ _emitAudit("restore.failure",
196
+ { bundleId: bundleId, reason: (e && e.message) || String(e) }, "failure");
197
+ throw new RestoreError(mappedCode,
198
+ "extract failed: " + ((e && e.message) || String(e)));
199
+ }
200
+
201
+ // 3. Atomic swap. On swap failure, the stagingDir is preserved so
202
+ // an operator can recover manually — we do NOT delete it here.
203
+ var swapResult;
204
+ try {
205
+ swapResult = restoreRollback.swap({
206
+ stagingDir: stagingDir,
207
+ dataDir: dataDir,
208
+ rollbackRoot: rollbackRoot,
209
+ marker: Object.assign({ bundleId: bundleId }, runOpts.marker || {}),
210
+ });
211
+ } catch (e) {
212
+ // Pull dir is safe to clean (the source bundle is in storage);
213
+ // staging stays for manual recovery.
214
+ try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
215
+ _emitAudit("restore.failure",
216
+ { bundleId: bundleId, reason: "swap: " + ((e && e.message) || String(e)) },
217
+ "failure");
218
+ var err = new RestoreError("restore/swap-failed",
219
+ "atomic swap failed after successful extract — staging preserved at " +
220
+ stagingDir + ": " + ((e && e.message) || String(e)));
221
+ err.stagingDir = stagingDir;
222
+ throw err;
223
+ }
224
+
225
+ // 4. Clean up the pull dir (source bundle still in storage)
226
+ try { fs.rmSync(pullDir, { recursive: true, force: true }); } catch (_e) {}
227
+
228
+ var summary = {
229
+ bundleId: bundleId,
230
+ fileCount: extracted.fileCount,
231
+ totalBytes: extracted.totalBytes,
232
+ rollbackPath: swapResult.rollbackPath,
233
+ vaultKeyJson: extracted.vaultKeyJson,
234
+ durationMs: Date.now() - t0,
235
+ };
236
+ _emitAudit("restore.success", {
237
+ bundleId: bundleId,
238
+ fileCount: extracted.fileCount,
239
+ totalBytes: extracted.totalBytes,
240
+ rollbackPath: swapResult.rollbackPath,
241
+ durationMs: summary.durationMs,
242
+ });
243
+ return summary;
244
+ }
245
+
246
+ async function rollback(rollbackOpts) {
247
+ rollbackOpts = rollbackOpts || {};
248
+ // Either an explicit rollbackPath OR pull the most-recent one
249
+ var target = rollbackOpts.rollbackPath;
250
+ if (!target) {
251
+ var bundles = restoreRollback.list({ rollbackRoot: rollbackRoot });
252
+ if (bundles.length === 0) {
253
+ throw new RestoreError("restore/no-rollbacks",
254
+ "rollback: no rollback points found at " + rollbackRoot);
255
+ }
256
+ target = bundles[0].rollbackPath;
257
+ }
258
+ var r;
259
+ try {
260
+ r = await restoreRollback.rollback({
261
+ dataDir: dataDir,
262
+ rollbackPath: target,
263
+ rollbackRoot: rollbackRoot,
264
+ });
265
+ } catch (e) {
266
+ _emitAudit("restore.rollback.failure",
267
+ { rollbackPath: target, reason: (e && e.message) || String(e) }, "failure");
268
+ throw new RestoreError("restore/rollback-failed",
269
+ "rollback failed: " + ((e && e.message) || String(e)));
270
+ }
271
+ _emitAudit("restore.rollback.success",
272
+ { rollbackPath: target, discardedAt: r.discardedAt });
273
+ return r;
274
+ }
275
+
276
+ function listRollbacks() {
277
+ return restoreRollback.list({ rollbackRoot: rollbackRoot });
278
+ }
279
+ function purgeRollbacks(purgeOpts) {
280
+ return restoreRollback.purge({
281
+ rollbackRoot: rollbackRoot,
282
+ keep: (purgeOpts && purgeOpts.keep) || 0,
283
+ });
284
+ }
285
+
286
+ return {
287
+ list: list,
288
+ inspect: inspect,
289
+ run: run,
290
+ rollback: rollback,
291
+ listRollbacks: listRollbacks,
292
+ purgeRollbacks: purgeRollbacks,
293
+ storage: storage,
294
+ rollbackRoot: rollbackRoot,
295
+ };
296
+ }
297
+
298
+ module.exports = {
299
+ create: create,
300
+ RestoreError: RestoreError,
301
+ };