@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.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/index.d.ts +3103 -0
- package/dist/index.js +1396 -0
- package/package.json +45 -0
package/dist/index.d.ts
ADDED
|
@@ -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 {};
|