@blamejs/core 0.14.11 → 0.14.13
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 +4 -0
- package/README.md +2 -2
- package/lib/agent-idempotency.js +113 -0
- package/lib/agent-orchestrator.js +108 -0
- package/lib/agent-snapshot.js +137 -0
- package/lib/agent-tenant.js +193 -17
- package/lib/archive-wrap.js +234 -1
- package/lib/archive.js +1 -0
- package/lib/auth/oid4vp.js +47 -28
- package/lib/cluster.js +186 -14
- package/lib/crypto-field.js +5 -0
- package/lib/db.js +15 -0
- package/lib/mail-srs.js +122 -19
- package/lib/safe-archive.js +196 -136
- package/lib/validate-opts.js +24 -0
- package/lib/vault/rotate.js +175 -15
- package/lib/vault-aad.js +84 -33
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/safe-archive.js
CHANGED
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
* Format auto-detection sniffs the first ~512 bytes for magic
|
|
25
25
|
* signatures: ZIP (LFH magic `0x04034b50` + EOCD magic `0x06054b50`),
|
|
26
26
|
* tar (`ustar` at offset 257), gzip / tar.gz (RFC 1952 magic), and
|
|
27
|
-
* `b.
|
|
28
|
-
* format detection). Unrecognized
|
|
29
|
-
* `safe-archive/format-unsupported`.
|
|
27
|
+
* `b.archive.wrap` recipient (`BAWRP`) / passphrase (`BAWPP`)
|
|
28
|
+
* envelopes (auto-unwrapped before format detection). Unrecognized
|
|
29
|
+
* inputs are flagged `safe-archive/format-unsupported`.
|
|
30
30
|
*
|
|
31
31
|
* The orchestrator refuses the WHOLE archive on any single critical
|
|
32
32
|
* guard issue — no partial extraction. Cleanup is `fs.rm`-recursive
|
|
@@ -58,13 +58,10 @@ var MAGIC_ZIP_LFH = 0x04034b50;
|
|
|
58
58
|
var MAGIC_ZIP_EOCD = 0x06054b50;
|
|
59
59
|
// GZIP magic per RFC 1952 §2.3.1.
|
|
60
60
|
var MAGIC_GZIP_BE = 0x1f8b;
|
|
61
|
-
// b.crypto.encryptPacked envelope magic — the prefix the framework's
|
|
62
|
-
// PQ envelope writes. (Sentinel value for v0.12.10+ Flavor 1 unwrap.)
|
|
63
|
-
var MAGIC_ENCPACKED = "EPACK";
|
|
64
61
|
|
|
65
62
|
async function _sniffMagic(adapter) {
|
|
66
63
|
// For random-access adapters, the format sniffer reads the first
|
|
67
|
-
// 512 bytes — enough for ZIP + GZIP +
|
|
64
|
+
// 512 bytes — enough for ZIP + GZIP + wrap-envelope magic
|
|
68
65
|
// detection. tar magic lives at offset 257 inside the first 512-
|
|
69
66
|
// byte header block, so we need at least 263 bytes; 512 covers it.
|
|
70
67
|
if (adapter.kind !== "random-access") {
|
|
@@ -91,13 +88,13 @@ async function _sniffMagic(adapter) {
|
|
|
91
88
|
var be2 = head.readUInt16BE(0);
|
|
92
89
|
if (be2 === MAGIC_GZIP_BE) return { format: "gzip" };
|
|
93
90
|
}
|
|
94
|
-
//
|
|
91
|
+
// archive-wrap envelopes — 5-byte ASCII prefix. BAWRP (recipient) and
|
|
92
|
+
// BAWPP (passphrase) are the only wrap envelopes the framework produces
|
|
93
|
+
// (b.archive.wrap / b.archive.wrapWithPassphrase) and the only ones
|
|
94
|
+
// b.archive.sniffEnvelope recognizes.
|
|
95
95
|
if (head.length >= 5) {
|
|
96
96
|
var prefix = head.slice(0, 5).toString("utf8");
|
|
97
|
-
if (prefix === MAGIC_ENCPACKED) return { format: "encryptPacked" };
|
|
98
|
-
// v0.12.15 — archive-wrap recipient envelope (v0.12.10 / BAWRP).
|
|
99
97
|
if (prefix === "BAWRP") return { format: "wrap-recipient" };
|
|
100
|
-
// v0.12.15 — archive-wrap passphrase envelope (v0.12.11 / BAWPP).
|
|
101
98
|
if (prefix === "BAWPP") return { format: "wrap-passphrase" };
|
|
102
99
|
}
|
|
103
100
|
// tar — "ustar" at offset 257 within the first 512-byte header.
|
|
@@ -124,6 +121,82 @@ async function _collectSourceBytes(source) {
|
|
|
124
121
|
return source.range(0, size);
|
|
125
122
|
}
|
|
126
123
|
|
|
124
|
+
// Shared source→adapter resolution + envelope auto-unwrap for the three
|
|
125
|
+
// orchestrator entry points (extract / extractToMemory / inspect). Returns
|
|
126
|
+
// { source, format } — `source` is a random-access adapter positioned at the
|
|
127
|
+
// (possibly unwrapped) archive and `format` is the sniffed inner format. The
|
|
128
|
+
// CALLER owns closing the returned source in its own `finally`; this helper
|
|
129
|
+
// performs the pre-unwrap fd-close-before-replace + the signal-forward-to-
|
|
130
|
+
// inner-adapter discipline internally, and closes a string-opened descriptor
|
|
131
|
+
// if it throws mid-resolve so a sniff/unwrap failure can't leak it.
|
|
132
|
+
async function _resolveAndUnwrap(opts, label, refuseTrustedStream) {
|
|
133
|
+
var openedFromString = typeof opts.source === "string";
|
|
134
|
+
var source = opts.source;
|
|
135
|
+
if (openedFromString) {
|
|
136
|
+
source = archiveAdapters().fs(source, { signal: opts.signal });
|
|
137
|
+
} else if (Buffer.isBuffer(source)) {
|
|
138
|
+
source = archiveAdapters().buffer(source, { signal: opts.signal });
|
|
139
|
+
} else if (refuseTrustedStream && archiveAdapters().isTrustedStreamAdapter(source)) {
|
|
140
|
+
// Trusted-stream adapters satisfy the adapter contract, but the
|
|
141
|
+
// orchestrator's adversarial-safe central-directory walk + LFH/CD skew
|
|
142
|
+
// defense needs random access. Refuse upfront with a typed error so the
|
|
143
|
+
// operator sees the constraint at the entry point rather than a
|
|
144
|
+
// downstream `archive-read/wrong-entry-point`.
|
|
145
|
+
throw new SafeArchiveError("safe-archive/trusted-stream-unsupported",
|
|
146
|
+
label + ": trusted-stream adapter sources are not supported by the orchestrator " +
|
|
147
|
+
"(the adversarial-safe central-directory walk requires random access). Collect the " +
|
|
148
|
+
"bytes into a buffer adapter — `b.archive.adapters.buffer(await collect(readable))` — " +
|
|
149
|
+
"and pass that, or read with `b.archive.read.zip.fromTrustedStream` directly.");
|
|
150
|
+
} else if (!archiveAdapters().isRandomAccessAdapter(source)) {
|
|
151
|
+
throw new SafeArchiveError("safe-archive/bad-source",
|
|
152
|
+
label + ": opts.source must be a string path, Buffer, or b.archive.adapters.* result");
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
var format = opts.format || "auto";
|
|
156
|
+
if (format === "auto") {
|
|
157
|
+
format = (await _sniffMagic(source)).format;
|
|
158
|
+
}
|
|
159
|
+
// Auto-unwrap path: when the sniffer identifies a wrap envelope, unwrap
|
|
160
|
+
// inline + re-sniff the inner bytes so operators get a single call
|
|
161
|
+
// regardless of envelope shape. Operator supplies opts.recipient or
|
|
162
|
+
// opts.passphrase matching the envelope kind.
|
|
163
|
+
if (format === "wrap-recipient" || format === "wrap-passphrase") {
|
|
164
|
+
var sealedBytes = await _collectSourceBytes(source);
|
|
165
|
+
var inner;
|
|
166
|
+
if (format === "wrap-recipient") {
|
|
167
|
+
if (!opts.recipient) {
|
|
168
|
+
throw new SafeArchiveError("safe-archive/no-recipient-for-wrap",
|
|
169
|
+
label + ": source is a wrap-recipient envelope (BAWRP) but opts.recipient was not supplied. " +
|
|
170
|
+
"Pass `{ recipient: { privateKey, ecPrivateKey } }` (or peer-cert form) to unwrap inline.");
|
|
171
|
+
}
|
|
172
|
+
inner = archiveWrap().unwrap(sealedBytes, { recipient: opts.recipient });
|
|
173
|
+
} else {
|
|
174
|
+
if (typeof opts.passphrase !== "string" && !Buffer.isBuffer(opts.passphrase)) {
|
|
175
|
+
throw new SafeArchiveError("safe-archive/no-passphrase-for-wrap",
|
|
176
|
+
label + ": source is a wrap-passphrase envelope (BAWPP) but opts.passphrase was not supplied. " +
|
|
177
|
+
"Pass `{ passphrase: <string|Buffer> }` to unwrap inline.");
|
|
178
|
+
}
|
|
179
|
+
inner = await archiveWrap().unwrapWithPassphrase(sealedBytes, { passphrase: opts.passphrase });
|
|
180
|
+
}
|
|
181
|
+
// Close the original string-opened descriptor BEFORE replacing the
|
|
182
|
+
// source reference (overwriting it would leak the fd across repeated
|
|
183
|
+
// calls → EMFILE under load), then forward opts.signal to the inner
|
|
184
|
+
// buffer adapter so abort propagation survives the unwrap boundary.
|
|
185
|
+
if (typeof source.close === "function" && openedFromString) {
|
|
186
|
+
try { source.close(); } catch (_e) { /* drop-silent */ }
|
|
187
|
+
}
|
|
188
|
+
source = archiveAdapters().buffer(inner, { signal: opts.signal });
|
|
189
|
+
format = (await _sniffMagic(source)).format;
|
|
190
|
+
}
|
|
191
|
+
return { source: source, format: format };
|
|
192
|
+
} catch (e) {
|
|
193
|
+
if (typeof source.close === "function" && openedFromString) {
|
|
194
|
+
try { source.close(); } catch (_e2) { /* drop-silent */ }
|
|
195
|
+
}
|
|
196
|
+
throw e;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
127
200
|
// ---- Public extract orchestrator ----------------------------------------
|
|
128
201
|
|
|
129
202
|
/**
|
|
@@ -168,84 +241,10 @@ async function extract(opts) {
|
|
|
168
241
|
opts = opts || {};
|
|
169
242
|
validateOpts.requireNonEmptyString(opts.destination,
|
|
170
243
|
"b.safeArchive.extract: opts.destination", SafeArchiveError, "safe-archive/no-destination");
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
var source = opts.source;
|
|
175
|
-
if (typeof source === "string") {
|
|
176
|
-
source = archiveAdapters().fs(source, { signal: opts.signal });
|
|
177
|
-
} else if (Buffer.isBuffer(source)) {
|
|
178
|
-
source = archiveAdapters().buffer(source, { signal: opts.signal });
|
|
179
|
-
} else if (archiveAdapters().isTrustedStreamAdapter(source)) {
|
|
180
|
-
// Trusted-stream adapters are accepted by the contract but the
|
|
181
|
-
// orchestrator's extract path needs random-access (CD-walk +
|
|
182
|
-
// LFH/CD skew defense). Refuse upfront with a typed safe-archive
|
|
183
|
-
// error so the operator sees the constraint at the entry point
|
|
184
|
-
// rather than an `archive-read/wrong-entry-point` thrown by the
|
|
185
|
-
// downstream reader. Trusted-stream extract via
|
|
186
|
-
// `b.archive.read.zip.fromTrustedStream` is deferred to v0.12.8
|
|
187
|
-
// alongside the tar reader's sequential mode.
|
|
188
|
-
throw new SafeArchiveError("safe-archive/trusted-stream-unsupported",
|
|
189
|
-
"extract: trusted-stream adapter sources are not supported by the orchestrator " +
|
|
190
|
-
"(the adversarial-safe CD-walk requires random-access). Collect the bytes via " +
|
|
191
|
-
"`b.archive.adapters.buffer(await collect(readable))` and pass that, or use " +
|
|
192
|
-
"`b.archive.read.zip.fromTrustedStream` directly when the v0.12.8 sequential " +
|
|
193
|
-
"extract path lands");
|
|
194
|
-
} else if (!archiveAdapters().isRandomAccessAdapter(source)) {
|
|
195
|
-
throw new SafeArchiveError("safe-archive/bad-source",
|
|
196
|
-
"extract: opts.source must be a string path, Buffer, or b.archive.adapters.* result");
|
|
197
|
-
}
|
|
198
|
-
|
|
244
|
+
var resolved = await _resolveAndUnwrap(opts, "extract", true);
|
|
245
|
+
var source = resolved.source;
|
|
246
|
+
var format = resolved.format;
|
|
199
247
|
try {
|
|
200
|
-
var format = opts.format || "auto";
|
|
201
|
-
if (format === "auto") {
|
|
202
|
-
var sniff = await _sniffMagic(source);
|
|
203
|
-
format = sniff.format;
|
|
204
|
-
}
|
|
205
|
-
// v0.12.15 — auto-unwrap path. When the sniffer identifies a
|
|
206
|
-
// wrap envelope, unwrap inline + re-sniff the inner bytes so
|
|
207
|
-
// operators get a single extract() call regardless of envelope
|
|
208
|
-
// shape. Operator must supply opts.recipient or opts.passphrase
|
|
209
|
-
// matching the envelope kind.
|
|
210
|
-
if (format === "wrap-recipient" || format === "wrap-passphrase") {
|
|
211
|
-
var sealedBytes = await _collectSourceBytes(source);
|
|
212
|
-
var inner;
|
|
213
|
-
if (format === "wrap-recipient") {
|
|
214
|
-
if (!opts.recipient) {
|
|
215
|
-
throw new SafeArchiveError("safe-archive/no-recipient-for-wrap",
|
|
216
|
-
"extract: source is a wrap-recipient envelope (BAWRP) but opts.recipient was not supplied. " +
|
|
217
|
-
"Pass `{ recipient: { privateKey, ecPrivateKey } }` (or peer-cert form) to unwrap inline.");
|
|
218
|
-
}
|
|
219
|
-
inner = archiveWrap().unwrap(sealedBytes, { recipient: opts.recipient });
|
|
220
|
-
} else {
|
|
221
|
-
if (typeof opts.passphrase !== "string" && !Buffer.isBuffer(opts.passphrase)) {
|
|
222
|
-
throw new SafeArchiveError("safe-archive/no-passphrase-for-wrap",
|
|
223
|
-
"extract: source is a wrap-passphrase envelope (BAWPP) but opts.passphrase was not supplied. " +
|
|
224
|
-
"Pass `{ passphrase: <string|Buffer> }` to unwrap inline.");
|
|
225
|
-
}
|
|
226
|
-
inner = await archiveWrap().unwrapWithPassphrase(sealedBytes, { passphrase: opts.passphrase });
|
|
227
|
-
}
|
|
228
|
-
// Close the original source
|
|
229
|
-
// adapter BEFORE replacing it. When opts.source was a string
|
|
230
|
-
// path, the fs adapter opened a file descriptor; overwriting
|
|
231
|
-
// `source` loses the close reference and the descriptor
|
|
232
|
-
// leaks across repeated extract() calls (eventually EMFILE
|
|
233
|
-
// under load). The outer finally still closes whatever
|
|
234
|
-
// `source` points at, but the original handle needs explicit
|
|
235
|
-
// release here.
|
|
236
|
-
if (typeof source.close === "function" && typeof opts.source === "string") {
|
|
237
|
-
try { source.close(); } catch (_e) { /* drop-silent */ }
|
|
238
|
-
}
|
|
239
|
-
// Forward opts.signal to the
|
|
240
|
-
// inner buffer adapter so abort propagation stays intact
|
|
241
|
-
// across the unwrap boundary. Without it, an abort raised
|
|
242
|
-
// after unwrapping would no longer cancel inner range()
|
|
243
|
-
// calls, breaking the documented signal contract for
|
|
244
|
-
// large wrapped archives.
|
|
245
|
-
source = archiveAdapters().buffer(inner, { signal: opts.signal });
|
|
246
|
-
var innerSniff = await _sniffMagic(source);
|
|
247
|
-
format = innerSniff.format;
|
|
248
|
-
}
|
|
249
248
|
var reader;
|
|
250
249
|
if (format === "zip") {
|
|
251
250
|
reader = archiveRead().zip(source, {
|
|
@@ -277,7 +276,7 @@ async function extract(opts) {
|
|
|
277
276
|
} else {
|
|
278
277
|
throw new SafeArchiveError("safe-archive/format-unsupported",
|
|
279
278
|
"extract: format=" + JSON.stringify(format) + " — supported formats are " +
|
|
280
|
-
"zip, tar, tar.gz; b.
|
|
279
|
+
"zip, tar, tar.gz; b.archive.wrap recipient/passphrase envelopes are auto-unwrapped first");
|
|
281
280
|
}
|
|
282
281
|
var result = await reader.extract({
|
|
283
282
|
destination: opts.destination,
|
|
@@ -291,6 +290,109 @@ async function extract(opts) {
|
|
|
291
290
|
}
|
|
292
291
|
}
|
|
293
292
|
|
|
293
|
+
/**
|
|
294
|
+
* @primitive b.safeArchive.extractToMemory
|
|
295
|
+
* @signature b.safeArchive.extractToMemory(opts)
|
|
296
|
+
* @since 0.14.13
|
|
297
|
+
* @status stable
|
|
298
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
299
|
+
* @related b.safeArchive.extract, b.archive.read.zip, b.archive.read.tar
|
|
300
|
+
*
|
|
301
|
+
* In-memory counterpart to `b.safeArchive.extract` for read-only /
|
|
302
|
+
* serverless filesystems. Resolves the source, sniffs the format,
|
|
303
|
+
* auto-unwraps recipient (`BAWRP`) / passphrase (`BAWPP`) envelopes, and
|
|
304
|
+
* dispatches to the zip / tar / tar.gz reader's in-memory `extractEntries`
|
|
305
|
+
* — an async generator that yields each regular file entry's decompressed
|
|
306
|
+
* bytes without ever writing to disk. Takes no `destination`; the caller
|
|
307
|
+
* owns where, if anywhere, the bytes land.
|
|
308
|
+
*
|
|
309
|
+
* Every defense the disk `extract` runs applies unchanged: the zip-bomb
|
|
310
|
+
* caps (entry-count / per-entry / total / expansion-ratio), the
|
|
311
|
+
* `b.guardArchive` metadata cascade (Zip-Slip / path-traversal / symlink-
|
|
312
|
+
* escape / encrypted-entry refusal — CVE-2025-3445 class), and the
|
|
313
|
+
* entry-type policy. Directory entries carry no bytes and are skipped. The
|
|
314
|
+
* disk-only realpath-agreement check (CVE-2025-4517 PATH_MAX TOCTOU
|
|
315
|
+
* defense) is intentionally absent — there is no extraction root — so the
|
|
316
|
+
* archive-level name refusals carry the containment guarantee here.
|
|
317
|
+
*
|
|
318
|
+
* Trusted-stream adapter sources are refused upfront: the adversarial-safe
|
|
319
|
+
* central-directory walk requires random access. Collect the bytes into a
|
|
320
|
+
* buffer adapter, or read with `b.archive.read.zip.fromTrustedStream`
|
|
321
|
+
* directly.
|
|
322
|
+
*
|
|
323
|
+
* @opts
|
|
324
|
+
* source: b.archive.adapters.* | Buffer | string,
|
|
325
|
+
* format: "auto" | "zip" | "tar" | "tar.gz",
|
|
326
|
+
* bombPolicy: b.guardArchive.zipBombPolicy(...) | { ... },
|
|
327
|
+
* entryTypePolicy: b.guardArchive.entryTypePolicy(...) | { ... },
|
|
328
|
+
* guardProfile: "strict" | "balanced" | "permissive" | "hipaa" | ...,
|
|
329
|
+
* recipient: { privateKey, ecPrivateKey }, // for BAWRP envelopes
|
|
330
|
+
* passphrase: string | Buffer, // for BAWPP envelopes
|
|
331
|
+
* audit: b.audit,
|
|
332
|
+
* signal: AbortSignal,
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* for await (var entry of b.safeArchive.extractToMemory({
|
|
336
|
+
* source: b.archive.adapters.fs("/var/uploads/payload.zip"),
|
|
337
|
+
* guardProfile: "strict",
|
|
338
|
+
* })) {
|
|
339
|
+
* // entry → { name, bytes, size } — never touches disk
|
|
340
|
+
* await store.put(entry.name, entry.bytes);
|
|
341
|
+
* }
|
|
342
|
+
*/
|
|
343
|
+
async function* extractToMemory(opts) {
|
|
344
|
+
opts = opts || {};
|
|
345
|
+
var resolved = await _resolveAndUnwrap(opts, "extractToMemory", true);
|
|
346
|
+
var source = resolved.source;
|
|
347
|
+
var format = resolved.format;
|
|
348
|
+
var extractOpts = { allowDangerous: opts.allowDangerous, allowEncrypted: opts.allowEncrypted };
|
|
349
|
+
try {
|
|
350
|
+
if (format === "zip") {
|
|
351
|
+
var zr = archiveRead().zip(source, {
|
|
352
|
+
bombPolicy: opts.bombPolicy,
|
|
353
|
+
entryTypePolicy: opts.entryTypePolicy,
|
|
354
|
+
guardProfile: opts.guardProfile,
|
|
355
|
+
audit: opts.audit,
|
|
356
|
+
});
|
|
357
|
+
for await (var ze of zr.extractEntries(extractOpts)) { yield ze; }
|
|
358
|
+
} else if (format === "tar") {
|
|
359
|
+
var tr = archiveTarRead().tar(source, {
|
|
360
|
+
bombPolicy: opts.bombPolicy,
|
|
361
|
+
entryTypePolicy: opts.entryTypePolicy,
|
|
362
|
+
guardProfile: opts.guardProfile,
|
|
363
|
+
audit: opts.audit,
|
|
364
|
+
});
|
|
365
|
+
for await (var te of tr.extractEntries(extractOpts)) { yield te; }
|
|
366
|
+
} else if (format === "tar.gz") {
|
|
367
|
+
// The gz reader's asTar() shim exposes inspect + extract but NOT
|
|
368
|
+
// extractEntries, so materialize the gz layer to a Buffer (the gz
|
|
369
|
+
// bomb caps still run during toBuffer()) and walk a fresh tar reader
|
|
370
|
+
// over it — the tar bomb / guard / entry-type caps run on the inner
|
|
371
|
+
// walk, so no defense is dropped.
|
|
372
|
+
var tarBytes = await archiveGz().read.gz(source, {
|
|
373
|
+
maxDecompressedBytes: opts.maxDecompressedBytes,
|
|
374
|
+
maxExpansionRatio: opts.maxExpansionRatio,
|
|
375
|
+
audit: opts.audit,
|
|
376
|
+
}).toBuffer();
|
|
377
|
+
var gtr = archiveTarRead().tar(archiveAdapters().buffer(tarBytes, { signal: opts.signal }), {
|
|
378
|
+
bombPolicy: opts.bombPolicy,
|
|
379
|
+
entryTypePolicy: opts.entryTypePolicy,
|
|
380
|
+
guardProfile: opts.guardProfile,
|
|
381
|
+
audit: opts.audit,
|
|
382
|
+
});
|
|
383
|
+
for await (var ge of gtr.extractEntries(extractOpts)) { yield ge; }
|
|
384
|
+
} else {
|
|
385
|
+
throw new SafeArchiveError("safe-archive/format-unsupported",
|
|
386
|
+
"extractToMemory: format=" + JSON.stringify(format) + " — supported formats are " +
|
|
387
|
+
"zip, tar, tar.gz; b.archive.wrap recipient/passphrase envelopes are auto-unwrapped first");
|
|
388
|
+
}
|
|
389
|
+
} finally {
|
|
390
|
+
if (typeof source.close === "function" && typeof opts.source === "string") {
|
|
391
|
+
try { source.close(); } catch (_e) { /* drop-silent */ }
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
294
396
|
/**
|
|
295
397
|
* @primitive b.safeArchive.inspect
|
|
296
398
|
* @signature b.safeArchive.inspect(opts)
|
|
@@ -316,53 +418,10 @@ async function extract(opts) {
|
|
|
316
418
|
*/
|
|
317
419
|
async function inspect(opts) {
|
|
318
420
|
opts = opts || {};
|
|
319
|
-
var
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
} else if (Buffer.isBuffer(source)) {
|
|
323
|
-
source = archiveAdapters().buffer(source, { signal: opts.signal });
|
|
324
|
-
} else if (!archiveAdapters().isRandomAccessAdapter(source)) {
|
|
325
|
-
throw new SafeArchiveError("safe-archive/bad-source",
|
|
326
|
-
"inspect: opts.source must be a string path, Buffer, or random-access adapter");
|
|
327
|
-
}
|
|
421
|
+
var resolved = await _resolveAndUnwrap(opts, "inspect", false);
|
|
422
|
+
var source = resolved.source;
|
|
423
|
+
var format = resolved.format;
|
|
328
424
|
try {
|
|
329
|
-
var format = opts.format || "auto";
|
|
330
|
-
if (format === "auto") {
|
|
331
|
-
var sniff = await _sniffMagic(source);
|
|
332
|
-
format = sniff.format;
|
|
333
|
-
}
|
|
334
|
-
// v0.12.16 — auto-unwrap path for inspect, parallel to the
|
|
335
|
-
// v0.12.15 extract path. Wrap envelopes (BAWRP / BAWPP) are
|
|
336
|
-
// unwrapped inline + re-sniffed so operators can enumerate
|
|
337
|
-
// entries of a sealed archive in a single inspect() call.
|
|
338
|
-
if (format === "wrap-recipient" || format === "wrap-passphrase") {
|
|
339
|
-
var sealedBytes = await _collectSourceBytes(source);
|
|
340
|
-
var inner;
|
|
341
|
-
if (format === "wrap-recipient") {
|
|
342
|
-
if (!opts.recipient) {
|
|
343
|
-
throw new SafeArchiveError("safe-archive/no-recipient-for-wrap",
|
|
344
|
-
"inspect: source is a wrap-recipient envelope (BAWRP) but opts.recipient was not supplied. " +
|
|
345
|
-
"Pass `{ recipient: { privateKey, ecPrivateKey } }` (or peer-cert form) to unwrap inline.");
|
|
346
|
-
}
|
|
347
|
-
inner = archiveWrap().unwrap(sealedBytes, { recipient: opts.recipient });
|
|
348
|
-
} else {
|
|
349
|
-
if (typeof opts.passphrase !== "string" && !Buffer.isBuffer(opts.passphrase)) {
|
|
350
|
-
throw new SafeArchiveError("safe-archive/no-passphrase-for-wrap",
|
|
351
|
-
"inspect: source is a wrap-passphrase envelope (BAWPP) but opts.passphrase was not supplied. " +
|
|
352
|
-
"Pass `{ passphrase: <string|Buffer> }` to unwrap inline.");
|
|
353
|
-
}
|
|
354
|
-
inner = await archiveWrap().unwrapWithPassphrase(sealedBytes, { passphrase: opts.passphrase });
|
|
355
|
-
}
|
|
356
|
-
// v0.12.15 P1 — close the original fs adapter (if string-
|
|
357
|
-
// backed) BEFORE replacing the source reference. v0.12.15 P2
|
|
358
|
-
// — forward opts.signal to the inner buffer adapter.
|
|
359
|
-
if (typeof source.close === "function" && typeof opts.source === "string") {
|
|
360
|
-
try { source.close(); } catch (_e) { /* drop-silent */ }
|
|
361
|
-
}
|
|
362
|
-
source = archiveAdapters().buffer(inner, { signal: opts.signal });
|
|
363
|
-
var innerSniff = await _sniffMagic(source);
|
|
364
|
-
format = innerSniff.format;
|
|
365
|
-
}
|
|
366
425
|
var reader;
|
|
367
426
|
if (format === "zip") {
|
|
368
427
|
reader = archiveRead().zip(source, {
|
|
@@ -388,7 +447,7 @@ async function inspect(opts) {
|
|
|
388
447
|
});
|
|
389
448
|
} else {
|
|
390
449
|
throw new SafeArchiveError("safe-archive/format-unsupported",
|
|
391
|
-
"inspect: format=" + JSON.stringify(format) + " —
|
|
450
|
+
"inspect: format=" + JSON.stringify(format) + " — supported formats are zip, tar, tar.gz; wrap envelopes are auto-unwrapped first");
|
|
392
451
|
}
|
|
393
452
|
var entries = await reader.inspect();
|
|
394
453
|
var totalCompressed = 0;
|
|
@@ -412,6 +471,7 @@ async function inspect(opts) {
|
|
|
412
471
|
|
|
413
472
|
module.exports = {
|
|
414
473
|
extract: extract,
|
|
474
|
+
extractToMemory: extractToMemory,
|
|
415
475
|
inspect: inspect,
|
|
416
476
|
SafeArchiveError: SafeArchiveError,
|
|
417
477
|
// Exposed for tests + sibling modules.
|
package/lib/validate-opts.js
CHANGED
|
@@ -190,6 +190,29 @@ function requireObject(opts, callerLabel, errorClass, code) {
|
|
|
190
190
|
return opts;
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// requireMethods — validate an injected dependency exposes the named
|
|
194
|
+
// methods. Collapses the repeated `if (!obj || typeof obj.fn !==
|
|
195
|
+
// "function" || ...) throw` injected-store / exporter / backend guards
|
|
196
|
+
// (b.agent.*.reseal stores, b.dsr / b.outbox create() backends, etc.)
|
|
197
|
+
// into one definition. Throws on null / non-object / any missing-or-
|
|
198
|
+
// non-function method; returns obj on success.
|
|
199
|
+
function requireMethods(obj, methods, callerLabel, errorClass, code) {
|
|
200
|
+
var label = callerLabel || "dependency";
|
|
201
|
+
if (!obj || typeof obj !== "object") {
|
|
202
|
+
_throw(errorClass, code, label + " must be an object exposing { " +
|
|
203
|
+
methods.join(", ") + " }, got " + (obj === null ? "null" : typeof obj),
|
|
204
|
+
"validate-opts/bad-methods-object");
|
|
205
|
+
}
|
|
206
|
+
for (var i = 0; i < methods.length; i += 1) {
|
|
207
|
+
if (typeof obj[methods[i]] !== "function") {
|
|
208
|
+
_throw(errorClass, code, label + " must expose a " + methods[i] +
|
|
209
|
+
"() method (requires { " + methods.join(", ") + " })",
|
|
210
|
+
"validate-opts/missing-method");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return obj;
|
|
214
|
+
}
|
|
215
|
+
|
|
193
216
|
function optionalNonEmptyString(value, label, errorClass, code) {
|
|
194
217
|
if (value === undefined || value === null) return value;
|
|
195
218
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -376,6 +399,7 @@ module.exports.optionalPlainObject = optionalPlainObject;
|
|
|
376
399
|
module.exports.requireNonEmptyString = requireNonEmptyString;
|
|
377
400
|
module.exports.observabilityShape = observabilityShape;
|
|
378
401
|
module.exports.requireObject = requireObject;
|
|
402
|
+
module.exports.requireMethods = requireMethods;
|
|
379
403
|
module.exports.applyDefaults = applyDefaults;
|
|
380
404
|
module.exports.makeAuditEmitter = makeAuditEmitter;
|
|
381
405
|
module.exports.makeNamespacedEmitters = makeNamespacedEmitters;
|