@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 +2 -2
- package/dist/client.d.ts +1 -0
- package/dist/client.js +55 -17
- package/dist/models.d.ts +2 -0
- package/dist/provider-signing.d.ts +1 -0
- package/dist/provider-signing.js +5 -2
- package/package.json +1 -1
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: "
|
|
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
|
-
|
|
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
|
-
|
|
144
|
+
requestTransferCancellation(transferId) {
|
|
138
145
|
validateId(transferId, "transferId");
|
|
139
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1434
|
+
async function cancelTransferAfterProviderFailure(cancelTransfer, transferId, cause) {
|
|
1397
1435
|
try {
|
|
1398
|
-
const result = await
|
|
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.
|
|
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;
|
package/dist/provider-signing.js
CHANGED
|
@@ -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),
|