@beam-network/sdk 0.5.17-dev.1 → 0.5.17-dev.2

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 CHANGED
@@ -155,6 +155,10 @@ The client signs up to 64 routes concurrently by default and emits 2,048-route l
155
155
 
156
156
  Callers that need durable recovery accounting can provide `onMultipartGroupReady`. The SDK invokes it after creating a multipart upload and before streaming that group’s signed routes. Its payload contains transfer, source, destination, object-key, upload, size, part-count, and expiry identities only; it never includes credentials, request headers, or signed URLs. A rejected callback aborts the newly created upload and fails preparation closed.
157
157
 
158
+ `resumeProviderTransfer` requires the complete saved callback payloads in `multipartGroups`. It validates every transfer/source/destination/object/size/part coordinate, then uses the same route-stream implementation as creation with those upload IDs. Missing, duplicate, or changed identities fail before new provider uploads or route publication. If a process died before recording an upload identity, treat it as uncertain cleanup instead of inventing a replacement. The `onPrepared` callback also runs during resume, before routes are streamed.
159
+
160
+ Create and resume accept an ownership `signal`. Aborting it stops initial routes, multipart creation, and recovery signing/replay; it does not cancel the transfer or abort a replacement owner's uploads. User cancellation still uses `cancelTransfer` and verified provider cleanup. `prepareProviderSource` accepts the same signal for metadata requests.
161
+
158
162
  The stream ID derives from the transfer, selected flow, and immutable plan identity, and each ordered batch ID includes its route-coordinate checksum. Manual multi-destination attachment requires `delivery_index` on every route. Lifecycle mutations retry transient failures up to three times with the same request identity. A non-recoverable provider setup or route failure raises `BeamProviderTransferError`, whose `errors`, `transferCancelled`, and `multipartCleanupComplete` fields retain the original provider, cancellation, and cleanup outcomes. `waitForTransfer` subscribes to the API-key-owned, at-most-once terminal signal before its first status read and reconciles every signal through authoritative status; subscription failure degrades to the same jittered 15-to-30-second status fallback. Call `close()` when the client is no longer needed; it also closes outstanding terminal waiters.
159
163
 
160
164
  On completed provider transfers, Runtime may include an `integrity_audit_challenge` in status. Before releasing provider signing state, the TypeScript client signs exact read-only source and final-destination GET ranges and submits `transfer.integrity_audit_grants`; audit submission failures are best-effort and do not change transfer completion.
package/dist/client.d.ts CHANGED
@@ -84,6 +84,7 @@ export declare class BeamClient {
84
84
  autoDistribute?: boolean;
85
85
  }): Promise<AttachSignedUrlsResponse>;
86
86
  prepareProviderTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
87
+ private executeProviderTransfer;
87
88
  /**
88
89
  * Negotiate every Hugging Face destination before the plan exists.
89
90
  *
package/dist/client.js CHANGED
@@ -115,62 +115,7 @@ export class BeamClient {
115
115
  }
116
116
  async resumeProviderTransfer(input) {
117
117
  validateId(input.transferId, "transferId");
118
- const expiresIn = input.expiresIn ?? 3600;
119
- const requestedSignedUrlFlow = input.signedUrlFlow ?? "signed_url";
120
- const retainedRecoveryInput = {
121
- ...input,
122
- sources: input.sources.map((source) => ({ ...source })),
123
- destinations: input.destinations.map((destination) => ({ ...destination }))
124
- };
125
- const preparedDestinations = retainedRecoveryInput.destinations.map((destination, index) => prepareProviderDestination(destination, { index }));
126
- const destinationsById = new Map();
127
- preparedDestinations.forEach((preparedDestination, index) => {
128
- const destination = retainedRecoveryInput.destinations[index];
129
- if (!destination) {
130
- throw new Error(`missing provider destination config for prepared destination ${preparedDestination.destination_id}`);
131
- }
132
- destinationsById.set(preparedDestination.destination_id, destination);
133
- });
134
- const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl })));
135
- const resumePrepareRequestKey = `transfer:${input.transferId}:prepare:resume:${randomUuid()}`;
136
- const prepared = await this.prepareTransferWithRequestKey({
137
- transferId: input.transferId,
138
- sources: preparedSources,
139
- destinations: preparedDestinations,
140
- name: input.name,
141
- testMode: input.testMode,
142
- signedUrlFlow: requestedSignedUrlFlow
143
- }, resumePrepareRequestKey);
144
- if (!prepared.success)
145
- return prepared;
146
- if (prepared.transfer_id !== input.transferId) {
147
- throw new Error("resumed provider transfer id mismatch");
148
- }
149
- const sourcesById = new Map();
150
- preparedSources.forEach((preparedSource, index) => {
151
- const source = retainedRecoveryInput.sources[index];
152
- if (!source) {
153
- throw new Error(`missing provider source config for prepared source ${preparedSource.source_id}`);
154
- }
155
- sourcesById.set(preparedSource.source_id, source);
156
- });
157
- await this.startProviderRouteRecoverySigner({
158
- prepared,
159
- sourcesById,
160
- destinationsById,
161
- multipartUploads: new Map(),
162
- expiresIn
163
- });
164
- this.control.registerRecoveryLease({
165
- transferId: prepared.transfer_id,
166
- planFingerprint: prepared.plan_fingerprint,
167
- coordinateChecksum: prepared.coordinate_checksum,
168
- replayRoutes: async () => {
169
- throw new Error("route replay is unavailable after provider action resume; route signing remains available");
170
- },
171
- disposeSecrets: () => clearProviderResumeSecrets(retainedRecoveryInput)
172
- });
173
- return prepared;
118
+ return this.executeProviderTransfer(input, input);
174
119
  }
175
120
  async transferStatus(transferId) {
176
121
  validateId(transferId, "transferId");
@@ -314,6 +259,10 @@ export class BeamClient {
314
259
  }
315
260
  }
316
261
  async prepareProviderTransfer(input) {
262
+ return this.executeProviderTransfer(input);
263
+ }
264
+ async executeProviderTransfer(input, resume) {
265
+ input.signal?.throwIfAborted();
317
266
  const expiresIn = input.expiresIn ?? 3600;
318
267
  const requestedSignedUrlFlow = input.signedUrlFlow ?? "signed_url";
319
268
  const retainedRecoveryInput = {
@@ -335,7 +284,7 @@ export class BeamClient {
335
284
  destinationsById.set(preparedDestination.destination_id, destination);
336
285
  });
337
286
  await input.throwIfCancelled?.();
338
- const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl })));
287
+ const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl, signal: input.signal })));
339
288
  await input.throwIfCancelled?.();
340
289
  const huggingFace = await this.planHuggingFaceUploads({
341
290
  sources: retainedRecoveryInput.sources,
@@ -345,16 +294,22 @@ export class BeamClient {
345
294
  });
346
295
  await input.throwIfCancelled?.();
347
296
  await input.onBeforeTransferPrepare?.();
348
- const prepared = await this.prepareTransfer({
297
+ input.signal?.throwIfAborted();
298
+ const prepareInput = {
349
299
  sources: preparedSources,
350
300
  destinations: preparedDestinations,
351
301
  name: input.name,
352
302
  testMode: input.testMode,
353
303
  chunkSize: huggingFace.chunkSize ?? input.chunkSize,
354
304
  signedUrlFlow: requestedSignedUrlFlow,
355
- idempotencyKey: input.idempotencyKey,
356
- routeGenerationId: input.routeGenerationId
357
- });
305
+ ...(resume ? { transferId: resume.transferId } : { idempotencyKey: input.idempotencyKey, routeGenerationId: input.routeGenerationId })
306
+ };
307
+ const prepared = resume
308
+ ? await this.prepareTransferWithRequestKey(prepareInput, `transfer:${resume.transferId}:prepare:resume:${randomUuid()}`)
309
+ : await this.prepareTransfer(prepareInput);
310
+ if (resume && prepared.transfer_id !== resume.transferId)
311
+ throw new Error("resumed provider transfer id mismatch");
312
+ input.signal?.throwIfAborted();
358
313
  if (!prepared.success) {
359
314
  return prepared;
360
315
  }
@@ -372,9 +327,12 @@ export class BeamClient {
372
327
  sourcesById.set(preparedSource.source_id, source);
373
328
  });
374
329
  const multipartUploads = new Map();
330
+ if (resume)
331
+ restoreProviderMultipartIdentities(prepared, destinationsById, resume.multipartGroups, multipartUploads);
375
332
  const recoveryMultipartUploads = new Map();
376
333
  const autoDistribute = input.distribute !== false;
377
334
  let initialThrowIfCancelled = input.throwIfCancelled;
335
+ const assertOwnership = () => input.signal?.throwIfAborted();
378
336
  const routeStreamLock = new AsyncMutex();
379
337
  const releaseInitialStream = await routeStreamLock.acquire();
380
338
  const streamPreparedRoutes = async (routeGenerationId, recoveryReplay) => {
@@ -387,9 +345,12 @@ export class BeamClient {
387
345
  let multipartManifestTask = null;
388
346
  let multipartGroupWaiters = new Map();
389
347
  let foregroundCancelled = false;
390
- const throwIfCancelled = recoveryReplay || !initialThrowIfCancelled ? undefined : async (transferId) => {
348
+ assertOwnership();
349
+ const throwIfCancelled = async (transferId) => {
391
350
  try {
392
- await initialThrowIfCancelled?.(transferId);
351
+ assertOwnership();
352
+ if (!recoveryReplay)
353
+ await initialThrowIfCancelled?.(transferId);
393
354
  }
394
355
  catch (error) {
395
356
  foregroundCancelled = true;
@@ -424,7 +385,9 @@ export class BeamClient {
424
385
  expiresIn,
425
386
  signedUrlFlow: requestedSignedUrlFlow,
426
387
  concurrency: this.multipartControlConcurrency,
388
+ signal: input.signal,
427
389
  onGroupReady: async (state) => {
390
+ assertOwnership();
428
391
  recoveryMultipartUploads.set(state.manifest.multipart_group_id, state);
429
392
  validateMultipartGroupManifest([state.manifest], prepared.transfer_id);
430
393
  await input.onMultipartGroupReady?.(multipartGroupIdentity(prepared.transfer_id, state.manifest));
@@ -436,6 +399,7 @@ export class BeamClient {
436
399
  const streamNextCompletedRoute = async () => {
437
400
  const settled = await Promise.race([...pendingRoutes].map((pending) => pending.then((route) => ({ pending, route }))));
438
401
  pendingRoutes.delete(settled.pending);
402
+ assertOwnership();
439
403
  await routeStream.addRoute(settled.route);
440
404
  signedInWindow += 1;
441
405
  if (signedInWindow === ROUTE_STREAM_BATCH_ROUTES) {
@@ -493,12 +457,21 @@ export class BeamClient {
493
457
  await streamNextCompletedRoute();
494
458
  const multipartGroupManifest = await multipartManifestTask;
495
459
  validateMultipartGroupManifest(multipartGroupManifest, prepared.transfer_id);
460
+ assertOwnership();
496
461
  const attached = await routeStream.complete();
497
462
  if (!attached.success) {
498
463
  throw new Error(attached.error ?? attached.message ?? "route stream failed");
499
464
  }
500
465
  }
501
466
  catch (error) {
467
+ if (input.signal?.aborted) {
468
+ this.stopRecoverySigner(prepared.transfer_id);
469
+ this.control.releaseRecoveryLease(prepared.transfer_id);
470
+ await Promise.allSettled(pendingRoutes);
471
+ if (multipartManifestTask)
472
+ await Promise.allSettled([multipartManifestTask]);
473
+ throw input.signal.reason;
474
+ }
502
475
  if (routeStream)
503
476
  await routeStream.abort().catch(() => undefined);
504
477
  await Promise.allSettled(pendingRoutes);
@@ -532,8 +505,14 @@ export class BeamClient {
532
505
  sourcesById,
533
506
  destinationsById,
534
507
  multipartUploads: recoveryMultipartUploads,
508
+ signal: input.signal,
535
509
  expiresIn
536
510
  });
511
+ const stopOwnedRecovery = () => {
512
+ this.stopRecoverySigner(prepared.transfer_id);
513
+ this.control.releaseRecoveryLease(prepared.transfer_id);
514
+ };
515
+ input.signal?.addEventListener("abort", stopOwnedRecovery, { once: true });
537
516
  this.control.registerRecoveryLease({
538
517
  transferId: prepared.transfer_id,
539
518
  planFingerprint: prepared.plan_fingerprint,
@@ -541,10 +520,14 @@ export class BeamClient {
541
520
  replayRoutes: async (routeGenerationId) => {
542
521
  await routeStreamLock.run(() => streamPreparedRoutes(routeGenerationId, true));
543
522
  },
544
- disposeSecrets: () => clearRecoverySecrets(retainedRecoveryInput)
523
+ disposeSecrets: () => {
524
+ input.signal?.removeEventListener("abort", stopOwnedRecovery);
525
+ clearRecoverySecrets(retainedRecoveryInput);
526
+ }
545
527
  });
546
528
  try {
547
529
  try {
530
+ assertOwnership();
548
531
  await input.onPrepared?.(prepared);
549
532
  }
550
533
  catch (error) {
@@ -788,9 +771,11 @@ export class BeamClient {
788
771
  async startProviderRouteRecoverySigner(input) {
789
772
  this.stopRecoverySigner(input.prepared.transfer_id);
790
773
  const stop = await this.control.serveRouteRecoverySigner(input.prepared.transfer_id, async (payload) => {
774
+ input.signal?.throwIfAborted();
791
775
  if (payload.route_generation_id.length === 0)
792
776
  throw new Error("route recovery generation is required");
793
777
  const routes = await mapOrderedWithConcurrency(payload.chunks, this.routeSigningConcurrency, async (requested) => {
778
+ input.signal?.throwIfAborted();
794
779
  if (requested.route_generation_id !== payload.route_generation_id) {
795
780
  throw new Error("route recovery chunk generation mismatch");
796
781
  }
@@ -1505,6 +1490,37 @@ function createMultipartGroupWaiters(prepared, destinationsById) {
1505
1490
  }
1506
1491
  return waiters;
1507
1492
  }
1493
+ function restoreProviderMultipartIdentities(prepared, destinations, identities, uploads) {
1494
+ if (!Array.isArray(identities))
1495
+ throw new Error("provider_multipart_recovery_identity_required");
1496
+ const expected = new Map();
1497
+ for (const source of prepared.plan_descriptor.sources) {
1498
+ for (const target of prepared.plan_descriptor.destinations) {
1499
+ const destination = destinations.get(target.destination_id);
1500
+ if (!destination)
1501
+ throw new Error("provider_multipart_recovery_destination_invalid");
1502
+ if (isDirectPutDestination(destination))
1503
+ continue;
1504
+ const objectKey = target.final_object_keys[source.source_id];
1505
+ if (!objectKey)
1506
+ throw new Error("provider_multipart_recovery_object_invalid");
1507
+ const key = multipartGroupStateKey(prepared.transfer_id, target.destination_id, source.source_id, objectKey);
1508
+ expected.set(key, { destination, objectKey, source, destinationId: target.destination_id });
1509
+ }
1510
+ }
1511
+ if (expected.size !== identities.length)
1512
+ throw new Error("provider_multipart_recovery_incomplete");
1513
+ for (const identity of identities) {
1514
+ const group = expected.get(identity.multipartGroupId);
1515
+ if (!group || uploads.has(identity.multipartGroupId) || identity.transferId !== prepared.transfer_id ||
1516
+ identity.sourceId !== group.source.source_id || identity.destinationId !== group.destinationId ||
1517
+ identity.objectKey !== group.objectKey || identity.expectedObjectSize !== group.source.size ||
1518
+ identity.expectedPartCount !== group.source.chunk_count || !identity.uploadId?.trim()) {
1519
+ throw new Error("provider_multipart_recovery_identity_invalid");
1520
+ }
1521
+ uploads.set(identity.multipartGroupId, { destination: group.destination, objectKey: group.objectKey, uploadId: identity.uploadId });
1522
+ }
1523
+ }
1508
1524
  async function createMultipartGroupManifest(input) {
1509
1525
  const groups = input.prepared.plan_descriptor.sources.flatMap((source) => input.prepared.plan_descriptor.destinations.map((destination) => ({ source, destination })));
1510
1526
  const results = await mapOrderedWithConcurrency(groups, input.concurrency, async ({ source, destination }) => {
@@ -1523,11 +1539,13 @@ async function createMultipartGroupManifest(input) {
1523
1539
  }
1524
1540
  multipartGroupId = multipartGroupStateKey(input.prepared.transfer_id, destination.destination_id, source.source_id, finalObjectKey);
1525
1541
  const finalObjectMetadata = { "beam-transfer-id": input.prepared.transfer_id };
1542
+ input.signal?.throwIfAborted();
1526
1543
  const retainedUpload = input.multipartUploads.get(multipartGroupId);
1527
1544
  const uploadId = retainedUpload?.uploadId ?? await createMultipartUpload({
1528
1545
  destination: destinationConfig,
1529
1546
  objectKey: finalObjectKey,
1530
- metadata: finalObjectMetadata
1547
+ metadata: finalObjectMetadata,
1548
+ signal: input.signal
1531
1549
  });
1532
1550
  const createdUpload = !retainedUpload;
1533
1551
  if (createdUpload) {
@@ -1574,7 +1592,7 @@ async function createMultipartGroupManifest(input) {
1574
1592
  return { ok: true, state };
1575
1593
  }
1576
1594
  catch (error) {
1577
- if (createdUpload) {
1595
+ if (createdUpload && !input.signal?.aborted) {
1578
1596
  try {
1579
1597
  await abortMultipartUpload(destinationConfig, finalObjectKey, uploadId);
1580
1598
  input.multipartUploads.delete(multipartGroupId);
@@ -1858,13 +1876,3 @@ function clearRecoverySecrets(input) {
1858
1876
  }
1859
1877
  }
1860
1878
  }
1861
- function clearProviderResumeSecrets(input) {
1862
- const secretKeys = new Set(["access_key_id", "secret_access_key", "session_token", "api_token", "token", "password"]);
1863
- for (const provider of [...input.sources, ...input.destinations]) {
1864
- const record = provider;
1865
- for (const key of secretKeys) {
1866
- if (key in record)
1867
- record[key] = "";
1868
- }
1869
- }
1870
- }
package/dist/models.d.ts CHANGED
@@ -171,19 +171,10 @@ export interface TransferStatusInfo {
171
171
  };
172
172
  integrity_audit_challenge?: IntegrityAuditChallenge;
173
173
  }
174
- export interface ProviderTransferResumeInput {
174
+ export interface ProviderTransferResumeInput extends Omit<ProviderTransferCreateInput, "idempotencyKey" | "routeGenerationId"> {
175
175
  transferId: string;
176
- sources: ProviderSourceConfig[];
177
- destinations: ProviderDestinationConfig[];
178
- name?: string;
179
- /**
180
- * Maps to BeamCore test_mode. Must match the original prepared transfer.
181
- */
182
- testMode?: boolean;
183
- expiresIn?: number;
184
- signedUrlFlow?: SignedUrlFlow;
185
- /** Requested plan chunk size. BeamCore may raise it; the response carries the effective value. */
186
- chunkSize?: number;
176
+ /** Complete durable multipart identities from onMultipartGroupReady; resume never creates replacement uploads. */
177
+ multipartGroups: ProviderMultipartGroupIdentity[];
187
178
  }
188
179
  export interface TransferTerminalEvent {
189
180
  schema_version: "transfer-client-control/v6";
@@ -290,6 +281,8 @@ export declare const HuggingFaceProviderConfig: Readonly<{
290
281
  create(input: HuggingFaceProviderConfigInput): HuggingFaceProviderConfig;
291
282
  }>;
292
283
  export interface ProviderTransferCreateInput {
284
+ /** Ownership fence. Aborting stops signing/replay without cancelling a replacement owner. */
285
+ signal?: AbortSignal;
293
286
  sources: ProviderSourceConfig[];
294
287
  destinations: ProviderDestinationConfig[];
295
288
  name?: string;
@@ -6,6 +6,7 @@ export declare function prepareProviderSource(source: ProviderSourceConfig, opti
6
6
  index?: number;
7
7
  expiresIn?: number;
8
8
  fetchImpl?: typeof fetch;
9
+ signal?: AbortSignal;
9
10
  }): Promise<PreparedHttpSource>;
10
11
  export declare function prepareProviderSourceForPlan(source: ProviderSourceConfig, options?: {
11
12
  index?: number;
@@ -10,10 +10,15 @@ export function expiresAtIso(expiresIn) {
10
10
  export async function prepareProviderSource(source, options = {}) {
11
11
  const index = options.index ?? 0;
12
12
  const expiresIn = options.expiresIn ?? 3600;
13
+ options.signal?.throwIfAborted();
14
+ const baseFetch = options.fetchImpl ?? globalThis.fetch;
15
+ const fetchImpl = options.signal
16
+ ? (input, init) => baseFetch(input, { ...init, signal: AbortSignal.any([options.signal, ...(init?.signal ? [init.signal] : [])]) })
17
+ : baseFetch;
13
18
  if (isS3CompatibleProvider(source)) {
14
19
  const endpoint = s3CompatibleEndpoint(source);
15
20
  const client = createS3CompatibleClient(source, endpoint);
16
- const head = await client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }));
21
+ const head = await client.send(new HeadObjectCommand({ Bucket: source.bucket, Key: source.key }), { abortSignal: options.signal });
17
22
  const getUrl = await getSignedUrl(client, new GetObjectCommand({ Bucket: source.bucket, Key: source.key }), { expiresIn });
18
23
  return {
19
24
  source_id: source.id ?? `src_${index}`,
@@ -33,7 +38,6 @@ export async function prepareProviderSource(source, options = {}) {
33
38
  };
34
39
  }
35
40
  if (isHippiusProvider(source)) {
36
- const fetchImpl = options.fetchImpl ?? globalThis.fetch;
37
41
  const baseUrl = source.base_url ?? "https://api.hippius.com";
38
42
  const size = await hippiusObjectSize(fetchImpl, baseUrl, source.api_token, source.bucket, source.key);
39
43
  const url = await hippiusPresign(fetchImpl, baseUrl, source.api_token, source.bucket, source.key, "get", expiresIn);
@@ -49,7 +53,7 @@ export async function prepareProviderSource(source, options = {}) {
49
53
  };
50
54
  }
51
55
  if (isHuggingFaceProvider(source)) {
52
- const metadata = await huggingFaceFileMetadata(options.fetchImpl ?? globalThis.fetch, source);
56
+ const metadata = await huggingFaceFileMetadata(fetchImpl, source);
53
57
  return {
54
58
  source_id: source.id ?? `src_${index}`,
55
59
  type: "http",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.5.17-dev.1",
3
+ "version": "0.5.17-dev.2",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",