@blamejs/blamejs-shop 0.0.83 → 0.0.85
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/lib/email-campaigns.js +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +4 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/SECURITY.md +1 -0
- package/lib/vendor/blamejs/api-snapshot.json +151 -2
- package/lib/vendor/blamejs/fuzz/safe-archive.fuzz.js +37 -0
- package/lib/vendor/blamejs/index.js +15 -1
- package/lib/vendor/blamejs/lib/archive-adapters.js +629 -0
- package/lib/vendor/blamejs/lib/archive-read.js +781 -0
- package/lib/vendor/blamejs/lib/archive-tar-read.js +418 -0
- package/lib/vendor/blamejs/lib/archive-tar.js +557 -0
- package/lib/vendor/blamejs/lib/archive.js +17 -0
- package/lib/vendor/blamejs/lib/audit.js +22 -7
- package/lib/vendor/blamejs/lib/backup/index.js +429 -0
- package/lib/vendor/blamejs/lib/guard-archive.js +180 -0
- package/lib/vendor/blamejs/lib/guard-filename.js +205 -0
- package/lib/vendor/blamejs/lib/safe-archive.js +295 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.7.json +86 -0
- package/lib/vendor/blamejs/release-notes/v0.12.8.json +81 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +247 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-tar.test.js +228 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +127 -0
- package/package.json +2 -2
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.archive.adapters
|
|
4
|
+
* @nav Tools
|
|
5
|
+
* @title Archive Adapters
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Source-bytes adapter contract for the `b.archive.read` family.
|
|
9
|
+
* Unifies how bytes flow into the reader regardless of where they
|
|
10
|
+
* live — a local file, an object-store bucket, an HTTP endpoint with
|
|
11
|
+
* Range support, an in-memory Buffer, or a trusted Readable.
|
|
12
|
+
*
|
|
13
|
+
* Two contract shapes, picked by the caller's use case:
|
|
14
|
+
*
|
|
15
|
+
* - **Random-access** — `{ size, range(offset, length) → Buffer }`
|
|
16
|
+
* Required for the read primitive's CD-walk path (the canonical
|
|
17
|
+
* adversarial-safe ZIP read). The reader fetches the EOCD
|
|
18
|
+
* trailer first (last ~64 KiB), walks the central directory,
|
|
19
|
+
* then per-entry seeks the LFH + compressed bytes. Defends the
|
|
20
|
+
* LFH/CD-skew + Zip-Slip + zip-bomb classes by validating every
|
|
21
|
+
* claim before decompressing.
|
|
22
|
+
*
|
|
23
|
+
* - **Trusted sequential** — `{ readable: <Readable> }`
|
|
24
|
+
* Forward-scan-only fallback for operators who control both ends
|
|
25
|
+
* (e.g. piping the framework's own `b.archive.zip().toStream()`
|
|
26
|
+
* back into a reader 30 seconds later). The reader walks local
|
|
27
|
+
* file headers in order; the CD/LFH skew defense + the
|
|
28
|
+
* "entries hidden from LFH but present in CD" attack class are
|
|
29
|
+
* OFF in this mode because there's no central directory to
|
|
30
|
+
* compare against. The trust boundary is in the API surface
|
|
31
|
+
* name — operators reaching for `trustedStream` are declaring
|
|
32
|
+
* they own the producer.
|
|
33
|
+
*
|
|
34
|
+
* AbortSignal is propagated end-to-end: every adapter accepts an
|
|
35
|
+
* `opts.signal` parameter; in-flight `range` calls abort when the
|
|
36
|
+
* caller cancels. Adapters refuse to return short reads silently —
|
|
37
|
+
* a 5-byte request that fulfills 3 bytes throws `adapter/short-read`
|
|
38
|
+
* so the reader can decide whether to refuse the archive or surface
|
|
39
|
+
* the truncation.
|
|
40
|
+
*
|
|
41
|
+
* Shipped adapters:
|
|
42
|
+
*
|
|
43
|
+
* b.archive.adapters.fs(path, opts?) — local file
|
|
44
|
+
* b.archive.adapters.buffer(buf, opts?) — in-memory
|
|
45
|
+
* b.archive.adapters.objectStore(client, key, opts?)
|
|
46
|
+
* — composes b.objectStore
|
|
47
|
+
* Range-GET path
|
|
48
|
+
* b.archive.adapters.http(url, opts?) — composes b.httpClient
|
|
49
|
+
* with Range: bytes= …
|
|
50
|
+
* b.archive.adapters.trustedStream(readable, opts?)
|
|
51
|
+
* — Readable fallback
|
|
52
|
+
*
|
|
53
|
+
* `objectStore` + `http` are composition entry points — operators
|
|
54
|
+
* wire their own `b.objectStore` client / `b.httpClient` instance in
|
|
55
|
+
* so the adapter inherits the framework's SSRF guard / TLS posture /
|
|
56
|
+
* audit chain without duplicating that surface here.
|
|
57
|
+
*
|
|
58
|
+
* @card
|
|
59
|
+
* Source-bytes adapter contract for the b.archive read family — fs / objectStore / http / buffer / trustedStream.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
var nodeFs = require("node:fs");
|
|
63
|
+
var nodeStream = require("node:stream");
|
|
64
|
+
var lazyRequire = require("./lazy-require");
|
|
65
|
+
var validateOpts = require("./validate-opts");
|
|
66
|
+
var numericBounds = require("./numeric-bounds");
|
|
67
|
+
var safeBuffer = require("./safe-buffer");
|
|
68
|
+
var C = require("./constants");
|
|
69
|
+
var { defineClass } = require("./framework-error");
|
|
70
|
+
|
|
71
|
+
void numericBounds;
|
|
72
|
+
void validateOpts;
|
|
73
|
+
|
|
74
|
+
var AdapterError = defineClass("AdapterError", { alwaysPermanent: true });
|
|
75
|
+
|
|
76
|
+
// Lazy because httpClient + objectStore pull in TLS / SSRF surface
|
|
77
|
+
// the adapter caller may not need (e.g. tests that only use the fs +
|
|
78
|
+
// buffer adapters).
|
|
79
|
+
var httpClient = lazyRequire(function () { return require("./http-client"); });
|
|
80
|
+
void httpClient;
|
|
81
|
+
|
|
82
|
+
// ---- Shared validation helpers --------------------------------------------
|
|
83
|
+
|
|
84
|
+
function _assertNonNegativeInteger(value, label) {
|
|
85
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
86
|
+
throw new AdapterError("adapter/bad-arg",
|
|
87
|
+
label + " must be a non-negative integer (got " + value + ")");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _assertPositiveInteger(value, label) {
|
|
92
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
93
|
+
throw new AdapterError("adapter/bad-arg",
|
|
94
|
+
label + " must be a positive integer (got " + value + ")");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _checkSignal(signal, where) {
|
|
99
|
+
if (signal && signal.aborted) {
|
|
100
|
+
var reason = signal.reason || new AdapterError("adapter/aborted", where + ": adapter aborted by operator");
|
|
101
|
+
throw reason;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- fs adapter -----------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @primitive b.archive.adapters.fs
|
|
109
|
+
* @signature b.archive.adapters.fs(path, opts?)
|
|
110
|
+
* @since 0.12.7
|
|
111
|
+
* @status stable
|
|
112
|
+
* @related b.archive.adapters.objectStore, b.archive.adapters.http, b.archive.adapters.buffer
|
|
113
|
+
*
|
|
114
|
+
* Local-file random-access adapter. Opens a read-only file descriptor
|
|
115
|
+
* + fstats the size at adapter-create time so the reader's CD walk
|
|
116
|
+
* can begin with the trailer offset known up-front. Subsequent
|
|
117
|
+
* `range(offset, length)` calls reuse the same fd — operators
|
|
118
|
+
* extracting an archive don't pay a fresh open per range. `close()`
|
|
119
|
+
* is idempotent + safe to call after errors.
|
|
120
|
+
*
|
|
121
|
+
* @opts
|
|
122
|
+
* signal: AbortSignal, // propagates to in-flight read()s
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* var adapter = b.archive.adapters.fs("/var/uploads/payload.zip");
|
|
126
|
+
* try {
|
|
127
|
+
* var reader = b.archive.read.zip(adapter);
|
|
128
|
+
* var entries = await reader.inspect();
|
|
129
|
+
* } finally {
|
|
130
|
+
* await adapter.close();
|
|
131
|
+
* }
|
|
132
|
+
*/
|
|
133
|
+
function fs(path, opts) {
|
|
134
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
135
|
+
throw new AdapterError("adapter/bad-arg", "fs: path must be a non-empty string");
|
|
136
|
+
}
|
|
137
|
+
opts = opts || {};
|
|
138
|
+
var signal = opts.signal || null;
|
|
139
|
+
|
|
140
|
+
var fd = nodeFs.openSync(path, "r");
|
|
141
|
+
var stat = nodeFs.fstatSync(fd);
|
|
142
|
+
var size = stat.size;
|
|
143
|
+
var closed = false;
|
|
144
|
+
|
|
145
|
+
function close() {
|
|
146
|
+
if (closed) return;
|
|
147
|
+
closed = true;
|
|
148
|
+
try { nodeFs.closeSync(fd); } catch (_e) { /* drop-silent — file already gone */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function range(offset, length) {
|
|
152
|
+
return new Promise(function (resolve, reject) {
|
|
153
|
+
try {
|
|
154
|
+
_checkSignal(signal, "fs.range");
|
|
155
|
+
_assertNonNegativeInteger(offset, "fs.range: offset");
|
|
156
|
+
_assertPositiveInteger(length, "fs.range: length");
|
|
157
|
+
if (closed) throw new AdapterError("adapter/closed", "fs.range: adapter is closed");
|
|
158
|
+
if (offset + length > size) {
|
|
159
|
+
throw new AdapterError("adapter/out-of-range",
|
|
160
|
+
"fs.range: read past EOF (offset=" + offset + " length=" + length + " size=" + size + ")");
|
|
161
|
+
}
|
|
162
|
+
var buf = Buffer.allocUnsafe(length);
|
|
163
|
+
nodeFs.read(fd, buf, 0, length, offset, function (err, bytesRead) {
|
|
164
|
+
if (err) return reject(err);
|
|
165
|
+
if (bytesRead !== length) {
|
|
166
|
+
return reject(new AdapterError("adapter/short-read",
|
|
167
|
+
"fs.range: short read (requested=" + length + " got=" + bytesRead + ")"));
|
|
168
|
+
}
|
|
169
|
+
resolve(buf);
|
|
170
|
+
});
|
|
171
|
+
} catch (e) { reject(e); }
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
kind: "random-access",
|
|
177
|
+
name: "fs",
|
|
178
|
+
size: size,
|
|
179
|
+
range: range,
|
|
180
|
+
close: close,
|
|
181
|
+
signal: signal,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---- Buffer adapter -------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @primitive b.archive.adapters.buffer
|
|
189
|
+
* @signature b.archive.adapters.buffer(buf, opts?)
|
|
190
|
+
* @since 0.12.7
|
|
191
|
+
* @status stable
|
|
192
|
+
* @related b.archive.adapters.fs
|
|
193
|
+
*
|
|
194
|
+
* In-memory random-access adapter — slices a Buffer on `range()`.
|
|
195
|
+
* Useful for tests, small operator-uploaded payloads already in
|
|
196
|
+
* memory, and round-tripping `b.archive.zip().toBuffer()` output
|
|
197
|
+
* back through the reader without touching disk.
|
|
198
|
+
*
|
|
199
|
+
* @opts
|
|
200
|
+
* signal: AbortSignal,
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* var produced = b.archive.zip();
|
|
204
|
+
* produced.addFile("readme.txt", "Hello\n");
|
|
205
|
+
* var bytes = produced.toBuffer();
|
|
206
|
+
* var reader = b.archive.read.zip(b.archive.adapters.buffer(bytes));
|
|
207
|
+
* var entries = await reader.inspect();
|
|
208
|
+
*/
|
|
209
|
+
function buffer(buf, opts) {
|
|
210
|
+
if (!Buffer.isBuffer(buf)) {
|
|
211
|
+
throw new AdapterError("adapter/bad-arg", "buffer: arg must be a Buffer");
|
|
212
|
+
}
|
|
213
|
+
opts = opts || {};
|
|
214
|
+
var signal = opts.signal || null;
|
|
215
|
+
var size = buf.length;
|
|
216
|
+
|
|
217
|
+
function close() { /* nothing to release */ }
|
|
218
|
+
|
|
219
|
+
function range(offset, length) {
|
|
220
|
+
return new Promise(function (resolve, reject) {
|
|
221
|
+
try {
|
|
222
|
+
_checkSignal(signal, "buffer.range");
|
|
223
|
+
_assertNonNegativeInteger(offset, "buffer.range: offset");
|
|
224
|
+
_assertPositiveInteger(length, "buffer.range: length");
|
|
225
|
+
if (offset + length > size) {
|
|
226
|
+
throw new AdapterError("adapter/out-of-range",
|
|
227
|
+
"buffer.range: read past EOF (offset=" + offset + " length=" + length + " size=" + size + ")");
|
|
228
|
+
}
|
|
229
|
+
// .slice shares the underlying ArrayBuffer; copy so the
|
|
230
|
+
// caller can mutate without surprising the next range() call.
|
|
231
|
+
var out = Buffer.allocUnsafe(length);
|
|
232
|
+
buf.copy(out, 0, offset, offset + length);
|
|
233
|
+
resolve(out);
|
|
234
|
+
} catch (e) { reject(e); }
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
kind: "random-access",
|
|
240
|
+
name: "buffer",
|
|
241
|
+
size: size,
|
|
242
|
+
range: range,
|
|
243
|
+
close: close,
|
|
244
|
+
signal: signal,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---- objectStore adapter --------------------------------------------------
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @primitive b.archive.adapters.objectStore
|
|
252
|
+
* @signature b.archive.adapters.objectStore(client, key, opts?)
|
|
253
|
+
* @since 0.12.7
|
|
254
|
+
* @status stable
|
|
255
|
+
* @compliance hipaa, pci-dss, gdpr, soc2
|
|
256
|
+
* @related b.archive.adapters.fs, b.archive.adapters.http, b.objectStore
|
|
257
|
+
*
|
|
258
|
+
* Random-access adapter backed by an operator-supplied
|
|
259
|
+
* `b.objectStore` client. The adapter calls `client.get(key, { range:
|
|
260
|
+
* [start, end] })` for every `range()` request and reads the response
|
|
261
|
+
* body into a Buffer. Composes the framework's existing SSRF guard /
|
|
262
|
+
* TLS posture / audit chain — adapter behaviour follows whatever the
|
|
263
|
+
* client was configured with.
|
|
264
|
+
*
|
|
265
|
+
* The client is expected to expose:
|
|
266
|
+
* client.head(key) → { size: <number> } (or similar size accessor)
|
|
267
|
+
* client.get(key, opts) → AsyncIterable<Buffer> | { body: Readable }
|
|
268
|
+
* (Range opt honored)
|
|
269
|
+
*
|
|
270
|
+
* Operators using bucket implementations that don't expose `.head()`
|
|
271
|
+
* pass `opts.size` explicitly.
|
|
272
|
+
*
|
|
273
|
+
* @opts
|
|
274
|
+
* size: number, // override size (skips head() call)
|
|
275
|
+
* signal: AbortSignal,
|
|
276
|
+
* audit: b.audit, // forwarded to client.get
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* var client = { get: async function () { return Buffer.alloc(0); }, head: async function () { return { size: 0 }; } };
|
|
280
|
+
* var adapter = b.archive.adapters.objectStore(client, "incoming/payload.zip");
|
|
281
|
+
* var reader = b.archive.read.zip(adapter);
|
|
282
|
+
* var policy = b.guardArchive.zipBombPolicy({ maxTotalDecompressedBytes: 268435456 });
|
|
283
|
+
* void reader; void policy;
|
|
284
|
+
*/
|
|
285
|
+
function objectStore(client, key, opts) {
|
|
286
|
+
if (!client || typeof client.get !== "function") {
|
|
287
|
+
throw new AdapterError("adapter/bad-arg",
|
|
288
|
+
"objectStore: client must expose a .get(key, opts) method");
|
|
289
|
+
}
|
|
290
|
+
if (typeof key !== "string" || key.length === 0) {
|
|
291
|
+
throw new AdapterError("adapter/bad-arg", "objectStore: key must be a non-empty string");
|
|
292
|
+
}
|
|
293
|
+
opts = opts || {};
|
|
294
|
+
var signal = opts.signal || null;
|
|
295
|
+
var sizeOverride = opts.size;
|
|
296
|
+
var size = null;
|
|
297
|
+
|
|
298
|
+
async function _resolveSize() {
|
|
299
|
+
if (size !== null) return size;
|
|
300
|
+
if (typeof sizeOverride === "number") {
|
|
301
|
+
_assertNonNegativeInteger(sizeOverride, "objectStore.size opt");
|
|
302
|
+
size = sizeOverride;
|
|
303
|
+
return size;
|
|
304
|
+
}
|
|
305
|
+
if (typeof client.head !== "function") {
|
|
306
|
+
throw new AdapterError("adapter/no-size",
|
|
307
|
+
"objectStore: client has no .head(key) — pass opts.size explicitly");
|
|
308
|
+
}
|
|
309
|
+
var meta = await client.head(key, { signal: signal });
|
|
310
|
+
if (!meta || typeof meta.size !== "number") {
|
|
311
|
+
throw new AdapterError("adapter/no-size",
|
|
312
|
+
"objectStore: client.head(key) did not return { size: <number> }");
|
|
313
|
+
}
|
|
314
|
+
_assertNonNegativeInteger(meta.size, "objectStore.head(key).size");
|
|
315
|
+
size = meta.size;
|
|
316
|
+
return size;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function range(offset, length) {
|
|
320
|
+
_checkSignal(signal, "objectStore.range");
|
|
321
|
+
_assertNonNegativeInteger(offset, "objectStore.range: offset");
|
|
322
|
+
_assertPositiveInteger(length, "objectStore.range: length");
|
|
323
|
+
var s = await _resolveSize();
|
|
324
|
+
if (offset + length > s) {
|
|
325
|
+
throw new AdapterError("adapter/out-of-range",
|
|
326
|
+
"objectStore.range: read past EOF (offset=" + offset + " length=" + length + " size=" + s + ")");
|
|
327
|
+
}
|
|
328
|
+
// HTTP Range is inclusive on both endpoints.
|
|
329
|
+
var resp = await client.get(key, {
|
|
330
|
+
range: [offset, offset + length - 1],
|
|
331
|
+
signal: signal,
|
|
332
|
+
audit: opts.audit,
|
|
333
|
+
});
|
|
334
|
+
var body = resp && (resp.body || resp);
|
|
335
|
+
if (Buffer.isBuffer(body)) {
|
|
336
|
+
if (body.length !== length) {
|
|
337
|
+
throw new AdapterError("adapter/short-read",
|
|
338
|
+
"objectStore.range: short read (requested=" + length + " got=" + body.length + ")");
|
|
339
|
+
}
|
|
340
|
+
return body;
|
|
341
|
+
}
|
|
342
|
+
if (body && typeof body[Symbol.asyncIterator] === "function") {
|
|
343
|
+
var collector = safeBuffer.boundedChunkCollector({
|
|
344
|
+
maxBytes: length,
|
|
345
|
+
errorClass: AdapterError,
|
|
346
|
+
sizeCode: "adapter/over-read",
|
|
347
|
+
});
|
|
348
|
+
for await (var chunk of body) {
|
|
349
|
+
collector.push(chunk);
|
|
350
|
+
}
|
|
351
|
+
if (collector.bytesCollected() !== length) {
|
|
352
|
+
throw new AdapterError("adapter/short-read",
|
|
353
|
+
"objectStore.range: short read (requested=" + length +
|
|
354
|
+
" got=" + collector.bytesCollected() + ")");
|
|
355
|
+
}
|
|
356
|
+
return collector.result();
|
|
357
|
+
}
|
|
358
|
+
throw new AdapterError("adapter/bad-response",
|
|
359
|
+
"objectStore.range: client.get(key) returned neither Buffer nor AsyncIterable<Buffer>");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function close() { /* the client owns its connection pool */ }
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
kind: "random-access",
|
|
366
|
+
name: "objectStore",
|
|
367
|
+
range: range,
|
|
368
|
+
close: close,
|
|
369
|
+
signal: signal,
|
|
370
|
+
// size is a property accessor — the reader awaits it before
|
|
371
|
+
// the first range() call so the head() round-trip is folded into
|
|
372
|
+
// the first interaction rather than the constructor.
|
|
373
|
+
get size() { return size; },
|
|
374
|
+
resolveSize: _resolveSize,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ---- HTTP adapter ---------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* @primitive b.archive.adapters.http
|
|
382
|
+
* @signature b.archive.adapters.http(url, opts?)
|
|
383
|
+
* @since 0.12.7
|
|
384
|
+
* @status stable
|
|
385
|
+
* @compliance gdpr, hipaa, pci-dss
|
|
386
|
+
* @related b.archive.adapters.objectStore, b.httpClient
|
|
387
|
+
*
|
|
388
|
+
* Random-access adapter backed by HTTP Range requests. Composes the
|
|
389
|
+
* framework's `b.httpClient` (SSRF guard + TLS posture + audit chain
|
|
390
|
+
* + PQC-hybrid agent) so the adapter inherits the operator's network
|
|
391
|
+
* surface configuration without duplicating it here.
|
|
392
|
+
*
|
|
393
|
+
* First call issues a HEAD to determine size + verify the server
|
|
394
|
+
* accepts Range requests (`Accept-Ranges: bytes`). Servers without
|
|
395
|
+
* Range support are refused with `adapter/no-range` — operators
|
|
396
|
+
* downloading the full byte stream first and feeding `b.archive.
|
|
397
|
+
* adapters.buffer` is the appropriate fallback in that case.
|
|
398
|
+
*
|
|
399
|
+
* @opts
|
|
400
|
+
* client: b.httpClient, // override the default (must already exist)
|
|
401
|
+
* headers: { ... },
|
|
402
|
+
* timeoutMs: number, // per-request
|
|
403
|
+
* signal: AbortSignal,
|
|
404
|
+
* audit: b.audit,
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* var adapter = b.archive.adapters.http("https://artifact-host.example.com/release.zip", {
|
|
408
|
+
* timeoutMs: 60_000,
|
|
409
|
+
* });
|
|
410
|
+
* var reader = b.archive.read.zip(adapter);
|
|
411
|
+
* var entries = await reader.inspect();
|
|
412
|
+
*/
|
|
413
|
+
function http(url, opts) {
|
|
414
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
415
|
+
throw new AdapterError("adapter/bad-arg", "http: url must be a non-empty string");
|
|
416
|
+
}
|
|
417
|
+
opts = opts || {};
|
|
418
|
+
var signal = opts.signal || null;
|
|
419
|
+
var client = opts.client || httpClient();
|
|
420
|
+
var headers = Object.assign({}, opts.headers || {});
|
|
421
|
+
var timeoutMs = opts.timeoutMs || C.TIME.seconds(30);
|
|
422
|
+
var size = null;
|
|
423
|
+
|
|
424
|
+
async function _resolveSize() {
|
|
425
|
+
if (size !== null) return size;
|
|
426
|
+
_checkSignal(signal, "http.head");
|
|
427
|
+
var res = await client.request({
|
|
428
|
+
method: "HEAD",
|
|
429
|
+
url: url,
|
|
430
|
+
headers: headers,
|
|
431
|
+
timeoutMs: timeoutMs,
|
|
432
|
+
signal: signal,
|
|
433
|
+
audit: opts.audit,
|
|
434
|
+
});
|
|
435
|
+
if (!res || !res.headers) {
|
|
436
|
+
throw new AdapterError("adapter/bad-response", "http: HEAD returned no headers");
|
|
437
|
+
}
|
|
438
|
+
var acceptRanges = res.headers["accept-ranges"];
|
|
439
|
+
if (!acceptRanges || String(acceptRanges).toLowerCase() !== "bytes") {
|
|
440
|
+
throw new AdapterError("adapter/no-range",
|
|
441
|
+
"http: server does not advertise 'Accept-Ranges: bytes' (got " + JSON.stringify(acceptRanges) + ")");
|
|
442
|
+
}
|
|
443
|
+
var lenHdr = res.headers["content-length"];
|
|
444
|
+
if (!lenHdr) {
|
|
445
|
+
throw new AdapterError("adapter/no-size",
|
|
446
|
+
"http: server did not send Content-Length on HEAD");
|
|
447
|
+
}
|
|
448
|
+
var parsed = Number(lenHdr);
|
|
449
|
+
_assertNonNegativeInteger(parsed, "http: parsed content-length");
|
|
450
|
+
size = parsed;
|
|
451
|
+
return size;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async function range(offset, length) {
|
|
455
|
+
_checkSignal(signal, "http.range");
|
|
456
|
+
_assertNonNegativeInteger(offset, "http.range: offset");
|
|
457
|
+
_assertPositiveInteger(length, "http.range: length");
|
|
458
|
+
var s = await _resolveSize();
|
|
459
|
+
if (offset + length > s) {
|
|
460
|
+
throw new AdapterError("adapter/out-of-range",
|
|
461
|
+
"http.range: read past EOF (offset=" + offset + " length=" + length + " size=" + s + ")");
|
|
462
|
+
}
|
|
463
|
+
var rangeHdr = "bytes=" + offset + "-" + (offset + length - 1);
|
|
464
|
+
var hdrs = Object.assign({}, headers, { "Range": rangeHdr });
|
|
465
|
+
var res = await client.request({
|
|
466
|
+
method: "GET",
|
|
467
|
+
url: url,
|
|
468
|
+
headers: hdrs,
|
|
469
|
+
timeoutMs: timeoutMs,
|
|
470
|
+
signal: signal,
|
|
471
|
+
audit: opts.audit,
|
|
472
|
+
});
|
|
473
|
+
if (!res || (res.status !== 206 && res.status !== 200)) {
|
|
474
|
+
throw new AdapterError("adapter/bad-response",
|
|
475
|
+
"http.range: expected 206 Partial Content, got " + (res && res.status));
|
|
476
|
+
}
|
|
477
|
+
var body = res.body;
|
|
478
|
+
if (Buffer.isBuffer(body)) {
|
|
479
|
+
if (body.length !== length) {
|
|
480
|
+
throw new AdapterError("adapter/short-read",
|
|
481
|
+
"http.range: short read (requested=" + length + " got=" + body.length + ")");
|
|
482
|
+
}
|
|
483
|
+
return body;
|
|
484
|
+
}
|
|
485
|
+
if (body && typeof body[Symbol.asyncIterator] === "function") {
|
|
486
|
+
var collector = safeBuffer.boundedChunkCollector({
|
|
487
|
+
maxBytes: length,
|
|
488
|
+
errorClass: AdapterError,
|
|
489
|
+
sizeCode: "adapter/over-read",
|
|
490
|
+
});
|
|
491
|
+
for await (var chunk of body) {
|
|
492
|
+
collector.push(chunk);
|
|
493
|
+
}
|
|
494
|
+
if (collector.bytesCollected() !== length) {
|
|
495
|
+
throw new AdapterError("adapter/short-read",
|
|
496
|
+
"http.range: short read (requested=" + length +
|
|
497
|
+
" got=" + collector.bytesCollected() + ")");
|
|
498
|
+
}
|
|
499
|
+
return collector.result();
|
|
500
|
+
}
|
|
501
|
+
throw new AdapterError("adapter/bad-response",
|
|
502
|
+
"http.range: response body is neither Buffer nor AsyncIterable<Buffer>");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function close() { /* httpClient owns its connection pool */ }
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
kind: "random-access",
|
|
509
|
+
name: "http",
|
|
510
|
+
range: range,
|
|
511
|
+
close: close,
|
|
512
|
+
signal: signal,
|
|
513
|
+
get size() { return size; },
|
|
514
|
+
resolveSize: _resolveSize,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ---- trustedStream adapter ------------------------------------------------
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @primitive b.archive.adapters.trustedStream
|
|
522
|
+
* @signature b.archive.adapters.trustedStream(readable, opts?)
|
|
523
|
+
* @since 0.12.7
|
|
524
|
+
* @status stable
|
|
525
|
+
* @related b.archive.adapters.fs, b.archive.adapters.buffer
|
|
526
|
+
*
|
|
527
|
+
* Forward-scan-only adapter for trusted Readable sources. The reader
|
|
528
|
+
* walks local file headers in order; the CD/LFH skew defense and the
|
|
529
|
+
* "entries hidden from LFH but present in CD" attack class are OFF
|
|
530
|
+
* in this mode because there's no central directory to compare
|
|
531
|
+
* against. Operators reaching for this primitive are declaring they
|
|
532
|
+
* own the producer (e.g. piping their own
|
|
533
|
+
* `b.archive.zip().toStream()` output back into a reader 30 seconds
|
|
534
|
+
* later for round-trip verification).
|
|
535
|
+
*
|
|
536
|
+
* Adversarial input MUST use `b.archive.adapters.fs` / `buffer` /
|
|
537
|
+
* `objectStore` / `http` — the random-access path is the only
|
|
538
|
+
* adversarial-safe one.
|
|
539
|
+
*
|
|
540
|
+
* @opts
|
|
541
|
+
* signal: AbortSignal,
|
|
542
|
+
*
|
|
543
|
+
* @example
|
|
544
|
+
* var produced = fs.createReadStream("./own-export.zip");
|
|
545
|
+
* var reader = b.archive.read.zip.fromTrustedStream(produced);
|
|
546
|
+
* var entries = [];
|
|
547
|
+
* for await (var e of reader.entries()) entries.push(e);
|
|
548
|
+
*/
|
|
549
|
+
function trustedStream(readable, opts) {
|
|
550
|
+
if (!readable || typeof readable.pipe !== "function" || typeof readable.on !== "function") {
|
|
551
|
+
throw new AdapterError("adapter/bad-arg",
|
|
552
|
+
"trustedStream: arg must be a Readable (or pipe/on-compatible stream)");
|
|
553
|
+
}
|
|
554
|
+
if (!(readable instanceof nodeStream.Readable) && !readable.readable) {
|
|
555
|
+
// Accept stream-like duck-typed objects; many libraries return
|
|
556
|
+
// Readable-flavored bytes via Symbol.asyncIterator only.
|
|
557
|
+
}
|
|
558
|
+
opts = opts || {};
|
|
559
|
+
var signal = opts.signal || null;
|
|
560
|
+
|
|
561
|
+
function close() {
|
|
562
|
+
if (typeof readable.destroy === "function") {
|
|
563
|
+
try { readable.destroy(); } catch (_e) { /* drop-silent */ }
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
kind: "trusted-sequential",
|
|
569
|
+
name: "trustedStream",
|
|
570
|
+
readable: readable,
|
|
571
|
+
signal: signal,
|
|
572
|
+
close: close,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ---- Adapter shape predicates --------------------------------------------
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* @primitive b.archive.adapters.isRandomAccessAdapter
|
|
580
|
+
* @signature b.archive.adapters.isRandomAccessAdapter(a)
|
|
581
|
+
* @since 0.12.7
|
|
582
|
+
* @status stable
|
|
583
|
+
* @related b.archive.adapters.fs, b.archive.adapters.objectStore
|
|
584
|
+
*
|
|
585
|
+
* Type-predicate: returns `true` when `a` is the random-access shape
|
|
586
|
+
* (`{ kind: "random-access", range, ... }`) produced by `fs` / `buffer`
|
|
587
|
+
* / `objectStore` / `http`. Operators routing through `b.archive.read.zip`
|
|
588
|
+
* compose this to refuse trusted-stream adapters at the wrong entry
|
|
589
|
+
* point.
|
|
590
|
+
*
|
|
591
|
+
* @example
|
|
592
|
+
* var ok = b.archive.adapters.isRandomAccessAdapter(adapter);
|
|
593
|
+
* if (!ok) throw new Error("need random-access adapter");
|
|
594
|
+
*/
|
|
595
|
+
function isRandomAccessAdapter(a) {
|
|
596
|
+
return !!(a && a.kind === "random-access" && typeof a.range === "function");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* @primitive b.archive.adapters.isTrustedStreamAdapter
|
|
601
|
+
* @signature b.archive.adapters.isTrustedStreamAdapter(a)
|
|
602
|
+
* @since 0.12.7
|
|
603
|
+
* @status stable
|
|
604
|
+
* @related b.archive.adapters.trustedStream
|
|
605
|
+
*
|
|
606
|
+
* Type-predicate: returns `true` when `a` is the trusted-sequential
|
|
607
|
+
* shape (`{ kind: "trusted-sequential", readable, ... }`) produced by
|
|
608
|
+
* `trustedStream`. Operators routing through `b.archive.read.zip.
|
|
609
|
+
* fromTrustedStream` compose this to refuse random-access adapters
|
|
610
|
+
* at the wrong entry point.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* var ok = b.archive.adapters.isTrustedStreamAdapter(adapter);
|
|
614
|
+
* if (!ok) throw new Error("need trusted-stream adapter");
|
|
615
|
+
*/
|
|
616
|
+
function isTrustedStreamAdapter(a) {
|
|
617
|
+
return !!(a && a.kind === "trusted-sequential" && a.readable);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
module.exports = {
|
|
621
|
+
fs: fs,
|
|
622
|
+
buffer: buffer,
|
|
623
|
+
objectStore: objectStore,
|
|
624
|
+
http: http,
|
|
625
|
+
trustedStream: trustedStream,
|
|
626
|
+
isRandomAccessAdapter: isRandomAccessAdapter,
|
|
627
|
+
isTrustedStreamAdapter: isTrustedStreamAdapter,
|
|
628
|
+
AdapterError: AdapterError,
|
|
629
|
+
};
|