@vxil/sdk 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -6
- package/dist/index.d.ts +759 -64
- package/dist/index.js +405 -270
- package/dist/qs.d.ts +15 -0
- package/dist/qs.js +48 -0
- package/dist/retry.d.ts +47 -0
- package/dist/retry.js +156 -0
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
// Every non-2xx response throws VxilError carrying the structured envelope
|
|
3
3
|
// (code/message/hint/fixUrl + request id) so callers — humans or agents —
|
|
4
4
|
// can self-correct.
|
|
5
|
+
/** The released API majors, as a CLOSED union — one per released major (docs/
|
|
6
|
+
* feature-versioning.md §3). `'v1'` is the only released major, so pinning
|
|
7
|
+
* anything else (`apiVersion: 'v2'`) is a COMPILE error until a v2 GAs and
|
|
8
|
+
* widens this union. Every path the SDK builds is major-versioned; the default
|
|
9
|
+
* is the compile-time constant `'v1'` (never a floating `latest` alias). */
|
|
10
|
+
import { qs } from './qs.js';
|
|
11
|
+
import { createTransport, parseRetryAfter } from './retry.js';
|
|
5
12
|
/** Rewrite a built `/v1/<ns>/…` path onto the version configured for its
|
|
6
13
|
* namespace: the per-namespace override wins, else the global default. PURE and
|
|
7
14
|
* exported so it is unit-testable with a hypothetical future major (the runtime
|
|
@@ -15,19 +22,28 @@ export function versionedPath(path, globalDefault, overrides) {
|
|
|
15
22
|
const version = overrides[ns] ?? globalDefault;
|
|
16
23
|
return `/${version}/${ns}${m[2]}`;
|
|
17
24
|
}
|
|
25
|
+
/** Every non-2xx answer is thrown as this class, carrying the structured error
|
|
26
|
+
* envelope. `status` is the HTTP status — or `0` when no response arrived
|
|
27
|
+
* (a `timeoutMs` timeout, `code: 'request_timeout'`). */
|
|
18
28
|
export class VxilError extends Error {
|
|
19
29
|
status;
|
|
20
30
|
code;
|
|
21
31
|
hint;
|
|
22
32
|
fixUrl;
|
|
23
33
|
requestId;
|
|
24
|
-
|
|
34
|
+
retryAfter;
|
|
35
|
+
constructor(status, code, message, hint, fixUrl, requestId,
|
|
36
|
+
/** Seconds the server asked the caller to wait — parsed from `Retry-After`
|
|
37
|
+
* (delta-seconds or an HTTP-date) when the answer carried it (a rate
|
|
38
|
+
* limit, a busy upstream); otherwise undefined. */
|
|
39
|
+
retryAfter) {
|
|
25
40
|
super(message);
|
|
26
41
|
this.status = status;
|
|
27
42
|
this.code = code;
|
|
28
43
|
this.hint = hint;
|
|
29
44
|
this.fixUrl = fixUrl;
|
|
30
45
|
this.requestId = requestId;
|
|
46
|
+
this.retryAfter = retryAfter;
|
|
31
47
|
this.name = 'VxilError';
|
|
32
48
|
}
|
|
33
49
|
}
|
|
@@ -54,17 +70,13 @@ function resolveBase(explicit) {
|
|
|
54
70
|
export function cmsPublicUrl(tenantId, collection, query, opts) {
|
|
55
71
|
const base = resolveBase(opts?.baseUrl);
|
|
56
72
|
const path = `/v1/cms/public/${encodeURIComponent(tenantId)}/${encodeURIComponent(collection)}`;
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (query?.cursor)
|
|
65
|
-
qs.set('cursor', query.cursor);
|
|
66
|
-
const s = qs.toString();
|
|
67
|
-
return `${base}${path}${s ? `?${s}` : ''}`;
|
|
73
|
+
const s = qs({
|
|
74
|
+
filter: query?.filter ? JSON.stringify(query.filter) : undefined,
|
|
75
|
+
sort: query?.sort || undefined,
|
|
76
|
+
limit: query?.limit,
|
|
77
|
+
cursor: query?.cursor || undefined,
|
|
78
|
+
});
|
|
79
|
+
return `${base}${path}${s}`;
|
|
68
80
|
}
|
|
69
81
|
/** Fetch a page of a collection's PUBLISHED items over the KEYLESS public lane
|
|
70
82
|
* (roadmap §4.4) — NO api key, no `Vxil` client, no auth of any kind. This is the
|
|
@@ -85,12 +97,12 @@ export async function listCmsPublic(tenantId, collection, query, opts) {
|
|
|
85
97
|
parsed = JSON.parse(text);
|
|
86
98
|
}
|
|
87
99
|
catch {
|
|
88
|
-
throw new VxilError(res.status, `http_${res.status}`, text.slice(0, 200));
|
|
100
|
+
throw new VxilError(res.status, `http_${res.status}`, text.slice(0, 200), undefined, undefined, undefined, parseRetryAfter(res.headers.get('retry-after')));
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
103
|
if (!res.ok || parsed.error) {
|
|
92
104
|
const e = parsed.error ?? { code: `http_${res.status}`, message: text.slice(0, 200) };
|
|
93
|
-
throw new VxilError(res.status, e.code, e.message, e.hint, e.fixUrl, parsed.meta?.request_id);
|
|
105
|
+
throw new VxilError(res.status, e.code, e.message, e.hint, e.fixUrl, parsed.meta?.request_id, parseRetryAfter(res.headers.get('retry-after')));
|
|
94
106
|
}
|
|
95
107
|
return parsed.data ?? { items: [], next_cursor: null };
|
|
96
108
|
}
|
|
@@ -103,6 +115,12 @@ export class Vxil {
|
|
|
103
115
|
/** The end-user session token threaded as `X-Vxil-End-User` when set (end-user
|
|
104
116
|
* mode). Undefined ⇒ no header ⇒ server-caller mode (unchanged). */
|
|
105
117
|
endUserToken;
|
|
118
|
+
/** The ONE path every request takes: retry + timeout + hooks around
|
|
119
|
+
* `fetchImpl`. With none of the three configured it is a single fetch. */
|
|
120
|
+
transport;
|
|
121
|
+
retryOpts;
|
|
122
|
+
timeoutMs;
|
|
123
|
+
hooks;
|
|
106
124
|
constructor(opts) {
|
|
107
125
|
this.key = opts.apiKey;
|
|
108
126
|
this.base = resolveBase(opts.baseUrl);
|
|
@@ -110,6 +128,16 @@ export class Vxil {
|
|
|
110
128
|
this.apiVersion = opts.apiVersion ?? 'v1';
|
|
111
129
|
this.apiVersions = opts.apiVersions ?? {};
|
|
112
130
|
this.endUserToken = opts.endUserToken;
|
|
131
|
+
this.retryOpts = opts.retry;
|
|
132
|
+
this.timeoutMs = opts.timeoutMs;
|
|
133
|
+
this.hooks = opts.hooks;
|
|
134
|
+
this.transport = createTransport({
|
|
135
|
+
fetch: this.fetchImpl,
|
|
136
|
+
...(opts.retry ? { retry: opts.retry } : {}),
|
|
137
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
138
|
+
...(opts.hooks ? { hooks: opts.hooks } : {}),
|
|
139
|
+
timeoutError: (ms) => new VxilError(0, 'request_timeout', `request timed out after ${ms}ms`),
|
|
140
|
+
});
|
|
113
141
|
}
|
|
114
142
|
/** The header bag every request layers on top of `authorization`: the
|
|
115
143
|
* `X-Vxil-End-User` session token in end-user mode, nothing in server mode.
|
|
@@ -120,9 +148,9 @@ export class Vxil {
|
|
|
120
148
|
/** Return a client that sends `X-Vxil-End-User: <token>` on every request —
|
|
121
149
|
* a per-call/scoped override of end-user mode over an otherwise server-mode
|
|
122
150
|
* client, mirroring how the edge threads the verified principal. The base
|
|
123
|
-
* URL, api key, fetch impl
|
|
124
|
-
* end-user token is (re)set. Pass a falsy token to get back a server-mode
|
|
125
|
-
* client (drops the header). See docs/
|
|
151
|
+
* URL, api key, fetch impl, version pins and the retry/timeout/hooks options
|
|
152
|
+
* are inherited unchanged; only the end-user token is (re)set. Pass a falsy token to get back a server-mode
|
|
153
|
+
* client (drops the header). See https://vxil.com/docs/guide/09-security-and-multitenancy. */
|
|
126
154
|
asEndUser(endUserToken) {
|
|
127
155
|
return new Vxil({
|
|
128
156
|
apiKey: this.key,
|
|
@@ -130,6 +158,9 @@ export class Vxil {
|
|
|
130
158
|
fetch: this.fetchImpl,
|
|
131
159
|
apiVersion: this.apiVersion,
|
|
132
160
|
apiVersions: this.apiVersions,
|
|
161
|
+
...(this.retryOpts ? { retry: this.retryOpts } : {}),
|
|
162
|
+
...(this.timeoutMs !== undefined ? { timeoutMs: this.timeoutMs } : {}),
|
|
163
|
+
...(this.hooks ? { hooks: this.hooks } : {}),
|
|
133
164
|
...(endUserToken ? { endUserToken } : {}),
|
|
134
165
|
});
|
|
135
166
|
}
|
|
@@ -188,26 +219,25 @@ export class Vxil {
|
|
|
188
219
|
// envelope), so bypass call()'s unwrap — read the body directly, like
|
|
189
220
|
// audit.export. Errors still surface as VxilError.
|
|
190
221
|
return async (payload) => {
|
|
191
|
-
const res = await this.
|
|
222
|
+
const { response: res, text } = await this.transport.send(`${this.base}${this.path(`/v1/fn/${encodeURIComponent(prop)}`)}`, {
|
|
192
223
|
method: 'POST',
|
|
193
224
|
headers: { authorization: `Bearer ${this.key}`, ...this.authHeaders(), 'content-type': 'application/json' },
|
|
194
225
|
body: JSON.stringify(payload ?? {}),
|
|
195
226
|
});
|
|
196
|
-
const text = await res.text();
|
|
197
227
|
if (!res.ok) {
|
|
198
228
|
let e = {};
|
|
199
229
|
try {
|
|
200
230
|
e = JSON.parse(text).error ?? {};
|
|
201
231
|
}
|
|
202
232
|
catch { /* non-JSON error body */ }
|
|
203
|
-
throw new VxilError(res.status, e.code ?? `http_${res.status}`, e.message ?? text.slice(0, 200));
|
|
233
|
+
throw new VxilError(res.status, e.code ?? `http_${res.status}`, e.message ?? text.slice(0, 200), undefined, undefined, undefined, parseRetryAfter(res.headers.get('retry-after')));
|
|
204
234
|
}
|
|
205
235
|
return text.length ? JSON.parse(text) : undefined;
|
|
206
236
|
};
|
|
207
237
|
},
|
|
208
238
|
});
|
|
209
239
|
async call(method, path, body, headers = {}) {
|
|
210
|
-
const res = await this.
|
|
240
|
+
const { response: res, text } = await this.transport.send(`${this.base}${this.path(path)}`, {
|
|
211
241
|
method,
|
|
212
242
|
headers: {
|
|
213
243
|
authorization: `Bearer ${this.key}`,
|
|
@@ -217,7 +247,6 @@ export class Vxil {
|
|
|
217
247
|
},
|
|
218
248
|
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
219
249
|
});
|
|
220
|
-
const text = await res.text();
|
|
221
250
|
// An empty body on a 2xx is a valid "no content" success (e.g. 204 from
|
|
222
251
|
// DELETE/markRead) — never an error. Only parse when there are bytes; a
|
|
223
252
|
// void-returning caller ignores `data` anyway. (audit #115)
|
|
@@ -231,16 +260,16 @@ export class Vxil {
|
|
|
231
260
|
// only surface the parse failure as an error on a non-2xx.
|
|
232
261
|
if (res.ok)
|
|
233
262
|
return { data: undefined, meta: { request_id: '' }, response: res };
|
|
234
|
-
throw new VxilError(res.status, `http_${res.status}`, text.slice(0, 200));
|
|
263
|
+
throw new VxilError(res.status, `http_${res.status}`, text.slice(0, 200), undefined, undefined, undefined, parseRetryAfter(res.headers.get('retry-after')));
|
|
235
264
|
}
|
|
236
265
|
}
|
|
237
266
|
else if (!res.ok) {
|
|
238
267
|
// Empty-body non-2xx: no envelope to surface, fall back to the status.
|
|
239
|
-
throw new VxilError(res.status, `http_${res.status}`, `request failed (${res.status})
|
|
268
|
+
throw new VxilError(res.status, `http_${res.status}`, `request failed (${res.status})`, undefined, undefined, undefined, parseRetryAfter(res.headers.get('retry-after')));
|
|
240
269
|
}
|
|
241
270
|
if (!res.ok || parsed.error) {
|
|
242
271
|
const e = parsed.error ?? { code: `http_${res.status}`, message: text.slice(0, 200) };
|
|
243
|
-
throw new VxilError(res.status, e.code, e.message, e.hint, e.fixUrl, parsed.meta?.request_id);
|
|
272
|
+
throw new VxilError(res.status, e.code, e.message, e.hint, e.fixUrl, parsed.meta?.request_id, parseRetryAfter(res.headers.get('retry-after')));
|
|
244
273
|
}
|
|
245
274
|
return { data: parsed.data, meta: parsed.meta ?? { request_id: '' }, response: res };
|
|
246
275
|
}
|
|
@@ -262,31 +291,36 @@ export class Vxil {
|
|
|
262
291
|
await this.call('DELETE', `/v1/users/${encodeURIComponent(id)}${opts?.erase ? '?erase=true' : ''}`);
|
|
263
292
|
},
|
|
264
293
|
list: async (q) => {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (q?.limit)
|
|
273
|
-
qs.set('limit', String(q.limit));
|
|
274
|
-
const s = qs.toString();
|
|
275
|
-
return (await this.call('GET', `/v1/users${s ? `?${s}` : ''}`)).data;
|
|
294
|
+
const s = qs({
|
|
295
|
+
email: q?.email || undefined,
|
|
296
|
+
q: q?.q || undefined,
|
|
297
|
+
cursor: q?.cursor || undefined,
|
|
298
|
+
limit: q?.limit || undefined,
|
|
299
|
+
});
|
|
300
|
+
return (await this.call('GET', `/v1/users${s}`)).data;
|
|
276
301
|
},
|
|
277
302
|
};
|
|
278
303
|
notifications = {
|
|
279
304
|
send: async (input, opts) => (await this.call('POST', '/v1/notifications/send', input, opts?.idempotencyKey ? { 'idempotency-key': opts.idempotencyKey } : {})).data,
|
|
305
|
+
/** Up to 100 sends in one call. Each item is INDEPENDENTLY atomic (recorded
|
|
306
|
+
* + enqueued, or neither) and `results` is index-aligned with `items`. */
|
|
307
|
+
sendBatch: async (items) => (await this.call('POST', '/v1/notifications/send/batch', { items })).data,
|
|
308
|
+
/** Render a template exactly as a send would, WITHOUT sending. `missing`
|
|
309
|
+
* lists the required data keys you left out. Needs `notifications:read`. */
|
|
310
|
+
preview: async (templateId, input) => (await this.call('POST', `/v1/notifications/templates/${encodeURIComponent(templateId)}/preview`, input ?? {})).data,
|
|
311
|
+
/** Read-only deliverability probe: is `config.fromEmail`'s domain verified
|
|
312
|
+
* at the provider? FAIL-SOFT — `verified: 'unknown'` whenever it cannot be
|
|
313
|
+
* determined (mock provider, no key, provider unreachable). */
|
|
314
|
+
senderDomain: async () => (await this.call('GET', '/v1/notifications/sender-domain')).data,
|
|
280
315
|
inbox: {
|
|
281
316
|
list: async (q) => {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return (await this.call('GET', `/v1/notifications/inbox?${qs.toString()}`)).data;
|
|
317
|
+
const s = qs({
|
|
318
|
+
user_id: q.user_id,
|
|
319
|
+
unread_only: q.unread_only || undefined,
|
|
320
|
+
cursor: q.cursor || undefined,
|
|
321
|
+
limit: q.limit || undefined,
|
|
322
|
+
});
|
|
323
|
+
return (await this.call('GET', `/v1/notifications/inbox${s}`)).data;
|
|
290
324
|
},
|
|
291
325
|
markRead: async (msgId, userId) => {
|
|
292
326
|
await this.call('POST', `/v1/notifications/inbox/${encodeURIComponent(msgId)}/read`, { user_id: userId });
|
|
@@ -294,15 +328,13 @@ export class Vxil {
|
|
|
294
328
|
markAllRead: async (userId) => (await this.call('POST', '/v1/notifications/inbox/read-all', { user_id: userId })).data.marked_read,
|
|
295
329
|
},
|
|
296
330
|
deliveries: async (q) => {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const s = qs.toString();
|
|
305
|
-
return (await this.call('GET', `/v1/notifications/deliveries${s ? `?${s}` : ''}`)).data.deliveries;
|
|
331
|
+
const s = qs({
|
|
332
|
+
user_id: q?.user_id || undefined,
|
|
333
|
+
status: q?.status || undefined,
|
|
334
|
+
engagement: q?.engagement || undefined,
|
|
335
|
+
limit: q?.limit || undefined,
|
|
336
|
+
});
|
|
337
|
+
return (await this.call('GET', `/v1/notifications/deliveries${s}`)).data.deliveries;
|
|
306
338
|
},
|
|
307
339
|
suppressions: {
|
|
308
340
|
list: async () => (await this.call('GET', '/v1/notifications/suppressions')).data.suppressions,
|
|
@@ -329,6 +361,19 @@ export class Vxil {
|
|
|
329
361
|
campaigns: {
|
|
330
362
|
create: async (input) => (await this.call('POST', '/v1/notifications/campaigns', input)).data,
|
|
331
363
|
list: async () => (await this.call('GET', '/v1/notifications/campaigns')).data.campaigns,
|
|
364
|
+
/** Stop a scheduled campaign from firing (the jobs cron is torn down). */
|
|
365
|
+
pause: async (campaignId) => (await this.call('POST', `/v1/notifications/campaigns/${encodeURIComponent(campaignId)}/pause`)).data,
|
|
366
|
+
/** Re-register the cron (back to `scheduled`, or `draft` with no cron). */
|
|
367
|
+
resume: async (campaignId) => (await this.call('POST', `/v1/notifications/campaigns/${encodeURIComponent(campaignId)}/resume`)).data,
|
|
368
|
+
/** Terminal stop — the cron is removed and the campaign cannot resume. */
|
|
369
|
+
cancel: async (campaignId) => (await this.call('POST', `/v1/notifications/campaigns/${encodeURIComponent(campaignId)}/cancel`)).data,
|
|
370
|
+
/** Soft-delete: hidden from the list, cron removed; the dedupe ledger
|
|
371
|
+
* survives so a re-created campaign cannot re-send to a served user. */
|
|
372
|
+
remove: async (campaignId) => (await this.call('DELETE', `/v1/notifications/campaigns/${encodeURIComponent(campaignId)}`)).data,
|
|
373
|
+
/** Push ONE audience page (≤ 1000 rows) into a campaign run — the shape a
|
|
374
|
+
* tenant audience callback uses. Omit `audience` for a campaign whose
|
|
375
|
+
* `audience_ref` is the built-in `users:all` selector. */
|
|
376
|
+
run: async (campaignId, audience) => (await this.call('POST', `/v1/notifications/campaigns/${encodeURIComponent(campaignId)}/run`, audience ? { audience } : {})).data,
|
|
332
377
|
},
|
|
333
378
|
};
|
|
334
379
|
config = {
|
|
@@ -357,7 +402,7 @@ export class Vxil {
|
|
|
357
402
|
* ADVISORY vxil plan — which features to enable, a materialized `config_draft`,
|
|
358
403
|
* and which truly-unique logic needs a tenant function. Deterministic + pure;
|
|
359
404
|
* it PROPOSES a plan, it never applies anything (review it, then push the
|
|
360
|
-
* config). Needs `features:read`. (POST /v1/plan; docs/
|
|
405
|
+
* config). Needs `features:read`. (POST /v1/plan; https://vxil.com/docs/guide/10-agents-and-mcp) */
|
|
361
406
|
planner = {
|
|
362
407
|
create: async (description, name) => (await this.call('POST', '/v1/plan', {
|
|
363
408
|
description,
|
|
@@ -366,35 +411,28 @@ export class Vxil {
|
|
|
366
411
|
};
|
|
367
412
|
audit = {
|
|
368
413
|
list: async (q) => {
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
qs.set('limit', String(q.limit));
|
|
376
|
-
const s = qs.toString();
|
|
377
|
-
return (await this.call('GET', `/v1/audit${s ? `?${s}` : ''}`)).data.events;
|
|
414
|
+
const s = qs({
|
|
415
|
+
since: q?.since || undefined,
|
|
416
|
+
cursor: q?.cursor || undefined,
|
|
417
|
+
limit: q?.limit || undefined,
|
|
418
|
+
});
|
|
419
|
+
return (await this.call('GET', `/v1/audit${s}`)).data.events;
|
|
378
420
|
},
|
|
379
421
|
/**
|
|
380
422
|
* NDJSON export (≤10k rows/call, id-ascending). Returns parsed events +
|
|
381
423
|
* next_after_id for resumption (null = done).
|
|
382
424
|
*/
|
|
383
425
|
export: async (q) => {
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
qs.set('limit', String(q.limit));
|
|
393
|
-
const s = qs.toString();
|
|
394
|
-
const res = await this.fetchImpl(`${this.base}${this.path(`/v1/audit/export${s ? `?${s}` : ''}`)}`, {
|
|
426
|
+
const s = qs({
|
|
427
|
+
since: q?.since || undefined,
|
|
428
|
+
until: q?.until || undefined,
|
|
429
|
+
after_id: q?.after_id || undefined,
|
|
430
|
+
limit: q?.limit || undefined,
|
|
431
|
+
});
|
|
432
|
+
const { response: res, text } = await this.transport.send(`${this.base}${this.path(`/v1/audit/export${s}`)}`, {
|
|
433
|
+
method: 'GET',
|
|
395
434
|
headers: { authorization: `Bearer ${this.key}` },
|
|
396
435
|
});
|
|
397
|
-
const text = await res.text();
|
|
398
436
|
if (!res.ok) {
|
|
399
437
|
// Preserve the structured error envelope on failure — the export path
|
|
400
438
|
// is hand-rolled (no this.call), so without this a 403/429 would lose
|
|
@@ -405,7 +443,7 @@ export class Vxil {
|
|
|
405
443
|
}
|
|
406
444
|
catch { /* non-JSON error body — fall through to the status default */ }
|
|
407
445
|
const e = env.error ?? { code: `http_${res.status}`, message: text.slice(0, 200) };
|
|
408
|
-
throw new VxilError(res.status, e.code, e.message, e.hint, e.fixUrl, env.meta?.request_id);
|
|
446
|
+
throw new VxilError(res.status, e.code, e.message, e.hint, e.fixUrl, env.meta?.request_id, parseRetryAfter(res.headers.get('retry-after')));
|
|
409
447
|
}
|
|
410
448
|
// Skip blank lines AND tolerate a single malformed NDJSON line rather
|
|
411
449
|
// than aborting the whole batch (e.g. a truncated final line on a large
|
|
@@ -459,17 +497,13 @@ export class Vxil {
|
|
|
459
497
|
* outstanding-holds ceiling → 429. */
|
|
460
498
|
generation: async (input) => (await this.call('POST', '/v1/jobs/generation', input)).data,
|
|
461
499
|
runs: async (q) => {
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (q?.limit)
|
|
470
|
-
qs.set('limit', String(q.limit));
|
|
471
|
-
const s = qs.toString();
|
|
472
|
-
return (await this.call('GET', `/v1/jobs/runs${s ? `?${s}` : ''}`)).data.runs;
|
|
500
|
+
const s = qs({
|
|
501
|
+
job_name: q?.job_name || undefined,
|
|
502
|
+
state: q?.state || undefined,
|
|
503
|
+
ids: q?.ids,
|
|
504
|
+
limit: q?.limit || undefined,
|
|
505
|
+
});
|
|
506
|
+
return (await this.call('GET', `/v1/jobs/runs${s}`)).data.runs;
|
|
473
507
|
},
|
|
474
508
|
run: async (runId) => (await this.call('GET', `/v1/jobs/runs/${encodeURIComponent(runId)}`)).data,
|
|
475
509
|
cancel: async (runId) => (await this.call('POST', `/v1/jobs/runs/${encodeURIComponent(runId)}/cancel`)).data,
|
|
@@ -510,6 +544,16 @@ export class Vxil {
|
|
|
510
544
|
},
|
|
511
545
|
},
|
|
512
546
|
};
|
|
547
|
+
/** vxil-auth. SCOPES (vxil.com/docs/guide/09-security-and-multitenancy): every flow here that
|
|
548
|
+
* obtains, renews, verifies or ends the caller's OWN session (signUp/signIn,
|
|
549
|
+
* magicLink, otp, anonymous, stepUp, oauth, sessions.refresh/revoke/verify,
|
|
550
|
+
* password reset) accepts the narrow `auth:signin` — the ONLY auth scope a
|
|
551
|
+
* key baked into a browser/mobile bundle should carry. `sessions.list` needs
|
|
552
|
+
* `auth:read`; `sessions.revokeById` and `users.erase` need `auth:write`
|
|
553
|
+
* (administrative — server keys only). In end-user mode (`endUserToken` /
|
|
554
|
+
* `asEndUser`) the administrative calls bind to the signed-in user: own
|
|
555
|
+
* sessions only, own devices only, erase self only. `auth:write` still
|
|
556
|
+
* satisfies every call. */
|
|
513
557
|
auth = {
|
|
514
558
|
signUp: async (input) => (await this.call('POST', '/v1/auth/sign-up', input)).data,
|
|
515
559
|
signIn: async (input) => (await this.call('POST', '/v1/auth/sign-in', input)).data,
|
|
@@ -523,9 +567,11 @@ export class Vxil {
|
|
|
523
567
|
* Server-side guessing budget (config otp.maxAttempts), single active code,
|
|
524
568
|
* resend cooldown (429 otp_rate_limited). */
|
|
525
569
|
otp: {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
570
|
+
/** `test_code` is present ONLY when the address matches auth config
|
|
571
|
+
* `otp.testRecipients` (store-review / CI accounts): no mail is sent and
|
|
572
|
+
* the code comes back instead. `captcha_token` is required when the
|
|
573
|
+
* tenant configured `security.captchaSecretRef`. */
|
|
574
|
+
request: async (input) => (await this.call('POST', '/v1/auth/otp/request', input)).data,
|
|
529
575
|
verify: async (input) => (await this.call('POST', '/v1/auth/otp/verify', input)).data,
|
|
530
576
|
},
|
|
531
577
|
/** Anonymous (guest) sessions: instant end-user + session (JWT carries
|
|
@@ -533,9 +579,12 @@ export class Vxil {
|
|
|
533
579
|
anonymous: {
|
|
534
580
|
signIn: async () => (await this.call('POST', '/v1/auth/anonymous/sign-in', {})).data,
|
|
535
581
|
link: {
|
|
536
|
-
request: async (input) =>
|
|
537
|
-
|
|
538
|
-
|
|
582
|
+
request: async (input) => (await this.call('POST', '/v1/auth/anonymous/link/request', input)).data,
|
|
583
|
+
/** On success the guest keeps its user id (`user_id` unchanged).
|
|
584
|
+
* If the claimed email ALREADY has an account, the guest is MERGED
|
|
585
|
+
* into it: `user_id` is the existing account, `merged: true`, and a
|
|
586
|
+
* fresh `session.token` (same session, re-signed for the merged
|
|
587
|
+
* identity — swap it client-side; other guest sessions are revoked). */
|
|
539
588
|
verify: async (input) => (await this.call('POST', '/v1/auth/anonymous/link/verify', input)).data,
|
|
540
589
|
},
|
|
541
590
|
},
|
|
@@ -543,9 +592,7 @@ export class Vxil {
|
|
|
543
592
|
* bearer token ROTATES (the returned session.token replaces the old one —
|
|
544
593
|
* swap it client-side) and the JWT gains an `elv` claim. */
|
|
545
594
|
stepUp: {
|
|
546
|
-
request: async (input) =>
|
|
547
|
-
await this.call('POST', '/v1/auth/step-up/request', input);
|
|
548
|
-
},
|
|
595
|
+
request: async (input) => (await this.call('POST', '/v1/auth/step-up/request', input)).data,
|
|
549
596
|
verify: async (input) => (await this.call('POST', '/v1/auth/step-up/verify', input)).data,
|
|
550
597
|
},
|
|
551
598
|
/** End-user administration (server/function surface, auth:write). */
|
|
@@ -554,8 +601,16 @@ export class Vxil {
|
|
|
554
601
|
* (email → tombstone, password/name/avatar cleared) and revoke every
|
|
555
602
|
* live session. Idempotent — a second call on an already-erased user is a
|
|
556
603
|
* no-op. Meant to be called from a "delete my account" server route or
|
|
557
|
-
* vxil function (which declares `auth:write`).
|
|
604
|
+
* vxil function (which declares `auth:write`). In END-USER mode the call
|
|
605
|
+
* is SELF-only: `userId` must be the signed-in user, any other id throws
|
|
606
|
+
* 403 `server_only` (a thin client can never erase someone else). */
|
|
558
607
|
erase: async (userId) => (await this.call('POST', `/v1/auth/users/${encodeURIComponent(userId)}/erase`, {})).data,
|
|
608
|
+
/** END-USER MODE ONLY (`endUserToken` / `asEndUser`): the signed-in user
|
|
609
|
+
* deep-merges a bounded `attributes` object into its OWN shared identity
|
|
610
|
+
* row (null deletes a key; ≤8 KB, ≤64 top-level keys, depth ≤8). Profile
|
|
611
|
+
* fields are not patchable here. In server mode the call throws 422
|
|
612
|
+
* `end_user_mode_required` — use `users.patch(id, …)` instead. */
|
|
613
|
+
patchMe: async (attributes) => (await this.call('PATCH', '/v1/auth/users/me', { attributes })).data,
|
|
559
614
|
},
|
|
560
615
|
sessions: {
|
|
561
616
|
verify: async (token) => (await this.call('POST', '/v1/auth/sessions/verify', { token })).data,
|
|
@@ -564,13 +619,24 @@ export class Vxil {
|
|
|
564
619
|
revoke: async (token) => {
|
|
565
620
|
await this.call('POST', '/v1/auth/sessions/revoke', { token });
|
|
566
621
|
},
|
|
622
|
+
/** "Sign out everywhere": revoke EVERY live session of a user in one call
|
|
623
|
+
* (one statement, one batched edge-cache write; each session's
|
|
624
|
+
* revoked_reason = 'revoke_all'). SERVER mode names the user (`userId`,
|
|
625
|
+
* scope `auth:write`); in END-USER mode the signed-in user's own sessions
|
|
626
|
+
* go and `userId` is ignored (scope `auth:signin` suffices). Idempotent:
|
|
627
|
+
* a user with no live sessions returns `revoked: 0`. */
|
|
628
|
+
revokeAll: async (userId) => (await this.call('POST', '/v1/auth/sessions/revoke-all', userId !== undefined ? { user_id: userId } : {})).data,
|
|
567
629
|
/** Force-revoke a SPECIFIC session by its `session_id` (from `list`) — the
|
|
568
630
|
* server-forced device-lockout path beyond the client-cooperative
|
|
569
|
-
* by-token `revoke`. Throws 404 when the id is unknown or already revoked
|
|
631
|
+
* by-token `revoke`. Throws 404 when the id is unknown or already revoked
|
|
632
|
+
* — or, in end-user mode, when it belongs to another user (a signed-in
|
|
633
|
+
* user can kick only their OWN devices). Scope `auth:write`. */
|
|
570
634
|
revokeById: async (sessionId) => {
|
|
571
635
|
await this.call('POST', `/v1/auth/sessions/${encodeURIComponent(sessionId)}/revoke`, {});
|
|
572
636
|
},
|
|
573
|
-
/** List a user's active sessions (the account "signed-in devices" surface).
|
|
637
|
+
/** List a user's active sessions (the account "signed-in devices" surface).
|
|
638
|
+
* Scope `auth:read`. In end-user mode the list is bound to the signed-in
|
|
639
|
+
* user regardless of `userId` (the verified principal wins). */
|
|
574
640
|
list: async (userId) => (await this.call('GET', `/v1/auth/sessions?user_id=${encodeURIComponent(userId)}`)).data.sessions,
|
|
575
641
|
},
|
|
576
642
|
password: {
|
|
@@ -582,13 +648,36 @@ export class Vxil {
|
|
|
582
648
|
await this.call('POST', '/v1/auth/password/reset/confirm', input);
|
|
583
649
|
},
|
|
584
650
|
},
|
|
651
|
+
/** Email verification (the flow behind auth config `emailVerification.required`,
|
|
652
|
+
* which makes password sign-in answer 403 `email_not_verified` until the
|
|
653
|
+
* address is confirmed). `request` always resolves (anti-enumeration —
|
|
654
|
+
* a link is mailed only to an existing, unverified user; `redirect_url`
|
|
655
|
+
* is your page that reads `?token=` and calls `confirm`). `confirm` is
|
|
656
|
+
* single-use and expires after 24h. */
|
|
657
|
+
emailVerification: {
|
|
658
|
+
request: async (input) => {
|
|
659
|
+
await this.call('POST', '/v1/auth/email/verify/request', input);
|
|
660
|
+
},
|
|
661
|
+
confirm: async (token) => (await this.call('POST', '/v1/auth/email/verify/confirm', { token })).data,
|
|
662
|
+
},
|
|
585
663
|
/** Social sign-in. The web `start`/`callback` flows are browser redirects
|
|
586
|
-
* (not JSON calls)
|
|
664
|
+
* (not JSON calls): send the browser to
|
|
665
|
+
* `GET /v1/auth/oauth/{provider}/start?redirect_uri=…` (server-side, with
|
|
666
|
+
* your key) and hand the returned `code`+`state` to `…/callback`; `native`
|
|
667
|
+
* is the mobile / broker token-exchange the SDK wraps. `oidc` is the
|
|
668
|
+
* tenant's generic OIDC / SSO issuer (auth config `providers.oidc` —
|
|
669
|
+
* Okta / Entra / Auth0 / any OpenID Connect IdP, or a SAML broker that
|
|
670
|
+
* speaks OIDC); it rides the same three routes. */
|
|
587
671
|
oauth: {
|
|
588
672
|
/** Native social sign-in: exchange a provider `id_token`/`access_token`
|
|
589
673
|
* for a vxil session (`linked` marks whether the user was created or
|
|
590
674
|
* matched to an existing identity). */
|
|
591
|
-
native: async (provider,
|
|
675
|
+
native: async (provider,
|
|
676
|
+
/** `anonymous_token`: the caller's CURRENT guest session bearer — the
|
|
677
|
+
* guest is PROMOTED to this identity (same user id, `linked: 'promoted'`)
|
|
678
|
+
* or, if the identity already has an account, MERGED into it
|
|
679
|
+
* (`linked: 'merged'`, `user_id` = that account). */
|
|
680
|
+
input) => (await this.call('POST', `/v1/auth/oauth/${encodeURIComponent(provider)}/native`, input)).data,
|
|
592
681
|
},
|
|
593
682
|
};
|
|
594
683
|
rateLimits = {
|
|
@@ -606,10 +695,8 @@ export class Vxil {
|
|
|
606
695
|
/** Per-policy usage analytics: hourly allowed/blocked buckets + top keys
|
|
607
696
|
* (last `hours`, clamped 1..48, default 24). */
|
|
608
697
|
analytics: async (input) => {
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
qs.set('hours', String(input.hours));
|
|
612
|
-
return (await this.call('GET', `/v1/rate-limits/analytics?${qs.toString()}`)).data;
|
|
698
|
+
const s = qs({ policy_id: input.policy_id, hours: input.hours });
|
|
699
|
+
return (await this.call('GET', `/v1/rate-limits/analytics${s}`)).data;
|
|
613
700
|
},
|
|
614
701
|
/**
|
|
615
702
|
* Consume budget. With behavior 'block' an exceeded check throws
|
|
@@ -638,6 +725,16 @@ export class Vxil {
|
|
|
638
725
|
addField: async (collection, field) => {
|
|
639
726
|
await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, field);
|
|
640
727
|
},
|
|
728
|
+
/** Set (or clear, with `null`/`[]`) a field's READ-ROLE gate (cms.md §18) —
|
|
729
|
+
* the same-type in-place alter on the fields route. Re-sends the field's
|
|
730
|
+
* `type` (required by the alter path); every OTHER attribute the field
|
|
731
|
+
* carries is re-sent from `rest`, because the alter overwrites the whole
|
|
732
|
+
* definition. In verified end-user mode a gated field is omitted from every
|
|
733
|
+
* read unless the session's verified roles intersect `roles`; server-caller
|
|
734
|
+
* reads and ALL writes are unaffected. */
|
|
735
|
+
setFieldReadRoles: async (collection, field, type, roles, rest = {}) => {
|
|
736
|
+
await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { ...rest, field, type, read_roles: roles ?? [] });
|
|
737
|
+
},
|
|
641
738
|
/** Set (or clear, with `null`) the collection's end-user owner-scope flag
|
|
642
739
|
* (design §5.1). Names an existing `string` field that holds the owner id. */
|
|
643
740
|
setOwnerField: async (collection, ownerField) => {
|
|
@@ -651,6 +748,13 @@ export class Vxil {
|
|
|
651
748
|
setPublic: async (collection, isPublic) => {
|
|
652
749
|
await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { public: isPublic });
|
|
653
750
|
},
|
|
751
|
+
/** Replace the collection's per-record ACTION list (cms.md §17) — the
|
|
752
|
+
* `{ actions }` fields-route meta-op; `[]` clears. Each action is ONE
|
|
753
|
+
* human-initiated step: the dashboard renders it as a button per record,
|
|
754
|
+
* and `items.runAction` invokes its deployed function. */
|
|
755
|
+
setActions: async (collection, actions) => {
|
|
756
|
+
await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { actions });
|
|
757
|
+
},
|
|
654
758
|
},
|
|
655
759
|
items: {
|
|
656
760
|
/** `lock` serializes same-key writers (per-tenant advisory lock); `guard`
|
|
@@ -668,17 +772,13 @@ export class Vxil {
|
|
|
668
772
|
* per-field `Filterable` unions on `vx.from(...).query` exclude.
|
|
669
773
|
*/
|
|
670
774
|
query: async (collection, q) => {
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if (q?.cursor)
|
|
679
|
-
qs.set('cursor', q.cursor);
|
|
680
|
-
const s = qs.toString();
|
|
681
|
-
return (await this.call('GET', `/v1/cms/items/${encodeURIComponent(collection)}${s ? `?${s}` : ''}`)).data;
|
|
775
|
+
const s = qs({
|
|
776
|
+
filter: q?.filter ? JSON.stringify(q.filter) : undefined,
|
|
777
|
+
sort: q?.sort || undefined,
|
|
778
|
+
limit: q?.limit || undefined,
|
|
779
|
+
cursor: q?.cursor || undefined,
|
|
780
|
+
});
|
|
781
|
+
return (await this.call('GET', `/v1/cms/items/${encodeURIComponent(collection)}${s}`)).data;
|
|
682
782
|
},
|
|
683
783
|
/** Merge-patch data keys; null clears a key. Concurrency opts (cms.md §9):
|
|
684
784
|
* `ifVersion` → If-Match CAS; `if` → bounded field precondition against
|
|
@@ -698,15 +798,29 @@ export class Vxil {
|
|
|
698
798
|
/** `{ count }` under the same bounded filter grammar (cms.md §9.4). 422
|
|
699
799
|
* count_unavailable_with_read_hooks on beforeRead-hooked collections. */
|
|
700
800
|
count: async (collection, filter) => {
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
qs.set('filter', JSON.stringify(filter));
|
|
704
|
-
return (await this.call('GET', `/v1/cms/items/${encodeURIComponent(collection)}?${qs.toString()}`)).data.count;
|
|
801
|
+
const s = qs({ count: 'true', filter: filter ? JSON.stringify(filter) : undefined });
|
|
802
|
+
return (await this.call('GET', `/v1/cms/items/${encodeURIComponent(collection)}${s}`)).data.count;
|
|
705
803
|
},
|
|
706
804
|
/** Returns the cascade tally (cms.md §11); `ifVersion` rides If-Match and
|
|
707
805
|
* a conflict aborts BEFORE any cascade side-effect. */
|
|
708
806
|
delete: async (collection, itemId, opts) => (await this.call('DELETE', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}`, undefined, opts?.ifVersion !== undefined ? { 'if-match': String(opts.ifVersion) } : {})).data,
|
|
709
807
|
publish: async (collection, itemId) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}/publish`)).data,
|
|
808
|
+
/** P1-12 (cms.md §11.1): the BOUNDED reverse read — which live items
|
|
809
|
+
* reference this one, through which relation field. Owner-scoped in
|
|
810
|
+
* end-user mode like `get`. `count` is the page returned (≤ limit, max
|
|
811
|
+
* 100), never a total; `has_more` says the cap was hit. A DELETE refused
|
|
812
|
+
* with 409 `referenced` (an `on_delete: 'restrict'` edge) names the same
|
|
813
|
+
* refs in its `referencing` field. */
|
|
814
|
+
backlinks: async (collection, itemId, opts) => {
|
|
815
|
+
const qs = opts?.limit !== undefined ? `?limit=${encodeURIComponent(String(opts.limit))}` : '';
|
|
816
|
+
return (await this.call('GET', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}/backlinks${qs}`)).data;
|
|
817
|
+
},
|
|
818
|
+
/** P1-7 (cms.md §17): run ONE declared per-record action — invokes the
|
|
819
|
+
* action's deployed function with `{ collection, item_id, action, actor,
|
|
820
|
+
* item }` and returns its result. 404 when the key is not declared;
|
|
821
|
+
* 502 `action_failed` (with `upstream.code` = the function's error
|
|
822
|
+
* class) when the function fails. Requires cms:write. */
|
|
823
|
+
runAction: async (collection, itemId, key) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}/actions/${encodeURIComponent(key)}`)).data,
|
|
710
824
|
/** cms-rel B2 (cms.md §12): bounded group-by aggregate. fns count|sum|
|
|
711
825
|
* min|max|avg over index-slot-bound fields; groupBy ≤2 slot-bound
|
|
712
826
|
* fields; filter = the full query DSL incl. ONE-hop dotted join terms
|
|
@@ -735,12 +849,8 @@ export class Vxil {
|
|
|
735
849
|
comments = {
|
|
736
850
|
create: async (input) => (await this.call('POST', '/v1/comments', input)).data,
|
|
737
851
|
list: async (q) => {
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
qs.set('cursor', q.cursor);
|
|
741
|
-
if (q.limit)
|
|
742
|
-
qs.set('limit', String(q.limit));
|
|
743
|
-
return (await this.call('GET', `/v1/comments?${qs.toString()}`)).data;
|
|
852
|
+
const s = qs({ topic: q.topic, cursor: q.cursor || undefined, limit: q.limit || undefined });
|
|
853
|
+
return (await this.call('GET', `/v1/comments${s}`)).data;
|
|
744
854
|
},
|
|
745
855
|
/** Author edit, allowed within the tenant's editWindowMinutes. */
|
|
746
856
|
edit: async (commentId, input) => {
|
|
@@ -755,17 +865,13 @@ export class Vxil {
|
|
|
755
865
|
/** Cross-topic recent-comments feed (tenant-wide, newest-first) — the
|
|
756
866
|
* dashboard "recent activity" surface. Keyset cursor + optional filters. */
|
|
757
867
|
recent: async (q) => {
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
if (q?.limit !== undefined)
|
|
766
|
-
qs.set('limit', String(q.limit));
|
|
767
|
-
const s = qs.toString();
|
|
768
|
-
return (await this.call('GET', `/v1/comments/recent${s ? `?${s}` : ''}`)).data;
|
|
868
|
+
const s = qs({
|
|
869
|
+
topic_prefix: q?.topic_prefix || undefined,
|
|
870
|
+
author_id: q?.author_id || undefined,
|
|
871
|
+
cursor: q?.cursor || undefined,
|
|
872
|
+
limit: q?.limit,
|
|
873
|
+
});
|
|
874
|
+
return (await this.call('GET', `/v1/comments/recent${s}`)).data;
|
|
769
875
|
},
|
|
770
876
|
};
|
|
771
877
|
/**
|
|
@@ -774,7 +880,7 @@ export class Vxil {
|
|
|
774
880
|
* only the envelope: membership, read-cursors, mute, and a directed block
|
|
775
881
|
* list; the messages themselves are `comments` rows on the
|
|
776
882
|
* `dm:<conversation_id>` topic. Gated by its own `dm` config, independent of
|
|
777
|
-
* comments. See docs/
|
|
883
|
+
* comments. See vxil.com/docs/guide/06-feature-catalog.
|
|
778
884
|
*/
|
|
779
885
|
dm = {
|
|
780
886
|
/** Read the resolved DM config (defaults merged with the stored partial). */
|
|
@@ -793,12 +899,8 @@ export class Vxil {
|
|
|
793
899
|
/** Read a conversation's messages (an authz'd participant only; messages
|
|
794
900
|
* from authors the viewer has blocked are filtered out). Keyset cursor. */
|
|
795
901
|
messages: async (conversationId, q) => {
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
qs.set('cursor', q.cursor);
|
|
799
|
-
if (q.limit !== undefined)
|
|
800
|
-
qs.set('limit', String(q.limit));
|
|
801
|
-
return (await this.call('GET', `/v1/dm/conversations/${encodeURIComponent(conversationId)}/messages?${qs.toString()}`)).data;
|
|
902
|
+
const s = qs({ user_id: q.user_id, cursor: q.cursor || undefined, limit: q.limit });
|
|
903
|
+
return (await this.call('GET', `/v1/dm/conversations/${encodeURIComponent(conversationId)}/messages${s}`)).data;
|
|
802
904
|
},
|
|
803
905
|
},
|
|
804
906
|
/** Send a message (an authz'd participant). Stored as a comment on the
|
|
@@ -841,21 +943,15 @@ export class Vxil {
|
|
|
841
943
|
* feed; `notificationState` filters to unseen | unread | all.
|
|
842
944
|
*/
|
|
843
945
|
read: async (group, feedId, q) => {
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
if (q?.markSeen)
|
|
854
|
-
qs.set('mark_seen', 'true');
|
|
855
|
-
if (q?.markRead)
|
|
856
|
-
qs.set('mark_read', 'true');
|
|
857
|
-
const s = qs.toString();
|
|
858
|
-
return (await this.call('GET', `/v1/feeds/${encodeURIComponent(group)}/${encodeURIComponent(feedId)}${s ? `?${s}` : ''}`)).data;
|
|
946
|
+
const s = qs({
|
|
947
|
+
limit: q?.limit,
|
|
948
|
+
id_lt: q?.id_lt || undefined,
|
|
949
|
+
id_gt: q?.id_gt || undefined,
|
|
950
|
+
notification_state: q?.notificationState || undefined,
|
|
951
|
+
mark_seen: q?.markSeen || undefined,
|
|
952
|
+
mark_read: q?.markRead || undefined,
|
|
953
|
+
});
|
|
954
|
+
return (await this.call('GET', `/v1/feeds/${encodeURIComponent(group)}/${encodeURIComponent(feedId)}${s}`)).data;
|
|
859
955
|
},
|
|
860
956
|
/** Remove an activity by id (or by foreign_id); tombstones + un-fans-out. */
|
|
861
957
|
removeActivity: async (group, feedId, ref) => {
|
|
@@ -886,13 +982,8 @@ export class Vxil {
|
|
|
886
982
|
/** Who a feed FOLLOWS (`following[]` + `following_count`) plus the true
|
|
887
983
|
* `followers_count` (who follows this feed). Keyset cursor via `after`. */
|
|
888
984
|
listFollows: async (group, feedId, q) => {
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
qs.set('limit', String(q.limit));
|
|
892
|
-
if (q?.after)
|
|
893
|
-
qs.set('after', q.after);
|
|
894
|
-
const s = qs.toString();
|
|
895
|
-
return (await this.call('GET', `/v1/feeds/${encodeURIComponent(group)}/${encodeURIComponent(feedId)}/follows${s ? `?${s}` : ''}`)).data;
|
|
985
|
+
const s = qs({ limit: q?.limit, after: q?.after || undefined });
|
|
986
|
+
return (await this.call('GET', `/v1/feeds/${encodeURIComponent(group)}/${encodeURIComponent(feedId)}/follows${s}`)).data;
|
|
896
987
|
},
|
|
897
988
|
/**
|
|
898
989
|
* Block (two-way fan-out suppression, removes existing rows) or mute (one-way
|
|
@@ -968,17 +1059,13 @@ export class Vxil {
|
|
|
968
1059
|
},
|
|
969
1060
|
events: {
|
|
970
1061
|
list: async (q) => {
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
if (q?.limit)
|
|
979
|
-
qs.set('limit', String(q.limit));
|
|
980
|
-
const s = qs.toString();
|
|
981
|
-
return (await this.call('GET', `/v1/webhooks/events${s ? `?${s}` : ''}`)).data;
|
|
1062
|
+
const s = qs({
|
|
1063
|
+
source_id: q?.source_id || undefined,
|
|
1064
|
+
status: q?.status || undefined,
|
|
1065
|
+
cursor: q?.cursor || undefined,
|
|
1066
|
+
limit: q?.limit || undefined,
|
|
1067
|
+
});
|
|
1068
|
+
return (await this.call('GET', `/v1/webhooks/events${s}`)).data;
|
|
982
1069
|
},
|
|
983
1070
|
replay: async (eventId) => {
|
|
984
1071
|
await this.call('POST', `/v1/webhooks/events/${encodeURIComponent(eventId)}/replay`);
|
|
@@ -987,6 +1074,12 @@ export class Vxil {
|
|
|
987
1074
|
* every jobs run this event's forwards created. */
|
|
988
1075
|
runs: async (eventId) => (await this.call('GET', `/v1/webhooks/events/${encodeURIComponent(eventId)}/runs`)).data,
|
|
989
1076
|
},
|
|
1077
|
+
/** Every audit event the platform can emit, with the subscribable prefixes
|
|
1078
|
+
* and their counts — the discovery surface for `subscribe({ event_prefixes })`.
|
|
1079
|
+
* Static reference data (CI-generated from the emitters), identical for every
|
|
1080
|
+
* tenant. `level` is the event CLASS ('lifecycle' | 'failure'), not the
|
|
1081
|
+
* payload's 'info'|'warn'|'error' severity key. */
|
|
1082
|
+
eventCatalog: async () => (await this.call('GET', '/v1/webhooks/events/catalog')).data,
|
|
990
1083
|
};
|
|
991
1084
|
orgs = {
|
|
992
1085
|
create: async (input) => (await this.call('POST', '/v1/orgs', input)).data,
|
|
@@ -1002,10 +1095,8 @@ export class Vxil {
|
|
|
1002
1095
|
* `opts.resource` to additionally consult per-resource ACL grants; the
|
|
1003
1096
|
* response `source` marks whether a grant came from the role lattice or an ACL. */
|
|
1004
1097
|
check: async (orgId, userId, permission, opts) => {
|
|
1005
|
-
const
|
|
1006
|
-
|
|
1007
|
-
qs.set('resource', opts.resource);
|
|
1008
|
-
return (await this.call('GET', `/v1/orgs/${encodeURIComponent(orgId)}/check?${qs.toString()}`)).data;
|
|
1098
|
+
const s = qs({ user_id: userId, permission, resource: opts?.resource || undefined });
|
|
1099
|
+
return (await this.call('GET', `/v1/orgs/${encodeURIComponent(orgId)}/check${s}`)).data;
|
|
1009
1100
|
},
|
|
1010
1101
|
/** The active-org snapshot auth embeds in session JWTs when the tenant
|
|
1011
1102
|
* enables auth config `orgClaims` (most-recent membership by joined_at).
|
|
@@ -1100,15 +1191,12 @@ export class Vxil {
|
|
|
1100
1191
|
complete: async (objectId) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/complete`)).data,
|
|
1101
1192
|
downloadUrl: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/download-url`)).data.download_url,
|
|
1102
1193
|
list: async (q) => {
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
qs.set('limit', String(q.limit));
|
|
1110
|
-
const s = qs.toString();
|
|
1111
|
-
return (await this.call('GET', `/v1/files${s ? `?${s}` : ''}`)).data;
|
|
1194
|
+
const s = qs({
|
|
1195
|
+
user_id: q?.user_id || undefined,
|
|
1196
|
+
cursor: q?.cursor || undefined,
|
|
1197
|
+
limit: q?.limit || undefined,
|
|
1198
|
+
});
|
|
1199
|
+
return (await this.call('GET', `/v1/files${s}`)).data;
|
|
1112
1200
|
},
|
|
1113
1201
|
/** Soft delete; bytes are hard-deleted 30 days later. */
|
|
1114
1202
|
delete: async (objectId) => {
|
|
@@ -1126,9 +1214,12 @@ export class Vxil {
|
|
|
1126
1214
|
* (else a future auto-delete after the given seconds; minimum 60). */
|
|
1127
1215
|
setTtl: async (objectId, expiresInSeconds) => (await this.call('PUT', `/v1/files/${encodeURIComponent(objectId)}/ttl`, { expiresInSeconds })).data,
|
|
1128
1216
|
sharedLinks: {
|
|
1129
|
-
/** Public (unauthenticated) URL for the object; revocable.
|
|
1217
|
+
/** Public (unauthenticated) URL for the object; revocable. Optional
|
|
1218
|
+
* `max_downloads` (1 = one-time link) and an absolute `expires_at`; the
|
|
1219
|
+
* public resolver answers 410 `link_exhausted` / `link_expired` past
|
|
1220
|
+
* either bound. */
|
|
1130
1221
|
create: async (objectId, opts) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/shared-links`, opts ?? {})).data,
|
|
1131
|
-
/** List an object's active (unrevoked, unexpired) shared links. */
|
|
1222
|
+
/** List an object's active (unrevoked, unexpired, unexhausted) shared links. */
|
|
1132
1223
|
list: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/shared-links`)).data.links,
|
|
1133
1224
|
revoke: async (linkId) => {
|
|
1134
1225
|
await this.call('DELETE', `/v1/files/shared-links/${encodeURIComponent(linkId)}`);
|
|
@@ -1152,15 +1243,12 @@ export class Vxil {
|
|
|
1152
1243
|
ingest: async (collection, input) => (await this.call('POST', `/v1/search/${encodeURIComponent(collection)}/documents`, input)).data,
|
|
1153
1244
|
/** List indexed documents (keyset cursor, optional `user_id` filter). */
|
|
1154
1245
|
listDocuments: async (collection, q) => {
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
qs.set('limit', String(q.limit));
|
|
1162
|
-
const s = qs.toString();
|
|
1163
|
-
return (await this.call('GET', `/v1/search/${encodeURIComponent(collection)}/documents${s ? `?${s}` : ''}`)).data;
|
|
1246
|
+
const s = qs({
|
|
1247
|
+
user_id: q?.user_id || undefined,
|
|
1248
|
+
cursor: q?.cursor || undefined,
|
|
1249
|
+
limit: q?.limit || undefined,
|
|
1250
|
+
});
|
|
1251
|
+
return (await this.call('GET', `/v1/search/${encodeURIComponent(collection)}/documents${s}`)).data;
|
|
1164
1252
|
},
|
|
1165
1253
|
/** De-index a document (+ invalidate the query cache). */
|
|
1166
1254
|
deleteDocument: async (collection, docId) => {
|
|
@@ -1230,19 +1318,34 @@ export class Vxil {
|
|
|
1230
1318
|
const s = q?.since != null ? `?since=${q.since}` : '';
|
|
1231
1319
|
return (await this.call('GET', `/v1/ai/generations/${encodeURIComponent(generationId)}/stream${s}`)).data;
|
|
1232
1320
|
},
|
|
1321
|
+
/** Classify an input into one of YOUR labels (or, with `multi:true`, every
|
|
1322
|
+
* label that applies) — a schema-forced verdict over the generate path:
|
|
1323
|
+
* the allowed labels ride the structured-output schema as an enum, so the
|
|
1324
|
+
* model cannot answer off-list; the reply is `{ label | labels,
|
|
1325
|
+
* confidence, rationale }`. Same metering/cache/provider rules as
|
|
1326
|
+
* `generate` (a still-invalid verdict is a billed 422
|
|
1327
|
+
* output_schema_mismatch). Caps: 2–64 labels (≤64 chars, unique), input
|
|
1328
|
+
* ≤32k chars (a string or ordered `{ text }` parts joined into ONE
|
|
1329
|
+
* input), rubric ≤4k, ≤16 examples. */
|
|
1330
|
+
classify: async (input) => (await this.call('POST', '/v1/ai/classify', input)).data,
|
|
1331
|
+
/** Grade a candidate response (or rank up to 8) against YOUR criteria —
|
|
1332
|
+
* a schema-forced integer score inside `scale` (default 0..10) plus
|
|
1333
|
+
* `pass|fail` for one candidate, or `scores[]` + `best` (+ `verdict:'tie'`)
|
|
1334
|
+
* for several — `candidates` always yields `scores`/`best`, even at
|
|
1335
|
+
* length 1. Same metering/cache/provider rules as `generate`. Caps:
|
|
1336
|
+
* input ≤16k chars, candidates ≤8 × 4k chars, criteria ≤16 items / 4k
|
|
1337
|
+
* chars, scale span ≤100. */
|
|
1338
|
+
judge: async (input) => (await this.call('POST', '/v1/ai/judge', input)).data,
|
|
1233
1339
|
/** Embed a batch of strings (1–256). */
|
|
1234
1340
|
embed: async (input) => (await this.call('POST', '/v1/ai/embed', input)).data,
|
|
1235
1341
|
/** Per-user token rollups (optionally scoped by `since`/`user_id`). */
|
|
1236
1342
|
usage: async (q) => {
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
qs.set('limit', String(q.limit));
|
|
1244
|
-
const s = qs.toString();
|
|
1245
|
-
return (await this.call('GET', `/v1/ai/usage${s ? `?${s}` : ''}`)).data;
|
|
1343
|
+
const s = qs({
|
|
1344
|
+
since: q?.since || undefined,
|
|
1345
|
+
user_id: q?.user_id || undefined,
|
|
1346
|
+
limit: q?.limit || undefined,
|
|
1347
|
+
});
|
|
1348
|
+
return (await this.call('GET', `/v1/ai/usage${s}`)).data;
|
|
1246
1349
|
},
|
|
1247
1350
|
};
|
|
1248
1351
|
/**
|
|
@@ -1293,12 +1396,7 @@ export class Vxil {
|
|
|
1293
1396
|
/** List an agent's conversations, newest-updated first (optionally scoped
|
|
1294
1397
|
* to one end user). */
|
|
1295
1398
|
list: async (agentId, q) => {
|
|
1296
|
-
const
|
|
1297
|
-
if (q?.user_id)
|
|
1298
|
-
qs.set('user_id', q.user_id);
|
|
1299
|
-
if (q?.limit)
|
|
1300
|
-
qs.set('limit', String(q.limit));
|
|
1301
|
-
const s = qs.size ? `?${qs.toString()}` : '';
|
|
1399
|
+
const s = qs({ user_id: q?.user_id || undefined, limit: q?.limit || undefined });
|
|
1302
1400
|
return (await this.call('GET', `/v1/copilot/${encodeURIComponent(agentId)}/conversations${s}`)).data;
|
|
1303
1401
|
},
|
|
1304
1402
|
/** A conversation row + the last N transcript messages (oldest-first). */
|
|
@@ -1316,11 +1414,8 @@ export class Vxil {
|
|
|
1316
1414
|
* frame with seq > `since` plus `done` — replays without re-running (or
|
|
1317
1415
|
* re-billing) the turn. Defaults to the conversation's latest turn. */
|
|
1318
1416
|
resume: async (conversationId, q) => {
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
if (q?.message_id)
|
|
1322
|
-
qs.set('message_id', q.message_id);
|
|
1323
|
-
return (await this.call('GET', `/v1/copilot/conversations/${encodeURIComponent(conversationId)}/stream?${qs.toString()}`)).data;
|
|
1417
|
+
const s = qs({ since: q?.since ?? 0, message_id: q?.message_id || undefined });
|
|
1418
|
+
return (await this.call('GET', `/v1/copilot/conversations/${encodeURIComponent(conversationId)}/stream${s}`)).data;
|
|
1324
1419
|
},
|
|
1325
1420
|
/** Confirm a proposed action — the ONLY way a write executes. The client
|
|
1326
1421
|
* sends just the path ids; everything that runs comes from the persisted
|
|
@@ -1330,14 +1425,11 @@ export class Vxil {
|
|
|
1330
1425
|
confirm: async (conversationId, messageId, opts) => (await this.call('POST', `/v1/copilot/conversations/${encodeURIComponent(conversationId)}/actions/${encodeURIComponent(messageId)}/confirm`, undefined, opts?.idempotencyKey ? { 'idempotency-key': opts.idempotencyKey } : {})).data,
|
|
1331
1426
|
/** Per-user token rollup across assistant turns. */
|
|
1332
1427
|
usage: async (q) => {
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
if (q?.limit)
|
|
1339
|
-
qs.set('limit', String(q.limit));
|
|
1340
|
-
const s = qs.size ? `?${qs.toString()}` : '';
|
|
1428
|
+
const s = qs({
|
|
1429
|
+
since: q?.since || undefined,
|
|
1430
|
+
user_id: q?.user_id || undefined,
|
|
1431
|
+
limit: q?.limit || undefined,
|
|
1432
|
+
});
|
|
1341
1433
|
return (await this.call('GET', `/v1/copilot/usage${s}`)).data;
|
|
1342
1434
|
},
|
|
1343
1435
|
};
|
|
@@ -1351,13 +1443,30 @@ export class Vxil {
|
|
|
1351
1443
|
*/
|
|
1352
1444
|
payments = {
|
|
1353
1445
|
/** Provider + ledger capability surface (+ the `missing` set an agent can act
|
|
1354
|
-
* on to recommend a provider).
|
|
1446
|
+
* on to recommend a provider). `client` is the RevenueCat tenant's PUBLIC
|
|
1447
|
+
* SDK handout (project id + public SDK key — never the secret key); null
|
|
1448
|
+
* for every other provider. */
|
|
1355
1449
|
capabilities: async () => (await this.call('GET', '/v1/payments/capabilities')).data,
|
|
1356
1450
|
/** The user's effective entitlement snapshot (the multi-sub tier fold):
|
|
1357
|
-
* tier + boolean entitlements + numeric quotas
|
|
1358
|
-
|
|
1451
|
+
* tier + boolean entitlements + numeric quotas — plus the access window
|
|
1452
|
+
* (`since` / `until` = the winning subscription's period end, or end +
|
|
1453
|
+
* grace when it is past_due), `as_of` (server time), the `environment`
|
|
1454
|
+
* the winning subscription was written from, and OPTIONAL inline reads:
|
|
1455
|
+
* `quota` inlines one quota, `creditType` inlines the same owner-bound
|
|
1456
|
+
* balance `getBalance` returns — ONE call for a thin client's paywall.
|
|
1457
|
+
* A non-2xx answer means UNKNOWN: render the last cached answer, never
|
|
1458
|
+
* free (payments.md §3a). */
|
|
1459
|
+
getEntitlements: async (userId, opts) => {
|
|
1460
|
+
const s = qs({
|
|
1461
|
+
user_id: userId,
|
|
1462
|
+
quota: opts?.quota || undefined,
|
|
1463
|
+
credit_type: opts?.creditType || undefined,
|
|
1464
|
+
});
|
|
1465
|
+
return (await this.call('GET', `/v1/payments/entitlements${s}`)).data;
|
|
1466
|
+
},
|
|
1359
1467
|
/** Boolean gate: does the user hold `entitlement`? Returns granted + its
|
|
1360
|
-
* source (subscription/tier) + the resolved tier
|
|
1468
|
+
* source (subscription/tier) + the resolved tier, plus `until` (how long
|
|
1469
|
+
* the answer is good for) and `as_of`. Non-2xx = unknown, never free. */
|
|
1361
1470
|
hasEntitlement: async (userId, entitlement) => (await this.call('GET', `/v1/payments/entitlements/check?user_id=${encodeURIComponent(userId)}&entitlement=${encodeURIComponent(entitlement)}`)).data,
|
|
1362
1471
|
/** A single numeric quota for the user (e.g. `seats`, `api_calls`); null when
|
|
1363
1472
|
* the resolved tier declares no such quota. Convenience over getEntitlements. */
|
|
@@ -1386,12 +1495,12 @@ export class Vxil {
|
|
|
1386
1495
|
/** Recent ledger rows for the user (the audit/trail surface), newest first;
|
|
1387
1496
|
* optionally scoped to one credit type. */
|
|
1388
1497
|
usage: async (q) => {
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
return (await this.call('GET', `/v1/payments/usage
|
|
1498
|
+
const s = qs({
|
|
1499
|
+
user_id: q.user_id,
|
|
1500
|
+
credit_type: q.credit_type || undefined,
|
|
1501
|
+
limit: q.limit || undefined,
|
|
1502
|
+
});
|
|
1503
|
+
return (await this.call('GET', `/v1/payments/usage${s}`)).data;
|
|
1395
1504
|
},
|
|
1396
1505
|
/** Start a subscription (mock = instant checkout; real providers redirect via
|
|
1397
1506
|
* their own flow). `tier` keys the tierMap fold; `price_ref` is your catalog
|
|
@@ -1403,13 +1512,8 @@ export class Vxil {
|
|
|
1403
1512
|
/** List subscriptions (the cancel enabler — discover the subscription_id);
|
|
1404
1513
|
* optionally scoped to one user / status. */
|
|
1405
1514
|
listSubscriptions: async (q) => {
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
qs.set('user_id', q.user_id);
|
|
1409
|
-
if (q?.status)
|
|
1410
|
-
qs.set('status', q.status);
|
|
1411
|
-
const s = qs.toString();
|
|
1412
|
-
return (await this.call('GET', `/v1/payments/subscriptions${s ? `?${s}` : ''}`)).data.subscriptions;
|
|
1515
|
+
const s = qs({ user_id: q?.user_id || undefined, status: q?.status || undefined });
|
|
1516
|
+
return (await this.call('GET', `/v1/payments/subscriptions${s}`)).data.subscriptions;
|
|
1413
1517
|
},
|
|
1414
1518
|
/**
|
|
1415
1519
|
* Create a hosted-checkout session (payments.md §3). Redirect the buyer to
|
|
@@ -1425,20 +1529,56 @@ export class Vxil {
|
|
|
1425
1529
|
* NEVER exceed the charge (422 refund_exceeds_charge).
|
|
1426
1530
|
*/
|
|
1427
1531
|
createRefund: async (input, opts) => (await this.call('POST', '/v1/payments/refunds', input, { 'idempotency-key': opts.idempotencyKey })).data,
|
|
1428
|
-
/**
|
|
1429
|
-
*
|
|
1532
|
+
/** Where the user MANAGES their subscription, resolved by FUNDING SOURCE
|
|
1533
|
+
* (the provider of their live subscription, not the tenant's primary):
|
|
1534
|
+
* `kind: 'portal'` = a provider-minted portal session URL (Stripe, Paddle
|
|
1535
|
+
* via the captured customer id, mock); `'store'` = the App Store / Play
|
|
1536
|
+
* Store manage-subscriptions page for a RevenueCat row that recorded its
|
|
1537
|
+
* store; `'none'` (url null) = nothing to link (PayPal, a manual grant, no
|
|
1538
|
+
* live subscription on a portal-less primary). */
|
|
1430
1539
|
getCustomerPortalUrl: async (userId) => (await this.call('GET', `/v1/payments/customer-portal-url?user_id=${encodeURIComponent(userId)}`)).data,
|
|
1540
|
+
/** SERVER-ONLY. A support / promotional / migration grant: a `manual`
|
|
1541
|
+
* subscription row (no provider behind it) folded like a webhook —
|
|
1542
|
+
* entitlements + the tier's recurring credits apply; `until` null =
|
|
1543
|
+
* open-ended, else access ends at `until`. Idempotent on
|
|
1544
|
+
* `idempotency_key` (a replay returns the first grant, `replayed: true`).
|
|
1545
|
+
* 422 unknown_tier when the tier is not in ledger.tierMap. */
|
|
1546
|
+
grantSubscription: async (input) => (await this.call('POST', '/v1/payments/subscriptions/grant', input)).data,
|
|
1547
|
+
/** SERVER-ONLY. Revoke a MANUAL grant now (409 not_manual for a provider
|
|
1548
|
+
* subscription — use `cancel`). Never touches a provider. */
|
|
1549
|
+
revokeSubscription: async (subscriptionId) => (await this.call('POST', `/v1/payments/subscriptions/${encodeURIComponent(subscriptionId)}/revoke`)).data,
|
|
1550
|
+
/** SERVER-ONLY. Re-read a customer's subscriptions FROM the provider and
|
|
1551
|
+
* fold them through the same conditional upsert a webhook uses (never a
|
|
1552
|
+
* second write path). `dry_run` diffs without writing. Capped 30/min per
|
|
1553
|
+
* project (429 rate_limited). The migration backfill (`vxil migrate
|
|
1554
|
+
* payments --from-provider …`) drives this per customer. */
|
|
1555
|
+
syncSubscriptions: async (input) => (await this.call('POST', '/v1/payments/subscriptions/sync', input)).data,
|
|
1556
|
+
/** The server leg of "Restore Purchases": in end-user mode the verified
|
|
1557
|
+
* principal's OWN provider state is re-read (no user id needed); in server
|
|
1558
|
+
* mode pass `user_id`. 3 per hour per user (429). Returns the sync outcome
|
|
1559
|
+
* plus the fresh entitlement view in one answer. */
|
|
1560
|
+
restoreSubscriptions: async (input) => (await this.call('POST', '/v1/payments/subscriptions/restore', input ?? {})).data,
|
|
1561
|
+
/** SERVER-ONLY. Account merge: move EVERY provider's subscriptions,
|
|
1562
|
+
* customer links and available credit balances of `from_user_id` onto
|
|
1563
|
+
* `into_user_id` — at-most-once per pair. The consumer of the
|
|
1564
|
+
* `auth.user.merged { from, into }` audit event. */
|
|
1565
|
+
reKeySubscriptions: async (input) => (await this.call('POST', '/v1/payments/subscriptions/re-key', input)).data,
|
|
1566
|
+
/** SERVER-ONLY, mock/dev tenants ONLY (403 simulation_not_allowed
|
|
1567
|
+
* elsewhere). Drive a scripted lifecycle (renewal, expiry, refund-pair,
|
|
1568
|
+
* past-due-grace, cross-platform-unlock, transfer) through the mock
|
|
1569
|
+
* webhook path and judge it with the conformance oracle. */
|
|
1570
|
+
simulate: async (input) => (await this.call('POST', '/v1/payments/simulate', input)).data,
|
|
1571
|
+
/** The last report-only reconciliation sweep result for this project
|
|
1572
|
+
* (`run: null` before the first daily tick). */
|
|
1573
|
+
reconcile: async () => (await this.call('GET', '/v1/payments/reconcile')).data,
|
|
1431
1574
|
/** List charges newest-first (the refund enabler — discover the charge_id).
|
|
1432
1575
|
* Filters: user_id, status, limit (clamped 1..100, default 50). */
|
|
1433
1576
|
listCharges: async (q) => {
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
if (q?.limit)
|
|
1440
|
-
qs.set('limit', String(q.limit));
|
|
1441
|
-
const suffix = qs.size ? `?${qs.toString()}` : '';
|
|
1577
|
+
const suffix = qs({
|
|
1578
|
+
user_id: q?.user_id || undefined,
|
|
1579
|
+
status: q?.status || undefined,
|
|
1580
|
+
limit: q?.limit || undefined,
|
|
1581
|
+
});
|
|
1442
1582
|
return (await this.call('GET', `/v1/payments/charges${suffix}`)).data;
|
|
1443
1583
|
},
|
|
1444
1584
|
/** Provider webhook event log (payments.md §7 "Event log & replay"):
|
|
@@ -1449,20 +1589,15 @@ export class Vxil {
|
|
|
1449
1589
|
/** List deliveries newest-first (keyset-paginated; pass `cursor` from a
|
|
1450
1590
|
* prior page's next_cursor). List rows omit payload/raw_body. */
|
|
1451
1591
|
list: async (q) => {
|
|
1452
|
-
const
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
if (q?.cursor)
|
|
1462
|
-
qs.set('cursor', q.cursor);
|
|
1463
|
-
if (q?.limit)
|
|
1464
|
-
qs.set('limit', String(q.limit));
|
|
1465
|
-
const suffix = qs.size ? `?${qs.toString()}` : '';
|
|
1592
|
+
const suffix = qs({
|
|
1593
|
+
provider: q?.provider || undefined,
|
|
1594
|
+
event_type: q?.event_type || undefined,
|
|
1595
|
+
outcome: q?.outcome || undefined,
|
|
1596
|
+
environment: q?.environment || undefined,
|
|
1597
|
+
since: q?.since || undefined,
|
|
1598
|
+
cursor: q?.cursor || undefined,
|
|
1599
|
+
limit: q?.limit || undefined,
|
|
1600
|
+
});
|
|
1466
1601
|
return (await this.call('GET', `/v1/payments/webhook-events${suffix}`)).data;
|
|
1467
1602
|
},
|
|
1468
1603
|
/** Full delivery detail incl. the verified payload and (for failed
|