@blamejs/core 0.7.18 → 0.7.19
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 +425 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- 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-external.js +365 -0
- package/lib/auth/jwt.js +337 -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 +628 -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/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 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- 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/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- 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 -369
- package/lib/mail.js +981 -981
- 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/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- 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 +82 -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 -111
- 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 +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- 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 -340
- 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/backup/manifest.js
CHANGED
|
@@ -1,282 +1,282 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* backup-manifest — schema + validation for backup bundle manifests.
|
|
4
|
-
*
|
|
5
|
-
* Every backup bundle the framework produces carries a manifest.json
|
|
6
|
-
* at its root. The manifest is the single source of truth for: which
|
|
7
|
-
* files are in the bundle, their plaintext size + checksum, the per-
|
|
8
|
-
* file salt used by backup-crypto, and the wrapped vault key needed
|
|
9
|
-
* to interpret vault-sealed file contents post-restore.
|
|
10
|
-
*
|
|
11
|
-
* The encrypted file BYTES are stored separately under the bundle's
|
|
12
|
-
* files/ directory (one file per entry) — the manifest references
|
|
13
|
-
* them by relativePath. This keeps the manifest small enough to
|
|
14
|
-
* inspect / log without carrying gigabytes of base64 inline, and lets
|
|
15
|
-
* a restorer stream-decrypt files one at a time without loading the
|
|
16
|
-
* whole bundle into memory.
|
|
17
|
-
*
|
|
18
|
-
* Manifest format (v1):
|
|
19
|
-
*
|
|
20
|
-
* {
|
|
21
|
-
* version: 1,
|
|
22
|
-
* createdAt: "<ISO 8601 string>",
|
|
23
|
-
* framework: "blamejs",
|
|
24
|
-
* frameworkVersion: "0.1.84",
|
|
25
|
-
* vaultKeySalt: "<hex>", // salt used to wrap vault.key
|
|
26
|
-
* vaultKeyEnc: "<base64>", // wrapped vault.key bytes
|
|
27
|
-
* files: [
|
|
28
|
-
* {
|
|
29
|
-
* relativePath: "db.enc", // operator-facing path under dataDir
|
|
30
|
-
* encryptedPath: "files/db.enc.bin", // path within the bundle
|
|
31
|
-
* size: 12345, // plaintext size in bytes
|
|
32
|
-
* checksum: "<sha3-512 hex>", // checksum of the PLAINTEXT
|
|
33
|
-
* encryptedSize: 12369, // size of the encrypted blob (bytes)
|
|
34
|
-
* salt: "<hex>", // per-file Argon2 salt
|
|
35
|
-
* kind: "raw" // "raw" | "vault-sealed" | "plaintext"
|
|
36
|
-
* },
|
|
37
|
-
* ...
|
|
38
|
-
* ],
|
|
39
|
-
* metadata: { ... operator-supplied free-form ... }
|
|
40
|
-
* }
|
|
41
|
-
*
|
|
42
|
-
* The `kind` field hints at how the post-restore framework should
|
|
43
|
-
* interpret the file:
|
|
44
|
-
* - raw on-disk binary that the framework reads as-is
|
|
45
|
-
* (db.enc, db.key.enc, etc.)
|
|
46
|
-
* - vault-sealed the file's contents are vault-prefix wrapped
|
|
47
|
-
* (legacy-style sealed PEMs); restore writes them
|
|
48
|
-
* back as-is and the framework unseals at use
|
|
49
|
-
* - plaintext operator content not under vault discipline
|
|
50
|
-
* (config snapshots, README, etc.)
|
|
51
|
-
*
|
|
52
|
-
* The kind is informational — the bundler does NOT re-seal or unseal
|
|
53
|
-
* based on it; backup-crypto encrypts every file's bytes the same
|
|
54
|
-
* way regardless of kind. Operators / dashboards can use kind for
|
|
55
|
-
* post-restore validation.
|
|
56
|
-
*
|
|
57
|
-
* var bm = b.backupManifest;
|
|
58
|
-
*
|
|
59
|
-
* var manifest = bm.create({
|
|
60
|
-
* vaultKeySalt: "...",
|
|
61
|
-
* vaultKeyEnc: "<base64>",
|
|
62
|
-
* files: [...],
|
|
63
|
-
* metadata: { reason: "scheduled-daily" },
|
|
64
|
-
* });
|
|
65
|
-
* var json = bm.serialize(manifest); // canonical JSON for storage
|
|
66
|
-
*
|
|
67
|
-
* var loaded = bm.parse(json); // validates on parse, throws on error
|
|
68
|
-
* var v = bm.validate(loaded); // → { ok, errors } without throwing
|
|
69
|
-
*/
|
|
70
|
-
|
|
71
|
-
var C = require("../constants");
|
|
72
|
-
var safeBuffer = require("../safe-buffer");
|
|
73
|
-
var safeJson = require("../safe-json");
|
|
74
|
-
var { FrameworkError } = require("../framework-error");
|
|
75
|
-
|
|
76
|
-
class BackupManifestError extends FrameworkError {
|
|
77
|
-
constructor(code, message) {
|
|
78
|
-
super(message, code);
|
|
79
|
-
this.name = "BackupManifestError";
|
|
80
|
-
this.permanent = true;
|
|
81
|
-
this.isBackupManifestError = true;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
var FORMAT_VERSION = 1;
|
|
86
|
-
var FRAMEWORK_NAME = "blamejs";
|
|
87
|
-
var VALID_KINDS = { "raw": 1, "vault-sealed": 1, "plaintext": 1 };
|
|
88
|
-
// SHA3-512 produces 64 raw bytes — each byte serializes as 2 hex chars
|
|
89
|
-
// in the manifest, so the hex string is 128 chars long.
|
|
90
|
-
var SHA3_512_HEX_LENGTH = 128;
|
|
91
|
-
var HEX_RE = safeBuffer.HEX_RE;
|
|
92
|
-
var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
93
|
-
|
|
94
|
-
function _isHex(s, evenLength) {
|
|
95
|
-
if (typeof s !== "string" || s.length === 0) return false;
|
|
96
|
-
if (!HEX_RE.test(s)) return false;
|
|
97
|
-
if (evenLength && s.length % 2 !== 0) return false;
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
function _isBase64(s) {
|
|
101
|
-
return typeof s === "string" && s.length > 0 && BASE64_RE.test(s);
|
|
102
|
-
}
|
|
103
|
-
function _isIso8601(s) {
|
|
104
|
-
if (typeof s !== "string" || s.length === 0) return false;
|
|
105
|
-
var d = new Date(s);
|
|
106
|
-
return !isNaN(d.getTime()) && d.toISOString() === s;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function _validateFileEntry(f, idx, errors) {
|
|
110
|
-
if (!f || typeof f !== "object") {
|
|
111
|
-
errors.push("files[" + idx + "]: must be an object");
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (typeof f.relativePath !== "string" || f.relativePath.length === 0) {
|
|
115
|
-
errors.push("files[" + idx + "].relativePath: required non-empty string");
|
|
116
|
-
} else if (f.relativePath.indexOf("..") !== -1 || /^[/\\]/.test(f.relativePath)) {
|
|
117
|
-
// No traversal — when restored, relativePath joins under dataDir
|
|
118
|
-
errors.push("files[" + idx + "].relativePath: must be a relative path without '..' or leading separator");
|
|
119
|
-
}
|
|
120
|
-
if (typeof f.encryptedPath !== "string" || f.encryptedPath.length === 0) {
|
|
121
|
-
errors.push("files[" + idx + "].encryptedPath: required non-empty string");
|
|
122
|
-
} else if (f.encryptedPath.indexOf("..") !== -1 || /^[/\\]/.test(f.encryptedPath)) {
|
|
123
|
-
errors.push("files[" + idx + "].encryptedPath: must be a relative path without '..' or leading separator");
|
|
124
|
-
}
|
|
125
|
-
if (typeof f.size !== "number" || !Number.isInteger(f.size) || f.size < 0) {
|
|
126
|
-
errors.push("files[" + idx + "].size: required non-negative integer");
|
|
127
|
-
}
|
|
128
|
-
if (typeof f.encryptedSize !== "number" || !Number.isInteger(f.encryptedSize) || f.encryptedSize < 0) {
|
|
129
|
-
errors.push("files[" + idx + "].encryptedSize: required non-negative integer");
|
|
130
|
-
}
|
|
131
|
-
if (!_isHex(f.checksum, true) || f.checksum.length !== SHA3_512_HEX_LENGTH) {
|
|
132
|
-
errors.push("files[" + idx + "].checksum: required 128-char hex string (sha3-512)");
|
|
133
|
-
}
|
|
134
|
-
if (!_isHex(f.salt, true)) {
|
|
135
|
-
errors.push("files[" + idx + "].salt: required hex string");
|
|
136
|
-
}
|
|
137
|
-
if (typeof f.kind !== "string" || !VALID_KINDS[f.kind]) {
|
|
138
|
-
errors.push("files[" + idx + "].kind: must be one of raw, vault-sealed, plaintext");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function validate(manifest) {
|
|
143
|
-
var errors = [];
|
|
144
|
-
if (!manifest || typeof manifest !== "object") {
|
|
145
|
-
return { ok: false, errors: ["manifest must be an object"] };
|
|
146
|
-
}
|
|
147
|
-
if (manifest.version !== FORMAT_VERSION) {
|
|
148
|
-
errors.push("version: required " + FORMAT_VERSION + ", got " + manifest.version);
|
|
149
|
-
}
|
|
150
|
-
if (manifest.framework !== FRAMEWORK_NAME) {
|
|
151
|
-
errors.push("framework: required '" + FRAMEWORK_NAME + "', got " + JSON.stringify(manifest.framework));
|
|
152
|
-
}
|
|
153
|
-
if (typeof manifest.frameworkVersion !== "string" || manifest.frameworkVersion.length === 0) {
|
|
154
|
-
errors.push("frameworkVersion: required non-empty string");
|
|
155
|
-
}
|
|
156
|
-
if (!_isIso8601(manifest.createdAt)) {
|
|
157
|
-
errors.push("createdAt: required ISO-8601 timestamp string");
|
|
158
|
-
}
|
|
159
|
-
if (!_isHex(manifest.vaultKeySalt, true)) {
|
|
160
|
-
errors.push("vaultKeySalt: required hex string");
|
|
161
|
-
}
|
|
162
|
-
if (!_isBase64(manifest.vaultKeyEnc)) {
|
|
163
|
-
errors.push("vaultKeyEnc: required base64 string");
|
|
164
|
-
}
|
|
165
|
-
if (!Array.isArray(manifest.files)) {
|
|
166
|
-
errors.push("files: required array");
|
|
167
|
-
} else {
|
|
168
|
-
// Detect duplicate relativePath / encryptedPath — would corrupt restore
|
|
169
|
-
var seenRel = Object.create(null);
|
|
170
|
-
var seenEnc = Object.create(null);
|
|
171
|
-
for (var i = 0; i < manifest.files.length; i++) {
|
|
172
|
-
_validateFileEntry(manifest.files[i], i, errors);
|
|
173
|
-
var f = manifest.files[i];
|
|
174
|
-
if (f && typeof f.relativePath === "string") {
|
|
175
|
-
if (seenRel[f.relativePath]) {
|
|
176
|
-
errors.push("files[" + i + "].relativePath: duplicate '" + f.relativePath + "'");
|
|
177
|
-
}
|
|
178
|
-
seenRel[f.relativePath] = true;
|
|
179
|
-
}
|
|
180
|
-
if (f && typeof f.encryptedPath === "string") {
|
|
181
|
-
if (seenEnc[f.encryptedPath]) {
|
|
182
|
-
errors.push("files[" + i + "].encryptedPath: duplicate '" + f.encryptedPath + "'");
|
|
183
|
-
}
|
|
184
|
-
seenEnc[f.encryptedPath] = true;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
if (manifest.metadata !== undefined &&
|
|
189
|
-
(manifest.metadata === null || typeof manifest.metadata !== "object" || Array.isArray(manifest.metadata))) {
|
|
190
|
-
errors.push("metadata: must be a plain object when present");
|
|
191
|
-
}
|
|
192
|
-
return { ok: errors.length === 0, errors: errors };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function create(opts) {
|
|
196
|
-
opts = opts || {};
|
|
197
|
-
var manifest = {
|
|
198
|
-
version: FORMAT_VERSION,
|
|
199
|
-
framework: FRAMEWORK_NAME,
|
|
200
|
-
frameworkVersion: typeof opts.frameworkVersion === "string" && opts.frameworkVersion.length > 0
|
|
201
|
-
? opts.frameworkVersion
|
|
202
|
-
: (C.version || "0.0.0"),
|
|
203
|
-
createdAt: opts.createdAt || new Date().toISOString(),
|
|
204
|
-
vaultKeySalt: opts.vaultKeySalt,
|
|
205
|
-
vaultKeyEnc: opts.vaultKeyEnc,
|
|
206
|
-
files: Array.isArray(opts.files) ? opts.files.slice() : [],
|
|
207
|
-
};
|
|
208
|
-
if (opts.metadata && typeof opts.metadata === "object" && !Array.isArray(opts.metadata)) {
|
|
209
|
-
manifest.metadata = Object.assign({}, opts.metadata);
|
|
210
|
-
}
|
|
211
|
-
var v = validate(manifest);
|
|
212
|
-
if (!v.ok) {
|
|
213
|
-
throw new BackupManifestError("backup-manifest/invalid",
|
|
214
|
-
"create: " + v.errors.join("; "));
|
|
215
|
-
}
|
|
216
|
-
return manifest;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function serialize(manifest) {
|
|
220
|
-
var v = validate(manifest);
|
|
221
|
-
if (!v.ok) {
|
|
222
|
-
throw new BackupManifestError("backup-manifest/invalid",
|
|
223
|
-
"serialize: " + v.errors.join("; "));
|
|
224
|
-
}
|
|
225
|
-
// Stable key ordering so the same manifest object always serializes
|
|
226
|
-
// to the same bytes (operators can hash the manifest as part of
|
|
227
|
-
// bundle-integrity logging without surprises across runs).
|
|
228
|
-
var canonical = {
|
|
229
|
-
version: manifest.version,
|
|
230
|
-
framework: manifest.framework,
|
|
231
|
-
frameworkVersion: manifest.frameworkVersion,
|
|
232
|
-
createdAt: manifest.createdAt,
|
|
233
|
-
vaultKeySalt: manifest.vaultKeySalt,
|
|
234
|
-
vaultKeyEnc: manifest.vaultKeyEnc,
|
|
235
|
-
files: manifest.files.map(function (f) {
|
|
236
|
-
return {
|
|
237
|
-
relativePath: f.relativePath,
|
|
238
|
-
encryptedPath: f.encryptedPath,
|
|
239
|
-
size: f.size,
|
|
240
|
-
encryptedSize: f.encryptedSize,
|
|
241
|
-
checksum: f.checksum,
|
|
242
|
-
salt: f.salt,
|
|
243
|
-
kind: f.kind,
|
|
244
|
-
};
|
|
245
|
-
}),
|
|
246
|
-
};
|
|
247
|
-
if (manifest.metadata) canonical.metadata = manifest.metadata;
|
|
248
|
-
return JSON.stringify(canonical, null, 2) + "\n";
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function parse(jsonStr) {
|
|
252
|
-
if (typeof jsonStr !== "string" && !Buffer.isBuffer(jsonStr)) {
|
|
253
|
-
throw new BackupManifestError("backup-manifest/bad-input",
|
|
254
|
-
"parse: argument must be a string or Buffer");
|
|
255
|
-
}
|
|
256
|
-
var s = Buffer.isBuffer(jsonStr) ? jsonStr.toString("utf8") : jsonStr;
|
|
257
|
-
var obj;
|
|
258
|
-
// Backup manifests are file-bounded operator-supplied input. 16 MiB
|
|
259
|
-
// is generous; real manifests are kilobytes.
|
|
260
|
-
try { obj = safeJson.parse(s, { maxBytes: C.BYTES.mib(16) }); }
|
|
261
|
-
catch (e) {
|
|
262
|
-
throw new BackupManifestError("backup-manifest/bad-json",
|
|
263
|
-
"parse: not valid JSON: " + ((e && e.message) || String(e)));
|
|
264
|
-
}
|
|
265
|
-
var v = validate(obj);
|
|
266
|
-
if (!v.ok) {
|
|
267
|
-
throw new BackupManifestError("backup-manifest/invalid",
|
|
268
|
-
"parse: " + v.errors.join("; "));
|
|
269
|
-
}
|
|
270
|
-
return obj;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
module.exports = {
|
|
274
|
-
create: create,
|
|
275
|
-
validate: validate,
|
|
276
|
-
serialize: serialize,
|
|
277
|
-
parse: parse,
|
|
278
|
-
FORMAT_VERSION: FORMAT_VERSION,
|
|
279
|
-
FRAMEWORK_NAME: FRAMEWORK_NAME,
|
|
280
|
-
VALID_KINDS: VALID_KINDS,
|
|
281
|
-
BackupManifestError: BackupManifestError,
|
|
282
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* backup-manifest — schema + validation for backup bundle manifests.
|
|
4
|
+
*
|
|
5
|
+
* Every backup bundle the framework produces carries a manifest.json
|
|
6
|
+
* at its root. The manifest is the single source of truth for: which
|
|
7
|
+
* files are in the bundle, their plaintext size + checksum, the per-
|
|
8
|
+
* file salt used by backup-crypto, and the wrapped vault key needed
|
|
9
|
+
* to interpret vault-sealed file contents post-restore.
|
|
10
|
+
*
|
|
11
|
+
* The encrypted file BYTES are stored separately under the bundle's
|
|
12
|
+
* files/ directory (one file per entry) — the manifest references
|
|
13
|
+
* them by relativePath. This keeps the manifest small enough to
|
|
14
|
+
* inspect / log without carrying gigabytes of base64 inline, and lets
|
|
15
|
+
* a restorer stream-decrypt files one at a time without loading the
|
|
16
|
+
* whole bundle into memory.
|
|
17
|
+
*
|
|
18
|
+
* Manifest format (v1):
|
|
19
|
+
*
|
|
20
|
+
* {
|
|
21
|
+
* version: 1,
|
|
22
|
+
* createdAt: "<ISO 8601 string>",
|
|
23
|
+
* framework: "blamejs",
|
|
24
|
+
* frameworkVersion: "0.1.84",
|
|
25
|
+
* vaultKeySalt: "<hex>", // salt used to wrap vault.key
|
|
26
|
+
* vaultKeyEnc: "<base64>", // wrapped vault.key bytes
|
|
27
|
+
* files: [
|
|
28
|
+
* {
|
|
29
|
+
* relativePath: "db.enc", // operator-facing path under dataDir
|
|
30
|
+
* encryptedPath: "files/db.enc.bin", // path within the bundle
|
|
31
|
+
* size: 12345, // plaintext size in bytes
|
|
32
|
+
* checksum: "<sha3-512 hex>", // checksum of the PLAINTEXT
|
|
33
|
+
* encryptedSize: 12369, // size of the encrypted blob (bytes)
|
|
34
|
+
* salt: "<hex>", // per-file Argon2 salt
|
|
35
|
+
* kind: "raw" // "raw" | "vault-sealed" | "plaintext"
|
|
36
|
+
* },
|
|
37
|
+
* ...
|
|
38
|
+
* ],
|
|
39
|
+
* metadata: { ... operator-supplied free-form ... }
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* The `kind` field hints at how the post-restore framework should
|
|
43
|
+
* interpret the file:
|
|
44
|
+
* - raw on-disk binary that the framework reads as-is
|
|
45
|
+
* (db.enc, db.key.enc, etc.)
|
|
46
|
+
* - vault-sealed the file's contents are vault-prefix wrapped
|
|
47
|
+
* (legacy-style sealed PEMs); restore writes them
|
|
48
|
+
* back as-is and the framework unseals at use
|
|
49
|
+
* - plaintext operator content not under vault discipline
|
|
50
|
+
* (config snapshots, README, etc.)
|
|
51
|
+
*
|
|
52
|
+
* The kind is informational — the bundler does NOT re-seal or unseal
|
|
53
|
+
* based on it; backup-crypto encrypts every file's bytes the same
|
|
54
|
+
* way regardless of kind. Operators / dashboards can use kind for
|
|
55
|
+
* post-restore validation.
|
|
56
|
+
*
|
|
57
|
+
* var bm = b.backupManifest;
|
|
58
|
+
*
|
|
59
|
+
* var manifest = bm.create({
|
|
60
|
+
* vaultKeySalt: "...",
|
|
61
|
+
* vaultKeyEnc: "<base64>",
|
|
62
|
+
* files: [...],
|
|
63
|
+
* metadata: { reason: "scheduled-daily" },
|
|
64
|
+
* });
|
|
65
|
+
* var json = bm.serialize(manifest); // canonical JSON for storage
|
|
66
|
+
*
|
|
67
|
+
* var loaded = bm.parse(json); // validates on parse, throws on error
|
|
68
|
+
* var v = bm.validate(loaded); // → { ok, errors } without throwing
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
var C = require("../constants");
|
|
72
|
+
var safeBuffer = require("../safe-buffer");
|
|
73
|
+
var safeJson = require("../safe-json");
|
|
74
|
+
var { FrameworkError } = require("../framework-error");
|
|
75
|
+
|
|
76
|
+
class BackupManifestError extends FrameworkError {
|
|
77
|
+
constructor(code, message) {
|
|
78
|
+
super(message, code);
|
|
79
|
+
this.name = "BackupManifestError";
|
|
80
|
+
this.permanent = true;
|
|
81
|
+
this.isBackupManifestError = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var FORMAT_VERSION = 1;
|
|
86
|
+
var FRAMEWORK_NAME = "blamejs";
|
|
87
|
+
var VALID_KINDS = { "raw": 1, "vault-sealed": 1, "plaintext": 1 };
|
|
88
|
+
// SHA3-512 produces 64 raw bytes — each byte serializes as 2 hex chars
|
|
89
|
+
// in the manifest, so the hex string is 128 chars long.
|
|
90
|
+
var SHA3_512_HEX_LENGTH = 128;
|
|
91
|
+
var HEX_RE = safeBuffer.HEX_RE;
|
|
92
|
+
var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
93
|
+
|
|
94
|
+
function _isHex(s, evenLength) {
|
|
95
|
+
if (typeof s !== "string" || s.length === 0) return false;
|
|
96
|
+
if (!HEX_RE.test(s)) return false;
|
|
97
|
+
if (evenLength && s.length % 2 !== 0) return false;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
function _isBase64(s) {
|
|
101
|
+
return typeof s === "string" && s.length > 0 && BASE64_RE.test(s);
|
|
102
|
+
}
|
|
103
|
+
function _isIso8601(s) {
|
|
104
|
+
if (typeof s !== "string" || s.length === 0) return false;
|
|
105
|
+
var d = new Date(s);
|
|
106
|
+
return !isNaN(d.getTime()) && d.toISOString() === s;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _validateFileEntry(f, idx, errors) {
|
|
110
|
+
if (!f || typeof f !== "object") {
|
|
111
|
+
errors.push("files[" + idx + "]: must be an object");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (typeof f.relativePath !== "string" || f.relativePath.length === 0) {
|
|
115
|
+
errors.push("files[" + idx + "].relativePath: required non-empty string");
|
|
116
|
+
} else if (f.relativePath.indexOf("..") !== -1 || /^[/\\]/.test(f.relativePath)) {
|
|
117
|
+
// No traversal — when restored, relativePath joins under dataDir
|
|
118
|
+
errors.push("files[" + idx + "].relativePath: must be a relative path without '..' or leading separator");
|
|
119
|
+
}
|
|
120
|
+
if (typeof f.encryptedPath !== "string" || f.encryptedPath.length === 0) {
|
|
121
|
+
errors.push("files[" + idx + "].encryptedPath: required non-empty string");
|
|
122
|
+
} else if (f.encryptedPath.indexOf("..") !== -1 || /^[/\\]/.test(f.encryptedPath)) {
|
|
123
|
+
errors.push("files[" + idx + "].encryptedPath: must be a relative path without '..' or leading separator");
|
|
124
|
+
}
|
|
125
|
+
if (typeof f.size !== "number" || !Number.isInteger(f.size) || f.size < 0) {
|
|
126
|
+
errors.push("files[" + idx + "].size: required non-negative integer");
|
|
127
|
+
}
|
|
128
|
+
if (typeof f.encryptedSize !== "number" || !Number.isInteger(f.encryptedSize) || f.encryptedSize < 0) {
|
|
129
|
+
errors.push("files[" + idx + "].encryptedSize: required non-negative integer");
|
|
130
|
+
}
|
|
131
|
+
if (!_isHex(f.checksum, true) || f.checksum.length !== SHA3_512_HEX_LENGTH) {
|
|
132
|
+
errors.push("files[" + idx + "].checksum: required 128-char hex string (sha3-512)");
|
|
133
|
+
}
|
|
134
|
+
if (!_isHex(f.salt, true)) {
|
|
135
|
+
errors.push("files[" + idx + "].salt: required hex string");
|
|
136
|
+
}
|
|
137
|
+
if (typeof f.kind !== "string" || !VALID_KINDS[f.kind]) {
|
|
138
|
+
errors.push("files[" + idx + "].kind: must be one of raw, vault-sealed, plaintext");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validate(manifest) {
|
|
143
|
+
var errors = [];
|
|
144
|
+
if (!manifest || typeof manifest !== "object") {
|
|
145
|
+
return { ok: false, errors: ["manifest must be an object"] };
|
|
146
|
+
}
|
|
147
|
+
if (manifest.version !== FORMAT_VERSION) {
|
|
148
|
+
errors.push("version: required " + FORMAT_VERSION + ", got " + manifest.version);
|
|
149
|
+
}
|
|
150
|
+
if (manifest.framework !== FRAMEWORK_NAME) {
|
|
151
|
+
errors.push("framework: required '" + FRAMEWORK_NAME + "', got " + JSON.stringify(manifest.framework));
|
|
152
|
+
}
|
|
153
|
+
if (typeof manifest.frameworkVersion !== "string" || manifest.frameworkVersion.length === 0) {
|
|
154
|
+
errors.push("frameworkVersion: required non-empty string");
|
|
155
|
+
}
|
|
156
|
+
if (!_isIso8601(manifest.createdAt)) {
|
|
157
|
+
errors.push("createdAt: required ISO-8601 timestamp string");
|
|
158
|
+
}
|
|
159
|
+
if (!_isHex(manifest.vaultKeySalt, true)) {
|
|
160
|
+
errors.push("vaultKeySalt: required hex string");
|
|
161
|
+
}
|
|
162
|
+
if (!_isBase64(manifest.vaultKeyEnc)) {
|
|
163
|
+
errors.push("vaultKeyEnc: required base64 string");
|
|
164
|
+
}
|
|
165
|
+
if (!Array.isArray(manifest.files)) {
|
|
166
|
+
errors.push("files: required array");
|
|
167
|
+
} else {
|
|
168
|
+
// Detect duplicate relativePath / encryptedPath — would corrupt restore
|
|
169
|
+
var seenRel = Object.create(null);
|
|
170
|
+
var seenEnc = Object.create(null);
|
|
171
|
+
for (var i = 0; i < manifest.files.length; i++) {
|
|
172
|
+
_validateFileEntry(manifest.files[i], i, errors);
|
|
173
|
+
var f = manifest.files[i];
|
|
174
|
+
if (f && typeof f.relativePath === "string") {
|
|
175
|
+
if (seenRel[f.relativePath]) {
|
|
176
|
+
errors.push("files[" + i + "].relativePath: duplicate '" + f.relativePath + "'");
|
|
177
|
+
}
|
|
178
|
+
seenRel[f.relativePath] = true;
|
|
179
|
+
}
|
|
180
|
+
if (f && typeof f.encryptedPath === "string") {
|
|
181
|
+
if (seenEnc[f.encryptedPath]) {
|
|
182
|
+
errors.push("files[" + i + "].encryptedPath: duplicate '" + f.encryptedPath + "'");
|
|
183
|
+
}
|
|
184
|
+
seenEnc[f.encryptedPath] = true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (manifest.metadata !== undefined &&
|
|
189
|
+
(manifest.metadata === null || typeof manifest.metadata !== "object" || Array.isArray(manifest.metadata))) {
|
|
190
|
+
errors.push("metadata: must be a plain object when present");
|
|
191
|
+
}
|
|
192
|
+
return { ok: errors.length === 0, errors: errors };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function create(opts) {
|
|
196
|
+
opts = opts || {};
|
|
197
|
+
var manifest = {
|
|
198
|
+
version: FORMAT_VERSION,
|
|
199
|
+
framework: FRAMEWORK_NAME,
|
|
200
|
+
frameworkVersion: typeof opts.frameworkVersion === "string" && opts.frameworkVersion.length > 0
|
|
201
|
+
? opts.frameworkVersion
|
|
202
|
+
: (C.version || "0.0.0"),
|
|
203
|
+
createdAt: opts.createdAt || new Date().toISOString(),
|
|
204
|
+
vaultKeySalt: opts.vaultKeySalt,
|
|
205
|
+
vaultKeyEnc: opts.vaultKeyEnc,
|
|
206
|
+
files: Array.isArray(opts.files) ? opts.files.slice() : [],
|
|
207
|
+
};
|
|
208
|
+
if (opts.metadata && typeof opts.metadata === "object" && !Array.isArray(opts.metadata)) {
|
|
209
|
+
manifest.metadata = Object.assign({}, opts.metadata);
|
|
210
|
+
}
|
|
211
|
+
var v = validate(manifest);
|
|
212
|
+
if (!v.ok) {
|
|
213
|
+
throw new BackupManifestError("backup-manifest/invalid",
|
|
214
|
+
"create: " + v.errors.join("; "));
|
|
215
|
+
}
|
|
216
|
+
return manifest;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function serialize(manifest) {
|
|
220
|
+
var v = validate(manifest);
|
|
221
|
+
if (!v.ok) {
|
|
222
|
+
throw new BackupManifestError("backup-manifest/invalid",
|
|
223
|
+
"serialize: " + v.errors.join("; "));
|
|
224
|
+
}
|
|
225
|
+
// Stable key ordering so the same manifest object always serializes
|
|
226
|
+
// to the same bytes (operators can hash the manifest as part of
|
|
227
|
+
// bundle-integrity logging without surprises across runs).
|
|
228
|
+
var canonical = {
|
|
229
|
+
version: manifest.version,
|
|
230
|
+
framework: manifest.framework,
|
|
231
|
+
frameworkVersion: manifest.frameworkVersion,
|
|
232
|
+
createdAt: manifest.createdAt,
|
|
233
|
+
vaultKeySalt: manifest.vaultKeySalt,
|
|
234
|
+
vaultKeyEnc: manifest.vaultKeyEnc,
|
|
235
|
+
files: manifest.files.map(function (f) {
|
|
236
|
+
return {
|
|
237
|
+
relativePath: f.relativePath,
|
|
238
|
+
encryptedPath: f.encryptedPath,
|
|
239
|
+
size: f.size,
|
|
240
|
+
encryptedSize: f.encryptedSize,
|
|
241
|
+
checksum: f.checksum,
|
|
242
|
+
salt: f.salt,
|
|
243
|
+
kind: f.kind,
|
|
244
|
+
};
|
|
245
|
+
}),
|
|
246
|
+
};
|
|
247
|
+
if (manifest.metadata) canonical.metadata = manifest.metadata;
|
|
248
|
+
return JSON.stringify(canonical, null, 2) + "\n";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parse(jsonStr) {
|
|
252
|
+
if (typeof jsonStr !== "string" && !Buffer.isBuffer(jsonStr)) {
|
|
253
|
+
throw new BackupManifestError("backup-manifest/bad-input",
|
|
254
|
+
"parse: argument must be a string or Buffer");
|
|
255
|
+
}
|
|
256
|
+
var s = Buffer.isBuffer(jsonStr) ? jsonStr.toString("utf8") : jsonStr;
|
|
257
|
+
var obj;
|
|
258
|
+
// Backup manifests are file-bounded operator-supplied input. 16 MiB
|
|
259
|
+
// is generous; real manifests are kilobytes.
|
|
260
|
+
try { obj = safeJson.parse(s, { maxBytes: C.BYTES.mib(16) }); }
|
|
261
|
+
catch (e) {
|
|
262
|
+
throw new BackupManifestError("backup-manifest/bad-json",
|
|
263
|
+
"parse: not valid JSON: " + ((e && e.message) || String(e)));
|
|
264
|
+
}
|
|
265
|
+
var v = validate(obj);
|
|
266
|
+
if (!v.ok) {
|
|
267
|
+
throw new BackupManifestError("backup-manifest/invalid",
|
|
268
|
+
"parse: " + v.errors.join("; "));
|
|
269
|
+
}
|
|
270
|
+
return obj;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = {
|
|
274
|
+
create: create,
|
|
275
|
+
validate: validate,
|
|
276
|
+
serialize: serialize,
|
|
277
|
+
parse: parse,
|
|
278
|
+
FORMAT_VERSION: FORMAT_VERSION,
|
|
279
|
+
FRAMEWORK_NAME: FRAMEWORK_NAME,
|
|
280
|
+
VALID_KINDS: VALID_KINDS,
|
|
281
|
+
BackupManifestError: BackupManifestError,
|
|
282
|
+
};
|