@beam-network/sdk 0.2.9 → 0.2.10

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/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AttachSignedUrlsResponse, BeamClientOptions, DistributeResponse, PreparedDestination, PreparedHttpSource, ProviderTransferCreateInput, RawTransferCreateInput, SignedChunkRoute, TransferCancelResponse, TransferCreateResponse, TransferPrepareResponse, TransferStatusInfo } from "./models.js";
1
+ import type { AttachSignedUrlsResponse, BeamClientOptions, DistributeResponse, PreparedDestination, PreparedHttpSource, ProviderTransferCreateInput, RawTransferCreateInput, SignedChunkRoute, SignedUrlFlow, TransferCancelResponse, TransferCreateResponse, TransferPrepareResponse, TransferStatusInfo } from "./models.js";
2
2
  export declare const BEAM_DEFAULT_URL = "https://beamcore.b1m.ai";
3
3
  export declare class BeamApiError extends Error {
4
4
  readonly status: number;
@@ -21,6 +21,7 @@ export declare class BeamClient {
21
21
  name?: string;
22
22
  testMode?: boolean;
23
23
  urlsExpiresAt?: string;
24
+ signedUrlFlow?: SignedUrlFlow;
24
25
  }): Promise<TransferPrepareResponse>;
25
26
  attachSignedUrls(transferId: string, input: {
26
27
  chunkRoutes: SignedChunkRoute[];
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { abortMultipartUpload, createMultipartUpload, nowIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDeleteObject, signDestinationRoute, signListMultipartUpload, signUploadPartCopy } from "./provider-signing.js";
1
+ import { abortMultipartUpload, computeChunkContentMd5, createMultipartUpload, nowIso, prepareProviderDestination, prepareProviderSource, isS3CompatibleProvider, listPartsPageMarker, listPartsVerifyMarker, signAbortMultipartUpload, signCompleteMultipartUpload, signDeleteObject, signDestinationRoute, signListMultipartUpload, signUploadPartCopy } from "./provider-signing.js";
2
2
  export const BEAM_DEFAULT_URL = "https://beamcore.b1m.ai";
3
3
  export class BeamApiError extends Error {
4
4
  status;
@@ -36,7 +36,8 @@ export class BeamClient {
36
36
  chunk_hashes: input.chunkHashes,
37
37
  callbacks: input.callbacks,
38
38
  test_mode: input.testMode || undefined,
39
- progressive_mode: input.progressiveMode || undefined
39
+ progressive_mode: input.progressiveMode || undefined,
40
+ signed_url_flow: input.signedUrlFlow
40
41
  });
41
42
  return this.request("POST", "/transfers/create", body);
42
43
  }
@@ -63,7 +64,8 @@ export class BeamClient {
63
64
  destinations: input.destinations,
64
65
  name: input.name,
65
66
  test_mode: input.testMode || undefined,
66
- urls_expires_at: input.urlsExpiresAt
67
+ urls_expires_at: input.urlsExpiresAt,
68
+ signed_url_flow: input.signedUrlFlow
67
69
  }));
68
70
  }
69
71
  attachSignedUrls(transferId, input) {
@@ -83,7 +85,8 @@ export class BeamClient {
83
85
  sources: preparedSources,
84
86
  destinations: preparedDestinations,
85
87
  name: input.name,
86
- testMode: input.testMode
88
+ testMode: input.testMode,
89
+ signedUrlFlow: input.signedUrlFlow
87
90
  });
88
91
  if (!prepared.success) {
89
92
  return prepared;
@@ -98,6 +101,25 @@ export class BeamClient {
98
101
  preparedSources.forEach((preparedSource, index) => {
99
102
  sourcesById.set(preparedSource.source_id, input.sources[index]);
100
103
  });
104
+ const signedUrlFlow = prepared.signed_url_flow ?? input.signedUrlFlow ?? "signed_url_v1";
105
+ const checksumCache = new ChunkChecksumCache({
106
+ concurrency: 4,
107
+ expiresIn,
108
+ fetchImpl: this.fetchImpl
109
+ });
110
+ if (signedUrlFlow === "signed_url_v1") {
111
+ for (const chunk of prepared.chunk_plan) {
112
+ const needsChecksum = chunk.destinations.some((target) => {
113
+ const destination = destinationsById.get(target.destination_id);
114
+ return destination ? isS3CompatibleProvider(destination) : false;
115
+ });
116
+ if (!needsChecksum)
117
+ continue;
118
+ const source = sourcesById.get(chunk.source_id);
119
+ if (source)
120
+ checksumCache.preload(source, chunk);
121
+ }
122
+ }
101
123
  const routes = [];
102
124
  const multipartUploads = new Map();
103
125
  try {
@@ -161,7 +183,9 @@ export class BeamClient {
161
183
  finalObjectKey,
162
184
  partNumber: typeof metadata.part_number === "number"
163
185
  ? metadata.part_number
164
- : chunk.source_chunk_index + 1
186
+ : chunk.source_chunk_index + 1,
187
+ signedUrlFlow,
188
+ checksumCache
165
189
  }));
166
190
  }
167
191
  }
@@ -207,6 +231,32 @@ export class BeamClient {
207
231
  if (!input.target.object_key) {
208
232
  throw new Error("destination signing target is missing object_key");
209
233
  }
234
+ if (input.signedUrlFlow === "signed_url_v1") {
235
+ const contentMd5 = await input.checksumCache.get(input.source, input.chunk);
236
+ const partVerifyUrl = await signListMultipartUpload(input.destination, input.finalObjectKey, input.upload.uploadId, input.expiresIn, { maxParts: 1, partNumberMarker: listPartsVerifyMarker(input.partNumber) });
237
+ const listPageUrl = await signListMultipartUpload(input.destination, input.finalObjectKey, input.upload.uploadId, input.expiresIn, { maxParts: 1000, partNumberMarker: listPartsPageMarker(input.partNumber) });
238
+ return signDestinationRoute({
239
+ chunk: input.chunk,
240
+ target: input.target,
241
+ source: input.source,
242
+ destination: input.destination,
243
+ expiresIn: input.expiresIn,
244
+ uploadId: input.upload.uploadId,
245
+ partNumber: input.partNumber,
246
+ multipartObjectKey: input.finalObjectKey,
247
+ completeUrl: input.upload.completeUrl,
248
+ abortUrl: input.upload.abortUrl,
249
+ listUrl: input.upload.listUrl,
250
+ partVerifyUrl,
251
+ listPageUrl,
252
+ contentMd5,
253
+ transferId: input.transferId,
254
+ multipartGroupId: input.upload.multipartGroupId,
255
+ multipartCreatedAt: input.upload.multipartCreatedAt,
256
+ commitMethod: "upload_part",
257
+ fetchImpl: this.fetchImpl
258
+ });
259
+ }
210
260
  const stagingObjectKey = buildStagingObjectKey({
211
261
  finalObjectKey: input.finalObjectKey,
212
262
  transferId: input.transferId,
@@ -292,6 +342,47 @@ export class BeamClient {
292
342
  return (text ? JSON.parse(text) : {});
293
343
  }
294
344
  }
345
+ class ChunkChecksumCache {
346
+ options;
347
+ queue = [];
348
+ cache = new Map();
349
+ active = 0;
350
+ constructor(options) {
351
+ this.options = options;
352
+ }
353
+ preload(source, chunk) {
354
+ void this.get(source, chunk).catch(() => undefined);
355
+ }
356
+ get(source, chunk) {
357
+ const key = `${chunk.source_id}:${chunk.source_chunk_index}`;
358
+ let existing = this.cache.get(key);
359
+ if (!existing) {
360
+ existing = this.run(() => computeChunkContentMd5({
361
+ source,
362
+ chunk,
363
+ expiresIn: this.options.expiresIn,
364
+ fetchImpl: this.options.fetchImpl
365
+ }));
366
+ this.cache.set(key, existing);
367
+ }
368
+ return existing;
369
+ }
370
+ run(task) {
371
+ return new Promise((resolve, reject) => {
372
+ const start = () => {
373
+ this.active += 1;
374
+ task().then(resolve, reject).finally(() => {
375
+ this.active -= 1;
376
+ this.queue.shift()?.();
377
+ });
378
+ };
379
+ if (this.active < Math.max(1, this.options.concurrency))
380
+ start();
381
+ else
382
+ this.queue.push(start);
383
+ });
384
+ }
385
+ }
295
386
  export function calculateOptimalChunkSize(totalSize) {
296
387
  const mb = 1024 * 1024;
297
388
  const gb = 1024 * mb;
package/dist/models.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export type SignedUrlFlow = "signed_url_v1" | "signed_url_v2";
1
2
  export interface BeamClientOptions {
2
3
  apiKey?: string;
3
4
  baseUrl?: string;
@@ -44,6 +45,7 @@ export interface TransferCreateRequest {
44
45
  callbacks?: CallbackConfig[];
45
46
  test_mode?: boolean;
46
47
  progressive_mode?: boolean;
48
+ signed_url_flow?: SignedUrlFlow;
47
49
  }
48
50
  export interface RawTransferCreateInput {
49
51
  sources: SourceConfig[];
@@ -59,6 +61,7 @@ export interface RawTransferCreateInput {
59
61
  */
60
62
  testMode?: boolean;
61
63
  progressiveMode?: boolean;
64
+ signedUrlFlow?: SignedUrlFlow;
62
65
  }
63
66
  export interface TransferCreateResponse {
64
67
  success: boolean;
@@ -186,6 +189,7 @@ export interface ProviderTransferCreateInput {
186
189
  distribute?: boolean;
187
190
  onPrepared?: (prepared: TransferPrepareResponse) => void | Promise<void>;
188
191
  throwIfCancelled?: (transferId: string) => void | Promise<void>;
192
+ signedUrlFlow?: SignedUrlFlow;
189
193
  }
190
194
  export interface PreparedHttpSource {
191
195
  source_id: string;
@@ -230,6 +234,7 @@ export interface SignedChunkRoute {
230
234
  chunk_size: number;
231
235
  expires_at?: string;
232
236
  headers?: Record<string, string>;
237
+ dest_headers?: Record<string, string>;
233
238
  metadata?: Record<string, unknown>;
234
239
  }
235
240
  export interface TransferPrepareResponse {
@@ -244,6 +249,7 @@ export interface TransferPrepareResponse {
244
249
  logical_chunks?: number;
245
250
  total_chunks?: number;
246
251
  chunk_plan: ChunkSigningPlanItem[];
252
+ signed_url_flow?: SignedUrlFlow;
247
253
  error?: string;
248
254
  message?: string;
249
255
  }
@@ -13,7 +13,12 @@ export declare function prepareProviderDestination(destination: ProviderDestinat
13
13
  export declare function createMultipartUpload(destination: ProviderDestinationConfig, objectKey: string): Promise<string>;
14
14
  export declare function signCompleteMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
15
15
  export declare function signAbortMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
16
- export declare function signListMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
16
+ export declare function signListMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number, options?: {
17
+ maxParts?: number;
18
+ partNumberMarker?: number;
19
+ }): Promise<string>;
20
+ export declare function listPartsPageMarker(partNumber: number, pageSize?: number): number | undefined;
21
+ export declare function listPartsVerifyMarker(partNumber: number): number | undefined;
17
22
  export declare function signUploadPartCopy(destination: ProviderDestinationConfig, finalObjectKey: string, stagingObjectKey: string, uploadId: string, partNumber: number, expiresIn: number, copySourceRange?: string): Promise<{
18
23
  copyUrl: string;
19
24
  copySource: string;
@@ -35,7 +40,10 @@ export declare function signDestinationRoute(input: {
35
40
  transferId?: string;
36
41
  multipartGroupId?: string;
37
42
  multipartCreatedAt?: string;
38
- commitMethod?: "upload_part_copy";
43
+ commitMethod?: "upload_part" | "upload_part_copy";
44
+ contentMd5?: string;
45
+ partVerifyUrl?: string;
46
+ listPageUrl?: string;
39
47
  stagingObjectKey?: string;
40
48
  copyUrl?: string;
41
49
  copySource?: string;
@@ -43,6 +51,12 @@ export declare function signDestinationRoute(input: {
43
51
  deleteUrl?: string;
44
52
  fetchImpl?: typeof fetch;
45
53
  }): Promise<SignedChunkRoute>;
54
+ export declare function computeChunkContentMd5(input: {
55
+ source: ProviderSourceConfig;
56
+ chunk: ChunkSigningPlanItem;
57
+ expiresIn: number;
58
+ fetchImpl?: typeof fetch;
59
+ }): Promise<string>;
46
60
  export declare function isS3CompatibleProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is AnyS3CompatibleProviderConfig;
47
61
  export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
48
62
  export declare function s3CompatibleRegion(source: AnyS3CompatibleProviderConfig): string;
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCopyCommand, UploadPartCommand } from "@aws-sdk/client-s3";
2
3
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
3
4
  export function nowIso() {
@@ -96,13 +97,26 @@ export async function signAbortMultipartUpload(destination, objectKey, uploadId,
96
97
  }
97
98
  throw new Error(`multipart abort is not supported for ${destination.provider}`);
98
99
  }
99
- export async function signListMultipartUpload(destination, objectKey, uploadId, expiresIn) {
100
+ export async function signListMultipartUpload(destination, objectKey, uploadId, expiresIn, options = {}) {
100
101
  if (isS3CompatibleProvider(destination)) {
101
102
  const client = createS3CompatibleClient(destination);
102
- return getSignedUrl(client, new ListPartsCommand({ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId }), { expiresIn });
103
+ return getSignedUrl(client, new ListPartsCommand({
104
+ Bucket: destination.bucket,
105
+ Key: objectKey,
106
+ UploadId: uploadId,
107
+ MaxParts: options.maxParts,
108
+ PartNumberMarker: options.partNumberMarker === undefined ? undefined : String(options.partNumberMarker)
109
+ }), { expiresIn });
103
110
  }
104
111
  throw new Error(`multipart list-parts is not supported for ${destination.provider}`);
105
112
  }
113
+ export function listPartsPageMarker(partNumber, pageSize = 1000) {
114
+ const marker = Math.floor((Math.max(1, partNumber) - 1) / pageSize) * pageSize;
115
+ return marker > 0 ? marker : undefined;
116
+ }
117
+ export function listPartsVerifyMarker(partNumber) {
118
+ return partNumber > 1 ? partNumber - 1 : undefined;
119
+ }
106
120
  export async function signUploadPartCopy(destination, finalObjectKey, stagingObjectKey, uploadId, partNumber, expiresIn, copySourceRange) {
107
121
  if (isS3CompatibleProvider(destination)) {
108
122
  const client = createS3CompatibleClient(destination);
@@ -163,7 +177,8 @@ export async function signDestinationRoute(input) {
163
177
  uploadId: input.uploadId,
164
178
  partNumber: input.partNumber,
165
179
  useStagingPut: input.commitMethod === "upload_part_copy",
166
- fetchImpl: input.fetchImpl
180
+ fetchImpl: input.fetchImpl,
181
+ contentMd5: input.contentMd5
167
182
  });
168
183
  const sourceRoute = await signSourceRoute({
169
184
  source: input.source,
@@ -182,6 +197,7 @@ export async function signDestinationRoute(input) {
182
197
  chunk_size: input.chunk.chunk_size,
183
198
  expires_at: expiresAtIso(input.expiresIn),
184
199
  headers: sourceRoute.headers,
200
+ ...(input.contentMd5 ? { dest_headers: { "Content-MD5": input.contentMd5 } } : {}),
185
201
  metadata: {
186
202
  ...(input.target.metadata ?? {}),
187
203
  ...(input.transferId ? { transfer_id: input.transferId } : {}),
@@ -195,6 +211,9 @@ export async function signDestinationRoute(input) {
195
211
  ...(input.deleteUrl ? { delete_url: input.deleteUrl } : {}),
196
212
  ...(input.uploadId ? { upload_id: input.uploadId } : {}),
197
213
  ...(input.partNumber ? { part_number: input.partNumber } : {}),
214
+ ...(input.contentMd5 ? { checksum_algorithm: "content-md5", checksum_value: input.contentMd5 } : {}),
215
+ ...(input.partVerifyUrl ? { part_verify_url: input.partVerifyUrl } : {}),
216
+ ...(input.listPageUrl ? { list_page_url: input.listPageUrl } : {}),
198
217
  ...(input.completeUrl ? { complete_url: input.completeUrl } : {}),
199
218
  ...(input.abortUrl ? { abort_url: input.abortUrl } : {}),
200
219
  ...(input.listUrl ? { list_url: input.listUrl } : {})
@@ -238,7 +257,8 @@ async function signDestinationUrl(input) {
238
257
  Bucket: input.destination.bucket,
239
258
  Key: input.uploadPartObjectKey,
240
259
  UploadId: input.uploadId,
241
- PartNumber: input.partNumber
260
+ PartNumber: input.partNumber,
261
+ ContentMD5: input.contentMd5
242
262
  }), { expiresIn: input.expiresIn });
243
263
  }
244
264
  return getSignedUrl(client, new PutObjectCommand({ Bucket: input.destination.bucket, Key: input.objectKey }), { expiresIn: input.expiresIn });
@@ -248,6 +268,25 @@ async function signDestinationUrl(input) {
248
268
  }
249
269
  return unsupportedProviderConfig(input.destination);
250
270
  }
271
+ export async function computeChunkContentMd5(input) {
272
+ const sourceRoute = await signSourceRoute({
273
+ source: input.source,
274
+ fallbackUrl: input.chunk.source_url,
275
+ chunk: input.chunk,
276
+ expiresIn: input.expiresIn,
277
+ fetchImpl: input.fetchImpl
278
+ });
279
+ const fetchImpl = input.fetchImpl ?? globalThis.fetch;
280
+ const response = await fetchImpl(sourceRoute.url, { method: "GET", headers: sourceRoute.headers });
281
+ if (!response.ok) {
282
+ throw new Error(`chunk checksum fetch failed with ${response.status}: ${await response.text()}`);
283
+ }
284
+ const body = Buffer.from(await response.arrayBuffer());
285
+ if (body.length !== input.chunk.chunk_size) {
286
+ throw new Error(`chunk checksum fetch size mismatch: expected ${input.chunk.chunk_size}, got ${body.length}`);
287
+ }
288
+ return createHash("md5").update(body).digest("base64");
289
+ }
251
290
  function createS3CompatibleClient(source, endpoint = s3CompatibleEndpoint(source)) {
252
291
  return new S3Client({
253
292
  region: s3CompatibleRegion(source),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",