@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.
- package/README.md +7 -1
- package/dist/client.d.ts +11 -6
- package/dist/client.js +273 -427
- package/dist/models.d.ts +14 -40
- package/dist/nats-control.d.ts +72 -0
- package/dist/nats-control.js +227 -0
- package/dist/provider-signing.d.ts +0 -16
- package/dist/provider-signing.js +0 -301
- package/package.json +35 -33
package/dist/provider-signing.js
CHANGED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
}
|