@beam-network/sdk 0.2.8 → 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,11 +85,14 @@ 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;
90
93
  }
94
+ await input.onPrepared?.(prepared);
95
+ await input.throwIfCancelled?.(prepared.transfer_id);
91
96
  const destinationsById = new Map();
92
97
  preparedDestinations.forEach((preparedDestination, index) => {
93
98
  destinationsById.set(preparedDestination.destination_id, input.destinations[index]);
@@ -96,10 +101,30 @@ export class BeamClient {
96
101
  preparedSources.forEach((preparedSource, index) => {
97
102
  sourcesById.set(preparedSource.source_id, input.sources[index]);
98
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
+ }
99
123
  const routes = [];
100
124
  const multipartUploads = new Map();
101
125
  try {
102
126
  for (const chunk of prepared.chunk_plan) {
127
+ await input.throwIfCancelled?.(prepared.transfer_id);
103
128
  for (const target of chunk.destinations) {
104
129
  const destination = destinationsById.get(target.destination_id);
105
130
  if (!destination) {
@@ -158,7 +183,9 @@ export class BeamClient {
158
183
  finalObjectKey,
159
184
  partNumber: typeof metadata.part_number === "number"
160
185
  ? metadata.part_number
161
- : chunk.source_chunk_index + 1
186
+ : chunk.source_chunk_index + 1,
187
+ signedUrlFlow,
188
+ checksumCache
162
189
  }));
163
190
  }
164
191
  }
@@ -168,6 +195,7 @@ export class BeamClient {
168
195
  throw error;
169
196
  }
170
197
  try {
198
+ await input.throwIfCancelled?.(prepared.transfer_id);
171
199
  const attached = await this.attachSignedUrls(prepared.transfer_id, {
172
200
  chunkRoutes: routes,
173
201
  transferKey: prepared.transfer_key
@@ -181,6 +209,7 @@ export class BeamClient {
181
209
  throw error;
182
210
  }
183
211
  if (input.distribute !== false) {
212
+ await input.throwIfCancelled?.(prepared.transfer_id);
184
213
  await this.distributeTransfer(prepared.transfer_id);
185
214
  }
186
215
  return prepared;
@@ -202,6 +231,32 @@ export class BeamClient {
202
231
  if (!input.target.object_key) {
203
232
  throw new Error("destination signing target is missing object_key");
204
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
+ }
205
260
  const stagingObjectKey = buildStagingObjectKey({
206
261
  finalObjectKey: input.finalObjectKey,
207
262
  transferId: input.transferId,
@@ -287,6 +342,47 @@ export class BeamClient {
287
342
  return (text ? JSON.parse(text) : {});
288
343
  }
289
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
+ }
290
386
  export function calculateOptimalChunkSize(totalSize) {
291
387
  const mb = 1024 * 1024;
292
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;
@@ -104,20 +107,10 @@ export interface DestinationStatusInfo {
104
107
  }
105
108
  export interface TransferStatusInfo {
106
109
  transfer_id: string;
107
- name?: string;
108
110
  status: string;
109
- total_size: number;
110
- total_chunks: number;
111
- total_sources?: number;
112
- total_destinations?: number;
113
- chunks_completed?: number;
114
- destinations_completed?: number;
115
- sources?: SourceStatusInfo[];
116
- destinations?: DestinationStatusInfo[];
117
- created_at?: string;
118
- started_at?: string;
119
- completed_at?: string;
120
- error_message?: string;
111
+ error_message: string | null;
112
+ started_at: string | null;
113
+ completed_at: string | null;
121
114
  }
122
115
  export interface S3ProviderConfig {
123
116
  provider: "s3";
@@ -194,6 +187,9 @@ export interface ProviderTransferCreateInput {
194
187
  * Defaults to true. Set to false to only prepare and attach signed URLs.
195
188
  */
196
189
  distribute?: boolean;
190
+ onPrepared?: (prepared: TransferPrepareResponse) => void | Promise<void>;
191
+ throwIfCancelled?: (transferId: string) => void | Promise<void>;
192
+ signedUrlFlow?: SignedUrlFlow;
197
193
  }
198
194
  export interface PreparedHttpSource {
199
195
  source_id: string;
@@ -238,6 +234,7 @@ export interface SignedChunkRoute {
238
234
  chunk_size: number;
239
235
  expires_at?: string;
240
236
  headers?: Record<string, string>;
237
+ dest_headers?: Record<string, string>;
241
238
  metadata?: Record<string, unknown>;
242
239
  }
243
240
  export interface TransferPrepareResponse {
@@ -252,6 +249,7 @@ export interface TransferPrepareResponse {
252
249
  logical_chunks?: number;
253
250
  total_chunks?: number;
254
251
  chunk_plan: ChunkSigningPlanItem[];
252
+ signed_url_flow?: SignedUrlFlow;
255
253
  error?: string;
256
254
  message?: string;
257
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.8",
3
+ "version": "0.2.10",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",