@vxil/sdk 0.1.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.
@@ -0,0 +1,3103 @@
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
+ export type ApiVersion = 'v1';
7
+ /** A per-feature API-version override key: the URL namespace that leads a path
8
+ * (`/v1/<namespace>/…`). Matches the segment `versionedPath` rewrites, so an
9
+ * override changes exactly that namespace's paths. Renamed client accessors map
10
+ * 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';
12
+ export interface VxilOptions {
13
+ apiKey: string;
14
+ /** defaults to the production edge */
15
+ baseUrl?: string;
16
+ fetch?: typeof fetch;
17
+ /** An end-user's vxil-`auth` **session token** (the ES256 JWT `auth` mints).
18
+ * When set, every request additionally carries it as the `X-Vxil-End-User`
19
+ * header — the edge verifies it and threads a **verified `end_user_id`** into
20
+ * the inner JWT, putting the request in **end-user mode** (owner-scoped reads/
21
+ * writes on owned collections; default-safe). This is the thin-client shape:
22
+ * a mobile/SPA holds a public/thin-client key + the signed-in user's session
23
+ * token and calls the edge directly, safely scoped to that user. Absent ⇒
24
+ * **no header** ⇒ server-caller mode, exactly as today (backward compatible).
25
+ * See docs/end-user-principals-design.md §4.1, §10. For a per-call override on
26
+ * an otherwise server-mode client, use `vx.asEndUser(token)`. */
27
+ endUserToken?: string;
28
+ /** Global API major every request pins (default `'v1'`). Path-major, per
29
+ * docs/feature-versioning.md — a breaking change ships as a new major on a
30
+ * new path, never in place. */
31
+ apiVersion?: ApiVersion;
32
+ /** Per-feature API-major overrides (`{ cms: 'v2' }`); each takes precedence
33
+ * over `apiVersion` for that namespace only. Optional; the compiled default
34
+ * is `'v1'` for every feature. */
35
+ apiVersions?: Partial<Record<FeatureKey, ApiVersion>>;
36
+ }
37
+ /** Rewrite a built `/v1/<ns>/…` path onto the version configured for its
38
+ * namespace: the per-namespace override wins, else the global default. PURE and
39
+ * exported so it is unit-testable with a hypothetical future major (the runtime
40
+ * accepts any version string; the public options constrain to the `ApiVersion`
41
+ * union). A path whose first segment is not `/v1` is returned untouched. */
42
+ export declare function versionedPath(path: string, globalDefault: string, overrides: Record<string, string>): string;
43
+ export interface Meta {
44
+ request_id: string;
45
+ tenant_id?: string;
46
+ }
47
+ export declare class VxilError extends Error {
48
+ readonly status: number;
49
+ readonly code: string;
50
+ readonly hint?: string | undefined;
51
+ readonly fixUrl?: string | undefined;
52
+ readonly requestId?: string | undefined;
53
+ constructor(status: number, code: string, message: string, hint?: string | undefined, fixUrl?: string | undefined, requestId?: string | undefined);
54
+ }
55
+ export interface VxilUser {
56
+ id: string;
57
+ email: string | null;
58
+ display_name: string | null;
59
+ avatar_url: string | null;
60
+ attributes: Record<string, unknown>;
61
+ created_at: string;
62
+ updated_at: string;
63
+ deleted_at: string | null;
64
+ /** Set when the row's PII was scrubbed via DELETE /v1/users/:id?erase=true (GDPR Article 17). null = retained. */
65
+ erased_at?: string | null;
66
+ }
67
+ export interface Delivery {
68
+ delivery_id: string;
69
+ end_user_id: string;
70
+ template_id: string;
71
+ locale: string;
72
+ to_email: string;
73
+ status: 'queued' | 'sent' | 'failed' | 'suppressed';
74
+ attempts: number;
75
+ provider: string;
76
+ provider_message_id: string | null;
77
+ last_error_code: string | null;
78
+ queued_at: string;
79
+ sent_at: string | null;
80
+ failed_at: string | null;
81
+ }
82
+ export interface FeatureConfig {
83
+ feature: string;
84
+ version: number;
85
+ manifest: Record<string, unknown>;
86
+ }
87
+ export interface DeadLetter {
88
+ delivery_id: string;
89
+ template_id: string;
90
+ to_email: string;
91
+ last_error_code: string | null;
92
+ created_at: string;
93
+ replayed_at: string | null;
94
+ }
95
+ export interface JobRun {
96
+ run_id: string;
97
+ job_name: string;
98
+ kind?: string;
99
+ state: 'queued' | 'running' | 'retrying' | 'waiting' | 'delayed' | 'succeeded' | 'failed' | 'dead' | 'cancelled';
100
+ attempt_number: number;
101
+ max_attempts?: number;
102
+ last_error_class?: string | null;
103
+ last_error_msg?: string | null;
104
+ /** earliest next pickup time; for a 'delayed' run this is deliver_after */
105
+ next_attempt_at?: string | null;
106
+ queued_at: string;
107
+ completed_at: string | null;
108
+ }
109
+ export interface JobFlowRule {
110
+ rule_id: string;
111
+ match_kind: 'endpoint' | 'job_name';
112
+ match_value: string;
113
+ max_parallel: number | null;
114
+ rate_per_minute: number | null;
115
+ state: string;
116
+ created_at: string;
117
+ }
118
+ export interface JobSchedule {
119
+ schedule_id: string;
120
+ job_name: string;
121
+ cron: string | null;
122
+ run_at: string | null;
123
+ next_run_at: string | null;
124
+ state: string;
125
+ }
126
+ export interface AuthSession {
127
+ token: string;
128
+ refresh_token: string;
129
+ expires_at: string;
130
+ }
131
+ export interface RateLimitPolicy {
132
+ policy_id: string;
133
+ name: string;
134
+ key_template: string;
135
+ limit: number;
136
+ window_seconds: number;
137
+ behavior: 'block' | 'shape';
138
+ }
139
+ /** A per-identifier override layered over a policy (exact pattern beats
140
+ * wildcard; among `*`-globs the longest literal prefix wins). Unset fields
141
+ * fall through to the policy. */
142
+ export interface RateLimitOverride {
143
+ override_id: string;
144
+ pattern: string;
145
+ limit?: number;
146
+ window_seconds?: number;
147
+ behavior?: 'block' | 'shape';
148
+ note?: string;
149
+ created_at: string;
150
+ updated_at?: string;
151
+ }
152
+ /** A payments provider webhook delivery in the event log (payments.md §7).
153
+ * `payload` and `raw_body` are only populated on the detail read. */
154
+ export interface PaymentsWebhookEvent {
155
+ event_id: string;
156
+ provider: string;
157
+ provider_evt_id: string;
158
+ event_type: string;
159
+ /** received | processed | error | sig_failed | parse_failed | reprocessed */
160
+ outcome: string | null;
161
+ signature_ok: boolean;
162
+ error: string | null;
163
+ received_at: string | null;
164
+ processed_at: string | null;
165
+ reprocess_count: number;
166
+ reprocessed_at: string | null;
167
+ /** detail-only: the verified provider payload (jsonb; '{}' for failed rows). */
168
+ payload?: unknown;
169
+ /** detail-only: the raw body of a sig_failed/parse_failed delivery (≤64KB). */
170
+ raw_body?: string | null;
171
+ provider_event_ts?: string | null;
172
+ }
173
+ export interface FileObject {
174
+ object_id: string;
175
+ filename: string;
176
+ content_type: string;
177
+ size_bytes: number;
178
+ status: 'pending' | 'available' | 'deleted';
179
+ created_at: string;
180
+ }
181
+ /** One recognized OCR text block (files.md §1.1). bbox is [x, y, w, h] in the
182
+ * provider's unit space; page is 1-based. confidence is 0..1 normalized per
183
+ * provider (Textract native 0..100 divided by 100; mock pins 0.95); absent
184
+ * when the provider supplied none. */
185
+ export interface OcrBlock {
186
+ text: string;
187
+ bbox: [number, number, number, number];
188
+ page: number;
189
+ confidence?: number;
190
+ }
191
+ /** A created/updated search collection (POST /v1/search/collections). */
192
+ export interface SearchCollection {
193
+ collection: string;
194
+ dimensions: number;
195
+ backend: string;
196
+ }
197
+ /** A document row in a collection listing (GET …/documents). */
198
+ export interface SearchDocument {
199
+ doc_id: string;
200
+ user_id: string | null;
201
+ source_file: string | null;
202
+ status: 'indexing' | 'indexed' | 'failed';
203
+ chunks: number;
204
+ metadata: Record<string, unknown>;
205
+ created_at: string;
206
+ }
207
+ /** One ingest input: chunk text OR bring-your-own precomputed vectors. */
208
+ export interface SearchIngestInput {
209
+ doc_id: string;
210
+ user_id?: string;
211
+ source_file?: string;
212
+ /** vxil chunks + embeds this. Omit when supplying `chunks`/`vector`. */
213
+ text?: string;
214
+ /** BYOV: explicit chunks, each optionally carrying its own precomputed vector. */
215
+ chunks?: Array<{
216
+ chunk_id?: string;
217
+ content: string;
218
+ vector?: number[];
219
+ }>;
220
+ /** BYOV shorthand: one precomputed vector applied to the whole document. */
221
+ vector?: number[];
222
+ metadata?: Record<string, unknown>;
223
+ /** Skip re-embedding when the (text, metadata) content hash matches the
224
+ * stored, indexed doc → `status: 'unchanged'` (no embed cost, no re-chunk). */
225
+ if_changed?: boolean;
226
+ }
227
+ /** The ingest acknowledgement (202 'indexed'; 200 'unchanged' on an
228
+ * `if_changed` hash match). */
229
+ export interface SearchIngestResult {
230
+ doc_id: string;
231
+ status: 'indexed' | 'unchanged' | string;
232
+ chunks?: number;
233
+ embedding_tokens?: number;
234
+ }
235
+ /** One hybrid/vector/keyword retrieval hit. */
236
+ export interface SearchHit {
237
+ chunk_id: string;
238
+ doc_id: string;
239
+ text: string;
240
+ score: number;
241
+ /** provider relevance score — present only when the hit went through the
242
+ * configured BYO reranker (`reranked: true` on the result set). */
243
+ rerank_score?: number;
244
+ metadata: Record<string, unknown>;
245
+ }
246
+ /** A query result set (POST …/query). */
247
+ export interface SearchQueryResult {
248
+ results: SearchHit[];
249
+ backend: string;
250
+ mode: 'hybrid' | 'vector' | 'keyword';
251
+ /** true when the configured BYO reranker re-ordered the fused candidates;
252
+ * false when not configured, opted out, or the provider failed (fail-open). */
253
+ reranked?: boolean;
254
+ }
255
+ /** Embedding-token + query + rerank counts since a timestamp (GET /v1/search/usage). */
256
+ export interface SearchUsage {
257
+ since: string;
258
+ embedding_tokens: number;
259
+ query_count: number;
260
+ rerank_count?: number;
261
+ }
262
+ /** One configured auto-sync source (config sync[]) LEFT-joined with its
263
+ * cron-cursor state (GET /v1/search/sync). */
264
+ export interface SearchSyncSource {
265
+ source_key: string;
266
+ source: 'cms';
267
+ cms_collection: string;
268
+ collection: string;
269
+ cron: string;
270
+ phase: 'backfill' | 'incremental' | string;
271
+ cursor_updated_at: string | null;
272
+ backfill_started_at: string | null;
273
+ last_run_at: string | null;
274
+ last_sweep_at: string | null;
275
+ last_error: string | null;
276
+ docs_synced: number;
277
+ }
278
+ /** Per-call provider token usage. */
279
+ export interface AiUsage {
280
+ input_tokens: number;
281
+ output_tokens: number;
282
+ }
283
+ /** One tool invocation the model asked for (tool-use passthrough — vxil relays,
284
+ * YOU execute). Echo `id` back as `tool_call_id` on the `role:'tool'` turn. */
285
+ export interface AiToolCall {
286
+ id?: string;
287
+ name: string;
288
+ arguments: unknown;
289
+ }
290
+ /** A prior conversation turn for the tool-calling round-trip: assistant turns
291
+ * carry the tool_calls they made; `role:'tool'` turns carry one call's result
292
+ * (`tool_call_id` correlates it; `name` is the tool's name). */
293
+ export interface AiChatMessage {
294
+ role: 'user' | 'assistant' | 'tool';
295
+ content?: string;
296
+ tool_calls?: AiToolCall[];
297
+ tool_call_id?: string;
298
+ name?: string;
299
+ }
300
+ /** A synchronous generation result (POST /v1/ai/generate without stream). */
301
+ export interface AiGeneration {
302
+ generation_id: string;
303
+ text: string;
304
+ usage: AiUsage;
305
+ finish: string;
306
+ tool_calls?: AiToolCall[];
307
+ cached: boolean;
308
+ }
309
+ /** A streamed generation handle: open the realtime channel for token frames. */
310
+ export interface AiStreamHandle {
311
+ generation_id: string;
312
+ channel: string;
313
+ token?: string;
314
+ ttl_seconds?: number;
315
+ connect_path?: string;
316
+ resume_path: string;
317
+ }
318
+ /** Frames delivered over the ai:<generation_id> realtime channel and the
319
+ * ?since= replay buffer (the WS envelope is { event, data } — `event` is the
320
+ * frame type; replayed frames carry it as `type`). `title` arrives early when
321
+ * the stream was requested with title/title_template; only the terminal usage
322
+ * frame carries done:true. */
323
+ export type AiStreamFrame = {
324
+ type?: 'token';
325
+ seq: number;
326
+ delta: string;
327
+ } | {
328
+ type?: 'title';
329
+ seq: number;
330
+ title: string;
331
+ } | {
332
+ type?: 'usage';
333
+ seq: number;
334
+ done: true;
335
+ finish: string;
336
+ usage: AiUsage;
337
+ tool_calls?: AiToolCall[];
338
+ };
339
+ /** The live-only `token_expiring` event (never seq'd, never replayed): fired
340
+ * ~30s before each 300s connect-token window ends on a long stream. Re-mint
341
+ * via ai.remintToken() and reconnect with ?since=<last seen seq>. */
342
+ export interface AiTokenExpiringEvent {
343
+ generation_id: string;
344
+ ttl_seconds: number;
345
+ remint_path: string;
346
+ }
347
+ /** A job-routed async generation handle (POST /v1/ai/generate with mode:'job').
348
+ * The jobs run drives the provider call; read the answer via the replay buffer
349
+ * (`resume_path`) or track the run through the jobs surface. */
350
+ export interface AiJobHandle {
351
+ generation_id: string;
352
+ run_id: string;
353
+ status: string;
354
+ resume_path: string;
355
+ }
356
+ /** A re-minted mid-stream connect token (GET /v1/ai/generations/{id}/token). */
357
+ export interface AiStreamToken {
358
+ generation_id: string;
359
+ channel: string;
360
+ token: string;
361
+ ttl_seconds: number;
362
+ connect_path: string;
363
+ resume_path: string;
364
+ }
365
+ /** An embedding result (POST /v1/ai/embed). */
366
+ export interface AiEmbedResult {
367
+ embeddings: number[][];
368
+ usage: AiUsage;
369
+ generation_id: string;
370
+ }
371
+ /** Per-user token rollups (GET /v1/ai/usage). */
372
+ export interface AiUsageReport {
373
+ input_tokens: number;
374
+ output_tokens: number;
375
+ total_tokens: number;
376
+ generations: number;
377
+ cached: number;
378
+ by_user: Record<string, {
379
+ input_tokens: number;
380
+ output_tokens: number;
381
+ }>;
382
+ }
383
+ /** A citation: the retrieved chunk that grounded the answer. */
384
+ export interface RagCitation {
385
+ chunk_id: string;
386
+ doc_id: string;
387
+ source?: {
388
+ file?: string;
389
+ page?: number;
390
+ };
391
+ score: number;
392
+ }
393
+ /** A synchronous grounded answer (POST /v1/rag/answer without stream). */
394
+ export interface RagAnswer {
395
+ answer: string;
396
+ citations?: RagCitation[];
397
+ usage: {
398
+ retrieval_ms: number;
399
+ retrieved: number;
400
+ used: number;
401
+ input_tokens: number | null;
402
+ output_tokens: number | null;
403
+ };
404
+ finish?: string;
405
+ }
406
+ /** A streamed grounded answer: citations up front, tokens over the channel. */
407
+ export interface RagStreamHandle {
408
+ generation_id: string;
409
+ channel: string;
410
+ token: string;
411
+ ttl_seconds: number;
412
+ /** RAG-native resume (`/v1/rag/answers/{id}/stream?since=0`) — a rag-scoped
413
+ * key suffices; see `rag.resume()`. */
414
+ resume_path: string;
415
+ /** The underlying ai resume path (needs an ai-scoped credential). */
416
+ ai_resume_path?: string;
417
+ citations?: RagCitation[];
418
+ usage: {
419
+ retrieval_ms: number;
420
+ retrieved: number;
421
+ used: number;
422
+ };
423
+ }
424
+ /** A scored hit from the retrieval-only surface (POST /v1/rag/search). */
425
+ export interface RagSearchHit {
426
+ chunk_id: string;
427
+ doc_id: string;
428
+ text: string;
429
+ /** the raw retrieval score (RRF, or rerank relevance when reranked). */
430
+ score: number;
431
+ /** the effective post-boost score results are RANKED by — present only when
432
+ * metadata boosts applied (rag.md §2f). */
433
+ boosted_score?: number;
434
+ metadata?: Record<string, unknown>;
435
+ }
436
+ /** Retrieval-only result: the exact chunks `rag.answer` would ground on, with
437
+ * rerank + metadata boosts applied — no generation, no token spend. */
438
+ export interface RagSearchResult {
439
+ /** post-boost order. */
440
+ results: RagSearchHit[];
441
+ /** true when vector-search's BYO rerank pass re-scored the hits. */
442
+ reranked: boolean;
443
+ usage: {
444
+ retrieval_ms: number;
445
+ retrieved: number;
446
+ };
447
+ }
448
+ /** A replayed frame page from the rag-native resume endpoint (proxies the ai
449
+ * §2a replay buffer: every recorded frame with seq > since, plus done). */
450
+ export interface RagResumePage {
451
+ generation_id: string;
452
+ since: number;
453
+ frames: Array<Record<string, unknown>>;
454
+ done: boolean;
455
+ max_seq: number;
456
+ }
457
+ /** Aggregated retrieval + generation usage (GET /v1/rag/usage). */
458
+ export interface RagUsage {
459
+ retrieval: SearchUsage | null;
460
+ generation: AiUsageReport | null;
461
+ }
462
+ /** A conversation row (a chat session pinned to one config agent). */
463
+ export interface CopilotConversation {
464
+ conversation_id: string;
465
+ agent_id: string;
466
+ end_user_id: string | null;
467
+ title: string | null;
468
+ status: 'open' | 'closed' | 'escalated';
469
+ created_at: string;
470
+ updated_at: string;
471
+ }
472
+ /** A citation attached to a grounded answer (the rag invariant: exactly the
473
+ * budgeted kept chunks — a citation can never lie). */
474
+ export interface CopilotCitation {
475
+ chunk_id: string;
476
+ document_id?: string;
477
+ score?: number;
478
+ [k: string]: unknown;
479
+ }
480
+ /** A proposed WRITE action the agent wants to take — inert until confirmed. */
481
+ export interface CopilotProposal {
482
+ message_id: string;
483
+ tool: string;
484
+ args: Record<string, unknown>;
485
+ feature: string | null;
486
+ proposed_at: string;
487
+ require_confirm: boolean;
488
+ confirm_path: string;
489
+ }
490
+ /** One transcript message (the getConversation payload). */
491
+ export interface CopilotMessage {
492
+ message_id: string;
493
+ role: 'user' | 'assistant' | 'tool' | 'system';
494
+ content: string;
495
+ citations: CopilotCitation[];
496
+ proposal: CopilotProposal | null;
497
+ action_status: 'none' | 'proposed' | 'confirmed' | 'rejected';
498
+ action_result: unknown;
499
+ confirmed_at: string | null;
500
+ input_tokens: number;
501
+ output_tokens: number;
502
+ created_at: string;
503
+ }
504
+ /** The synchronous turn result: the answer text + citations + an optional
505
+ * proposal, plus the realtime streaming handle (channel/token/resume_path) for
506
+ * callers that prefer the live token stream over the synchronous `answer`. */
507
+ export interface CopilotTurn {
508
+ conversation_id: string;
509
+ message_id: string;
510
+ answer: string;
511
+ action_status: 'none' | 'proposed';
512
+ proposal?: CopilotProposal;
513
+ citations: CopilotCitation[];
514
+ channel: string;
515
+ token?: string;
516
+ ttl_seconds?: number;
517
+ connect_path?: string;
518
+ resume_path: string;
519
+ usage: {
520
+ input_tokens: number;
521
+ output_tokens: number;
522
+ steps: number;
523
+ };
524
+ }
525
+ /** A resumed page of streamed frames (citations → token → terminal usage). */
526
+ export interface CopilotStreamPage {
527
+ conversation_id: string;
528
+ message_id: string;
529
+ since: number;
530
+ frames: Array<Record<string, unknown>>;
531
+ done: boolean;
532
+ max_seq: number;
533
+ }
534
+ /** The confirm result: a fresh follow-up assistant message + the downstream
535
+ * envelope verbatim. `already:true` marks an idempotent replay of a prior
536
+ * confirm (same key, no double-execute). */
537
+ export interface CopilotConfirmResult {
538
+ message_id: string;
539
+ proposal_message_id?: string;
540
+ action_status: 'confirmed';
541
+ already?: boolean;
542
+ confirmed_at: string | null;
543
+ result: unknown;
544
+ }
545
+ /** Per-user token rollup (assistant rows summed by end_user_id). */
546
+ export interface CopilotUsage {
547
+ input_tokens: number;
548
+ output_tokens: number;
549
+ total_tokens: number;
550
+ turns: number;
551
+ by_user: Record<string, {
552
+ input_tokens: number;
553
+ output_tokens: number;
554
+ }>;
555
+ }
556
+ /** A single activity row as returned in a flat feed page. */
557
+ export interface FeedActivity {
558
+ id: string;
559
+ actor: string;
560
+ verb: string;
561
+ object: string;
562
+ target: string | null;
563
+ time: string;
564
+ extra: Record<string, unknown>;
565
+ }
566
+ /** A flat feed read page (push∪pull union, deduped + merged on sort_key). */
567
+ export interface FlatFeedPage {
568
+ feed: string;
569
+ type: 'flat';
570
+ activities: FeedActivity[];
571
+ next_cursor: string | null;
572
+ }
573
+ /** An aggregation / notification group row. `seen`/`read` are present only on
574
+ * notification feeds. */
575
+ export interface FeedGroup {
576
+ group: string;
577
+ activity_count: number;
578
+ actor_count: number;
579
+ last_actor: string;
580
+ updated_at: string;
581
+ seen?: boolean;
582
+ read?: boolean;
583
+ }
584
+ /** An aggregated- or notification-feed read page (groups by updated_at DESC).
585
+ * Notification feeds additionally carry the badge envelope (unseen/unread/total). */
586
+ export interface GroupedFeedPage {
587
+ feed: string;
588
+ type: 'aggregated' | 'notification';
589
+ groups: FeedGroup[];
590
+ next_cursor: string | null;
591
+ /** Notification feeds only: the live badge alongside the page. */
592
+ unseen?: number;
593
+ unread?: number;
594
+ total?: number;
595
+ }
596
+ export type FeedPage = FlatFeedPage | GroupedFeedPage;
597
+ /** The notification badge ({ unseen, unread, total }). */
598
+ export interface FeedBadge {
599
+ unseen: number;
600
+ unread: number;
601
+ total: number;
602
+ }
603
+ /** Per-user notification preference policy. */
604
+ export interface FeedPreferences {
605
+ policy: Record<string, unknown>;
606
+ mute_until: string | null;
607
+ updated_at?: string;
608
+ }
609
+ /** A realtime connect token scoped to one of the requested feed channels. */
610
+ export interface FeedRealtimeToken {
611
+ feed: string;
612
+ token: string;
613
+ connect_path: string;
614
+ expires_at: string;
615
+ }
616
+ /** A single activity to add to a feed. `to` CCs the activity into other feeds. */
617
+ export interface FeedActivityInput {
618
+ actor: string;
619
+ verb: string;
620
+ object: string;
621
+ target?: string;
622
+ /** ISO-8601; defaults to now(). Forms the idempotency key with foreign_id. */
623
+ time?: string;
624
+ /** Idempotency key (upsert is on (foreign_id, time)). */
625
+ foreign_id?: string;
626
+ /** CC the activity into these other feed refs ("{group}:{feed_id}"). */
627
+ to?: string[];
628
+ extra?: Record<string, unknown>;
629
+ ranking_vars?: Record<string, unknown>;
630
+ }
631
+ /** A DM conversation envelope (open/reuse result). `reused` is true when a
632
+ * direct pair re-opens its existing thread. */
633
+ export interface DmConversation {
634
+ conversation_id: string;
635
+ topic: string;
636
+ kind: string;
637
+ participants: string[];
638
+ reused: boolean;
639
+ }
640
+ /** One row of the "my inbox" listing (a user's active conversations). */
641
+ export interface DmInboxConversation {
642
+ conversation_id: string;
643
+ topic: string;
644
+ kind: string;
645
+ subject: string | null;
646
+ last_message_at: string | null;
647
+ muted: boolean;
648
+ unread: boolean;
649
+ }
650
+ /** A DM message — a `comments` row on the `dm:<conversation_id>` topic. */
651
+ export interface DmMessage {
652
+ comment_id: string;
653
+ topic: string;
654
+ parent_id: string | null;
655
+ author_id: string;
656
+ body: string | null;
657
+ reactions: Record<string, string[]>;
658
+ created_at: string;
659
+ edited_at: string | null;
660
+ deleted: boolean;
661
+ }
662
+ /** The DM feature config (its `dm`-gated leaf flags) + its persisted version. */
663
+ export interface DmConfigState {
664
+ config: {
665
+ enabled: boolean;
666
+ maxParticipants: number;
667
+ realtimeDelivery: boolean;
668
+ blockingEnabled: boolean;
669
+ } & Record<string, unknown>;
670
+ version: number;
671
+ configured: boolean;
672
+ }
673
+ /** The structural shape a `vxil gen`-generated `VxilSchema` satisfies. The base
674
+ * `Vxil` class is generic over it (`new Vxil<VxilSchema>(...)`), exactly the
675
+ * `createClient<Database>()` move — types are layered on; the runtime is
676
+ * unchanged. The default keeps `new Vxil({ apiKey })` (no generic) permissive. */
677
+ export interface VxilSchemaShape {
678
+ features: string;
679
+ cms: Record<string, {
680
+ Row: unknown;
681
+ Insert: unknown;
682
+ Patch: unknown;
683
+ Sortable: string;
684
+ Filterable: Record<string, unknown>;
685
+ }>;
686
+ functions: Record<string, {
687
+ Input: unknown;
688
+ Output: unknown;
689
+ }>;
690
+ }
691
+ /** feature client property → the feature key (in S['features']) that enables it.
692
+ * The renamed namespaces map to their real feature ids. Used by EnabledVxil to
693
+ * disable namespaces a tenant hasn't enabled. */
694
+ interface FeatureMap {
695
+ notifications: 'notifications';
696
+ jobs: 'jobs';
697
+ auth: 'auth';
698
+ rateLimits: 'rate-limits';
699
+ files: 'files';
700
+ webhooks: 'webhooks';
701
+ comments: 'comments';
702
+ cms: 'cms';
703
+ realtime: 'realtime';
704
+ orgs: 'orgs';
705
+ feeds: 'activity-feed';
706
+ activityFeed: 'activity-feed';
707
+ search: 'vector-search';
708
+ ai: 'ai';
709
+ rag: 'rag';
710
+ payments: 'payments';
711
+ copilot: 'copilot';
712
+ }
713
+ /** A disabled namespace resolves to this string-literal type — its TEXT is the
714
+ * fix, so `vx.rag.answer()` errors with "Property 'answer' does not exist on
715
+ * type 'vxil: feature 'rag' is not enabled — …'". */
716
+ type DisabledFeature<K extends string> = `vxil: feature '${K}' is not enabled — add it to vxil.config.ts and re-run \`vxil gen\``;
717
+ type DisabledFeatures<S extends VxilSchemaShape> = {
718
+ [P in keyof FeatureMap]: FeatureMap[P] extends S['features'] ? never : P;
719
+ }[keyof FeatureMap];
720
+ /** The feature-narrowed client: feature namespaces the tenant hasn't enabled become
721
+ * a compile error (the design's §4.4 headline). `Vxil.connect<VxilSchema>()`
722
+ * returns it; plain `new Vxil<VxilSchema>()` stays un-narrowed for back-compat. */
723
+ export type EnabledVxil<S extends VxilSchemaShape> = Omit<Vxil<S>, DisabledFeatures<S>> & {
724
+ [P in DisabledFeatures<S>]: DisabledFeature<FeatureMap[P] & string>;
725
+ };
726
+ /** A write guard (cms.md §10): "after this write, at most `max` live items
727
+ * match `filter`". Requires `lock` — an unlocked guard is racy by construction. */
728
+ export interface CmsGuard {
729
+ filter: Record<string, unknown>;
730
+ max: number;
731
+ }
732
+ /** One guard of a `guards[]` multi-invariant write (cms.md §10). Same
733
+ * { filter, max } as CmsGuard plus an optional custom 409 `message` surfaced on
734
+ * the FIRST violation. */
735
+ export interface CmsGuardTerm extends CmsGuard {
736
+ /** Custom guard_failed message (≤200 chars) for THIS invariant. */
737
+ message?: string;
738
+ }
739
+ /** Concurrency options shared by the cms write methods (cms.md §9–10). */
740
+ export interface CmsWriteOpts {
741
+ /** Optimistic CAS: sent as `If-Match: <version>`; mismatch → 409 version_conflict. */
742
+ ifVersion?: number;
743
+ /** Bounded field precondition checked against the locked row (≤4 terms;
744
+ * `{field: null}` = "absent or null" — the slot-claim shape). 409 precondition_failed. */
745
+ if?: Record<string, unknown>;
746
+ /** Per-(tenant,collection,key) advisory lock — serializes same-key writers. */
747
+ lock?: string;
748
+ /** Declarative capacity/overlap invariant; requires `lock`. 409 guard_failed.
749
+ * Mutually exclusive with `guards`. */
750
+ guard?: CmsGuard;
751
+ /** Multiple capacity/overlap invariants evaluated co-atomically under the ONE
752
+ * `lock` (≤4; requires `lock`; cms.md §10). Mutually exclusive with `guard`.
753
+ * The FIRST failing guard's optional `message` rides the 409 guard_failed. */
754
+ guards?: CmsGuardTerm[];
755
+ }
756
+ /** The keys of Row whose values are numbers — the only legal `$inc` targets. */
757
+ type NumericKeys<R> = {
758
+ [K in keyof R]-?: NonNullable<R[K]> extends number ? K : never;
759
+ }[keyof R];
760
+ /** cms-rel B2/B3 shared window: a t*-slotted field or created_at/updated_at/
761
+ * published_at, ≤366 days wide. */
762
+ export interface CmsWindow {
763
+ field: string;
764
+ sinceDays?: number;
765
+ since?: string;
766
+ until?: string;
767
+ }
768
+ /** cms-rel B2: the aggregate request body (cms.md §12.1). */
769
+ export interface CmsAggregateBody {
770
+ /** 1–4 exprs; fn ∈ count|sum|min|max|avg (sum/avg need an n*-slot field). */
771
+ aggregates: Array<{
772
+ fn: 'count' | 'sum' | 'min' | 'max' | 'avg';
773
+ field?: string;
774
+ as?: string;
775
+ }>;
776
+ /** ≤2 slot-bound own fields (a relation field groups by related item id). */
777
+ groupBy?: string[];
778
+ /** full query DSL + ONE-hop dotted join terms. */
779
+ filter?: Record<string, unknown>;
780
+ window?: CmsWindow;
781
+ /** an aggregate alias or a groupBy key; '-x' = desc. Default: first aggregate desc. */
782
+ sort?: string;
783
+ /** group rows returned; clamped to 500. */
784
+ limit?: number;
785
+ }
786
+ /** cms-rel B3: the rank request body (cms.md §12.2). */
787
+ export interface CmsRankBody {
788
+ /** REQUIRED: the ranked entity — a slot-bound own field (often a relation). */
789
+ groupBy: string;
790
+ /** default {fn:'count'}. */
791
+ metric?: {
792
+ fn: 'count' | 'sum' | 'min' | 'max' | 'avg';
793
+ field?: string;
794
+ as?: string;
795
+ };
796
+ rank: 'row_number' | 'rank' | 'percent_rank';
797
+ partitionBy?: string;
798
+ direction?: 'asc' | 'desc';
799
+ filter?: Record<string, unknown>;
800
+ window?: CmsWindow;
801
+ limit?: number;
802
+ }
803
+ /** cms-rel B4: one transaction step (cms.md §13). `$where` = the bounded CAS
804
+ * precondition (the PATCH `if` grammar: ≤4 terms, scalar / null /
805
+ * {$eq $ne $gt $gte $lt $lte $in}). */
806
+ export type CmsTxStep = {
807
+ op: 'insert';
808
+ collection: string;
809
+ data: Record<string, unknown>;
810
+ status?: 'draft' | 'published';
811
+ } | {
812
+ op: 'update';
813
+ collection: string;
814
+ item_id: string;
815
+ data: Record<string, unknown>;
816
+ $where?: Record<string, unknown>;
817
+ } | {
818
+ op: 'delete';
819
+ collection: string;
820
+ item_id: string;
821
+ $where?: Record<string, unknown>;
822
+ };
823
+ /** A per-collection typed handle returned by `vx.from(collection)`. */
824
+ export interface CollectionClient<C extends VxilSchemaShape['cms'][string]> {
825
+ create(data: C['Insert'], opts?: {
826
+ status?: 'draft' | 'published';
827
+ lock?: string;
828
+ guard?: CmsGuard;
829
+ guards?: CmsGuardTerm[];
830
+ }): Promise<{
831
+ item_id: string;
832
+ status: string;
833
+ version: number;
834
+ data: C['Row'];
835
+ }>;
836
+ get(itemId: string): Promise<C['Row']>;
837
+ query(q?: {
838
+ /** Keys AND values come from the generated `Filterable`: each field's value
839
+ * is its operator union (`VxilFilterRange*` on slotted fields, equality/
840
+ * `$in`/`$contains` otherwise), so a range op on a non-indexed field is a
841
+ * compile error. Old generated schemas (scalar-valued `Filterable`) still
842
+ * compile — they just stay equality-only. Escape hatch for anything the
843
+ * types deliberately exclude: the untyped `vx.cms.items.query`. */
844
+ filter?: Partial<C['Filterable']>;
845
+ sort?: C['Sortable'] | `-${C['Sortable'] & string}`;
846
+ limit?: number;
847
+ cursor?: string;
848
+ }): Promise<{
849
+ items: C['Row'][];
850
+ next_cursor: string | null;
851
+ }>;
852
+ /** `SELECT count(*)` under the same bounded filter grammar (cms.md §9.4).
853
+ * Refused (422) on collections with beforeRead visibility hooks. */
854
+ count(filter?: Partial<C['Filterable']>): Promise<number>;
855
+ patch(itemId: string, data: C['Patch'], opts?: CmsWriteOpts): Promise<C['Row']>;
856
+ /** Atomic in-database increment — ONE conditional UPDATE; never
857
+ * read-modify-write (cms.md §9.3). Delta keys are the numeric Row fields. */
858
+ inc(itemId: string, incs: Partial<Record<NumericKeys<C['Row']> & string, number>>, opts?: Pick<CmsWriteOpts, 'ifVersion' | 'if'>): Promise<C['Row']>;
859
+ delete(itemId: string, opts?: Pick<CmsWriteOpts, 'ifVersion'>): Promise<{
860
+ item_id: string;
861
+ deleted: boolean;
862
+ cascaded: number;
863
+ set_null: number;
864
+ }>;
865
+ publish(itemId: string): Promise<C['Row']>;
866
+ /** cms-rel B2: bounded group-by aggregate (spec §7 `groupBy()` surface).
867
+ * groupBy/field names are typed over the Row's keys; slot-existence stays a
868
+ * runtime check — exactly like `query`. Joins stay expressed as dotted
869
+ * filter keys (no `.join()` builder — one grammar, not two). */
870
+ aggregate(q: Omit<CmsAggregateBody, 'groupBy' | 'aggregates'> & {
871
+ aggregates: Array<{
872
+ fn: 'count' | 'sum' | 'min' | 'max' | 'avg';
873
+ field?: keyof C['Row'] & string;
874
+ as?: string;
875
+ }>;
876
+ groupBy?: Array<keyof C['Row'] & string>;
877
+ }): Promise<{
878
+ groups: Array<Record<string, unknown> & {
879
+ key?: Record<string, unknown>;
880
+ }>;
881
+ scanned: number;
882
+ }>;
883
+ /** cms-rel B3: window ranking over the aggregate. */
884
+ rank(q: Omit<CmsRankBody, 'groupBy' | 'partitionBy' | 'metric'> & {
885
+ groupBy: keyof C['Row'] & string;
886
+ partitionBy?: keyof C['Row'] & string;
887
+ metric?: {
888
+ fn: 'count' | 'sum' | 'min' | 'max' | 'avg';
889
+ field?: keyof C['Row'] & string;
890
+ as?: string;
891
+ };
892
+ }): Promise<{
893
+ entries: Array<{
894
+ key: Record<string, unknown>;
895
+ partition?: Record<string, unknown>;
896
+ metric: unknown;
897
+ rank: number;
898
+ }>;
899
+ scanned: number;
900
+ }>;
901
+ }
902
+ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
903
+ private readonly base;
904
+ private readonly key;
905
+ private readonly fetchImpl;
906
+ private readonly apiVersion;
907
+ private readonly apiVersions;
908
+ /** The end-user session token threaded as `X-Vxil-End-User` when set (end-user
909
+ * mode). Undefined ⇒ no header ⇒ server-caller mode (unchanged). */
910
+ private readonly endUserToken;
911
+ constructor(opts: VxilOptions);
912
+ /** The header bag every request layers on top of `authorization`: the
913
+ * `X-Vxil-End-User` session token in end-user mode, nothing in server mode.
914
+ * One place so `call()` and the `fn` proxy stay in lockstep. */
915
+ private authHeaders;
916
+ /** Return a client that sends `X-Vxil-End-User: <token>` on every request —
917
+ * a per-call/scoped override of end-user mode over an otherwise server-mode
918
+ * client, mirroring how the edge threads the verified principal. The base
919
+ * URL, api key, fetch impl and version pins are inherited unchanged; only the
920
+ * end-user token is (re)set. Pass a falsy token to get back a server-mode
921
+ * client (drops the header). See docs/end-user-principals-design.md §4.1. */
922
+ asEndUser(endUserToken: string | undefined): Vxil<S>;
923
+ /** Build the versioned request path — the ONE place a wire path is finalized.
924
+ * Every request (call(), audit.export, the fn proxy) routes through here, so
925
+ * the per-feature/global major pin applies uniformly. Default → unchanged
926
+ * `/v1/…`. */
927
+ private path;
928
+ /** Construct a FEATURE-NARROWED client: feature namespaces the tenant hasn't
929
+ * enabled (per the generated `VxilSchema['features']`) become compile errors —
930
+ * `Vxil.connect<VxilSchema>({ apiKey }).rag` won't type-check if `rag` is off.
931
+ * Runtime is identical to `new Vxil`; this only adds the compile-time gate. */
932
+ static connect<S extends VxilSchemaShape = VxilSchemaShape>(opts: VxilOptions): EnabledVxil<S>;
933
+ /** Typed per-collection CMS handle (design §4.8). A thin wrapper over the
934
+ * generic `cms.items.*` methods — the wire calls are identical; the generated
935
+ * `VxilSchema` supplies the field types. `vx.cms.items.*` stays as the
936
+ * always-available un-generic fallback. */
937
+ from<C extends keyof S['cms'] & string>(collection: C): CollectionClient<S['cms'][C]>;
938
+ /** Typed function invoke (design §4.5). `vx.fn.<name>(payload)` POSTs to
939
+ * /v1/fn/<name>; the generated `VxilSchema` types Input/Output (Level-0
940
+ * opaque until a function declares a signature). A Proxy gives the
941
+ * `vx.fn.<name>` accessor shape without enumerating names at runtime. */
942
+ readonly fn: { [K in keyof S["functions"] & string]: (input: S["functions"][K]["Input"]) => Promise<S["functions"][K]["Output"]>; };
943
+ private call;
944
+ readonly users: {
945
+ upsert: (user: {
946
+ id: string;
947
+ email?: string;
948
+ display_name?: string;
949
+ avatar_url?: string;
950
+ attributes?: Record<string, unknown>;
951
+ }) => Promise<VxilUser>;
952
+ bulk: (users: Array<{
953
+ id: string;
954
+ email?: string;
955
+ }>) => Promise<number>;
956
+ get: (id: string, opts?: {
957
+ includeDeleted?: boolean;
958
+ }) => Promise<VxilUser>;
959
+ patch: (id: string, patch: {
960
+ email?: string | null;
961
+ display_name?: string | null;
962
+ avatar_url?: string | null;
963
+ attributes?: Record<string, unknown>;
964
+ }) => Promise<VxilUser>;
965
+ /**
966
+ * Delete a user. By default this is a SOFT delete (sets deleted_at; the PII
967
+ * persists for the 30-day retention window). Pass { erase: true } for GDPR
968
+ * Article 17 erasure: the row is soft-deleted AND its PII
969
+ * (email/display_name/avatar_url/attributes) is scrubbed, while the id is
970
+ * kept so cross-feature references stay intact. NOTE: a full account-delete
971
+ * flow also calls the auth half — POST /v1/auth/users/:id/erase — to erase
972
+ * the credential/session identity (see features/tenant-users.md §3).
973
+ */
974
+ delete: (id: string, opts?: {
975
+ erase?: boolean;
976
+ }) => Promise<void>;
977
+ list: (q?: {
978
+ email?: string;
979
+ /** substring match on email + display name (server-side, trgm-indexed), 1-100 chars */
980
+ q?: string;
981
+ cursor?: string;
982
+ limit?: number;
983
+ }) => Promise<{
984
+ users: VxilUser[];
985
+ next_cursor: string | null;
986
+ }>;
987
+ };
988
+ readonly notifications: {
989
+ send: (input: {
990
+ user_id: string;
991
+ template: "magic-link" | "welcome" | "transactional";
992
+ data: Record<string, unknown>;
993
+ locale?: string;
994
+ /** 'inbox'/'both' require config inboxEnabled */
995
+ channel?: "email" | "inbox" | "both";
996
+ }, opts?: {
997
+ idempotencyKey?: string;
998
+ }) => Promise<{
999
+ delivery_id?: string;
1000
+ inbox_message_id?: string;
1001
+ status: string;
1002
+ }>;
1003
+ inbox: {
1004
+ list: (q: {
1005
+ user_id: string;
1006
+ unread_only?: boolean;
1007
+ cursor?: string;
1008
+ limit?: number;
1009
+ }) => Promise<{
1010
+ messages: Array<{
1011
+ msg_id: string;
1012
+ template_id: string;
1013
+ title: string;
1014
+ body: string;
1015
+ data: Record<string, unknown>;
1016
+ read_at: string | null;
1017
+ created_at: string;
1018
+ }>;
1019
+ unread_count: number;
1020
+ next_cursor: string | null;
1021
+ }>;
1022
+ markRead: (msgId: string, userId: string) => Promise<void>;
1023
+ markAllRead: (userId: string) => Promise<number>;
1024
+ };
1025
+ deliveries: (q?: {
1026
+ user_id?: string;
1027
+ status?: string;
1028
+ limit?: number;
1029
+ }) => Promise<Delivery[]>;
1030
+ suppressions: {
1031
+ list: () => Promise<Array<{
1032
+ email_lower: string;
1033
+ reason: string;
1034
+ created_at: string;
1035
+ }>>;
1036
+ add: (email: string) => Promise<void>;
1037
+ remove: (email: string) => Promise<void>;
1038
+ };
1039
+ /** Deliveries the retry budget could not save (the dashboard "Resend" surface). */
1040
+ deadLetters: {
1041
+ list: () => Promise<DeadLetter[]>;
1042
+ /** Reset the row and re-enqueue the original message. */
1043
+ replay: (deliveryId: string) => Promise<void>;
1044
+ };
1045
+ /** A single delivery by id (the list is `deliveries()`). */
1046
+ delivery: (deliveryId: string) => Promise<Delivery>;
1047
+ /** Email broadcast campaigns (notifications.md §11b): audience-ref fan-out
1048
+ * with quiet-hours + frequency-cap policy. `schedule_cron` sets a recurring
1049
+ * send (status `scheduled`); omit it for a `draft`. */
1050
+ campaigns: {
1051
+ create: (input: {
1052
+ name: string;
1053
+ template: string;
1054
+ audience_ref: string;
1055
+ channel?: "email" | "push" | "all";
1056
+ data?: Record<string, unknown>;
1057
+ schedule_cron?: string;
1058
+ quiet_hours?: {
1059
+ tz: string;
1060
+ start: string;
1061
+ end: string;
1062
+ };
1063
+ freq_cap?: {
1064
+ perUserPerDay?: number;
1065
+ perUserPerCampaign?: number;
1066
+ };
1067
+ }) => Promise<{
1068
+ campaign_id: string;
1069
+ status: string;
1070
+ }>;
1071
+ list: () => Promise<Array<{
1072
+ campaign_id: string;
1073
+ name: string;
1074
+ channel: string;
1075
+ template_id: string;
1076
+ audience_ref: string;
1077
+ schedule_cron: string | null;
1078
+ status: string;
1079
+ created_at: string;
1080
+ }>>;
1081
+ };
1082
+ };
1083
+ readonly config: {
1084
+ get: (feature: string) => Promise<FeatureConfig>;
1085
+ /** Optimistic-concurrency write: pass the version you read (etag). */
1086
+ set: (feature: string, manifest: Record<string, unknown>, opts?: {
1087
+ ifMatch?: number;
1088
+ surface?: "cli" | "mcp" | "dashboard";
1089
+ }) => Promise<FeatureConfig>;
1090
+ };
1091
+ readonly features: {
1092
+ /** The tenant's enabled-feature summary (GET /v1/features). Privilege-
1093
+ * independent: any valid key may read it — it leaks no secrets, only which
1094
+ * features the tenant has turned on. Mirrors what the MCP tool-list
1095
+ * aggregation sees. (audit #113) */
1096
+ list: () => Promise<string[]>;
1097
+ /** The FULL enabled-feature summary: `features` + `api_versions` (released
1098
+ * API majors per feature) + the additive `key` block — the CALLING key's
1099
+ * identity and per-tool MCP permissions (allowed_tools/denied_tools
1100
+ * patterns, api_keys 0057; mcp.md §6.7). `key` is absent for non-key
1101
+ * callers and for keys without explicit tool perms. `list()` stays the
1102
+ * stable flat-array shorthand. */
1103
+ summary: () => Promise<{
1104
+ features: string[];
1105
+ api_versions: Record<string, string[]>;
1106
+ key?: {
1107
+ key_id: string;
1108
+ agent?: string;
1109
+ allowed_tools?: string[];
1110
+ denied_tools?: string[];
1111
+ };
1112
+ }>;
1113
+ };
1114
+ readonly audit: {
1115
+ list: (q?: {
1116
+ since?: string;
1117
+ cursor?: string;
1118
+ limit?: number;
1119
+ }) => Promise<Array<{
1120
+ id: string;
1121
+ event: string;
1122
+ actor: string;
1123
+ surface: string;
1124
+ payload: Record<string, unknown>;
1125
+ created_at: string;
1126
+ }>>;
1127
+ /**
1128
+ * NDJSON export (≤10k rows/call, id-ascending). Returns parsed events +
1129
+ * next_after_id for resumption (null = done).
1130
+ */
1131
+ export: (q?: {
1132
+ since?: string;
1133
+ until?: string;
1134
+ after_id?: string;
1135
+ limit?: number;
1136
+ }) => Promise<{
1137
+ events: Array<{
1138
+ id: string;
1139
+ event: string;
1140
+ created_at: string;
1141
+ } & Record<string, unknown>>;
1142
+ next_after_id: string | null;
1143
+ /** Present only if a line failed to parse — the batch is NOT aborted. */
1144
+ parse_errors?: Array<{
1145
+ line: number;
1146
+ raw: string;
1147
+ }>;
1148
+ }>;
1149
+ };
1150
+ readonly jobs: {
1151
+ /** Enqueue a one-off job; Vxil POSTs a signed callback to target_url with
1152
+ * retries. deliver_after (ISO) / delay_seconds (≤ 30 d, at most one of the
1153
+ * two) defer the first delivery; > 12 h returns state 'delayed'. */
1154
+ enqueue: (input: {
1155
+ job_name: string;
1156
+ target_url: string;
1157
+ payload?: Record<string, unknown>;
1158
+ idempotency_key?: string;
1159
+ max_attempts?: number;
1160
+ deliver_after?: string;
1161
+ delay_seconds?: number;
1162
+ }) => Promise<{
1163
+ run_id: string;
1164
+ state: string;
1165
+ deduplicated?: boolean;
1166
+ deliver_after?: string;
1167
+ }>;
1168
+ /** Atomic multi-enqueue (≤100 items; any invalid item rejects the whole
1169
+ * batch). Each item = the enqueue input, incl. per-item idempotency_key
1170
+ * and deliver_after/delay_seconds. Results align with the input order. */
1171
+ enqueueBatch: (items: Array<{
1172
+ job_name: string;
1173
+ target_url: string;
1174
+ payload?: Record<string, unknown>;
1175
+ idempotency_key?: string;
1176
+ max_attempts?: number;
1177
+ deliver_after?: string;
1178
+ delay_seconds?: number;
1179
+ }>) => Promise<{
1180
+ runs: Array<{
1181
+ run_id: string;
1182
+ state: string;
1183
+ deduplicated?: boolean;
1184
+ }>;
1185
+ count: number;
1186
+ }>;
1187
+ /** Enqueue a long-running EXTERNAL generation run (jobs.md §11): Vxil calls
1188
+ * the provider (BYO key), tracks completion via poll/webhook, mirrors a typed
1189
+ * generation_status onto a tenant record, enforces a built-in timeout, and
1190
+ * (on failure) fires the payments credit-reversal.
1191
+ *
1192
+ * `reserve_credits` (§11.8) takes a PROVISIONAL held credit debit at enqueue
1193
+ * (linked to the run), committed on `completed` and reversed on
1194
+ * failed/timeout/DLQ. `amount` is positive-only and CLAMPED to the platform
1195
+ * `config.generation.maxReserveCredits` cap; in end-user mode `user_id` is
1196
+ * FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
1197
+ * the enqueue (insufficient balance); a runaway over the per-tenant
1198
+ * outstanding-holds ceiling → 429. */
1199
+ generation: (input: {
1200
+ job_name: string;
1201
+ provider: {
1202
+ url: string;
1203
+ method?: string;
1204
+ headers?: Record<string, string>;
1205
+ body?: Record<string, unknown>;
1206
+ };
1207
+ completion: {
1208
+ mode: "poll" | "webhook";
1209
+ status_path?: string;
1210
+ poll?: {
1211
+ url: string;
1212
+ method?: string;
1213
+ headers?: Record<string, string>;
1214
+ interval_ms?: number;
1215
+ };
1216
+ };
1217
+ status_mirror?: {
1218
+ feature: string;
1219
+ collection: string;
1220
+ record_id: string;
1221
+ column?: string;
1222
+ };
1223
+ timeout?: {
1224
+ after_ms: number;
1225
+ terminal_state?: "failed";
1226
+ };
1227
+ on_failure?: {
1228
+ payments?: {
1229
+ reverse?: {
1230
+ job_ref?: string;
1231
+ };
1232
+ };
1233
+ };
1234
+ reserve_credits?: {
1235
+ amount: number;
1236
+ user_id: string;
1237
+ credit_type: string;
1238
+ reason?: string;
1239
+ };
1240
+ payload?: Record<string, unknown>;
1241
+ idempotency_key?: string;
1242
+ max_attempts?: number;
1243
+ }) => Promise<{
1244
+ run_id: string;
1245
+ generation_status: string;
1246
+ state?: string;
1247
+ deduplicated?: boolean;
1248
+ }>;
1249
+ runs: (q?: {
1250
+ job_name?: string;
1251
+ state?: string;
1252
+ ids?: string[];
1253
+ limit?: number;
1254
+ }) => Promise<JobRun[]>;
1255
+ run: (runId: string) => Promise<JobRun>;
1256
+ cancel: (runId: string) => Promise<{
1257
+ run_id: string;
1258
+ state: string;
1259
+ }>;
1260
+ /** Clone a terminal run into a fresh queued run. */
1261
+ replay: (runId: string) => Promise<{
1262
+ run_id: string;
1263
+ replayed_from: string;
1264
+ }>;
1265
+ /**
1266
+ * Suspend the RUNNING run until an event (call from the executing
1267
+ * handler, then return 200 — the suspension wins).
1268
+ */
1269
+ wait: (runId: string, input: {
1270
+ event: string;
1271
+ timeout_seconds?: number;
1272
+ }) => Promise<void>;
1273
+ /** Wake every run waiting on the event. */
1274
+ emitEvent: (event: string, payload?: Record<string, unknown>) => Promise<number>;
1275
+ /** Secret for verifying X-Vxil-Jobs-Signature on your callback endpoints. */
1276
+ signingSecret: () => Promise<string>;
1277
+ schedules: {
1278
+ /** Recurring (5-field cron, UTC) or one-shot (run_at). Exactly one of cron/run_at. */
1279
+ create: (input: {
1280
+ job_name: string;
1281
+ target_url: string;
1282
+ payload?: Record<string, unknown>;
1283
+ cron?: string;
1284
+ run_at?: string;
1285
+ }) => Promise<JobSchedule>;
1286
+ list: () => Promise<JobSchedule[]>;
1287
+ delete: (scheduleId: string) => Promise<void>;
1288
+ /** Stop an active schedule firing (the row + next_run_at survive). */
1289
+ pause: (scheduleId: string) => Promise<{
1290
+ schedule_id: string;
1291
+ state: string;
1292
+ }>;
1293
+ /** Resume a paused schedule; next_run_at recomputes (cron) or re-arms
1294
+ * (one-shot; a lapsed run_at → 409 run_at_passed). */
1295
+ resume: (scheduleId: string) => Promise<{
1296
+ schedule_id: string;
1297
+ state: string;
1298
+ next_run_at: string;
1299
+ }>;
1300
+ };
1301
+ /** Per-endpoint/job flow control: rate (per minute) + max parallelism,
1302
+ * enforced by the executor. 'endpoint' matches the target_url https
1303
+ * ORIGIN; 'job_name' matches exactly. */
1304
+ flowRules: {
1305
+ create: (input: {
1306
+ match_kind: "endpoint" | "job_name";
1307
+ match_value: string;
1308
+ max_parallel?: number;
1309
+ rate_per_minute?: number;
1310
+ }) => Promise<JobFlowRule>;
1311
+ list: () => Promise<JobFlowRule[]>;
1312
+ delete: (ruleId: string) => Promise<void>;
1313
+ };
1314
+ };
1315
+ readonly auth: {
1316
+ signUp: (input: {
1317
+ email: string;
1318
+ password: string;
1319
+ }) => Promise<{
1320
+ user_id: string;
1321
+ session: AuthSession;
1322
+ verified: boolean;
1323
+ }>;
1324
+ signIn: (input: {
1325
+ email: string;
1326
+ password: string;
1327
+ }) => Promise<{
1328
+ user_id: string;
1329
+ session: AuthSession;
1330
+ verified: boolean;
1331
+ }>;
1332
+ magicLink: {
1333
+ request: (input: {
1334
+ email: string;
1335
+ redirect_url: string;
1336
+ }) => Promise<void>;
1337
+ verify: (token: string) => Promise<{
1338
+ user_id: string;
1339
+ session: AuthSession;
1340
+ verified: boolean;
1341
+ }>;
1342
+ };
1343
+ /** Email OTP sign-in: a 6-digit single-use code (distinct from magic-link).
1344
+ * Server-side guessing budget (config otp.maxAttempts), single active code,
1345
+ * resend cooldown (429 otp_rate_limited). */
1346
+ otp: {
1347
+ request: (input: {
1348
+ email: string;
1349
+ }) => Promise<void>;
1350
+ verify: (input: {
1351
+ email: string;
1352
+ code: string;
1353
+ }) => Promise<{
1354
+ user_id: string;
1355
+ session: AuthSession;
1356
+ verified: boolean;
1357
+ }>;
1358
+ };
1359
+ /** Anonymous (guest) sessions: instant end-user + session (JWT carries
1360
+ * `anon: true`); later convertible via the email-claim OTP link flow. */
1361
+ anonymous: {
1362
+ signIn: () => Promise<{
1363
+ user_id: string;
1364
+ session: AuthSession;
1365
+ anonymous: true;
1366
+ }>;
1367
+ link: {
1368
+ request: (input: {
1369
+ token: string;
1370
+ email: string;
1371
+ }) => Promise<void>;
1372
+ verify: (input: {
1373
+ token: string;
1374
+ email: string;
1375
+ code: string;
1376
+ }) => Promise<{
1377
+ user_id: string;
1378
+ email: string;
1379
+ verified: boolean;
1380
+ }>;
1381
+ };
1382
+ };
1383
+ /** Step-up re-auth: an OTP challenge on the CURRENT session. On verify the
1384
+ * bearer token ROTATES (the returned session.token replaces the old one —
1385
+ * swap it client-side) and the JWT gains an `elv` claim. */
1386
+ stepUp: {
1387
+ request: (input: {
1388
+ token: string;
1389
+ }) => Promise<void>;
1390
+ verify: (input: {
1391
+ token: string;
1392
+ code: string;
1393
+ }) => Promise<{
1394
+ session: {
1395
+ token: string;
1396
+ expires_at: string;
1397
+ };
1398
+ elevated_at: string;
1399
+ }>;
1400
+ };
1401
+ /** End-user administration (server/function surface, auth:write). */
1402
+ users: {
1403
+ /** GDPR PII erasure: anonymize the end-user's auth + tenant_users record
1404
+ * (email → tombstone, password/name/avatar cleared) and revoke every
1405
+ * live session. Idempotent — a second call on an already-erased user is a
1406
+ * no-op. Meant to be called from a "delete my account" server route or
1407
+ * vxil function (which declares `auth:write`). */
1408
+ erase: (userId: string) => Promise<{
1409
+ user_id: string;
1410
+ erased: boolean;
1411
+ }>;
1412
+ };
1413
+ sessions: {
1414
+ verify: (token: string) => Promise<{
1415
+ user_id: string;
1416
+ email: string | null;
1417
+ verified: boolean;
1418
+ display_name: string | null;
1419
+ expires_at: string;
1420
+ /** when this session last passed a step-up OTP re-auth (null = never) */
1421
+ elevated_at: string | null;
1422
+ }>;
1423
+ /** Rotates the refresh token: store the returned pair, discard the old one. */
1424
+ refresh: (refreshToken: string) => Promise<{
1425
+ user_id: string;
1426
+ session: AuthSession;
1427
+ }>;
1428
+ revoke: (token: string) => Promise<void>;
1429
+ /** Force-revoke a SPECIFIC session by its `session_id` (from `list`) — the
1430
+ * server-forced device-lockout path beyond the client-cooperative
1431
+ * by-token `revoke`. Throws 404 when the id is unknown or already revoked. */
1432
+ revokeById: (sessionId: string) => Promise<void>;
1433
+ /** List a user's active sessions (the account "signed-in devices" surface). */
1434
+ list: (userId: string) => Promise<Array<{
1435
+ session_id: string;
1436
+ created_at: string;
1437
+ expires_at: string;
1438
+ user_agent: string | null;
1439
+ }>>;
1440
+ };
1441
+ password: {
1442
+ requestReset: (input: {
1443
+ email: string;
1444
+ redirect_url: string;
1445
+ }) => Promise<void>;
1446
+ /** Confirming kills every existing session for the user. */
1447
+ confirmReset: (input: {
1448
+ token: string;
1449
+ password: string;
1450
+ }) => Promise<void>;
1451
+ };
1452
+ /** Social sign-in. The web `start`/`callback` flows are browser redirects
1453
+ * (not JSON calls); `native` is the mobile token-exchange the SDK wraps. */
1454
+ oauth: {
1455
+ /** Native social sign-in: exchange a provider `id_token`/`access_token`
1456
+ * for a vxil session (`linked` marks whether the user was created or
1457
+ * matched to an existing identity). */
1458
+ native: (provider: "google" | "apple" | "github" | "facebook" | "mock", input: {
1459
+ id_token?: string;
1460
+ access_token?: string;
1461
+ nonce?: string;
1462
+ }) => Promise<{
1463
+ user_id: string;
1464
+ session: AuthSession;
1465
+ verified: boolean;
1466
+ linked: "created" | "existing";
1467
+ }>;
1468
+ };
1469
+ };
1470
+ readonly rateLimits: {
1471
+ createPolicy: (input: {
1472
+ name: string;
1473
+ key_template: string;
1474
+ limit: number;
1475
+ window_seconds: number;
1476
+ behavior?: "block" | "shape";
1477
+ }) => Promise<RateLimitPolicy>;
1478
+ listPolicies: () => Promise<RateLimitPolicy[]>;
1479
+ deletePolicy: (policyId: string) => Promise<void>;
1480
+ /** Update a policy's mutable fields (name/limit/window/behavior/algorithm);
1481
+ * `policy_id` + `key_template` are immutable. */
1482
+ updatePolicy: (policyId: string, patch: {
1483
+ name?: string;
1484
+ limit?: number;
1485
+ window_seconds?: number;
1486
+ behavior?: "block" | "shape";
1487
+ algorithm?: "sliding_window" | "token_bucket";
1488
+ }) => Promise<RateLimitPolicy>;
1489
+ /** Reset a single rendered counter back to a full budget (the policy_id +
1490
+ * the key_values that render its key). */
1491
+ resetCounter: (input: {
1492
+ policy_id: string;
1493
+ key_values?: Record<string, string>;
1494
+ }) => Promise<{
1495
+ policy_id: string;
1496
+ key: string;
1497
+ reset: boolean;
1498
+ }>;
1499
+ /** Per-policy usage analytics: hourly allowed/blocked buckets + top keys
1500
+ * (last `hours`, clamped 1..48, default 24). */
1501
+ analytics: (input: {
1502
+ policy_id: string;
1503
+ hours?: number;
1504
+ }) => Promise<{
1505
+ policy_id: string;
1506
+ hours: number;
1507
+ buckets: Array<{
1508
+ hour: number;
1509
+ allowed: number;
1510
+ blocked: number;
1511
+ }>;
1512
+ top_keys: Array<{
1513
+ key: string;
1514
+ allowed: number;
1515
+ blocked: number;
1516
+ }>;
1517
+ totals: {
1518
+ allowed: number;
1519
+ blocked: number;
1520
+ };
1521
+ }>;
1522
+ /**
1523
+ * Consume budget. With behavior 'block' an exceeded check throws
1524
+ * VxilError(429); 'shape' resolves with allowed=false instead.
1525
+ * `override_id` is present when a per-identifier override was applied.
1526
+ */
1527
+ check: (input: {
1528
+ policy_id: string;
1529
+ key_values?: Record<string, string>;
1530
+ cost?: number;
1531
+ }) => Promise<{
1532
+ allowed: boolean;
1533
+ remaining: number;
1534
+ reset_seconds: number;
1535
+ override_id?: string;
1536
+ }>;
1537
+ /** Per-identifier overrides layered over a policy: `pattern` matches the
1538
+ * RENDERED key (exact, or a `*`-glob where the longest literal prefix
1539
+ * wins). Propagates to the check path within ≤30s (KV cacheTtl). */
1540
+ overrides: {
1541
+ list: (policyId: string) => Promise<RateLimitOverride[]>;
1542
+ create: (policyId: string, input: {
1543
+ pattern: string;
1544
+ limit?: number;
1545
+ window_seconds?: number;
1546
+ behavior?: "block" | "shape";
1547
+ note?: string;
1548
+ }) => Promise<RateLimitOverride>;
1549
+ /** `pattern` is immutable (like a policy's key_template). */
1550
+ update: (policyId: string, overrideId: string, patch: {
1551
+ limit?: number;
1552
+ window_seconds?: number;
1553
+ behavior?: "block" | "shape";
1554
+ note?: string;
1555
+ }) => Promise<RateLimitOverride>;
1556
+ delete: (policyId: string, overrideId: string) => Promise<void>;
1557
+ };
1558
+ };
1559
+ readonly cms: {
1560
+ collections: {
1561
+ /** Define a content type. The model is data — not config. */
1562
+ create: (input: {
1563
+ collection: string;
1564
+ singular?: string;
1565
+ /** End-user owner-scoping (docs/end-user-principals-design.md §5.1):
1566
+ * names an existing `string` field that holds the owner id. When set,
1567
+ * the cms worker auto-scopes owned reads/writes to the VERIFIED
1568
+ * end-user principal (default-deny) in end-user mode; a no-op in
1569
+ * server-caller mode. Omit for shared/reference collections. */
1570
+ owner_field?: string;
1571
+ fields?: Array<{
1572
+ field: string;
1573
+ type: "string" | "text" | "int" | "float" | "bool" | "datetime" | "json" | "relation" | "file";
1574
+ required?: boolean;
1575
+ validation?: {
1576
+ min?: number;
1577
+ max?: number;
1578
+ regex?: string;
1579
+ enum?: unknown[];
1580
+ };
1581
+ index_slot?: "s1" | "s2" | "s3" | "s4" | "n1" | "n2" | "t1" | "t2";
1582
+ relation_to?: string;
1583
+ /** Value uniqueness across the collection's LIVE items (cms.md §9.3);
1584
+ * scalar-valued types only. Concurrent duplicates → 409 unique_violation. */
1585
+ unique?: boolean;
1586
+ /** relation fields only: what a delete of the referenced item does to
1587
+ * this one (bounded fan-out, cms.md §11). */
1588
+ on_delete?: "cascade" | "set_null";
1589
+ }>;
1590
+ }) => Promise<{
1591
+ collection: string;
1592
+ fields: number;
1593
+ owner_field?: string;
1594
+ }>;
1595
+ list: () => Promise<Array<{
1596
+ collection: string;
1597
+ singular: string;
1598
+ fields: unknown[];
1599
+ owner_field?: string | null;
1600
+ }>>;
1601
+ addField: (collection: string, field: Record<string, unknown>) => Promise<void>;
1602
+ /** Set (or clear, with `null`) the collection's end-user owner-scope flag
1603
+ * (design §5.1). Names an existing `string` field that holds the owner id. */
1604
+ setOwnerField: (collection: string, ownerField: string | null) => Promise<void>;
1605
+ };
1606
+ items: {
1607
+ /** `lock` serializes same-key writers (per-tenant advisory lock); `guard`
1608
+ * is the declarative capacity/overlap invariant (requires lock) — cms.md §10. */
1609
+ create: (collection: string, input: {
1610
+ data: Record<string, unknown>;
1611
+ status?: "draft" | "published";
1612
+ lock?: string;
1613
+ guard?: CmsGuard;
1614
+ guards?: CmsGuardTerm[];
1615
+ }) => Promise<{
1616
+ item_id: string;
1617
+ status: string;
1618
+ version: number;
1619
+ data: Record<string, unknown>;
1620
+ }>;
1621
+ get: (collection: string, itemId: string) => Promise<Record<string, unknown>>;
1622
+ /**
1623
+ * The bounded query DSL. filter ops: $eq $ne $gt $gte $lt $lte $in
1624
+ * $contains $startsWith (LIKE-escaped; trigram-indexed on s*-slotted
1625
+ * fields) $arrayContains/$anyOf (json/relation array containment via the
1626
+ * JSONB GIN); range/sort needs slot-indexed fields. This is the
1627
+ * DELIBERATELY-UNTYPED escape hatch: it accepts everything the server
1628
+ * admits, including unslotted range ops (served by unindexed JSONB
1629
+ * scans, bounded only by the statement timeout) that the generated
1630
+ * per-field `Filterable` unions on `vx.from(...).query` exclude.
1631
+ */
1632
+ query: (collection: string, q?: {
1633
+ filter?: Record<string, unknown>;
1634
+ sort?: string;
1635
+ limit?: number;
1636
+ cursor?: string;
1637
+ }) => Promise<{
1638
+ items: Array<Record<string, unknown>>;
1639
+ next_cursor: string | null;
1640
+ }>;
1641
+ /** Merge-patch data keys; null clears a key. Concurrency opts (cms.md §9):
1642
+ * `ifVersion` → If-Match CAS; `if` → bounded field precondition against
1643
+ * the locked row; `lock`/`guard` → the §10 serialization primitives. */
1644
+ patch: (collection: string, itemId: string, data: Record<string, unknown>, opts?: CmsWriteOpts) => Promise<Record<string, unknown>>;
1645
+ /** Atomic in-database increment — PATCH `{ $inc: {field: delta} }`, ONE
1646
+ * conditional UPDATE guarded by the field's validation min/max (the quota
1647
+ * shape), the optional `if` precondition, and If-Match. 409
1648
+ * inc_out_of_bounds when the guard refuses (cms.md §9.3). */
1649
+ inc: (collection: string, itemId: string, incs: Record<string, number>, opts?: Pick<CmsWriteOpts, "ifVersion" | "if">) => Promise<Record<string, unknown>>;
1650
+ /** `{ count }` under the same bounded filter grammar (cms.md §9.4). 422
1651
+ * count_unavailable_with_read_hooks on beforeRead-hooked collections. */
1652
+ count: (collection: string, filter?: Record<string, unknown>) => Promise<number>;
1653
+ /** Returns the cascade tally (cms.md §11); `ifVersion` rides If-Match and
1654
+ * a conflict aborts BEFORE any cascade side-effect. */
1655
+ delete: (collection: string, itemId: string, opts?: Pick<CmsWriteOpts, "ifVersion">) => Promise<{
1656
+ item_id: string;
1657
+ deleted: boolean;
1658
+ cascaded: number;
1659
+ set_null: number;
1660
+ }>;
1661
+ publish: (collection: string, itemId: string) => Promise<Record<string, unknown>>;
1662
+ /** cms-rel B2 (cms.md §12): bounded group-by aggregate. fns count|sum|
1663
+ * min|max|avg over index-slot-bound fields; groupBy ≤2 slot-bound
1664
+ * fields; filter = the full query DSL incl. ONE-hop dotted join terms
1665
+ * ({"channel.visibility":"public"}); scan capped at 50k rows → 422
1666
+ * window_too_large (narrow the window or materialize a read-model). */
1667
+ aggregate: (collection: string, body: CmsAggregateBody) => Promise<{
1668
+ groups: Array<Record<string, unknown> & {
1669
+ key?: Record<string, unknown>;
1670
+ }>;
1671
+ scanned: number;
1672
+ }>;
1673
+ /** cms-rel B3 (cms.md §12): window ranking over the aggregate — rank ∈
1674
+ * row_number|rank|percent_rank, computed over ≤500 aggregated groups
1675
+ * (never raw rows), optional partitionBy. Same scan cap as aggregate. */
1676
+ rank: (collection: string, body: CmsRankBody) => Promise<{
1677
+ entries: Array<{
1678
+ key: Record<string, unknown>;
1679
+ partition?: Record<string, unknown>;
1680
+ metric: unknown;
1681
+ rank: number;
1682
+ }>;
1683
+ scanned: number;
1684
+ }>;
1685
+ };
1686
+ /** cms-rel B4 (cms.md §13): atomic multi-collection transaction — 1–5
1687
+ * steps over ≤3 collections, per-step `$where` CAS preconditions (the
1688
+ * PATCH `if` grammar). All-or-nothing: any failed precondition/validation
1689
+ * rolls the WHOLE transaction back (409 precondition_failed names the
1690
+ * step). cms-internal only — no cross-feature effects inside the tx. */
1691
+ transaction: (steps: CmsTxStep[]) => Promise<{
1692
+ results: Array<{
1693
+ op: string;
1694
+ collection: string;
1695
+ item_id: string;
1696
+ status: string | null;
1697
+ version: number;
1698
+ }>;
1699
+ committed: boolean;
1700
+ tx_id: string;
1701
+ }>;
1702
+ /** cms-rel B5 (cms.md §12.4): declared read-models (config `readModels`
1703
+ * bag) — list with last-run status, and "run now" materialization into
1704
+ * the rollup collection. */
1705
+ readModels: {
1706
+ list: () => Promise<Array<{
1707
+ name: string;
1708
+ collection: string;
1709
+ kind: "aggregate" | "rank";
1710
+ cron: string | null;
1711
+ to: string | null;
1712
+ enabled: boolean;
1713
+ last_run: Record<string, unknown> | null;
1714
+ }>>;
1715
+ materialize: (name: string) => Promise<{
1716
+ name: string;
1717
+ to: string;
1718
+ groups_written: number;
1719
+ scanned: number;
1720
+ run_id: string;
1721
+ }>;
1722
+ };
1723
+ };
1724
+ readonly comments: {
1725
+ create: (input: {
1726
+ topic: string;
1727
+ body: string;
1728
+ author_id: string;
1729
+ parent_id?: string;
1730
+ }) => Promise<{
1731
+ comment_id: string;
1732
+ topic: string;
1733
+ parent_id: string | null;
1734
+ author_id: string;
1735
+ body: string;
1736
+ created_at: string;
1737
+ }>;
1738
+ list: (q: {
1739
+ topic: string;
1740
+ cursor?: string;
1741
+ limit?: number;
1742
+ }) => Promise<{
1743
+ comments: Array<{
1744
+ comment_id: string;
1745
+ topic: string;
1746
+ parent_id: string | null;
1747
+ author_id: string;
1748
+ body: string | null;
1749
+ reactions: Record<string, string[]>;
1750
+ created_at: string;
1751
+ edited_at: string | null;
1752
+ deleted: boolean;
1753
+ }>;
1754
+ next_cursor: string | null;
1755
+ }>;
1756
+ /** Author edit, allowed within the tenant's editWindowMinutes. */
1757
+ edit: (commentId: string, input: {
1758
+ body: string;
1759
+ author_id: string;
1760
+ }) => Promise<void>;
1761
+ /** Tombstone. Pass author_id for self-delete; omit for tenant moderation. */
1762
+ delete: (commentId: string, opts?: {
1763
+ author_id?: string;
1764
+ }) => Promise<void>;
1765
+ /** Toggle an emoji reaction for an author. */
1766
+ react: (commentId: string, input: {
1767
+ emoji: string;
1768
+ author_id: string;
1769
+ }) => Promise<{
1770
+ comment_id: string;
1771
+ reactions: Record<string, string[]>;
1772
+ }>;
1773
+ /** Cross-topic recent-comments feed (tenant-wide, newest-first) — the
1774
+ * dashboard "recent activity" surface. Keyset cursor + optional filters. */
1775
+ recent: (q?: {
1776
+ topic_prefix?: string;
1777
+ author_id?: string;
1778
+ cursor?: string;
1779
+ limit?: number;
1780
+ }) => Promise<{
1781
+ comments: Array<{
1782
+ comment_id: string;
1783
+ topic: string;
1784
+ parent_id: string | null;
1785
+ author_id: string;
1786
+ body: string | null;
1787
+ reactions: Record<string, string[]>;
1788
+ created_at: string;
1789
+ edited_at: string | null;
1790
+ deleted: boolean;
1791
+ }>;
1792
+ next_cursor: string | null;
1793
+ }>;
1794
+ };
1795
+ /**
1796
+ * DM / inbox (the `dm` feature) — a conversation ENVELOPE composed over
1797
+ * `comments` (message store) + `realtime` (delivery) + `presence`. DM owns
1798
+ * only the envelope: membership, read-cursors, mute, and a directed block
1799
+ * list; the messages themselves are `comments` rows on the
1800
+ * `dm:<conversation_id>` topic. Gated by its own `dm` config, independent of
1801
+ * comments. See docs/features/dm.md.
1802
+ */
1803
+ readonly dm: {
1804
+ /** Read the resolved DM config (defaults merged with the stored partial). */
1805
+ getConfig: () => Promise<DmConfigState>;
1806
+ /** Write the DM master config — a partial merge (omitted leaves keep their
1807
+ * current value), version-bumped and republished to the gate's KV key. */
1808
+ setConfig: (patch: {
1809
+ enabled?: boolean;
1810
+ maxParticipants?: number;
1811
+ realtimeDelivery?: boolean;
1812
+ blockingEnabled?: boolean;
1813
+ }) => Promise<DmConfigState>;
1814
+ conversations: {
1815
+ /** Open (or, for a direct pair, return the existing) conversation. A
1816
+ * `direct` conversation (exactly 2 people) dedupes on the unordered pair
1817
+ * (reused:true); a block on either side rejects the open (403). */
1818
+ open: (input: {
1819
+ opener_id: string;
1820
+ participant_ids: string[];
1821
+ kind?: "direct" | "group";
1822
+ subject?: string;
1823
+ }) => Promise<DmConversation>;
1824
+ /** "My inbox": a user's active conversations, newest-activity first, each
1825
+ * with `muted` + `unread`. */
1826
+ list: (userId: string) => Promise<DmInboxConversation[]>;
1827
+ /** Read a conversation's messages (an authz'd participant only; messages
1828
+ * from authors the viewer has blocked are filtered out). Keyset cursor. */
1829
+ messages: (conversationId: string, q: {
1830
+ user_id: string;
1831
+ cursor?: string;
1832
+ limit?: number;
1833
+ }) => Promise<{
1834
+ conversation_id: string;
1835
+ messages: DmMessage[];
1836
+ next_cursor: string | null;
1837
+ }>;
1838
+ };
1839
+ /** Send a message (an authz'd participant). Stored as a comment on the
1840
+ * conversation topic + fanned out over realtime to the unmuted, non-sender
1841
+ * participants (best-effort; degrades to pull). */
1842
+ sendMessage: (conversationId: string, input: {
1843
+ sender_id: string;
1844
+ body: string;
1845
+ }) => Promise<{
1846
+ message_id: string;
1847
+ conversation_id: string;
1848
+ }>;
1849
+ /** Advance a participant's read-cursor to `message_id` (forward-only — a
1850
+ * mark-read for an older message is a no-op; the cursor never rewinds). */
1851
+ markRead: (conversationId: string, input: {
1852
+ user_id: string;
1853
+ message_id: string;
1854
+ }) => Promise<{
1855
+ conversation_id: string;
1856
+ advanced: boolean;
1857
+ last_read_message_id: string | null;
1858
+ }>;
1859
+ /** Mute / unmute a participant — suppresses realtime delivery + unread
1860
+ * badging WITHOUT removing them from the conversation. */
1861
+ mute: (conversationId: string, input: {
1862
+ user_id: string;
1863
+ muted: boolean;
1864
+ }) => Promise<{
1865
+ conversation_id: string;
1866
+ user_id: string;
1867
+ muted: boolean;
1868
+ }>;
1869
+ /** Set / clear a directed user block edge (conversation-independent). Omit
1870
+ * `blocked_state` (or pass true) to block; false to unblock. Requires the
1871
+ * tenant's `blockingEnabled` DM config (else 403). */
1872
+ block: (input: {
1873
+ blocker: string;
1874
+ blocked: string;
1875
+ blocked_state?: boolean;
1876
+ }) => Promise<{
1877
+ blocker: string;
1878
+ blocked: string;
1879
+ blocked_state: boolean;
1880
+ }>;
1881
+ };
1882
+ /** The MCP aggregation surface (the `mcp` feature). */
1883
+ readonly mcp: {
1884
+ /** Per-tenant secret for verifying the X-Vxil-Mcp-Signature header on custom
1885
+ * MCP tool calls (mcp.md §6.5) — the mirror of jobs.signingSecret(). */
1886
+ signingSecret: () => Promise<string>;
1887
+ };
1888
+ /**
1889
+ * Activity streams + in-app notification feeds (the `activity-feed` feature).
1890
+ * Feed refs are addressed as ({group}, {feed_id}); a group's behaviour
1891
+ * (flat / aggregated / notification) is the tenant's config, not an argument.
1892
+ */
1893
+ readonly feeds: {
1894
+ /**
1895
+ * Add one activity (or a batch — all-or-nothing) to a flat feed. Idempotent
1896
+ * upsert on (foreign_id, time); `to:[]` CCs into other feeds; triggers
1897
+ * fan-out + a realtime new-activity event.
1898
+ */
1899
+ addActivity: (group: string, feedId: string, activity: FeedActivityInput | FeedActivityInput[]) => Promise<FeedActivity[]>;
1900
+ /**
1901
+ * Read a feed with a keyset cursor. Flat feeds return `activities`; aggregated
1902
+ * and notification feeds return `groups` (notification also carries the badge).
1903
+ * Pass `markSeen`/`markRead` to acknowledge the returned page on a notification
1904
+ * feed; `notificationState` filters to unseen | unread | all.
1905
+ */
1906
+ read: (group: string, feedId: string, q?: {
1907
+ limit?: number;
1908
+ id_lt?: string;
1909
+ id_gt?: string;
1910
+ notificationState?: "unread" | "unseen" | "all";
1911
+ markSeen?: boolean;
1912
+ markRead?: boolean;
1913
+ }) => Promise<FeedPage>;
1914
+ /** Remove an activity by id (or by foreign_id); tombstones + un-fans-out. */
1915
+ removeActivity: (group: string, feedId: string, ref: {
1916
+ id?: string;
1917
+ foreign_id?: string;
1918
+ }) => Promise<void>;
1919
+ /**
1920
+ * Follow one target (or many in one txn — all-or-nothing under follow.maxFollowing).
1921
+ * Backfills up to `copyLimit` recent activities into this timeline.
1922
+ */
1923
+ follow: (group: string, feedId: string, target: string | string[], opts?: {
1924
+ copyLimit?: number;
1925
+ }) => Promise<{
1926
+ follower: string;
1927
+ followed: string[];
1928
+ }>;
1929
+ /** Unfollow a target; un-fans the source out of this timeline (idempotent). */
1930
+ unfollow: (group: string, feedId: string, target: string) => Promise<void>;
1931
+ /** Who a feed FOLLOWS (`following[]` + `following_count`) plus the true
1932
+ * `followers_count` (who follows this feed). Keyset cursor via `after`. */
1933
+ listFollows: (group: string, feedId: string, q?: {
1934
+ limit?: number;
1935
+ after?: string;
1936
+ }) => Promise<{
1937
+ following: Array<{
1938
+ target: string;
1939
+ copy_limit: number;
1940
+ created_at: string;
1941
+ }>;
1942
+ following_count: number;
1943
+ followers_count: number;
1944
+ next_after?: string;
1945
+ }>;
1946
+ /**
1947
+ * Block (two-way fan-out suppression, removes existing rows) or mute (one-way
1948
+ * read-side filter) the `blocked` feed for `owner`. Defaults to block.
1949
+ */
1950
+ block: (input: {
1951
+ owner: string;
1952
+ blocked: string;
1953
+ mode?: "block" | "mute";
1954
+ }) => Promise<{
1955
+ owner: string;
1956
+ blocked: string;
1957
+ mode: "block" | "mute";
1958
+ }>;
1959
+ /** Remove a block/mute. */
1960
+ unblock: (owner: string, blocked: string) => Promise<void>;
1961
+ notification: {
1962
+ /**
1963
+ * Mark groups seen. Pass `ids` to scope, `before` for everything older than a
1964
+ * timestamp, or neither to mark all. Recomputes + publishes the badge.
1965
+ */
1966
+ markSeen: (userId: string, scope?: {
1967
+ ids?: string[];
1968
+ before?: string;
1969
+ }) => Promise<FeedBadge>;
1970
+ /** Mark groups read (read implies seen). Same scoping as markSeen. */
1971
+ markRead: (userId: string, scope?: {
1972
+ ids?: string[];
1973
+ before?: string;
1974
+ }) => Promise<FeedBadge>;
1975
+ /** Mark every group for the user read (the "mark all read" affordance). */
1976
+ markAll: (userId: string) => Promise<FeedBadge>;
1977
+ /** Archive/dismiss groups (hidden from inbox + dropped from the badge). */
1978
+ archive: (userId: string, scope?: {
1979
+ ids?: string[];
1980
+ before?: string;
1981
+ }) => Promise<FeedBadge>;
1982
+ /** The badge: { unseen, unread, total } (KV-cached over the authority). */
1983
+ unreadCount: (userId: string) => Promise<FeedBadge>;
1984
+ };
1985
+ /**
1986
+ * Mint realtime connect token(s) for a user, one per requested feed channel.
1987
+ * Open wss://api…{connect_path} per token in the browser.
1988
+ */
1989
+ realtimeToken: (input: {
1990
+ user_id: string;
1991
+ feeds: string[];
1992
+ ttl_seconds?: number;
1993
+ }) => Promise<{
1994
+ user_id: string;
1995
+ tokens: FeedRealtimeToken[];
1996
+ }>;
1997
+ /** Read a user's notification preference policy. */
1998
+ getPreferences: (userId: string) => Promise<FeedPreferences>;
1999
+ /** Write a user's notification preference policy. */
2000
+ setPreferences: (userId: string, policy: {
2001
+ channels?: Record<string, boolean>;
2002
+ categories?: Record<string, boolean>;
2003
+ verbs?: Record<string, boolean>;
2004
+ mute_until?: string;
2005
+ }) => Promise<FeedPreferences>;
2006
+ };
2007
+ /** Alias for `feeds` — the `activity-feed` feature namespace. */
2008
+ readonly activityFeed: {
2009
+ /**
2010
+ * Add one activity (or a batch — all-or-nothing) to a flat feed. Idempotent
2011
+ * upsert on (foreign_id, time); `to:[]` CCs into other feeds; triggers
2012
+ * fan-out + a realtime new-activity event.
2013
+ */
2014
+ addActivity: (group: string, feedId: string, activity: FeedActivityInput | FeedActivityInput[]) => Promise<FeedActivity[]>;
2015
+ /**
2016
+ * Read a feed with a keyset cursor. Flat feeds return `activities`; aggregated
2017
+ * and notification feeds return `groups` (notification also carries the badge).
2018
+ * Pass `markSeen`/`markRead` to acknowledge the returned page on a notification
2019
+ * feed; `notificationState` filters to unseen | unread | all.
2020
+ */
2021
+ read: (group: string, feedId: string, q?: {
2022
+ limit?: number;
2023
+ id_lt?: string;
2024
+ id_gt?: string;
2025
+ notificationState?: "unread" | "unseen" | "all";
2026
+ markSeen?: boolean;
2027
+ markRead?: boolean;
2028
+ }) => Promise<FeedPage>;
2029
+ /** Remove an activity by id (or by foreign_id); tombstones + un-fans-out. */
2030
+ removeActivity: (group: string, feedId: string, ref: {
2031
+ id?: string;
2032
+ foreign_id?: string;
2033
+ }) => Promise<void>;
2034
+ /**
2035
+ * Follow one target (or many in one txn — all-or-nothing under follow.maxFollowing).
2036
+ * Backfills up to `copyLimit` recent activities into this timeline.
2037
+ */
2038
+ follow: (group: string, feedId: string, target: string | string[], opts?: {
2039
+ copyLimit?: number;
2040
+ }) => Promise<{
2041
+ follower: string;
2042
+ followed: string[];
2043
+ }>;
2044
+ /** Unfollow a target; un-fans the source out of this timeline (idempotent). */
2045
+ unfollow: (group: string, feedId: string, target: string) => Promise<void>;
2046
+ /** Who a feed FOLLOWS (`following[]` + `following_count`) plus the true
2047
+ * `followers_count` (who follows this feed). Keyset cursor via `after`. */
2048
+ listFollows: (group: string, feedId: string, q?: {
2049
+ limit?: number;
2050
+ after?: string;
2051
+ }) => Promise<{
2052
+ following: Array<{
2053
+ target: string;
2054
+ copy_limit: number;
2055
+ created_at: string;
2056
+ }>;
2057
+ following_count: number;
2058
+ followers_count: number;
2059
+ next_after?: string;
2060
+ }>;
2061
+ /**
2062
+ * Block (two-way fan-out suppression, removes existing rows) or mute (one-way
2063
+ * read-side filter) the `blocked` feed for `owner`. Defaults to block.
2064
+ */
2065
+ block: (input: {
2066
+ owner: string;
2067
+ blocked: string;
2068
+ mode?: "block" | "mute";
2069
+ }) => Promise<{
2070
+ owner: string;
2071
+ blocked: string;
2072
+ mode: "block" | "mute";
2073
+ }>;
2074
+ /** Remove a block/mute. */
2075
+ unblock: (owner: string, blocked: string) => Promise<void>;
2076
+ notification: {
2077
+ /**
2078
+ * Mark groups seen. Pass `ids` to scope, `before` for everything older than a
2079
+ * timestamp, or neither to mark all. Recomputes + publishes the badge.
2080
+ */
2081
+ markSeen: (userId: string, scope?: {
2082
+ ids?: string[];
2083
+ before?: string;
2084
+ }) => Promise<FeedBadge>;
2085
+ /** Mark groups read (read implies seen). Same scoping as markSeen. */
2086
+ markRead: (userId: string, scope?: {
2087
+ ids?: string[];
2088
+ before?: string;
2089
+ }) => Promise<FeedBadge>;
2090
+ /** Mark every group for the user read (the "mark all read" affordance). */
2091
+ markAll: (userId: string) => Promise<FeedBadge>;
2092
+ /** Archive/dismiss groups (hidden from inbox + dropped from the badge). */
2093
+ archive: (userId: string, scope?: {
2094
+ ids?: string[];
2095
+ before?: string;
2096
+ }) => Promise<FeedBadge>;
2097
+ /** The badge: { unseen, unread, total } (KV-cached over the authority). */
2098
+ unreadCount: (userId: string) => Promise<FeedBadge>;
2099
+ };
2100
+ /**
2101
+ * Mint realtime connect token(s) for a user, one per requested feed channel.
2102
+ * Open wss://api…{connect_path} per token in the browser.
2103
+ */
2104
+ realtimeToken: (input: {
2105
+ user_id: string;
2106
+ feeds: string[];
2107
+ ttl_seconds?: number;
2108
+ }) => Promise<{
2109
+ user_id: string;
2110
+ tokens: FeedRealtimeToken[];
2111
+ }>;
2112
+ /** Read a user's notification preference policy. */
2113
+ getPreferences: (userId: string) => Promise<FeedPreferences>;
2114
+ /** Write a user's notification preference policy. */
2115
+ setPreferences: (userId: string, policy: {
2116
+ channels?: Record<string, boolean>;
2117
+ categories?: Record<string, boolean>;
2118
+ verbs?: Record<string, boolean>;
2119
+ mute_until?: string;
2120
+ }) => Promise<FeedPreferences>;
2121
+ };
2122
+ /** Shared executor for the seen/read/archive notification-state endpoints. */
2123
+ private feedsMark;
2124
+ readonly webhooks: {
2125
+ /**
2126
+ * Subscribe an https endpoint to audit-stream events (optionally
2127
+ * filtered by event-name prefixes like 'user.'). Deliveries are jobs
2128
+ * callbacks: verify X-Vxil-Jobs-Signature with jobs.signingSecret().
2129
+ */
2130
+ subscribe: (input: {
2131
+ target_url: string;
2132
+ event_prefixes?: string[];
2133
+ }) => Promise<{
2134
+ sub_id: string;
2135
+ target_url: string;
2136
+ event_prefixes: string[];
2137
+ state: string;
2138
+ }>;
2139
+ list: () => Promise<Array<{
2140
+ sub_id: string;
2141
+ target_url: string;
2142
+ event_prefixes: string[];
2143
+ state: string;
2144
+ }>>;
2145
+ unsubscribe: (subId: string) => Promise<void>;
2146
+ /**
2147
+ * Send a synthetic 'webhooks.test' event to the subscription's endpoint
2148
+ * through the REAL signing+delivery pipeline and wait inline (≤ ~8 s).
2149
+ * Terminal → { delivered, state, last_error_* }; still in flight → the
2150
+ * 202 pending shape { sub_id, run_id, state } (poll jobs.run(run_id)).
2151
+ */
2152
+ test: (subId: string) => Promise<{
2153
+ sub_id: string;
2154
+ run_id: string;
2155
+ state: string;
2156
+ delivered: boolean;
2157
+ last_error_class?: string | null;
2158
+ last_error_msg?: string | null;
2159
+ }>;
2160
+ sources: {
2161
+ /** Register an inbound source; the receiver URL is returned ONCE. */
2162
+ create: (input: {
2163
+ provider: "stripe" | "paddle" | "github" | "slack" | "revenuecat" | "generic";
2164
+ name: string;
2165
+ forward_url?: string;
2166
+ }) => Promise<{
2167
+ source_id: string;
2168
+ receiver_url_path: string;
2169
+ provider: string;
2170
+ }>;
2171
+ list: () => Promise<Array<{
2172
+ source_id: string;
2173
+ provider: string;
2174
+ name: string;
2175
+ }>>;
2176
+ delete: (sourceId: string) => Promise<void>;
2177
+ /** Send a sample envelope to the source's forward_url through the real
2178
+ * delivery path (inline result; 202 pending shape when still in flight). */
2179
+ test: (sourceId: string) => Promise<{
2180
+ source_id: string;
2181
+ run_id: string;
2182
+ state: string;
2183
+ delivered: boolean;
2184
+ last_error_class?: string | null;
2185
+ last_error_msg?: string | null;
2186
+ }>;
2187
+ };
2188
+ events: {
2189
+ list: (q?: {
2190
+ source_id?: string;
2191
+ status?: string;
2192
+ cursor?: string;
2193
+ limit?: number;
2194
+ }) => Promise<{
2195
+ events: Array<{
2196
+ event_id: string;
2197
+ event_type: string | null;
2198
+ payload: Record<string, unknown>;
2199
+ sig_verified: boolean;
2200
+ status: string;
2201
+ }>;
2202
+ next_cursor: string | null;
2203
+ }>;
2204
+ replay: (eventId: string) => Promise<void>;
2205
+ /** The Svix message-attempt view: delivery state/attempts/DLQ flag for
2206
+ * every jobs run this event's forwards created. */
2207
+ runs: (eventId: string) => Promise<{
2208
+ event_id: string;
2209
+ status: string;
2210
+ runs: Array<{
2211
+ run_id: string;
2212
+ state: string;
2213
+ attempt_number?: number;
2214
+ max_attempts?: number | null;
2215
+ last_error_class?: string | null;
2216
+ last_error_msg?: string | null;
2217
+ queued_at?: string | null;
2218
+ completed_at?: string | null;
2219
+ dead_lettered: boolean;
2220
+ }>;
2221
+ }>;
2222
+ };
2223
+ };
2224
+ readonly orgs: {
2225
+ create: (input: {
2226
+ slug: string;
2227
+ name: string;
2228
+ owner_user_id: string;
2229
+ }) => Promise<{
2230
+ org_id: string;
2231
+ slug: string;
2232
+ name: string;
2233
+ }>;
2234
+ list: (q?: {
2235
+ user_id?: string;
2236
+ }) => Promise<Array<{
2237
+ org_id: string;
2238
+ slug: string;
2239
+ name: string;
2240
+ role?: string;
2241
+ }>>;
2242
+ delete: (orgId: string) => Promise<void>;
2243
+ /** RBAC: org:read (viewer+), org:write (member+), members:manage (admin+),
2244
+ * org:admin (owner) — plus any custom-role permission-set. Pass
2245
+ * `opts.resource` to additionally consult per-resource ACL grants; the
2246
+ * response `source` marks whether a grant came from the role lattice or an ACL. */
2247
+ check: (orgId: string, userId: string, permission: string, opts?: {
2248
+ resource?: string;
2249
+ }) => Promise<{
2250
+ allowed: boolean;
2251
+ role: string | null;
2252
+ source?: string;
2253
+ }>;
2254
+ /** The active-org snapshot auth embeds in session JWTs when the tenant
2255
+ * enables auth config `orgClaims` (most-recent membership by joined_at).
2256
+ * EVENTUAL-CONSISTENT for JWTs (recomputed at refresh); use `check` for
2257
+ * revocation-grade authz. No membership → org_id null. */
2258
+ sessionClaims: (userId: string) => Promise<{
2259
+ user_id: string;
2260
+ org_id: string | null;
2261
+ role: string | null;
2262
+ perms: string[];
2263
+ }>;
2264
+ /** Read one workspace (incl. `settings`). */
2265
+ get: (orgId: string) => Promise<{
2266
+ org_id: string;
2267
+ slug: string;
2268
+ name: string;
2269
+ created_at?: string;
2270
+ settings?: Record<string, unknown>;
2271
+ role?: string | null;
2272
+ }>;
2273
+ /** Update a workspace's name / `settings` jsonb. */
2274
+ update: (orgId: string, patch: {
2275
+ name?: string;
2276
+ settings?: Record<string, unknown>;
2277
+ }) => Promise<{
2278
+ org_id: string;
2279
+ slug: string;
2280
+ name: string;
2281
+ settings?: Record<string, unknown>;
2282
+ }>;
2283
+ members: {
2284
+ add: (orgId: string, userId: string, role: "owner" | "admin" | "member" | "viewer") => Promise<void>;
2285
+ list: (orgId: string) => Promise<Array<{
2286
+ user_id: string;
2287
+ role: string;
2288
+ }>>;
2289
+ remove: (orgId: string, userId: string) => Promise<void>;
2290
+ };
2291
+ invitations: {
2292
+ /** invite_token is returned ONCE — your app builds the accept URL. */
2293
+ create: (orgId: string, email: string, role: "admin" | "member" | "viewer") => Promise<{
2294
+ invite_id: string;
2295
+ invite_token: string;
2296
+ }>;
2297
+ accept: (token: string, userId: string) => Promise<{
2298
+ org_id: string;
2299
+ role: string;
2300
+ }>;
2301
+ revoke: (inviteId: string) => Promise<void>;
2302
+ /** List an org's pending invitations. */
2303
+ list: (orgId: string) => Promise<Array<{
2304
+ invite_id: string;
2305
+ org_id?: string;
2306
+ email: string | null;
2307
+ role: string;
2308
+ created_at?: string;
2309
+ expires_at?: string;
2310
+ accepted_at?: string | null;
2311
+ revoked_at?: string | null;
2312
+ pending?: boolean;
2313
+ }>>;
2314
+ };
2315
+ /** Custom tenant roles (permission-sets; migration orgs/0019). A custom role
2316
+ * is a named set of permissions assignable like any built-in; the four
2317
+ * built-ins reproduce the fixed owner>admin>member>viewer lattice. */
2318
+ roles: {
2319
+ /** Define (or upsert) a custom role. `role_key` must not collide with a built-in. */
2320
+ define: (input: {
2321
+ role_key: string;
2322
+ name: string;
2323
+ permissions: string[];
2324
+ rank?: number;
2325
+ }) => Promise<{
2326
+ role_key: string;
2327
+ name: string;
2328
+ permissions: string[];
2329
+ rank: number | null;
2330
+ }>;
2331
+ /** The tenant role catalog: `builtin` (the fixed lattice) + `custom`. */
2332
+ list: () => Promise<{
2333
+ builtin: Array<{
2334
+ role_key: string;
2335
+ name: string;
2336
+ permissions: string[];
2337
+ builtin?: boolean;
2338
+ }>;
2339
+ custom: Array<{
2340
+ role_key: string;
2341
+ name: string;
2342
+ permissions: string[];
2343
+ rank: number | null;
2344
+ }>;
2345
+ }>;
2346
+ /** Delete a custom role (built-in roles cannot be deleted). */
2347
+ delete: (roleKey: string) => Promise<void>;
2348
+ };
2349
+ /** Per-org/user resource ACL grants (migration orgs/0019): grant a permission
2350
+ * on a specific resource string; `check(...,{ resource })` consults them. */
2351
+ acl: {
2352
+ grant: (orgId: string, input: {
2353
+ user_id: string;
2354
+ resource: string;
2355
+ permission: string;
2356
+ }) => Promise<{
2357
+ org_id: string;
2358
+ user_id: string;
2359
+ resource: string;
2360
+ permission: string;
2361
+ granted: boolean;
2362
+ }>;
2363
+ /** List an org's resource ACL grants (optionally scoped to one user). */
2364
+ list: (orgId: string, q?: {
2365
+ user_id?: string;
2366
+ }) => Promise<Array<{
2367
+ user_id: string;
2368
+ resource: string;
2369
+ permission: string;
2370
+ created_at?: string;
2371
+ }>>;
2372
+ /** Revoke a resource ACL grant. */
2373
+ revoke: (orgId: string, input: {
2374
+ user_id: string;
2375
+ resource: string;
2376
+ permission: string;
2377
+ }) => Promise<void>;
2378
+ };
2379
+ };
2380
+ readonly realtime: {
2381
+ /** Server-side: mint a connect token for an end-user; the browser opens
2382
+ * wss://api…/v1/realtime/connect?token=… with it. */
2383
+ mintToken: (input: {
2384
+ channel: string;
2385
+ user_id: string;
2386
+ ttl_seconds?: number;
2387
+ }) => Promise<{
2388
+ token: string;
2389
+ connect_path: string;
2390
+ expires_at: string;
2391
+ }>;
2392
+ publish: (channel: string, event: string, data?: unknown) => Promise<number>;
2393
+ /** Needs the presence feature enabled. */
2394
+ presence: (channel: string) => Promise<{
2395
+ users: Array<{
2396
+ user_id: string;
2397
+ connections: number;
2398
+ }>;
2399
+ total_connections: number;
2400
+ }>;
2401
+ /** Glue for the `@vxil/realtime` reconnecting client: returns a function
2402
+ * matching its TokenProvider type *structurally* (no import — a static
2403
+ * import would break the single-file served sdk.mjs) that re-mints via
2404
+ * POST /v1/realtime/tokens with `defaults` merged over the channel the
2405
+ * client asks for. Server-side (Node) use only — it needs the API key;
2406
+ * browsers must fetch tokens from YOUR backend instead. */
2407
+ tokenProvider: (defaults: {
2408
+ user_id: string;
2409
+ ttl_seconds?: number;
2410
+ }) => (ctx: {
2411
+ channel: string;
2412
+ }) => Promise<{
2413
+ token: string;
2414
+ connect_path: string;
2415
+ expires_at: string;
2416
+ }>;
2417
+ /** One-shot: mint a token AND open the WebSocket. Uses the global
2418
+ * `WebSocket` (browser / Node ≥22). On older Node, pass a constructor —
2419
+ * `import WebSocket from 'ws'; await vxil.realtime.connect(opts, { WebSocket })`
2420
+ * — instead of failing with a silent `WebSocket is not defined`. For
2421
+ * auto-reconnect / token refresh / presence helpers use `@vxil/realtime`
2422
+ * with `vxil.realtime.tokenProvider(...)` instead. */
2423
+ connect: (input: {
2424
+ channel: string;
2425
+ user_id: string;
2426
+ ttl_seconds?: number;
2427
+ }, opts?: {
2428
+ WebSocket?: unknown;
2429
+ }) => Promise<WebSocket>;
2430
+ };
2431
+ readonly files: {
2432
+ /** Mint a presigned PUT; upload the bytes yourself, then call complete(). */
2433
+ createUploadUrl: (input: {
2434
+ user_id: string;
2435
+ filename: string;
2436
+ content_type: string;
2437
+ size_bytes: number;
2438
+ }) => Promise<{
2439
+ object_id: string;
2440
+ upload_url: string;
2441
+ upload_method: string;
2442
+ expires_at: string;
2443
+ }>;
2444
+ complete: (objectId: string) => Promise<{
2445
+ object_id: string;
2446
+ status: string;
2447
+ }>;
2448
+ downloadUrl: (objectId: string) => Promise<string>;
2449
+ list: (q?: {
2450
+ user_id?: string;
2451
+ cursor?: string;
2452
+ limit?: number;
2453
+ }) => Promise<{
2454
+ files: FileObject[];
2455
+ next_cursor: string | null;
2456
+ }>;
2457
+ /** Soft delete; bytes are hard-deleted 30 days later. */
2458
+ delete: (objectId: string) => Promise<void>;
2459
+ /** Aggregate storage usage vs quotas (the FilesManager Storage panel). */
2460
+ usage: () => Promise<{
2461
+ usage: {
2462
+ object_count: number;
2463
+ total_bytes: number;
2464
+ };
2465
+ quotas: {
2466
+ maxObjectCount?: number;
2467
+ maxObjectBytes?: number;
2468
+ maxTotalBytes?: number;
2469
+ };
2470
+ available: Record<string, number | null>;
2471
+ }>;
2472
+ /** OCR / text extraction (files.md §1.1, BYO-key add-on). Small/mock inputs
2473
+ * extract inline (status `available`); large inputs (or `async:true`) return
2474
+ * 202 with a `job_id` — poll `getText()`. */
2475
+ extractText: (objectId: string, opts?: {
2476
+ langs?: string[];
2477
+ boundingBoxes?: boolean;
2478
+ async?: boolean;
2479
+ }) => Promise<{
2480
+ object_id: string;
2481
+ text?: string;
2482
+ blocks?: OcrBlock[] | null;
2483
+ provider?: string;
2484
+ job_id?: string;
2485
+ status?: string;
2486
+ extracted_at?: string;
2487
+ }>;
2488
+ /** Fetch the cached extraction (status: not_extracted|pending|available|failed). */
2489
+ getText: (objectId: string) => Promise<{
2490
+ object_id: string;
2491
+ status: string;
2492
+ text: string | null;
2493
+ blocks?: OcrBlock[] | null;
2494
+ provider?: string | null;
2495
+ extracted_at: string | null;
2496
+ error?: string | null;
2497
+ }>;
2498
+ /** Set / extend / clear an object's TTL. `expiresInSeconds: null` clears it
2499
+ * (else a future auto-delete after the given seconds; minimum 60). */
2500
+ setTtl: (objectId: string, expiresInSeconds: number | null) => Promise<{
2501
+ object_id: string;
2502
+ expires_at: string | null;
2503
+ }>;
2504
+ sharedLinks: {
2505
+ /** Public (unauthenticated) URL for the object; revocable. */
2506
+ create: (objectId: string, opts?: {
2507
+ ttl_seconds?: number;
2508
+ }) => Promise<{
2509
+ link_id: string;
2510
+ url: string;
2511
+ expires_at: string | null;
2512
+ }>;
2513
+ /** List an object's active (unrevoked, unexpired) shared links. */
2514
+ list: (objectId: string) => Promise<Array<{
2515
+ link_id: string;
2516
+ created_at: string;
2517
+ expires_at: string | null;
2518
+ }>>;
2519
+ revoke: (linkId: string) => Promise<void>;
2520
+ };
2521
+ };
2522
+ /**
2523
+ * Managed hybrid search (the `vector-search` feature): store documents → chunk →
2524
+ * embed → index, then retrieve top-k via hybrid (BM25 ∪ vector, RRF-fused),
2525
+ * vector, or keyword. The embedder is config: 'mock' (deterministic default),
2526
+ * 'byov' (you supply vectors), or a BYO-key provider (openai/cohere).
2527
+ */
2528
+ readonly search: {
2529
+ /** Define a collection. `dimensions` (and an `embed` override) pin its vector
2530
+ * width — one of 256|384|512|768|1024|1536|3072 (3072 = halfvec, sized for
2531
+ * OpenAI text-embedding-3-large); immutable after creation. */
2532
+ createCollection: (input: {
2533
+ collection: string;
2534
+ dimensions?: number;
2535
+ embed?: {
2536
+ provider?: "byov" | "mock" | "openai" | "cohere";
2537
+ model?: string;
2538
+ apiKeyRef?: string;
2539
+ dimensions?: number;
2540
+ };
2541
+ }) => Promise<SearchCollection>;
2542
+ /** List the tenant's collections (with per-collection document counts). */
2543
+ listCollections: () => Promise<Array<{
2544
+ collection: string;
2545
+ dimensions: number;
2546
+ backend: string;
2547
+ provider?: string;
2548
+ documents?: number;
2549
+ created_at?: string;
2550
+ }>>;
2551
+ /** Ingest a document: chunk → embed → index (202). BYOV skips embedding. */
2552
+ ingest: (collection: string, input: SearchIngestInput) => Promise<SearchIngestResult>;
2553
+ /** List indexed documents (keyset cursor, optional `user_id` filter). */
2554
+ listDocuments: (collection: string, q?: {
2555
+ user_id?: string;
2556
+ cursor?: string;
2557
+ limit?: number;
2558
+ }) => Promise<{
2559
+ documents: SearchDocument[];
2560
+ next_cursor: string | null;
2561
+ }>;
2562
+ /** De-index a document (+ invalidate the query cache). */
2563
+ deleteDocument: (collection: string, docId: string) => Promise<void>;
2564
+ /** Retrieve top-k. `mode` defaults to the collection's hybrid config; pass
2565
+ * `vector` to run a BYOV query (skips the embedder). `rerank` opts a single
2566
+ * request in/out of the configured BYO reranker (config `rerank` block;
2567
+ * fail-open — a provider fault returns the RRF order with reranked:false). */
2568
+ query: (collection: string, input: {
2569
+ query: string;
2570
+ top_k?: number;
2571
+ mode?: "hybrid" | "vector" | "keyword";
2572
+ filter?: Record<string, unknown>;
2573
+ rrf_k?: number;
2574
+ vector?: number[];
2575
+ rerank?: boolean;
2576
+ }) => Promise<SearchQueryResult>;
2577
+ /** Embedding-token + query + rerank counts since a timestamp (default 30 days). */
2578
+ usage: (q?: {
2579
+ since?: string;
2580
+ }) => Promise<SearchUsage>;
2581
+ /** List the configured cms auto-sync sources with their cursor state. */
2582
+ syncSources: () => Promise<SearchSyncSource[]>;
2583
+ /** Run one budgeted sync tick inline (409 when a tick is already running).
2584
+ * `sweep: true` runs the hard-delete reconcile leg instead. */
2585
+ syncRun: (sourceKey: string, opts?: {
2586
+ sweep?: boolean;
2587
+ }) => Promise<{
2588
+ source_key: string;
2589
+ phase: string;
2590
+ processed: number;
2591
+ deleted: number;
2592
+ skipped_unchanged: number;
2593
+ swept: boolean;
2594
+ cursor_updated_at: string | null;
2595
+ }>;
2596
+ /** Reset a source to a fresh backfill; `purge: true` also hard-deletes every
2597
+ * synced document (chunks + cache invalidation). */
2598
+ syncReset: (sourceKey: string, opts?: {
2599
+ purge?: boolean;
2600
+ }) => Promise<{
2601
+ source_key: string;
2602
+ phase: string;
2603
+ purged: number;
2604
+ }>;
2605
+ };
2606
+ /**
2607
+ * The AI substrate (the `ai` feature): store prompt TEMPLATES (config-as-code),
2608
+ * then generate (sync or streamed) and embed across providers. 'mock' is the
2609
+ * deterministic default; the real providers (openai/anthropic/gemini/azure/
2610
+ * openrouter) route via BYO keys in tenant_secrets. `images` on the generate
2611
+ * inputs takes up to 8 vision refs: a public https:// URL, a
2612
+ * data:image/...;base64 URL, or file:<object_id> (a files-feature object) —
2613
+ * fetched images are capped at 4 MiB each / 16 MiB total.
2614
+ */
2615
+ readonly ai: {
2616
+ templates: {
2617
+ /** Store a tenant-authored prompt template; versions are monotonic per name.
2618
+ * `{{var}}` placeholders are filled from `generate`'s `input`. */
2619
+ put: (input: {
2620
+ template: string;
2621
+ user: string;
2622
+ system?: string;
2623
+ schema?: Record<string, unknown>;
2624
+ }) => Promise<{
2625
+ template: string;
2626
+ version: number;
2627
+ }>;
2628
+ /** List stored templates (each name + its latest version). */
2629
+ list: () => Promise<Array<{
2630
+ name: string;
2631
+ latest_version: number;
2632
+ created_at: string;
2633
+ }>>;
2634
+ };
2635
+ /** Provider × capability matrix + the tenant's default provider (the set an
2636
+ * agent/dashboard reads to know which providers/features are configured). */
2637
+ capabilities: () => Promise<{
2638
+ default_provider: string;
2639
+ providers: Record<string, unknown>;
2640
+ }>;
2641
+ /** Synchronous generation: pass `template` (+ `input` vars) or a raw `prompt`. */
2642
+ generate: (input: {
2643
+ template?: string;
2644
+ template_version?: number;
2645
+ input?: Record<string, unknown>;
2646
+ prompt?: string;
2647
+ provider?: string;
2648
+ model?: string;
2649
+ max_tokens?: number;
2650
+ temperature?: number;
2651
+ tools?: Array<{
2652
+ name: string;
2653
+ description?: string;
2654
+ parameters?: Record<string, unknown>;
2655
+ }>;
2656
+ /** prior turns for the tool-calling round-trip (assistant tool_calls +
2657
+ * role:'tool' results) — vxil relays; you execute the tools. */
2658
+ messages?: AiChatMessage[];
2659
+ json_mode?: boolean;
2660
+ images?: string[];
2661
+ user_id?: string;
2662
+ }) => Promise<AiGeneration>;
2663
+ /** Streamed generation: returns the channel + connect token immediately; open
2664
+ * the realtime channel for AiStreamFrame frames (token/title, then a
2665
+ * terminal usage frame). `title:true` (or `title_template`) emits an early
2666
+ * seq'd title frame billed into the same generation. Long streams also get
2667
+ * live-only `token_expiring` events — see AiTokenExpiringEvent. */
2668
+ stream: (input: {
2669
+ template?: string;
2670
+ template_version?: number;
2671
+ input?: Record<string, unknown>;
2672
+ prompt?: string;
2673
+ provider?: string;
2674
+ model?: string;
2675
+ max_tokens?: number;
2676
+ temperature?: number;
2677
+ tools?: Array<{
2678
+ name: string;
2679
+ description?: string;
2680
+ parameters?: Record<string, unknown>;
2681
+ }>;
2682
+ messages?: AiChatMessage[];
2683
+ json_mode?: boolean;
2684
+ images?: string[];
2685
+ /** emit an early 'title' frame (stream-only; built-in instruction). */
2686
+ title?: boolean;
2687
+ /** name of a stored ai template authoring the title prompt (rendered with
2688
+ * {{prompt}}); overrides the built-in instruction (stream-only). */
2689
+ title_template?: string;
2690
+ user_id?: string;
2691
+ }) => Promise<AiStreamHandle>;
2692
+ /** Job-routed async generation (long/vision/batch): 202 + a jobs run drives
2693
+ * the provider call; the settled answer lands in the replay buffer at
2694
+ * `resume_path`. Reserve/settle + credit reversal-on-failure ride the run. */
2695
+ generateAsync: (input: {
2696
+ template?: string;
2697
+ template_version?: number;
2698
+ input?: Record<string, unknown>;
2699
+ prompt?: string;
2700
+ provider?: string;
2701
+ model?: string;
2702
+ max_tokens?: number;
2703
+ temperature?: number;
2704
+ tools?: Array<{
2705
+ name: string;
2706
+ description?: string;
2707
+ parameters?: Record<string, unknown>;
2708
+ }>;
2709
+ messages?: AiChatMessage[];
2710
+ json_mode?: boolean;
2711
+ images?: string[];
2712
+ user_id?: string;
2713
+ }) => Promise<AiJobHandle>;
2714
+ /** Re-mint a fresh connect token for a LIVE stream (a generation that
2715
+ * outlives the ≤300s realtime token TTL); reconnect with `?since=<seq>`. */
2716
+ remintToken: (generationId: string) => Promise<AiStreamToken>;
2717
+ /** Resume a streamed generation after a dropped socket: every recorded frame
2718
+ * with seq > `since` plus `done` (the §2a replay buffer — plain JSON, not
2719
+ * an SSE stream). The rag-native mirror is `rag.resume()`. */
2720
+ resume: (generationId: string, q?: {
2721
+ since?: number;
2722
+ }) => Promise<{
2723
+ generation_id: string;
2724
+ since: number;
2725
+ frames: Array<Record<string, unknown>>;
2726
+ done: boolean;
2727
+ max_seq: number;
2728
+ }>;
2729
+ /** Embed a batch of strings (1–256). */
2730
+ embed: (input: {
2731
+ input: string[];
2732
+ provider?: string;
2733
+ model?: string;
2734
+ user_id?: string;
2735
+ }) => Promise<AiEmbedResult>;
2736
+ /** Per-user token rollups (optionally scoped by `since`/`user_id`). */
2737
+ usage: (q?: {
2738
+ since?: string;
2739
+ user_id?: string;
2740
+ limit?: number;
2741
+ }) => Promise<AiUsageReport>;
2742
+ };
2743
+ /**
2744
+ * Grounded composition (the `rag` feature): retrieve → budget → generate → cite, as
2745
+ * one endpoint. It composes `search` (retrieval) and `ai` (generation); it owns
2746
+ * no datastore. `collection`/`template` fall back to the rag config defaults.
2747
+ */
2748
+ readonly rag: {
2749
+ /** Synchronous grounded answer (full text + citations + usage). */
2750
+ answer: (input: {
2751
+ query: string;
2752
+ collection?: string;
2753
+ template?: string;
2754
+ top_k?: number;
2755
+ filter?: Record<string, unknown>;
2756
+ mode?: "hybrid" | "vector" | "keyword";
2757
+ provider?: string;
2758
+ model?: string;
2759
+ user_id?: string;
2760
+ input?: Record<string, unknown>;
2761
+ }) => Promise<RagAnswer>;
2762
+ /** Streamed grounded answer: citations + the channel handle up front, tokens
2763
+ * over the realtime channel. */
2764
+ stream: (input: {
2765
+ query: string;
2766
+ collection?: string;
2767
+ template?: string;
2768
+ top_k?: number;
2769
+ filter?: Record<string, unknown>;
2770
+ mode?: "hybrid" | "vector" | "keyword";
2771
+ provider?: string;
2772
+ model?: string;
2773
+ user_id?: string;
2774
+ input?: Record<string, unknown>;
2775
+ }) => Promise<RagStreamHandle>;
2776
+ /** Retrieval-only grounding preview (rag.md §1c): the exact chunks `answer`
2777
+ * would ground on, with rerank + metadata boosts applied — no generation,
2778
+ * no token spend. `boosts`/`rerank`/`min_score` override the rag config. */
2779
+ search: (input: {
2780
+ query: string;
2781
+ collection?: string;
2782
+ top_k?: number;
2783
+ filter?: Record<string, unknown>;
2784
+ mode?: "hybrid" | "vector" | "keyword";
2785
+ rerank?: boolean;
2786
+ boosts?: Record<string, unknown>;
2787
+ min_score?: number;
2788
+ }) => Promise<RagSearchResult>;
2789
+ /** Ingest into the backing vector-search collection (chunk → embed → index). */
2790
+ ingest: (collection: string, input: SearchIngestInput) => Promise<SearchIngestResult>;
2791
+ /** Resume a streamed answer after a dropped socket: every recorded frame
2792
+ * with seq > `since` plus `done` — the RAG-native proxy of the ai replay
2793
+ * buffer, so a rag-scoped key suffices. */
2794
+ resume: (generationId: string, q?: {
2795
+ since?: number;
2796
+ }) => Promise<RagResumePage>;
2797
+ /** Aggregated retrieval (vector-search) + generation (ai) usage. */
2798
+ usage: (q?: {
2799
+ since?: string;
2800
+ }) => Promise<RagUsage>;
2801
+ };
2802
+ /**
2803
+ * Copilot — embeddable AI agents (the `copilot` feature). A thin COMPOSITION over
2804
+ * ai + vector-search + rag + mcp: each agent is CONFIG (persona + knowledge +
2805
+ * a curated action allowlist + guardrails). A turn retrieves, grounds, and
2806
+ * generates a cited answer; a WRITE action is PROPOSED, never auto-run — you
2807
+ * `confirm` it, and only then does the real internal route execute (with the
2808
+ * caller's own scopes, so nothing widens). The `mock` ai provider is the
2809
+ * zero-config default — the whole loop is exercisable without provider keys.
2810
+ */
2811
+ readonly copilot: {
2812
+ /** Conversations: open / list / read a chat session pinned to one agent. */
2813
+ conversations: {
2814
+ /** Open a session for `agentId` (agents are config; an unknown id 404s).
2815
+ * Omit `user_id` for a guest conversation. */
2816
+ create: (agentId: string, input?: {
2817
+ user_id?: string;
2818
+ title?: string;
2819
+ }) => Promise<CopilotConversation>;
2820
+ /** List an agent's conversations, newest-updated first (optionally scoped
2821
+ * to one end user). */
2822
+ list: (agentId: string, q?: {
2823
+ user_id?: string;
2824
+ limit?: number;
2825
+ }) => Promise<{
2826
+ conversations: CopilotConversation[];
2827
+ }>;
2828
+ /** A conversation row + the last N transcript messages (oldest-first). */
2829
+ get: (conversationId: string, q?: {
2830
+ limit?: number;
2831
+ }) => Promise<{
2832
+ conversation: CopilotConversation;
2833
+ messages: CopilotMessage[];
2834
+ }>;
2835
+ };
2836
+ /** Send a message — the agentic turn (retrieve → generate → tool_calls →
2837
+ * propose). Returns the synchronous `answer` + citations (+ a `proposal`
2838
+ * when the agent wants a write) AND the streaming handle. `conversation_id`
2839
+ * continues an existing session; omit it to open a new one. */
2840
+ send: (agentId: string, input: {
2841
+ message: string;
2842
+ conversation_id?: string;
2843
+ user_id?: string;
2844
+ title?: string;
2845
+ }) => Promise<CopilotTurn>;
2846
+ /** Resume a turn's streamed frames after a dropped socket: every recorded
2847
+ * frame with seq > `since` plus `done` — replays without re-running (or
2848
+ * re-billing) the turn. Defaults to the conversation's latest turn. */
2849
+ resume: (conversationId: string, q?: {
2850
+ since?: number;
2851
+ message_id?: string;
2852
+ }) => Promise<CopilotStreamPage>;
2853
+ /** Confirm a proposed action — the ONLY way a write executes. The client
2854
+ * sends just the path ids; everything that runs comes from the persisted
2855
+ * proposal. Idempotent: pass an Idempotency-Key to make a racing/retried
2856
+ * double-confirm a downstream no-op (defaults to the proposal message_id
2857
+ * server-side). Replaying returns `already:true`. */
2858
+ confirm: (conversationId: string, messageId: string, opts?: {
2859
+ idempotencyKey?: string;
2860
+ }) => Promise<CopilotConfirmResult>;
2861
+ /** Per-user token rollup across assistant turns. */
2862
+ usage: (q?: {
2863
+ since?: string;
2864
+ user_id?: string;
2865
+ limit?: number;
2866
+ }) => Promise<CopilotUsage>;
2867
+ };
2868
+ /**
2869
+ * Payments + the credits & entitlements LEDGER (the `payments` feature). vxil owns
2870
+ * the MECHANISM (a balance, a tier flag, an idempotent debit, a grant cron, a
2871
+ * product→credit translation, an at-most-once reversal); you own the POLICY
2872
+ * (pricing/packaging, what a "scan" costs, when to consume). The `mock` provider
2873
+ * is the zero-config default — the WHOLE ledger path (subscribe → entitlements →
2874
+ * consume → grant → reverse) is exercisable without real provider keys.
2875
+ */
2876
+ readonly payments: {
2877
+ /** Provider + ledger capability surface (+ the `missing` set an agent can act
2878
+ * on to recommend a provider). */
2879
+ capabilities: () => Promise<{
2880
+ provider: string;
2881
+ capabilities: string[];
2882
+ ledger_capabilities: string[];
2883
+ missing: string[];
2884
+ }>;
2885
+ /** The user's effective entitlement snapshot (the multi-sub tier fold):
2886
+ * tier + boolean entitlements + numeric quotas. */
2887
+ getEntitlements: (userId: string) => Promise<{
2888
+ user_id: string;
2889
+ tier: string;
2890
+ entitlements: string[];
2891
+ quotas: Record<string, number>;
2892
+ }>;
2893
+ /** Boolean gate: does the user hold `entitlement`? Returns granted + its
2894
+ * source (subscription/tier) + the resolved tier. */
2895
+ hasEntitlement: (userId: string, entitlement: string) => Promise<{
2896
+ granted: boolean;
2897
+ source: string | null;
2898
+ tier: string;
2899
+ }>;
2900
+ /** A single numeric quota for the user (e.g. `seats`, `api_calls`); null when
2901
+ * the resolved tier declares no such quota. Convenience over getEntitlements. */
2902
+ getQuota: (userId: string, quota: string) => Promise<number | null>;
2903
+ /** The credit balance for a credit type: balance/held/available (= balance −
2904
+ * held) + the current period_end. */
2905
+ getBalance: (userId: string, creditType: string) => Promise<{
2906
+ credit_type: string;
2907
+ balance: number;
2908
+ held: number;
2909
+ available: number;
2910
+ period_end: string | null;
2911
+ }>;
2912
+ /**
2913
+ * Debit credits — idempotent (Idempotency-Key REQUIRED) + balance-guarded
2914
+ * (no oversell). Pass `job_id` to make the debit PROVISIONAL (held, not yet
2915
+ * committed): a linked jobs run that terminally fails auto-refunds the hold,
2916
+ * a success settles it. Throws VxilError(402, 'INSUFFICIENT_CREDITS') when the
2917
+ * available balance can't cover `amount`.
2918
+ */
2919
+ consume: (input: {
2920
+ user_id: string;
2921
+ credit_type: string;
2922
+ amount: number;
2923
+ /** link this debit to a jobs run_id → provisional hold + auto-reverse. */
2924
+ job_id?: string;
2925
+ reason?: string;
2926
+ }, opts: {
2927
+ idempotencyKey: string;
2928
+ }) => Promise<{
2929
+ balance_after: number;
2930
+ ledger_entry_id: string | null;
2931
+ reversed: boolean;
2932
+ }>;
2933
+ /** Additive grant (recurring/top-up) — idempotent per Idempotency-Key. A grant
2934
+ * only ADDS; `amount` must be ≥ 1. */
2935
+ grant: (input: {
2936
+ user_id: string;
2937
+ credit_type: string;
2938
+ amount: number;
2939
+ period?: string;
2940
+ source?: string;
2941
+ }, opts: {
2942
+ idempotencyKey: string;
2943
+ }) => Promise<{
2944
+ balance_after: number;
2945
+ ledger_entry_id: string | null;
2946
+ }>;
2947
+ /** One-time grant idempotent on `grant_key` (at-most-once across ALL flows,
2948
+ * not just retries). A repeat with the same key → granted:false. */
2949
+ grantOnce: (input: {
2950
+ user_id: string;
2951
+ credit_type: string;
2952
+ amount: number;
2953
+ grant_key: string;
2954
+ }) => Promise<{
2955
+ balance_after: number;
2956
+ granted: boolean;
2957
+ }>;
2958
+ /** Recent ledger rows for the user (the audit/trail surface), newest first;
2959
+ * optionally scoped to one credit type. */
2960
+ usage: (q: {
2961
+ user_id: string;
2962
+ credit_type?: string;
2963
+ limit?: number;
2964
+ }) => Promise<{
2965
+ user_id: string;
2966
+ entries: Array<{
2967
+ ledger_entry_id: string;
2968
+ credit_type: string;
2969
+ delta: number;
2970
+ kind: string;
2971
+ state: string;
2972
+ outcome: string;
2973
+ balance_after: number | null;
2974
+ source: string | null;
2975
+ job_id: string | null;
2976
+ created_at: string;
2977
+ }>;
2978
+ }>;
2979
+ /** Start a subscription (mock = instant checkout; real providers redirect via
2980
+ * their own flow). `tier` keys the tierMap fold; `price_ref` is your catalog
2981
+ * ref. Returns the vxil subscription_id + provider sub id + status. */
2982
+ subscribe: (input: {
2983
+ user_id: string;
2984
+ price_ref: string;
2985
+ tier?: string;
2986
+ trial_days?: number;
2987
+ }) => Promise<{
2988
+ subscription_id: string;
2989
+ provider_sub_id: string;
2990
+ status: string;
2991
+ tier: string;
2992
+ }>;
2993
+ /** Cancel a subscription. `atPeriodEnd` true keeps entitlements until the
2994
+ * period closes; false revokes immediately and re-folds entitlements. */
2995
+ cancel: (subscriptionId: string, opts?: {
2996
+ atPeriodEnd?: boolean;
2997
+ }) => Promise<{
2998
+ subscription_id: string;
2999
+ status: string;
3000
+ cancel_at: string | null;
3001
+ }>;
3002
+ /** List subscriptions (the cancel enabler — discover the subscription_id);
3003
+ * optionally scoped to one user / status. */
3004
+ listSubscriptions: (q?: {
3005
+ user_id?: string;
3006
+ status?: string;
3007
+ }) => Promise<Array<Record<string, unknown>>>;
3008
+ /**
3009
+ * Create a hosted-checkout session (payments.md §3). Redirect the buyer to
3010
+ * the returned `url`; completion lands server-side via the provider webhook
3011
+ * (the matching session flips to completed, the charge/grant is folded).
3012
+ * Idempotency-Key REQUIRED — a retry replays the SAME session verbatim.
3013
+ */
3014
+ createCheckoutSession: (input: {
3015
+ user_id: string;
3016
+ line_items: Array<{
3017
+ price_ref: string;
3018
+ quantity?: number;
3019
+ /** amount-based providers (PayPal payment mode) need amount_cents. */
3020
+ amount_cents?: number;
3021
+ currency?: string;
3022
+ }>;
3023
+ mode: "subscription" | "payment";
3024
+ success_url: string;
3025
+ cancel_url: string;
3026
+ }, opts: {
3027
+ idempotencyKey: string;
3028
+ }) => Promise<{
3029
+ checkout_session_id: string;
3030
+ url: string;
3031
+ }>;
3032
+ /**
3033
+ * Refund a charge at-most-once (payments.md §6a). Omit `amount_cents` to
3034
+ * refund the full un-refunded remainder. Idempotency-Key REQUIRED — a retry
3035
+ * replays the recorded refund (never a second provider refund); a refund can
3036
+ * NEVER exceed the charge (422 refund_exceeds_charge).
3037
+ */
3038
+ createRefund: (input: {
3039
+ charge_id: string;
3040
+ amount_cents?: number;
3041
+ reason?: string;
3042
+ }, opts: {
3043
+ idempotencyKey: string;
3044
+ }) => Promise<{
3045
+ refund_id: string;
3046
+ status: "pending" | "succeeded" | "failed";
3047
+ amount_cents: number;
3048
+ }>;
3049
+ /** The provider-hosted billing-management portal URL for an end user (Stripe
3050
+ * billing portal; mock is deterministic; RC/Paddle/PayPal → 501). */
3051
+ getCustomerPortalUrl: (userId: string) => Promise<{
3052
+ url: string;
3053
+ }>;
3054
+ /** List charges newest-first (the refund enabler — discover the charge_id).
3055
+ * Filters: user_id, status, limit (clamped 1..100, default 50). */
3056
+ listCharges: (q?: {
3057
+ user_id?: string;
3058
+ status?: "succeeded" | "failed" | "refunded" | "partially_refunded" | "pending";
3059
+ limit?: number;
3060
+ }) => Promise<{
3061
+ charges: Array<{
3062
+ charge_id: string;
3063
+ end_user_id: string;
3064
+ provider: string;
3065
+ amount_cents: number;
3066
+ amount_refunded: number;
3067
+ currency: string;
3068
+ status: string;
3069
+ created_at: string;
3070
+ }>;
3071
+ }>;
3072
+ /** Provider webhook event log (payments.md §7 "Event log & replay"):
3073
+ * operator visibility over every delivery — incl. persisted signature
3074
+ * failures — plus an idempotent reprocess verb. Needs payments:read
3075
+ * (reprocess: payments:write). */
3076
+ webhookEvents: {
3077
+ /** List deliveries newest-first (keyset-paginated; pass `cursor` from a
3078
+ * prior page's next_cursor). List rows omit payload/raw_body. */
3079
+ list: (q?: {
3080
+ provider?: "mock" | "stripe" | "paddle" | "revenuecat" | "paypal";
3081
+ event_type?: string;
3082
+ outcome?: "received" | "processed" | "error" | "sig_failed" | "parse_failed" | "reprocessed";
3083
+ since?: string;
3084
+ cursor?: string;
3085
+ limit?: number;
3086
+ }) => Promise<{
3087
+ events: PaymentsWebhookEvent[];
3088
+ next_cursor: string | null;
3089
+ }>;
3090
+ /** Full delivery detail incl. the verified payload and (for failed
3091
+ * deliveries) the truncated raw body. */
3092
+ get: (eventId: string) => Promise<PaymentsWebhookEvent>;
3093
+ /** Re-run the fold from the STORED payload (idempotent — never
3094
+ * double-grants; 409 not_reprocessable for sig_failed/parse_failed). */
3095
+ reprocess: (eventId: string) => Promise<{
3096
+ event_id: string;
3097
+ reprocessed: boolean;
3098
+ event_type: string;
3099
+ }>;
3100
+ };
3101
+ };
3102
+ }
3103
+ export {};