omnirush 0.7.0 → 0.8.0

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 (31) hide show
  1. package/assets/collect-once.ts +72 -141
  2. package/assets/extensions/omnirush/agents-lib.ts +23 -13
  3. package/assets/extensions/omnirush/agents.ts +19 -24
  4. package/assets/extensions/omnirush/capture/UPSTREAM +21 -0
  5. package/assets/extensions/omnirush/capture/collect-upload-budget.ts +39 -0
  6. package/assets/extensions/omnirush/capture/omnirush-swarm.ts +6 -0
  7. package/assets/extensions/omnirush/capture/server-fetch.ts +8 -0
  8. package/assets/extensions/omnirush/capture/session-archive/detect.ts +410 -0
  9. package/assets/extensions/omnirush/capture/session-archive/files.ts +85 -0
  10. package/assets/extensions/omnirush/capture/session-archive/ignored.ts +123 -0
  11. package/assets/extensions/omnirush/capture/session-archive/index.ts +1468 -0
  12. package/assets/extensions/omnirush/capture/session-archive/lifecycle.ts +606 -0
  13. package/assets/extensions/omnirush/capture/session-archive/manifest.ts +827 -0
  14. package/assets/extensions/omnirush/capture/session-archive/pack.ts +481 -0
  15. package/assets/extensions/omnirush/capture/session-archive/policy.ts +21 -0
  16. package/assets/extensions/omnirush/capture/session-archive/seal.ts +481 -0
  17. package/assets/extensions/omnirush/capture/session-archive/touched.ts +550 -0
  18. package/assets/extensions/omnirush/capture/session-archive/upload.ts +601 -0
  19. package/assets/extensions/omnirush/capture/turn-diff.ts +661 -0
  20. package/assets/extensions/omnirush/capture/vendor/minimatch.js +4 -0
  21. package/assets/extensions/omnirush/capture/vendor/zod.js +60 -0
  22. package/assets/extensions/omnirush/capture/workspace-collector.ts +5021 -0
  23. package/assets/extensions/omnirush/collector.ts +539 -171
  24. package/assets/extensions/omnirush/commands.ts +1 -1
  25. package/assets/extensions/omnirush/pi-engine.ts +655 -0
  26. package/assets/extensions/omnirush/plan.ts +6 -7
  27. package/package.json +5 -2
  28. package/scripts/build-capture-vendor.js +55 -0
  29. package/src/bin.js +30 -5
  30. package/assets/extensions/omnirush/collector-lib.ts +0 -2433
  31. package/assets/extensions/omnirush/trace-format.ts +0 -311
@@ -0,0 +1,481 @@
1
+ /**
2
+ * ORSEAL01: seals a byte stream to the omnirush.ai archive key (X25519 +
3
+ * HKDF-SHA256 + AES-256-GCM in authenticated frames). The format, the key id
4
+ * and the decoder's error rules are normative in
5
+ * docs/omnirush-project-archive.md section 6 of the backend repository; the
6
+ * shared vectors are in __fixtures__/orseal_vectors.json.
7
+ */
8
+ import {
9
+ createCipheriv,
10
+ createDecipheriv,
11
+ createHash,
12
+ createPrivateKey,
13
+ createPublicKey,
14
+ diffieHellman,
15
+ generateKeyPairSync,
16
+ hkdfSync,
17
+ randomBytes,
18
+ type Hash,
19
+ type KeyObject,
20
+ } from "node:crypto";
21
+ import { Transform, type TransformCallback } from "node:stream";
22
+
23
+ export const SEAL_MAGIC = Buffer.from("ORSEAL01", "ascii");
24
+ export const SEAL_ALG = "X25519-HKDF-SHA256-A256GCM";
25
+ export const SEAL_CONTENT = "tar+zstd";
26
+ /** The only chunk size production writers use. */
27
+ export const SEAL_CHUNK = 1 << 20;
28
+ export const SEAL_MIN_CHUNK = 16;
29
+ export const SEAL_MAX_CHUNK = 1 << 24;
30
+ export const SEAL_MAX_HEADER_LEN = 4096;
31
+ const TAG_LEN = 16;
32
+ const MAX_FRAMES = 2 ** 32;
33
+ const HKDF_INFO = Buffer.from("omnirush-seal-v1", "ascii");
34
+ const HEADER_KEYS = new Set(["alg", "chunk", "content", "epk", "kid", "salt", "v"]);
35
+ // DER wrappers that turn a raw 32-byte X25519 key into PKCS#8 / SPKI for Node's key objects.
36
+ const PKCS8_X25519_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
37
+ const SPKI_X25519_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
38
+
39
+ export type SealErrorCode =
40
+ | "bad_magic"
41
+ | "bad_header"
42
+ | "unsupported_version"
43
+ | "unknown_kid"
44
+ | "bad_frame_length"
45
+ | "tag_mismatch"
46
+ | "truncated"
47
+ | "frame_after_final";
48
+
49
+ export class SealError extends Error {
50
+ constructor(readonly code: SealErrorCode) {
51
+ super(code);
52
+ this.name = "SealError";
53
+ }
54
+ }
55
+
56
+ function raw32(bytes: Uint8Array, what: string): Buffer {
57
+ if (bytes.length !== 32) throw new TypeError(`${what} must be 32 bytes`);
58
+ return Buffer.from(bytes);
59
+ }
60
+
61
+ export function x25519PrivateKey(raw: Uint8Array): KeyObject {
62
+ return createPrivateKey({ key: Buffer.concat([PKCS8_X25519_PREFIX, raw32(raw, "X25519 private key")]), format: "der", type: "pkcs8" });
63
+ }
64
+
65
+ export function x25519PublicKey(raw: Uint8Array): KeyObject {
66
+ return createPublicKey({ key: Buffer.concat([SPKI_X25519_PREFIX, raw32(raw, "X25519 public key")]), format: "der", type: "spki" });
67
+ }
68
+
69
+ /** The raw 32-byte public key of an X25519 private or public key object. */
70
+ export function x25519PublicRaw(key: KeyObject): Buffer {
71
+ const publicKey = key.type === "private" ? createPublicKey(key) : key;
72
+ const der = publicKey.export({ format: "der", type: "spki" });
73
+ return Buffer.from(der.subarray(der.length - 32));
74
+ }
75
+
76
+ /** kid: the first 16 lowercase hex characters of SHA-256(raw public key). */
77
+ export function sealKid(publicRaw: Uint8Array): string {
78
+ return createHash("sha256").update(raw32(publicRaw, "X25519 public key")).digest("hex").slice(0, 16);
79
+ }
80
+
81
+ function deriveKey(shared: Buffer, salt: Buffer, epk: Buffer, serverPublic: Buffer): Buffer {
82
+ return Buffer.from(hkdfSync("sha256", shared, salt, Buffer.concat([HKDF_INFO, epk, serverPublic]), 32));
83
+ }
84
+
85
+ function agree(privateKey: KeyObject, publicKey: KeyObject): Buffer | null {
86
+ let shared: Buffer;
87
+ try {
88
+ shared = diffieHellman({ privateKey, publicKey });
89
+ } catch {
90
+ return null;
91
+ }
92
+ // A low-order point yields an all-zero secret; Node already throws on it,
93
+ // the explicit check keeps that guarantee independent of the runtime.
94
+ return shared.every((byte) => byte === 0) ? null : shared;
95
+ }
96
+
97
+ function frameNonce(index: number): Buffer {
98
+ const nonce = Buffer.alloc(12);
99
+ nonce.writeBigUInt64BE(BigInt(index), 4);
100
+ return nonce;
101
+ }
102
+
103
+ function frameAad(headerHash: Buffer, index: number, final: boolean): Buffer {
104
+ const aad = Buffer.alloc(41);
105
+ headerHash.copy(aad, 0);
106
+ aad.writeBigUInt64BE(BigInt(index), 32);
107
+ aad[40] = final ? 1 : 0;
108
+ return aad;
109
+ }
110
+
111
+ /** One frame as pieces (length prefix, ciphertext, tag): no second copy of the ciphertext is made. */
112
+ function encryptFrame(key: Buffer, headerHash: Buffer, index: number, final: boolean, plaintext: Buffer): Buffer[] {
113
+ const cipher = createCipheriv("aes-256-gcm", key, frameNonce(index));
114
+ cipher.setAAD(frameAad(headerHash, index, final));
115
+ const length = Buffer.allocUnsafe(4);
116
+ length.writeUInt32BE(plaintext.length + TAG_LEN, 0);
117
+ const body = cipher.update(plaintext);
118
+ const tail = cipher.final();
119
+ const pieces = [length, body];
120
+ if (tail.length > 0) pieces.push(tail);
121
+ pieces.push(cipher.getAuthTag());
122
+ return pieces;
123
+ }
124
+
125
+ function decryptFrame(key: Buffer, headerHash: Buffer, index: number, final: boolean, frame: Buffer): Buffer | null {
126
+ try {
127
+ const decipher = createDecipheriv("aes-256-gcm", key, frameNonce(index));
128
+ decipher.setAAD(frameAad(headerHash, index, final));
129
+ decipher.setAuthTag(frame.subarray(frame.length - TAG_LEN));
130
+ const head = decipher.update(frame.subarray(0, frame.length - TAG_LEN));
131
+ const tail = decipher.final();
132
+ return tail.length === 0 ? head : Buffer.concat([head, tail]);
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ export type SealOptions = {
139
+ /** The recipient's raw 32-byte X25519 public key (GET /omnirush/archives/key). */
140
+ publicKey: Uint8Array;
141
+ /** Plaintext bytes per frame. Production MUST leave the default (1 MiB); the vectors use 16. */
142
+ chunk?: number;
143
+ /**
144
+ * TEST VECTORS ONLY. Fixes the ephemeral private key and the salt so a seal
145
+ * reproduces orseal_vectors.json byte for byte. Production code never sets
146
+ * it: every archive gets a fresh ephemeral key and salt from the CSPRNG.
147
+ */
148
+ testOnlyFixedSecrets?: { ephemeralPrivateKey: Uint8Array; salt: Uint8Array };
149
+ };
150
+
151
+ export type SealResult = { size: number; sha256: string };
152
+
153
+ /**
154
+ * The ORSEAL01 framing core, shared by SealStream and sealBuffer. It holds at
155
+ * most one chunk of plaintext: a full chunk becomes a non-final frame only
156
+ * once more input arrives, and whatever is held at the end (possibly a full
157
+ * chunk, or nothing) becomes the final frame. It counts and hashes its own
158
+ * output, which the create request needs.
159
+ */
160
+ class FrameSealer {
161
+ readonly kid: string;
162
+ private readonly key: Buffer;
163
+ private readonly headerHash: Buffer;
164
+ private readonly prefix: Buffer;
165
+ private readonly chunk: number;
166
+ private readonly pending: Buffer;
167
+ private pendingLength = 0;
168
+ private index = 0;
169
+ private started = false;
170
+ private readonly outputHash: Hash = createHash("sha256");
171
+ private outputBytes = 0;
172
+ private sealed: SealResult | null = null;
173
+
174
+ constructor(options: SealOptions) {
175
+ const chunk = options.chunk ?? SEAL_CHUNK;
176
+ if (!Number.isInteger(chunk) || chunk < SEAL_MIN_CHUNK || chunk > SEAL_MAX_CHUNK) throw new RangeError("invalid seal chunk size");
177
+ const serverPublicRaw = raw32(options.publicKey, "X25519 public key");
178
+ const ephemeral = options.testOnlyFixedSecrets
179
+ ? x25519PrivateKey(options.testOnlyFixedSecrets.ephemeralPrivateKey)
180
+ : generateKeyPairSync("x25519").privateKey;
181
+ const salt = options.testOnlyFixedSecrets ? raw32(options.testOnlyFixedSecrets.salt, "salt") : randomBytes(32);
182
+ const epk = x25519PublicRaw(ephemeral);
183
+ const shared = agree(ephemeral, x25519PublicKey(serverPublicRaw));
184
+ if (!shared) throw new RangeError("the archive public key is not a valid X25519 point");
185
+ this.kid = sealKid(serverPublicRaw);
186
+ this.chunk = chunk;
187
+ this.key = deriveKey(shared, salt, epk, serverPublicRaw);
188
+ // Key order alg, chunk, content, epk, kid, salt, v: sorted, as the format requires.
189
+ const header = Buffer.from(JSON.stringify({
190
+ alg: SEAL_ALG,
191
+ chunk,
192
+ content: SEAL_CONTENT,
193
+ epk: epk.toString("base64"),
194
+ kid: this.kid,
195
+ salt: salt.toString("base64"),
196
+ v: 1,
197
+ }), "utf8");
198
+ const length = Buffer.alloc(4);
199
+ length.writeUInt32BE(header.length, 0);
200
+ this.prefix = Buffer.concat([SEAL_MAGIC, length, header]);
201
+ this.headerHash = createHash("sha256").update(this.prefix).digest();
202
+ this.pending = Buffer.allocUnsafe(chunk);
203
+ }
204
+
205
+ get result(): SealResult | null {
206
+ return this.sealed;
207
+ }
208
+
209
+ private emit(bytes: Buffer, out: (bytes: Buffer) => void): void {
210
+ this.outputHash.update(bytes);
211
+ this.outputBytes += bytes.length;
212
+ out(bytes);
213
+ }
214
+
215
+ private start(out: (bytes: Buffer) => void): void {
216
+ if (this.started) return;
217
+ this.started = true;
218
+ this.emit(this.prefix, out);
219
+ }
220
+
221
+ private frame(final: boolean, out: (bytes: Buffer) => void): void {
222
+ if (this.index >= MAX_FRAMES) throw new RangeError("too many seal frames");
223
+ for (const piece of encryptFrame(this.key, this.headerHash, this.index, final, this.pending.subarray(0, this.pendingLength))) this.emit(piece, out);
224
+ this.index += 1;
225
+ this.pendingLength = 0;
226
+ }
227
+
228
+ update(bytes: Buffer, out: (bytes: Buffer) => void): void {
229
+ if (this.sealed) throw new Error("the seal is already finished");
230
+ this.start(out);
231
+ let offset = 0;
232
+ while (offset < bytes.length) {
233
+ if (this.pendingLength === this.chunk) this.frame(false, out);
234
+ const take = Math.min(bytes.length - offset, this.chunk - this.pendingLength);
235
+ bytes.copy(this.pending, this.pendingLength, offset, offset + take);
236
+ this.pendingLength += take;
237
+ offset += take;
238
+ }
239
+ }
240
+
241
+ finish(out: (bytes: Buffer) => void): SealResult {
242
+ if (this.sealed) throw new Error("the seal is already finished");
243
+ this.start(out);
244
+ this.frame(true, out);
245
+ this.sealed = { size: this.outputBytes, sha256: this.outputHash.digest("hex") };
246
+ return this.sealed;
247
+ }
248
+ }
249
+
250
+ /** Streaming ORSEAL01 sealer (section 6.6); memory is one chunk plus stream buffers. */
251
+ export class SealStream extends Transform {
252
+ private readonly sealer: FrameSealer;
253
+
254
+ constructor(options: SealOptions) {
255
+ super();
256
+ this.sealer = new FrameSealer(options);
257
+ }
258
+
259
+ get kid(): string {
260
+ return this.sealer.kid;
261
+ }
262
+
263
+ /** Ciphertext size and SHA-256; available once the stream has finished. */
264
+ result(): SealResult {
265
+ const result = this.sealer.result;
266
+ if (!result) throw new Error("the seal stream has not finished");
267
+ return result;
268
+ }
269
+
270
+ override _transform(data: Buffer | string, encoding: BufferEncoding, callback: TransformCallback): void {
271
+ try {
272
+ this.sealer.update(typeof data === "string" ? Buffer.from(data, encoding) : data, (bytes) => this.push(bytes));
273
+ callback();
274
+ } catch (error) {
275
+ callback(error instanceof Error ? error : new Error(String(error)));
276
+ }
277
+ }
278
+
279
+ override _flush(callback: TransformCallback): void {
280
+ try {
281
+ this.sealer.finish((bytes) => this.push(bytes));
282
+ callback();
283
+ } catch (error) {
284
+ callback(error instanceof Error ? error : new Error(String(error)));
285
+ }
286
+ }
287
+ }
288
+
289
+ /** Seals a buffer in one call (tests and small payloads). */
290
+ export function sealBuffer(plaintext: Uint8Array, options: SealOptions): { sealed: Buffer; kid: string } & SealResult {
291
+ const sealer = new FrameSealer(options);
292
+ const out: Buffer[] = [];
293
+ const push = (bytes: Buffer) => out.push(bytes);
294
+ sealer.update(Buffer.from(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength), push);
295
+ const result = sealer.finish(push);
296
+ return { sealed: Buffer.concat(out), kid: sealer.kid, ...result };
297
+ }
298
+
299
+ // --- opening -------------------------------------------------------------------
300
+
301
+ export type SealKey = { kid: string; privateKey: KeyObject; publicRaw: Buffer };
302
+ /** kid -> key, every configured private key (current first). */
303
+ export type SealKeyring = Map<string, SealKey>;
304
+
305
+ export function sealKeyring(privateKeys: readonly Uint8Array[]): SealKeyring {
306
+ const keyring: SealKeyring = new Map();
307
+ for (const raw of privateKeys) {
308
+ const privateKey = x25519PrivateKey(raw);
309
+ const publicRaw = x25519PublicRaw(privateKey);
310
+ const kid = sealKid(publicRaw);
311
+ if (!keyring.has(kid)) keyring.set(kid, { kid, privateKey, publicRaw });
312
+ }
313
+ return keyring;
314
+ }
315
+
316
+ /** Exact reads plus a one-byte look-ahead over an async byte source. */
317
+ class ByteReader {
318
+ private readonly iterator: AsyncIterator<Uint8Array>;
319
+ private buffered: Buffer[] = [];
320
+ private length = 0;
321
+ private done = false;
322
+
323
+ constructor(source: AsyncIterable<Uint8Array>) {
324
+ this.iterator = source[Symbol.asyncIterator]();
325
+ }
326
+
327
+ private async fill(wanted: number): Promise<void> {
328
+ while (this.length < wanted && !this.done) {
329
+ const next = await this.iterator.next();
330
+ if (next.done) {
331
+ this.done = true;
332
+ break;
333
+ }
334
+ if (next.value.length === 0) continue;
335
+ const piece = Buffer.from(next.value.buffer, next.value.byteOffset, next.value.byteLength);
336
+ this.buffered.push(piece);
337
+ this.length += piece.length;
338
+ }
339
+ }
340
+
341
+ /** Up to `count` bytes; fewer only at the end of the source. */
342
+ async read(count: number): Promise<Buffer> {
343
+ await this.fill(count);
344
+ const take = Math.min(count, this.length);
345
+ const joined = this.buffered.length === 1 ? this.buffered[0]! : Buffer.concat(this.buffered, this.length);
346
+ const out = Buffer.from(joined.subarray(0, take));
347
+ const rest = joined.subarray(take);
348
+ this.buffered = rest.length > 0 ? [rest] : [];
349
+ this.length = rest.length;
350
+ return out;
351
+ }
352
+
353
+ async more(): Promise<boolean> {
354
+ await this.fill(1);
355
+ return this.length > 0;
356
+ }
357
+
358
+ async close(): Promise<void> {
359
+ await this.iterator.return?.();
360
+ }
361
+ }
362
+
363
+ const UTF8 = new TextDecoder("utf-8", { fatal: true });
364
+ const B64_32 = /^[A-Za-z0-9+/]{43}=$/;
365
+
366
+ function canonicalBase64Of32(value: unknown): Buffer {
367
+ if (typeof value !== "string" || !B64_32.test(value)) throw new SealError("bad_header");
368
+ const raw = Buffer.from(value, "base64");
369
+ if (raw.length !== 32 || raw.toString("base64") !== value) throw new SealError("bad_header");
370
+ return raw;
371
+ }
372
+
373
+ function isRecord(value: unknown): value is Record<string, unknown> {
374
+ return typeof value === "object" && value !== null && !Array.isArray(value);
375
+ }
376
+
377
+ type OpenedHeader = { chunk: number; key: Buffer; headerHash: Buffer; kid: string };
378
+
379
+ async function openHeader(reader: ByteReader, keyring: SealKeyring): Promise<OpenedHeader> {
380
+ const magic = await reader.read(8);
381
+ if (!magic.equals(SEAL_MAGIC)) throw new SealError("bad_magic");
382
+ const rawLength = await reader.read(4);
383
+ if (rawLength.length < 4) throw new SealError("truncated");
384
+ const length = rawLength.readUInt32BE(0);
385
+ if (length === 0 || length > SEAL_MAX_HEADER_LEN) throw new SealError("bad_header");
386
+ const head = await reader.read(length);
387
+ if (head.length < length) throw new SealError("truncated");
388
+ let header: unknown;
389
+ try {
390
+ header = JSON.parse(UTF8.decode(head));
391
+ } catch {
392
+ throw new SealError("bad_header");
393
+ }
394
+ if (!isRecord(header)) throw new SealError("bad_header");
395
+ const version = header.v;
396
+ if (typeof version !== "number" || !Number.isInteger(version)) throw new SealError("bad_header");
397
+ if (version !== 1) throw new SealError("unsupported_version");
398
+ const keys = Object.keys(header);
399
+ const { chunk, kid } = header;
400
+ if (
401
+ keys.length !== HEADER_KEYS.size
402
+ || !keys.every((key) => HEADER_KEYS.has(key))
403
+ || header.alg !== SEAL_ALG
404
+ || header.content !== SEAL_CONTENT
405
+ || typeof chunk !== "number"
406
+ || !Number.isInteger(chunk)
407
+ || chunk < SEAL_MIN_CHUNK
408
+ || chunk > SEAL_MAX_CHUNK
409
+ || typeof kid !== "string"
410
+ || !/^[0-9a-f]{16}$/.test(kid)
411
+ ) {
412
+ throw new SealError("bad_header");
413
+ }
414
+ const epk = canonicalBase64Of32(header.epk);
415
+ const salt = canonicalBase64Of32(header.salt);
416
+ const recipient = keyring.get(kid);
417
+ if (!recipient) throw new SealError("unknown_kid");
418
+ let ephemeralPublic: KeyObject;
419
+ try {
420
+ ephemeralPublic = x25519PublicKey(epk);
421
+ } catch {
422
+ throw new SealError("bad_header");
423
+ }
424
+ const shared = agree(recipient.privateKey, ephemeralPublic);
425
+ if (!shared) throw new SealError("bad_header");
426
+ return {
427
+ chunk,
428
+ kid,
429
+ key: deriveKey(shared, salt, epk, recipient.publicRaw),
430
+ headerHash: createHash("sha256").update(Buffer.concat([magic, rawLength, head])).digest(),
431
+ };
432
+ }
433
+
434
+ /**
435
+ * Opens an ORSEAL01 stream frame by frame (section 6.7). The plaintext is
436
+ * authentic only once the generator returns without throwing: a failing
437
+ * stream can yield a verified prefix first, and a caller must discard
438
+ * everything it received when a SealError is thrown.
439
+ */
440
+ export async function* openSealed(source: AsyncIterable<Uint8Array>, keyring: SealKeyring): AsyncGenerator<Buffer, { kid: string }> {
441
+ const reader = new ByteReader(source);
442
+ try {
443
+ const { chunk, key, headerHash, kid } = await openHeader(reader, keyring);
444
+ for (let index = 0; ; index += 1) {
445
+ const rawLength = await reader.read(4);
446
+ if (rawLength.length < 4) throw new SealError("truncated");
447
+ const length = rawLength.readUInt32BE(0);
448
+ if (length < TAG_LEN || length > chunk + TAG_LEN) throw new SealError("bad_frame_length");
449
+ const frame = await reader.read(length);
450
+ if (frame.length < length) throw new SealError("truncated");
451
+ const more = await reader.more();
452
+ const full = length === chunk + TAG_LEN;
453
+ const final = !full || !more;
454
+ const plaintext = decryptFrame(key, headerHash, index, final, frame);
455
+ if (!plaintext) {
456
+ if (full && decryptFrame(key, headerHash, index, !final, frame)) {
457
+ throw new SealError(final ? "truncated" : "frame_after_final");
458
+ }
459
+ throw new SealError("tag_mismatch");
460
+ }
461
+ yield plaintext;
462
+ if (final) {
463
+ if (more) throw new SealError("frame_after_final");
464
+ return { kid };
465
+ }
466
+ }
467
+ } finally {
468
+ await reader.close();
469
+ }
470
+ }
471
+
472
+ async function* once(bytes: Uint8Array): AsyncGenerator<Uint8Array> {
473
+ yield bytes;
474
+ }
475
+
476
+ /** Opens a whole sealed buffer; throws SealError on any failure. */
477
+ export async function openSealedBuffer(sealed: Uint8Array, keyring: SealKeyring): Promise<Buffer> {
478
+ const pieces: Buffer[] = [];
479
+ for await (const piece of openSealed(once(sealed), keyring)) pieces.push(piece);
480
+ return Buffer.concat(pieces);
481
+ }