@beam-network/sdk 0.5.17-dev.2 → 0.5.17-dev.4

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
@@ -12,7 +12,7 @@ Example:
12
12
  import { BeamClient, R2ProviderConfig, S3ProviderConfig } from "@beam-network/sdk";
13
13
 
14
14
  const beam = new BeamClient({
15
- apiKey: "bk_your_key"
15
+ apiKey: "b1m_your_key"
16
16
  });
17
17
 
18
18
  const transfer = await beam.createTransfer({
@@ -161,7 +161,7 @@ Create and resume accept an ownership `signal`. Aborting it stops initial routes
161
161
 
162
162
  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.
163
163
 
164
- 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.
164
+ During the pre-completion `integrity_check` phase, Runtime may include an `integrity_audit_challenge` in status. The TypeScript client signs exact read-only source and final-destination GET ranges and submits `transfer.integrity_audit_grants`. S3-compatible source and destination grants use the prepared source ETag and Runtime-verified final-object ETag as `If-Match` conditions. Submission failures retry on the next status poll and appear as a URL-safe `integrity_audit_submission_error` on that status response; the transfer completes with a warning if the check cannot finish by its deadline.
165
165
 
166
166
  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.
167
167
 
package/dist/client.d.ts CHANGED
@@ -56,6 +56,7 @@ export declare class BeamClient {
56
56
  resumeProviderTransfer(input: ProviderTransferResumeInput): Promise<TransferPrepareResponse>;
57
57
  transferStatus(transferId: string): Promise<TransferStatusInfo>;
58
58
  distributeTransfer(transferId: string): Promise<DistributeResponse>;
59
+ private requestTransferCancellation;
59
60
  cancelTransfer(transferId: string): Promise<TransferCancelResponse>;
60
61
  planTransfer(input: {
61
62
  sources: PlanningHttpSource[];
package/dist/client.js CHANGED
@@ -120,8 +120,15 @@ export class BeamClient {
120
120
  async transferStatus(transferId) {
121
121
  validateId(transferId, "transferId");
122
122
  const status = await this.control.request("transfer.status", { transfer_id: transferId }, { transferId });
123
+ if (status.integrity_audit_challenge) {
124
+ try {
125
+ await this.submitIntegrityAuditGrantsIfPresent(status);
126
+ }
127
+ catch (error) {
128
+ status.integrity_audit_submission_error = integrityAuditErrorSummary(error);
129
+ }
130
+ }
123
131
  if (["completed", "failed", "cancelled"].includes(status.status)) {
124
- await this.submitIntegrityAuditGrantsIfPresent(status).catch(() => undefined);
125
132
  this.control.releaseRecoveryLease(transferId);
126
133
  this.stopRecoverySigner(transferId);
127
134
  }
@@ -134,12 +141,15 @@ export class BeamClient {
134
141
  idempotencyKey: `transfer:${transferId}:distribute`
135
142
  });
136
143
  }
137
- async cancelTransfer(transferId) {
144
+ requestTransferCancellation(transferId) {
138
145
  validateId(transferId, "transferId");
139
- const result = await this.control.request("transfer.cancel", { transfer_id: transferId }, {
146
+ return this.control.request("transfer.cancel", { transfer_id: transferId }, {
140
147
  transferId,
141
148
  idempotencyKey: `transfer:${transferId}:cancel`
142
149
  });
150
+ }
151
+ async cancelTransfer(transferId) {
152
+ const result = await this.requestTransferCancellation(transferId);
143
153
  this.control.releaseRecoveryLease(transferId);
144
154
  this.stopRecoverySigner(transferId);
145
155
  return result;
@@ -486,15 +496,22 @@ export class BeamClient {
486
496
  throw new BeamRouteRecoveryPendingError(prepared.transfer_id, error);
487
497
  }
488
498
  else {
489
- this.control.releaseRecoveryLease(prepared.transfer_id);
490
- await cancelAndAbortProviderFailure({
491
- client: this,
492
- transferId: prepared.transfer_id,
493
- cause: error,
494
- uploads: multipartUploads,
495
- abortBeforeCancel: !routeStreamBeginAttempted,
496
- cleanupConcurrency: this.multipartControlConcurrency
497
- });
499
+ try {
500
+ await cancelAndAbortProviderFailure({
501
+ cancelTransfer: () => this.requestTransferCancellation(prepared.transfer_id),
502
+ transferId: prepared.transfer_id,
503
+ cause: error,
504
+ uploads: multipartUploads,
505
+ abortBeforeCancel: !routeStreamBeginAttempted,
506
+ cleanupConcurrency: this.multipartControlConcurrency
507
+ });
508
+ }
509
+ finally {
510
+ // The retained recovery input owns the same destination objects used by
511
+ // multipart cleanup. Releasing it earlier scrubs their scoped credentials
512
+ // before abortMultipartUpload can clean up already-created uploads.
513
+ this.control.releaseRecoveryLease(prepared.transfer_id);
514
+ }
498
515
  }
499
516
  }
500
517
  throw error;
@@ -917,7 +934,7 @@ export class BeamClient {
917
934
  return;
918
935
  const signer = this.integrityAuditSigners.get(challenge.transfer_id);
919
936
  if (!signer)
920
- return;
937
+ throw new Error("integrity audit signer unavailable");
921
938
  const existing = this.integrityAuditSubmissions.get(challenge.audit_id);
922
939
  if (existing) {
923
940
  await existing;
@@ -925,7 +942,13 @@ export class BeamClient {
925
942
  }
926
943
  const submission = signer(challenge);
927
944
  this.integrityAuditSubmissions.set(challenge.audit_id, submission);
928
- await submission;
945
+ try {
946
+ await submission;
947
+ }
948
+ catch (error) {
949
+ this.integrityAuditSubmissions.delete(challenge.audit_id);
950
+ throw error;
951
+ }
929
952
  }
930
953
  async submitProviderIntegrityAuditGrants(input) {
931
954
  if (input.challenge.transfer_id !== input.prepared.transfer_id) {
@@ -948,12 +971,17 @@ export class BeamClient {
948
971
  }
949
972
  const source = input.sourcesById.get(chunk.source_id);
950
973
  const destination = input.destinationsById.get(chunk.destination_id);
974
+ const plannedSource = input.prepared.plan_descriptor.sources.find((candidate) => candidate.source_id === chunk.source_id);
951
975
  if (!source || !destination) {
952
976
  throw new Error("integrity audit provider configuration is unavailable");
953
977
  }
978
+ const sourceEtag = plannedSource?.metadata?.etag;
979
+ const sourceVersionId = plannedSource?.metadata?.version_id;
954
980
  const [sourceGrant, destinationGrant] = await Promise.all([
955
981
  signSourceReadRange({
956
982
  source,
983
+ ...(typeof sourceEtag === "string" ? { ifMatch: sourceEtag } : {}),
984
+ ...(typeof sourceVersionId === "string" ? { versionId: sourceVersionId } : {}),
957
985
  offset: chunk.source_offset,
958
986
  length: chunk.range_length,
959
987
  expiresIn: input.expiresIn,
@@ -962,6 +990,7 @@ export class BeamClient {
962
990
  signDestinationReadRange({
963
991
  destination,
964
992
  objectKey: chunk.final_object_key,
993
+ ...(chunk.final_object_etag ? { ifMatch: chunk.final_object_etag } : {}),
965
994
  offset: chunk.destination_offset,
966
995
  length: chunk.range_length,
967
996
  expiresIn: input.expiresIn,
@@ -1333,6 +1362,15 @@ async function mapOrderedWithConcurrency(values, concurrency, map) {
1333
1362
  await Promise.all(workers);
1334
1363
  return results;
1335
1364
  }
1365
+ function integrityAuditErrorSummary(error) {
1366
+ if (!(error instanceof Error))
1367
+ return "unknown_error";
1368
+ const message = error.message.trim();
1369
+ if (!message || /https?:\/\/|x-amz|password|secret|token|authorization|credential/i.test(message)) {
1370
+ return error.name || "unknown_error";
1371
+ }
1372
+ return message.slice(0, 200);
1373
+ }
1336
1374
  function positiveInteger(value, label) {
1337
1375
  if (!Number.isInteger(value) || value < 1)
1338
1376
  throw new Error(`${label} must be a positive integer.`);
@@ -1393,9 +1431,9 @@ async function sha256Bytes(value) {
1393
1431
  function hex(bytes) {
1394
1432
  return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1395
1433
  }
1396
- async function cancelTransferAfterProviderFailure(client, transferId, cause) {
1434
+ async function cancelTransferAfterProviderFailure(cancelTransfer, transferId, cause) {
1397
1435
  try {
1398
- const result = await client.cancelTransfer(transferId);
1436
+ const result = await cancelTransfer();
1399
1437
  if (!result.success) {
1400
1438
  throw new Error(result.message ?? `Beam rejected cancellation for ${transferId}`);
1401
1439
  }
@@ -1418,7 +1456,7 @@ async function cancelAndAbortProviderFailure(input) {
1418
1456
  }
1419
1457
  }
1420
1458
  try {
1421
- await cancelTransferAfterProviderFailure(input.client, input.transferId, input.cause);
1459
+ await cancelTransferAfterProviderFailure(input.cancelTransfer, input.transferId, input.cause);
1422
1460
  }
1423
1461
  catch (error) {
1424
1462
  cancelError = error;
package/dist/models.d.ts CHANGED
@@ -132,6 +132,7 @@ export interface IntegrityAuditChallengeChunk {
132
132
  destination_offset: number;
133
133
  range_length: number;
134
134
  final_object_key: string;
135
+ final_object_etag: string | null;
135
136
  }
136
137
  export interface IntegrityAuditChallenge {
137
138
  audit_id: string;
@@ -170,6 +171,7 @@ export interface TransferStatusInfo {
170
171
  completedDestinationIds: string[];
171
172
  };
172
173
  integrity_audit_challenge?: IntegrityAuditChallenge;
174
+ integrity_audit_submission_error?: string;
173
175
  }
174
176
  export interface ProviderTransferResumeInput extends Omit<ProviderTransferCreateInput, "idempotencyKey" | "routeGenerationId"> {
175
177
  transferId: string;
@@ -91,6 +91,7 @@ export declare function signSourceReadRange(input: {
91
91
  export declare function signDestinationReadRange(input: {
92
92
  destination: ProviderDestinationConfig;
93
93
  objectKey: string;
94
+ ifMatch?: string;
94
95
  offset: number;
95
96
  length: number;
96
97
  expiresIn: number;
@@ -337,11 +337,14 @@ export async function signDestinationReadRange(input) {
337
337
  url: await getSignedUrl(client, new GetObjectCommand({
338
338
  Bucket: input.destination.bucket,
339
339
  Key: input.objectKey,
340
- Range: range
340
+ Range: range,
341
+ IfMatch: input.ifMatch
341
342
  }), { expiresIn: input.expiresIn }),
342
- headers: { Range: range }
343
+ headers: { Range: range, ...(input.ifMatch ? { "If-Match": input.ifMatch } : {}) }
343
344
  };
344
345
  }
346
+ if (input.ifMatch)
347
+ throw new Error("conditional destination ranges require S3-compatible storage");
345
348
  if (isHippiusProvider(input.destination)) {
346
349
  return {
347
350
  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),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.5.17-dev.2",
3
+ "version": "0.5.17-dev.4",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",