@beam-network/sdk 0.5.21 → 0.6.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 +6 -1
- package/dist/client.js +9 -7
- package/dist/models.d.ts +3 -3
- package/dist/multipart-limits.d.ts +1 -1
- package/dist/multipart-limits.js +3 -3
- package/dist/nats-control.d.ts +10 -2
- package/dist/nats-control.js +2 -1
- package/dist/provider-signing.d.ts +9 -0
- package/dist/provider-signing.js +64 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# BEAM SDK for TypeScript
|
|
2
2
|
|
|
3
|
+
This release requires Core's `transfer-client-control/v7` contract. Multipart uploads use consecutive parts;
|
|
4
|
+
Core may request retained staging for recovery through the existing SDK signing connection. Workers receive
|
|
5
|
+
ordinary upload URLs. Keep the client connected until the transfer reaches a terminal state so Core can renew
|
|
6
|
+
grants, promote recovered data, and clean up staging. Drain active transfers before upgrading Core and SDK consumers.
|
|
7
|
+
|
|
3
8
|
Install:
|
|
4
9
|
|
|
5
10
|
```bash
|
|
@@ -149,7 +154,7 @@ For low-level raw transfer configs, use `createRawTransfer`; lifecycle transport
|
|
|
149
154
|
|
|
150
155
|
## Route Streaming And Payload Size
|
|
151
156
|
|
|
152
|
-
Provider transfers use `transfer-client-control/
|
|
157
|
+
Provider transfers use `transfer-client-control/v7`. Every reply carries Runtime and transport epochs; prepare and every route-stream message carry a UUID route generation. S3, R2, and S3-compatible destinations retain direct multipart UploadPart/ListParts/HEAD handling. Hippius and Hugging Face destinations take a plain PUT per chunk instead, so they carry no multipart group manifest.
|
|
153
158
|
|
|
154
159
|
The client signs up to 64 routes concurrently by default and emits 2,048-route logical batches. Encoded MessagePack requests target 4 MiB physical batches under `maxPayloadBytes` (8 MiB by default; explicit positive overrides remain supported). The splitter reserves control-envelope headroom for the live auth token and stable request identity so the final encoded request remains below the configured guard. Lifecycle control refreshes cached SDK auth before the final 30 seconds of token lifetime and retries `auth_token_expired` replies with the same request identity and payload but a fresh token, preserving Core request-conflict protection during large route streams. A single route above the 4 MiB target but within the configured guard publishes alone; a single route larger than the configured limit fails before publication with both sizes in the error. Multipart create and abort requests use a separate `multipartControlConcurrency` limit of 2 by default, with explicit positive overrides supported, and S3-compatible control requests make up to five attempts for transient provider failures. This keeps URL signing throughput independent from provider control-plane pressure.
|
|
155
160
|
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationReadRange, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signSourceReadRange, isHuggingFaceProvider } from "./provider-signing.js";
|
|
1
|
+
import { signMultipartRecovery, abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationReadRange, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signSourceReadRange, isHuggingFaceProvider } from "./provider-signing.js";
|
|
2
2
|
import { describe as describeHuggingFace, hashSourceStream, huggingFaceCommit, huggingFaceCompleteLfsUpload, huggingFaceLfsBatch, huggingFacePreupload, huggingFaceVerifyLfsUpload, readSourceSample } from "./huggingface.js";
|
|
3
3
|
import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes, isRecoverableRouteStreamError } from "./nats-control.js";
|
|
4
4
|
export class BeamRouteRecoveryPendingError extends Error {
|
|
@@ -821,7 +821,7 @@ export class BeamClient {
|
|
|
821
821
|
multipartUploads: input.multipartUploads,
|
|
822
822
|
expiresIn: input.expiresIn
|
|
823
823
|
});
|
|
824
|
-
|
|
824
|
+
const route = await this.signProviderRoute({
|
|
825
825
|
chunk,
|
|
826
826
|
target: {
|
|
827
827
|
...target,
|
|
@@ -852,6 +852,7 @@ export class BeamClient {
|
|
|
852
852
|
signedUrlFlow: "signed_url",
|
|
853
853
|
partNumber: requested.part_number
|
|
854
854
|
});
|
|
855
|
+
return signMultipartRecovery({ destination, transferId: input.prepared.transfer_id, requested, route, expiresIn: input.expiresIn });
|
|
855
856
|
});
|
|
856
857
|
return {
|
|
857
858
|
transfer_id: payload.transfer_id,
|
|
@@ -1661,7 +1662,7 @@ function multipartListPageMarkers(maxPartNumber) {
|
|
|
1661
1662
|
return markers;
|
|
1662
1663
|
}
|
|
1663
1664
|
function multipartMaxPartNumber(chunkCount) {
|
|
1664
|
-
return multipartPartNumber(positiveInteger(chunkCount, "source chunk_count") - 1
|
|
1665
|
+
return multipartPartNumber(positiveInteger(chunkCount, "source chunk_count") - 1);
|
|
1665
1666
|
}
|
|
1666
1667
|
function validateMultipartPartNumber(partNumber, manifest) {
|
|
1667
1668
|
if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > manifest.max_part_number) {
|
|
@@ -1715,7 +1716,7 @@ const COMPACT_TRANSFER_PLAN_KEYS = new Set([
|
|
|
1715
1716
|
const COMPACT_TRANSFER_PLAN_FORMULAS = {
|
|
1716
1717
|
source_offset: "source_chunk_index * chunk_size",
|
|
1717
1718
|
delivery_index: "chunk_index * destination_count + destination_index",
|
|
1718
|
-
part_number: "source_chunk_index
|
|
1719
|
+
part_number: "source_chunk_index + 1",
|
|
1719
1720
|
route_generation_id: "initial-{chunk_index}-{destination_id}"
|
|
1720
1721
|
};
|
|
1721
1722
|
function validateCompactTransferPlan(descriptor, signedUrlFlow) {
|
|
@@ -1729,7 +1730,7 @@ function validateCompactTransferPlan(descriptor, signedUrlFlow) {
|
|
|
1729
1730
|
if (descriptor.version !== "compact-transfer-plan/v1") {
|
|
1730
1731
|
throw new Error(`BeamCore returned unsupported plan version: ${descriptor.version}`);
|
|
1731
1732
|
}
|
|
1732
|
-
if (descriptor.multipart_attempt_slots !==
|
|
1733
|
+
if (descriptor.multipart_attempt_slots !== 1) {
|
|
1733
1734
|
throw new Error("BeamCore returned unsupported multipart attempt slot count");
|
|
1734
1735
|
}
|
|
1735
1736
|
const formulaKeys = Object.keys(descriptor.formulas);
|
|
@@ -1782,7 +1783,7 @@ function validateMultipartGroupManifest(manifest, transferId) {
|
|
|
1782
1783
|
if (!Number.isInteger(group.expected_part_count) || group.expected_part_count < 1 || group.expected_part_count > 10_000) {
|
|
1783
1784
|
throw new Error(`multipart group ${group.multipart_group_id} has invalid expected_part_count`);
|
|
1784
1785
|
}
|
|
1785
|
-
if (!Number.isInteger(group.max_part_number) || group.max_part_number
|
|
1786
|
+
if (!Number.isInteger(group.max_part_number) || group.max_part_number !== group.expected_part_count || group.max_part_number > 10_000) {
|
|
1786
1787
|
throw new Error(`multipart group ${group.multipart_group_id} has invalid max_part_number`);
|
|
1787
1788
|
}
|
|
1788
1789
|
if (!Number.isSafeInteger(group.expected_object_size) || group.expected_object_size <= 0) {
|
|
@@ -1810,7 +1811,8 @@ const PART_ROUTE_METADATA_KEYS = new Set([
|
|
|
1810
1811
|
"logical_attempt_index",
|
|
1811
1812
|
"attempt_slot",
|
|
1812
1813
|
"route_generation_id",
|
|
1813
|
-
"delivery_index"
|
|
1814
|
+
"delivery_index",
|
|
1815
|
+
"etag_required"
|
|
1814
1816
|
]);
|
|
1815
1817
|
function partRouteMetadata(metadata) {
|
|
1816
1818
|
return Object.fromEntries(Object.entries(metadata ?? {}).filter(([key]) => PART_ROUTE_METADATA_KEYS.has(key)));
|
package/dist/models.d.ts
CHANGED
|
@@ -179,7 +179,7 @@ export interface ProviderTransferResumeInput extends Omit<ProviderTransferCreate
|
|
|
179
179
|
multipartGroups: ProviderMultipartGroupIdentity[];
|
|
180
180
|
}
|
|
181
181
|
export interface TransferTerminalEvent {
|
|
182
|
-
schema_version: "transfer-client-control/
|
|
182
|
+
schema_version: "transfer-client-control/v7";
|
|
183
183
|
producer: "transfer-runtime";
|
|
184
184
|
transfer_id: string;
|
|
185
185
|
status: "completed" | "failed" | "cancelled";
|
|
@@ -377,11 +377,11 @@ export interface CompactTransferPlanDescriptor {
|
|
|
377
377
|
destinations: CompactTransferPlanDestination[];
|
|
378
378
|
logical_chunk_count: number;
|
|
379
379
|
delivery_route_count: number;
|
|
380
|
-
multipart_attempt_slots:
|
|
380
|
+
multipart_attempt_slots: 1;
|
|
381
381
|
formulas: {
|
|
382
382
|
source_offset: "source_chunk_index * chunk_size";
|
|
383
383
|
delivery_index: "chunk_index * destination_count + destination_index";
|
|
384
|
-
part_number: "source_chunk_index
|
|
384
|
+
part_number: "source_chunk_index + 1";
|
|
385
385
|
route_generation_id: "initial-{chunk_index}-{destination_id}";
|
|
386
386
|
};
|
|
387
387
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare const MULTIPART_MAX_PART_NUMBER = 10000;
|
|
2
|
-
export declare const MULTIPART_ATTEMPT_SLOT_COUNT =
|
|
2
|
+
export declare const MULTIPART_ATTEMPT_SLOT_COUNT = 1;
|
|
3
3
|
export declare const MULTIPART_MAX_SOURCE_CHUNKS: number;
|
|
4
4
|
export declare function multipartPartNumber(chunkIndex: number, attemptSlot?: number): number;
|
package/dist/multipart-limits.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const MULTIPART_MAX_PART_NUMBER = 10_000;
|
|
2
|
-
export const MULTIPART_ATTEMPT_SLOT_COUNT =
|
|
2
|
+
export const MULTIPART_ATTEMPT_SLOT_COUNT = 1;
|
|
3
3
|
export const MULTIPART_MAX_SOURCE_CHUNKS = Math.floor(MULTIPART_MAX_PART_NUMBER / MULTIPART_ATTEMPT_SLOT_COUNT);
|
|
4
4
|
export function multipartPartNumber(chunkIndex, attemptSlot = 0) {
|
|
5
5
|
if (!Number.isInteger(chunkIndex) || chunkIndex < 0) {
|
|
@@ -9,8 +9,8 @@ export function multipartPartNumber(chunkIndex, attemptSlot = 0) {
|
|
|
9
9
|
throw new Error(`source_chunk_index must be less than ${MULTIPART_MAX_SOURCE_CHUNKS}`);
|
|
10
10
|
}
|
|
11
11
|
if (!Number.isInteger(attemptSlot) || attemptSlot < 0 || attemptSlot >= MULTIPART_ATTEMPT_SLOT_COUNT) {
|
|
12
|
-
throw new Error("attempt_slot must be 0
|
|
12
|
+
throw new Error("attempt_slot must be 0 for consecutive multipart uploads");
|
|
13
13
|
}
|
|
14
|
-
const partNumber = chunkIndex
|
|
14
|
+
const partNumber = chunkIndex + 1;
|
|
15
15
|
return partNumber;
|
|
16
16
|
}
|
package/dist/nats-control.d.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { type ConnectionOptions } from "nats";
|
|
2
2
|
import type { MultipartGroupManifest, SignedChunkRoute, TransferTerminalSignalWaiter } from "./models.js";
|
|
3
|
-
export declare const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/
|
|
3
|
+
export declare const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v7";
|
|
4
4
|
export declare const BEAM_DEFAULT_NATS_URL = "tls://orch-gateway.b1m.ai:4222";
|
|
5
5
|
export declare const BEAM_DEFAULT_NATS_WS_URL = "wss://orch-gateway.b1m.ai:443";
|
|
6
6
|
export declare const BEAM_DEFAULT_MAX_PAYLOAD_BYTES: number;
|
|
7
7
|
export declare const BEAM_ROUTE_TARGET_PAYLOAD_BYTES: number;
|
|
8
8
|
export declare const ROUTE_RECOVERY_SIGN_MESSAGE_TYPE: "transfer.route_recovery.sign";
|
|
9
9
|
export interface RouteRecoverySignChunk {
|
|
10
|
+
recovery?: {
|
|
11
|
+
operation: "upload" | "controls" | "list" | "delete" | "renew";
|
|
12
|
+
mode: "staged" | "direct";
|
|
13
|
+
attempt_id: string;
|
|
14
|
+
object_key?: string;
|
|
15
|
+
etag?: string;
|
|
16
|
+
continuation_token?: string;
|
|
17
|
+
};
|
|
10
18
|
source_id: string;
|
|
11
19
|
destination_id: string;
|
|
12
20
|
chunk_index: number;
|
|
@@ -14,7 +22,7 @@ export interface RouteRecoverySignChunk {
|
|
|
14
22
|
source_offset: number;
|
|
15
23
|
chunk_size: number;
|
|
16
24
|
logical_attempt_index: number;
|
|
17
|
-
attempt_slot: 0
|
|
25
|
+
attempt_slot: 0;
|
|
18
26
|
part_number: number;
|
|
19
27
|
route_generation_id: string;
|
|
20
28
|
multipart_group_id: string;
|
package/dist/nats-control.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { encode, decode } from "@msgpack/msgpack";
|
|
2
2
|
import { connect } from "nats";
|
|
3
|
-
export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/
|
|
3
|
+
export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v7";
|
|
4
4
|
export const BEAM_DEFAULT_NATS_URL = "tls://orch-gateway.b1m.ai:4222";
|
|
5
5
|
export const BEAM_DEFAULT_NATS_WS_URL = "wss://orch-gateway.b1m.ai:443";
|
|
6
6
|
// NATS enforces max_payload per message. This guard splits signed-route control
|
|
@@ -604,6 +604,7 @@ const ROUTE_ATTEMPT_METADATA_KEYS = new Set([
|
|
|
604
604
|
"part_number",
|
|
605
605
|
"logical_attempt_index",
|
|
606
606
|
"attempt_slot",
|
|
607
|
+
"etag_required",
|
|
607
608
|
"route_generation_id"
|
|
608
609
|
]);
|
|
609
610
|
const ROUTE_IDENTITY_METADATA_KEYS = [
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import type { RouteRecoverySignChunk } from "./nats-control.js";
|
|
1
2
|
import type { ChunkDestinationSigningTarget, ChunkSigningPlanItem, HuggingFaceProviderConfig, PlanningHttpSource, PreparedDestination, PreparedHttpSource, ProviderDestinationConfig, ProviderSourceConfig, R2ProviderConfig, S3CompatibleProviderConfig, S3ProviderConfig, SignedChunkRoute } from "./models.js";
|
|
2
3
|
export type AnyS3CompatibleProviderConfig = S3ProviderConfig | R2ProviderConfig | S3CompatibleProviderConfig;
|
|
4
|
+
/** Core-only grants: the worker receives just the ordinary upload URL. */
|
|
5
|
+
export declare function signMultipartRecovery(input: {
|
|
6
|
+
destination: ProviderDestinationConfig;
|
|
7
|
+
transferId: string;
|
|
8
|
+
requested: RouteRecoverySignChunk;
|
|
9
|
+
route: SignedChunkRoute;
|
|
10
|
+
expiresIn: number;
|
|
11
|
+
}): Promise<SignedChunkRoute>;
|
|
3
12
|
export declare function nowIso(): string;
|
|
4
13
|
export declare function expiresAtIso(expiresIn: number): string;
|
|
5
14
|
export declare function prepareProviderSource(source: ProviderSourceConfig, options?: {
|
package/dist/provider-signing.js
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
import { AbortMultipartUploadCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, GetObjectCommand, HeadObjectCommand, ListPartsCommand, PutObjectCommand, S3Client, UploadPartCommand } from "@aws-sdk/client-s3";
|
|
2
|
+
import { UploadPartCopyCommand, DeleteObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
|
2
3
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
3
4
|
import { huggingFaceEndpoint, huggingFaceFileMetadata, huggingFaceRepoType, huggingFaceRevision } from "./huggingface.js";
|
|
5
|
+
/** Core-only grants: the worker receives just the ordinary upload URL. */
|
|
6
|
+
export async function signMultipartRecovery(input) {
|
|
7
|
+
const { requested: r, destination, expiresIn } = input;
|
|
8
|
+
const request = r.recovery;
|
|
9
|
+
if (request?.operation === "renew") {
|
|
10
|
+
const count = Number(input.route.metadata?.expected_part_count);
|
|
11
|
+
if (!Number.isInteger(count) || count < 1 || count > 10_000)
|
|
12
|
+
throw new Error("invalid multipart count");
|
|
13
|
+
const [complete, abort, head, pages] = await Promise.all([
|
|
14
|
+
signCompleteMultipartUpload(destination, r.final_object_key, r.upload_id, expiresIn),
|
|
15
|
+
signAbortMultipartUpload(destination, r.final_object_key, r.upload_id, expiresIn),
|
|
16
|
+
signFinalObjectHead(destination, r.final_object_key, expiresIn),
|
|
17
|
+
Promise.all(Array.from({ length: Math.ceil(count / 1000) }, (_, i) => signListMultipartUpload(destination, r.final_object_key, r.upload_id, expiresIn, { maxParts: 1000, partNumberMarker: i * 1000 })))
|
|
18
|
+
]);
|
|
19
|
+
return { ...input.route, metadata: { ...input.route.metadata, complete_url: complete, abort_url: abort, final_head_url: head,
|
|
20
|
+
list_page_urls: pages, list_page_url: pages[Math.floor((r.part_number - 1) / 1000)], control_urls_expires_at: expiresAtIso(expiresIn) } };
|
|
21
|
+
}
|
|
22
|
+
if (!request || request.mode === "direct") {
|
|
23
|
+
const metadata = { ...input.route.metadata };
|
|
24
|
+
delete metadata.recovery_staging;
|
|
25
|
+
return { ...input.route, metadata };
|
|
26
|
+
}
|
|
27
|
+
if (!isS3CompatibleProvider(destination))
|
|
28
|
+
throw new Error("multipart recovery requires an S3-compatible destination");
|
|
29
|
+
const prefix = `${r.final_object_key}.beam-recovery/${input.transferId}/${encodeURIComponent(r.multipart_group_id).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)}/`;
|
|
30
|
+
const objectKey = `${prefix}${r.part_number}/${request.attempt_id}`;
|
|
31
|
+
if (request.object_key && request.object_key !== objectKey)
|
|
32
|
+
throw new Error("recovery staging identity mismatch");
|
|
33
|
+
const client = createS3CompatibleClient(destination);
|
|
34
|
+
const sign = (command) => {
|
|
35
|
+
if (command instanceof PutObjectCommand)
|
|
36
|
+
return getSignedUrl(client, command, { expiresIn });
|
|
37
|
+
if (command instanceof HeadObjectCommand)
|
|
38
|
+
return getSignedUrl(client, command, { expiresIn });
|
|
39
|
+
if (command instanceof UploadPartCopyCommand)
|
|
40
|
+
return getSignedUrl(client, command, { expiresIn, unhoistableHeaders: new Set(["x-amz-copy-source", "x-amz-copy-source-if-match"]) });
|
|
41
|
+
if (command instanceof DeleteObjectCommand)
|
|
42
|
+
return getSignedUrl(client, command, { expiresIn });
|
|
43
|
+
return getSignedUrl(client, command, { expiresIn });
|
|
44
|
+
};
|
|
45
|
+
const metadata = { ...input.route.metadata };
|
|
46
|
+
delete metadata.recovery_staging;
|
|
47
|
+
if (request.operation === "list") {
|
|
48
|
+
metadata.recovery_listing = { prefix, url: await sign(new ListObjectsV2Command({ Bucket: destination.bucket, Prefix: prefix, ContinuationToken: request.continuation_token, MaxKeys: 1000 })) };
|
|
49
|
+
return { ...input.route, metadata };
|
|
50
|
+
}
|
|
51
|
+
const params = { Bucket: destination.bucket, Key: objectKey };
|
|
52
|
+
if (request.operation === "delete") {
|
|
53
|
+
metadata.recovery_delete = { object_key: objectKey, url: await sign(new DeleteObjectCommand(params)) };
|
|
54
|
+
return { ...input.route, metadata };
|
|
55
|
+
}
|
|
56
|
+
const copySource = `${encodeURIComponent(destination.bucket)}/${objectKey.split("/").map(encodeURIComponent).join("/")}`;
|
|
57
|
+
// R2 does not promise to enforce copy source conditions. Each attempt has its own object.
|
|
58
|
+
const condition = destination.provider === "s3" && request.etag ? `"${request.etag.replace(/^"|"$/g, "")}"` : undefined;
|
|
59
|
+
const [head, copy, remove] = await Promise.all([
|
|
60
|
+
sign(new HeadObjectCommand(params)),
|
|
61
|
+
sign(new UploadPartCopyCommand({ Bucket: destination.bucket, Key: r.final_object_key, UploadId: r.upload_id, PartNumber: r.part_number, CopySource: copySource, CopySourceIfMatch: condition })),
|
|
62
|
+
sign(new DeleteObjectCommand(params))
|
|
63
|
+
]);
|
|
64
|
+
metadata.recovery_staging = { object_key: objectKey, attempt_id: request.attempt_id, head_url: head, copy_url: copy, delete_url: remove,
|
|
65
|
+
copy_headers: { "x-amz-copy-source": copySource, ...(condition ? { "x-amz-copy-source-if-match": condition } : {}) }, expires_at: expiresAtIso(expiresIn) };
|
|
66
|
+
return { ...input.route, metadata, ...(request.operation === "upload" ? { dest_url: await sign(new PutObjectCommand(params)) } : {}) };
|
|
67
|
+
}
|
|
4
68
|
export function nowIso() {
|
|
5
69
|
return new Date().toISOString();
|
|
6
70
|
}
|