@beam-network/sdk 0.5.16 → 0.5.17-dev.0
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 +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/provider-signing.d.ts +35 -0
- package/dist/provider-signing.js +50 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,3 +166,15 @@ 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.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -27,6 +27,30 @@ export declare function signListMultipartUpload(destination: ProviderDestination
|
|
|
27
27
|
partNumberMarker?: number;
|
|
28
28
|
}): Promise<string>;
|
|
29
29
|
export declare function abortMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string): Promise<void>;
|
|
30
|
+
/** Provider metadata only. These operations never read or proxy object bytes. */
|
|
31
|
+
export declare function listMultipartParts(destination: ProviderDestinationConfig, objectKey: string, uploadId: string): Promise<{
|
|
32
|
+
partNumber: number;
|
|
33
|
+
etag: string;
|
|
34
|
+
size: number;
|
|
35
|
+
}[]>;
|
|
36
|
+
export declare function completeMultipartUpload(input: {
|
|
37
|
+
destination: ProviderDestinationConfig;
|
|
38
|
+
objectKey: string;
|
|
39
|
+
uploadId: string;
|
|
40
|
+
parts: {
|
|
41
|
+
partNumber: number;
|
|
42
|
+
etag: string;
|
|
43
|
+
}[];
|
|
44
|
+
}): Promise<{
|
|
45
|
+
etag: string | undefined;
|
|
46
|
+
versionId: string | undefined;
|
|
47
|
+
}>;
|
|
48
|
+
export declare function inspectDestinationObject(destination: ProviderDestinationConfig, objectKey: string): Promise<{
|
|
49
|
+
size: number | undefined;
|
|
50
|
+
etag: string | undefined;
|
|
51
|
+
versionId: string | undefined;
|
|
52
|
+
metadata: Record<string, string>;
|
|
53
|
+
}>;
|
|
30
54
|
export declare function signDestinationRoute(input: {
|
|
31
55
|
chunk: ChunkSigningPlanItem;
|
|
32
56
|
target: ChunkDestinationSigningTarget;
|
|
@@ -51,6 +75,8 @@ export declare function signDestinationRoute(input: {
|
|
|
51
75
|
}): Promise<SignedChunkRoute>;
|
|
52
76
|
export declare function signSourceReadRange(input: {
|
|
53
77
|
source: ProviderSourceConfig;
|
|
78
|
+
ifMatch?: string;
|
|
79
|
+
versionId?: string;
|
|
54
80
|
offset: number;
|
|
55
81
|
length: number;
|
|
56
82
|
expiresIn: number;
|
|
@@ -70,6 +96,15 @@ export declare function signDestinationReadRange(input: {
|
|
|
70
96
|
url: string;
|
|
71
97
|
headers: Record<string, string>;
|
|
72
98
|
}>;
|
|
99
|
+
export declare function signDestinationUrl(input: {
|
|
100
|
+
contentMd5?: string;
|
|
101
|
+
destination: ProviderDestinationConfig;
|
|
102
|
+
objectKey: string;
|
|
103
|
+
uploadId?: string;
|
|
104
|
+
partNumber?: number;
|
|
105
|
+
expiresIn: number;
|
|
106
|
+
fetchImpl?: typeof fetch;
|
|
107
|
+
}): Promise<string>;
|
|
73
108
|
export declare function isS3CompatibleProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is AnyS3CompatibleProviderConfig;
|
|
74
109
|
export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
|
|
75
110
|
export declare function s3CompatibleRegion(source: AnyS3CompatibleProviderConfig): string;
|
package/dist/provider-signing.js
CHANGED
|
@@ -201,6 +201,44 @@ export async function abortMultipartUpload(destination, objectKey, uploadId) {
|
|
|
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) {
|
|
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
|
+
}));
|
|
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
|
+
}));
|
|
234
|
+
return { etag: result.ETag, versionId: result.VersionId };
|
|
235
|
+
}
|
|
236
|
+
export async function inspectDestinationObject(destination, objectKey) {
|
|
237
|
+
if (!isS3CompatibleProvider(destination))
|
|
238
|
+
return unsupportedProviderConfig(destination);
|
|
239
|
+
const result = await createS3CompatibleClient(destination).send(new HeadObjectCommand({ Bucket: destination.bucket, Key: objectKey }));
|
|
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
|
}
|