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

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
@@ -166,3 +166,17 @@ NATS requires this guard because the broker rejects messages above its configure
166
166
  ## Restart Recovery
167
167
 
168
168
  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.
169
+
170
+ ## Hybrid endpoint signing
171
+
172
+ Credential adapters can use `signDestinationUrl` independently of `signDestinationRoute`, using the same provider configuration and multipart signer. Destination-only signing requires no synthetic source descriptor and reads no source data. Optional `contentMd5` binds the worker-computed checksum to the provider upload without a separate source checksum scan.
173
+
174
+ `signSourceReadRange` accepts `ifMatch` and `versionId` for frozen S3-compatible sources. Replay the returned headers unchanged. The ETag condition is signed and the version is included in the signed request. Unsupported non-S3 credential modes reject these conditions instead of dropping them. These helpers support the existing S3-compatible profiles used for R2, Hippius S3 and Hugging Face Storage Buckets; Hub repository tokens are a different provider mode.
175
+
176
+ Only short-lived range/upload routes go to workers. Multipart creation, verification, completion and abort remain in the existing control path. A worker upload response alone is not final-object completion.
177
+
178
+ `listMultipartParts`, `completeMultipartUpload`, and `inspectDestinationObject` reuse the same provider client and return metadata only. ListParts walks every page; completion submits the selected provider-verified parts. Adapters must durably retain the upload and verified manifest before completion so restart recovery cannot infer delivery from size alone.
179
+
180
+ Manual releases from `dev` require a committed `-dev.N` version and publish only to npm tag `dev`; the stable tag is unchanged.
181
+
182
+ Multipart control helpers (`createMultipartUpload`, `listMultipartParts`, `completeMultipartUpload`, `inspectDestinationObject`, and `abortMultipartUpload`) accept an optional `AbortSignal` and cancel through the existing provider HTTP transport. Pass it as `signal` in object arguments or as the last positional argument. Cancellation stops waiting and network activity; it cannot undo a provider operation already accepted. After an interrupted completion, inspect the durable upload/object identity before retrying or reporting cleanup complete.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./client.js";
2
2
  export * from "./models.js";
3
3
  export * from "./provider-signing.js";
4
+ export * from "./multipart-limits.js";
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./client.js";
2
2
  export * from "./models.js";
3
3
  export * from "./provider-signing.js";
4
+ export * from "./multipart-limits.js";
@@ -18,6 +18,7 @@ export declare function createMultipartUpload(input: {
18
18
  destination: ProviderDestinationConfig;
19
19
  objectKey: string;
20
20
  metadata: Record<string, string>;
21
+ signal?: AbortSignal;
21
22
  }): Promise<string>;
22
23
  export declare function signFinalObjectHead(destination: ProviderDestinationConfig, objectKey: string, expiresIn: number): Promise<string>;
23
24
  export declare function signCompleteMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
@@ -26,7 +27,32 @@ export declare function signListMultipartUpload(destination: ProviderDestination
26
27
  maxParts?: number;
27
28
  partNumberMarker?: number;
28
29
  }): Promise<string>;
29
- export declare function abortMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string): Promise<void>;
30
+ export declare function abortMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, signal?: AbortSignal): Promise<void>;
31
+ /** Provider metadata only. These operations never read or proxy object bytes. */
32
+ export declare function listMultipartParts(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, signal?: AbortSignal): Promise<{
33
+ partNumber: number;
34
+ etag: string;
35
+ size: number;
36
+ }[]>;
37
+ export declare function completeMultipartUpload(input: {
38
+ destination: ProviderDestinationConfig;
39
+ objectKey: string;
40
+ uploadId: string;
41
+ parts: {
42
+ partNumber: number;
43
+ etag: string;
44
+ }[];
45
+ signal?: AbortSignal;
46
+ }): Promise<{
47
+ etag: string | undefined;
48
+ versionId: string | undefined;
49
+ }>;
50
+ export declare function inspectDestinationObject(destination: ProviderDestinationConfig, objectKey: string, signal?: AbortSignal): Promise<{
51
+ size: number | undefined;
52
+ etag: string | undefined;
53
+ versionId: string | undefined;
54
+ metadata: Record<string, string>;
55
+ }>;
30
56
  export declare function signDestinationRoute(input: {
31
57
  chunk: ChunkSigningPlanItem;
32
58
  target: ChunkDestinationSigningTarget;
@@ -51,6 +77,8 @@ export declare function signDestinationRoute(input: {
51
77
  }): Promise<SignedChunkRoute>;
52
78
  export declare function signSourceReadRange(input: {
53
79
  source: ProviderSourceConfig;
80
+ ifMatch?: string;
81
+ versionId?: string;
54
82
  offset: number;
55
83
  length: number;
56
84
  expiresIn: number;
@@ -70,6 +98,15 @@ export declare function signDestinationReadRange(input: {
70
98
  url: string;
71
99
  headers: Record<string, string>;
72
100
  }>;
101
+ export declare function signDestinationUrl(input: {
102
+ contentMd5?: string;
103
+ destination: ProviderDestinationConfig;
104
+ objectKey: string;
105
+ uploadId?: string;
106
+ partNumber?: number;
107
+ expiresIn: number;
108
+ fetchImpl?: typeof fetch;
109
+ }): Promise<string>;
73
110
  export declare function isS3CompatibleProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is AnyS3CompatibleProviderConfig;
74
111
  export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
75
112
  export declare function s3CompatibleRegion(source: AnyS3CompatibleProviderConfig): string;
@@ -152,7 +152,7 @@ export async function createMultipartUpload(input) {
152
152
  Bucket: input.destination.bucket,
153
153
  Key: input.objectKey,
154
154
  Metadata: input.metadata
155
- }));
155
+ }), { abortSignal: input.signal });
156
156
  if (!response.UploadId) {
157
157
  throw new Error(`provider did not return UploadId for ${input.objectKey}`);
158
158
  }
@@ -194,13 +194,51 @@ export async function signListMultipartUpload(destination, objectKey, uploadId,
194
194
  }
195
195
  throw new Error(`multipart list-parts is not supported for ${destination.provider}`);
196
196
  }
197
- export async function abortMultipartUpload(destination, objectKey, uploadId) {
197
+ export async function abortMultipartUpload(destination, objectKey, uploadId, signal) {
198
198
  if (isS3CompatibleProvider(destination)) {
199
- await createS3CompatibleClient(destination).send(new AbortMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId }));
199
+ await createS3CompatibleClient(destination).send(new AbortMultipartUploadCommand({ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId }), { abortSignal: signal });
200
200
  return;
201
201
  }
202
202
  throw new Error(`multipart abort is not supported for ${destination.provider}`);
203
203
  }
204
+ /** Provider metadata only. These operations never read or proxy object bytes. */
205
+ export async function listMultipartParts(destination, objectKey, uploadId, signal) {
206
+ if (!isS3CompatibleProvider(destination))
207
+ return unsupportedProviderConfig(destination);
208
+ const parts = [];
209
+ let marker;
210
+ do {
211
+ const page = await createS3CompatibleClient(destination).send(new ListPartsCommand({
212
+ Bucket: destination.bucket, Key: objectKey, UploadId: uploadId, MaxParts: 1000, PartNumberMarker: marker
213
+ }), { abortSignal: signal });
214
+ for (const part of page.Parts ?? []) {
215
+ if (!part.PartNumber || !part.ETag || !Number.isSafeInteger(part.Size))
216
+ throw new Error("invalid multipart part metadata");
217
+ parts.push({ partNumber: part.PartNumber, etag: part.ETag, size: part.Size });
218
+ }
219
+ const next = page.IsTruncated ? page.NextPartNumberMarker : undefined;
220
+ if (page.IsTruncated && (!next || next === marker))
221
+ throw new Error("invalid multipart pagination");
222
+ marker = next;
223
+ } while (marker);
224
+ return parts;
225
+ }
226
+ export async function completeMultipartUpload(input) {
227
+ if (!isS3CompatibleProvider(input.destination))
228
+ return unsupportedProviderConfig(input.destination);
229
+ const result = await createS3CompatibleClient(input.destination).send(new CompleteMultipartUploadCommand({
230
+ Bucket: input.destination.bucket, Key: input.objectKey, UploadId: input.uploadId,
231
+ MultipartUpload: { Parts: [...input.parts].sort((a, b) => a.partNumber - b.partNumber)
232
+ .map((part) => ({ PartNumber: part.partNumber, ETag: part.etag })) }
233
+ }), { abortSignal: input.signal });
234
+ return { etag: result.ETag, versionId: result.VersionId };
235
+ }
236
+ export async function inspectDestinationObject(destination, objectKey, signal) {
237
+ if (!isS3CompatibleProvider(destination))
238
+ return unsupportedProviderConfig(destination);
239
+ const result = await createS3CompatibleClient(destination).send(new HeadObjectCommand({ Bucket: destination.bucket, Key: objectKey }), { abortSignal: signal });
240
+ return { size: result.ContentLength, etag: result.ETag, versionId: result.VersionId, metadata: result.Metadata ?? {} };
241
+ }
204
242
  export async function signDestinationRoute(input) {
205
243
  const targetObjectKey = input.target.object_key;
206
244
  if (!targetObjectKey) {
@@ -266,11 +304,15 @@ export async function signSourceReadRange(input) {
266
304
  url: await getSignedUrl(client, new GetObjectCommand({
267
305
  Bucket: input.source.bucket,
268
306
  Key: input.source.key,
269
- Range: range
307
+ Range: range,
308
+ IfMatch: input.ifMatch,
309
+ VersionId: input.versionId
270
310
  }), { expiresIn: input.expiresIn }),
271
- headers: { Range: range }
311
+ headers: { Range: range, ...(input.ifMatch ? { "If-Match": input.ifMatch } : {}) }
272
312
  };
273
313
  }
314
+ if (input.ifMatch || input.versionId)
315
+ throw new Error("conditional source ranges require S3-compatible storage");
274
316
  if (isHippiusProvider(input.source)) {
275
317
  return {
276
318
  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),
@@ -341,7 +383,7 @@ async function signSourceRoute(input) {
341
383
  }
342
384
  return unsupportedProviderConfig(input.source);
343
385
  }
344
- async function signDestinationUrl(input) {
386
+ export async function signDestinationUrl(input) {
345
387
  if (isS3CompatibleProvider(input.destination)) {
346
388
  const client = createS3CompatibleClient(input.destination);
347
389
  if (input.uploadId && input.partNumber) {
@@ -349,11 +391,14 @@ async function signDestinationUrl(input) {
349
391
  Bucket: input.destination.bucket,
350
392
  Key: input.objectKey,
351
393
  UploadId: input.uploadId,
352
- PartNumber: input.partNumber
394
+ PartNumber: input.partNumber,
395
+ ContentMD5: input.contentMd5
353
396
  }), { expiresIn: input.expiresIn });
354
397
  }
355
- return getSignedUrl(client, new PutObjectCommand({ Bucket: input.destination.bucket, Key: input.objectKey }), { expiresIn: input.expiresIn });
398
+ return getSignedUrl(client, new PutObjectCommand({ Bucket: input.destination.bucket, Key: input.objectKey, ContentMD5: input.contentMd5 }), { expiresIn: input.expiresIn });
356
399
  }
400
+ if (input.contentMd5)
401
+ throw new Error("checksum-bound uploads require S3-compatible storage");
357
402
  if (isHippiusProvider(input.destination)) {
358
403
  return hippiusPresign(input.fetchImpl ?? globalThis.fetch, input.destination.base_url ?? "https://api.hippius.com", input.destination.api_token, input.destination.bucket, input.objectKey, "put", input.expiresIn);
359
404
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.5.16",
3
+ "version": "0.5.17-dev.1",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",