@vxil/sdk 0.10.0 → 0.11.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +666 -40
  2. package/dist/index.js +143 -12
  3. package/package.json +1 -1
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,59 @@ 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), and
4002
+ * `enabled`: false while the project's functions are paused as a whole
4003
+ * (a Free project on the production workload, a downgrade) — invokes then
4004
+ * answer 404. Any authenticated key may read it. (GET /v1/functions) */
4005
+ list: () => Promise<{
4006
+ enabled?: boolean;
4007
+ functions: DeployedFunction[];
4008
+ runtime?: {
4009
+ compatibilityDate: string;
4010
+ compatibilityFlags: string[];
4011
+ };
4012
+ cpuMsCeiling?: number;
4013
+ }>;
4014
+ /**
4015
+ * Remove ONE function from the deployed set — a new functions config
4016
+ * version without it (invokes answer 404, its crons and trigger
4017
+ * subscriptions are unbound). The uploaded code is kept until the nightly
4018
+ * cleanup, so a functions config rollback restores it exactly. Pass
4019
+ * `ifMatch` (the active functions config version) to refuse the delete if
4020
+ * someone else changed the set first (409 `config_version_conflict`);
4021
+ * with or without it, a delete that loses a race to a concurrent deploy
4022
+ * is refused the same way and never erases that deploy. A
4023
+ * name that is not deployed is 404 `function_not_found`. Needs
4024
+ * `features:write`. (DELETE /v1/functions/:name)
4025
+ */
4026
+ delete: (name: string, opts?: {
4027
+ ifMatch?: number;
4028
+ }) => Promise<{
4029
+ deleted: string;
4030
+ version: number;
4031
+ hook_reconcile?: {
4032
+ created: number;
4033
+ updated: number;
4034
+ deleted: number;
4035
+ failed: number;
4036
+ };
4037
+ cron_schedules?: {
4038
+ created: number;
4039
+ deleted: number;
4040
+ failed: number;
4041
+ };
4042
+ warnings?: string[];
4043
+ }>;
4044
+ };
3511
4045
  /** The MCP aggregation surface (the `mcp` feature). */
3512
4046
  readonly mcp: {
3513
4047
  /** Per-tenant secret for verifying the X-Vxil-Mcp-Signature header on custom
@@ -4108,6 +4642,25 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4108
4642
  status: string;
4109
4643
  }>;
4110
4644
  downloadUrl: (objectId: string) => Promise<string>;
4645
+ /** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
4646
+ * — a gallery page costs one request, not one per image. `urls[]` keeps
4647
+ * your order (duplicates collapse); an id that is unknown, someone else's
4648
+ * (end-user mode), deleted or still uploading lands in `errors[]` instead of
4649
+ * failing the batch. Raster images (png/jpeg/webp/gif/avif/heic) are served
4650
+ * `inline`. For expo-image, cache on the object id, not the URL:
4651
+ * `<Image source={{ uri: u.download_url, cacheKey: u.object_id }} />`. */
4652
+ downloadUrls: (objectIds: string[]) => Promise<{
4653
+ urls: Array<{
4654
+ object_id: string;
4655
+ download_url: string;
4656
+ content_type: string;
4657
+ }>;
4658
+ errors: Array<{
4659
+ object_id: string;
4660
+ code: "not_found" | "upload_incomplete";
4661
+ }>;
4662
+ expires_in: number;
4663
+ }>;
4111
4664
  list: (q?: {
4112
4665
  user_id?: string;
4113
4666
  cursor?: string;
@@ -4318,6 +4871,16 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4318
4871
  created_at: string;
4319
4872
  content_sha256?: string;
4320
4873
  }>>;
4874
+ /** Retire a stored template (soft): every live version of the name leaves
4875
+ * the list and can no longer be rendered (by name or by version → 404
4876
+ * `template_not_found`); the rows stay for the generations that used
4877
+ * them, and storing the name again starts a new live version. A name
4878
+ * with no live version is a 404. `vxil push --allow-destructive` retires
4879
+ * the templates a config no longer declares. */
4880
+ retire: (name: string) => Promise<{
4881
+ retired: string;
4882
+ versions: number;
4883
+ }>;
4321
4884
  };
4322
4885
  /** Provider × capability matrix + the tenant's default provider (the set an
4323
4886
  * agent/dashboard reads to know which providers/features are configured). */
@@ -4413,8 +4976,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4413
4976
  correlation_id?: string;
4414
4977
  }) => Promise<AiStreamHandle>;
4415
4978
  /** 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. */
4979
+ * the provider call; the settled answer is stored on the generation (read
4980
+ * it with `getGeneration`, 30 days) and replayed at `resume_path`.
4981
+ * Reserve/settle + credit reversal-on-failure ride the run.
4982
+ * `opts.idempotencyKey` deduplicates for 24 h: a re-send after a timeout
4983
+ * or a lost answer returns the ORIGINAL handle (`deduplicated: true`) and
4984
+ * never pays the provider twice. */
4418
4985
  generateAsync: (input: {
4419
4986
  template?: string;
4420
4987
  template_version?: number;
@@ -4463,22 +5030,35 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4463
5030
  * the row, echoed on the answer and the replay read, and (job mode)
4464
5031
  * carried on the `job.generation.*` events with `generation_id`. */
4465
5032
  correlation_id?: string;
5033
+ }, opts?: {
5034
+ idempotencyKey?: string;
4466
5035
  }) => Promise<AiJobHandle>;
5036
+ /** One generation: status, usage, run, and — for a settled job-lane
5037
+ * generation — the settled answer (`result`, kept 30 days). In end-user
5038
+ * mode only the caller's own generation reads (a foreign id is a 404). */
5039
+ getGeneration: (generationId: string) => Promise<AiGenerationRecord>;
5040
+ /** Find generations by YOUR `correlation_id` (newest first, at most 20) —
5041
+ * the recovery read after a timed-out or lost `generateAsync` answer.
5042
+ * Each carries `result: null` (`result_state` still says whether an answer
5043
+ * is stored): read the text with `getGeneration(generation_id)`. */
5044
+ findGenerations: (q: {
5045
+ correlation_id: string;
5046
+ }) => Promise<{
5047
+ generations: AiGenerationRecord[];
5048
+ }>;
4467
5049
  /** Re-mint a fresh connect token for a LIVE stream (a generation that
4468
5050
  * outlives the ≤300s realtime token TTL); reconnect with `?since=<seq>`. */
4469
5051
  remintToken: (generationId: string) => Promise<AiStreamToken>;
4470
5052
  /** Resume a streamed generation after a dropped socket: every recorded frame
4471
5053
  * with seq > `since` plus `done` (the §2a replay buffer — plain JSON, not
4472
- * an SSE stream). The rag-native mirror is `rag.resume()`. */
5054
+ * an SSE stream; the buffer lives 1 h). A settled JOB-lane generation is
5055
+ * served from its stored answer instead (30 days). `status` says where the
5056
+ * generation is; `expired: true` means it settled but its frames are gone
5057
+ * (`done` is then true — nothing more will come). The rag-native mirror
5058
+ * is `rag.resume()`. */
4473
5059
  resume: (generationId: string, q?: {
4474
5060
  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
- }>;
5061
+ }) => Promise<AiResumePage>;
4482
5062
  /** Classify an input into one of YOUR labels (or, with `multi:true`, every
4483
5063
  * label that applies) — a schema-forced verdict over the generate path:
4484
5064
  * the allowed labels ride the structured-output schema as an enum, so the
@@ -4741,21 +5321,22 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4741
5321
  * committed): a linked jobs run that terminally fails auto-refunds the hold,
4742
5322
  * a success settles it. Throws VxilError(402, 'INSUFFICIENT_CREDITS') when the
4743
5323
  * available balance can't cover `amount`.
5324
+ *
5325
+ * Ordered credits: pass `credit_types` (1–8, in spend order — e.g.
5326
+ * `['free', 'subscription', 'topup']`) instead of `credit_type`; the WHOLE
5327
+ * amount comes from the FIRST type whose available balance covers it (one
5328
+ * ledger row, never split across types) and the answer's `credit_type` names
5329
+ * it. A retry with the same key answers the same type.
4744
5330
  */
4745
- consume: (input: {
5331
+ consume: (input: PaymentsCreditTypeChoice & {
4746
5332
  user_id: string;
4747
- credit_type: string;
4748
5333
  amount: number;
4749
5334
  /** link this debit to a jobs run_id → provisional hold + auto-reverse. */
4750
5335
  job_id?: string;
4751
5336
  reason?: string;
4752
5337
  }, opts: {
4753
5338
  idempotencyKey: string;
4754
- }) => Promise<{
4755
- balance_after: number;
4756
- ledger_entry_id: string | null;
4757
- reversed: boolean;
4758
- }>;
5339
+ }) => Promise<PaymentsConsumeResult>;
4759
5340
  /** Additive grant (recurring/top-up) — idempotent per Idempotency-Key. A grant
4760
5341
  * only ADDS; `amount` must be ≥ 1. */
4761
5342
  grant: (input: {
@@ -4870,14 +5451,18 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4870
5451
  * approving webhook flips it to `succeeded`. A rejected adjustment flips it
4871
5452
  * to `failed`, releases the reserved amount back onto the charge and emits
4872
5453
  * `payments.refund.failed`. While a refund is `pending`, the credits the
4873
- * charge bought are NOT yet reversed. Stripe and the mock settle inline.
5454
+ * charge bought are NOT yet reversed, and (since 2026-10-01) the charge's
5455
+ * `amount_refunded` / `status` do not move either — the amount shows in its
5456
+ * `amount_refund_pending` until the provider settles it. Stripe and the
5457
+ * mock settle inline. Poll it with `listRefunds({ refund_id })`.
4874
5458
  *
4875
5459
  * If the provider has nothing left to refund on the charge — e.g. a refund
4876
5460
  * was already issued in the provider's own dashboard, which vxil's running
4877
5461
  * total cannot see — the call returns 422 `refund_not_allocatable` with the
4878
5462
  * precise reason, and nothing is sent to the provider. It is deterministic:
4879
5463
  * retrying the same refund fails the same way. A provider call that actually
4880
- * fails is still 502 `provider_error`.
5464
+ * fails is still 502 `provider_error`. Both failure bodies carry `refund_id`
5465
+ * (the row that went `failed`) so you can tie them to this call.
4881
5466
  */
4882
5467
  createRefund: (input: {
4883
5468
  charge_id: string;
@@ -4989,7 +5574,9 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
4989
5574
  /** The last report-only reconciliation sweep result for this project
4990
5575
  * (`run: null` before the first daily tick). Finding kinds:
4991
5576
  * `balance_drift`, `entitlement_lag`, `null_tier`, `refund_pending`,
4992
- * `stranded_webhook`, `terminal_entitled`, `webhook_secret_missing`.
5577
+ * `stranded_webhook`, `terminal_entitled`, `webhook_secret_missing`,
5578
+ * `refunded_but_entitled`, `paid_without_grant`, `refund_reserve_drift`
5579
+ * (a charge's pending refund amount no pending refund explains).
4993
5580
  * SERVER-SIDE KEYS ONLY — a thin-client (`end_user_required`) key gets
4994
5581
  * 403 `server_only`; the sweep is a tenant-wide operator surface. */
4995
5582
  reconcile: () => Promise<{
@@ -5010,18 +5597,45 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
5010
5597
  } | null;
5011
5598
  }>;
5012
5599
  /** List charges newest-first (the refund enabler — discover the charge_id).
5013
- * Filters: user_id, status, limit (clamped 1..100, default 50).
5600
+ * Filters: user_id, status, and (2026-10-01) EXACT `charge_id` /
5601
+ * `provider_charge_id` lookups; keyset-paged — pass `cursor` from a prior
5602
+ * page's `next_cursor` (limit clamped 1..100, default 50). An unknown query
5603
+ * parameter is a 422 `unknown_query_param`, never an unfiltered page.
5014
5604
  * Each row carries the provider's own charge id (`provider_charge_id`),
5015
5605
  * the provider's charge state (`provider_status`: `'paid'` — Paddle's
5016
5606
  * money-taken state — or `'completed'`; null on rows recorded before
5017
5607
  * 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. */
5608
+ * match a charge to a provider event without reading the delivery log.
5609
+ * `amount_refunded` / `status` are SETTLED money; a refund awaiting the
5610
+ * provider shows in `amount_refund_pending`. */
5019
5611
  listCharges: (q?: {
5020
5612
  user_id?: string;
5021
- status?: "succeeded" | "failed" | "refunded" | "partially_refunded" | "pending";
5613
+ status?: "succeeded" | "failed" | "refunded" | "partially_refunded" | "pending" | "disputed";
5614
+ /** exact match on the vxil charge id (`chg_…`) */
5615
+ charge_id?: string;
5616
+ /** exact match on the provider's own charge id (a Paddle `txn_…`, a Stripe `pi_…`) */
5617
+ provider_charge_id?: string;
5618
+ /** `production` = revenue; `sandbox` = store-review test purchases */
5619
+ environment?: PaymentsEventEnvironment;
5620
+ cursor?: string;
5022
5621
  limit?: number;
5023
5622
  }) => Promise<{
5024
5623
  charges: PaymentsCharge[];
5624
+ next_cursor: string | null;
5625
+ }>;
5626
+ /** (2026-10-01) The refunds of ONE charge (`charge_id`) or one refund by id
5627
+ * (`refund_id`) — one of the two is required — newest first, with each
5628
+ * refund's `status` (`pending` until the provider settles it). Use it to
5629
+ * tie a `refund_id` from `createRefund`, its 502 / 422 answer or a
5630
+ * `payments.refund.failed` / `payments.charge.refunded` event back to the
5631
+ * money. A thin-client key sees only its own charges' refunds. */
5632
+ listRefunds: (q: {
5633
+ charge_id?: string;
5634
+ refund_id?: string;
5635
+ status?: "pending" | "succeeded" | "failed";
5636
+ limit?: number;
5637
+ }) => Promise<{
5638
+ refunds: PaymentsRefund[];
5025
5639
  }>;
5026
5640
  /** Provider webhook event log (payments.md §7 "Event log & replay"):
5027
5641
  * operator visibility over every delivery — incl. persisted signature
@@ -5068,3 +5682,15 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
5068
5682
  };
5069
5683
  }
5070
5684
  export { withReporting, report, buildEnvelope, parseDsn, exceptionEvent, reportServerErrors, REPORT_TIMEOUT_MS, type ReportingOptions, type ReportEvent, type FetchHandler, } from './reporting.js';
5685
+ /** `auth.users.import` (POST /v1/auth/users/import) — one result per id. */
5686
+ export interface AuthImportResult {
5687
+ results: Array<{
5688
+ id: string;
5689
+ status: 'created' | 'updated' | 'kept' | 'skipped';
5690
+ reason?: 'invalid_id' | 'invalid_hash' | 'duplicate_id' | 'not_registered' | 'no_email' | 'deleted' | 'email_taken' | 'id_taken';
5691
+ }>;
5692
+ created: number;
5693
+ updated: number;
5694
+ kept: number;
5695
+ skipped: number;
5696
+ }
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,34 @@ 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), and
1392
+ * `enabled`: false while the project's functions are paused as a whole
1393
+ * (a Free project on the production workload, a downgrade) — invokes then
1394
+ * answer 404. Any authenticated key may read it. (GET /v1/functions) */
1395
+ list: async () => (await this.call('GET', '/v1/functions')).data,
1396
+ /**
1397
+ * Remove ONE function from the deployed set — a new functions config
1398
+ * version without it (invokes answer 404, its crons and trigger
1399
+ * subscriptions are unbound). The uploaded code is kept until the nightly
1400
+ * cleanup, so a functions config rollback restores it exactly. Pass
1401
+ * `ifMatch` (the active functions config version) to refuse the delete if
1402
+ * someone else changed the set first (409 `config_version_conflict`);
1403
+ * with or without it, a delete that loses a race to a concurrent deploy
1404
+ * is refused the same way and never erases that deploy. A
1405
+ * name that is not deployed is 404 `function_not_found`. Needs
1406
+ * `features:write`. (DELETE /v1/functions/:name)
1407
+ */
1408
+ delete: async (name, opts) => (await this.call('DELETE', `/v1/functions/${encodeURIComponent(name)}`, undefined, opts?.ifMatch !== undefined ? { 'if-match': String(opts.ifMatch) } : {})).data,
1409
+ };
1347
1410
  /** The MCP aggregation surface (the `mcp` feature). */
1348
1411
  mcp = {
1349
1412
  /** Per-tenant secret for verifying the X-Vxil-Mcp-Signature header on custom
@@ -1624,6 +1687,14 @@ export class Vxil {
1624
1687
  createUploadUrl: async (input) => (await this.call('POST', '/v1/files/upload-url', input)).data,
1625
1688
  complete: async (objectId) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/complete`)).data,
1626
1689
  downloadUrl: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/download-url`)).data.download_url,
1690
+ /** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
1691
+ * — a gallery page costs one request, not one per image. `urls[]` keeps
1692
+ * your order (duplicates collapse); an id that is unknown, someone else's
1693
+ * (end-user mode), deleted or still uploading lands in `errors[]` instead of
1694
+ * failing the batch. Raster images (png/jpeg/webp/gif/avif/heic) are served
1695
+ * `inline`. For expo-image, cache on the object id, not the URL:
1696
+ * `<Image source={{ uri: u.download_url, cacheKey: u.object_id }} />`. */
1697
+ downloadUrls: async (objectIds) => (await this.call('GET', `/v1/files/download-urls?object_ids=${encodeURIComponent(objectIds.join(','))}`)).data,
1627
1698
  list: async (q) => {
1628
1699
  const s = qs({
1629
1700
  user_id: q?.user_id || undefined,
@@ -1741,6 +1812,13 @@ export class Vxil {
1741
1812
  * declared `ai.templates[]` entry against; absent on a version stored
1742
1813
  * before 2026-09-23 (the next push re-puts that template once). */
1743
1814
  list: async () => (await this.call('GET', '/v1/ai/templates')).data.templates,
1815
+ /** Retire a stored template (soft): every live version of the name leaves
1816
+ * the list and can no longer be rendered (by name or by version → 404
1817
+ * `template_not_found`); the rows stay for the generations that used
1818
+ * them, and storing the name again starts a new live version. A name
1819
+ * with no live version is a 404. `vxil push --allow-destructive` retires
1820
+ * the templates a config no longer declares. */
1821
+ retire: async (name) => (await this.call('DELETE', `/v1/ai/templates/${encodeURIComponent(name)}`)).data,
1744
1822
  },
1745
1823
  /** Provider × capability matrix + the tenant's default provider (the set an
1746
1824
  * agent/dashboard reads to know which providers/features are configured). */
@@ -1754,15 +1832,32 @@ export class Vxil {
1754
1832
  * live-only `token_expiring` events — see AiTokenExpiringEvent. */
1755
1833
  stream: async (input) => (await this.call('POST', '/v1/ai/generate', { ...input, stream: true })).data,
1756
1834
  /** 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,
1835
+ * the provider call; the settled answer is stored on the generation (read
1836
+ * it with `getGeneration`, 30 days) and replayed at `resume_path`.
1837
+ * Reserve/settle + credit reversal-on-failure ride the run.
1838
+ * `opts.idempotencyKey` deduplicates for 24 h: a re-send after a timeout
1839
+ * or a lost answer returns the ORIGINAL handle (`deduplicated: true`) and
1840
+ * never pays the provider twice. */
1841
+ generateAsync: async (input, opts) => (await this.call('POST', '/v1/ai/generate', { ...input, mode: 'job' }, opts?.idempotencyKey ? { 'idempotency-key': opts.idempotencyKey } : {})).data,
1842
+ /** One generation: status, usage, run, and — for a settled job-lane
1843
+ * generation — the settled answer (`result`, kept 30 days). In end-user
1844
+ * mode only the caller's own generation reads (a foreign id is a 404). */
1845
+ getGeneration: async (generationId) => (await this.call('GET', `/v1/ai/generations/${encodeURIComponent(generationId)}`)).data,
1846
+ /** Find generations by YOUR `correlation_id` (newest first, at most 20) —
1847
+ * the recovery read after a timed-out or lost `generateAsync` answer.
1848
+ * Each carries `result: null` (`result_state` still says whether an answer
1849
+ * is stored): read the text with `getGeneration(generation_id)`. */
1850
+ findGenerations: async (q) => (await this.call('GET', `/v1/ai/generations?correlation_id=${encodeURIComponent(q.correlation_id)}`)).data,
1760
1851
  /** Re-mint a fresh connect token for a LIVE stream (a generation that
1761
1852
  * outlives the ≤300s realtime token TTL); reconnect with `?since=<seq>`. */
1762
1853
  remintToken: async (generationId) => (await this.call('GET', `/v1/ai/generations/${encodeURIComponent(generationId)}/token`)).data,
1763
1854
  /** Resume a streamed generation after a dropped socket: every recorded frame
1764
1855
  * with seq > `since` plus `done` (the §2a replay buffer — plain JSON, not
1765
- * an SSE stream). The rag-native mirror is `rag.resume()`. */
1856
+ * an SSE stream; the buffer lives 1 h). A settled JOB-lane generation is
1857
+ * served from its stored answer instead (30 days). `status` says where the
1858
+ * generation is; `expired: true` means it settled but its frames are gone
1859
+ * (`done` is then true — nothing more will come). The rag-native mirror
1860
+ * is `rag.resume()`. */
1766
1861
  resume: async (generationId, q) => {
1767
1862
  const s = q?.since != null ? `?since=${q.since}` : '';
1768
1863
  return (await this.call('GET', `/v1/ai/generations/${encodeURIComponent(generationId)}/stream${s}`)).data;
@@ -1949,6 +2044,12 @@ export class Vxil {
1949
2044
  * committed): a linked jobs run that terminally fails auto-refunds the hold,
1950
2045
  * a success settles it. Throws VxilError(402, 'INSUFFICIENT_CREDITS') when the
1951
2046
  * available balance can't cover `amount`.
2047
+ *
2048
+ * Ordered credits: pass `credit_types` (1–8, in spend order — e.g.
2049
+ * `['free', 'subscription', 'topup']`) instead of `credit_type`; the WHOLE
2050
+ * amount comes from the FIRST type whose available balance covers it (one
2051
+ * ledger row, never split across types) and the answer's `credit_type` names
2052
+ * it. A retry with the same key answers the same type.
1952
2053
  */
1953
2054
  consume: async (input, opts) => (await this.call('POST', '/v1/payments/credits/consume', input, { 'idempotency-key': opts.idempotencyKey })).data,
1954
2055
  /** Additive grant (recurring/top-up) — idempotent per Idempotency-Key. A grant
@@ -2002,14 +2103,18 @@ export class Vxil {
2002
2103
  * approving webhook flips it to `succeeded`. A rejected adjustment flips it
2003
2104
  * to `failed`, releases the reserved amount back onto the charge and emits
2004
2105
  * `payments.refund.failed`. While a refund is `pending`, the credits the
2005
- * charge bought are NOT yet reversed. Stripe and the mock settle inline.
2106
+ * charge bought are NOT yet reversed, and (since 2026-10-01) the charge's
2107
+ * `amount_refunded` / `status` do not move either — the amount shows in its
2108
+ * `amount_refund_pending` until the provider settles it. Stripe and the
2109
+ * mock settle inline. Poll it with `listRefunds({ refund_id })`.
2006
2110
  *
2007
2111
  * If the provider has nothing left to refund on the charge — e.g. a refund
2008
2112
  * was already issued in the provider's own dashboard, which vxil's running
2009
2113
  * total cannot see — the call returns 422 `refund_not_allocatable` with the
2010
2114
  * precise reason, and nothing is sent to the provider. It is deterministic:
2011
2115
  * retrying the same refund fails the same way. A provider call that actually
2012
- * fails is still 502 `provider_error`.
2116
+ * fails is still 502 `provider_error`. Both failure bodies carry `refund_id`
2117
+ * (the row that went `failed`) so you can tie them to this call.
2013
2118
  */
2014
2119
  createRefund: async (input, opts) => (await this.call('POST', '/v1/payments/refunds', input, { 'idempotency-key': opts.idempotencyKey })).data,
2015
2120
  /** Where the user MANAGES their subscription, resolved by FUNDING SOURCE
@@ -2062,25 +2167,51 @@ export class Vxil {
2062
2167
  /** The last report-only reconciliation sweep result for this project
2063
2168
  * (`run: null` before the first daily tick). Finding kinds:
2064
2169
  * `balance_drift`, `entitlement_lag`, `null_tier`, `refund_pending`,
2065
- * `stranded_webhook`, `terminal_entitled`, `webhook_secret_missing`.
2170
+ * `stranded_webhook`, `terminal_entitled`, `webhook_secret_missing`,
2171
+ * `refunded_but_entitled`, `paid_without_grant`, `refund_reserve_drift`
2172
+ * (a charge's pending refund amount no pending refund explains).
2066
2173
  * SERVER-SIDE KEYS ONLY — a thin-client (`end_user_required`) key gets
2067
2174
  * 403 `server_only`; the sweep is a tenant-wide operator surface. */
2068
2175
  reconcile: async () => (await this.call('GET', '/v1/payments/reconcile')).data,
2069
2176
  /** List charges newest-first (the refund enabler — discover the charge_id).
2070
- * Filters: user_id, status, limit (clamped 1..100, default 50).
2177
+ * Filters: user_id, status, and (2026-10-01) EXACT `charge_id` /
2178
+ * `provider_charge_id` lookups; keyset-paged — pass `cursor` from a prior
2179
+ * page's `next_cursor` (limit clamped 1..100, default 50). An unknown query
2180
+ * parameter is a 422 `unknown_query_param`, never an unfiltered page.
2071
2181
  * Each row carries the provider's own charge id (`provider_charge_id`),
2072
2182
  * the provider's charge state (`provider_status`: `'paid'` — Paddle's
2073
2183
  * money-taken state — or `'completed'`; null on rows recorded before
2074
2184
  * 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. */
2185
+ * match a charge to a provider event without reading the delivery log.
2186
+ * `amount_refunded` / `status` are SETTLED money; a refund awaiting the
2187
+ * provider shows in `amount_refund_pending`. */
2076
2188
  listCharges: async (q) => {
2077
2189
  const suffix = qs({
2078
2190
  user_id: q?.user_id || undefined,
2079
2191
  status: q?.status || undefined,
2192
+ charge_id: q?.charge_id || undefined,
2193
+ provider_charge_id: q?.provider_charge_id || undefined,
2194
+ environment: q?.environment || undefined,
2195
+ cursor: q?.cursor || undefined,
2080
2196
  limit: q?.limit || undefined,
2081
2197
  });
2082
2198
  return (await this.call('GET', `/v1/payments/charges${suffix}`)).data;
2083
2199
  },
2200
+ /** (2026-10-01) The refunds of ONE charge (`charge_id`) or one refund by id
2201
+ * (`refund_id`) — one of the two is required — newest first, with each
2202
+ * refund's `status` (`pending` until the provider settles it). Use it to
2203
+ * tie a `refund_id` from `createRefund`, its 502 / 422 answer or a
2204
+ * `payments.refund.failed` / `payments.charge.refunded` event back to the
2205
+ * money. A thin-client key sees only its own charges' refunds. */
2206
+ listRefunds: async (q) => {
2207
+ const suffix = qs({
2208
+ charge_id: q.charge_id || undefined,
2209
+ refund_id: q.refund_id || undefined,
2210
+ status: q.status || undefined,
2211
+ limit: q.limit || undefined,
2212
+ });
2213
+ return (await this.call('GET', `/v1/payments/refunds${suffix}`)).data;
2214
+ },
2084
2215
  /** Provider webhook event log (payments.md §7 "Event log & replay"):
2085
2216
  * operator visibility over every delivery — incl. persisted signature
2086
2217
  * 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.10.0",
3
+ "version": "0.11.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).",