@beam-network/sdk 0.2.15 → 0.2.17

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/dist/models.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  export type SignedUrlFlow = "signed_url_v1" | "signed_url_v2";
2
2
  export interface BeamClientOptions {
3
3
  apiKey?: string;
4
- baseUrl?: string;
4
+ natsUrl?: string;
5
+ natsWsUrl?: string;
6
+ environment?: string;
7
+ transferClientSubjectPrefix?: string;
8
+ transferRuntimeShardCount?: number;
9
+ requestTimeoutMs?: number;
10
+ maxPayloadBytes?: number;
5
11
  fetch?: typeof fetch;
6
12
  }
7
13
  export interface SourceConfig {
@@ -44,7 +50,6 @@ export interface TransferCreateRequest {
44
50
  chunk_hashes?: string[];
45
51
  callbacks?: CallbackConfig[];
46
52
  test_mode?: boolean;
47
- experimental?: boolean;
48
53
  progressive_mode?: boolean;
49
54
  signed_url_flow?: SignedUrlFlow;
50
55
  }
@@ -61,17 +66,13 @@ export interface RawTransferCreateInput {
61
66
  * Maps to BeamCore test_mode.
62
67
  */
63
68
  testMode?: boolean;
64
- /**
65
- * Experimental transfers keep positive evidence but exclude failures and overseer interventions from PRISM.
66
- */
67
- experimental?: boolean;
68
69
  progressiveMode?: boolean;
69
70
  signedUrlFlow?: SignedUrlFlow;
70
71
  }
71
72
  export interface TransferCreateResponse {
72
73
  success: boolean;
73
74
  transfer_id: string;
74
- experimental?: boolean;
75
+ transfer_key?: string;
75
76
  total_chunks: number;
76
77
  total_sources: number;
77
78
  total_destinations: number;
@@ -113,11 +114,15 @@ export interface DestinationStatusInfo {
113
114
  }
114
115
  export interface TransferStatusInfo {
115
116
  transfer_id: string;
117
+ name?: string;
116
118
  status: string;
117
119
  error_message: string | null;
118
- experimental?: boolean;
119
120
  started_at: string | null;
120
121
  completed_at: string | null;
122
+ total_bytes?: number;
123
+ total_size?: number;
124
+ total_chunks?: number;
125
+ chunks_completed?: number;
121
126
  }
122
127
  export interface S3ProviderConfig {
123
128
  provider: "s3";
@@ -164,26 +169,6 @@ export interface HippiusProviderConfig {
164
169
  }
165
170
  export type ProviderConfig = S3ProviderConfig | R2ProviderConfig | S3CompatibleProviderConfig | HippiusProviderConfig;
166
171
  export type ProviderSourceConfig = ProviderConfig;
167
- export interface ChunkChecksumProviderGetInput {
168
- source: ProviderSourceConfig;
169
- chunk: ChunkSigningPlanItem;
170
- algorithm: "content-md5";
171
- }
172
- export interface ChunkChecksumProviderSetInput extends ChunkChecksumProviderGetInput {
173
- contentMd5: string;
174
- }
175
- export interface ChunkChecksumProvider {
176
- getContentMd5(input: ChunkChecksumProviderGetInput): Promise<string | null | undefined>;
177
- setContentMd5?(input: ChunkChecksumProviderSetInput): Promise<void>;
178
- }
179
- export interface ChunkChecksumEvent {
180
- event: "checksum_warmup_started" | "checksum_warmup_completed" | "checksum_cache_hit" | "checksum_cache_miss" | "checksum_cold_chunk_cached" | "checksum_cold_group_completed";
181
- sourceId: string;
182
- sourceChunkIndex?: number;
183
- chunkCount?: number;
184
- bytes?: number;
185
- durationMs?: number;
186
- }
187
172
  export type ProviderDestinationConfig = ProviderConfig;
188
173
  export type S3ProviderConfigInput = Omit<S3ProviderConfig, "provider">;
189
174
  export type R2ProviderConfigInput = Omit<R2ProviderConfig, "provider">;
@@ -209,25 +194,15 @@ export interface ProviderTransferCreateInput {
209
194
  * Maps to BeamCore test_mode.
210
195
  */
211
196
  testMode?: boolean;
212
- /**
213
- * Experimental transfers keep positive evidence but exclude failures and overseer interventions from PRISM.
214
- */
215
- experimental?: boolean;
216
197
  expiresIn?: number;
217
198
  /**
218
- * Defaults to true. Set to false to only prepare and attach signed URLs.
199
+ * Defaults to true. Set to false to only prepare and stream signed routes without distribution.
219
200
  */
220
201
  distribute?: boolean;
221
- onPlanned?: (plan: TransferPlanResponse) => void | Promise<void>;
222
202
  onBeforeTransferPrepare?: () => void | Promise<void>;
223
203
  onPrepared?: (prepared: TransferPrepareResponse) => void | Promise<void>;
224
204
  throwIfCancelled?: (transferId?: string) => void | Promise<void>;
225
205
  signedUrlFlow?: SignedUrlFlow;
226
- checksumProvider?: ChunkChecksumProvider;
227
- checksumReadConcurrency?: number;
228
- checksumGroupSizeBytes?: number;
229
- checksumReadTimeoutMs?: number;
230
- onChecksumEvent?: (event: ChunkChecksumEvent) => void | Promise<void>;
231
206
  }
232
207
  export interface PlanningHttpSource {
233
208
  source_id: string;
@@ -296,7 +271,6 @@ export interface TransferPrepareResponse {
296
271
  transfer_id: string;
297
272
  transfer_key?: string;
298
273
  test_mode?: boolean;
299
- experimental?: boolean;
300
274
  chunk_size?: number;
301
275
  total_size?: number;
302
276
  total_sources?: number;
@@ -0,0 +1,72 @@
1
+ export declare const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v1";
2
+ export declare const BEAM_DEFAULT_NATS_URL = "tls://nats.b1m.ai:4222";
3
+ export declare const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
4
+ export declare const BEAM_DEFAULT_MAX_PAYLOAD_BYTES: number;
5
+ export declare const ROUTE_RECOVERY_SIGN_MESSAGE_TYPE = "transfer.route_recovery.sign";
6
+ export type TransferClientMessageType = "transfer.plan" | "transfer.create" | "transfer.prepare" | "transfer.route_stream.begin" | "transfer.route_stream.batch" | "transfer.route_stream.complete" | "transfer.distribute" | "transfer.status" | "transfer.cancel";
7
+ export interface RouteRecoverySignChunk {
8
+ source_id: string;
9
+ destination_id: string;
10
+ chunk_index: number;
11
+ delivery_index: number;
12
+ source_offset: number;
13
+ chunk_size: number;
14
+ logical_attempt_index: number;
15
+ attempt_slot: 0 | 1 | 2;
16
+ part_number: number;
17
+ route_generation_id: string;
18
+ multipart_group_id: string;
19
+ final_object_key: string;
20
+ upload_id?: string;
21
+ urls_expires_at?: string;
22
+ multipart_created_at?: string;
23
+ source_metadata?: Record<string, unknown>;
24
+ destination_metadata?: Record<string, unknown>;
25
+ }
26
+ export interface RouteRecoverySignRequestPayload {
27
+ transfer_id: string;
28
+ route_generation_id: string;
29
+ requested_at?: string;
30
+ chunks: RouteRecoverySignChunk[];
31
+ }
32
+ export interface RouteRecoverySignReplyPayload {
33
+ transfer_id: string;
34
+ route_generation_id: string;
35
+ signed_at?: string;
36
+ chunk_routes: unknown[];
37
+ }
38
+ export interface TransferControlOptions {
39
+ apiKey: string;
40
+ natsUrl?: string;
41
+ natsWsUrl?: string;
42
+ environment?: string;
43
+ subjectPrefix?: string;
44
+ transferRuntimeShardCount?: number;
45
+ requestTimeoutMs?: number;
46
+ maxPayloadBytes?: number;
47
+ }
48
+ export declare class BeamTransferControl {
49
+ readonly environment: string;
50
+ readonly keyPrefix: string;
51
+ readonly shardCount: number;
52
+ readonly maxPayloadBytes: number;
53
+ private readonly subjectPrefix;
54
+ private readonly requestTimeoutMs;
55
+ private readonly natsUrl;
56
+ private readonly apiKey;
57
+ private nc;
58
+ private authToken;
59
+ private authTokenExpiresAt;
60
+ constructor(options: TransferControlOptions);
61
+ serveRouteRecoverySigner(transferId: string, handler: (payload: RouteRecoverySignRequestPayload) => Promise<RouteRecoverySignReplyPayload>): Promise<() => void>;
62
+ close(): Promise<void>;
63
+ request<T>(messageType: TransferClientMessageType, payload: Record<string, unknown>, transferId?: string): Promise<T>;
64
+ splitRoutesForPayload(messageType: TransferClientMessageType, basePayload: Record<string, unknown>, routes: unknown[]): unknown[][];
65
+ private connection;
66
+ private resolveAuthToken;
67
+ private authSubject;
68
+ private routeRecoverySignSubject;
69
+ private buildRouteRecoveryReply;
70
+ private requestSubject;
71
+ }
72
+ export declare function transferShardId(transferId: string, shardCount: number): number;
@@ -0,0 +1,227 @@
1
+ import { encode, decode } from "@msgpack/msgpack";
2
+ import { connect } from "nats";
3
+ export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v1";
4
+ export const BEAM_DEFAULT_NATS_URL = "tls://nats.b1m.ai:4222";
5
+ export const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
6
+ // NATS enforces max_payload per message. This guard splits signed-route control
7
+ // messages before the broker rejects them; transfer bytes never flow through NATS.
8
+ export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES = 24 * 1024 * 1024;
9
+ export const ROUTE_RECOVERY_SIGN_MESSAGE_TYPE = "transfer.route_recovery.sign";
10
+ export class BeamTransferControl {
11
+ environment;
12
+ keyPrefix;
13
+ shardCount;
14
+ maxPayloadBytes;
15
+ subjectPrefix;
16
+ requestTimeoutMs;
17
+ natsUrl;
18
+ apiKey;
19
+ nc = null;
20
+ authToken = null;
21
+ authTokenExpiresAt = 0;
22
+ constructor(options) {
23
+ this.apiKey = options.apiKey;
24
+ this.keyPrefix = options.apiKey.slice(0, 12);
25
+ this.environment = options.environment ?? "prod";
26
+ this.subjectPrefix = normalizeSubjectPrefix(options.subjectPrefix ?? "beam.transfer.client");
27
+ this.shardCount = options.transferRuntimeShardCount ?? 1;
28
+ this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
29
+ this.maxPayloadBytes = options.maxPayloadBytes ?? BEAM_DEFAULT_MAX_PAYLOAD_BYTES;
30
+ this.natsUrl = options.natsUrl ?? options.natsWsUrl ?? BEAM_DEFAULT_NATS_URL;
31
+ if (/^https?:\/\//i.test(this.natsUrl) || /^wss?:\/\//i.test(this.natsUrl) && !options.natsWsUrl) {
32
+ throw new Error("Beam SDK lifecycle endpoint must be nats://, tls://, or configured as natsWsUrl.");
33
+ }
34
+ if (!Number.isInteger(this.shardCount) || this.shardCount < 1) {
35
+ throw new Error("transferRuntimeShardCount must be a positive integer.");
36
+ }
37
+ }
38
+ async serveRouteRecoverySigner(transferId, handler) {
39
+ const nc = await this.connection();
40
+ const subject = this.routeRecoverySignSubject(transferId);
41
+ const sub = nc.subscribe(subject);
42
+ let active = true;
43
+ void (async () => {
44
+ for await (const msg of sub) {
45
+ if (!active)
46
+ break;
47
+ let request = null;
48
+ try {
49
+ request = decode(msg.data);
50
+ if (request.schema_version !== TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION ||
51
+ request.environment !== this.environment ||
52
+ request.key_prefix !== this.keyPrefix ||
53
+ request.transfer_id !== transferId ||
54
+ request.message_type !== ROUTE_RECOVERY_SIGN_MESSAGE_TYPE ||
55
+ request.producer !== "transfer-runtime") {
56
+ throw new Error("route recovery request envelope mismatch");
57
+ }
58
+ const payload = await handler(request.payload);
59
+ msg.respond(encode(this.buildRouteRecoveryReply(request, true, 200, payload)));
60
+ }
61
+ catch (error) {
62
+ const fallback = request ?? {
63
+ message_id: "unknown",
64
+ request_id: "unknown",
65
+ transfer_id: transferId
66
+ };
67
+ msg.respond(encode(this.buildRouteRecoveryReply(fallback, false, 500, undefined, {
68
+ code: "route_recovery_sign_failed",
69
+ message: error instanceof Error ? error.message : String(error)
70
+ })));
71
+ }
72
+ }
73
+ })();
74
+ return () => {
75
+ active = false;
76
+ sub.unsubscribe();
77
+ };
78
+ }
79
+ async close() {
80
+ if (!this.nc)
81
+ return;
82
+ await this.nc.drain();
83
+ this.nc = null;
84
+ }
85
+ async request(messageType, payload, transferId) {
86
+ const nc = await this.connection();
87
+ const authToken = await this.resolveAuthToken();
88
+ const shardId = transferId ? transferShardId(transferId, this.shardCount) : 0;
89
+ const requestId = randomId();
90
+ const envelope = {
91
+ message_id: `${TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION}:${this.environment}:${this.keyPrefix}:${messageType}:${requestId}`,
92
+ schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
93
+ environment: this.environment,
94
+ key_prefix: this.keyPrefix,
95
+ shard_id: shardId,
96
+ message_type: messageType,
97
+ request_id: requestId,
98
+ auth_token: authToken,
99
+ occurred_at: new Date().toISOString(),
100
+ producer: "sdk",
101
+ payload
102
+ };
103
+ const bytes = encode(envelope);
104
+ if (bytes.byteLength > this.maxPayloadBytes) {
105
+ throw new Error(`NATS lifecycle request is ${bytes.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
106
+ }
107
+ const subject = this.requestSubject(messageType, shardId);
108
+ const response = await nc.request(subject, bytes, { timeout: this.requestTimeoutMs });
109
+ const decoded = decode(response.data);
110
+ if (!decoded.ok) {
111
+ const body = JSON.stringify(decoded.error ?? {});
112
+ throw new Error(`Beam lifecycle request failed with ${decoded.status}: ${body}`);
113
+ }
114
+ return (decoded.payload ?? {});
115
+ }
116
+ splitRoutesForPayload(messageType, basePayload, routes) {
117
+ const chunks = [];
118
+ let current = [];
119
+ for (const route of routes) {
120
+ const candidate = [...current, route];
121
+ const encoded = encode({
122
+ schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
123
+ environment: this.environment,
124
+ key_prefix: this.keyPrefix,
125
+ shard_id: 0,
126
+ message_type: messageType,
127
+ request_id: randomId(),
128
+ auth_token: "x".repeat(512),
129
+ occurred_at: new Date().toISOString(),
130
+ producer: "sdk",
131
+ payload: { ...basePayload, chunk_routes: candidate }
132
+ });
133
+ if (current.length > 0 && encoded.byteLength > this.maxPayloadBytes) {
134
+ chunks.push(current);
135
+ current = [route];
136
+ }
137
+ else {
138
+ current = candidate;
139
+ }
140
+ }
141
+ if (current.length > 0)
142
+ chunks.push(current);
143
+ return chunks;
144
+ }
145
+ async connection() {
146
+ if (this.nc && !this.nc.isClosed())
147
+ return this.nc;
148
+ this.nc = await connect({
149
+ servers: this.natsUrl,
150
+ user: this.keyPrefix,
151
+ pass: this.apiKey,
152
+ name: `beam-sdk-${this.keyPrefix}`
153
+ });
154
+ return this.nc;
155
+ }
156
+ async resolveAuthToken() {
157
+ const now = Math.floor(Date.now() / 1000);
158
+ if (this.authToken && this.authTokenExpiresAt - 5 > now)
159
+ return this.authToken;
160
+ const nc = await this.connection();
161
+ const response = await nc.request(this.authSubject(), new TextEncoder().encode("{}"), { timeout: this.requestTimeoutMs });
162
+ const parsed = JSON.parse(new TextDecoder().decode(response.data));
163
+ if (!parsed.ok || !parsed.token) {
164
+ throw new Error(`Beam SDK NATS auth resolve failed: ${parsed.error ?? "unknown_error"}`);
165
+ }
166
+ const claims = decodeJwtPayload(parsed.token);
167
+ this.authToken = parsed.token;
168
+ this.authTokenExpiresAt = claims.exp;
169
+ return parsed.token;
170
+ }
171
+ authSubject() {
172
+ return `${this.subjectPrefix}.${this.environment}.auth.${this.keyPrefix}.resolve`;
173
+ }
174
+ routeRecoverySignSubject(transferId) {
175
+ return `${this.subjectPrefix}.${this.environment}.sdk.${this.keyPrefix}.transfer.${transferId}.route_recovery_sign`;
176
+ }
177
+ buildRouteRecoveryReply(request, ok, status, payload, error) {
178
+ return {
179
+ message_id: request.message_id,
180
+ schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
181
+ environment: this.environment,
182
+ key_prefix: this.keyPrefix,
183
+ transfer_id: request.transfer_id,
184
+ message_type: ROUTE_RECOVERY_SIGN_MESSAGE_TYPE,
185
+ request_id: request.request_id,
186
+ occurred_at: new Date().toISOString(),
187
+ producer: "sdk",
188
+ ok,
189
+ status,
190
+ ...(payload ? { payload } : {}),
191
+ ...(error ? { error } : {})
192
+ };
193
+ }
194
+ requestSubject(messageType, shardId) {
195
+ return `${this.subjectPrefix}.${this.environment}.sdk.${this.keyPrefix}.shard.${shardId}.${messageType.replace(/\./g, "_")}`;
196
+ }
197
+ }
198
+ export function transferShardId(transferId, shardCount) {
199
+ let hash = 2166136261;
200
+ for (let i = 0; i < transferId.length; i += 1) {
201
+ hash ^= transferId.charCodeAt(i);
202
+ hash = Math.imul(hash, 16777619) >>> 0;
203
+ }
204
+ return hash % shardCount;
205
+ }
206
+ function decodeJwtPayload(jwt) {
207
+ const parts = jwt.split(".");
208
+ if (parts.length < 2)
209
+ throw new Error("invalid JWT");
210
+ return JSON.parse(base64UrlDecode(parts[1]));
211
+ }
212
+ function base64UrlDecode(value) {
213
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
214
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
215
+ if (typeof Buffer !== "undefined")
216
+ return Buffer.from(padded, "base64").toString("utf8");
217
+ return atob(padded);
218
+ }
219
+ function normalizeSubjectPrefix(value) {
220
+ return value.replace(/^\.+|\.+$/g, "");
221
+ }
222
+ function randomId() {
223
+ const cryptoApi = globalThis.crypto;
224
+ if (cryptoApi?.randomUUID)
225
+ return cryptoApi.randomUUID();
226
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
227
+ }
@@ -46,7 +46,6 @@ export declare function signDestinationRoute(input: {
46
46
  multipartGroupId?: string;
47
47
  multipartCreatedAt?: string;
48
48
  commitMethod?: "upload_part" | "upload_part_copy";
49
- contentMd5?: string;
50
49
  partVerifyUrl?: string;
51
50
  listPageUrl?: string;
52
51
  stagingObjectKey?: string;
@@ -57,21 +56,6 @@ export declare function signDestinationRoute(input: {
57
56
  deleteUrl?: string;
58
57
  fetchImpl?: typeof fetch;
59
58
  }): Promise<SignedChunkRoute>;
60
- export declare function computeChunksContentMd5(input: {
61
- source: ProviderSourceConfig;
62
- chunks: ChunkSigningPlanItem[];
63
- expiresIn: number;
64
- fetchImpl?: typeof fetch;
65
- groupSizeBytes?: number;
66
- readTimeoutMs?: number;
67
- }): Promise<Map<number, string>>;
68
- export declare function computeChunkContentMd5(input: {
69
- source: ProviderSourceConfig;
70
- chunk: ChunkSigningPlanItem;
71
- expiresIn: number;
72
- fetchImpl?: typeof fetch;
73
- readTimeoutMs?: number;
74
- }): Promise<string>;
75
59
  export declare function isS3CompatibleProvider(config: ProviderSourceConfig | ProviderDestinationConfig): config is AnyS3CompatibleProviderConfig;
76
60
  export declare function s3CompatibleEndpoint(source: AnyS3CompatibleProviderConfig): string | undefined;
77
61
  export declare function s3CompatibleRegion(source: AnyS3CompatibleProviderConfig): string;