@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 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 (!stop)
598
- return;
599
- stop();
600
- this.recoverySigners.delete(transferId);
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";
@@ -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;
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.5.10",
3
+ "version": "0.5.11",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",