@vxil/sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,15 +1,77 @@
1
- /** The released API majors, as a CLOSED union — one per released major (docs/
2
- * feature-versioning.md §3). `'v1'` is the only released major, so pinning
3
- * anything else (`apiVersion: 'v2'`) is a COMPILE error until a v2 GAs and
4
- * widens this union. Every path the SDK builds is major-versioned; the default
5
- * is the compile-time constant `'v1'` (never a floating `latest` alias). */
6
1
  export type ApiVersion = 'v1';
7
2
  /** A per-feature API-version override key: the URL namespace that leads a path
8
3
  * (`/v1/<namespace>/…`). Matches the segment `versionedPath` rewrites, so an
9
4
  * override changes exactly that namespace's paths. Renamed client accessors map
10
5
  * onto their real namespaces (`vx.search` → `search`, `vx.feeds` → `feeds`). */
11
- export type FeatureKey = 'users' | 'notifications' | 'config' | 'features' | 'audit' | 'jobs' | 'auth' | 'rate-limits' | 'files' | 'cms' | 'comments' | 'dm' | 'feeds' | 'realtime' | 'orgs' | 'webhooks' | 'search' | 'ai' | 'rag' | 'payments' | 'fn' | 'copilot';
6
+ export type FeatureKey = 'users' | 'notifications' | 'config' | 'features' | 'audit' | 'usage' | 'jobs' | 'auth' | 'rate-limits' | 'files' | 'cms' | 'comments' | 'dm' | 'feeds' | 'realtime' | 'orgs' | 'webhooks' | 'search' | 'ai' | 'rag' | 'payments' | 'fn' | 'copilot';
7
+ /** Retry policy for the client (DEFAULT OFF: `attempts` is `0`). Only
8
+ * idempotent requests are ever retried — GET/HEAD/PUT/DELETE, and a POST only
9
+ * when the call carries an `Idempotency-Key` header (`{ idempotencyKey }` on
10
+ * the money routes) — so a retry can never double-apply a write. */
11
+ export interface VxilRetryOptions {
12
+ /** How many RETRIES to make after the first attempt (`2` ⇒ up to 3 requests).
13
+ * Default `0` = retries off. */
14
+ attempts?: number;
15
+ /** Response statuses that trigger a retry. Default `[429, 502, 503, 504]`. */
16
+ retryOn?: number[];
17
+ /** Base delay before the first retry; doubles each retry (with jitter in
18
+ * `[½, 1]` of the computed delay). Default `250`. */
19
+ backoffMs?: number;
20
+ /** The longest the client will ever wait between attempts. Also the cap on
21
+ * an honoured `Retry-After`: when the server asks for MORE than this the
22
+ * client does not retry and surfaces the error (its `retryAfter` tells you
23
+ * how long the server asked for). Default `10_000`. */
24
+ maxBackoffMs?: number;
25
+ /** Use the response's `Retry-After` header (seconds or an HTTP-date) as the
26
+ * delay instead of the backoff schedule when present. Default `true`. */
27
+ respectRetryAfter?: boolean;
28
+ /** Also retry when `fetch` itself fails (DNS, connection reset, a
29
+ * `timeoutMs` timeout) — again only for idempotent requests. Default `true`
30
+ * (inert while `attempts` is `0`). */
31
+ retryOnNetworkError?: boolean;
32
+ }
33
+ /** What a hook sees of a request: a FROZEN descriptor. Credential headers
34
+ * (`authorization`, `x-vxil-end-user`) are never included. */
35
+ export interface VxilRequestInfo {
36
+ readonly method: string;
37
+ readonly url: string;
38
+ readonly headers: Readonly<Record<string, string>>;
39
+ /** 1 for the first attempt, 2 for the first retry, … */
40
+ readonly attempt: number;
41
+ }
42
+ /** Request lifecycle hooks. Each may be async; a thrown error propagates to
43
+ * the caller (the request is not sent / the response is not returned). */
44
+ export interface VxilHooks {
45
+ /** Runs before EVERY attempt (so a retry can carry a fresh trace id). May
46
+ * return extra headers to add for that attempt — a correlation id, a
47
+ * tracing header. The credential headers cannot be set from a hook (they
48
+ * are dropped); rotate an end-user session with `vx.asEndUser(token)`. */
49
+ beforeRequest?: (request: VxilRequestInfo) => void | Record<string, string> | Promise<void | Record<string, string>>;
50
+ /** Runs after every response that arrives (2xx or not, retried or final),
51
+ * before the retry decision. The body has already been read into the
52
+ * client — inspect `response.status` / `response.headers`, not the body. */
53
+ afterResponse?: (info: {
54
+ request: VxilRequestInfo;
55
+ response: Response;
56
+ durationMs: number;
57
+ }) => void | Promise<void>;
58
+ /** Runs right before the client sleeps for a retry. `status` is set for a
59
+ * retryable response, `error` for a network failure / timeout. */
60
+ onRetry?: (info: {
61
+ request: VxilRequestInfo;
62
+ delayMs: number;
63
+ status?: number;
64
+ error?: unknown;
65
+ }) => void | Promise<void>;
66
+ }
12
67
  export interface VxilOptions {
68
+ /** The tenant API key (`vxil_live_…` / `vxil_test_…`).
69
+ *
70
+ * A key may optionally be minted with an **IP allowlist** (`allowed_cidrs`,
71
+ * set in the dashboard at mint time). When it is, a call from an address
72
+ * outside that list is refused at the edge with `403 ip_not_allowed` —
73
+ * before it reaches any feature — and the key is otherwise unchanged. Keys
74
+ * minted without one (the default) are not IP-restricted. */
13
75
  apiKey: string;
14
76
  /** defaults to the production edge */
15
77
  baseUrl?: string;
@@ -33,6 +95,19 @@ export interface VxilOptions {
33
95
  * over `apiVersion` for that namespace only. Optional; the compiled default
34
96
  * is `'v1'` for every feature. */
35
97
  apiVersions?: Partial<Record<FeatureKey, ApiVersion>>;
98
+ /** Retry policy — DEFAULT OFF (`attempts: 0`). Only idempotent requests are
99
+ * ever retried (GET/HEAD/PUT/DELETE, and a POST only when it carries an
100
+ * `Idempotency-Key`), so a retry can never double-apply a write. See
101
+ * `VxilRetryOptions` for the schedule and `Retry-After` handling. */
102
+ retry?: VxilRetryOptions;
103
+ /** Per-attempt timeout in milliseconds (default: none). On expiry the
104
+ * request — body read included — is aborted and a `VxilError`
105
+ * `{ status: 0, code: 'request_timeout' }` is thrown. A timeout counts as a
106
+ * network error for `retry`. */
107
+ timeoutMs?: number;
108
+ /** Request lifecycle hooks — correlation ids, logging, metrics, a retry
109
+ * audit. Nothing runs unless set. See `VxilHooks`. */
110
+ hooks?: VxilHooks;
36
111
  }
37
112
  /** Rewrite a built `/v1/<ns>/…` path onto the version configured for its
38
113
  * namespace: the per-namespace override wins, else the global default. PURE and
@@ -44,13 +119,24 @@ export interface Meta {
44
119
  request_id: string;
45
120
  tenant_id?: string;
46
121
  }
122
+ /** Every non-2xx answer is thrown as this class, carrying the structured error
123
+ * envelope. `status` is the HTTP status — or `0` when no response arrived
124
+ * (a `timeoutMs` timeout, `code: 'request_timeout'`). */
47
125
  export declare class VxilError extends Error {
48
126
  readonly status: number;
49
127
  readonly code: string;
50
128
  readonly hint?: string | undefined;
51
129
  readonly fixUrl?: string | undefined;
52
130
  readonly requestId?: string | undefined;
53
- constructor(status: number, code: string, message: string, hint?: string | undefined, fixUrl?: string | undefined, requestId?: string | undefined);
131
+ /** Seconds the server asked the caller to wait — parsed from `Retry-After`
132
+ * (delta-seconds or an HTTP-date) when the answer carried it (a rate
133
+ * limit, a busy upstream); otherwise undefined. */
134
+ readonly retryAfter?: number | undefined;
135
+ constructor(status: number, code: string, message: string, hint?: string | undefined, fixUrl?: string | undefined, requestId?: string | undefined,
136
+ /** Seconds the server asked the caller to wait — parsed from `Retry-After`
137
+ * (delta-seconds or an HTTP-date) when the answer carried it (a rate
138
+ * limit, a busy upstream); otherwise undefined. */
139
+ retryAfter?: number | undefined);
54
140
  }
55
141
  export interface VxilUser {
56
142
  id: string;
@@ -78,6 +164,44 @@ export interface Delivery {
78
164
  queued_at: string;
79
165
  sent_at: string | null;
80
166
  failed_at: string | null;
167
+ /** Engagement stamps from the signed provider webhook (first-wins). `null`
168
+ * means "no such event received", NOT "did not happen". */
169
+ delivered_at?: string | null;
170
+ opened_at?: string | null;
171
+ clicked_at?: string | null;
172
+ }
173
+ /** An ADDITIVE, non-fatal note on a 202 send — today only `mock_provider`
174
+ * (the send is recorded but no email leaves vxil). */
175
+ export interface NotificationWarning {
176
+ code: string;
177
+ message: string;
178
+ hint?: string;
179
+ }
180
+ export interface NotificationSendResult {
181
+ delivery_id?: string;
182
+ inbox_message_id?: string;
183
+ status: string;
184
+ /** present only for a scheduled send (`send_at` / `delay_seconds`) */
185
+ scheduled_for?: string;
186
+ warnings?: NotificationWarning[];
187
+ }
188
+ export interface Campaign {
189
+ campaign_id: string;
190
+ name: string;
191
+ channel: string;
192
+ template_id: string;
193
+ audience_ref: string;
194
+ schedule_cron: string | null;
195
+ status: 'draft' | 'scheduled' | 'running' | 'done' | 'paused' | 'cancelled';
196
+ created_at: string;
197
+ /** the jobs schedule row this campaign owns; null = no cron registered */
198
+ schedule_id?: string | null;
199
+ last_run_at?: string | null;
200
+ }
201
+ export interface CampaignState {
202
+ campaign_id: string;
203
+ status: string;
204
+ schedule_id: string | null;
81
205
  }
82
206
  export interface FeatureConfig {
83
207
  feature: string;
@@ -131,6 +255,33 @@ export interface VxilPlan {
131
255
  /** which brain produced the plan. */
132
256
  source: 'ai' | 'deterministic';
133
257
  }
258
+ /** Current-month usage projection (GET /v1/usage) — the same meter the
259
+ * platform bills and enforces request quotas from. */
260
+ export interface UsageCurrent {
261
+ /** the current UTC month, 'YYYY-MM'. */
262
+ period: string;
263
+ /** the plan tier the quota is derived from. */
264
+ tier: string;
265
+ requests: {
266
+ /** requests recorded this month (0 until the meter has rows). */
267
+ used: number;
268
+ /** the plan's included monthly requests; null = no fixed cap. */
269
+ quota: number | null;
270
+ /** max(quota − used, 0); null when quota is null. */
271
+ remaining: number | null;
272
+ /** the PLAN's hard-cap policy — true only on the free tier (which is cut
273
+ * off with 429 quota_exceeded when exhausted); paid tiers are never
274
+ * interrupted mid-month. Not a live "the next request will 429" signal. */
275
+ enforced: boolean;
276
+ };
277
+ /** month-to-date per-feature metered detail. Aggregated periodically — it
278
+ * may lag and may be empty. */
279
+ features: Array<{
280
+ feature: string;
281
+ metric: string;
282
+ quantity: number;
283
+ }>;
284
+ }
134
285
  export interface DeadLetter {
135
286
  delivery_id: string;
136
287
  template_id: string;
@@ -203,8 +354,14 @@ export interface PaymentsWebhookEvent {
203
354
  provider: string;
204
355
  provider_evt_id: string;
205
356
  event_type: string;
206
- /** received | processed | error | sig_failed | parse_failed | reprocessed */
357
+ /** received | processed | error | sig_failed | parse_failed | reprocessed |
358
+ * ignored (a well-formed provider type we deliberately do not fold) |
359
+ * unowned (no end user resolvable) | rejected_environment (a sandbox event
360
+ * the tenant did not opt into) — the last three are 200-acked, never folded. */
207
361
  outcome: string | null;
362
+ /** the PROVIDER-reported environment of the delivery ('production' |
363
+ * 'sandbox') — a separate axis from the API key's live/test label. */
364
+ environment?: 'production' | 'sandbox';
208
365
  signature_ok: boolean;
209
366
  error: string | null;
210
367
  received_at: string | null;
@@ -217,6 +374,70 @@ export interface PaymentsWebhookEvent {
217
374
  raw_body?: string | null;
218
375
  provider_event_ts?: string | null;
219
376
  }
377
+ /** The outcome of a provider re-sync / Restore Purchases (payments.md §7d). */
378
+ export interface PaymentsSyncOutcome {
379
+ provider: string;
380
+ synced: number;
381
+ changed: number;
382
+ unchanged: number;
383
+ errors: string[];
384
+ changes: Array<{
385
+ provider_sub_id: string;
386
+ status: string;
387
+ tier: string | null;
388
+ until: string | null;
389
+ }>;
390
+ dry_run?: boolean;
391
+ }
392
+ /** A lifecycle simulation run (payments.md §7d; mock/dev tenants only). */
393
+ export interface PaymentsSimulationResult {
394
+ scenario: string;
395
+ run_id: string;
396
+ user_id: string;
397
+ tier: string;
398
+ tenant_kind: 'standard' | 'dev' | 'unknown';
399
+ note: string;
400
+ steps: Array<{
401
+ event_type: string;
402
+ status: number;
403
+ outcome: string;
404
+ }>;
405
+ oracle: {
406
+ expected: Record<string, 'premium' | 'free'>;
407
+ observed: Record<string, {
408
+ tier: string;
409
+ until: string | null;
410
+ }>;
411
+ pass: boolean;
412
+ };
413
+ }
414
+ /** The ONE complete entitlement read (GET /v1/payments/entitlements; payments.md
415
+ * §3a). `until` is the winning subscription's current_period_end (+ the
416
+ * configured grace window when it is past_due); null on the free baseline. */
417
+ export interface PaymentsEntitlementView {
418
+ user_id: string;
419
+ tier: string;
420
+ entitlements: string[];
421
+ quotas: Record<string, number>;
422
+ source_sub_id: string | null;
423
+ since: string | null;
424
+ until: string | null;
425
+ as_of: string;
426
+ environment: 'production' | 'sandbox';
427
+ /** present only when `?quota=` was asked for */
428
+ quota?: {
429
+ name: string;
430
+ value: number | null;
431
+ };
432
+ /** present only when `?credit_type=` was asked for */
433
+ credits?: {
434
+ credit_type: string;
435
+ balance: number;
436
+ held: number;
437
+ available: number;
438
+ period_end: string | null;
439
+ };
440
+ }
220
441
  export interface FileObject {
221
442
  object_id: string;
222
443
  filename: string;
@@ -225,6 +446,26 @@ export interface FileObject {
225
446
  status: 'pending' | 'available' | 'deleted';
226
447
  created_at: string;
227
448
  }
449
+ /** A live shared link as listed by GET /v1/files/{id}/shared-links (files.md
450
+ * §6). `downloads` is the burn-on-read counter; `max_downloads` null =
451
+ * unlimited (1 = a one-time link). A link that hit its cap, expired, or was
452
+ * revoked no longer lists. */
453
+ export interface FileSharedLink {
454
+ link_id: string;
455
+ created_at: string;
456
+ expires_at: string;
457
+ downloads: number;
458
+ max_downloads: number | null;
459
+ }
460
+ /** Options for POST /v1/files/{id}/shared-links. `expires_at` (ISO 8601) takes
461
+ * precedence over `ttl_seconds`; both are clamped to the tenant's
462
+ * `sharedLinks.maxTtl`. `max_downloads` ≥ 1 caps successful public
463
+ * resolutions (1 = one-time link); omit for unlimited. */
464
+ export interface CreateSharedLinkOptions {
465
+ ttl_seconds?: number;
466
+ expires_at?: string;
467
+ max_downloads?: number | null;
468
+ }
228
469
  /** One recognized OCR text block (files.md §1.1). bbox is [x, y, w, h] in the
229
470
  * provider's unit space; page is 1-based. confidence is 0..1 normalized per
230
471
  * provider (Textract native 0..100 divided by 100; mock pins 0.95); absent
@@ -351,6 +592,12 @@ export interface AiGeneration {
351
592
  usage: AiUsage;
352
593
  finish: string;
353
594
  tool_calls?: AiToolCall[];
595
+ /** the parsed JSON output — present when the request (or its template)
596
+ * carried a response schema; guaranteed to satisfy it (a 200 never violates
597
+ * the schema — final-invalid is a 422 output_schema_mismatch). */
598
+ output?: unknown;
599
+ /** true when the output only validated after the single repair pass. */
600
+ repaired?: boolean;
354
601
  cached: boolean;
355
602
  }
356
603
  /** A streamed generation handle: open the realtime channel for token frames. */
@@ -415,6 +662,57 @@ export interface AiEmbedResult {
415
662
  usage: AiUsage;
416
663
  generation_id: string;
417
664
  }
665
+ /** The per-request knobs every verdict route shares (POST /v1/ai/classify and
666
+ * /v1/ai/judge): a stored template may author the whole prompt (rendered with
667
+ * the verdict vars — `{{input}}`, `{{labels}}`, `{{rubric}}`, `{{examples}}` /
668
+ * `{{candidate}}`, `{{candidates}}`, `{{criteria}}`, `{{scale_min}}`,
669
+ * `{{scale_max}}` — plus your own `vars`); provider/model override the tenant
670
+ * defaults; `max_tokens` defaults to min(config maxTokens, 512) and
671
+ * `temperature` to 0. Verdicts are sync-only (no stream / job mode). */
672
+ export interface AiVerdictOptions {
673
+ template?: string;
674
+ template_version?: number;
675
+ vars?: Record<string, unknown>;
676
+ provider?: string;
677
+ model?: string;
678
+ max_tokens?: number;
679
+ temperature?: number;
680
+ user_id?: string;
681
+ }
682
+ /** A classify verdict (POST /v1/ai/classify). `label` (single) or `labels`
683
+ * (multi:true — every applicable label, possibly empty) is guaranteed to be
684
+ * drawn from the request's `labels`: the allowed set is forced through the
685
+ * structured-output schema, so a 200 can never carry an off-list label. */
686
+ export interface AiClassifyResult {
687
+ generation_id: string;
688
+ label?: string;
689
+ labels?: string[];
690
+ /** 0..1 */
691
+ confidence: number;
692
+ rationale?: string;
693
+ usage: AiUsage;
694
+ /** true when the verdict only validated after the one repair pass. */
695
+ repaired?: boolean;
696
+ cached: boolean;
697
+ }
698
+ /** A judge verdict (POST /v1/ai/judge). One `candidate` → `score` (an integer
699
+ * inside `scale`, default 0..10) + `verdict: 'pass' | 'fail'`; several
700
+ * `candidates` → `scores` (one per candidate, in order) + `best` (the index of
701
+ * the top score) + `verdict: 'tie'` when the top score is shared. Sending
702
+ * `candidates` ALWAYS yields `scores` + `best` — a one-element list keeps the
703
+ * richer `score`/`pass|fail` fields too, so the shape never flips out from
704
+ * under a variable-length array. */
705
+ export interface AiJudgeResult {
706
+ generation_id: string;
707
+ score?: number;
708
+ scores?: number[];
709
+ verdict?: 'pass' | 'fail' | 'tie';
710
+ best?: number;
711
+ rationale?: string;
712
+ usage: AiUsage;
713
+ repaired?: boolean;
714
+ cached: boolean;
715
+ }
418
716
  /** Per-user token rollups (GET /v1/ai/usage). */
419
717
  export interface AiUsageReport {
420
718
  input_tokens: number;
@@ -817,6 +1115,35 @@ type DisabledFeatures<S extends VxilSchemaShape> = {
817
1115
  export type EnabledVxil<S extends VxilSchemaShape> = Omit<Vxil<S>, DisabledFeatures<S>> & {
818
1116
  [P in DisabledFeatures<S>]: DisabledFeature<FeatureMap[P] & string>;
819
1117
  };
1118
+ /** One config-declared per-record ACTION (cms.md §17): a button on a record row
1119
+ * that invokes the deployed tenant function `fn` ONCE with `{ collection,
1120
+ * item_id, action, actor, item }`. Exactly one human-initiated step — no
1121
+ * conditions, no chaining, no scheduling. ≤8 per collection. */
1122
+ export interface CmsActionDef {
1123
+ /** /^[a-z][a-z0-9_]{0,31}$/ — the URL key of `items.runAction` */
1124
+ key: string;
1125
+ /** the button label (1–64 chars) */
1126
+ label: string;
1127
+ /** the deployed function name (/^[a-z][a-z0-9-]{0,47}$/) */
1128
+ fn: string;
1129
+ }
1130
+ /** One live item referencing another through a relation field (cms.md §11.1). */
1131
+ export interface CmsBacklink {
1132
+ collection: string;
1133
+ field: string;
1134
+ item_id: string;
1135
+ status: string;
1136
+ }
1137
+ /** The bounded reverse read (`items.backlinks`): `count` = rows in THIS page
1138
+ * (≤ `limit`), never a total; `has_more` = the cap was hit. */
1139
+ export interface CmsBacklinksPage {
1140
+ collection: string;
1141
+ item_id: string;
1142
+ backlinks: CmsBacklink[];
1143
+ count: number;
1144
+ has_more: boolean;
1145
+ limit: number;
1146
+ }
820
1147
  /** A write guard (cms.md §10): "after this write, at most `max` live items
821
1148
  * match `filter`". Requires `lock` — an unlocked guard is racy by construction. */
822
1149
  export interface CmsGuard {
@@ -1002,6 +1329,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1002
1329
  /** The end-user session token threaded as `X-Vxil-End-User` when set (end-user
1003
1330
  * mode). Undefined ⇒ no header ⇒ server-caller mode (unchanged). */
1004
1331
  private readonly endUserToken;
1332
+ /** The ONE path every request takes: retry + timeout + hooks around
1333
+ * `fetchImpl`. With none of the three configured it is a single fetch. */
1334
+ private readonly transport;
1335
+ private readonly retryOpts;
1336
+ private readonly timeoutMs;
1337
+ private readonly hooks;
1005
1338
  constructor(opts: VxilOptions);
1006
1339
  /** The header bag every request layers on top of `authorization`: the
1007
1340
  * `X-Vxil-End-User` session token in end-user mode, nothing in server mode.
@@ -1010,8 +1343,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1010
1343
  /** Return a client that sends `X-Vxil-End-User: <token>` on every request —
1011
1344
  * a per-call/scoped override of end-user mode over an otherwise server-mode
1012
1345
  * client, mirroring how the edge threads the verified principal. The base
1013
- * URL, api key, fetch impl and version pins are inherited unchanged; only the
1014
- * end-user token is (re)set. Pass a falsy token to get back a server-mode
1346
+ * URL, api key, fetch impl, version pins and the retry/timeout/hooks options
1347
+ * are inherited unchanged; only the end-user token is (re)set. Pass a falsy token to get back a server-mode
1015
1348
  * client (drops the header). See docs/end-user-principals-design.md §4.1. */
1016
1349
  asEndUser(endUserToken: string | undefined): Vxil<S>;
1017
1350
  /** Build the versioned request path — the ONE place a wire path is finalized.
@@ -1084,15 +1417,72 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1084
1417
  user_id: string;
1085
1418
  template: "magic-link" | "welcome" | "transactional";
1086
1419
  data: Record<string, unknown>;
1420
+ /** Explicit wins; otherwise the recipient's stored `locale` attribute
1421
+ * (set it via `users.upsert({ attributes: { locale } })`), then
1422
+ * `config.defaultLocale`. */
1087
1423
  locale?: string;
1088
1424
  /** 'inbox'/'both' require config inboxEnabled */
1089
1425
  channel?: "email" | "inbox" | "both";
1426
+ /** hold the send until this ISO instant (≤ 30 d out) */
1427
+ send_at?: string;
1428
+ /** …or this many seconds from now (≤ 30 d). At most one of the two. */
1429
+ delay_seconds?: number;
1090
1430
  }, opts?: {
1091
1431
  idempotencyKey?: string;
1432
+ }) => Promise<NotificationSendResult>;
1433
+ /** Up to 100 sends in one call. Each item is INDEPENDENTLY atomic (recorded
1434
+ * + enqueued, or neither) and `results` is index-aligned with `items`. */
1435
+ sendBatch: (items: Array<{
1436
+ user_id: string;
1437
+ template: "magic-link" | "welcome" | "transactional";
1438
+ data: Record<string, unknown>;
1439
+ locale?: string;
1440
+ channel?: "email" | "inbox" | "both";
1441
+ send_at?: string;
1442
+ delay_seconds?: number;
1443
+ /** per-item 24 h idempotency key (the header equivalent for one item) */
1444
+ idempotency_key?: string;
1445
+ }>) => Promise<{
1446
+ results: Array<{
1447
+ index: number;
1448
+ delivery_id?: string;
1449
+ inbox_message_id?: string;
1450
+ status?: string;
1451
+ scheduled_for?: string;
1452
+ error?: {
1453
+ code: string;
1454
+ message: string;
1455
+ hint?: string;
1456
+ };
1457
+ }>;
1458
+ count: number;
1459
+ accepted: number;
1460
+ failed: number;
1461
+ warnings?: NotificationWarning[];
1462
+ }>;
1463
+ /** Render a template exactly as a send would, WITHOUT sending. `missing`
1464
+ * lists the required data keys you left out. Needs `notifications:read`. */
1465
+ preview: (templateId: string, input?: {
1466
+ data?: Record<string, unknown>;
1467
+ locale?: string;
1092
1468
  }) => Promise<{
1093
- delivery_id?: string;
1094
- inbox_message_id?: string;
1095
- status: string;
1469
+ template_id: string;
1470
+ locale: string;
1471
+ overridden: boolean;
1472
+ subject: string;
1473
+ html: string;
1474
+ text: string;
1475
+ missing: string[];
1476
+ }>;
1477
+ /** Read-only deliverability probe: is `config.fromEmail`'s domain verified
1478
+ * at the provider? FAIL-SOFT — `verified: 'unknown'` whenever it cannot be
1479
+ * determined (mock provider, no key, provider unreachable). */
1480
+ senderDomain: () => Promise<{
1481
+ domain: string | null;
1482
+ verified: boolean | "unknown";
1483
+ provider: string;
1484
+ checked_at: string;
1485
+ reason?: string;
1096
1486
  }>;
1097
1487
  inbox: {
1098
1488
  list: (q: {
@@ -1120,6 +1510,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1120
1510
  user_id?: string;
1121
1511
  status?: string;
1122
1512
  limit?: number;
1513
+ /** A7: only deliveries that reached this engagement state */
1514
+ engagement?: "delivered" | "opened" | "clicked";
1123
1515
  }) => Promise<Delivery[]>;
1124
1516
  suppressions: {
1125
1517
  list: () => Promise<Array<{
@@ -1161,17 +1553,36 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1161
1553
  }) => Promise<{
1162
1554
  campaign_id: string;
1163
1555
  status: string;
1556
+ schedule_id: string | null;
1164
1557
  }>;
1165
- list: () => Promise<Array<{
1558
+ list: () => Promise<Campaign[]>;
1559
+ /** Stop a scheduled campaign from firing (the jobs cron is torn down). */
1560
+ pause: (campaignId: string) => Promise<CampaignState>;
1561
+ /** Re-register the cron (back to `scheduled`, or `draft` with no cron). */
1562
+ resume: (campaignId: string) => Promise<CampaignState>;
1563
+ /** Terminal stop — the cron is removed and the campaign cannot resume. */
1564
+ cancel: (campaignId: string) => Promise<CampaignState>;
1565
+ /** Soft-delete: hidden from the list, cron removed; the dedupe ledger
1566
+ * survives so a re-created campaign cannot re-send to a served user. */
1567
+ remove: (campaignId: string) => Promise<CampaignState & {
1568
+ deleted?: boolean;
1569
+ }>;
1570
+ /** Push ONE audience page (≤ 1000 rows) into a campaign run — the shape a
1571
+ * tenant audience callback uses. Omit `audience` for a campaign whose
1572
+ * `audience_ref` is the built-in `users:all` selector. */
1573
+ run: (campaignId: string, audience?: Array<{
1574
+ end_user_id: string;
1575
+ locale?: string | null;
1576
+ }>) => Promise<{
1166
1577
  campaign_id: string;
1167
- name: string;
1168
- channel: string;
1169
- template_id: string;
1170
- audience_ref: string;
1171
- schedule_cron: string | null;
1172
- status: string;
1173
- created_at: string;
1174
- }>>;
1578
+ audience_source: "supplied" | "builtin" | "external";
1579
+ audience_size: number;
1580
+ sent: number;
1581
+ deferred: number;
1582
+ skipped_freq_cap: number;
1583
+ skipped_no_email: number;
1584
+ already_sent: number;
1585
+ }>;
1175
1586
  };
1176
1587
  };
1177
1588
  readonly config: {
@@ -1249,6 +1660,13 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1249
1660
  }>;
1250
1661
  }>;
1251
1662
  };
1663
+ /** Current-month usage: requests used vs the plan's included quota, plus
1664
+ * month-to-date per-feature metered detail. Read-only (needs `usage:read`
1665
+ * or `features:read`). Agents: call this before a bulk run to self-check
1666
+ * remaining quota. */
1667
+ readonly usage: {
1668
+ current: () => Promise<UsageCurrent>;
1669
+ };
1252
1670
  readonly jobs: {
1253
1671
  /** Enqueue a one-off job; Vxil POSTs a signed callback to target_url with
1254
1672
  * retries. deliver_after (ISO) / delay_seconds (≤ 30 d, at most one of the
@@ -1414,6 +1832,16 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1414
1832
  delete: (ruleId: string) => Promise<void>;
1415
1833
  };
1416
1834
  };
1835
+ /** vxil-auth. SCOPES (docs/features/auth.md §7.6): every flow here that
1836
+ * obtains, renews, verifies or ends the caller's OWN session (signUp/signIn,
1837
+ * magicLink, otp, anonymous, stepUp, oauth, sessions.refresh/revoke/verify,
1838
+ * password reset) accepts the narrow `auth:signin` — the ONLY auth scope a
1839
+ * key baked into a browser/mobile bundle should carry. `sessions.list` needs
1840
+ * `auth:read`; `sessions.revokeById` and `users.erase` need `auth:write`
1841
+ * (administrative — server keys only). In end-user mode (`endUserToken` /
1842
+ * `asEndUser`) the administrative calls bind to the signed-in user: own
1843
+ * sessions only, own devices only, erase self only. `auth:write` still
1844
+ * satisfies every call. */
1417
1845
  readonly auth: {
1418
1846
  signUp: (input: {
1419
1847
  email: string;
@@ -1446,9 +1874,18 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1446
1874
  * Server-side guessing budget (config otp.maxAttempts), single active code,
1447
1875
  * resend cooldown (429 otp_rate_limited). */
1448
1876
  otp: {
1877
+ /** `test_code` is present ONLY when the address matches auth config
1878
+ * `otp.testRecipients` (store-review / CI accounts): no mail is sent and
1879
+ * the code comes back instead. `captcha_token` is required when the
1880
+ * tenant configured `security.captchaSecretRef`. */
1449
1881
  request: (input: {
1450
1882
  email: string;
1451
- }) => Promise<void>;
1883
+ locale?: string;
1884
+ captcha_token?: string;
1885
+ }) => Promise<{
1886
+ sent: true;
1887
+ test_code?: string;
1888
+ }>;
1452
1889
  verify: (input: {
1453
1890
  email: string;
1454
1891
  code: string;
@@ -1470,7 +1907,16 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1470
1907
  request: (input: {
1471
1908
  token: string;
1472
1909
  email: string;
1473
- }) => Promise<void>;
1910
+ locale?: string;
1911
+ }) => Promise<{
1912
+ sent: true;
1913
+ test_code?: string;
1914
+ }>;
1915
+ /** On success the guest keeps its user id (`user_id` unchanged).
1916
+ * If the claimed email ALREADY has an account, the guest is MERGED
1917
+ * into it: `user_id` is the existing account, `merged: true`, and a
1918
+ * fresh `session.token` (same session, re-signed for the merged
1919
+ * identity — swap it client-side; other guest sessions are revoked). */
1474
1920
  verify: (input: {
1475
1921
  token: string;
1476
1922
  email: string;
@@ -1479,6 +1925,11 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1479
1925
  user_id: string;
1480
1926
  email: string;
1481
1927
  verified: boolean;
1928
+ merged?: true;
1929
+ session?: {
1930
+ token: string;
1931
+ expires_at: string;
1932
+ };
1482
1933
  }>;
1483
1934
  };
1484
1935
  };
@@ -1488,7 +1939,11 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1488
1939
  stepUp: {
1489
1940
  request: (input: {
1490
1941
  token: string;
1491
- }) => Promise<void>;
1942
+ locale?: string;
1943
+ }) => Promise<{
1944
+ sent: true;
1945
+ test_code?: string;
1946
+ }>;
1492
1947
  verify: (input: {
1493
1948
  token: string;
1494
1949
  code: string;
@@ -1506,11 +1961,22 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1506
1961
  * (email → tombstone, password/name/avatar cleared) and revoke every
1507
1962
  * live session. Idempotent — a second call on an already-erased user is a
1508
1963
  * no-op. Meant to be called from a "delete my account" server route or
1509
- * vxil function (which declares `auth:write`). */
1964
+ * vxil function (which declares `auth:write`). In END-USER mode the call
1965
+ * is SELF-only: `userId` must be the signed-in user, any other id throws
1966
+ * 403 `server_only` (a thin client can never erase someone else). */
1510
1967
  erase: (userId: string) => Promise<{
1511
1968
  user_id: string;
1512
1969
  erased: boolean;
1513
1970
  }>;
1971
+ /** END-USER MODE ONLY (`endUserToken` / `asEndUser`): the signed-in user
1972
+ * deep-merges a bounded `attributes` object into its OWN shared identity
1973
+ * row (null deletes a key; ≤8 KB, ≤64 top-level keys, depth ≤8). Profile
1974
+ * fields are not patchable here. In server mode the call throws 422
1975
+ * `end_user_mode_required` — use `users.patch(id, …)` instead. */
1976
+ patchMe: (attributes: Record<string, unknown>) => Promise<{
1977
+ user_id: string;
1978
+ attributes: Record<string, unknown>;
1979
+ }>;
1514
1980
  };
1515
1981
  sessions: {
1516
1982
  verify: (token: string) => Promise<{
@@ -1528,11 +1994,26 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1528
1994
  session: AuthSession;
1529
1995
  }>;
1530
1996
  revoke: (token: string) => Promise<void>;
1997
+ /** "Sign out everywhere": revoke EVERY live session of a user in one call
1998
+ * (one statement, one batched edge-cache write; each session's
1999
+ * revoked_reason = 'revoke_all'). SERVER mode names the user (`userId`,
2000
+ * scope `auth:write`); in END-USER mode the signed-in user's own sessions
2001
+ * go and `userId` is ignored (scope `auth:signin` suffices). Idempotent:
2002
+ * a user with no live sessions returns `revoked: 0`. */
2003
+ revokeAll: (userId?: string) => Promise<{
2004
+ user_id: string;
2005
+ revoked: number;
2006
+ session_ids: string[];
2007
+ }>;
1531
2008
  /** Force-revoke a SPECIFIC session by its `session_id` (from `list`) — the
1532
2009
  * server-forced device-lockout path beyond the client-cooperative
1533
- * by-token `revoke`. Throws 404 when the id is unknown or already revoked. */
2010
+ * by-token `revoke`. Throws 404 when the id is unknown or already revoked
2011
+ * — or, in end-user mode, when it belongs to another user (a signed-in
2012
+ * user can kick only their OWN devices). Scope `auth:write`. */
1534
2013
  revokeById: (sessionId: string) => Promise<void>;
1535
- /** List a user's active sessions (the account "signed-in devices" surface). */
2014
+ /** List a user's active sessions (the account "signed-in devices" surface).
2015
+ * Scope `auth:read`. In end-user mode the list is bound to the signed-in
2016
+ * user regardless of `userId` (the verified principal wins). */
1536
2017
  list: (userId: string) => Promise<Array<{
1537
2018
  session_id: string;
1538
2019
  created_at: string;
@@ -1551,21 +2032,48 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1551
2032
  password: string;
1552
2033
  }) => Promise<void>;
1553
2034
  };
2035
+ /** Email verification (the flow behind auth config `emailVerification.required`,
2036
+ * which makes password sign-in answer 403 `email_not_verified` until the
2037
+ * address is confirmed). `request` always resolves (anti-enumeration —
2038
+ * a link is mailed only to an existing, unverified user; `redirect_url`
2039
+ * is your page that reads `?token=` and calls `confirm`). `confirm` is
2040
+ * single-use and expires after 24h. */
2041
+ emailVerification: {
2042
+ request: (input: {
2043
+ email: string;
2044
+ redirect_url: string;
2045
+ locale?: string;
2046
+ captcha_token?: string;
2047
+ }) => Promise<void>;
2048
+ confirm: (token: string) => Promise<{
2049
+ user_id: string;
2050
+ verified: true;
2051
+ }>;
2052
+ };
1554
2053
  /** Social sign-in. The web `start`/`callback` flows are browser redirects
1555
- * (not JSON calls); `native` is the mobile token-exchange the SDK wraps. */
2054
+ * (not JSON calls): send the browser to
2055
+ * `GET /v1/auth/oauth/{provider}/start?redirect_uri=…` (server-side, with
2056
+ * your key) and hand the returned `code`+`state` to `…/callback`; `native`
2057
+ * is the mobile / broker token-exchange the SDK wraps. `oidc` is the
2058
+ * tenant's generic OIDC / SSO issuer (auth config `providers.oidc` —
2059
+ * Okta / Entra / Auth0 / any OpenID Connect IdP, or a SAML broker that
2060
+ * speaks OIDC); it rides the same three routes. */
1556
2061
  oauth: {
1557
2062
  /** Native social sign-in: exchange a provider `id_token`/`access_token`
1558
2063
  * for a vxil session (`linked` marks whether the user was created or
1559
2064
  * matched to an existing identity). */
1560
- native: (provider: "google" | "apple" | "github" | "facebook" | "mock", input: {
2065
+ native: (provider: "google" | "apple" | "github" | "facebook" | "mock" | "oidc", input: {
1561
2066
  id_token?: string;
1562
2067
  access_token?: string;
1563
2068
  nonce?: string;
2069
+ anonymous_token?: string;
1564
2070
  }) => Promise<{
1565
2071
  user_id: string;
1566
2072
  session: AuthSession;
1567
2073
  verified: boolean;
1568
- linked: "created" | "existing";
2074
+ linked: "created" | "existing" | "promoted" | "merged";
2075
+ /** sessions taken over by auth config session.maxConcurrent (present once opted in) */
2076
+ took_over?: string[];
1569
2077
  }>;
1570
2078
  };
1571
2079
  };
@@ -1573,8 +2081,10 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1573
2081
  createPolicy: (input: {
1574
2082
  name: string;
1575
2083
  key_template: string;
1576
- limit: number;
1577
- window_seconds: number;
2084
+ /** omitted ⇒ seeded from the tenant's rate-limits config `defaults` (F8-54) */
2085
+ limit?: number;
2086
+ /** omitted ⇒ seeded from the tenant's rate-limits config `defaults` (F8-54) */
2087
+ window_seconds?: number;
1578
2088
  behavior?: "block" | "shape";
1579
2089
  }) => Promise<RateLimitPolicy>;
1580
2090
  listPolicies: () => Promise<RateLimitPolicy[]>;
@@ -1692,14 +2202,31 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1692
2202
  * scalar-valued types only. Concurrent duplicates → 409 unique_violation. */
1693
2203
  unique?: boolean;
1694
2204
  /** relation fields only: what a delete of the referenced item does to
1695
- * this one (bounded fan-out, cms.md §11). */
1696
- on_delete?: "cascade" | "set_null";
2205
+ * this one — cascade / set_null (bounded fan-out, cms.md §11) or
2206
+ * `restrict` (§11.1: the delete is refused with 409 `referenced`
2207
+ * while a live reference exists). */
2208
+ on_delete?: "cascade" | "set_null" | "restrict";
2209
+ /** FIELD-LEVEL read security (cms.md §18): the end-user ORG ROLE slugs
2210
+ * allowed to READ this field. Omitted/`[]` = ungated. A non-empty list
2211
+ * is FAIL-SAFE — in verified end-user mode the field is OMITTED from
2212
+ * every read (get / list / query / `$expand` / the write-response echo)
2213
+ * unless the session's verified roles intersect it, it is UNQUERYABLE
2214
+ * (filter/sort/group-by on it is a 422), and it is NEVER served on the
2215
+ * anonymous public lane. A SERVER caller (an API key with no end-user
2216
+ * session) still sees every field. Writes are unaffected. ≤16 entries,
2217
+ * each `^[a-z0-9][a-z0-9_-]{0,31}$` (the `orgs` role alphabet). */
2218
+ read_roles?: string[];
1697
2219
  }>;
2220
+ /** Per-record action buttons (cms.md §17): `[{ key, label, fn }]` —
2221
+ * exactly ONE human-initiated step each; `fn` names a deployed tenant
2222
+ * function invoked by `items.runAction`. ≤8 per collection. */
2223
+ actions?: CmsActionDef[];
1698
2224
  }) => Promise<{
1699
2225
  collection: string;
1700
2226
  fields: number;
1701
2227
  owner_field?: string;
1702
2228
  public?: boolean;
2229
+ actions?: CmsActionDef[];
1703
2230
  }>;
1704
2231
  list: () => Promise<Array<{
1705
2232
  collection: string;
@@ -1708,6 +2235,14 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1708
2235
  owner_field?: string | null;
1709
2236
  }>>;
1710
2237
  addField: (collection: string, field: Record<string, unknown>) => Promise<void>;
2238
+ /** Set (or clear, with `null`/`[]`) a field's READ-ROLE gate (cms.md §18) —
2239
+ * the same-type in-place alter on the fields route. Re-sends the field's
2240
+ * `type` (required by the alter path); every OTHER attribute the field
2241
+ * carries is re-sent from `rest`, because the alter overwrites the whole
2242
+ * definition. In verified end-user mode a gated field is omitted from every
2243
+ * read unless the session's verified roles intersect `roles`; server-caller
2244
+ * reads and ALL writes are unaffected. */
2245
+ setFieldReadRoles: (collection: string, field: string, type: "string" | "text" | "int" | "float" | "bool" | "datetime" | "json" | "relation" | "file", roles: string[] | null, rest?: Record<string, unknown>) => Promise<void>;
1711
2246
  /** Set (or clear, with `null`) the collection's end-user owner-scope flag
1712
2247
  * (design §5.1). Names an existing `string` field that holds the owner id. */
1713
2248
  setOwnerField: (collection: string, ownerField: string | null) => Promise<void>;
@@ -1717,6 +2252,11 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1717
2252
  * the owner_field are never exposed. `false` closes the lane (and purges the
1718
2253
  * edge cache). Read the public side with `cmsPublicUrl` / `listCmsPublic`. */
1719
2254
  setPublic: (collection: string, isPublic: boolean) => Promise<void>;
2255
+ /** Replace the collection's per-record ACTION list (cms.md §17) — the
2256
+ * `{ actions }` fields-route meta-op; `[]` clears. Each action is ONE
2257
+ * human-initiated step: the dashboard renders it as a button per record,
2258
+ * and `items.runAction` invokes its deployed function. */
2259
+ setActions: (collection: string, actions: CmsActionDef[]) => Promise<void>;
1720
2260
  };
1721
2261
  items: {
1722
2262
  /** `lock` serializes same-key writers (per-tenant advisory lock); `guard`
@@ -1774,6 +2314,27 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1774
2314
  set_null: number;
1775
2315
  }>;
1776
2316
  publish: (collection: string, itemId: string) => Promise<Record<string, unknown>>;
2317
+ /** P1-12 (cms.md §11.1): the BOUNDED reverse read — which live items
2318
+ * reference this one, through which relation field. Owner-scoped in
2319
+ * end-user mode like `get`. `count` is the page returned (≤ limit, max
2320
+ * 100), never a total; `has_more` says the cap was hit. A DELETE refused
2321
+ * with 409 `referenced` (an `on_delete: 'restrict'` edge) names the same
2322
+ * refs in its `referencing` field. */
2323
+ backlinks: (collection: string, itemId: string, opts?: {
2324
+ limit?: number;
2325
+ }) => Promise<CmsBacklinksPage>;
2326
+ /** P1-7 (cms.md §17): run ONE declared per-record action — invokes the
2327
+ * action's deployed function with `{ collection, item_id, action, actor,
2328
+ * item }` and returns its result. 404 when the key is not declared;
2329
+ * 502 `action_failed` (with `upstream.code` = the function's error
2330
+ * class) when the function fails. Requires cms:write. */
2331
+ runAction: (collection: string, itemId: string, key: string) => Promise<{
2332
+ collection: string;
2333
+ item_id: string;
2334
+ action: string;
2335
+ fn: string;
2336
+ result: unknown;
2337
+ }>;
1777
2338
  /** cms-rel B2 (cms.md §12): bounded group-by aggregate. fns count|sum|
1778
2339
  * min|max|avg over index-slot-bound fields; groupBy ≤2 slot-bound
1779
2340
  * fields; filter = the full query DSL incl. ONE-hop dotted join terms
@@ -2335,6 +2896,24 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2335
2896
  }>;
2336
2897
  }>;
2337
2898
  };
2899
+ /** Every audit event the platform can emit, with the subscribable prefixes
2900
+ * and their counts — the discovery surface for `subscribe({ event_prefixes })`.
2901
+ * Static reference data (CI-generated from the emitters), identical for every
2902
+ * tenant. `level` is the event CLASS ('lifecycle' | 'failure'), not the
2903
+ * payload's 'info'|'warn'|'error' severity key. */
2904
+ eventCatalog: () => Promise<{
2905
+ count: number;
2906
+ events: Array<{
2907
+ name: string;
2908
+ feature: string;
2909
+ level: "lifecycle" | "failure";
2910
+ payload_keys?: string[];
2911
+ }>;
2912
+ prefixes: Array<{
2913
+ prefix: string;
2914
+ count: number;
2915
+ }>;
2916
+ }>;
2338
2917
  };
2339
2918
  readonly orgs: {
2340
2919
  create: (input: {
@@ -2617,20 +3196,20 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2617
3196
  expires_at: string | null;
2618
3197
  }>;
2619
3198
  sharedLinks: {
2620
- /** Public (unauthenticated) URL for the object; revocable. */
2621
- create: (objectId: string, opts?: {
2622
- ttl_seconds?: number;
2623
- }) => Promise<{
3199
+ /** Public (unauthenticated) URL for the object; revocable. Optional
3200
+ * `max_downloads` (1 = one-time link) and an absolute `expires_at`; the
3201
+ * public resolver answers 410 `link_exhausted` / `link_expired` past
3202
+ * either bound. */
3203
+ create: (objectId: string, opts?: CreateSharedLinkOptions) => Promise<{
2624
3204
  link_id: string;
2625
3205
  url: string;
2626
- expires_at: string | null;
3206
+ expires_in: number;
3207
+ expires_at: string;
3208
+ max_downloads: number | null;
3209
+ downloads: number;
2627
3210
  }>;
2628
- /** List an object's active (unrevoked, unexpired) shared links. */
2629
- list: (objectId: string) => Promise<Array<{
2630
- link_id: string;
2631
- created_at: string;
2632
- expires_at: string | null;
2633
- }>>;
3211
+ /** List an object's active (unrevoked, unexpired, unexhausted) shared links. */
3212
+ list: (objectId: string) => Promise<FileSharedLink[]>;
2634
3213
  revoke: (linkId: string) => Promise<void>;
2635
3214
  };
2636
3215
  };
@@ -2730,7 +3309,10 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2730
3309
  readonly ai: {
2731
3310
  templates: {
2732
3311
  /** Store a tenant-authored prompt template; versions are monotonic per name.
2733
- * `{{var}}` placeholders are filled from `generate`'s `input`. */
3312
+ * `{{var}}` placeholders are filled from `generate`'s `input`.
3313
+ * `schema` (a JSON Schema) is now ENFORCED at generate time on sync/job
3314
+ * requests — the output is validated (with one repair pass) against it;
3315
+ * a per-request `response_schema` overrides it. Ignored on streams. */
2734
3316
  put: (input: {
2735
3317
  template: string;
2736
3318
  user: string;
@@ -2772,6 +3354,13 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2772
3354
  * role:'tool' results) — vxil relays; you execute the tools. */
2773
3355
  messages?: AiChatMessage[];
2774
3356
  json_mode?: boolean;
3357
+ /** JSON Schema the completion must satisfy (schema-guaranteed structured
3358
+ * output): enforced natively where the provider supports strict
3359
+ * structured output, else via one validate-and-repair pass; the parsed
3360
+ * object lands in `output` (+`repaired` when the repair pass fired). A
3361
+ * still-invalid result is a 422 `output_schema_mismatch` (billed). Wins
3362
+ * over the template's stored schema. */
3363
+ response_schema?: Record<string, unknown>;
2775
3364
  images?: string[];
2776
3365
  user_id?: string;
2777
3366
  }) => Promise<AiGeneration>;
@@ -2823,6 +3412,11 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2823
3412
  }>;
2824
3413
  messages?: AiChatMessage[];
2825
3414
  json_mode?: boolean;
3415
+ /** JSON Schema the completion must satisfy — same contract as
3416
+ * `generate`; the validated answer lands in the replay buffer (a
3417
+ * schema-mismatch delivers finish:'schema_mismatch' on the terminal
3418
+ * usage frame). */
3419
+ response_schema?: Record<string, unknown>;
2826
3420
  images?: string[];
2827
3421
  user_id?: string;
2828
3422
  }) => Promise<AiJobHandle>;
@@ -2841,6 +3435,47 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2841
3435
  done: boolean;
2842
3436
  max_seq: number;
2843
3437
  }>;
3438
+ /** Classify an input into one of YOUR labels (or, with `multi:true`, every
3439
+ * label that applies) — a schema-forced verdict over the generate path:
3440
+ * the allowed labels ride the structured-output schema as an enum, so the
3441
+ * model cannot answer off-list; the reply is `{ label | labels,
3442
+ * confidence, rationale }`. Same metering/cache/provider rules as
3443
+ * `generate` (a still-invalid verdict is a billed 422
3444
+ * output_schema_mismatch). Caps: 2–64 labels (≤64 chars, unique), input
3445
+ * ≤32k chars (a string or ordered `{ text }` parts joined into ONE
3446
+ * input), rubric ≤4k, ≤16 examples. */
3447
+ classify: (input: AiVerdictOptions & {
3448
+ input: string | Array<{
3449
+ text: string;
3450
+ }>;
3451
+ labels: string[];
3452
+ multi?: boolean;
3453
+ rubric?: string;
3454
+ examples?: Array<{
3455
+ input: string;
3456
+ label: string;
3457
+ }>;
3458
+ }) => Promise<AiClassifyResult>;
3459
+ /** Grade a candidate response (or rank up to 8) against YOUR criteria —
3460
+ * a schema-forced integer score inside `scale` (default 0..10) plus
3461
+ * `pass|fail` for one candidate, or `scores[]` + `best` (+ `verdict:'tie'`)
3462
+ * for several — `candidates` always yields `scores`/`best`, even at
3463
+ * length 1. Same metering/cache/provider rules as `generate`. Caps:
3464
+ * input ≤16k chars, candidates ≤8 × 4k chars, criteria ≤16 items / 4k
3465
+ * chars, scale span ≤100. */
3466
+ judge: (input: AiVerdictOptions & {
3467
+ input: string;
3468
+ candidate?: string;
3469
+ candidates?: string[];
3470
+ criteria: string | Array<{
3471
+ name: string;
3472
+ weight?: number;
3473
+ }>;
3474
+ scale?: {
3475
+ min: number;
3476
+ max: number;
3477
+ };
3478
+ }) => Promise<AiJudgeResult>;
2844
3479
  /** Embed a batch of strings (1–256). */
2845
3480
  embed: (input: {
2846
3481
  input: string[];
@@ -2990,27 +3625,43 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2990
3625
  */
2991
3626
  readonly payments: {
2992
3627
  /** Provider + ledger capability surface (+ the `missing` set an agent can act
2993
- * on to recommend a provider). */
3628
+ * on to recommend a provider). `client` is the RevenueCat tenant's PUBLIC
3629
+ * SDK handout (project id + public SDK key — never the secret key); null
3630
+ * for every other provider. */
2994
3631
  capabilities: () => Promise<{
2995
3632
  provider: string;
2996
3633
  capabilities: string[];
2997
3634
  ledger_capabilities: string[];
2998
3635
  missing: string[];
3636
+ client: {
3637
+ revenuecat: {
3638
+ project_id: string;
3639
+ public_sdk_key: string;
3640
+ };
3641
+ } | null;
2999
3642
  }>;
3000
3643
  /** The user's effective entitlement snapshot (the multi-sub tier fold):
3001
- * tier + boolean entitlements + numeric quotas. */
3002
- getEntitlements: (userId: string) => Promise<{
3003
- user_id: string;
3004
- tier: string;
3005
- entitlements: string[];
3006
- quotas: Record<string, number>;
3007
- }>;
3644
+ * tier + boolean entitlements + numeric quotas — plus the access window
3645
+ * (`since` / `until` = the winning subscription's period end, or end +
3646
+ * grace when it is past_due), `as_of` (server time), the `environment`
3647
+ * the winning subscription was written from, and OPTIONAL inline reads:
3648
+ * `quota` inlines one quota, `creditType` inlines the same owner-bound
3649
+ * balance `getBalance` returns — ONE call for a thin client's paywall.
3650
+ * A non-2xx answer means UNKNOWN: render the last cached answer, never
3651
+ * free (payments.md §3a). */
3652
+ getEntitlements: (userId: string, opts?: {
3653
+ quota?: string;
3654
+ creditType?: string;
3655
+ }) => Promise<PaymentsEntitlementView>;
3008
3656
  /** Boolean gate: does the user hold `entitlement`? Returns granted + its
3009
- * source (subscription/tier) + the resolved tier. */
3657
+ * source (subscription/tier) + the resolved tier, plus `until` (how long
3658
+ * the answer is good for) and `as_of`. Non-2xx = unknown, never free. */
3010
3659
  hasEntitlement: (userId: string, entitlement: string) => Promise<{
3011
3660
  granted: boolean;
3012
3661
  source: string | null;
3013
3662
  tier: string;
3663
+ until: string | null;
3664
+ as_of: string;
3014
3665
  }>;
3015
3666
  /** A single numeric quota for the user (e.g. `seats`, `api_calls`); null when
3016
3667
  * the resolved tier declares no such quota. Convenience over getEntitlements. */
@@ -3161,10 +3812,107 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
3161
3812
  status: "pending" | "succeeded" | "failed";
3162
3813
  amount_cents: number;
3163
3814
  }>;
3164
- /** The provider-hosted billing-management portal URL for an end user (Stripe
3165
- * billing portal; mock is deterministic; RC/Paddle/PayPal → 501). */
3815
+ /** Where the user MANAGES their subscription, resolved by FUNDING SOURCE
3816
+ * (the provider of their live subscription, not the tenant's primary):
3817
+ * `kind: 'portal'` = a provider-minted portal session URL (Stripe, Paddle
3818
+ * via the captured customer id, mock); `'store'` = the App Store / Play
3819
+ * Store manage-subscriptions page for a RevenueCat row that recorded its
3820
+ * store; `'none'` (url null) = nothing to link (PayPal, a manual grant, no
3821
+ * live subscription on a portal-less primary). */
3166
3822
  getCustomerPortalUrl: (userId: string) => Promise<{
3167
- url: string;
3823
+ url: string | null;
3824
+ kind: "portal" | "store" | "none";
3825
+ provider: string | null;
3826
+ }>;
3827
+ /** SERVER-ONLY. A support / promotional / migration grant: a `manual`
3828
+ * subscription row (no provider behind it) folded like a webhook —
3829
+ * entitlements + the tier's recurring credits apply; `until` null =
3830
+ * open-ended, else access ends at `until`. Idempotent on
3831
+ * `idempotency_key` (a replay returns the first grant, `replayed: true`).
3832
+ * 422 unknown_tier when the tier is not in ledger.tierMap. */
3833
+ grantSubscription: (input: {
3834
+ user_id: string;
3835
+ tier: string;
3836
+ until?: string | null;
3837
+ reason: string;
3838
+ idempotency_key: string;
3839
+ }) => Promise<{
3840
+ subscription_id: string | null;
3841
+ user_id: string;
3842
+ tier: string;
3843
+ status: string;
3844
+ until: string | null;
3845
+ replayed: boolean;
3846
+ }>;
3847
+ /** SERVER-ONLY. Revoke a MANUAL grant now (409 not_manual for a provider
3848
+ * subscription — use `cancel`). Never touches a provider. */
3849
+ revokeSubscription: (subscriptionId: string) => Promise<{
3850
+ subscription_id: string;
3851
+ status: string;
3852
+ cancel_at: string | null;
3853
+ revoked_at: string | null;
3854
+ }>;
3855
+ /** SERVER-ONLY. Re-read a customer's subscriptions FROM the provider and
3856
+ * fold them through the same conditional upsert a webhook uses (never a
3857
+ * second write path). `dry_run` diffs without writing. Capped 30/min per
3858
+ * project (429 rate_limited). The migration backfill (`vxil migrate
3859
+ * payments --from-provider …`) drives this per customer. */
3860
+ syncSubscriptions: (input: {
3861
+ user_id?: string;
3862
+ customer_ref?: string;
3863
+ provider?: "mock" | "stripe" | "paddle" | "revenuecat" | "paypal";
3864
+ dry_run?: boolean;
3865
+ }) => Promise<PaymentsSyncOutcome>;
3866
+ /** The server leg of "Restore Purchases": in end-user mode the verified
3867
+ * principal's OWN provider state is re-read (no user id needed); in server
3868
+ * mode pass `user_id`. 3 per hour per user (429). Returns the sync outcome
3869
+ * plus the fresh entitlement view in one answer. */
3870
+ restoreSubscriptions: (input?: {
3871
+ user_id?: string;
3872
+ provider?: "mock" | "stripe" | "paddle" | "revenuecat" | "paypal";
3873
+ }) => Promise<PaymentsSyncOutcome & {
3874
+ entitlements: PaymentsEntitlementView;
3875
+ }>;
3876
+ /** SERVER-ONLY. Account merge: move EVERY provider's subscriptions,
3877
+ * customer links and available credit balances of `from_user_id` onto
3878
+ * `into_user_id` — at-most-once per pair. The consumer of the
3879
+ * `auth.user.merged { from, into }` audit event. */
3880
+ reKeySubscriptions: (input: {
3881
+ from_user_id: string;
3882
+ into_user_id: string;
3883
+ }) => Promise<{
3884
+ from_user_id: string;
3885
+ into_user_id: string;
3886
+ moved: number;
3887
+ credits_moved: number;
3888
+ }>;
3889
+ /** SERVER-ONLY, mock/dev tenants ONLY (403 simulation_not_allowed
3890
+ * elsewhere). Drive a scripted lifecycle (renewal, expiry, refund-pair,
3891
+ * past-due-grace, cross-platform-unlock, transfer) through the mock
3892
+ * webhook path and judge it with the conformance oracle. */
3893
+ simulate: (input: {
3894
+ scenario: "refund-pair" | "cross-platform-unlock" | "renewal" | "expiry" | "past-due-grace" | "transfer";
3895
+ user_id: string;
3896
+ tier?: string;
3897
+ }) => Promise<PaymentsSimulationResult>;
3898
+ /** The last report-only reconciliation sweep result for this project
3899
+ * (`run: null` before the first daily tick). */
3900
+ reconcile: () => Promise<{
3901
+ run: {
3902
+ run_id: string;
3903
+ ran_at: string | null;
3904
+ clean: boolean;
3905
+ discrepancy_count: number;
3906
+ lapsed_count: number;
3907
+ findings: Array<{
3908
+ kind: string;
3909
+ count: number;
3910
+ ids: string[];
3911
+ }>;
3912
+ } | null;
3913
+ enforce_period_end: {
3914
+ slack_hours: number;
3915
+ } | null;
3168
3916
  }>;
3169
3917
  /** List charges newest-first (the refund enabler — discover the charge_id).
3170
3918
  * Filters: user_id, status, limit (clamped 1..100, default 50). */
@@ -3194,7 +3942,9 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
3194
3942
  list: (q?: {
3195
3943
  provider?: "mock" | "stripe" | "paddle" | "revenuecat" | "paypal";
3196
3944
  event_type?: string;
3197
- outcome?: "received" | "processed" | "error" | "sig_failed" | "parse_failed" | "reprocessed";
3945
+ outcome?: "received" | "processed" | "error" | "sig_failed" | "parse_failed" | "reprocessed" | "ignored" | "unowned" | "rejected_environment";
3946
+ /** the provider-reported environment axis (not the API key's label) */
3947
+ environment?: "production" | "sandbox";
3198
3948
  since?: string;
3199
3949
  cursor?: string;
3200
3950
  limit?: number;