@beam-network/sdk 0.2.15 → 0.2.17

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.
@@ -1,10 +1,8 @@
1
- import { createHash } from "node:crypto";
2
1
  import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCopyCommand, UploadPartCommand } from "@aws-sdk/client-s3";
3
2
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
3
  export function nowIso() {
5
4
  return new Date().toISOString();
6
5
  }
7
- const DEFAULT_CHECKSUM_READ_TIMEOUT_MS = 300_000;
8
6
  export function expiresAtIso(expiresIn) {
9
7
  return new Date(Date.now() + expiresIn * 1000).toISOString();
10
8
  }
@@ -228,7 +226,6 @@ export async function signDestinationRoute(input) {
228
226
  partNumber: input.partNumber,
229
227
  useStagingPut: input.commitMethod === "upload_part_copy",
230
228
  fetchImpl: input.fetchImpl,
231
- contentMd5: input.contentMd5
232
229
  });
233
230
  const sourceRoute = await signSourceRoute({
234
231
  source: input.source,
@@ -247,7 +244,6 @@ export async function signDestinationRoute(input) {
247
244
  chunk_size: input.chunk.chunk_size,
248
245
  expires_at: expiresAtIso(input.expiresIn),
249
246
  headers: sourceRoute.headers,
250
- ...(input.contentMd5 ? { dest_headers: { "Content-MD5": input.contentMd5 } } : {}),
251
247
  metadata: {
252
248
  ...(input.target.metadata ?? {}),
253
249
  ...(input.transferId ? { transfer_id: input.transferId } : {}),
@@ -262,7 +258,6 @@ export async function signDestinationRoute(input) {
262
258
  ...(input.deleteUrl ? { delete_url: input.deleteUrl } : {}),
263
259
  ...(input.uploadId ? { upload_id: input.uploadId } : {}),
264
260
  ...(input.partNumber ? { part_number: input.partNumber } : {}),
265
- ...(input.contentMd5 ? { checksum_algorithm: "content-md5", checksum_value: input.contentMd5 } : {}),
266
261
  ...(input.partVerifyUrl ? { part_verify_url: input.partVerifyUrl } : {}),
267
262
  ...(input.listPageUrl ? { list_page_url: input.listPageUrl } : {}),
268
263
  ...(input.completeUrl ? { complete_url: input.completeUrl } : {}),
@@ -303,27 +298,6 @@ async function signSourceRoute(input) {
303
298
  }
304
299
  return unsupportedProviderConfig(input.source);
305
300
  }
306
- async function signSourceRangeRoute(input) {
307
- const range = rangeHeaderForRange(input.sourceOffset, input.chunkSize);
308
- if (isS3CompatibleProvider(input.source)) {
309
- const client = createS3CompatibleClient(input.source);
310
- return {
311
- url: await getSignedUrl(client, new GetObjectCommand({
312
- Bucket: input.source.bucket,
313
- Key: input.source.key,
314
- Range: range
315
- }), { expiresIn: input.expiresIn }),
316
- headers: { Range: range }
317
- };
318
- }
319
- if (isHippiusProvider(input.source)) {
320
- return {
321
- url: await hippiusPresign(input.fetchImpl ?? globalThis.fetch, input.source.base_url ?? "https://api.hippius.com", input.source.api_token, input.source.bucket, input.source.key, "get", input.expiresIn),
322
- headers: { Range: range }
323
- };
324
- }
325
- return { url: input.fallbackUrl, headers: { Range: range } };
326
- }
327
301
  async function signDestinationUrl(input) {
328
302
  if (isS3CompatibleProvider(input.destination)) {
329
303
  const client = createS3CompatibleClient(input.destination);
@@ -333,7 +307,6 @@ async function signDestinationUrl(input) {
333
307
  Key: input.uploadPartObjectKey,
334
308
  UploadId: input.uploadId,
335
309
  PartNumber: input.partNumber,
336
- ContentMD5: input.contentMd5
337
310
  }), { expiresIn: input.expiresIn });
338
311
  }
339
312
  return getSignedUrl(client, new PutObjectCommand({ Bucket: input.destination.bucket, Key: input.objectKey }), { expiresIn: input.expiresIn });
@@ -343,280 +316,6 @@ async function signDestinationUrl(input) {
343
316
  }
344
317
  return unsupportedProviderConfig(input.destination);
345
318
  }
346
- export async function computeChunksContentMd5(input) {
347
- const output = new Map();
348
- const chunks = [...input.chunks].sort((a, b) => a.source_offset - b.source_offset || a.source_chunk_index - b.source_chunk_index);
349
- const groups = groupContiguousChunks(chunks, input.groupSizeBytes ?? 256 * 1024 * 1024);
350
- for (const group of groups) {
351
- const groupOutput = await computeChunkGroupContentMd5Adaptive({
352
- source: input.source,
353
- chunks: group,
354
- expiresIn: input.expiresIn,
355
- fetchImpl: input.fetchImpl,
356
- readTimeoutMs: input.readTimeoutMs
357
- });
358
- for (const [sourceChunkIndex, checksum] of groupOutput) {
359
- output.set(sourceChunkIndex, checksum);
360
- }
361
- }
362
- return output;
363
- }
364
- export async function computeChunkContentMd5(input) {
365
- const checksums = await computeChunksContentMd5({
366
- source: input.source,
367
- chunks: [input.chunk],
368
- expiresIn: input.expiresIn,
369
- fetchImpl: input.fetchImpl,
370
- groupSizeBytes: input.chunk.chunk_size,
371
- readTimeoutMs: input.readTimeoutMs
372
- });
373
- const checksum = checksums.get(input.chunk.source_chunk_index);
374
- if (!checksum) {
375
- throw new Error(`chunk checksum missing for source chunk ${input.chunk.source_chunk_index}`);
376
- }
377
- return checksum;
378
- }
379
- function groupContiguousChunks(chunks, maxGroupSizeBytes) {
380
- const groups = [];
381
- let current = [];
382
- let groupStart = 0;
383
- let groupEnd = 0;
384
- const maxBytes = Math.max(1, Math.floor(maxGroupSizeBytes));
385
- for (const chunk of chunks) {
386
- const chunkStart = Math.max(0, Math.floor(chunk.source_offset));
387
- const chunkEnd = chunkStart + Math.max(1, Math.floor(chunk.chunk_size));
388
- const startsNewGroup = current.length === 0 ||
389
- chunkStart !== groupEnd ||
390
- chunkEnd - groupStart > maxBytes;
391
- if (startsNewGroup) {
392
- if (current.length)
393
- groups.push(current);
394
- current = [chunk];
395
- groupStart = chunkStart;
396
- groupEnd = chunkEnd;
397
- continue;
398
- }
399
- current.push(chunk);
400
- groupEnd = chunkEnd;
401
- }
402
- if (current.length)
403
- groups.push(current);
404
- return groups;
405
- }
406
- async function computeChunkGroupContentMd5Adaptive(input) {
407
- try {
408
- return await retryChecksumRead(() => computeChunkGroupContentMd5(input));
409
- }
410
- catch (error) {
411
- if (input.chunks.length <= 1) {
412
- throw error;
413
- }
414
- const midpoint = Math.ceil(input.chunks.length / 2);
415
- const output = new Map();
416
- for (const chunks of [input.chunks.slice(0, midpoint), input.chunks.slice(midpoint)]) {
417
- const partial = await computeChunkGroupContentMd5Adaptive({ ...input, chunks });
418
- for (const [sourceChunkIndex, checksum] of partial) {
419
- output.set(sourceChunkIndex, checksum);
420
- }
421
- }
422
- return output;
423
- }
424
- }
425
- async function computeChunkGroupContentMd5(input) {
426
- if (!input.chunks.length)
427
- return new Map();
428
- const chunks = [...input.chunks].sort((a, b) => a.source_offset - b.source_offset || a.source_chunk_index - b.source_chunk_index);
429
- const first = chunks[0];
430
- const last = chunks[chunks.length - 1];
431
- const sourceOffset = first.source_offset;
432
- const totalSize = last.source_offset + last.chunk_size - sourceOffset;
433
- return withChecksumReadTimeout(input.readTimeoutMs, async (abortSignal) => {
434
- if (isS3CompatibleProvider(input.source)) {
435
- const client = createS3CompatibleClient(input.source);
436
- const response = await client.send(new GetObjectCommand({
437
- Bucket: input.source.bucket,
438
- Key: input.source.key,
439
- Range: `bytes=${sourceOffset}-${sourceOffset + totalSize - 1}`
440
- }), abortSignal ? { abortSignal } : undefined);
441
- if (!response.Body) {
442
- throw new Error(`chunk checksum read returned no body for ${input.source.bucket}/${input.source.key}`);
443
- }
444
- return hashChunkGroupContentMd5(chunks, totalSize, bodyToAsyncBytes(response.Body));
445
- }
446
- const sourceRoute = await signSourceRangeRoute({
447
- source: input.source,
448
- fallbackUrl: first.source_url,
449
- sourceOffset,
450
- chunkSize: totalSize,
451
- expiresIn: input.expiresIn,
452
- fetchImpl: input.fetchImpl
453
- });
454
- const fetchImpl = input.fetchImpl ?? globalThis.fetch;
455
- const response = await fetchImpl(sourceRoute.url, { method: "GET", headers: sourceRoute.headers, signal: abortSignal });
456
- if (!response.ok) {
457
- const message = `chunk checksum fetch failed with ${response.status}: ${await response.text()}`;
458
- throw Object.assign(new Error(message), { status: response.status });
459
- }
460
- if (response.body) {
461
- return hashChunkGroupContentMd5(chunks, totalSize, readableStreamToAsyncBytes(response.body));
462
- }
463
- return hashChunkGroupContentMd5(chunks, totalSize, [new Uint8Array(await response.arrayBuffer())]);
464
- });
465
- }
466
- function normalizedChecksumReadTimeoutMs(value) {
467
- if (value === undefined)
468
- return DEFAULT_CHECKSUM_READ_TIMEOUT_MS;
469
- if (!Number.isFinite(value) || value <= 0)
470
- return 0;
471
- return Math.floor(value);
472
- }
473
- async function withChecksumReadTimeout(readTimeoutMs, task) {
474
- const timeoutMs = normalizedChecksumReadTimeoutMs(readTimeoutMs);
475
- if (timeoutMs <= 0) {
476
- return task(undefined);
477
- }
478
- const controller = new AbortController();
479
- const timeoutError = Object.assign(new Error(`chunk checksum read timed out after ${timeoutMs}ms`), { code: "CHECKSUM_READ_TIMEOUT" });
480
- let timer;
481
- const timeout = new Promise((_resolve, reject) => {
482
- timer = setTimeout(() => {
483
- controller.abort(timeoutError);
484
- reject(timeoutError);
485
- }, timeoutMs);
486
- });
487
- try {
488
- return await Promise.race([task(controller.signal), timeout]);
489
- }
490
- catch (error) {
491
- if (controller.signal.aborted) {
492
- throw timeoutError;
493
- }
494
- throw error;
495
- }
496
- finally {
497
- if (timer)
498
- clearTimeout(timer);
499
- }
500
- }
501
- async function hashChunkGroupContentMd5(chunks, totalSize, body) {
502
- const output = new Map();
503
- let chunkIndex = 0;
504
- let bytesInChunk = 0;
505
- let totalRead = 0;
506
- let hash = createHash("md5");
507
- const consume = (value) => {
508
- let offset = 0;
509
- while (offset < value.byteLength) {
510
- const chunk = chunks[chunkIndex];
511
- if (!chunk) {
512
- throw new Error(`chunk checksum fetch size mismatch: read more than ${totalSize} bytes`);
513
- }
514
- const remainingInChunk = chunk.chunk_size - bytesInChunk;
515
- const take = Math.min(remainingInChunk, value.byteLength - offset);
516
- hash.update(value.subarray(offset, offset + take));
517
- offset += take;
518
- totalRead += take;
519
- bytesInChunk += take;
520
- if (bytesInChunk === chunk.chunk_size) {
521
- output.set(chunk.source_chunk_index, hash.digest("base64"));
522
- chunkIndex += 1;
523
- bytesInChunk = 0;
524
- hash = createHash("md5");
525
- }
526
- }
527
- };
528
- for await (const value of body) {
529
- consume(value);
530
- }
531
- if (totalRead !== totalSize || output.size !== chunks.length) {
532
- throw new Error(`chunk checksum fetch size mismatch: expected ${totalSize}, got ${totalRead}`);
533
- }
534
- return output;
535
- }
536
- async function* readableStreamToAsyncBytes(stream) {
537
- const reader = stream.getReader();
538
- try {
539
- while (true) {
540
- const { done, value } = await reader.read();
541
- if (done)
542
- break;
543
- if (value)
544
- yield value;
545
- }
546
- }
547
- finally {
548
- reader.releaseLock();
549
- }
550
- }
551
- async function* bodyToAsyncBytes(body) {
552
- if (body instanceof Uint8Array) {
553
- yield body;
554
- return;
555
- }
556
- if (typeof body === "string") {
557
- yield new TextEncoder().encode(body);
558
- return;
559
- }
560
- if (body && typeof body.transformToByteArray === "function") {
561
- yield new Uint8Array(await body.transformToByteArray());
562
- return;
563
- }
564
- if (body && typeof body.arrayBuffer === "function") {
565
- yield new Uint8Array(await body.arrayBuffer());
566
- return;
567
- }
568
- if (body && typeof body.getReader === "function") {
569
- yield* readableStreamToAsyncBytes(body);
570
- return;
571
- }
572
- if (body && Symbol.asyncIterator in Object(body)) {
573
- for await (const value of body) {
574
- yield toUint8Array(value);
575
- }
576
- return;
577
- }
578
- throw new Error("provider checksum read returned an unsupported body stream");
579
- }
580
- function toUint8Array(value) {
581
- if (value instanceof Uint8Array)
582
- return value;
583
- if (typeof value === "string")
584
- return new TextEncoder().encode(value);
585
- if (ArrayBuffer.isView(value)) {
586
- return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
587
- }
588
- if (value instanceof ArrayBuffer)
589
- return new Uint8Array(value);
590
- throw new Error("provider checksum stream yielded an unsupported chunk");
591
- }
592
- async function retryChecksumRead(task) {
593
- let lastError;
594
- for (let attempt = 0; attempt < 3; attempt += 1) {
595
- try {
596
- return await task();
597
- }
598
- catch (error) {
599
- lastError = error;
600
- if (isChecksumReadTimeout(error))
601
- break;
602
- const status = typeof error === "object" && error && "status" in error ? Number(error.status) : undefined;
603
- if (status && status >= 400 && status < 500 && status !== 408 && status !== 429)
604
- break;
605
- if (attempt < 2)
606
- await sleep(250 * (attempt + 1));
607
- }
608
- }
609
- throw lastError;
610
- }
611
- function isChecksumReadTimeout(error) {
612
- return Boolean(error &&
613
- typeof error === "object" &&
614
- "code" in error &&
615
- error.code === "CHECKSUM_READ_TIMEOUT");
616
- }
617
- function sleep(ms) {
618
- return new Promise((resolve) => setTimeout(resolve, ms));
619
- }
620
319
  const s3ClientCache = new Map();
621
320
  function s3ClientCacheKey(source, endpoint) {
622
321
  return JSON.stringify({
package/package.json CHANGED
@@ -1,36 +1,38 @@
1
1
  {
2
- "name": "@beam-network/sdk",
3
- "version": "0.2.15",
4
- "description": "TypeScript SDK for BEAM transfer creation and management.",
5
- "type": "module",
6
- "license": "MIT",
7
- "sideEffects": false,
8
- "main": "./dist/index.js",
9
- "types": "./dist/index.d.ts",
10
- "exports": {
11
- ".": {
12
- "types": "./dist/index.d.ts",
13
- "import": "./dist/index.js"
2
+ "name": "@beam-network/sdk",
3
+ "version": "0.2.17",
4
+ "description": "TypeScript SDK for BEAM transfer creation and management.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "test": "tsc -p tsconfig.json --noEmit && npm run test:functional",
23
+ "test:functional": "npm run build && node --test test/client.functional.test.mjs",
24
+ "lint": "tsc -p tsconfig.json --noEmit"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "@aws-sdk/client-s3": "^3.806.0",
31
+ "@aws-sdk/s3-request-presigner": "^3.806.0",
32
+ "@msgpack/msgpack": "^3.0.0",
33
+ "nats": "^2.29.3"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "^5.5.0"
14
37
  }
15
- },
16
- "files": [
17
- "dist",
18
- "README.md"
19
- ],
20
- "scripts": {
21
- "build": "tsc -p tsconfig.json",
22
- "test": "tsc -p tsconfig.json --noEmit && npm run test:functional",
23
- "test:functional": "npm run build && node --test test/*.test.mjs",
24
- "lint": "tsc -p tsconfig.json --noEmit"
25
- },
26
- "publishConfig": {
27
- "access": "public"
28
- },
29
- "dependencies": {
30
- "@aws-sdk/client-s3": "^3.806.0",
31
- "@aws-sdk/s3-request-presigner": "^3.806.0"
32
- },
33
- "devDependencies": {
34
- "typescript": "^5.5.0"
35
- }
36
38
  }