@beam-network/sdk 0.5.14 → 0.5.15

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
@@ -98,7 +98,7 @@ For low-level raw transfer configs, use `createRawTransfer`; lifecycle transport
98
98
 
99
99
  Provider transfers use `transfer-client-control/v6`. Every reply carries Runtime and transport epochs; prepare and every route-stream message carry a UUID route generation. S3, R2, and S3-compatible destinations retain direct multipart UploadPart/ListParts/HEAD handling.
100
100
 
101
- The client signs up to 64 routes concurrently by default and emits 2,048-route logical batches. Encoded MessagePack requests target 4 MiB physical batches under `maxPayloadBytes` (8 MiB by default; explicit positive overrides remain supported). The splitter reserves control-envelope headroom for the live auth token and stable request identity so the final encoded request remains below the configured guard. A single route above the 4 MiB target but within the configured guard publishes alone; a single route larger than the configured limit fails before publication with both sizes in the error. Multipart create and abort requests use a separate `multipartControlConcurrency` limit of 2 by default, with explicit positive overrides supported, and S3-compatible control requests make up to five attempts for transient provider failures. This keeps URL signing throughput independent from provider control-plane pressure.
101
+ The client signs up to 64 routes concurrently by default and emits 2,048-route logical batches. Encoded MessagePack requests target 4 MiB physical batches under `maxPayloadBytes` (8 MiB by default; explicit positive overrides remain supported). The splitter reserves control-envelope headroom for the live auth token and stable request identity so the final encoded request remains below the configured guard. Lifecycle control refreshes cached SDK auth before the final 30 seconds of token lifetime and retries `auth_token_expired` replies with the same request identity and payload but a fresh token, preserving Core request-conflict protection during large route streams. A single route above the 4 MiB target but within the configured guard publishes alone; a single route larger than the configured limit fails before publication with both sizes in the error. Multipart create and abort requests use a separate `multipartControlConcurrency` limit of 2 by default, with explicit positive overrides supported, and S3-compatible control requests make up to five attempts for transient provider failures. This keeps URL signing throughput independent from provider control-plane pressure.
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
 
@@ -8,6 +8,7 @@ export const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
8
8
  export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
9
9
  export const BEAM_ROUTE_TARGET_PAYLOAD_BYTES = 4 * 1024 * 1024;
10
10
  const ROUTE_BATCH_AUTH_TOKEN_ESTIMATE_BYTES = 64 * 1024;
11
+ const AUTH_TOKEN_REFRESH_SAFETY_SECONDS = 30;
11
12
  export const ROUTE_RECOVERY_SIGN_MESSAGE_TYPE = "transfer.route_recovery.sign";
12
13
  export function buildConnectionOptions(options) {
13
14
  const connectionOptions = {
@@ -199,32 +200,33 @@ export class BeamTransferControl {
199
200
  void this.recoverLease(lease);
200
201
  }
201
202
  async request(messageType, payload, options = {}) {
202
- const authToken = await this.resolveAuthToken();
203
203
  const shardId = options.shardId ?? (options.transferId ? transferShardId(options.transferId, this.shardCount) : 0);
204
204
  const requestId = options.idempotencyKey
205
205
  ? await stableRequestId(`${messageType}:${options.idempotencyKey}`)
206
206
  : randomId();
207
- const envelope = {
208
- message_id: `${TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION}:${this.environment}:${this.keyPrefix}:${messageType}:${requestId}`,
209
- schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
210
- environment: this.environment,
211
- key_prefix: this.keyPrefix,
212
- shard_id: shardId,
213
- message_type: messageType,
214
- request_id: requestId,
215
- auth_token: authToken,
216
- occurred_at: new Date().toISOString(),
217
- producer: "sdk",
218
- payload
219
- };
220
- const bytes = encode(envelope);
221
- if (bytes.byteLength > this.maxPayloadBytes) {
222
- throw new Error(`NATS lifecycle request is ${bytes.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
223
- }
207
+ const occurredAt = new Date().toISOString();
224
208
  const subject = this.requestSubject(messageType, shardId);
225
209
  let lastError;
226
210
  for (let attempt = 0; attempt < LIFECYCLE_REQUEST_MAX_ATTEMPTS; attempt += 1) {
227
211
  try {
212
+ const authToken = await this.resolveAuthToken();
213
+ const envelope = {
214
+ message_id: `${TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION}:${this.environment}:${this.keyPrefix}:${messageType}:${requestId}`,
215
+ schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
216
+ environment: this.environment,
217
+ key_prefix: this.keyPrefix,
218
+ shard_id: shardId,
219
+ message_type: messageType,
220
+ request_id: requestId,
221
+ auth_token: authToken,
222
+ occurred_at: occurredAt,
223
+ producer: "sdk",
224
+ payload
225
+ };
226
+ const bytes = encode(envelope);
227
+ if (bytes.byteLength > this.maxPayloadBytes) {
228
+ throw new Error(`NATS lifecycle request is ${bytes.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
229
+ }
228
230
  const nc = await this.connection();
229
231
  const response = await nc.request(subject, bytes, { timeout: this.requestTimeoutMs });
230
232
  const decoded = decode(response.data);
@@ -233,6 +235,13 @@ export class BeamTransferControl {
233
235
  return (decoded.payload ?? {});
234
236
  const body = JSON.stringify(decoded.error ?? {});
235
237
  const error = new LifecycleRequestError(decoded.status, `Beam lifecycle request failed with ${decoded.status}: ${body}`);
238
+ if (isExpiredAuthTokenReply(decoded) && attempt + 1 < LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
239
+ this.authToken = null;
240
+ this.authTokenExpiresAt = 0;
241
+ lastError = error;
242
+ await sleepWithJitter(LIFECYCLE_REQUEST_RETRY_DELAYS_MS[attempt] ?? 1_000);
243
+ continue;
244
+ }
236
245
  if (!isRetryableLifecycleStatus(decoded.status) || attempt + 1 >= LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
237
246
  throw error;
238
247
  }
@@ -507,7 +516,7 @@ export class BeamTransferControl {
507
516
  }
508
517
  async resolveAuthToken() {
509
518
  const now = Math.floor(Date.now() / 1000);
510
- if (this.authToken && this.authTokenExpiresAt - 5 > now)
519
+ if (this.authToken && this.authTokenExpiresAt - AUTH_TOKEN_REFRESH_SAFETY_SECONDS > now)
511
520
  return this.authToken;
512
521
  if (this.authResolvePromise)
513
522
  return this.authResolvePromise;
@@ -707,6 +716,9 @@ const LIFECYCLE_REQUEST_RETRY_DELAYS_MS = [150, 500];
707
716
  function isRetryableLifecycleStatus(status) {
708
717
  return status === 408 || status === 425 || status === 429 || status >= 500;
709
718
  }
719
+ function isExpiredAuthTokenReply(reply) {
720
+ return reply.status === 401 && reply.error?.code === "auth_token_expired";
721
+ }
710
722
  export function isRetryableLifecycleError(error) {
711
723
  if (error instanceof LifecycleRequestError)
712
724
  return isRetryableLifecycleStatus(error.status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beam-network/sdk",
3
- "version": "0.5.14",
3
+ "version": "0.5.15",
4
4
  "description": "TypeScript SDK for BEAM transfer creation and management.",
5
5
  "type": "module",
6
6
  "license": "MIT",