@beam-network/sdk 0.5.13 → 0.5.14

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
@@ -110,4 +110,4 @@ NATS requires this guard because the broker rejects messages above its configure
110
110
 
111
111
  ## Restart Recovery
112
112
 
113
- The client keeps one in-memory recovery lease per active transfer and one `runtime.hello` monitor per active shard. The lease is installed before route-stream begin, and a per-transfer lock coalesces initial streaming with replay. Multipart recovery retains the existing upload IDs and compact group state, then re-signs only the expiring route and commit controls. A Runtime epoch change invalidates cached auth, coalesces one `transfer.resume`, and regenerates routes under a fresh generation; a transport-only epoch change replays only when Runtime reports routes missing or expired. Provider credentials and signing inputs are retained only in memory and released on terminal status or `close()`. Low-level `attachSignedUrls` requires `routeGenerationId`, the plan fingerprint/checksum, and a `recoveryFactory`; signed URLs are never journaled to disk. Foreground cancellation, deadline expiry, and Runtime state-loss responses keep the background lease and retained multipart upload alive.
113
+ The client keeps one in-memory recovery lease per active transfer and one `runtime.hello` monitor per active shard. The lease is installed before route-stream begin, and a per-transfer lock coalesces initial streaming with replay. Multipart recovery retains the existing upload IDs and compact group state, then re-signs only the expiring route and commit controls. Each `resumeProviderTransfer` invocation re-prepares an existing transfer by explicit transfer id with a fresh request and route generation, while transport retries within that invocation reuse the same request. It then reattaches route-recovery signing from the current provider configs without creating duplicate multipart uploads. A Runtime epoch change invalidates cached auth, coalesces one `transfer.resume`, and regenerates routes under a fresh generation; a transport-only epoch change replays only when Runtime reports routes missing or expired. Provider credentials and signing inputs are retained only in memory and released on terminal status or `close()`. Low-level `attachSignedUrls` requires `routeGenerationId`, the plan fingerprint/checksum, and a `recoveryFactory`; signed URLs are never journaled to disk. Foreground cancellation, deadline expiry, and Runtime state-loss responses keep the background lease and retained multipart upload alive.
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AttachSignedUrlsResponse, BeamClientOptions, DistributeResponse, MultipartGroupManifest, PlanningHttpSource, PreparedDestination, PreparedHttpSource, ProviderTransferCreateInput, RawTransferCreateInput, SignedChunkRoute, SignedUrlFlow, TransferCancelResponse, TransferCreateResponse, TransferPlanResponse, TransferPrepareResponse, TransferStatusInfo, TransferTerminalSignalWaiter } from "./models.js";
1
+ import type { AttachSignedUrlsResponse, BeamClientOptions, DistributeResponse, MultipartGroupManifest, PlanningHttpSource, PreparedDestination, PreparedHttpSource, ProviderTransferCreateInput, ProviderTransferResumeInput, RawTransferCreateInput, SignedChunkRoute, SignedUrlFlow, TransferCancelResponse, TransferCreateResponse, TransferPlanResponse, TransferPrepareResponse, TransferStatusInfo, TransferTerminalSignalWaiter } from "./models.js";
2
2
  import { BEAM_DEFAULT_NATS_URL } from "./nats-control.js";
3
3
  export declare class BeamRouteRecoveryPendingError extends Error {
4
4
  readonly transferId: string;
@@ -24,6 +24,17 @@ export declare class BeamApiError extends Error {
24
24
  readonly body: string;
25
25
  constructor(message: string, status: number, body: string);
26
26
  }
27
+ type TransferPrepareInput = {
28
+ transferId?: string;
29
+ sources: PreparedHttpSource[];
30
+ destinations: PreparedDestination[];
31
+ name?: string;
32
+ testMode?: boolean;
33
+ urlsExpiresAt?: string;
34
+ signedUrlFlow?: SignedUrlFlow;
35
+ idempotencyKey?: string;
36
+ routeGenerationId?: string;
37
+ };
27
38
  export declare class BeamClient {
28
39
  readonly apiKey: string;
29
40
  readonly natsUrl: string;
@@ -40,6 +51,7 @@ export declare class BeamClient {
40
51
  openTransferTerminalWaiter(transferId: string): Promise<TransferTerminalSignalWaiter>;
41
52
  createRawTransfer(input: RawTransferCreateInput): Promise<TransferCreateResponse>;
42
53
  createTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
54
+ resumeProviderTransfer(input: ProviderTransferResumeInput): Promise<TransferPrepareResponse>;
43
55
  transferStatus(transferId: string): Promise<TransferStatusInfo>;
44
56
  distributeTransfer(transferId: string): Promise<DistributeResponse>;
45
57
  cancelTransfer(transferId: string): Promise<TransferCancelResponse>;
@@ -51,16 +63,8 @@ export declare class BeamClient {
51
63
  urlsExpiresAt?: string;
52
64
  signedUrlFlow?: SignedUrlFlow;
53
65
  }): Promise<TransferPlanResponse>;
54
- prepareTransfer(input: {
55
- sources: PreparedHttpSource[];
56
- destinations: PreparedDestination[];
57
- name?: string;
58
- testMode?: boolean;
59
- urlsExpiresAt?: string;
60
- signedUrlFlow?: SignedUrlFlow;
61
- idempotencyKey?: string;
62
- routeGenerationId?: string;
63
- }): Promise<TransferPrepareResponse>;
66
+ prepareTransfer(input: TransferPrepareInput): Promise<TransferPrepareResponse>;
67
+ private prepareTransferWithRequestKey;
64
68
  attachSignedUrls(transferId: string, input: {
65
69
  chunkRoutes: SignedChunkRoute[];
66
70
  multipartGroupManifest: MultipartGroupManifest[];
@@ -79,6 +83,7 @@ export declare class BeamClient {
79
83
  prepareProviderTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
80
84
  private signProviderRoute;
81
85
  private startProviderRouteRecoverySigner;
86
+ private recoveryMultipartUploadState;
82
87
  private submitIntegrityAuditGrantsIfPresent;
83
88
  private submitProviderIntegrityAuditGrants;
84
89
  private stopRecoverySigner;
package/dist/client.js CHANGED
@@ -111,6 +111,65 @@ export class BeamClient {
111
111
  createTransfer(input) {
112
112
  return this.prepareProviderTransfer(input);
113
113
  }
114
+ async resumeProviderTransfer(input) {
115
+ validateId(input.transferId, "transferId");
116
+ const expiresIn = input.expiresIn ?? 3600;
117
+ const requestedSignedUrlFlow = input.signedUrlFlow ?? "signed_url";
118
+ const retainedRecoveryInput = {
119
+ ...input,
120
+ sources: input.sources.map((source) => ({ ...source })),
121
+ destinations: input.destinations.map((destination) => ({ ...destination }))
122
+ };
123
+ const preparedDestinations = retainedRecoveryInput.destinations.map((destination, index) => prepareProviderDestination(destination, { index }));
124
+ const destinationsById = new Map();
125
+ preparedDestinations.forEach((preparedDestination, index) => {
126
+ const destination = retainedRecoveryInput.destinations[index];
127
+ if (!destination) {
128
+ throw new Error(`missing provider destination config for prepared destination ${preparedDestination.destination_id}`);
129
+ }
130
+ destinationsById.set(preparedDestination.destination_id, destination);
131
+ });
132
+ const preparedSources = await Promise.all(retainedRecoveryInput.sources.map((source, index) => prepareProviderSource(source, { index, expiresIn, fetchImpl: this.fetchImpl })));
133
+ const resumePrepareRequestKey = `transfer:${input.transferId}:prepare:resume:${randomUuid()}`;
134
+ const prepared = await this.prepareTransferWithRequestKey({
135
+ transferId: input.transferId,
136
+ sources: preparedSources,
137
+ destinations: preparedDestinations,
138
+ name: input.name,
139
+ testMode: input.testMode,
140
+ signedUrlFlow: requestedSignedUrlFlow
141
+ }, resumePrepareRequestKey);
142
+ if (!prepared.success)
143
+ return prepared;
144
+ if (prepared.transfer_id !== input.transferId) {
145
+ throw new Error("resumed provider transfer id mismatch");
146
+ }
147
+ const sourcesById = new Map();
148
+ preparedSources.forEach((preparedSource, index) => {
149
+ const source = retainedRecoveryInput.sources[index];
150
+ if (!source) {
151
+ throw new Error(`missing provider source config for prepared source ${preparedSource.source_id}`);
152
+ }
153
+ sourcesById.set(preparedSource.source_id, source);
154
+ });
155
+ await this.startProviderRouteRecoverySigner({
156
+ prepared,
157
+ sourcesById,
158
+ destinationsById,
159
+ multipartUploads: new Map(),
160
+ expiresIn
161
+ });
162
+ this.control.registerRecoveryLease({
163
+ transferId: prepared.transfer_id,
164
+ planFingerprint: prepared.plan_fingerprint,
165
+ coordinateChecksum: prepared.coordinate_checksum,
166
+ replayRoutes: async () => {
167
+ throw new Error("route replay is unavailable after provider action resume; route signing remains available");
168
+ },
169
+ disposeSecrets: () => clearProviderResumeSecrets(retainedRecoveryInput)
170
+ });
171
+ return prepared;
172
+ }
114
173
  async transferStatus(transferId) {
115
174
  validateId(transferId, "transferId");
116
175
  const status = await this.control.request("transfer.status", { transfer_id: transferId }, { transferId });
@@ -151,9 +210,13 @@ export class BeamClient {
151
210
  validateCompactTransferPlan(result.plan_descriptor, result.signed_url_flow);
152
211
  return result;
153
212
  }
154
- async prepareTransfer(input) {
155
- const transferId = await transferIdForIdempotencyKey(input.idempotencyKey);
156
- const prepareIdempotencyKey = `transfer:${transferId}:prepare`;
213
+ prepareTransfer(input) {
214
+ return this.prepareTransferWithRequestKey(input);
215
+ }
216
+ async prepareTransferWithRequestKey(input, requestKey) {
217
+ const transferId = input.transferId ?? await transferIdForIdempotencyKey(input.idempotencyKey);
218
+ validateId(transferId, "transferId");
219
+ const prepareIdempotencyKey = requestKey ?? `transfer:${transferId}:prepare`;
157
220
  const routeGenerationId = input.routeGenerationId ?? await routeGenerationIdForPrepareIdempotencyKey(prepareIdempotencyKey);
158
221
  const result = await this.control.request("transfer.prepare", compact({
159
222
  transfer_id: transferId,
@@ -548,15 +611,15 @@ export class BeamClient {
548
611
  const destination = input.destinationsById.get(requested.destination_id);
549
612
  if (!source || !destination)
550
613
  throw new Error("route recovery provider configuration is unavailable");
551
- const upload = input.multipartUploads.get(requested.multipart_group_id);
552
- if (!upload)
553
- throw new Error(`route recovery multipart upload is unavailable: ${requested.multipart_group_id}`);
554
- if (upload.uploadId !== requested.upload_id
555
- || upload.objectKey !== requested.final_object_key
556
- || upload.manifest.source_id !== requested.source_id
557
- || upload.manifest.destination_id !== requested.destination_id) {
558
- throw new Error("route recovery multipart identity mismatch");
559
- }
614
+ const upload = isHippiusDestination(destination)
615
+ ? undefined
616
+ : await this.recoveryMultipartUploadState({
617
+ prepared: input.prepared,
618
+ destination,
619
+ requested,
620
+ multipartUploads: input.multipartUploads,
621
+ expiresIn: input.expiresIn
622
+ });
560
623
  return this.signProviderRoute({
561
624
  chunk,
562
625
  target: {
@@ -579,7 +642,7 @@ export class BeamClient {
579
642
  source,
580
643
  destination,
581
644
  expiresIn: input.expiresIn,
582
- upload,
645
+ ...(upload ? { upload } : {}),
583
646
  transferId: input.prepared.transfer_id,
584
647
  finalObjectKey: requested.final_object_key,
585
648
  signedUrlFlow: "signed_url",
@@ -604,6 +667,63 @@ export class BeamClient {
604
667
  });
605
668
  });
606
669
  }
670
+ async recoveryMultipartUploadState(input) {
671
+ const existing = input.multipartUploads.get(input.requested.multipart_group_id);
672
+ if (existing) {
673
+ if (existing.uploadId !== input.requested.upload_id
674
+ || existing.objectKey !== input.requested.final_object_key
675
+ || existing.manifest.source_id !== input.requested.source_id
676
+ || existing.manifest.destination_id !== input.requested.destination_id) {
677
+ throw new Error("route recovery multipart identity mismatch");
678
+ }
679
+ return existing;
680
+ }
681
+ if (!input.requested.upload_id) {
682
+ throw new Error("route recovery multipart upload id is required");
683
+ }
684
+ const source = input.prepared.plan_descriptor.sources.find((candidate) => candidate.source_id === input.requested.source_id);
685
+ if (!source) {
686
+ throw new Error("route recovery source plan is unavailable");
687
+ }
688
+ const expectedGroupId = multipartGroupStateKey(input.prepared.transfer_id, input.requested.destination_id, input.requested.source_id, input.requested.final_object_key);
689
+ if (input.requested.multipart_group_id !== expectedGroupId) {
690
+ throw new Error("route recovery multipart group identity mismatch");
691
+ }
692
+ const finalObjectMetadata = { "beam-transfer-id": input.prepared.transfer_id };
693
+ const maxPartNumber = multipartMaxPartNumber(source.chunk_count);
694
+ const listPageMarkers = multipartListPageMarkers(maxPartNumber);
695
+ const [completeUrl, abortUrl, listPageUrls, finalHeadUrl] = await Promise.all([
696
+ signCompleteMultipartUpload(input.destination, input.requested.final_object_key, input.requested.upload_id, input.expiresIn),
697
+ signAbortMultipartUpload(input.destination, input.requested.final_object_key, input.requested.upload_id, input.expiresIn),
698
+ Promise.all(listPageMarkers.map((partNumberMarker) => signListMultipartUpload(input.destination, input.requested.final_object_key, input.requested.upload_id, input.expiresIn, { maxParts: 1_000, ...(partNumberMarker ? { partNumberMarker } : {}) }))),
699
+ signFinalObjectHead(input.destination, input.requested.final_object_key, input.expiresIn)
700
+ ]);
701
+ const manifest = {
702
+ multipart_group_id: input.requested.multipart_group_id,
703
+ source_id: input.requested.source_id,
704
+ destination_id: input.requested.destination_id,
705
+ final_object_key: input.requested.final_object_key,
706
+ upload_id: input.requested.upload_id,
707
+ expected_object_size: source.size,
708
+ expected_part_count: source.chunk_count,
709
+ max_part_number: maxPartNumber,
710
+ complete_url: completeUrl,
711
+ abort_url: abortUrl,
712
+ list_page_urls: listPageUrls,
713
+ final_head_url: finalHeadUrl,
714
+ final_object_metadata: finalObjectMetadata,
715
+ urls_expires_at: expiresAtIso(input.expiresIn)
716
+ };
717
+ validateMultipartGroupManifest([manifest], input.prepared.transfer_id);
718
+ const state = {
719
+ destination: input.destination,
720
+ objectKey: input.requested.final_object_key,
721
+ uploadId: input.requested.upload_id,
722
+ manifest
723
+ };
724
+ input.multipartUploads.set(input.requested.multipart_group_id, state);
725
+ return state;
726
+ }
607
727
  async submitIntegrityAuditGrantsIfPresent(status) {
608
728
  const challenge = status.integrity_audit_challenge;
609
729
  if (!challenge)
@@ -1496,3 +1616,13 @@ function clearRecoverySecrets(input) {
1496
1616
  }
1497
1617
  }
1498
1618
  }
1619
+ function clearProviderResumeSecrets(input) {
1620
+ const secretKeys = new Set(["access_key_id", "secret_access_key", "session_token", "api_token", "token", "password"]);
1621
+ for (const provider of [...input.sources, ...input.destinations]) {
1622
+ const record = provider;
1623
+ for (const key of secretKeys) {
1624
+ if (key in record)
1625
+ record[key] = "";
1626
+ }
1627
+ }
1628
+ }
package/dist/models.d.ts CHANGED
@@ -171,6 +171,18 @@ export interface TransferStatusInfo {
171
171
  };
172
172
  integrity_audit_challenge?: IntegrityAuditChallenge;
173
173
  }
174
+ export interface ProviderTransferResumeInput {
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
+ }
174
186
  export interface TransferTerminalEvent {
175
187
  schema_version: "transfer-client-control/v6";
176
188
  producer: "transfer-runtime";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.5.13",
3
+ "version": "0.5.14",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",