@beam-network/sdk 0.5.10 → 0.5.11
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 +2 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +92 -5
- package/dist/models.d.ts +24 -0
- package/dist/nats-control.d.ts +1 -1
- package/dist/provider-signing.d.ts +21 -0
- package/dist/provider-signing.js +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,6 +102,8 @@ The client signs up to 64 routes concurrently by default and emits 2,048-route l
|
|
|
102
102
|
|
|
103
103
|
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.
|
|
104
104
|
|
|
105
|
+
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.
|
|
106
|
+
|
|
105
107
|
Hippius uses canonical non-multipart `signed_url` routes, so its manifest is empty and group-level final HEAD verification is not available from that provider flow.
|
|
106
108
|
|
|
107
109
|
NATS requires this guard because the broker rejects messages above its configured `max_payload`. This limit applies only to lifecycle/control metadata; transfer file bytes do not flow through NATS.
|
package/dist/client.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export declare class BeamClient {
|
|
|
33
33
|
private readonly routeSigningConcurrencyOverridden;
|
|
34
34
|
private readonly multipartControlConcurrency;
|
|
35
35
|
private readonly recoverySigners;
|
|
36
|
+
private readonly integrityAuditSigners;
|
|
37
|
+
private readonly integrityAuditSubmissions;
|
|
36
38
|
constructor(options?: BeamClientOptions);
|
|
37
39
|
close(): Promise<void>;
|
|
38
40
|
openTransferTerminalWaiter(transferId: string): Promise<TransferTerminalSignalWaiter>;
|
|
@@ -77,6 +79,8 @@ export declare class BeamClient {
|
|
|
77
79
|
prepareProviderTransfer(input: ProviderTransferCreateInput): Promise<TransferPrepareResponse>;
|
|
78
80
|
private signProviderRoute;
|
|
79
81
|
private startProviderRouteRecoverySigner;
|
|
82
|
+
private submitIntegrityAuditGrantsIfPresent;
|
|
83
|
+
private submitProviderIntegrityAuditGrants;
|
|
80
84
|
private stopRecoverySigner;
|
|
81
85
|
private stopAllRecoverySigners;
|
|
82
86
|
createAndDistribute(input: RawTransferCreateInput): Promise<TransferCreateResponse>;
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationRoute, signFinalObjectHead, signListMultipartUpload } from "./provider-signing.js";
|
|
1
|
+
import { abortMultipartUpload, createMultipartUpload, expiresAtIso, prepareProviderDestination, prepareProviderSource, signAbortMultipartUpload, signCompleteMultipartUpload, signDestinationReadRange, signDestinationRoute, signFinalObjectHead, signListMultipartUpload, signSourceReadRange } from "./provider-signing.js";
|
|
2
2
|
import { BeamTransferControl, BEAM_DEFAULT_NATS_URL, compactSignedRoutes, isRecoverableRouteStreamError } from "./nats-control.js";
|
|
3
3
|
export class BeamRouteRecoveryPendingError extends Error {
|
|
4
4
|
transferId;
|
|
@@ -53,6 +53,8 @@ export class BeamClient {
|
|
|
53
53
|
routeSigningConcurrencyOverridden;
|
|
54
54
|
multipartControlConcurrency;
|
|
55
55
|
recoverySigners = new Map();
|
|
56
|
+
integrityAuditSigners = new Map();
|
|
57
|
+
integrityAuditSubmissions = new Map();
|
|
56
58
|
constructor(options = {}) {
|
|
57
59
|
if (!options.apiKey?.trim()) {
|
|
58
60
|
throw new Error("apiKey is required.");
|
|
@@ -113,6 +115,7 @@ export class BeamClient {
|
|
|
113
115
|
validateId(transferId, "transferId");
|
|
114
116
|
const status = await this.control.request("transfer.status", { transfer_id: transferId }, { transferId });
|
|
115
117
|
if (["completed", "failed", "cancelled"].includes(status.status)) {
|
|
118
|
+
await this.submitIntegrityAuditGrantsIfPresent(status).catch(() => undefined);
|
|
116
119
|
this.control.releaseRecoveryLease(transferId);
|
|
117
120
|
this.stopRecoverySigner(transferId);
|
|
118
121
|
}
|
|
@@ -591,13 +594,97 @@ export class BeamClient {
|
|
|
591
594
|
};
|
|
592
595
|
});
|
|
593
596
|
this.recoverySigners.set(input.prepared.transfer_id, stop);
|
|
597
|
+
this.integrityAuditSigners.set(input.prepared.transfer_id, async (challenge) => {
|
|
598
|
+
await this.submitProviderIntegrityAuditGrants({
|
|
599
|
+
challenge,
|
|
600
|
+
prepared: input.prepared,
|
|
601
|
+
sourcesById: input.sourcesById,
|
|
602
|
+
destinationsById: input.destinationsById,
|
|
603
|
+
expiresIn: input.expiresIn
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
async submitIntegrityAuditGrantsIfPresent(status) {
|
|
608
|
+
const challenge = status.integrity_audit_challenge;
|
|
609
|
+
if (!challenge)
|
|
610
|
+
return;
|
|
611
|
+
const signer = this.integrityAuditSigners.get(challenge.transfer_id);
|
|
612
|
+
if (!signer)
|
|
613
|
+
return;
|
|
614
|
+
const existing = this.integrityAuditSubmissions.get(challenge.audit_id);
|
|
615
|
+
if (existing) {
|
|
616
|
+
await existing;
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const submission = signer(challenge);
|
|
620
|
+
this.integrityAuditSubmissions.set(challenge.audit_id, submission);
|
|
621
|
+
await submission;
|
|
622
|
+
}
|
|
623
|
+
async submitProviderIntegrityAuditGrants(input) {
|
|
624
|
+
if (input.challenge.transfer_id !== input.prepared.transfer_id) {
|
|
625
|
+
throw new Error("integrity audit challenge transfer mismatch");
|
|
626
|
+
}
|
|
627
|
+
const submittedAt = new Date().toISOString();
|
|
628
|
+
const chunks = await mapOrderedWithConcurrency(input.challenge.chunks, Math.min(this.routeSigningConcurrency, input.challenge.chunks.length || 1), async (chunk) => {
|
|
629
|
+
const planChunk = materializePlanChunk(input.prepared.plan_descriptor, input.prepared.transfer_id, chunk.source_id, chunk.route_chunk_index);
|
|
630
|
+
if (chunk.source_offset < planChunk.source_offset
|
|
631
|
+
|| chunk.source_offset + chunk.range_length > planChunk.source_offset + planChunk.chunk_size) {
|
|
632
|
+
throw new Error("integrity audit source range is outside the planned chunk");
|
|
633
|
+
}
|
|
634
|
+
const target = planChunk.destinations.find((candidate) => candidate.destination_id === chunk.destination_id
|
|
635
|
+
&& Number(candidate.metadata?.delivery_index) === chunk.delivery_index);
|
|
636
|
+
if (!target || target.metadata?.final_object_key !== chunk.final_object_key) {
|
|
637
|
+
throw new Error("integrity audit destination coordinate mismatch");
|
|
638
|
+
}
|
|
639
|
+
if (chunk.destination_offset !== chunk.source_offset) {
|
|
640
|
+
throw new Error("integrity audit destination range mismatch");
|
|
641
|
+
}
|
|
642
|
+
const source = input.sourcesById.get(chunk.source_id);
|
|
643
|
+
const destination = input.destinationsById.get(chunk.destination_id);
|
|
644
|
+
if (!source || !destination) {
|
|
645
|
+
throw new Error("integrity audit provider configuration is unavailable");
|
|
646
|
+
}
|
|
647
|
+
const [sourceGrant, destinationGrant] = await Promise.all([
|
|
648
|
+
signSourceReadRange({
|
|
649
|
+
source,
|
|
650
|
+
offset: chunk.source_offset,
|
|
651
|
+
length: chunk.range_length,
|
|
652
|
+
expiresIn: input.expiresIn,
|
|
653
|
+
fetchImpl: this.fetchImpl
|
|
654
|
+
}),
|
|
655
|
+
signDestinationReadRange({
|
|
656
|
+
destination,
|
|
657
|
+
objectKey: chunk.final_object_key,
|
|
658
|
+
offset: chunk.destination_offset,
|
|
659
|
+
length: chunk.range_length,
|
|
660
|
+
expiresIn: input.expiresIn,
|
|
661
|
+
fetchImpl: this.fetchImpl
|
|
662
|
+
})
|
|
663
|
+
]);
|
|
664
|
+
const grantExpiresAt = expiresAtIso(input.expiresIn);
|
|
665
|
+
return {
|
|
666
|
+
...chunk,
|
|
667
|
+
source: { ...sourceGrant, expires_at: grantExpiresAt },
|
|
668
|
+
destination: { ...destinationGrant, expires_at: grantExpiresAt }
|
|
669
|
+
};
|
|
670
|
+
});
|
|
671
|
+
await this.control.request("transfer.integrity_audit_grants", {
|
|
672
|
+
transfer_id: input.challenge.transfer_id,
|
|
673
|
+
audit_id: input.challenge.audit_id,
|
|
674
|
+
submitted_at: submittedAt,
|
|
675
|
+
chunks
|
|
676
|
+
}, {
|
|
677
|
+
transferId: input.challenge.transfer_id,
|
|
678
|
+
idempotencyKey: `transfer:${input.challenge.transfer_id}:integrity-audit:${input.challenge.audit_id}`
|
|
679
|
+
});
|
|
594
680
|
}
|
|
595
681
|
stopRecoverySigner(transferId) {
|
|
596
682
|
const stop = this.recoverySigners.get(transferId);
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
683
|
+
if (stop) {
|
|
684
|
+
stop();
|
|
685
|
+
this.recoverySigners.delete(transferId);
|
|
686
|
+
}
|
|
687
|
+
this.integrityAuditSigners.delete(transferId);
|
|
601
688
|
}
|
|
602
689
|
stopAllRecoverySigners() {
|
|
603
690
|
for (const transferId of [...this.recoverySigners.keys()])
|
package/dist/models.d.ts
CHANGED
|
@@ -117,6 +117,29 @@ export interface DestinationStatusInfo {
|
|
|
117
117
|
bytes_delivered?: number;
|
|
118
118
|
location?: string;
|
|
119
119
|
}
|
|
120
|
+
export interface IntegrityAuditChallengeChunk {
|
|
121
|
+
challenge_id: string;
|
|
122
|
+
task_id: string;
|
|
123
|
+
attempt_id: string | null;
|
|
124
|
+
orchestrator_id: string;
|
|
125
|
+
orchestrator_hotkey: string;
|
|
126
|
+
worker_id: string | null;
|
|
127
|
+
source_id: string;
|
|
128
|
+
destination_id: string;
|
|
129
|
+
route_chunk_index: number;
|
|
130
|
+
delivery_index: number;
|
|
131
|
+
source_offset: number;
|
|
132
|
+
destination_offset: number;
|
|
133
|
+
range_length: number;
|
|
134
|
+
final_object_key: string;
|
|
135
|
+
}
|
|
136
|
+
export interface IntegrityAuditChallenge {
|
|
137
|
+
audit_id: string;
|
|
138
|
+
transfer_id: string;
|
|
139
|
+
requested_at: string;
|
|
140
|
+
range_bytes: number;
|
|
141
|
+
chunks: IntegrityAuditChallengeChunk[];
|
|
142
|
+
}
|
|
120
143
|
export interface TransferStatusInfo {
|
|
121
144
|
transfer_id: string;
|
|
122
145
|
name?: string;
|
|
@@ -146,6 +169,7 @@ export interface TransferStatusInfo {
|
|
|
146
169
|
completedDestinations: number;
|
|
147
170
|
completedDestinationIds: string[];
|
|
148
171
|
};
|
|
172
|
+
integrity_audit_challenge?: IntegrityAuditChallenge;
|
|
149
173
|
}
|
|
150
174
|
export interface TransferTerminalEvent {
|
|
151
175
|
schema_version: "transfer-client-control/v6";
|
package/dist/nats-control.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface RouteRecoverySignReplyPayload {
|
|
|
37
37
|
signed_at?: string;
|
|
38
38
|
chunk_routes: SignedChunkRoute[];
|
|
39
39
|
}
|
|
40
|
-
export type TransferClientMessageType = "runtime.hello" | "transfer.plan" | "transfer.create" | "transfer.prepare" | "transfer.route_stream.begin" | "transfer.route_stream.manifest" | "transfer.route_stream.batch" | "transfer.route_stream.complete" | "transfer.distribute" | "transfer.status" | "transfer.cancel" | "transfer.resume";
|
|
40
|
+
export type TransferClientMessageType = "runtime.hello" | "transfer.plan" | "transfer.create" | "transfer.prepare" | "transfer.route_stream.begin" | "transfer.route_stream.manifest" | "transfer.route_stream.batch" | "transfer.route_stream.complete" | "transfer.distribute" | "transfer.status" | "transfer.integrity_audit_grants" | "transfer.cancel" | "transfer.resume";
|
|
41
41
|
export interface CompactSignedRouteBatch {
|
|
42
42
|
source_chunks: Array<{
|
|
43
43
|
source_ref: number;
|
|
@@ -47,6 +47,27 @@ export declare function signDestinationRoute(input: {
|
|
|
47
47
|
multipartGroupId?: string;
|
|
48
48
|
fetchImpl?: typeof fetch;
|
|
49
49
|
}): Promise<SignedChunkRoute>;
|
|
50
|
+
export declare function signSourceReadRange(input: {
|
|
51
|
+
source: ProviderSourceConfig;
|
|
52
|
+
offset: number;
|
|
53
|
+
length: number;
|
|
54
|
+
expiresIn: number;
|
|
55
|
+
fetchImpl?: typeof fetch;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
url: string;
|
|
58
|
+
headers: Record<string, string>;
|
|
59
|
+
}>;
|
|
60
|
+
export declare function signDestinationReadRange(input: {
|
|
61
|
+
destination: ProviderDestinationConfig;
|
|
62
|
+
objectKey: string;
|
|
63
|
+
offset: number;
|
|
64
|
+
length: number;
|
|
65
|
+
expiresIn: number;
|
|
66
|
+
fetchImpl?: typeof fetch;
|
|
67
|
+
}): Promise<{
|
|
68
|
+
url: string;
|
|
69
|
+
headers: Record<string, string>;
|
|
70
|
+
}>;
|
|
50
71
|
export declare function isS3CompatibleProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is AnyS3CompatibleProviderConfig;
|
|
51
72
|
export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
|
|
52
73
|
export declare function s3CompatibleRegion(source: AnyS3CompatibleProviderConfig): string;
|
package/dist/provider-signing.js
CHANGED
|
@@ -226,6 +226,48 @@ function rangeHeaderForRange(sourceOffset, chunkSize) {
|
|
|
226
226
|
const size = Math.max(1, Math.floor(chunkSize));
|
|
227
227
|
return `bytes=${start}-${start + size - 1}`;
|
|
228
228
|
}
|
|
229
|
+
export async function signSourceReadRange(input) {
|
|
230
|
+
const range = rangeHeaderForRange(input.offset, input.length);
|
|
231
|
+
if (isS3CompatibleProvider(input.source)) {
|
|
232
|
+
const client = createS3CompatibleClient(input.source);
|
|
233
|
+
return {
|
|
234
|
+
url: await getSignedUrl(client, new GetObjectCommand({
|
|
235
|
+
Bucket: input.source.bucket,
|
|
236
|
+
Key: input.source.key,
|
|
237
|
+
Range: range
|
|
238
|
+
}), { expiresIn: input.expiresIn }),
|
|
239
|
+
headers: { Range: range }
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (isHippiusProvider(input.source)) {
|
|
243
|
+
return {
|
|
244
|
+
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),
|
|
245
|
+
headers: { Range: range }
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return unsupportedProviderConfig(input.source);
|
|
249
|
+
}
|
|
250
|
+
export async function signDestinationReadRange(input) {
|
|
251
|
+
const range = rangeHeaderForRange(input.offset, input.length);
|
|
252
|
+
if (isS3CompatibleProvider(input.destination)) {
|
|
253
|
+
const client = createS3CompatibleClient(input.destination);
|
|
254
|
+
return {
|
|
255
|
+
url: await getSignedUrl(client, new GetObjectCommand({
|
|
256
|
+
Bucket: input.destination.bucket,
|
|
257
|
+
Key: input.objectKey,
|
|
258
|
+
Range: range
|
|
259
|
+
}), { expiresIn: input.expiresIn }),
|
|
260
|
+
headers: { Range: range }
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (isHippiusProvider(input.destination)) {
|
|
264
|
+
return {
|
|
265
|
+
url: await hippiusPresign(input.fetchImpl ?? globalThis.fetch, input.destination.base_url ?? "https://api.hippius.com", input.destination.api_token, input.destination.bucket, input.objectKey, "get", input.expiresIn),
|
|
266
|
+
headers: { Range: range }
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return unsupportedProviderConfig(input.destination);
|
|
270
|
+
}
|
|
229
271
|
async function signSourceRoute(input) {
|
|
230
272
|
const range = rangeHeaderForChunk(input.chunk);
|
|
231
273
|
if (!input.source) {
|