@vxil/sdk 0.9.0 → 0.11.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/README.md CHANGED
@@ -16,7 +16,7 @@ await vx.notifications.send({ user_id: 'u_1', template: 'welcome', data: { app_n
16
16
  const { items } = await vx.from('tasks').query({ filter: { status: 'open' } });
17
17
  ```
18
18
 
19
- - Zero dependencies. Runs anywhere `fetch` exists: Node ≥ 18, browsers, edge runtimes, and **React Native / Expo** (Hermes) — the SDK uses none of the WHATWG `URL` / `URLSearchParams` surface React Native only partially provides.
19
+ - Zero dependencies. Runs anywhere `fetch` exists: Node ≥ 22, browsers, edge runtimes, and **React Native / Expo** (Hermes) — the SDK uses none of the WHATWG `URL` / `URLSearchParams` surface React Native only partially provides.
20
20
  - Defaults to `https://api.vxil.com`; pass `baseUrl` to target another environment.
21
21
  - Every feature the tenant has enabled is available as a typed namespace; generate a project-exact client with `npx @vxil/cli gen`.
22
22
  - Every non-2xx response throws a `VxilError` carrying the structured error envelope (`status`, `code`, `message`, `hint`, `fixUrl`, `requestId`, and `retryAfter` in seconds when the server sent `Retry-After`).
package/dist/index.d.ts CHANGED
@@ -367,6 +367,21 @@ export interface JobRun {
367
367
  /** the single-run read (`vx.jobs.run` / `waitForRun`) only: your enqueue
368
368
  * payload — `{ redacted: true }` for a platform-internal run; never on lists */
369
369
  payload_json?: unknown;
370
+ /** a platform webhook delivery run (`job_name: 'webhooks.deliver'`) only:
371
+ * the event type it delivers (e.g. `payments.charge.refunded`) — so a dead
372
+ * delivery names WHICH event was lost while its payload stays redacted;
373
+ * null on every other run (list + single read, since 2026-10-01) */
374
+ webhook_event?: string | null;
375
+ /** a platform webhook delivery run only: the event's id — the `audit_id`
376
+ * the delivery carries to your handler and the row id in `GET /v1/audit`;
377
+ * null on every other run */
378
+ audit_id?: number | null;
379
+ /** the schedule (`sch_…`) that fired this run; null for a run you enqueued */
380
+ schedule_id?: string | null;
381
+ /** the SLOT a schedule-fired run was due for (the schedule's next_run_at at
382
+ * the fire); `started_at − scheduled_for` is how late it started. null for a
383
+ * run you enqueued (and for runs fired before 2026-10-01). */
384
+ scheduled_for?: string | null;
370
385
  }
371
386
  /** The run states no later write can move — what `waitForRun` and an
372
387
  * async+wait invoke resolve `done: true` on. */
@@ -404,13 +419,35 @@ export interface JobFlowRule {
404
419
  state: string;
405
420
  created_at: string;
406
421
  }
422
+ /** A schedule that fired on time but whose run has not STARTED for more than
423
+ * two of the schedule's intervals past its slot — held downstream (a busy
424
+ * queue, your concurrency cap, a flow rule). Announced once as
425
+ * `jobs.schedule.missed` with `reason: 'held'` — `functions.schedule.missed`
426
+ * (with `function`) for a function's cron trigger (`fn-cron:*`). */
427
+ export interface JobScheduleHeld {
428
+ run_id: string;
429
+ scheduled_for: string;
430
+ held_seconds: number;
431
+ }
407
432
  export interface JobSchedule {
408
433
  schedule_id: string;
409
434
  job_name: string;
410
- cron: string | null;
411
- run_at: string | null;
435
+ /** the create echo carries no cron; the list names it `cron_expr` */
436
+ cron?: string | null;
437
+ cron_expr?: string | null;
438
+ run_at?: string | null;
412
439
  next_run_at: string | null;
440
+ last_run_at?: string | null;
413
441
  state: string;
442
+ /** `'skip'`: a due slot fires nothing while an earlier run of this schedule
443
+ * is still open (queued / running / retrying / waiting / delayed); the slot
444
+ * is counted in `skipped_fires`. `'allow'` (default): every slot fires. */
445
+ overlap?: 'allow' | 'skip';
446
+ /** slots skipped since the schedule last fired a run (list only) */
447
+ skipped_fires?: number;
448
+ last_skipped_at?: string | null;
449
+ /** non-null while the schedule is HELD (list only) */
450
+ held?: JobScheduleHeld | null;
414
451
  }
415
452
  export interface AuthSession {
416
453
  token: string;
@@ -557,11 +594,42 @@ export interface PaymentsCharge {
557
594
  /** the one-off product the charge bought (the `ledger.productMap` key), if any */
558
595
  product_id: string | null;
559
596
  amount_cents: number;
597
+ /** SETTLED refunds only (2026-10-01): a refund still awaiting the provider is
598
+ * in `amount_refund_pending`, and `status` follows the settled total */
560
599
  amount_refunded: number;
600
+ /** reserved by outbound refunds the provider has not settled yet (a Paddle
601
+ * adjustment awaiting approval); moves to `amount_refunded` when approved,
602
+ * back to 0 when refused */
603
+ amount_refund_pending: number;
561
604
  currency: string;
605
+ /** succeeded | partially_refunded | refunded | disputed | pending | failed —
606
+ * of SETTLED money */
562
607
  status: string;
608
+ /** the provider-reported environment of the money (2026-10-01): a store
609
+ * reviewer's sandbox purchase (`revenuecat.sandboxUsers`) is `sandbox` —
610
+ * leave it out of revenue */
611
+ environment: PaymentsEventEnvironment;
563
612
  created_at: string;
564
613
  }
614
+ /** A refund row as listed by GET /v1/payments/refunds (2026-10-01). */
615
+ export interface PaymentsRefund {
616
+ /** `ref_…` — what `createRefund` returned, and what a 502 / `payments.refund.failed`
617
+ * / `payments.charge.refunded` names */
618
+ refund_id: string;
619
+ charge_id: string;
620
+ provider: string;
621
+ /** the provider's own refund / adjustment id (null until the provider answered) */
622
+ provider_refund_id: string | null;
623
+ amount_cents: number;
624
+ currency: string;
625
+ reason: string | null;
626
+ /** `pending` until the provider settles it (a Paddle adjustment awaiting approval) */
627
+ status: 'pending' | 'succeeded' | 'failed';
628
+ /** `api` = issued through POST /v1/payments/refunds; `webhook` = recorded from the
629
+ * provider (a refund made in the provider's own dashboard) */
630
+ source: 'api' | 'webhook';
631
+ created_at: string | null;
632
+ }
565
633
  /** A subscription row as listed by GET /v1/payments/subscriptions (payments.md
566
634
  * §3) — provider subscriptions, manual grants and purchase passes alike.
567
635
  * `current_period_start` (2026-09-25) is the row's own window start: for a
@@ -797,8 +865,9 @@ export interface AiTokenExpiringEvent {
797
865
  remint_path: string;
798
866
  }
799
867
  /** A job-routed async generation handle (POST /v1/ai/generate with mode:'job').
800
- * The jobs run drives the provider call; read the answer via the replay buffer
801
- * (`resume_path`) or track the run through the jobs surface. */
868
+ * The jobs run drives the provider call; read the settled answer with
869
+ * `ai.getGeneration(generation_id)` (kept 30 days) or via `resume_path`, or
870
+ * track the run through the jobs surface. */
802
871
  export interface AiJobHandle {
803
872
  generation_id: string;
804
873
  run_id: string;
@@ -806,8 +875,46 @@ export interface AiJobHandle {
806
875
  * `job.generation.completed|failed` events carry BOTH `generation_id` and
807
876
  * `correlation_id` next to `run_id`, so no run→record link is needed. */
808
877
  correlation_id?: string | null;
809
- status: string;
878
+ /** `pending` on a fresh handle; on a replay (`deduplicated`) the
879
+ * generation's status NOW — `processing`, `completed` or `failed` too */
880
+ status: 'pending' | 'processing' | 'completed' | 'failed';
881
+ /** a /v1 route, stable within the v1 major */
810
882
  resume_path: string;
883
+ /** true when this answer is the REPLAY of an earlier request carrying the
884
+ * same `Idempotency-Key` and body (within 24 h) — nothing was enqueued
885
+ * again. The same key with a DIFFERENT body is `422 idempotency_key_reused`. */
886
+ deduplicated?: true;
887
+ }
888
+ /** One generation as `GET /v1/ai/generations/{id}` (and the `?correlation_id=`
889
+ * lookup) reads it. `result` is the settled answer of a JOB-lane generation
890
+ * (`generateAsync`), kept 30 days after it settles; a sync/stream generation
891
+ * returned its answer to you directly and records none. The lookup always
892
+ * carries `result: null` — read the text by id. */
893
+ export interface AiGenerationRecord {
894
+ generation_id: string;
895
+ correlation_id: string | null;
896
+ /** the jobs run carrying a job-lane generation (null on the other lanes) */
897
+ run_id: string | null;
898
+ status: 'pending' | 'processing' | 'completed' | 'failed';
899
+ provider: string;
900
+ model: string;
901
+ template: string | null;
902
+ usage: {
903
+ input_tokens: number;
904
+ output_tokens: number;
905
+ };
906
+ cached: boolean;
907
+ created_at: string;
908
+ settled_at: string | null;
909
+ result: {
910
+ text: string;
911
+ finish: string;
912
+ truncated: boolean;
913
+ } | null;
914
+ /** `stored` (in `result`) · `purged` (past the 30-day retention, or the
915
+ * user was erased) · `not_recorded` (not settled yet, or a sync/stream
916
+ * generation) */
917
+ result_state: 'stored' | 'purged' | 'not_recorded';
811
918
  }
812
919
  /** The `data` of `job.generation.queued` (jobs.md §11): the run was accepted. */
813
920
  export interface JobGenerationQueuedEventPayload {
@@ -823,15 +930,18 @@ export interface JobGenerationQueuedEventPayload {
823
930
  /** when the run is failed as expired if the provider never settles it */
824
931
  expires_at: string;
825
932
  }
826
- /** The `data` of `job.generation.completed` / `job.generation.failed`. */
933
+ /** The `data` of `job.generation.completed` / `job.generation.failed`. Every
934
+ * run that emitted `job.generation.queued` ends with exactly one of the two —
935
+ * a run refused at its credit hold included (`failed`, `error_class:
936
+ * 'ReserveInsufficient'`, beside the older `job.generation.reserve_aborted`). */
827
937
  export interface JobGenerationSettledEventPayload {
828
938
  run_id: string;
829
939
  generation_id: string | null;
830
940
  correlation_id: string | null;
831
941
  /** `completed` on the healthy terminal, `failed` on the broken one */
832
942
  status: 'completed' | 'failed';
833
- /** the run's SETTLED error class (`provider_error`, `PollExhausted`, …);
834
- * always null on `completed` */
943
+ /** the run's SETTLED error class (`provider_error`, `PollExhausted`,
944
+ * `GenerationExpired`, `ReserveInsufficient`, …); always null on `completed` */
835
945
  error_class: string | null;
836
946
  /** the provider hint alone (`Upstream said: gemini 400: …`); null on
837
947
  * `completed` and whenever the failure carried none */
@@ -859,15 +969,274 @@ export interface JobRunEventPayload {
859
969
  * exhausted its attempts normally. */
860
970
  reason?: 'reaped' | 'queue_backstop';
861
971
  }
862
- /** Event name → typed `data`, for the events that settle work you started.
863
- * `VxilEventPayload<'job.generation.failed'>` names one; everything else on
864
- * the catalog is `Record<string, unknown>` until typed here. */
972
+ /** The provider-reported environment of the money (`production` | `sandbox`). */
973
+ export type PaymentsEventEnvironment = 'production' | 'sandbox';
974
+ /** Which credits a debit spends: ONE `credit_type`, or an ORDERED
975
+ * `credit_types` (1–8 distinct) — the whole amount from the FIRST type whose
976
+ * available balance covers it, never split across types (2026-10-01 W9). */
977
+ export type PaymentsCreditTypeChoice = {
978
+ credit_type: string;
979
+ credit_types?: never;
980
+ } | {
981
+ credit_types: string[];
982
+ credit_type?: never;
983
+ };
984
+ /** `POST /v1/payments/credits/consume` → `data`. */
985
+ export interface PaymentsConsumeResult {
986
+ balance_after: number;
987
+ ledger_entry_id: string | null;
988
+ reversed: boolean;
989
+ /** the credit type the debit came from (the chosen one for an ordered consume) */
990
+ credit_type: string;
991
+ }
992
+ /** `payments.charge.succeeded` (a new charge row) / `payments.charge.completed`
993
+ * (a recorded charge reached the provider's completed state — Paddle's
994
+ * `transaction.completed` after `transaction.paid`). */
995
+ export interface PaymentsChargeEventPayload {
996
+ end_user_id: string;
997
+ provider: string;
998
+ /** the vxil charge id (`chg_…`) */
999
+ charge_id: string | null;
1000
+ /** the provider's own transaction / payment id */
1001
+ provider_charge_id: string | null;
1002
+ amount_cents: number | null;
1003
+ currency: string;
1004
+ product_id: string | null;
1005
+ provider_status: PaymentsProviderChargeStatus | null;
1006
+ environment: PaymentsEventEnvironment;
1007
+ level: 'info';
1008
+ }
1009
+ /** `payments.charge.refunded` / `payments.charge.disputed` — the SAME keys on
1010
+ * the outbound route (`source: 'api'`, emitted when the route fully refunds a
1011
+ * charge) and on a provider delivery (`source: 'webhook'`). */
1012
+ export interface PaymentsChargeRefundedEventPayload {
1013
+ end_user_id: string;
1014
+ provider: string;
1015
+ charge_id: string | null;
1016
+ provider_charge_id: string | null;
1017
+ provider_refund_id: string | null;
1018
+ /** the refunds row (`ref_…`, readable at `GET /v1/payments/refunds?refund_id=`);
1019
+ * null for a dispute */
1020
+ refund_id: string | null;
1021
+ /** what THIS refund moved (a Stripe delivery's own delta, never the running total) */
1022
+ amount_cents: number | null;
1023
+ currency: string;
1024
+ /** the charge is fully refunded (or disputed): what it bought has ended */
1025
+ full: boolean;
1026
+ reason: string | null;
1027
+ /** the credits taken back, per credit type (bounded by what was unspent) */
1028
+ reversed: Array<{
1029
+ credit_type: string;
1030
+ amount: number;
1031
+ }>;
1032
+ source: 'api' | 'webhook';
1033
+ environment: PaymentsEventEnvironment;
1034
+ level: 'info';
1035
+ }
1036
+ /** `payments.refund.failed` — the provider refused (or the call failed) and the
1037
+ * reserve went back onto the charge. */
1038
+ export interface PaymentsRefundFailedEventPayload {
1039
+ refund_id: string;
1040
+ charge_id: string;
1041
+ end_user_id: string | null;
1042
+ provider: string;
1043
+ provider_charge_id: string | null;
1044
+ provider_refund_id: string | null;
1045
+ amount_cents: number;
1046
+ reason: string | null;
1047
+ source: 'api' | 'webhook';
1048
+ environment: PaymentsEventEnvironment;
1049
+ level: 'warn';
1050
+ state: 'broken';
1051
+ }
1052
+ /** `payments.subscription.<status>` (`trialing` | `active` | `past_due` |
1053
+ * `cancelled` | `expired` | `lapsed`). `slack_hours` is set on `lapsed` only
1054
+ * (the period-end sweep's own ending). */
1055
+ export interface PaymentsSubscriptionStatusEventPayload {
1056
+ end_user_id: string;
1057
+ subscription_id: string;
1058
+ provider: string;
1059
+ provider_sub_id: string | null;
1060
+ tier: string | null;
1061
+ status: 'trialing' | 'active' | 'past_due' | 'cancelled' | 'expired' | 'lapsed';
1062
+ current_period_start: string | null;
1063
+ current_period_end: string | null;
1064
+ environment: PaymentsEventEnvironment;
1065
+ /** what wrote it: the provider event type, `subscription.synced` /
1066
+ * `subscription.restored`, or `period_end_enforced` */
1067
+ event_type: string;
1068
+ slack_hours: number | null;
1069
+ level: 'info';
1070
+ state: 'ok';
1071
+ }
1072
+ /** `payments.subscription.granted` — a manual / support grant (`source:
1073
+ * 'manual'`) or a purchase pass the fold wrote (`source: 'purchase'`). */
1074
+ export interface PaymentsSubscriptionGrantedEventPayload {
1075
+ end_user_id: string;
1076
+ subscription_id: string | null;
1077
+ tier: string;
1078
+ /** the window start (a queued pass's first day may be in the future) */
1079
+ since: string;
1080
+ until: string | null;
1081
+ reason: string;
1082
+ source: 'manual' | 'purchase';
1083
+ charge_id: string | null;
1084
+ /** the manual grant's own key; null for a purchase */
1085
+ idempotency_key: string | null;
1086
+ /** the product a purchase bought; null for a manual grant */
1087
+ product_id: string | null;
1088
+ /** the provider of the purchase; null for a manual grant */
1089
+ provider: string | null;
1090
+ environment: PaymentsEventEnvironment | null;
1091
+ level: 'info';
1092
+ state: 'ok';
1093
+ }
1094
+ /** `payments.subscription.revoked` — a row ended by `/revoke` or `DELETE`, or by
1095
+ * its charge's full refund / chargeback. */
1096
+ export interface PaymentsSubscriptionRevokedEventPayload {
1097
+ end_user_id: string;
1098
+ subscription_id: string;
1099
+ tier: string | null;
1100
+ via: 'delete' | 'revoke' | 'refund' | 'chargeback';
1101
+ charge_id: string | null;
1102
+ provider_refund_id: string | null;
1103
+ level: 'info';
1104
+ state: 'ok';
1105
+ }
1106
+ /** `payments.subscription.transferred` — a RevenueCat TRANSFER (`reason:
1107
+ * 'transfer'`) or the account-merge re-key (`reason: 'account_merge'`,
1108
+ * `provider: null`). */
1109
+ export interface PaymentsSubscriptionTransferredEventPayload {
1110
+ end_user_id: string;
1111
+ from_user_ids: string[];
1112
+ moved: number;
1113
+ credits_moved: number;
1114
+ provider: string | null;
1115
+ reason: 'transfer' | 'account_merge';
1116
+ environment: PaymentsEventEnvironment;
1117
+ level: 'info';
1118
+ }
1119
+ /** `payments.entitlement.changed` — the user's resolved snapshot changed. */
1120
+ export interface PaymentsEntitlementChangedEventPayload {
1121
+ end_user_id: string;
1122
+ from_tier: string | null;
1123
+ tier: string;
1124
+ entitlements: string[];
1125
+ quotas: Record<string, number>;
1126
+ source_sub_id: string | null;
1127
+ status: string | null;
1128
+ until: string | null;
1129
+ reason: string;
1130
+ environment: PaymentsEventEnvironment;
1131
+ level: 'info';
1132
+ }
1133
+ /** `payments.entitlement.revoke.failed` — the daily sweep found a refunded or
1134
+ * disputed charge whose access is still live (`refunded_but_entitled`). */
1135
+ export interface PaymentsEntitlementRevokeFailedEventPayload {
1136
+ kind: 'refunded_but_entitled';
1137
+ count: number;
1138
+ subscription_ids: string[];
1139
+ level: 'error';
1140
+ state: 'broken';
1141
+ }
1142
+ /** `payments.grant.failed` — a recurring tier grant could not be written. */
1143
+ export interface PaymentsGrantFailedEventPayload {
1144
+ end_user_id: string;
1145
+ credit_type: string;
1146
+ amount: number;
1147
+ grant_key: string;
1148
+ source: string;
1149
+ error: string;
1150
+ level: 'error';
1151
+ state: 'broken';
1152
+ }
1153
+ /** `payments.webhook.rejected` — a signature / parse failure was persisted. */
1154
+ export interface PaymentsWebhookRejectedEventPayload {
1155
+ event_id: string;
1156
+ provider: string;
1157
+ reason: 'sig_failed' | 'parse_failed';
1158
+ fingerprint: string;
1159
+ level: 'warn';
1160
+ state: 'broken';
1161
+ }
1162
+ /** `payments.webhook_event.failed` — a fold threw or resolved to `error`. */
1163
+ export interface PaymentsWebhookEventFailedEventPayload {
1164
+ /** the delivery-log row (`whe_…`) when known */
1165
+ event_id: string | null;
1166
+ provider: string;
1167
+ /** the provider's own event id when known */
1168
+ provider_evt_id: string | null;
1169
+ event_type: string;
1170
+ error: string;
1171
+ level: 'error';
1172
+ state: 'broken';
1173
+ }
1174
+ /** `payments.webhook_event.reprocessed` — an operator replay ran. */
1175
+ export interface PaymentsWebhookEventReprocessedEventPayload {
1176
+ event_id: string;
1177
+ provider: string;
1178
+ event_type: string;
1179
+ }
1180
+ /** `payments.reconcile.discrepancy` — one per finding kind per daily sweep. */
1181
+ export interface PaymentsReconcileDiscrepancyEventPayload {
1182
+ kind: string;
1183
+ count: number;
1184
+ /** opaque ids only (users / subscriptions / charges / events / refunds) */
1185
+ ids: string[];
1186
+ level: 'error';
1187
+ state: 'broken';
1188
+ }
1189
+ /** `payments.reconcile.ok` — the tenant crossed back to clean. */
1190
+ export interface PaymentsReconcileOkEventPayload {
1191
+ level: 'info';
1192
+ state: 'ok';
1193
+ }
1194
+ /** `payments.conformance` — a lifecycle simulation ran (mock/dev tenants). */
1195
+ export interface PaymentsConformanceEventPayload {
1196
+ scenario: string;
1197
+ run_id: string;
1198
+ end_user_id: string;
1199
+ tier: string;
1200
+ expected: Record<string, unknown>;
1201
+ observed: Record<string, unknown>;
1202
+ pass: boolean;
1203
+ tenant_kind: string;
1204
+ level: 'info' | 'error';
1205
+ state: 'ok' | 'broken';
1206
+ }
1207
+ /** Event name → typed `data`, for the events that settle work you started and
1208
+ * (since 2026-10-01) every `payments.*` event. `VxilEventPayload<'job.generation.failed'>`
1209
+ * names one; everything else on the catalog is `Record<string, unknown>` until
1210
+ * typed here. */
865
1211
  export interface VxilEventPayloads {
866
1212
  'job.generation.queued': JobGenerationQueuedEventPayload;
867
1213
  'job.generation.completed': JobGenerationSettledEventPayload;
868
1214
  'job.generation.failed': JobGenerationSettledEventPayload;
869
1215
  'job.succeeded': JobRunEventPayload;
870
1216
  'job.dead_lettered': JobRunEventPayload;
1217
+ 'payments.charge.succeeded': PaymentsChargeEventPayload;
1218
+ 'payments.charge.completed': PaymentsChargeEventPayload;
1219
+ 'payments.charge.refunded': PaymentsChargeRefundedEventPayload;
1220
+ 'payments.charge.disputed': PaymentsChargeRefundedEventPayload;
1221
+ 'payments.refund.failed': PaymentsRefundFailedEventPayload;
1222
+ 'payments.subscription.trialing': PaymentsSubscriptionStatusEventPayload;
1223
+ 'payments.subscription.active': PaymentsSubscriptionStatusEventPayload;
1224
+ 'payments.subscription.past_due': PaymentsSubscriptionStatusEventPayload;
1225
+ 'payments.subscription.cancelled': PaymentsSubscriptionStatusEventPayload;
1226
+ 'payments.subscription.expired': PaymentsSubscriptionStatusEventPayload;
1227
+ 'payments.subscription.lapsed': PaymentsSubscriptionStatusEventPayload;
1228
+ 'payments.subscription.granted': PaymentsSubscriptionGrantedEventPayload;
1229
+ 'payments.subscription.revoked': PaymentsSubscriptionRevokedEventPayload;
1230
+ 'payments.subscription.transferred': PaymentsSubscriptionTransferredEventPayload;
1231
+ 'payments.entitlement.changed': PaymentsEntitlementChangedEventPayload;
1232
+ 'payments.entitlement.revoke.failed': PaymentsEntitlementRevokeFailedEventPayload;
1233
+ 'payments.grant.failed': PaymentsGrantFailedEventPayload;
1234
+ 'payments.webhook.rejected': PaymentsWebhookRejectedEventPayload;
1235
+ 'payments.webhook_event.failed': PaymentsWebhookEventFailedEventPayload;
1236
+ 'payments.webhook_event.reprocessed': PaymentsWebhookEventReprocessedEventPayload;
1237
+ 'payments.reconcile.discrepancy': PaymentsReconcileDiscrepancyEventPayload;
1238
+ 'payments.reconcile.ok': PaymentsReconcileOkEventPayload;
1239
+ 'payments.conformance': PaymentsConformanceEventPayload;
871
1240
  }
872
1241
  export type VxilEventPayload<E extends string> = E extends keyof VxilEventPayloads ? VxilEventPayloads[E] : Record<string, unknown>;
873
1242
  /** The `trigger` label on the envelope. The config spells two of them in
@@ -960,10 +1329,14 @@ export interface QueueFunctionEnvelope<P = unknown> extends FunctionEnvelopeBase
960
1329
  trigger: 'queue';
961
1330
  payload: P;
962
1331
  }
963
- /** A schedule tick carries no data: `payload` is `{}`. */
1332
+ /** A schedule tick carries no data: `payload` is `{}`. `scheduled_for` is the
1333
+ * SLOT this tick was due for (ISO, UTC) — stable across a late start and a
1334
+ * re-delivery, so key per-slot work on it (a rollup "for 13:15"). Absent only
1335
+ * on a tick fired before 2026-10-01. */
964
1336
  export interface CronFunctionEnvelope extends FunctionEnvelopeBase {
965
1337
  trigger: 'cron';
966
1338
  payload: Record<string, unknown>;
1339
+ scheduled_for?: string;
967
1340
  }
968
1341
  export interface WebhookFunctionEnvelope<D = Record<string, unknown>> extends FunctionEnvelopeBase {
969
1342
  trigger: 'webhook';
@@ -984,6 +1357,21 @@ export interface JobDelivery<P = unknown> {
984
1357
  attempt: number;
985
1358
  /** exactly what the run was enqueued with */
986
1359
  payload: P;
1360
+ /** a schedule-fired run only: the slot it was due for (ISO, UTC) */
1361
+ scheduled_for?: string;
1362
+ }
1363
+ /** The `?since=` replay read (GET /v1/ai/generations/{id}/stream). */
1364
+ export interface AiResumePage {
1365
+ generation_id: string;
1366
+ correlation_id: string | null;
1367
+ since: number;
1368
+ status: 'pending' | 'processing' | 'completed' | 'failed';
1369
+ frames: Array<Record<string, unknown>>;
1370
+ done: boolean;
1371
+ max_seq: number;
1372
+ /** the generation settled but its frames are gone (the 1 h buffer was
1373
+ * reclaimed, or a job-lane answer passed its 30-day retention) */
1374
+ expired: boolean;
987
1375
  }
988
1376
  /** A re-minted mid-stream connect token (GET /v1/ai/generations/{id}/token). */
989
1377
  export interface AiStreamToken {
@@ -1424,6 +1812,47 @@ export interface VxilSchemaShape {
1424
1812
  Output: unknown;
1425
1813
  }>;
1426
1814
  }
1815
+ /** One deployed tenant function as GET /v1/functions lists it. */
1816
+ export interface DeployedFunction {
1817
+ name: string;
1818
+ /** the content-addressed script name (`fn-<tenant8>-<name>-<sha12>`) */
1819
+ scriptRef: string;
1820
+ /** `/v1/fn/<name>` */
1821
+ endpoint: string;
1822
+ scopes: string[];
1823
+ egressAllow: string[];
1824
+ /** secret REFS only (`secret:<name>`) — never values */
1825
+ secrets: string[];
1826
+ enabled: boolean;
1827
+ trigger: string;
1828
+ bindings: Array<{
1829
+ kind: string;
1830
+ schedule?: string;
1831
+ source?: string;
1832
+ path?: string;
1833
+ collection?: string;
1834
+ event?: string;
1835
+ retry?: {
1836
+ maxAttempts: number;
1837
+ };
1838
+ }>;
1839
+ signature: {
1840
+ input?: unknown;
1841
+ output?: unknown;
1842
+ } | null;
1843
+ limits: {
1844
+ cpuMs?: number;
1845
+ timeoutMs?: number;
1846
+ } | null;
1847
+ /** the runtime settings the deployed script runs under; `cpuMs` = the
1848
+ * per-invocation CPU limit (ms) its script carries — min(limits.cpuMs, the
1849
+ * plan's ceiling). Absent on scripts deployed before it was recorded. */
1850
+ runtime?: {
1851
+ compatibilityDate: string;
1852
+ compatibilityFlags: string[];
1853
+ cpuMs?: number;
1854
+ };
1855
+ }
1427
1856
  /** Per-call options of `vx.fn.<name>(input, opts)`. */
1428
1857
  export interface FnInvokeOptions {
1429
1858
  /** `true` ⇒ the ASYNC http lane (`x-vxil-async: 1`): the call resolves to the
@@ -2425,13 +2854,26 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2425
2854
  * generation_status onto a tenant record, enforces a built-in timeout, and
2426
2855
  * (on failure) fires the payments credit-reversal.
2427
2856
  *
2857
+ * The answer's `generation_status` is `pending` on a fresh enqueue; a replay
2858
+ * of the same `idempotency_key` (`deduplicated: true`) carries the run's
2859
+ * status NOW (`processing`, `completed` or `failed` too).
2860
+ *
2428
2861
  * `reserve_credits` (§11.8) takes a PROVISIONAL held credit debit at enqueue
2429
2862
  * (linked to the run), committed on `completed` and reversed on
2430
2863
  * failed/timeout/DLQ. `amount` is positive-only and CLAMPED to the platform
2431
2864
  * `config.generation.maxReserveCredits` cap; in end-user mode `user_id` is
2432
2865
  * FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
2433
- * the enqueue (insufficient balance); a runaway over the per-tenant
2434
- * outstanding-holds ceiling → 429. */
2866
+ * the enqueue (insufficient balance; the run ends with job.generation.failed
2867
+ * `ReserveInsufficient` and the 402 names its `run_id`); a runaway over the
2868
+ * per-tenant outstanding-holds ceiling → 429 (Retry-After 5).
2869
+ *
2870
+ * Generic completion options for queue-style providers (fal.ai and alike —
2871
+ * no vendor adapter): `completion.callback.query_param` puts the signed
2872
+ * callback URL in the provider URL's query string (`?fal_webhook=…`) and
2873
+ * sends `provider.body` exactly; `completion.status_map` maps up to 8
2874
+ * provider words to completed | failed | processing (`{ OK: 'completed',
2875
+ * ERROR: 'failed' }`); `completion.result_path` names the value the run
2876
+ * settles with — the status mirror receives it as `result`. */
2435
2877
  generation: (input: {
2436
2878
  job_name: string;
2437
2879
  provider: {
@@ -2449,6 +2891,14 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2449
2891
  headers?: Record<string, string>;
2450
2892
  interval_ms?: number;
2451
2893
  };
2894
+ /** webhook mode: the signed callback URL rides `?<query_param>=` */
2895
+ callback?: {
2896
+ query_param: string;
2897
+ };
2898
+ /** ≤ 8 provider words → the run's terminal (or processing) state */
2899
+ status_map?: Record<string, "completed" | "failed" | "processing">;
2900
+ /** dotted path of the value a completed run settles with (mirrored as `result`) */
2901
+ result_path?: string;
2452
2902
  };
2453
2903
  status_mirror?: {
2454
2904
  feature: string;
@@ -2467,10 +2917,11 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2467
2917
  };
2468
2918
  };
2469
2919
  };
2470
- reserve_credits?: {
2920
+ /** the held debit; name ONE `credit_type`, or an ordered `credit_types`
2921
+ * (the whole amount is held on the first type that covers it) */
2922
+ reserve_credits?: PaymentsCreditTypeChoice & {
2471
2923
  amount: number;
2472
2924
  user_id: string;
2473
- credit_type: string;
2474
2925
  reason?: string;
2475
2926
  };
2476
2927
  payload?: Record<string, unknown>;
@@ -2532,13 +2983,16 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2532
2983
  /** Secret for verifying X-Vxil-Jobs-Signature on your callback endpoints. */
2533
2984
  signingSecret: () => Promise<string>;
2534
2985
  schedules: {
2535
- /** Recurring (5-field cron, UTC) or one-shot (run_at). Exactly one of cron/run_at. */
2986
+ /** Recurring (5-field cron, UTC) or one-shot (run_at). Exactly one of cron/run_at.
2987
+ * `overlap: 'skip'` (cron only): no new run while the previous one is
2988
+ * still open — a missed window is never caught up either way. */
2536
2989
  create: (input: {
2537
2990
  job_name: string;
2538
2991
  target_url: string;
2539
2992
  payload?: Record<string, unknown>;
2540
2993
  cron?: string;
2541
2994
  run_at?: string;
2995
+ overlap?: "allow" | "skip";
2542
2996
  }) => Promise<JobSchedule>;
2543
2997
  list: () => Promise<JobSchedule[]>;
2544
2998
  delete: (scheduleId: string) => Promise<void>;
@@ -2791,6 +3245,28 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2791
3245
  user_id: string;
2792
3246
  erased: boolean;
2793
3247
  }>;
3248
+ /** LIVE-APP MIGRATION (server only, `auth:write`; 1–500 ids per call):
3249
+ * give ids already registered through `users.bulk` their sign-in
3250
+ * identity, carrying each user's SOURCE password hash — bcrypt
3251
+ * (`$2a$`/`$2b$`/`$2y$`, cost 4–10; verified once at that user's first
3252
+ * sign-in and replaced there) or a hash from vxil's own owner-only
3253
+ * credential export. An existing identity is updated only while it still
3254
+ * holds an imported hash; one with a vxil password, or a passwordless
3255
+ * (OAuth / magic-link) one, is `kept`, never overwritten. Ids are unique
3256
+ * across every project in an environment: an id another project holds
3257
+ * (e.g. a rehearsal dev backend) is `skipped` / `id_taken`. `oidc_issuer` links each id as that
3258
+ * issuer's subject, so an installed app can trade its old session token
3259
+ * for a vxil one (`auth.oauth.native('oidc', …)`). One result per id:
3260
+ * `created` / `updated` / `kept` / `skipped` + `reason`. `vxil migrate
3261
+ * data --with-password-hashes` drives this for you. */
3262
+ import: (input: {
3263
+ users: Array<{
3264
+ id: string;
3265
+ password_hash?: string | null;
3266
+ email_verified?: boolean;
3267
+ }>;
3268
+ oidc_issuer?: string;
3269
+ }) => Promise<AuthImportResult>;
2794
3270
  /** END-USER MODE ONLY (`endUserToken` / `asEndUser`): the signed-in user
2795
3271
  * deep-merges a bounded `attributes` object into its OWN shared identity
2796
3272
  * row (null deletes a key; ≤8 KB, ≤64 top-level keys, depth ≤8). Profile
@@ -3184,6 +3660,11 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
3184
3660
  * admits, including unslotted range ops (served by unindexed
3185
3661
  * scans, bounded only by the statement timeout) that the generated
3186
3662
  * per-field `Filterable` unions on `vx.from(...).query` exclude.
3663
+ *
3664
+ * Paging: feed `next_cursor` back as `cursor` with the SAME `filter` and
3665
+ * `sort` — it works for the default order and (since 2026-10-01) for any
3666
+ * slot-indexed or `created_at`/`updated_at`/`published_at` sort; a cursor
3667
+ * sent with a different sort is a 422. A joined (dotted) sort has no cursor.
3187
3668
  */
3188
3669
  query: (collection: string, q?: {
3189
3670
  filter?: Record<string, unknown>;
@@ -3508,6 +3989,56 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
3508
3989
  blocked_state: boolean;
3509
3990
  }>;
3510
3991
  };
3992
+ /**
3993
+ * The deployed tenant functions — the MANAGEMENT surface (`vx.fn.<name>()`
3994
+ * invokes them). Deploying stays on `vxil push` / `vxil functions deploy`
3995
+ * (the CLI bundles the source); this namespace reads and removes.
3996
+ */
3997
+ readonly functions: {
3998
+ /** The deployed functions (name, endpoint, bindings, scopes, limits, the
3999
+ * runtime each script runs under), `runtime`, the settings a deploy uses
4000
+ * now, and `cpuMsCeiling`, the per-invocation CPU limit (ms) the plan
4001
+ * gives a function now (its own `limits.cpuMs` can only lower it). Any
4002
+ * authenticated key may read it. (GET /v1/functions) */
4003
+ list: () => Promise<{
4004
+ functions: DeployedFunction[];
4005
+ runtime?: {
4006
+ compatibilityDate: string;
4007
+ compatibilityFlags: string[];
4008
+ };
4009
+ cpuMsCeiling?: number;
4010
+ }>;
4011
+ /**
4012
+ * Remove ONE function from the deployed set — a new functions config
4013
+ * version without it (invokes answer 404, its crons and trigger
4014
+ * subscriptions are unbound). The uploaded code is kept until the nightly
4015
+ * cleanup, so a functions config rollback restores it exactly. Pass
4016
+ * `ifMatch` (the active functions config version) to refuse the delete if
4017
+ * someone else changed the set first (409 `config_version_conflict`);
4018
+ * with or without it, a delete that loses a race to a concurrent deploy
4019
+ * is refused the same way and never erases that deploy. A
4020
+ * name that is not deployed is 404 `function_not_found`. Needs
4021
+ * `features:write`. (DELETE /v1/functions/:name)
4022
+ */
4023
+ delete: (name: string, opts?: {
4024
+ ifMatch?: number;
4025
+ }) => Promise<{
4026
+ deleted: string;
4027
+ version: number;
4028
+ hook_reconcile?: {
4029
+ created: number;
4030
+ updated: number;
4031
+ deleted: number;
4032
+ failed: number;
4033
+ };
4034
+ cron_schedules?: {
4035
+ created: number;
4036
+ deleted: number;
4037
+ failed: number;
4038
+ };
4039
+ warnings?: string[];
4040
+ }>;
4041
+ };
3511
4042
  /** The MCP aggregation surface (the `mcp` feature). */
3512
4043
  readonly mcp: {
3513
4044
  /** Per-tenant secret for verifying the X-Vxil-Mcp-Signature header on custom
@@ -4108,6 +4639,25 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4108
4639
  status: string;
4109
4640
  }>;
4110
4641
  downloadUrl: (objectId: string) => Promise<string>;
4642
+ /** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
4643
+ * — a gallery page costs one request, not one per image. `urls[]` keeps
4644
+ * your order (duplicates collapse); an id that is unknown, someone else's
4645
+ * (end-user mode), deleted or still uploading lands in `errors[]` instead of
4646
+ * failing the batch. Raster images (png/jpeg/webp/gif/avif/heic) are served
4647
+ * `inline`. For expo-image, cache on the object id, not the URL:
4648
+ * `<Image source={{ uri: u.download_url, cacheKey: u.object_id }} />`. */
4649
+ downloadUrls: (objectIds: string[]) => Promise<{
4650
+ urls: Array<{
4651
+ object_id: string;
4652
+ download_url: string;
4653
+ content_type: string;
4654
+ }>;
4655
+ errors: Array<{
4656
+ object_id: string;
4657
+ code: "not_found" | "upload_incomplete";
4658
+ }>;
4659
+ expires_in: number;
4660
+ }>;
4111
4661
  list: (q?: {
4112
4662
  user_id?: string;
4113
4663
  cursor?: string;
@@ -4318,6 +4868,16 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4318
4868
  created_at: string;
4319
4869
  content_sha256?: string;
4320
4870
  }>>;
4871
+ /** Retire a stored template (soft): every live version of the name leaves
4872
+ * the list and can no longer be rendered (by name or by version → 404
4873
+ * `template_not_found`); the rows stay for the generations that used
4874
+ * them, and storing the name again starts a new live version. A name
4875
+ * with no live version is a 404. `vxil push --allow-destructive` retires
4876
+ * the templates a config no longer declares. */
4877
+ retire: (name: string) => Promise<{
4878
+ retired: string;
4879
+ versions: number;
4880
+ }>;
4321
4881
  };
4322
4882
  /** Provider × capability matrix + the tenant's default provider (the set an
4323
4883
  * agent/dashboard reads to know which providers/features are configured). */
@@ -4413,8 +4973,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4413
4973
  correlation_id?: string;
4414
4974
  }) => Promise<AiStreamHandle>;
4415
4975
  /** Job-routed async generation (long/vision/batch): 202 + a jobs run drives
4416
- * the provider call; the settled answer lands in the replay buffer at
4417
- * `resume_path`. Reserve/settle + credit reversal-on-failure ride the run. */
4976
+ * the provider call; the settled answer is stored on the generation (read
4977
+ * it with `getGeneration`, 30 days) and replayed at `resume_path`.
4978
+ * Reserve/settle + credit reversal-on-failure ride the run.
4979
+ * `opts.idempotencyKey` deduplicates for 24 h: a re-send after a timeout
4980
+ * or a lost answer returns the ORIGINAL handle (`deduplicated: true`) and
4981
+ * never pays the provider twice. */
4418
4982
  generateAsync: (input: {
4419
4983
  template?: string;
4420
4984
  template_version?: number;
@@ -4463,22 +5027,35 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4463
5027
  * the row, echoed on the answer and the replay read, and (job mode)
4464
5028
  * carried on the `job.generation.*` events with `generation_id`. */
4465
5029
  correlation_id?: string;
5030
+ }, opts?: {
5031
+ idempotencyKey?: string;
4466
5032
  }) => Promise<AiJobHandle>;
5033
+ /** One generation: status, usage, run, and — for a settled job-lane
5034
+ * generation — the settled answer (`result`, kept 30 days). In end-user
5035
+ * mode only the caller's own generation reads (a foreign id is a 404). */
5036
+ getGeneration: (generationId: string) => Promise<AiGenerationRecord>;
5037
+ /** Find generations by YOUR `correlation_id` (newest first, at most 20) —
5038
+ * the recovery read after a timed-out or lost `generateAsync` answer.
5039
+ * Each carries `result: null` (`result_state` still says whether an answer
5040
+ * is stored): read the text with `getGeneration(generation_id)`. */
5041
+ findGenerations: (q: {
5042
+ correlation_id: string;
5043
+ }) => Promise<{
5044
+ generations: AiGenerationRecord[];
5045
+ }>;
4467
5046
  /** Re-mint a fresh connect token for a LIVE stream (a generation that
4468
5047
  * outlives the ≤300s realtime token TTL); reconnect with `?since=<seq>`. */
4469
5048
  remintToken: (generationId: string) => Promise<AiStreamToken>;
4470
5049
  /** Resume a streamed generation after a dropped socket: every recorded frame
4471
5050
  * with seq > `since` plus `done` (the §2a replay buffer — plain JSON, not
4472
- * an SSE stream). The rag-native mirror is `rag.resume()`. */
5051
+ * an SSE stream; the buffer lives 1 h). A settled JOB-lane generation is
5052
+ * served from its stored answer instead (30 days). `status` says where the
5053
+ * generation is; `expired: true` means it settled but its frames are gone
5054
+ * (`done` is then true — nothing more will come). The rag-native mirror
5055
+ * is `rag.resume()`. */
4473
5056
  resume: (generationId: string, q?: {
4474
5057
  since?: number;
4475
- }) => Promise<{
4476
- generation_id: string;
4477
- since: number;
4478
- frames: Array<Record<string, unknown>>;
4479
- done: boolean;
4480
- max_seq: number;
4481
- }>;
5058
+ }) => Promise<AiResumePage>;
4482
5059
  /** Classify an input into one of YOUR labels (or, with `multi:true`, every
4483
5060
  * label that applies) — a schema-forced verdict over the generate path:
4484
5061
  * the allowed labels ride the structured-output schema as an enum, so the
@@ -4741,21 +5318,22 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4741
5318
  * committed): a linked jobs run that terminally fails auto-refunds the hold,
4742
5319
  * a success settles it. Throws VxilError(402, 'INSUFFICIENT_CREDITS') when the
4743
5320
  * available balance can't cover `amount`.
5321
+ *
5322
+ * Ordered credits: pass `credit_types` (1–8, in spend order — e.g.
5323
+ * `['free', 'subscription', 'topup']`) instead of `credit_type`; the WHOLE
5324
+ * amount comes from the FIRST type whose available balance covers it (one
5325
+ * ledger row, never split across types) and the answer's `credit_type` names
5326
+ * it. A retry with the same key answers the same type.
4744
5327
  */
4745
- consume: (input: {
5328
+ consume: (input: PaymentsCreditTypeChoice & {
4746
5329
  user_id: string;
4747
- credit_type: string;
4748
5330
  amount: number;
4749
5331
  /** link this debit to a jobs run_id → provisional hold + auto-reverse. */
4750
5332
  job_id?: string;
4751
5333
  reason?: string;
4752
5334
  }, opts: {
4753
5335
  idempotencyKey: string;
4754
- }) => Promise<{
4755
- balance_after: number;
4756
- ledger_entry_id: string | null;
4757
- reversed: boolean;
4758
- }>;
5336
+ }) => Promise<PaymentsConsumeResult>;
4759
5337
  /** Additive grant (recurring/top-up) — idempotent per Idempotency-Key. A grant
4760
5338
  * only ADDS; `amount` must be ≥ 1. */
4761
5339
  grant: (input: {
@@ -4870,14 +5448,18 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4870
5448
  * approving webhook flips it to `succeeded`. A rejected adjustment flips it
4871
5449
  * to `failed`, releases the reserved amount back onto the charge and emits
4872
5450
  * `payments.refund.failed`. While a refund is `pending`, the credits the
4873
- * charge bought are NOT yet reversed. Stripe and the mock settle inline.
5451
+ * charge bought are NOT yet reversed, and (since 2026-10-01) the charge's
5452
+ * `amount_refunded` / `status` do not move either — the amount shows in its
5453
+ * `amount_refund_pending` until the provider settles it. Stripe and the
5454
+ * mock settle inline. Poll it with `listRefunds({ refund_id })`.
4874
5455
  *
4875
5456
  * If the provider has nothing left to refund on the charge — e.g. a refund
4876
5457
  * was already issued in the provider's own dashboard, which vxil's running
4877
5458
  * total cannot see — the call returns 422 `refund_not_allocatable` with the
4878
5459
  * precise reason, and nothing is sent to the provider. It is deterministic:
4879
5460
  * retrying the same refund fails the same way. A provider call that actually
4880
- * fails is still 502 `provider_error`.
5461
+ * fails is still 502 `provider_error`. Both failure bodies carry `refund_id`
5462
+ * (the row that went `failed`) so you can tie them to this call.
4881
5463
  */
4882
5464
  createRefund: (input: {
4883
5465
  charge_id: string;
@@ -4989,7 +5571,9 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4989
5571
  /** The last report-only reconciliation sweep result for this project
4990
5572
  * (`run: null` before the first daily tick). Finding kinds:
4991
5573
  * `balance_drift`, `entitlement_lag`, `null_tier`, `refund_pending`,
4992
- * `stranded_webhook`, `terminal_entitled`, `webhook_secret_missing`.
5574
+ * `stranded_webhook`, `terminal_entitled`, `webhook_secret_missing`,
5575
+ * `refunded_but_entitled`, `paid_without_grant`, `refund_reserve_drift`
5576
+ * (a charge's pending refund amount no pending refund explains).
4993
5577
  * SERVER-SIDE KEYS ONLY — a thin-client (`end_user_required`) key gets
4994
5578
  * 403 `server_only`; the sweep is a tenant-wide operator surface. */
4995
5579
  reconcile: () => Promise<{
@@ -5010,18 +5594,45 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
5010
5594
  } | null;
5011
5595
  }>;
5012
5596
  /** List charges newest-first (the refund enabler — discover the charge_id).
5013
- * Filters: user_id, status, limit (clamped 1..100, default 50).
5597
+ * Filters: user_id, status, and (2026-10-01) EXACT `charge_id` /
5598
+ * `provider_charge_id` lookups; keyset-paged — pass `cursor` from a prior
5599
+ * page's `next_cursor` (limit clamped 1..100, default 50). An unknown query
5600
+ * parameter is a 422 `unknown_query_param`, never an unfiltered page.
5014
5601
  * Each row carries the provider's own charge id (`provider_charge_id`),
5015
5602
  * the provider's charge state (`provider_status`: `'paid'` — Paddle's
5016
5603
  * money-taken state — or `'completed'`; null on rows recorded before
5017
5604
  * 2026-09-25) and the one-off `product_id` it bought, so a function can
5018
- * match a charge to a provider event without reading the delivery log. */
5605
+ * match a charge to a provider event without reading the delivery log.
5606
+ * `amount_refunded` / `status` are SETTLED money; a refund awaiting the
5607
+ * provider shows in `amount_refund_pending`. */
5019
5608
  listCharges: (q?: {
5020
5609
  user_id?: string;
5021
- status?: "succeeded" | "failed" | "refunded" | "partially_refunded" | "pending";
5610
+ status?: "succeeded" | "failed" | "refunded" | "partially_refunded" | "pending" | "disputed";
5611
+ /** exact match on the vxil charge id (`chg_…`) */
5612
+ charge_id?: string;
5613
+ /** exact match on the provider's own charge id (a Paddle `txn_…`, a Stripe `pi_…`) */
5614
+ provider_charge_id?: string;
5615
+ /** `production` = revenue; `sandbox` = store-review test purchases */
5616
+ environment?: PaymentsEventEnvironment;
5617
+ cursor?: string;
5022
5618
  limit?: number;
5023
5619
  }) => Promise<{
5024
5620
  charges: PaymentsCharge[];
5621
+ next_cursor: string | null;
5622
+ }>;
5623
+ /** (2026-10-01) The refunds of ONE charge (`charge_id`) or one refund by id
5624
+ * (`refund_id`) — one of the two is required — newest first, with each
5625
+ * refund's `status` (`pending` until the provider settles it). Use it to
5626
+ * tie a `refund_id` from `createRefund`, its 502 / 422 answer or a
5627
+ * `payments.refund.failed` / `payments.charge.refunded` event back to the
5628
+ * money. A thin-client key sees only its own charges' refunds. */
5629
+ listRefunds: (q: {
5630
+ charge_id?: string;
5631
+ refund_id?: string;
5632
+ status?: "pending" | "succeeded" | "failed";
5633
+ limit?: number;
5634
+ }) => Promise<{
5635
+ refunds: PaymentsRefund[];
5025
5636
  }>;
5026
5637
  /** Provider webhook event log (payments.md §7 "Event log & replay"):
5027
5638
  * operator visibility over every delivery — incl. persisted signature
@@ -5068,3 +5679,15 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
5068
5679
  };
5069
5680
  }
5070
5681
  export { withReporting, report, buildEnvelope, parseDsn, exceptionEvent, reportServerErrors, REPORT_TIMEOUT_MS, type ReportingOptions, type ReportEvent, type FetchHandler, } from './reporting.js';
5682
+ /** `auth.users.import` (POST /v1/auth/users/import) — one result per id. */
5683
+ export interface AuthImportResult {
5684
+ results: Array<{
5685
+ id: string;
5686
+ status: 'created' | 'updated' | 'kept' | 'skipped';
5687
+ reason?: 'invalid_id' | 'invalid_hash' | 'duplicate_id' | 'not_registered' | 'no_email' | 'deleted' | 'email_taken' | 'id_taken';
5688
+ }>;
5689
+ created: number;
5690
+ updated: number;
5691
+ kept: number;
5692
+ skipped: number;
5693
+ }
package/dist/index.js CHANGED
@@ -662,13 +662,26 @@ export class Vxil {
662
662
  * generation_status onto a tenant record, enforces a built-in timeout, and
663
663
  * (on failure) fires the payments credit-reversal.
664
664
  *
665
+ * The answer's `generation_status` is `pending` on a fresh enqueue; a replay
666
+ * of the same `idempotency_key` (`deduplicated: true`) carries the run's
667
+ * status NOW (`processing`, `completed` or `failed` too).
668
+ *
665
669
  * `reserve_credits` (§11.8) takes a PROVISIONAL held credit debit at enqueue
666
670
  * (linked to the run), committed on `completed` and reversed on
667
671
  * failed/timeout/DLQ. `amount` is positive-only and CLAMPED to the platform
668
672
  * `config.generation.maxReserveCredits` cap; in end-user mode `user_id` is
669
673
  * FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
670
- * the enqueue (insufficient balance); a runaway over the per-tenant
671
- * outstanding-holds ceiling → 429. */
674
+ * the enqueue (insufficient balance; the run ends with job.generation.failed
675
+ * `ReserveInsufficient` and the 402 names its `run_id`); a runaway over the
676
+ * per-tenant outstanding-holds ceiling → 429 (Retry-After 5).
677
+ *
678
+ * Generic completion options for queue-style providers (fal.ai and alike —
679
+ * no vendor adapter): `completion.callback.query_param` puts the signed
680
+ * callback URL in the provider URL's query string (`?fal_webhook=…`) and
681
+ * sends `provider.body` exactly; `completion.status_map` maps up to 8
682
+ * provider words to completed | failed | processing (`{ OK: 'completed',
683
+ * ERROR: 'failed' }`); `completion.result_path` names the value the run
684
+ * settles with — the status mirror receives it as `result`. */
672
685
  generation: async (input) => (await this.call('POST', '/v1/jobs/generation', input)).data,
673
686
  runs: async (q) => {
674
687
  const s = qs({
@@ -714,7 +727,9 @@ export class Vxil {
714
727
  /** Secret for verifying X-Vxil-Jobs-Signature on your callback endpoints. */
715
728
  signingSecret: async () => (await this.call('GET', '/v1/jobs/signing-secret')).data.signing_secret,
716
729
  schedules: {
717
- /** Recurring (5-field cron, UTC) or one-shot (run_at). Exactly one of cron/run_at. */
730
+ /** Recurring (5-field cron, UTC) or one-shot (run_at). Exactly one of cron/run_at.
731
+ * `overlap: 'skip'` (cron only): no new run while the previous one is
732
+ * still open — a missed window is never caught up either way. */
718
733
  create: async (input) => (await this.call('POST', '/v1/jobs/schedules', input)).data,
719
734
  list: async () => (await this.call('GET', '/v1/jobs/schedules')).data.schedules,
720
735
  delete: async (scheduleId) => {
@@ -856,6 +871,21 @@ export class Vxil {
856
871
  * is SELF-only: `userId` must be the signed-in user, any other id throws
857
872
  * 403 `server_only` (a thin client can never erase someone else). */
858
873
  erase: async (userId) => (await this.call('POST', `/v1/auth/users/${encodeURIComponent(userId)}/erase`, {})).data,
874
+ /** LIVE-APP MIGRATION (server only, `auth:write`; 1–500 ids per call):
875
+ * give ids already registered through `users.bulk` their sign-in
876
+ * identity, carrying each user's SOURCE password hash — bcrypt
877
+ * (`$2a$`/`$2b$`/`$2y$`, cost 4–10; verified once at that user's first
878
+ * sign-in and replaced there) or a hash from vxil's own owner-only
879
+ * credential export. An existing identity is updated only while it still
880
+ * holds an imported hash; one with a vxil password, or a passwordless
881
+ * (OAuth / magic-link) one, is `kept`, never overwritten. Ids are unique
882
+ * across every project in an environment: an id another project holds
883
+ * (e.g. a rehearsal dev backend) is `skipped` / `id_taken`. `oidc_issuer` links each id as that
884
+ * issuer's subject, so an installed app can trade its old session token
885
+ * for a vxil one (`auth.oauth.native('oidc', …)`). One result per id:
886
+ * `created` / `updated` / `kept` / `skipped` + `reason`. `vxil migrate
887
+ * data --with-password-hashes` drives this for you. */
888
+ import: async (input) => (await this.call('POST', '/v1/auth/users/import', input)).data,
859
889
  /** END-USER MODE ONLY (`endUserToken` / `asEndUser`): the signed-in user
860
890
  * deep-merges a bounded `attributes` object into its OWN shared identity
861
891
  * row (null deletes a key; ≤8 KB, ≤64 top-level keys, depth ≤8). Profile
@@ -1152,6 +1182,11 @@ export class Vxil {
1152
1182
  * admits, including unslotted range ops (served by unindexed
1153
1183
  * scans, bounded only by the statement timeout) that the generated
1154
1184
  * per-field `Filterable` unions on `vx.from(...).query` exclude.
1185
+ *
1186
+ * Paging: feed `next_cursor` back as `cursor` with the SAME `filter` and
1187
+ * `sort` — it works for the default order and (since 2026-10-01) for any
1188
+ * slot-indexed or `created_at`/`updated_at`/`published_at` sort; a cursor
1189
+ * sent with a different sort is a 422. A joined (dotted) sort has no cursor.
1155
1190
  */
1156
1191
  query: async (collection, q) => {
1157
1192
  const s = qs({
@@ -1344,6 +1379,32 @@ export class Vxil {
1344
1379
  * tenant's `blockingEnabled` DM config (else 403). */
1345
1380
  block: async (input) => (await this.call('POST', '/v1/dm/blocks', input)).data,
1346
1381
  };
1382
+ /**
1383
+ * The deployed tenant functions — the MANAGEMENT surface (`vx.fn.<name>()`
1384
+ * invokes them). Deploying stays on `vxil push` / `vxil functions deploy`
1385
+ * (the CLI bundles the source); this namespace reads and removes.
1386
+ */
1387
+ functions = {
1388
+ /** The deployed functions (name, endpoint, bindings, scopes, limits, the
1389
+ * runtime each script runs under), `runtime`, the settings a deploy uses
1390
+ * now, and `cpuMsCeiling`, the per-invocation CPU limit (ms) the plan
1391
+ * gives a function now (its own `limits.cpuMs` can only lower it). Any
1392
+ * authenticated key may read it. (GET /v1/functions) */
1393
+ list: async () => (await this.call('GET', '/v1/functions')).data,
1394
+ /**
1395
+ * Remove ONE function from the deployed set — a new functions config
1396
+ * version without it (invokes answer 404, its crons and trigger
1397
+ * subscriptions are unbound). The uploaded code is kept until the nightly
1398
+ * cleanup, so a functions config rollback restores it exactly. Pass
1399
+ * `ifMatch` (the active functions config version) to refuse the delete if
1400
+ * someone else changed the set first (409 `config_version_conflict`);
1401
+ * with or without it, a delete that loses a race to a concurrent deploy
1402
+ * is refused the same way and never erases that deploy. A
1403
+ * name that is not deployed is 404 `function_not_found`. Needs
1404
+ * `features:write`. (DELETE /v1/functions/:name)
1405
+ */
1406
+ delete: async (name, opts) => (await this.call('DELETE', `/v1/functions/${encodeURIComponent(name)}`, undefined, opts?.ifMatch !== undefined ? { 'if-match': String(opts.ifMatch) } : {})).data,
1407
+ };
1347
1408
  /** The MCP aggregation surface (the `mcp` feature). */
1348
1409
  mcp = {
1349
1410
  /** Per-tenant secret for verifying the X-Vxil-Mcp-Signature header on custom
@@ -1624,6 +1685,14 @@ export class Vxil {
1624
1685
  createUploadUrl: async (input) => (await this.call('POST', '/v1/files/upload-url', input)).data,
1625
1686
  complete: async (objectId) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/complete`)).data,
1626
1687
  downloadUrl: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/download-url`)).data.download_url,
1688
+ /** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
1689
+ * — a gallery page costs one request, not one per image. `urls[]` keeps
1690
+ * your order (duplicates collapse); an id that is unknown, someone else's
1691
+ * (end-user mode), deleted or still uploading lands in `errors[]` instead of
1692
+ * failing the batch. Raster images (png/jpeg/webp/gif/avif/heic) are served
1693
+ * `inline`. For expo-image, cache on the object id, not the URL:
1694
+ * `<Image source={{ uri: u.download_url, cacheKey: u.object_id }} />`. */
1695
+ downloadUrls: async (objectIds) => (await this.call('GET', `/v1/files/download-urls?object_ids=${encodeURIComponent(objectIds.join(','))}`)).data,
1627
1696
  list: async (q) => {
1628
1697
  const s = qs({
1629
1698
  user_id: q?.user_id || undefined,
@@ -1741,6 +1810,13 @@ export class Vxil {
1741
1810
  * declared `ai.templates[]` entry against; absent on a version stored
1742
1811
  * before 2026-09-23 (the next push re-puts that template once). */
1743
1812
  list: async () => (await this.call('GET', '/v1/ai/templates')).data.templates,
1813
+ /** Retire a stored template (soft): every live version of the name leaves
1814
+ * the list and can no longer be rendered (by name or by version → 404
1815
+ * `template_not_found`); the rows stay for the generations that used
1816
+ * them, and storing the name again starts a new live version. A name
1817
+ * with no live version is a 404. `vxil push --allow-destructive` retires
1818
+ * the templates a config no longer declares. */
1819
+ retire: async (name) => (await this.call('DELETE', `/v1/ai/templates/${encodeURIComponent(name)}`)).data,
1744
1820
  },
1745
1821
  /** Provider × capability matrix + the tenant's default provider (the set an
1746
1822
  * agent/dashboard reads to know which providers/features are configured). */
@@ -1754,15 +1830,32 @@ export class Vxil {
1754
1830
  * live-only `token_expiring` events — see AiTokenExpiringEvent. */
1755
1831
  stream: async (input) => (await this.call('POST', '/v1/ai/generate', { ...input, stream: true })).data,
1756
1832
  /** Job-routed async generation (long/vision/batch): 202 + a jobs run drives
1757
- * the provider call; the settled answer lands in the replay buffer at
1758
- * `resume_path`. Reserve/settle + credit reversal-on-failure ride the run. */
1759
- generateAsync: async (input) => (await this.call('POST', '/v1/ai/generate', { ...input, mode: 'job' })).data,
1833
+ * the provider call; the settled answer is stored on the generation (read
1834
+ * it with `getGeneration`, 30 days) and replayed at `resume_path`.
1835
+ * Reserve/settle + credit reversal-on-failure ride the run.
1836
+ * `opts.idempotencyKey` deduplicates for 24 h: a re-send after a timeout
1837
+ * or a lost answer returns the ORIGINAL handle (`deduplicated: true`) and
1838
+ * never pays the provider twice. */
1839
+ generateAsync: async (input, opts) => (await this.call('POST', '/v1/ai/generate', { ...input, mode: 'job' }, opts?.idempotencyKey ? { 'idempotency-key': opts.idempotencyKey } : {})).data,
1840
+ /** One generation: status, usage, run, and — for a settled job-lane
1841
+ * generation — the settled answer (`result`, kept 30 days). In end-user
1842
+ * mode only the caller's own generation reads (a foreign id is a 404). */
1843
+ getGeneration: async (generationId) => (await this.call('GET', `/v1/ai/generations/${encodeURIComponent(generationId)}`)).data,
1844
+ /** Find generations by YOUR `correlation_id` (newest first, at most 20) —
1845
+ * the recovery read after a timed-out or lost `generateAsync` answer.
1846
+ * Each carries `result: null` (`result_state` still says whether an answer
1847
+ * is stored): read the text with `getGeneration(generation_id)`. */
1848
+ findGenerations: async (q) => (await this.call('GET', `/v1/ai/generations?correlation_id=${encodeURIComponent(q.correlation_id)}`)).data,
1760
1849
  /** Re-mint a fresh connect token for a LIVE stream (a generation that
1761
1850
  * outlives the ≤300s realtime token TTL); reconnect with `?since=<seq>`. */
1762
1851
  remintToken: async (generationId) => (await this.call('GET', `/v1/ai/generations/${encodeURIComponent(generationId)}/token`)).data,
1763
1852
  /** Resume a streamed generation after a dropped socket: every recorded frame
1764
1853
  * with seq > `since` plus `done` (the §2a replay buffer — plain JSON, not
1765
- * an SSE stream). The rag-native mirror is `rag.resume()`. */
1854
+ * an SSE stream; the buffer lives 1 h). A settled JOB-lane generation is
1855
+ * served from its stored answer instead (30 days). `status` says where the
1856
+ * generation is; `expired: true` means it settled but its frames are gone
1857
+ * (`done` is then true — nothing more will come). The rag-native mirror
1858
+ * is `rag.resume()`. */
1766
1859
  resume: async (generationId, q) => {
1767
1860
  const s = q?.since != null ? `?since=${q.since}` : '';
1768
1861
  return (await this.call('GET', `/v1/ai/generations/${encodeURIComponent(generationId)}/stream${s}`)).data;
@@ -1949,6 +2042,12 @@ export class Vxil {
1949
2042
  * committed): a linked jobs run that terminally fails auto-refunds the hold,
1950
2043
  * a success settles it. Throws VxilError(402, 'INSUFFICIENT_CREDITS') when the
1951
2044
  * available balance can't cover `amount`.
2045
+ *
2046
+ * Ordered credits: pass `credit_types` (1–8, in spend order — e.g.
2047
+ * `['free', 'subscription', 'topup']`) instead of `credit_type`; the WHOLE
2048
+ * amount comes from the FIRST type whose available balance covers it (one
2049
+ * ledger row, never split across types) and the answer's `credit_type` names
2050
+ * it. A retry with the same key answers the same type.
1952
2051
  */
1953
2052
  consume: async (input, opts) => (await this.call('POST', '/v1/payments/credits/consume', input, { 'idempotency-key': opts.idempotencyKey })).data,
1954
2053
  /** Additive grant (recurring/top-up) — idempotent per Idempotency-Key. A grant
@@ -2002,14 +2101,18 @@ export class Vxil {
2002
2101
  * approving webhook flips it to `succeeded`. A rejected adjustment flips it
2003
2102
  * to `failed`, releases the reserved amount back onto the charge and emits
2004
2103
  * `payments.refund.failed`. While a refund is `pending`, the credits the
2005
- * charge bought are NOT yet reversed. Stripe and the mock settle inline.
2104
+ * charge bought are NOT yet reversed, and (since 2026-10-01) the charge's
2105
+ * `amount_refunded` / `status` do not move either — the amount shows in its
2106
+ * `amount_refund_pending` until the provider settles it. Stripe and the
2107
+ * mock settle inline. Poll it with `listRefunds({ refund_id })`.
2006
2108
  *
2007
2109
  * If the provider has nothing left to refund on the charge — e.g. a refund
2008
2110
  * was already issued in the provider's own dashboard, which vxil's running
2009
2111
  * total cannot see — the call returns 422 `refund_not_allocatable` with the
2010
2112
  * precise reason, and nothing is sent to the provider. It is deterministic:
2011
2113
  * retrying the same refund fails the same way. A provider call that actually
2012
- * fails is still 502 `provider_error`.
2114
+ * fails is still 502 `provider_error`. Both failure bodies carry `refund_id`
2115
+ * (the row that went `failed`) so you can tie them to this call.
2013
2116
  */
2014
2117
  createRefund: async (input, opts) => (await this.call('POST', '/v1/payments/refunds', input, { 'idempotency-key': opts.idempotencyKey })).data,
2015
2118
  /** Where the user MANAGES their subscription, resolved by FUNDING SOURCE
@@ -2062,25 +2165,51 @@ export class Vxil {
2062
2165
  /** The last report-only reconciliation sweep result for this project
2063
2166
  * (`run: null` before the first daily tick). Finding kinds:
2064
2167
  * `balance_drift`, `entitlement_lag`, `null_tier`, `refund_pending`,
2065
- * `stranded_webhook`, `terminal_entitled`, `webhook_secret_missing`.
2168
+ * `stranded_webhook`, `terminal_entitled`, `webhook_secret_missing`,
2169
+ * `refunded_but_entitled`, `paid_without_grant`, `refund_reserve_drift`
2170
+ * (a charge's pending refund amount no pending refund explains).
2066
2171
  * SERVER-SIDE KEYS ONLY — a thin-client (`end_user_required`) key gets
2067
2172
  * 403 `server_only`; the sweep is a tenant-wide operator surface. */
2068
2173
  reconcile: async () => (await this.call('GET', '/v1/payments/reconcile')).data,
2069
2174
  /** List charges newest-first (the refund enabler — discover the charge_id).
2070
- * Filters: user_id, status, limit (clamped 1..100, default 50).
2175
+ * Filters: user_id, status, and (2026-10-01) EXACT `charge_id` /
2176
+ * `provider_charge_id` lookups; keyset-paged — pass `cursor` from a prior
2177
+ * page's `next_cursor` (limit clamped 1..100, default 50). An unknown query
2178
+ * parameter is a 422 `unknown_query_param`, never an unfiltered page.
2071
2179
  * Each row carries the provider's own charge id (`provider_charge_id`),
2072
2180
  * the provider's charge state (`provider_status`: `'paid'` — Paddle's
2073
2181
  * money-taken state — or `'completed'`; null on rows recorded before
2074
2182
  * 2026-09-25) and the one-off `product_id` it bought, so a function can
2075
- * match a charge to a provider event without reading the delivery log. */
2183
+ * match a charge to a provider event without reading the delivery log.
2184
+ * `amount_refunded` / `status` are SETTLED money; a refund awaiting the
2185
+ * provider shows in `amount_refund_pending`. */
2076
2186
  listCharges: async (q) => {
2077
2187
  const suffix = qs({
2078
2188
  user_id: q?.user_id || undefined,
2079
2189
  status: q?.status || undefined,
2190
+ charge_id: q?.charge_id || undefined,
2191
+ provider_charge_id: q?.provider_charge_id || undefined,
2192
+ environment: q?.environment || undefined,
2193
+ cursor: q?.cursor || undefined,
2080
2194
  limit: q?.limit || undefined,
2081
2195
  });
2082
2196
  return (await this.call('GET', `/v1/payments/charges${suffix}`)).data;
2083
2197
  },
2198
+ /** (2026-10-01) The refunds of ONE charge (`charge_id`) or one refund by id
2199
+ * (`refund_id`) — one of the two is required — newest first, with each
2200
+ * refund's `status` (`pending` until the provider settles it). Use it to
2201
+ * tie a `refund_id` from `createRefund`, its 502 / 422 answer or a
2202
+ * `payments.refund.failed` / `payments.charge.refunded` event back to the
2203
+ * money. A thin-client key sees only its own charges' refunds. */
2204
+ listRefunds: async (q) => {
2205
+ const suffix = qs({
2206
+ charge_id: q.charge_id || undefined,
2207
+ refund_id: q.refund_id || undefined,
2208
+ status: q.status || undefined,
2209
+ limit: q.limit || undefined,
2210
+ });
2211
+ return (await this.call('GET', `/v1/payments/refunds${suffix}`)).data;
2212
+ },
2084
2213
  /** Provider webhook event log (payments.md §7 "Event log & replay"):
2085
2214
  * operator visibility over every delivery — incl. persisted signature
2086
2215
  * failures — plus an idempotent reprocess verb. Needs payments:read
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/sdk",
3
- "version": "0.9.0",
3
+ "version": "0.11.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).",
@@ -13,7 +13,7 @@
13
13
  "typed-client"
14
14
  ],
15
15
  "engines": {
16
- "node": ">=18"
16
+ "node": ">=22"
17
17
  },
18
18
  "main": "./dist/index.js",
19
19
  "types": "./dist/index.d.ts",