@vxil/sdk 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -6
- package/dist/index.d.ts +810 -60
- package/dist/index.js +413 -268
- 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 +1 -1
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,8 +148,8 @@ 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
|
|
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
|
|
125
153
|
* client (drops the header). See docs/end-user-principals-design.md §4.1. */
|
|
126
154
|
asEndUser(endUserToken) {
|
|
127
155
|
return new Vxil({
|
|
@@ -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 = {
|
|
@@ -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
|
|
@@ -429,6 +467,13 @@ export class Vxil {
|
|
|
429
467
|
};
|
|
430
468
|
},
|
|
431
469
|
};
|
|
470
|
+
/** Current-month usage: requests used vs the plan's included quota, plus
|
|
471
|
+
* month-to-date per-feature metered detail. Read-only (needs `usage:read`
|
|
472
|
+
* or `features:read`). Agents: call this before a bulk run to self-check
|
|
473
|
+
* remaining quota. */
|
|
474
|
+
usage = {
|
|
475
|
+
current: async () => (await this.call('GET', '/v1/usage')).data,
|
|
476
|
+
};
|
|
432
477
|
jobs = {
|
|
433
478
|
/** Enqueue a one-off job; Vxil POSTs a signed callback to target_url with
|
|
434
479
|
* retries. deliver_after (ISO) / delay_seconds (≤ 30 d, at most one of the
|
|
@@ -452,17 +497,13 @@ export class Vxil {
|
|
|
452
497
|
* outstanding-holds ceiling → 429. */
|
|
453
498
|
generation: async (input) => (await this.call('POST', '/v1/jobs/generation', input)).data,
|
|
454
499
|
runs: async (q) => {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (q?.limit)
|
|
463
|
-
qs.set('limit', String(q.limit));
|
|
464
|
-
const s = qs.toString();
|
|
465
|
-
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;
|
|
466
507
|
},
|
|
467
508
|
run: async (runId) => (await this.call('GET', `/v1/jobs/runs/${encodeURIComponent(runId)}`)).data,
|
|
468
509
|
cancel: async (runId) => (await this.call('POST', `/v1/jobs/runs/${encodeURIComponent(runId)}/cancel`)).data,
|
|
@@ -503,6 +544,16 @@ export class Vxil {
|
|
|
503
544
|
},
|
|
504
545
|
},
|
|
505
546
|
};
|
|
547
|
+
/** vxil-auth. SCOPES (docs/features/auth.md §7.6): 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. */
|
|
506
557
|
auth = {
|
|
507
558
|
signUp: async (input) => (await this.call('POST', '/v1/auth/sign-up', input)).data,
|
|
508
559
|
signIn: async (input) => (await this.call('POST', '/v1/auth/sign-in', input)).data,
|
|
@@ -516,9 +567,11 @@ export class Vxil {
|
|
|
516
567
|
* Server-side guessing budget (config otp.maxAttempts), single active code,
|
|
517
568
|
* resend cooldown (429 otp_rate_limited). */
|
|
518
569
|
otp: {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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,
|
|
522
575
|
verify: async (input) => (await this.call('POST', '/v1/auth/otp/verify', input)).data,
|
|
523
576
|
},
|
|
524
577
|
/** Anonymous (guest) sessions: instant end-user + session (JWT carries
|
|
@@ -526,9 +579,12 @@ export class Vxil {
|
|
|
526
579
|
anonymous: {
|
|
527
580
|
signIn: async () => (await this.call('POST', '/v1/auth/anonymous/sign-in', {})).data,
|
|
528
581
|
link: {
|
|
529
|
-
request: async (input) =>
|
|
530
|
-
|
|
531
|
-
|
|
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). */
|
|
532
588
|
verify: async (input) => (await this.call('POST', '/v1/auth/anonymous/link/verify', input)).data,
|
|
533
589
|
},
|
|
534
590
|
},
|
|
@@ -536,9 +592,7 @@ export class Vxil {
|
|
|
536
592
|
* bearer token ROTATES (the returned session.token replaces the old one —
|
|
537
593
|
* swap it client-side) and the JWT gains an `elv` claim. */
|
|
538
594
|
stepUp: {
|
|
539
|
-
request: async (input) =>
|
|
540
|
-
await this.call('POST', '/v1/auth/step-up/request', input);
|
|
541
|
-
},
|
|
595
|
+
request: async (input) => (await this.call('POST', '/v1/auth/step-up/request', input)).data,
|
|
542
596
|
verify: async (input) => (await this.call('POST', '/v1/auth/step-up/verify', input)).data,
|
|
543
597
|
},
|
|
544
598
|
/** End-user administration (server/function surface, auth:write). */
|
|
@@ -547,8 +601,16 @@ export class Vxil {
|
|
|
547
601
|
* (email → tombstone, password/name/avatar cleared) and revoke every
|
|
548
602
|
* live session. Idempotent — a second call on an already-erased user is a
|
|
549
603
|
* no-op. Meant to be called from a "delete my account" server route or
|
|
550
|
-
* 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). */
|
|
551
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,
|
|
552
614
|
},
|
|
553
615
|
sessions: {
|
|
554
616
|
verify: async (token) => (await this.call('POST', '/v1/auth/sessions/verify', { token })).data,
|
|
@@ -557,13 +619,24 @@ export class Vxil {
|
|
|
557
619
|
revoke: async (token) => {
|
|
558
620
|
await this.call('POST', '/v1/auth/sessions/revoke', { token });
|
|
559
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,
|
|
560
629
|
/** Force-revoke a SPECIFIC session by its `session_id` (from `list`) — the
|
|
561
630
|
* server-forced device-lockout path beyond the client-cooperative
|
|
562
|
-
* 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`. */
|
|
563
634
|
revokeById: async (sessionId) => {
|
|
564
635
|
await this.call('POST', `/v1/auth/sessions/${encodeURIComponent(sessionId)}/revoke`, {});
|
|
565
636
|
},
|
|
566
|
-
/** 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). */
|
|
567
640
|
list: async (userId) => (await this.call('GET', `/v1/auth/sessions?user_id=${encodeURIComponent(userId)}`)).data.sessions,
|
|
568
641
|
},
|
|
569
642
|
password: {
|
|
@@ -575,13 +648,36 @@ export class Vxil {
|
|
|
575
648
|
await this.call('POST', '/v1/auth/password/reset/confirm', input);
|
|
576
649
|
},
|
|
577
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
|
+
},
|
|
578
663
|
/** Social sign-in. The web `start`/`callback` flows are browser redirects
|
|
579
|
-
* (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. */
|
|
580
671
|
oauth: {
|
|
581
672
|
/** Native social sign-in: exchange a provider `id_token`/`access_token`
|
|
582
673
|
* for a vxil session (`linked` marks whether the user was created or
|
|
583
674
|
* matched to an existing identity). */
|
|
584
|
-
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,
|
|
585
681
|
},
|
|
586
682
|
};
|
|
587
683
|
rateLimits = {
|
|
@@ -599,10 +695,8 @@ export class Vxil {
|
|
|
599
695
|
/** Per-policy usage analytics: hourly allowed/blocked buckets + top keys
|
|
600
696
|
* (last `hours`, clamped 1..48, default 24). */
|
|
601
697
|
analytics: async (input) => {
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
qs.set('hours', String(input.hours));
|
|
605
|
-
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;
|
|
606
700
|
},
|
|
607
701
|
/**
|
|
608
702
|
* Consume budget. With behavior 'block' an exceeded check throws
|
|
@@ -631,6 +725,16 @@ export class Vxil {
|
|
|
631
725
|
addField: async (collection, field) => {
|
|
632
726
|
await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, field);
|
|
633
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
|
+
},
|
|
634
738
|
/** Set (or clear, with `null`) the collection's end-user owner-scope flag
|
|
635
739
|
* (design §5.1). Names an existing `string` field that holds the owner id. */
|
|
636
740
|
setOwnerField: async (collection, ownerField) => {
|
|
@@ -644,6 +748,13 @@ export class Vxil {
|
|
|
644
748
|
setPublic: async (collection, isPublic) => {
|
|
645
749
|
await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { public: isPublic });
|
|
646
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
|
+
},
|
|
647
758
|
},
|
|
648
759
|
items: {
|
|
649
760
|
/** `lock` serializes same-key writers (per-tenant advisory lock); `guard`
|
|
@@ -661,17 +772,13 @@ export class Vxil {
|
|
|
661
772
|
* per-field `Filterable` unions on `vx.from(...).query` exclude.
|
|
662
773
|
*/
|
|
663
774
|
query: async (collection, q) => {
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (q?.cursor)
|
|
672
|
-
qs.set('cursor', q.cursor);
|
|
673
|
-
const s = qs.toString();
|
|
674
|
-
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;
|
|
675
782
|
},
|
|
676
783
|
/** Merge-patch data keys; null clears a key. Concurrency opts (cms.md §9):
|
|
677
784
|
* `ifVersion` → If-Match CAS; `if` → bounded field precondition against
|
|
@@ -691,15 +798,29 @@ export class Vxil {
|
|
|
691
798
|
/** `{ count }` under the same bounded filter grammar (cms.md §9.4). 422
|
|
692
799
|
* count_unavailable_with_read_hooks on beforeRead-hooked collections. */
|
|
693
800
|
count: async (collection, filter) => {
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
qs.set('filter', JSON.stringify(filter));
|
|
697
|
-
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;
|
|
698
803
|
},
|
|
699
804
|
/** Returns the cascade tally (cms.md §11); `ifVersion` rides If-Match and
|
|
700
805
|
* a conflict aborts BEFORE any cascade side-effect. */
|
|
701
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,
|
|
702
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,
|
|
703
824
|
/** cms-rel B2 (cms.md §12): bounded group-by aggregate. fns count|sum|
|
|
704
825
|
* min|max|avg over index-slot-bound fields; groupBy ≤2 slot-bound
|
|
705
826
|
* fields; filter = the full query DSL incl. ONE-hop dotted join terms
|
|
@@ -728,12 +849,8 @@ export class Vxil {
|
|
|
728
849
|
comments = {
|
|
729
850
|
create: async (input) => (await this.call('POST', '/v1/comments', input)).data,
|
|
730
851
|
list: async (q) => {
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
qs.set('cursor', q.cursor);
|
|
734
|
-
if (q.limit)
|
|
735
|
-
qs.set('limit', String(q.limit));
|
|
736
|
-
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;
|
|
737
854
|
},
|
|
738
855
|
/** Author edit, allowed within the tenant's editWindowMinutes. */
|
|
739
856
|
edit: async (commentId, input) => {
|
|
@@ -748,17 +865,13 @@ export class Vxil {
|
|
|
748
865
|
/** Cross-topic recent-comments feed (tenant-wide, newest-first) — the
|
|
749
866
|
* dashboard "recent activity" surface. Keyset cursor + optional filters. */
|
|
750
867
|
recent: async (q) => {
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
if (q?.limit !== undefined)
|
|
759
|
-
qs.set('limit', String(q.limit));
|
|
760
|
-
const s = qs.toString();
|
|
761
|
-
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;
|
|
762
875
|
},
|
|
763
876
|
};
|
|
764
877
|
/**
|
|
@@ -786,12 +899,8 @@ export class Vxil {
|
|
|
786
899
|
/** Read a conversation's messages (an authz'd participant only; messages
|
|
787
900
|
* from authors the viewer has blocked are filtered out). Keyset cursor. */
|
|
788
901
|
messages: async (conversationId, q) => {
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
qs.set('cursor', q.cursor);
|
|
792
|
-
if (q.limit !== undefined)
|
|
793
|
-
qs.set('limit', String(q.limit));
|
|
794
|
-
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;
|
|
795
904
|
},
|
|
796
905
|
},
|
|
797
906
|
/** Send a message (an authz'd participant). Stored as a comment on the
|
|
@@ -834,21 +943,15 @@ export class Vxil {
|
|
|
834
943
|
* feed; `notificationState` filters to unseen | unread | all.
|
|
835
944
|
*/
|
|
836
945
|
read: async (group, feedId, q) => {
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
if (q?.markSeen)
|
|
847
|
-
qs.set('mark_seen', 'true');
|
|
848
|
-
if (q?.markRead)
|
|
849
|
-
qs.set('mark_read', 'true');
|
|
850
|
-
const s = qs.toString();
|
|
851
|
-
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;
|
|
852
955
|
},
|
|
853
956
|
/** Remove an activity by id (or by foreign_id); tombstones + un-fans-out. */
|
|
854
957
|
removeActivity: async (group, feedId, ref) => {
|
|
@@ -879,13 +982,8 @@ export class Vxil {
|
|
|
879
982
|
/** Who a feed FOLLOWS (`following[]` + `following_count`) plus the true
|
|
880
983
|
* `followers_count` (who follows this feed). Keyset cursor via `after`. */
|
|
881
984
|
listFollows: async (group, feedId, q) => {
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
qs.set('limit', String(q.limit));
|
|
885
|
-
if (q?.after)
|
|
886
|
-
qs.set('after', q.after);
|
|
887
|
-
const s = qs.toString();
|
|
888
|
-
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;
|
|
889
987
|
},
|
|
890
988
|
/**
|
|
891
989
|
* Block (two-way fan-out suppression, removes existing rows) or mute (one-way
|
|
@@ -961,17 +1059,13 @@ export class Vxil {
|
|
|
961
1059
|
},
|
|
962
1060
|
events: {
|
|
963
1061
|
list: async (q) => {
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
if (q?.limit)
|
|
972
|
-
qs.set('limit', String(q.limit));
|
|
973
|
-
const s = qs.toString();
|
|
974
|
-
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;
|
|
975
1069
|
},
|
|
976
1070
|
replay: async (eventId) => {
|
|
977
1071
|
await this.call('POST', `/v1/webhooks/events/${encodeURIComponent(eventId)}/replay`);
|
|
@@ -980,6 +1074,12 @@ export class Vxil {
|
|
|
980
1074
|
* every jobs run this event's forwards created. */
|
|
981
1075
|
runs: async (eventId) => (await this.call('GET', `/v1/webhooks/events/${encodeURIComponent(eventId)}/runs`)).data,
|
|
982
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,
|
|
983
1083
|
};
|
|
984
1084
|
orgs = {
|
|
985
1085
|
create: async (input) => (await this.call('POST', '/v1/orgs', input)).data,
|
|
@@ -995,10 +1095,8 @@ export class Vxil {
|
|
|
995
1095
|
* `opts.resource` to additionally consult per-resource ACL grants; the
|
|
996
1096
|
* response `source` marks whether a grant came from the role lattice or an ACL. */
|
|
997
1097
|
check: async (orgId, userId, permission, opts) => {
|
|
998
|
-
const
|
|
999
|
-
|
|
1000
|
-
qs.set('resource', opts.resource);
|
|
1001
|
-
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;
|
|
1002
1100
|
},
|
|
1003
1101
|
/** The active-org snapshot auth embeds in session JWTs when the tenant
|
|
1004
1102
|
* enables auth config `orgClaims` (most-recent membership by joined_at).
|
|
@@ -1093,15 +1191,12 @@ export class Vxil {
|
|
|
1093
1191
|
complete: async (objectId) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/complete`)).data,
|
|
1094
1192
|
downloadUrl: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/download-url`)).data.download_url,
|
|
1095
1193
|
list: async (q) => {
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
qs.set('limit', String(q.limit));
|
|
1103
|
-
const s = qs.toString();
|
|
1104
|
-
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;
|
|
1105
1200
|
},
|
|
1106
1201
|
/** Soft delete; bytes are hard-deleted 30 days later. */
|
|
1107
1202
|
delete: async (objectId) => {
|
|
@@ -1119,9 +1214,12 @@ export class Vxil {
|
|
|
1119
1214
|
* (else a future auto-delete after the given seconds; minimum 60). */
|
|
1120
1215
|
setTtl: async (objectId, expiresInSeconds) => (await this.call('PUT', `/v1/files/${encodeURIComponent(objectId)}/ttl`, { expiresInSeconds })).data,
|
|
1121
1216
|
sharedLinks: {
|
|
1122
|
-
/** 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. */
|
|
1123
1221
|
create: async (objectId, opts) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/shared-links`, opts ?? {})).data,
|
|
1124
|
-
/** List an object's active (unrevoked, unexpired) shared links. */
|
|
1222
|
+
/** List an object's active (unrevoked, unexpired, unexhausted) shared links. */
|
|
1125
1223
|
list: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/shared-links`)).data.links,
|
|
1126
1224
|
revoke: async (linkId) => {
|
|
1127
1225
|
await this.call('DELETE', `/v1/files/shared-links/${encodeURIComponent(linkId)}`);
|
|
@@ -1145,15 +1243,12 @@ export class Vxil {
|
|
|
1145
1243
|
ingest: async (collection, input) => (await this.call('POST', `/v1/search/${encodeURIComponent(collection)}/documents`, input)).data,
|
|
1146
1244
|
/** List indexed documents (keyset cursor, optional `user_id` filter). */
|
|
1147
1245
|
listDocuments: async (collection, q) => {
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
qs.set('limit', String(q.limit));
|
|
1155
|
-
const s = qs.toString();
|
|
1156
|
-
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;
|
|
1157
1252
|
},
|
|
1158
1253
|
/** De-index a document (+ invalidate the query cache). */
|
|
1159
1254
|
deleteDocument: async (collection, docId) => {
|
|
@@ -1190,7 +1285,10 @@ export class Vxil {
|
|
|
1190
1285
|
ai = {
|
|
1191
1286
|
templates: {
|
|
1192
1287
|
/** Store a tenant-authored prompt template; versions are monotonic per name.
|
|
1193
|
-
* `{{var}}` placeholders are filled from `generate`'s `input`.
|
|
1288
|
+
* `{{var}}` placeholders are filled from `generate`'s `input`.
|
|
1289
|
+
* `schema` (a JSON Schema) is now ENFORCED at generate time on sync/job
|
|
1290
|
+
* requests — the output is validated (with one repair pass) against it;
|
|
1291
|
+
* a per-request `response_schema` overrides it. Ignored on streams. */
|
|
1194
1292
|
put: async (input) => (await this.call('POST', '/v1/ai/templates', input)).data,
|
|
1195
1293
|
/** List stored templates (each name + its latest version). */
|
|
1196
1294
|
list: async () => (await this.call('GET', '/v1/ai/templates')).data.templates,
|
|
@@ -1220,19 +1318,34 @@ export class Vxil {
|
|
|
1220
1318
|
const s = q?.since != null ? `?since=${q.since}` : '';
|
|
1221
1319
|
return (await this.call('GET', `/v1/ai/generations/${encodeURIComponent(generationId)}/stream${s}`)).data;
|
|
1222
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,
|
|
1223
1339
|
/** Embed a batch of strings (1–256). */
|
|
1224
1340
|
embed: async (input) => (await this.call('POST', '/v1/ai/embed', input)).data,
|
|
1225
1341
|
/** Per-user token rollups (optionally scoped by `since`/`user_id`). */
|
|
1226
1342
|
usage: async (q) => {
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
qs.set('limit', String(q.limit));
|
|
1234
|
-
const s = qs.toString();
|
|
1235
|
-
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;
|
|
1236
1349
|
},
|
|
1237
1350
|
};
|
|
1238
1351
|
/**
|
|
@@ -1283,12 +1396,7 @@ export class Vxil {
|
|
|
1283
1396
|
/** List an agent's conversations, newest-updated first (optionally scoped
|
|
1284
1397
|
* to one end user). */
|
|
1285
1398
|
list: async (agentId, q) => {
|
|
1286
|
-
const
|
|
1287
|
-
if (q?.user_id)
|
|
1288
|
-
qs.set('user_id', q.user_id);
|
|
1289
|
-
if (q?.limit)
|
|
1290
|
-
qs.set('limit', String(q.limit));
|
|
1291
|
-
const s = qs.size ? `?${qs.toString()}` : '';
|
|
1399
|
+
const s = qs({ user_id: q?.user_id || undefined, limit: q?.limit || undefined });
|
|
1292
1400
|
return (await this.call('GET', `/v1/copilot/${encodeURIComponent(agentId)}/conversations${s}`)).data;
|
|
1293
1401
|
},
|
|
1294
1402
|
/** A conversation row + the last N transcript messages (oldest-first). */
|
|
@@ -1306,11 +1414,8 @@ export class Vxil {
|
|
|
1306
1414
|
* frame with seq > `since` plus `done` — replays without re-running (or
|
|
1307
1415
|
* re-billing) the turn. Defaults to the conversation's latest turn. */
|
|
1308
1416
|
resume: async (conversationId, q) => {
|
|
1309
|
-
const
|
|
1310
|
-
|
|
1311
|
-
if (q?.message_id)
|
|
1312
|
-
qs.set('message_id', q.message_id);
|
|
1313
|
-
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;
|
|
1314
1419
|
},
|
|
1315
1420
|
/** Confirm a proposed action — the ONLY way a write executes. The client
|
|
1316
1421
|
* sends just the path ids; everything that runs comes from the persisted
|
|
@@ -1320,14 +1425,11 @@ export class Vxil {
|
|
|
1320
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,
|
|
1321
1426
|
/** Per-user token rollup across assistant turns. */
|
|
1322
1427
|
usage: async (q) => {
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
if (q?.limit)
|
|
1329
|
-
qs.set('limit', String(q.limit));
|
|
1330
|
-
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
|
+
});
|
|
1331
1433
|
return (await this.call('GET', `/v1/copilot/usage${s}`)).data;
|
|
1332
1434
|
},
|
|
1333
1435
|
};
|
|
@@ -1341,13 +1443,30 @@ export class Vxil {
|
|
|
1341
1443
|
*/
|
|
1342
1444
|
payments = {
|
|
1343
1445
|
/** Provider + ledger capability surface (+ the `missing` set an agent can act
|
|
1344
|
-
* 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. */
|
|
1345
1449
|
capabilities: async () => (await this.call('GET', '/v1/payments/capabilities')).data,
|
|
1346
1450
|
/** The user's effective entitlement snapshot (the multi-sub tier fold):
|
|
1347
|
-
* tier + boolean entitlements + numeric quotas
|
|
1348
|
-
|
|
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
|
+
},
|
|
1349
1467
|
/** Boolean gate: does the user hold `entitlement`? Returns granted + its
|
|
1350
|
-
* 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. */
|
|
1351
1470
|
hasEntitlement: async (userId, entitlement) => (await this.call('GET', `/v1/payments/entitlements/check?user_id=${encodeURIComponent(userId)}&entitlement=${encodeURIComponent(entitlement)}`)).data,
|
|
1352
1471
|
/** A single numeric quota for the user (e.g. `seats`, `api_calls`); null when
|
|
1353
1472
|
* the resolved tier declares no such quota. Convenience over getEntitlements. */
|
|
@@ -1376,12 +1495,12 @@ export class Vxil {
|
|
|
1376
1495
|
/** Recent ledger rows for the user (the audit/trail surface), newest first;
|
|
1377
1496
|
* optionally scoped to one credit type. */
|
|
1378
1497
|
usage: async (q) => {
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
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;
|
|
1385
1504
|
},
|
|
1386
1505
|
/** Start a subscription (mock = instant checkout; real providers redirect via
|
|
1387
1506
|
* their own flow). `tier` keys the tierMap fold; `price_ref` is your catalog
|
|
@@ -1393,13 +1512,8 @@ export class Vxil {
|
|
|
1393
1512
|
/** List subscriptions (the cancel enabler — discover the subscription_id);
|
|
1394
1513
|
* optionally scoped to one user / status. */
|
|
1395
1514
|
listSubscriptions: async (q) => {
|
|
1396
|
-
const
|
|
1397
|
-
|
|
1398
|
-
qs.set('user_id', q.user_id);
|
|
1399
|
-
if (q?.status)
|
|
1400
|
-
qs.set('status', q.status);
|
|
1401
|
-
const s = qs.toString();
|
|
1402
|
-
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;
|
|
1403
1517
|
},
|
|
1404
1518
|
/**
|
|
1405
1519
|
* Create a hosted-checkout session (payments.md §3). Redirect the buyer to
|
|
@@ -1415,20 +1529,56 @@ export class Vxil {
|
|
|
1415
1529
|
* NEVER exceed the charge (422 refund_exceeds_charge).
|
|
1416
1530
|
*/
|
|
1417
1531
|
createRefund: async (input, opts) => (await this.call('POST', '/v1/payments/refunds', input, { 'idempotency-key': opts.idempotencyKey })).data,
|
|
1418
|
-
/**
|
|
1419
|
-
*
|
|
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). */
|
|
1420
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,
|
|
1421
1574
|
/** List charges newest-first (the refund enabler — discover the charge_id).
|
|
1422
1575
|
* Filters: user_id, status, limit (clamped 1..100, default 50). */
|
|
1423
1576
|
listCharges: async (q) => {
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
if (q?.limit)
|
|
1430
|
-
qs.set('limit', String(q.limit));
|
|
1431
|
-
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
|
+
});
|
|
1432
1582
|
return (await this.call('GET', `/v1/payments/charges${suffix}`)).data;
|
|
1433
1583
|
},
|
|
1434
1584
|
/** Provider webhook event log (payments.md §7 "Event log & replay"):
|
|
@@ -1439,20 +1589,15 @@ export class Vxil {
|
|
|
1439
1589
|
/** List deliveries newest-first (keyset-paginated; pass `cursor` from a
|
|
1440
1590
|
* prior page's next_cursor). List rows omit payload/raw_body. */
|
|
1441
1591
|
list: async (q) => {
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
if (q?.cursor)
|
|
1452
|
-
qs.set('cursor', q.cursor);
|
|
1453
|
-
if (q?.limit)
|
|
1454
|
-
qs.set('limit', String(q.limit));
|
|
1455
|
-
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
|
+
});
|
|
1456
1601
|
return (await this.call('GET', `/v1/payments/webhook-events${suffix}`)).data;
|
|
1457
1602
|
},
|
|
1458
1603
|
/** Full delivery detail incl. the verified payload and (for failed
|