@vxil/sdk 0.15.0 → 0.16.0

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
@@ -865,6 +865,47 @@ export interface FileObject {
865
865
  /** The object's public URL when it is published to the public asset host
866
866
  * (`files.publish`), else null. */
867
867
  public_url?: string | null;
868
+ /** When the object expires (null = never). An expired object is no longer
869
+ * listed and answers 404, even before the sweep deletes it. */
870
+ expires_at?: string | null;
871
+ }
872
+ /** `files.createUploadUrl` answer. */
873
+ export interface FileUploadUrl {
874
+ object_id: string;
875
+ upload_url: string;
876
+ upload_method: string;
877
+ /** Seconds the UPLOAD URL stays valid (`uploadUrlTtl`, default 900). */
878
+ expires_in: number;
879
+ /** When the OBJECT expires (null = never). */
880
+ expires_at: string | null;
881
+ }
882
+ /** `files.usage` answer. */
883
+ export interface FilesUsage {
884
+ usage: {
885
+ object_count: number;
886
+ total_bytes: number;
887
+ };
888
+ /** `maxTotalBytes` = the storage quota enforced now (see `storage_plan`). */
889
+ quotas: {
890
+ maxObjectCount?: number;
891
+ maxObjectBytes?: number;
892
+ maxTotalBytes?: number;
893
+ };
894
+ available: Record<string, number | null>;
895
+ /** Your plan's storage ceiling, and whether `quotas.maxTotalBytes` is that
896
+ * ceiling (`plan`) or your own lower value (`config`). */
897
+ storage_plan?: {
898
+ tier: string | null;
899
+ max_total_bytes: number;
900
+ source: 'plan' | 'config';
901
+ };
902
+ public_assets?: {
903
+ enabled: boolean;
904
+ published_objects: number;
905
+ published_bytes: number;
906
+ max_published_bytes: number;
907
+ bytes_remaining: number;
908
+ };
868
909
  }
869
910
  /** A published object (guide ch. 6, files, "Public asset delivery"): a stable,
870
911
  * content-addressed URL on vxil's public asset host, served with
@@ -1167,13 +1208,23 @@ export interface JobGenerationSettledEventPayload {
1167
1208
  /** `completed` on the healthy terminal, `failed` on the broken one */
1168
1209
  status: 'completed' | 'failed';
1169
1210
  /** the run's SETTLED error class (`provider_error`, `PollExhausted`,
1170
- * `GenerationExpired`, `ReserveInsufficient`, …); always null on `completed` */
1211
+ * `GenerationExpired`, `RetryableHttp`, `ReserveInsufficient`,
1212
+ * `PaymentsUnavailable`, `Cancelled`, …); always null on `completed`.
1213
+ * BRANCH ON THIS in a settle handler: `ReserveInsufficient` (the caller got
1214
+ * a 402), `PaymentsUnavailable` (the run was refused before it started —
1215
+ * usually the caller got a 503; a caller handed this run by a duplicate
1216
+ * request that raced it (202 `deduplicated`) learns it HERE and re-sends
1217
+ * the same request — a NEW run follows) and `Cancelled` are not render
1218
+ * failures. */
1171
1219
  error_class: string | null;
1172
1220
  /** the provider hint alone (`Upstream said: gemini 400: …`); null on
1173
1221
  * `completed` and whenever the failure carried none */
1174
1222
  error_hint: string | null;
1175
- /** the failure-event vocabulary every `*.failed` carries (guide 12) */
1176
- level: 'info' | 'error';
1223
+ /** the failure-event vocabulary every `*.failed` carries (guide 12):
1224
+ * `completed` → info; `failed` → error, except a refusal at admission
1225
+ * (`ReserveInsufficient`, `PaymentsUnavailable`) → warn and a cancel
1226
+ * (`Cancelled`) → info, so neither pages through the immediate digest */
1227
+ level: 'info' | 'warn' | 'error';
1177
1228
  state: 'ok' | 'broken';
1178
1229
  }
1179
1230
  /** The union a handler subscribed to `job.generation.` receives as `data`:
@@ -3176,7 +3227,10 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
3176
3227
  * FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
3177
3228
  * the enqueue (insufficient balance; the run ends with job.generation.failed
3178
3229
  * `ReserveInsufficient` and the 402 names its `run_id`); a runaway over the
3179
- * per-tenant outstanding-holds ceiling → 429 (Retry-After 5).
3230
+ * per-tenant outstanding-holds ceiling → 429 (Retry-After 5). Payments
3231
+ * unreachable at the hold → 503 `payments_unavailable` (Retry-After 15;
3232
+ * nothing started, nothing held — re-send the same request), unless
3233
+ * `reserve_credits.on_unavailable: 'proceed'`.
3180
3234
  *
3181
3235
  * Generic completion options for queue-style providers (fal.ai and alike —
3182
3236
  * no vendor adapter): `completion.callback.query_param` puts the signed
@@ -3193,7 +3247,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
3193
3247
  *
3194
3248
  * `opts.retryOnCapacity: { maxWaitMs }` retries a CAPACITY 429
3195
3249
  * (`generation_concurrency_exceeded`, `reserve_holds_exceeded`,
3196
- * `queue_full`) honouring Retry-After with jitter, with the SAME
3250
+ * `queue_full`) and a 503 `payments_unavailable`, honouring Retry-After
3251
+ * with jitter, with the SAME
3197
3252
  * idempotency_key (one is generated when the input has none), until
3198
3253
  * maxWaitMs has passed — then the last 429 is thrown. */
3199
3254
  generation: (input: {
@@ -3249,9 +3304,20 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
3249
3304
  amount: number;
3250
3305
  user_id: string;
3251
3306
  reason?: string;
3307
+ /** payments unreachable when the hold is placed (a 5xx other than
3308
+ * 501, a 429, no answer within 5 s; payments switched off is NOT
3309
+ * unreachable — that run starts without a hold): `'fail'` (default)
3310
+ * → 503 `payments_unavailable` + Retry-After, nothing started or
3311
+ * held — re-send the SAME request; `'proceed'` → start WITHOUT a
3312
+ * hold (a free run while payments is down) */
3313
+ on_unavailable?: "fail" | "proceed";
3252
3314
  };
3253
3315
  payload?: Record<string, unknown>;
3254
3316
  idempotency_key?: string;
3317
+ /** START attempts = provider calls (1..20, default jobs.retry
3318
+ * .defaultMaxAttempts 5); a 408 / 429 / 5xx / network error retries
3319
+ * after min(2^n × 15, 600) s — or the provider's Retry-After on a
3320
+ * 429 / 503 (1 s..10 min, never past the deadline) */
3255
3321
  max_attempts?: number;
3256
3322
  }, opts?: {
3257
3323
  retryOnCapacity?: {
@@ -4998,21 +5064,26 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4998
5064
  }) => Promise<WebSocket>;
4999
5065
  };
5000
5066
  readonly files: {
5001
- /** Mint a presigned PUT; upload the bytes yourself, then call complete(). */
5067
+ /** Mint a presigned PUT; upload the bytes yourself, then call complete().
5068
+ * `expiresInSeconds` (60 s up to 100 years) makes the OBJECT expire — it is deleted that
5069
+ * long after the mint, whether or not `files.ttl` is enabled; `null` =
5070
+ * never. The answer's `expires_in` is the UPLOAD URL's life in seconds;
5071
+ * `expires_at` is when the object expires (null = never). `user_id` is
5072
+ * required with a server key and omitted in end-user mode. */
5002
5073
  createUploadUrl: (input: {
5003
- user_id: string;
5074
+ user_id?: string;
5004
5075
  filename: string;
5005
5076
  content_type: string;
5006
5077
  size_bytes: number;
5007
- }) => Promise<{
5008
- object_id: string;
5009
- upload_url: string;
5010
- upload_method: string;
5011
- expires_at: string;
5012
- }>;
5078
+ expiresInSeconds?: number | null;
5079
+ }) => Promise<FileUploadUrl>;
5080
+ /** Confirm the PUT (bytes verified, object → available). Answers the
5081
+ * object's `expires_at` (null = never). */
5013
5082
  complete: (objectId: string) => Promise<{
5014
5083
  object_id: string;
5015
5084
  status: string;
5085
+ checksum_sha256?: string | null;
5086
+ expires_at?: string | null;
5016
5087
  }>;
5017
5088
  downloadUrl: (objectId: string) => Promise<string>;
5018
5089
  /** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
@@ -5045,27 +5116,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
5045
5116
  /** Soft delete; bytes are hard-deleted 30 days later. */
5046
5117
  delete: (objectId: string) => Promise<void>;
5047
5118
  /** Aggregate storage usage vs quotas (the FilesManager Storage panel).
5119
+ * `quotas.maxTotalBytes` is the quota ENFORCED now — the plan's ceiling
5120
+ * unless you set a lower `quotas.maxTotalBytes` (`storage_plan.source`
5121
+ * says which). An object past its expiry no longer counts.
5048
5122
  * `public_assets` = published copies vs the plan's published-bytes ceiling
5049
5123
  * (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
- }>;
5124
+ usage: () => Promise<FilesUsage>;
5069
5125
  /** SERVER-ONLY (403 server_only in end-user mode). Publish an available
5070
5126
  * object to vxil's public asset host: a stable, content-addressed URL
5071
5127
  * (`https://cdn.vxil.app/<tenant>/<sha256>.<ext>`) served from a global
@@ -5125,7 +5181,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
5125
5181
  error?: string | null;
5126
5182
  }>;
5127
5183
  /** Set / extend / clear an object's TTL. `expiresInSeconds: null` clears it
5128
- * (else a future auto-delete after the given seconds; minimum 60). */
5184
+ * (else a future auto-delete after the given seconds; 60 s up to 100
5185
+ * years). An object already past its expiry is a 404 — never revived. */
5129
5186
  setTtl: (objectId: string, expiresInSeconds: number | null) => Promise<{
5130
5187
  object_id: string;
5131
5188
  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.0",
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).",