@vxil/sdk 0.15.0 → 0.16.1

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/index.d.ts CHANGED
@@ -414,8 +414,10 @@ export interface JobRun {
414
414
  concurrency_limit?: number | null;
415
415
  /** the fan-in batch the run joined (enqueue `batch_id`); null otherwise */
416
416
  batch_id?: string | null;
417
- /** a plain run enqueued with `ttl_seconds`: its START deadline (it is
418
- * dead-lettered `Expired` if not started by then); null otherwise */
417
+ /** the run's deadline. A plain run enqueued with `ttl_seconds`: its START
418
+ * deadline (dead-lettered `Expired` if not started by then). A generation
419
+ * run: its completion deadline (`timeout.after_ms`; past it the run ends
420
+ * `GenerationExpired` and any credit hold is released). Null otherwise. */
419
421
  expires_at?: string | null;
420
422
  /** the single-run read only: the debounce key of a debounced run */
421
423
  debounce_key?: string | null;
@@ -865,6 +867,47 @@ export interface FileObject {
865
867
  /** The object's public URL when it is published to the public asset host
866
868
  * (`files.publish`), else null. */
867
869
  public_url?: string | null;
870
+ /** When the object expires (null = never). An expired object is no longer
871
+ * listed and answers 404, even before the sweep deletes it. */
872
+ expires_at?: string | null;
873
+ }
874
+ /** `files.createUploadUrl` answer. */
875
+ export interface FileUploadUrl {
876
+ object_id: string;
877
+ upload_url: string;
878
+ upload_method: string;
879
+ /** Seconds the UPLOAD URL stays valid (`uploadUrlTtl`, default 900). */
880
+ expires_in: number;
881
+ /** When the OBJECT expires (null = never). */
882
+ expires_at: string | null;
883
+ }
884
+ /** `files.usage` answer. */
885
+ export interface FilesUsage {
886
+ usage: {
887
+ object_count: number;
888
+ total_bytes: number;
889
+ };
890
+ /** `maxTotalBytes` = the storage quota enforced now (see `storage_plan`). */
891
+ quotas: {
892
+ maxObjectCount?: number;
893
+ maxObjectBytes?: number;
894
+ maxTotalBytes?: number;
895
+ };
896
+ available: Record<string, number | null>;
897
+ /** Your plan's storage ceiling, and whether `quotas.maxTotalBytes` is that
898
+ * ceiling (`plan`) or your own lower value (`config`). */
899
+ storage_plan?: {
900
+ tier: string | null;
901
+ max_total_bytes: number;
902
+ source: 'plan' | 'config';
903
+ };
904
+ public_assets?: {
905
+ enabled: boolean;
906
+ published_objects: number;
907
+ published_bytes: number;
908
+ max_published_bytes: number;
909
+ bytes_remaining: number;
910
+ };
868
911
  }
869
912
  /** A published object (guide ch. 6, files, "Public asset delivery"): a stable,
870
913
  * content-addressed URL on vxil's public asset host, served with
@@ -1167,13 +1210,23 @@ export interface JobGenerationSettledEventPayload {
1167
1210
  /** `completed` on the healthy terminal, `failed` on the broken one */
1168
1211
  status: 'completed' | 'failed';
1169
1212
  /** the run's SETTLED error class (`provider_error`, `PollExhausted`,
1170
- * `GenerationExpired`, `ReserveInsufficient`, …); always null on `completed` */
1213
+ * `GenerationExpired`, `RetryableHttp`, `ReserveInsufficient`,
1214
+ * `PaymentsUnavailable`, `Cancelled`, …); always null on `completed`.
1215
+ * BRANCH ON THIS in a settle handler: `ReserveInsufficient` (the caller got
1216
+ * a 402), `PaymentsUnavailable` (the run was refused before it started —
1217
+ * usually the caller got a 503; a caller handed this run by a duplicate
1218
+ * request that raced it (202 `deduplicated`) learns it HERE and re-sends
1219
+ * the same request — a NEW run follows) and `Cancelled` are not render
1220
+ * failures. */
1171
1221
  error_class: string | null;
1172
1222
  /** the provider hint alone (`Upstream said: gemini 400: …`); null on
1173
1223
  * `completed` and whenever the failure carried none */
1174
1224
  error_hint: string | null;
1175
- /** the failure-event vocabulary every `*.failed` carries (guide 12) */
1176
- level: 'info' | 'error';
1225
+ /** the failure-event vocabulary every `*.failed` carries (guide 12):
1226
+ * `completed` → info; `failed` → error, except a refusal at admission
1227
+ * (`ReserveInsufficient`, `PaymentsUnavailable`) → warn and a cancel
1228
+ * (`Cancelled`) → info, so neither pages through the immediate digest */
1229
+ level: 'info' | 'warn' | 'error';
1177
1230
  state: 'ok' | 'broken';
1178
1231
  }
1179
1232
  /** The union a handler subscribed to `job.generation.` receives as `data`:
@@ -3176,7 +3229,10 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
3176
3229
  * FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
3177
3230
  * the enqueue (insufficient balance; the run ends with job.generation.failed
3178
3231
  * `ReserveInsufficient` and the 402 names its `run_id`); a runaway over the
3179
- * per-tenant outstanding-holds ceiling → 429 (Retry-After 5).
3232
+ * per-tenant outstanding-holds ceiling → 429 (Retry-After 5). Payments
3233
+ * unreachable at the hold → 503 `payments_unavailable` (Retry-After 15;
3234
+ * nothing started, nothing held — re-send the same request), unless
3235
+ * `reserve_credits.on_unavailable: 'proceed'`.
3180
3236
  *
3181
3237
  * Generic completion options for queue-style providers (fal.ai and alike —
3182
3238
  * no vendor adapter): `completion.callback.query_param` puts the signed
@@ -3193,7 +3249,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
3193
3249
  *
3194
3250
  * `opts.retryOnCapacity: { maxWaitMs }` retries a CAPACITY 429
3195
3251
  * (`generation_concurrency_exceeded`, `reserve_holds_exceeded`,
3196
- * `queue_full`) honouring Retry-After with jitter, with the SAME
3252
+ * `queue_full`) and a 503 `payments_unavailable`, honouring Retry-After
3253
+ * with jitter, with the SAME
3197
3254
  * idempotency_key (one is generated when the input has none), until
3198
3255
  * maxWaitMs has passed — then the last 429 is thrown. */
3199
3256
  generation: (input: {
@@ -3249,9 +3306,20 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
3249
3306
  amount: number;
3250
3307
  user_id: string;
3251
3308
  reason?: string;
3309
+ /** payments unreachable when the hold is placed (a 5xx other than
3310
+ * 501, a 429, no answer within 5 s; payments switched off is NOT
3311
+ * unreachable — that run starts without a hold): `'fail'` (default)
3312
+ * → 503 `payments_unavailable` + Retry-After, nothing started or
3313
+ * held — re-send the SAME request; `'proceed'` → start WITHOUT a
3314
+ * hold (a free run while payments is down) */
3315
+ on_unavailable?: "fail" | "proceed";
3252
3316
  };
3253
3317
  payload?: Record<string, unknown>;
3254
3318
  idempotency_key?: string;
3319
+ /** START attempts = provider calls (1..20, default jobs.retry
3320
+ * .defaultMaxAttempts 5); a 408 / 429 / 5xx / network error retries
3321
+ * after min(2^n × 15, 600) s — or the provider's Retry-After on a
3322
+ * 429 / 503 (1 s..10 min, never past the deadline) */
3255
3323
  max_attempts?: number;
3256
3324
  }, opts?: {
3257
3325
  retryOnCapacity?: {
@@ -4998,21 +5066,26 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4998
5066
  }) => Promise<WebSocket>;
4999
5067
  };
5000
5068
  readonly files: {
5001
- /** Mint a presigned PUT; upload the bytes yourself, then call complete(). */
5069
+ /** Mint a presigned PUT; upload the bytes yourself, then call complete().
5070
+ * `expiresInSeconds` (60 s up to 100 years) makes the OBJECT expire — it is deleted that
5071
+ * long after the mint, whether or not `files.ttl` is enabled; `null` =
5072
+ * never. The answer's `expires_in` is the UPLOAD URL's life in seconds;
5073
+ * `expires_at` is when the object expires (null = never). `user_id` is
5074
+ * required with a server key and omitted in end-user mode. */
5002
5075
  createUploadUrl: (input: {
5003
- user_id: string;
5076
+ user_id?: string;
5004
5077
  filename: string;
5005
5078
  content_type: string;
5006
5079
  size_bytes: number;
5007
- }) => Promise<{
5008
- object_id: string;
5009
- upload_url: string;
5010
- upload_method: string;
5011
- expires_at: string;
5012
- }>;
5080
+ expiresInSeconds?: number | null;
5081
+ }) => Promise<FileUploadUrl>;
5082
+ /** Confirm the PUT (bytes verified, object → available). Answers the
5083
+ * object's `expires_at` (null = never). */
5013
5084
  complete: (objectId: string) => Promise<{
5014
5085
  object_id: string;
5015
5086
  status: string;
5087
+ checksum_sha256?: string | null;
5088
+ expires_at?: string | null;
5016
5089
  }>;
5017
5090
  downloadUrl: (objectId: string) => Promise<string>;
5018
5091
  /** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
@@ -5045,27 +5118,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
5045
5118
  /** Soft delete; bytes are hard-deleted 30 days later. */
5046
5119
  delete: (objectId: string) => Promise<void>;
5047
5120
  /** Aggregate storage usage vs quotas (the FilesManager Storage panel).
5121
+ * `quotas.maxTotalBytes` is the quota ENFORCED now — the plan's ceiling
5122
+ * unless you set a lower `quotas.maxTotalBytes` (`storage_plan.source`
5123
+ * says which). An object past its expiry no longer counts.
5048
5124
  * `public_assets` = published copies vs the plan's published-bytes ceiling
5049
5125
  * (identical bytes count once). */
5050
- usage: () => Promise<{
5051
- usage: {
5052
- object_count: number;
5053
- total_bytes: number;
5054
- };
5055
- quotas: {
5056
- maxObjectCount?: number;
5057
- maxObjectBytes?: number;
5058
- maxTotalBytes?: number;
5059
- };
5060
- available: Record<string, number | null>;
5061
- public_assets?: {
5062
- enabled: boolean;
5063
- published_objects: number;
5064
- published_bytes: number;
5065
- max_published_bytes: number;
5066
- bytes_remaining: number;
5067
- };
5068
- }>;
5126
+ usage: () => Promise<FilesUsage>;
5069
5127
  /** SERVER-ONLY (403 server_only in end-user mode). Publish an available
5070
5128
  * object to vxil's public asset host: a stable, content-addressed URL
5071
5129
  * (`https://cdn.vxil.app/<tenant>/<sha256>.<ext>`) served from a global
@@ -5125,7 +5183,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
5125
5183
  error?: string | null;
5126
5184
  }>;
5127
5185
  /** Set / extend / clear an object's TTL. `expiresInSeconds: null` clears it
5128
- * (else a future auto-delete after the given seconds; minimum 60). */
5186
+ * (else a future auto-delete after the given seconds; 60 s up to 100
5187
+ * years). An object already past its expiry is a 404 — never revived. */
5129
5188
  setTtl: (objectId: string, expiresInSeconds: number | null) => Promise<{
5130
5189
  object_id: string;
5131
5190
  expires_at: string | null;
package/dist/index.js CHANGED
@@ -353,9 +353,20 @@ export class Vxil {
353
353
  body: JSON.stringify(payload ?? {}),
354
354
  });
355
355
  if (!res.ok) {
356
+ // A function answers its OWN error body: the platform envelope
357
+ // `{ error: { code, message } }`, or a bare string code
358
+ // `{ error: 'role_required', message? }` (the blueprints' shape) —
359
+ // both keep their code (a string used to collapse to http_<status>).
356
360
  let e = {};
357
361
  try {
358
- e = JSON.parse(text).error ?? {};
362
+ const b = JSON.parse(text);
363
+ if (typeof b.error === 'string') {
364
+ if (b.error)
365
+ e = { code: b.error, message: typeof b.message === 'string' && b.message ? b.message : b.error };
366
+ }
367
+ else if (b.error && typeof b.error === 'object') {
368
+ e = b.error;
369
+ }
359
370
  }
360
371
  catch { /* non-JSON error body */ }
361
372
  throw new VxilError(res.status, e.code ?? `http_${res.status}`, e.message ?? text.slice(0, 200), undefined, undefined, undefined, parseRetryAfter(res.headers.get('retry-after')));
@@ -754,7 +765,10 @@ export class Vxil {
754
765
  * FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
755
766
  * the enqueue (insufficient balance; the run ends with job.generation.failed
756
767
  * `ReserveInsufficient` and the 402 names its `run_id`); a runaway over the
757
- * per-tenant outstanding-holds ceiling → 429 (Retry-After 5).
768
+ * per-tenant outstanding-holds ceiling → 429 (Retry-After 5). Payments
769
+ * unreachable at the hold → 503 `payments_unavailable` (Retry-After 15;
770
+ * nothing started, nothing held — re-send the same request), unless
771
+ * `reserve_credits.on_unavailable: 'proceed'`.
758
772
  *
759
773
  * Generic completion options for queue-style providers (fal.ai and alike —
760
774
  * no vendor adapter): `completion.callback.query_param` puts the signed
@@ -771,7 +785,8 @@ export class Vxil {
771
785
  *
772
786
  * `opts.retryOnCapacity: { maxWaitMs }` retries a CAPACITY 429
773
787
  * (`generation_concurrency_exceeded`, `reserve_holds_exceeded`,
774
- * `queue_full`) honouring Retry-After with jitter, with the SAME
788
+ * `queue_full`) and a 503 `payments_unavailable`, honouring Retry-After
789
+ * with jitter, with the SAME
775
790
  * idempotency_key (one is generated when the input has none), until
776
791
  * maxWaitMs has passed — then the last 429 is thrown. */
777
792
  generation: async (input, opts) => {
@@ -1832,8 +1847,15 @@ export class Vxil {
1832
1847
  },
1833
1848
  };
1834
1849
  files = {
1835
- /** Mint a presigned PUT; upload the bytes yourself, then call complete(). */
1850
+ /** Mint a presigned PUT; upload the bytes yourself, then call complete().
1851
+ * `expiresInSeconds` (60 s up to 100 years) makes the OBJECT expire — it is deleted that
1852
+ * long after the mint, whether or not `files.ttl` is enabled; `null` =
1853
+ * never. The answer's `expires_in` is the UPLOAD URL's life in seconds;
1854
+ * `expires_at` is when the object expires (null = never). `user_id` is
1855
+ * required with a server key and omitted in end-user mode. */
1836
1856
  createUploadUrl: async (input) => (await this.call('POST', '/v1/files/upload-url', input)).data,
1857
+ /** Confirm the PUT (bytes verified, object → available). Answers the
1858
+ * object's `expires_at` (null = never). */
1837
1859
  complete: async (objectId) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/complete`)).data,
1838
1860
  downloadUrl: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/download-url`)).data.download_url,
1839
1861
  /** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
@@ -1857,6 +1879,9 @@ export class Vxil {
1857
1879
  await this.call('DELETE', `/v1/files/${encodeURIComponent(objectId)}`);
1858
1880
  },
1859
1881
  /** Aggregate storage usage vs quotas (the FilesManager Storage panel).
1882
+ * `quotas.maxTotalBytes` is the quota ENFORCED now — the plan's ceiling
1883
+ * unless you set a lower `quotas.maxTotalBytes` (`storage_plan.source`
1884
+ * says which). An object past its expiry no longer counts.
1860
1885
  * `public_assets` = published copies vs the plan's published-bytes ceiling
1861
1886
  * (identical bytes count once). */
1862
1887
  usage: async () => (await this.call('GET', '/v1/files/usage')).data,
@@ -1894,7 +1919,8 @@ export class Vxil {
1894
1919
  /** Fetch the cached extraction (status: not_extracted|pending|available|failed). */
1895
1920
  getText: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/text`)).data,
1896
1921
  /** Set / extend / clear an object's TTL. `expiresInSeconds: null` clears it
1897
- * (else a future auto-delete after the given seconds; minimum 60). */
1922
+ * (else a future auto-delete after the given seconds; 60 s up to 100
1923
+ * years). An object already past its expiry is a 404 — never revived. */
1898
1924
  setTtl: async (objectId, expiresInSeconds) => (await this.call('PUT', `/v1/files/${encodeURIComponent(objectId)}/ttl`, { expiresInSeconds })).data,
1899
1925
  /** SERVER-ONLY (403 server_only in end-user mode). Account merge: move
1900
1926
  * every object `from_user_id` owns (every status, tombstones included) and
package/dist/retry.d.ts CHANGED
@@ -50,6 +50,14 @@ export declare function createTransport(opts: TransportOptions): Transport;
50
50
  * and outstanding-holds ceiling, and the queue admission cap). Any other 429
51
51
  * (a rate limit, a plan quota) is not a capacity answer and is rethrown. */
52
52
  export declare const CAPACITY_ERROR_CODES: ReadonlySet<string>;
53
+ /** The 503 answers retried the same way (M1, 2026-10-04): the generation
54
+ * lane could not reach payments to place its credit hold — nothing was
55
+ * started and nothing held, and the SAME request re-sent after Retry-After
56
+ * starts the run (the refused run released its idempotency key). */
57
+ export declare const UNAVAILABLE_ERROR_CODES: ReadonlySet<string>;
58
+ /** Is this thrown error one `retryOnCapacity` waits out? Duck-typed on
59
+ * `{ status, code }` (VxilError's fields). */
60
+ export declare function isCapacityRetryable(e: unknown): boolean;
53
61
  /** `retryOnCapacity` options. */
54
62
  export interface RetryOnCapacityOptions {
55
63
  /** Give up (rethrow the last 429) once waiting again would pass this many
@@ -60,7 +68,8 @@ export interface RetryOnCapacityOptions {
60
68
  now?: () => number;
61
69
  }
62
70
  /**
63
- * Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`):
71
+ * Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`) or
72
+ * a 503 `payments_unavailable` (`UNAVAILABLE_ERROR_CODES`):
64
73
  * wait the server's `Retry-After` (else 5 s) plus up to 20 % jitter (≤ 1 s,
65
74
  * so many clients told the same second do not all return on it), and never
66
75
  * past `maxWaitMs` since the first attempt — then the last 429 is rethrown.
package/dist/retry.js CHANGED
@@ -162,10 +162,25 @@ export function createTransport(opts) {
162
162
  export const CAPACITY_ERROR_CODES = new Set([
163
163
  'generation_concurrency_exceeded', 'reserve_holds_exceeded', 'queue_full',
164
164
  ]);
165
+ /** The 503 answers retried the same way (M1, 2026-10-04): the generation
166
+ * lane could not reach payments to place its credit hold — nothing was
167
+ * started and nothing held, and the SAME request re-sent after Retry-After
168
+ * starts the run (the refused run released its idempotency key). */
169
+ export const UNAVAILABLE_ERROR_CODES = new Set(['payments_unavailable']);
170
+ /** Is this thrown error one `retryOnCapacity` waits out? Duck-typed on
171
+ * `{ status, code }` (VxilError's fields). */
172
+ export function isCapacityRetryable(e) {
173
+ const err = e;
174
+ if (!err || typeof err.code !== 'string')
175
+ return false;
176
+ return (err.status === 429 && CAPACITY_ERROR_CODES.has(err.code))
177
+ || (err.status === 503 && UNAVAILABLE_ERROR_CODES.has(err.code));
178
+ }
165
179
  /** Fallback wait (ms) when a capacity 429 carries no Retry-After. */
166
180
  const CAPACITY_DEFAULT_WAIT_MS = 5_000;
167
181
  /**
168
- * Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`):
182
+ * Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`) or
183
+ * a 503 `payments_unavailable` (`UNAVAILABLE_ERROR_CODES`):
169
184
  * wait the server's `Retry-After` (else 5 s) plus up to 20 % jitter (≤ 1 s,
170
185
  * so many clients told the same second do not all return on it), and never
171
186
  * past `maxWaitMs` since the first attempt — then the last 429 is rethrown.
@@ -184,7 +199,7 @@ export async function retryOnCapacity(attempt, opts) {
184
199
  }
185
200
  catch (e) {
186
201
  const err = e;
187
- if (!err || err.status !== 429 || typeof err.code !== 'string' || !CAPACITY_ERROR_CODES.has(err.code))
202
+ if (!err || !isCapacityRetryable(err))
188
203
  throw e;
189
204
  const baseMs = typeof err.retryAfter === 'number' && err.retryAfter >= 0
190
205
  ? Math.round(err.retryAfter * 1000)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/sdk",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Typed client for the Vxil REST API (notifications, auth, jobs, files, cms, comments, webhooks, realtime, orgs, rate-limits).",