@vxil/sdk 0.4.1 → 0.5.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/dist/index.d.ts +469 -15
- package/dist/index.js +242 -15
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -208,6 +208,41 @@ export interface FeatureConfig {
|
|
|
208
208
|
version: number;
|
|
209
209
|
manifest: Record<string, unknown>;
|
|
210
210
|
}
|
|
211
|
+
/** One rule in the advisory architecture brief. `title`, `fact`, `replaces` and
|
|
212
|
+
* `docs` are platform copy rendered by the server from a gate-checked rule
|
|
213
|
+
* catalog — they are never model prose. `why` explains why the rule applies to
|
|
214
|
+
* the app you described. */
|
|
215
|
+
export interface VxilBriefItem {
|
|
216
|
+
/** stable rule id, e.g. 'anonymous-session-for-guests'. */
|
|
217
|
+
rule_id: string;
|
|
218
|
+
title: string;
|
|
219
|
+
/** the platform rule itself, numbers inline. */
|
|
220
|
+
fact: string;
|
|
221
|
+
/** why it applies to this app. */
|
|
222
|
+
why: string;
|
|
223
|
+
/** the older habit this rule supersedes, when there is one. */
|
|
224
|
+
replaces?: string;
|
|
225
|
+
/** site path to the chapter section, e.g. '/docs/guide/16-…#…'. */
|
|
226
|
+
docs: string;
|
|
227
|
+
}
|
|
228
|
+
/** One section of the brief: the eight subject sections plus 'old-patterns',
|
|
229
|
+
* a derived view of every cited rule that supersedes an older habit. */
|
|
230
|
+
export interface VxilBriefSection {
|
|
231
|
+
/** 'identity' | 'data' | 'money' | 'background' | 'notifications' |
|
|
232
|
+
* 'rate-limits' | 'environments' | 'limits' | 'old-patterns' */
|
|
233
|
+
key: string;
|
|
234
|
+
title: string;
|
|
235
|
+
items: VxilBriefItem[];
|
|
236
|
+
}
|
|
237
|
+
/** The ADVISORY architecture brief attached to a plan: the platform rules the
|
|
238
|
+
* plan's shape implies. `source` is 'ai' when a model chose the rules and
|
|
239
|
+
* 'deterministic' when they were derived from the feature set alone. */
|
|
240
|
+
export interface VxilPlanBrief {
|
|
241
|
+
source: 'ai' | 'deterministic';
|
|
242
|
+
sections: VxilBriefSection[];
|
|
243
|
+
/** decisions the description does not settle (empty on the deterministic path). */
|
|
244
|
+
open_questions: string[];
|
|
245
|
+
}
|
|
211
246
|
/** An advisory plan from the planner (POST /v1/plan) — a proposal, not applied. */
|
|
212
247
|
export interface VxilPlan {
|
|
213
248
|
app: {
|
|
@@ -254,6 +289,8 @@ export interface VxilPlan {
|
|
|
254
289
|
rationale: string;
|
|
255
290
|
/** which brain produced the plan. */
|
|
256
291
|
source: 'ai' | 'deterministic';
|
|
292
|
+
/** the advisory architecture brief. Absent when you pass `{ brief: false }`. */
|
|
293
|
+
brief?: VxilPlanBrief;
|
|
257
294
|
}
|
|
258
295
|
/** Current-month usage projection (GET /v1/usage) — the same meter the
|
|
259
296
|
* platform bills and enforces request quotas from. */
|
|
@@ -281,6 +318,15 @@ export interface UsageCurrent {
|
|
|
281
318
|
metric: string;
|
|
282
319
|
quantity: number;
|
|
283
320
|
}>;
|
|
321
|
+
/** month-to-date split of `requests.used` by the SURFACE that served it —
|
|
322
|
+
* the edge's resolved upstream (a bounded set), with /v1/fn/* reported as
|
|
323
|
+
* 'fn-invoke' and /v1/functions/* as its management surface,
|
|
324
|
+
* 'control-plane'. Busiest first. Observational only: never a billing
|
|
325
|
+
* basis, and [] wherever request metering is not switched on. */
|
|
326
|
+
routes: Array<{
|
|
327
|
+
route_class: string;
|
|
328
|
+
requests: number;
|
|
329
|
+
}>;
|
|
284
330
|
}
|
|
285
331
|
export interface DeadLetter {
|
|
286
332
|
delivery_id: string;
|
|
@@ -567,6 +613,12 @@ export interface SearchSyncSource {
|
|
|
567
613
|
export interface AiUsage {
|
|
568
614
|
input_tokens: number;
|
|
569
615
|
output_tokens: number;
|
|
616
|
+
/** PROMPT-CACHE accounting, present only when the provider reports it
|
|
617
|
+
* (anthropic, when the request asked for a cache breakpoint). A cache read
|
|
618
|
+
* is already excluded from `input_tokens`, so metering and the daily budget
|
|
619
|
+
* need no adjustment — these are for your own visibility. */
|
|
620
|
+
cache_creation_input_tokens?: number;
|
|
621
|
+
cache_read_input_tokens?: number;
|
|
570
622
|
}
|
|
571
623
|
/** One tool invocation the model asked for (tool-use passthrough — vxil relays,
|
|
572
624
|
* YOU execute). Echo `id` back as `tool_call_id` on the `role:'tool'` turn. */
|
|
@@ -1074,6 +1126,12 @@ export interface VxilSchemaShape {
|
|
|
1074
1126
|
Patch: unknown;
|
|
1075
1127
|
Sortable: string;
|
|
1076
1128
|
Filterable: Record<string, unknown>;
|
|
1129
|
+
/** `vxil gen` emits this per collection: every `$expand`-able field (relation
|
|
1130
|
+
* + file) → its target collection slug (`'files'` for a file field, `null`
|
|
1131
|
+
* for a relation with no declared target). OPTIONAL on the shape so a client
|
|
1132
|
+
* generated BEFORE typed expand still satisfies the constraint — the expand
|
|
1133
|
+
* names then stay `string` and an expanded member resolves to an opaque bag. */
|
|
1134
|
+
Relations?: Record<string, string | null>;
|
|
1077
1135
|
}>;
|
|
1078
1136
|
functions: Record<string, {
|
|
1079
1137
|
Input: unknown;
|
|
@@ -1144,6 +1202,33 @@ export interface CmsBacklinksPage {
|
|
|
1144
1202
|
has_more: boolean;
|
|
1145
1203
|
limit: number;
|
|
1146
1204
|
}
|
|
1205
|
+
/** ONE page of the slot re-index (cms.md §19). Loop while `complete` is false,
|
|
1206
|
+
* feeding `next_cursor` back in. `skipped` counts rows a concurrent write moved
|
|
1207
|
+
* under the page — the re-index never overwrites them (their own writer
|
|
1208
|
+
* re-projected them), but a `$inc` or a cascade `set_null` only re-projects its
|
|
1209
|
+
* OWN field's slot, so re-run the page while `skipped > 0`. */
|
|
1210
|
+
export interface CmsReindexPage {
|
|
1211
|
+
collection: string;
|
|
1212
|
+
field: string | null;
|
|
1213
|
+
scanned: number;
|
|
1214
|
+
updated: number;
|
|
1215
|
+
skipped: number;
|
|
1216
|
+
next_cursor: string | null;
|
|
1217
|
+
complete: boolean;
|
|
1218
|
+
}
|
|
1219
|
+
/** The result of a bounded filtered delete (cms.md §20). `matched` counts the
|
|
1220
|
+
* rows this page selected (≤ `limit`); `deleted` counts the ones actually
|
|
1221
|
+
* removed (a row that vanished between the match and the delete is skipped).
|
|
1222
|
+
* Loop while `deleted > 0` — the filter re-evaluates against live rows. */
|
|
1223
|
+
export interface CmsBulkDeleteResult {
|
|
1224
|
+
collection: string;
|
|
1225
|
+
matched: number;
|
|
1226
|
+
deleted: number;
|
|
1227
|
+
cascaded: number;
|
|
1228
|
+
set_null: number;
|
|
1229
|
+
dry_run: boolean;
|
|
1230
|
+
next_cursor: string | null;
|
|
1231
|
+
}
|
|
1147
1232
|
/** A write guard (cms.md §10): "after this write, at most `max` live items
|
|
1148
1233
|
* match `filter`". Requires `lock` — an unlocked guard is racy by construction. */
|
|
1149
1234
|
export interface CmsGuard {
|
|
@@ -1241,8 +1326,56 @@ export type CmsTxStep = {
|
|
|
1241
1326
|
item_id: string;
|
|
1242
1327
|
$where?: Record<string, unknown>;
|
|
1243
1328
|
};
|
|
1244
|
-
/**
|
|
1245
|
-
|
|
1329
|
+
/** One CMS item as the API returns it — the envelope `$expand` inlines in place
|
|
1330
|
+
* of a stored relation id (cms.md §6.2). `R` is the target collection's Row. */
|
|
1331
|
+
export interface CmsItemEnvelope<R> {
|
|
1332
|
+
item_id: string;
|
|
1333
|
+
collection: string;
|
|
1334
|
+
status: string;
|
|
1335
|
+
version: number;
|
|
1336
|
+
data: R;
|
|
1337
|
+
created_at: string;
|
|
1338
|
+
updated_at: string;
|
|
1339
|
+
published_at: string | null;
|
|
1340
|
+
}
|
|
1341
|
+
/** What an expanded FILE field resolves to — a stub, never a cms row. */
|
|
1342
|
+
export interface CmsFileRef {
|
|
1343
|
+
object_id: string;
|
|
1344
|
+
$ref: 'files';
|
|
1345
|
+
}
|
|
1346
|
+
/** Every shape an expanded member can take (cms.md §6.2, all four verified in
|
|
1347
|
+
* cms-v1 core.ts): the full envelope; a `{ object_id, $ref: 'files' }` stub for
|
|
1348
|
+
* a file field; the BARE id when the expansion hit a cycle or the depth budget;
|
|
1349
|
+
* `null` when the target is soft-deleted, not visible to this caller, or owned
|
|
1350
|
+
* by another end-user. A
|
|
1351
|
+
* relation field may hold an ARRAY of ids, in which case the expansion is an
|
|
1352
|
+
* array of those same shapes — `vxil gen` types a relation as `string` and does
|
|
1353
|
+
* not carry arity, so the union includes the array form rather than inventing
|
|
1354
|
+
* a precision the model does not have. */
|
|
1355
|
+
export type CmsExpanded<R> = CmsItemEnvelope<R> | CmsFileRef | string | null | Array<CmsItemEnvelope<R> | CmsFileRef | string | null>;
|
|
1356
|
+
/** The expand-able field names of a collection: the keys of the generated
|
|
1357
|
+
* `Relations` map, or plain `string` for a client generated before it existed. */
|
|
1358
|
+
type ExpandableOf<C> = C extends {
|
|
1359
|
+
Relations: infer R;
|
|
1360
|
+
} ? (R extends Record<string, unknown> ? keyof R & string : string) : string;
|
|
1361
|
+
/** Resolve one expanded member's row type: the target collection's `Row` when
|
|
1362
|
+
* `Relations` names a cms collection in THIS schema, else an opaque bag (a file
|
|
1363
|
+
* stub, an untargeted relation, or an older generated client). */
|
|
1364
|
+
type TargetRow<C, S extends VxilSchemaShape, K extends string> = C extends {
|
|
1365
|
+
Relations: infer R;
|
|
1366
|
+
} ? (K extends keyof R ? (R[K] extends keyof S['cms'] ? S['cms'][R[K]]['Row'] : Record<string, unknown>) : Record<string, unknown>) : Record<string, unknown>;
|
|
1367
|
+
/** The Row with the expanded members re-typed. Non-expanded fields are untouched,
|
|
1368
|
+
* so a query WITHOUT `expand` keeps exactly today's type. */
|
|
1369
|
+
export type ExpandedRow<C extends {
|
|
1370
|
+
Row: unknown;
|
|
1371
|
+
}, S extends VxilSchemaShape, E extends readonly string[]> = Omit<C['Row'], E[number]> & {
|
|
1372
|
+
[K in E[number]]: CmsExpanded<TargetRow<C, S, K>>;
|
|
1373
|
+
};
|
|
1374
|
+
/** A per-collection typed handle returned by `vx.from(collection)`. `S` (the
|
|
1375
|
+
* whole generated schema) is DEFAULTED so `CollectionClient<X>` — the only
|
|
1376
|
+
* spelling that existed before typed expand — still compiles; it is what lets
|
|
1377
|
+
* an expanded relation resolve to the TARGET collection's Row. */
|
|
1378
|
+
export interface CollectionClient<C extends VxilSchemaShape['cms'][string], S extends VxilSchemaShape = VxilSchemaShape> {
|
|
1246
1379
|
create(data: C['Insert'], opts?: {
|
|
1247
1380
|
status?: 'draft' | 'published';
|
|
1248
1381
|
lock?: string;
|
|
@@ -1254,7 +1387,26 @@ export interface CollectionClient<C extends VxilSchemaShape['cms'][string]> {
|
|
|
1254
1387
|
version: number;
|
|
1255
1388
|
data: C['Row'];
|
|
1256
1389
|
}>;
|
|
1390
|
+
/** `expand` inlines the named relation/file fields (cms.md §6.2) — the names
|
|
1391
|
+
* come from the generated `Relations`, so a typo is a compile error. Bounds
|
|
1392
|
+
* (worker-clamped regardless of config): depth 1 (ceiling 2), ≤5 fields
|
|
1393
|
+
* (ceiling 25). Omit it and the result type is exactly today's `Row`. */
|
|
1394
|
+
get<E extends readonly ExpandableOf<C>[]>(itemId: string, opts: {
|
|
1395
|
+
expand: E;
|
|
1396
|
+
}): Promise<ExpandedRow<C, S, E>>;
|
|
1257
1397
|
get(itemId: string): Promise<C['Row']>;
|
|
1398
|
+
query<E extends readonly ExpandableOf<C>[]>(q: {
|
|
1399
|
+
filter?: Partial<C['Filterable']>;
|
|
1400
|
+
sort?: C['Sortable'] | `-${C['Sortable'] & string}`;
|
|
1401
|
+
limit?: number;
|
|
1402
|
+
cursor?: string;
|
|
1403
|
+
/** relation/file fields to inline — see `get`. Cannot be combined with
|
|
1404
|
+
* `count` (a separate method here; the raw API 422s when both are sent). */
|
|
1405
|
+
expand: E;
|
|
1406
|
+
}): Promise<{
|
|
1407
|
+
items: ExpandedRow<C, S, E>[];
|
|
1408
|
+
next_cursor: string | null;
|
|
1409
|
+
}>;
|
|
1258
1410
|
query(q?: {
|
|
1259
1411
|
/** Keys AND values come from the generated `Filterable`: each field's value
|
|
1260
1412
|
* is its operator union (`VxilFilterRange*` on slotted fields, equality/
|
|
@@ -1283,6 +1435,15 @@ export interface CollectionClient<C extends VxilSchemaShape['cms'][string]> {
|
|
|
1283
1435
|
cascaded: number;
|
|
1284
1436
|
set_null: number;
|
|
1285
1437
|
}>;
|
|
1438
|
+
/** cms.md §20: bounded filtered delete over the generated `Filterable` — a
|
|
1439
|
+
* filter is required, ≤100 rows per call, `dryRun` previews. Loop while
|
|
1440
|
+
* `deleted > 0`. */
|
|
1441
|
+
deleteMany(q: {
|
|
1442
|
+
filter: Partial<C['Filterable']>;
|
|
1443
|
+
limit?: number;
|
|
1444
|
+
cursor?: string;
|
|
1445
|
+
dryRun?: boolean;
|
|
1446
|
+
}): Promise<CmsBulkDeleteResult>;
|
|
1286
1447
|
publish(itemId: string): Promise<C['Row']>;
|
|
1287
1448
|
/** cms-rel B2: bounded group-by aggregate (spec §7 `groupBy()` surface).
|
|
1288
1449
|
* groupBy/field names are typed over the Row's keys; slot-existence stays a
|
|
@@ -1361,7 +1522,7 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
1361
1522
|
* generic `cms.items.*` methods — the wire calls are identical; the generated
|
|
1362
1523
|
* `VxilSchema` supplies the field types. `vx.cms.items.*` stays as the
|
|
1363
1524
|
* always-available un-generic fallback. */
|
|
1364
|
-
from<C extends keyof S['cms'] & string>(collection: C): CollectionClient<S['cms'][C]>;
|
|
1525
|
+
from<C extends keyof S['cms'] & string>(collection: C): CollectionClient<S['cms'][C], S>;
|
|
1365
1526
|
/** Typed function invoke (design §4.5). `vx.fn.<name>(payload)` POSTs to
|
|
1366
1527
|
* /v1/fn/<name>; the generated `VxilSchema` types Input/Output (Level-0
|
|
1367
1528
|
* opaque until a function declares a signature). A Proxy gives the
|
|
@@ -1415,7 +1576,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
1415
1576
|
readonly notifications: {
|
|
1416
1577
|
send: (input: {
|
|
1417
1578
|
user_id: string;
|
|
1418
|
-
template
|
|
1579
|
+
/** The four shipped template ids (notifications.md §7). */
|
|
1580
|
+
template: "magic-link" | "otp-code" | "welcome" | "transactional";
|
|
1419
1581
|
data: Record<string, unknown>;
|
|
1420
1582
|
/** Explicit wins; otherwise the recipient's stored `locale` attribute
|
|
1421
1583
|
* (set it via `users.upsert({ attributes: { locale } })`), then
|
|
@@ -1434,7 +1596,7 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
1434
1596
|
* + enqueued, or neither) and `results` is index-aligned with `items`. */
|
|
1435
1597
|
sendBatch: (items: Array<{
|
|
1436
1598
|
user_id: string;
|
|
1437
|
-
template: "magic-link" | "welcome" | "transactional";
|
|
1599
|
+
template: "magic-link" | "otp-code" | "welcome" | "transactional";
|
|
1438
1600
|
data: Record<string, unknown>;
|
|
1439
1601
|
locale?: string;
|
|
1440
1602
|
channel?: "email" | "inbox" | "both";
|
|
@@ -1521,6 +1683,80 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
1521
1683
|
}>>;
|
|
1522
1684
|
add: (email: string) => Promise<void>;
|
|
1523
1685
|
remove: (email: string) => Promise<void>;
|
|
1686
|
+
/** Bounce/complaint AGGREGATE over a 1–90 day window (default 30; an
|
|
1687
|
+
* out-of-range or non-numeric `days` clamps rather than erroring).
|
|
1688
|
+
* `counts` always carries all four reasons with explicit zeros; `rate` is
|
|
1689
|
+
* `counts.total / sent` to 4 dp, or null when nothing was sent. `sent`
|
|
1690
|
+
* counts deliveries the provider ACCEPTED, not inbox-delivered mail —
|
|
1691
|
+
* this is a suppressions-per-accepted-send ratio, not an RFC bounce rate.
|
|
1692
|
+
* Operator surface: a verified end-user token gets 403. */
|
|
1693
|
+
stats: (q?: {
|
|
1694
|
+
days?: number;
|
|
1695
|
+
}) => Promise<{
|
|
1696
|
+
window_days: number;
|
|
1697
|
+
since: string;
|
|
1698
|
+
counts: {
|
|
1699
|
+
hard_bounce: number;
|
|
1700
|
+
spam_complaint: number;
|
|
1701
|
+
soft_bounce: number;
|
|
1702
|
+
manual: number;
|
|
1703
|
+
total: number;
|
|
1704
|
+
};
|
|
1705
|
+
sent: number;
|
|
1706
|
+
rate: number | null;
|
|
1707
|
+
}>;
|
|
1708
|
+
};
|
|
1709
|
+
/** Per-end-user MUTES — the opt-out table every send channel checks before
|
|
1710
|
+
* it delivers. Absence of a row means "not muted". A row with no
|
|
1711
|
+
* `template_id` is the ALL-TEMPLATES row.
|
|
1712
|
+
*
|
|
1713
|
+
* MUTE WINS. Enforcement is `(template_id = <template> OR template_id IS
|
|
1714
|
+
* NULL) AND muted`, so the all-templates row SHADOWS a per-template
|
|
1715
|
+
* `muted: false` — to re-enable one template after a master mute, clear the
|
|
1716
|
+
* all-templates row (`{ muted: false }`, no `template_id`) and mute the
|
|
1717
|
+
* others individually. `get` returns a computed `effective` per row, which
|
|
1718
|
+
* is the answer enforcement gives; render THAT, not the stored `muted`.
|
|
1719
|
+
*
|
|
1720
|
+
* AUTH MAIL IS NEVER MUTEABLE. `magic-link` and `otp-code` carry sign-in
|
|
1721
|
+
* links and codes: `set` rejects them with a 422 and the all-templates row
|
|
1722
|
+
* does not cover them, so an "unsubscribe from everything" click can never
|
|
1723
|
+
* lock a user out of its own account. Muteable ids: `welcome`,
|
|
1724
|
+
* `transactional`. (Password-reset mail rides `transactional`, which stays
|
|
1725
|
+
* muteable — see the notifications feature doc §6c.)
|
|
1726
|
+
*
|
|
1727
|
+
* SCOPES: with an end-user token both calls are confined to the verified
|
|
1728
|
+
* principal and take `notifications:read` (a browser key that can mute its
|
|
1729
|
+
* own mail must not also be able to send mail). With a server key
|
|
1730
|
+
* `user_id` is required and `set` takes `notifications:send`. */
|
|
1731
|
+
preferences: {
|
|
1732
|
+
get: (q: {
|
|
1733
|
+
user_id?: string;
|
|
1734
|
+
}) => Promise<{
|
|
1735
|
+
preferences: Array<{
|
|
1736
|
+
user_id: string;
|
|
1737
|
+
template_id: string | null;
|
|
1738
|
+
muted: boolean;
|
|
1739
|
+
/** what enforcement actually does with this row: the stored `muted`
|
|
1740
|
+
* OR the all-templates row's mute (which shadows it). */
|
|
1741
|
+
effective: boolean;
|
|
1742
|
+
updated_at: string;
|
|
1743
|
+
}>;
|
|
1744
|
+
count: number;
|
|
1745
|
+
}>;
|
|
1746
|
+
/** Idempotent upsert. OMIT `template_id` for the all-templates row; pass
|
|
1747
|
+
* a MUTEABLE id to mute just that template. Auth mail (`magic-link`,
|
|
1748
|
+
* `otp-code`) is a 422 — it always sends. There is no delete —
|
|
1749
|
+
* `muted: false` is the un-mute. */
|
|
1750
|
+
set: (input: {
|
|
1751
|
+
user_id?: string;
|
|
1752
|
+
template_id?: "welcome" | "transactional";
|
|
1753
|
+
muted: boolean;
|
|
1754
|
+
}) => Promise<{
|
|
1755
|
+
user_id: string;
|
|
1756
|
+
template_id: string | null;
|
|
1757
|
+
muted: boolean;
|
|
1758
|
+
updated_at: string;
|
|
1759
|
+
}>;
|
|
1524
1760
|
};
|
|
1525
1761
|
/** Deliveries the retry budget could not save (the dashboard "Resend" surface). */
|
|
1526
1762
|
deadLetters: {
|
|
@@ -1618,11 +1854,15 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
1618
1854
|
};
|
|
1619
1855
|
/** The planner (config-architect): describe an app in plain English and get an
|
|
1620
1856
|
* ADVISORY vxil plan — which features to enable, a materialized `config_draft`,
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1857
|
+
* which truly-unique logic needs a tenant function, and an architecture brief
|
|
1858
|
+
* (identity, data, money path, background work, limits, and the older patterns
|
|
1859
|
+
* vxil replaces). It PROPOSES a plan, it never applies anything (review it,
|
|
1860
|
+
* then push the config). Pass `{ brief: false }` to skip the brief.
|
|
1861
|
+
* Needs `features:read`. (POST /v1/plan; https://vxil.com/docs/guide/10-agents-and-mcp) */
|
|
1624
1862
|
readonly planner: {
|
|
1625
|
-
create: (description: string, name?: string
|
|
1863
|
+
create: (description: string, name?: string, opts?: {
|
|
1864
|
+
brief?: boolean;
|
|
1865
|
+
}) => Promise<VxilPlan>;
|
|
1626
1866
|
};
|
|
1627
1867
|
readonly audit: {
|
|
1628
1868
|
list: (q?: {
|
|
@@ -1859,11 +2099,27 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
1859
2099
|
session: AuthSession;
|
|
1860
2100
|
verified: boolean;
|
|
1861
2101
|
}>;
|
|
2102
|
+
/** Magic-link sign-in: vxil mails a single-use link to the address and the
|
|
2103
|
+
* click returns here with `?token=`. The request is always a 202 —
|
|
2104
|
+
* identical for a known and an unknown address (anti-enumeration). A
|
|
2105
|
+
* second request for the SAME address inside the per-identifier cooldown
|
|
2106
|
+
* (auth config `magicLink.resendCooldownSec`, default 60s, 0 disables) is
|
|
2107
|
+
* `429 magic_link_rate_limited` with a `Retry-After` header. */
|
|
1862
2108
|
magicLink: {
|
|
2109
|
+
/** `test_link` is present ONLY when the address matches auth config
|
|
2110
|
+
* `otp.testRecipients` (store-review / CI accounts): no mail is sent and
|
|
2111
|
+
* the full sign-in link comes back instead. `captcha_token` is required
|
|
2112
|
+
* when the tenant configured `security.captchaSecretRef`; `locale`
|
|
2113
|
+
* chooses the mail's language. */
|
|
1863
2114
|
request: (input: {
|
|
1864
2115
|
email: string;
|
|
1865
2116
|
redirect_url: string;
|
|
1866
|
-
|
|
2117
|
+
locale?: string;
|
|
2118
|
+
captcha_token?: string;
|
|
2119
|
+
}) => Promise<{
|
|
2120
|
+
sent: true;
|
|
2121
|
+
test_link?: string;
|
|
2122
|
+
}>;
|
|
1867
2123
|
verify: (token: string) => Promise<{
|
|
1868
2124
|
user_id: string;
|
|
1869
2125
|
session: AuthSession;
|
|
@@ -1933,6 +2189,37 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
1933
2189
|
}>;
|
|
1934
2190
|
};
|
|
1935
2191
|
};
|
|
2192
|
+
/** E-mail CHANGE for a signed-in (non-anonymous) user: a 6-digit code is
|
|
2193
|
+
* mailed to the NEW address; verify swaps the account's address (both the
|
|
2194
|
+
* auth row and the shared identity row, in one transaction), marks it
|
|
2195
|
+
* verified, and revokes every OTHER session of the user (reason
|
|
2196
|
+
* `email_changed`) — the presenting `token` keeps working. `request` is
|
|
2197
|
+
* always 202 whether or not the address is taken (no enumeration);
|
|
2198
|
+
* a taken address surfaces at `verify` as 409 `email_in_use`. A guest
|
|
2199
|
+
* session is 409 `anonymous_user` — guests use `anonymous.link`. Shares
|
|
2200
|
+
* the OTP knobs (cooldown → 429 `otp_rate_limited`, attempts, TTL,
|
|
2201
|
+
* `test_code` for `otp.testRecipients`) but does NOT need `otp.enabled`. */
|
|
2202
|
+
email: {
|
|
2203
|
+
change: {
|
|
2204
|
+
request: (input: {
|
|
2205
|
+
token: string;
|
|
2206
|
+
new_email: string;
|
|
2207
|
+
locale?: string;
|
|
2208
|
+
}) => Promise<{
|
|
2209
|
+
sent: true;
|
|
2210
|
+
test_code?: string;
|
|
2211
|
+
}>;
|
|
2212
|
+
verify: (input: {
|
|
2213
|
+
token: string;
|
|
2214
|
+
new_email: string;
|
|
2215
|
+
code: string;
|
|
2216
|
+
}) => Promise<{
|
|
2217
|
+
user_id: string;
|
|
2218
|
+
email: string;
|
|
2219
|
+
verified: true;
|
|
2220
|
+
}>;
|
|
2221
|
+
};
|
|
2222
|
+
};
|
|
1936
2223
|
/** Step-up re-auth: an OTP challenge on the CURRENT session. On verify the
|
|
1937
2224
|
* bearer token ROTATES (the returned session.token replaces the old one —
|
|
1938
2225
|
* swap it client-side) and the JWT gains an `elv` claim. */
|
|
@@ -2252,6 +2539,35 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
2252
2539
|
* the owner_field are never exposed. `false` closes the lane (and purges the
|
|
2253
2540
|
* edge cache). Read the public side with `cmsPublicUrl` / `listCmsPublic`. */
|
|
2254
2541
|
setPublic: (collection: string, isPublic: boolean) => Promise<void>;
|
|
2542
|
+
/** Re-project the collection's index slots after an `index_slot` move
|
|
2543
|
+
* (cms.md §19). Slots are projected on WRITE only, so until this runs,
|
|
2544
|
+
* stored rows keep their OLD projection: the new slot is NULL and the
|
|
2545
|
+
* VACATED slot still holds the old field's values — range/sort on the
|
|
2546
|
+
* moved field returns the WRONG rows, not merely missing ones. ONE page
|
|
2547
|
+
* per call (≤2000 rows, default 500), resumable via `next_cursor`,
|
|
2548
|
+
* idempotent (a second full pass reports `updated: 0`), and never a
|
|
2549
|
+
* version bump / CDC event (slots are derived columns). Server-key only.
|
|
2550
|
+
* `field` narrows the write to that field's slot plus any unowned slot. */
|
|
2551
|
+
reindex: (collection: string, opts?: {
|
|
2552
|
+
field?: string;
|
|
2553
|
+
cursor?: string;
|
|
2554
|
+
limit?: number;
|
|
2555
|
+
}) => Promise<CmsReindexPage>;
|
|
2556
|
+
/** Drive `reindex` to completion, page by page. Returns the totals.
|
|
2557
|
+
* A page that reports `skipped > 0` lost rows to a concurrent writer, so
|
|
2558
|
+
* it is re-run ONCE before the loop advances (the write is idempotent and
|
|
2559
|
+
* the retry is bounded — a permanently hot collection never spins). The
|
|
2560
|
+
* residual `skipped` is returned, not swallowed. */
|
|
2561
|
+
reindexAll: (collection: string, opts?: {
|
|
2562
|
+
field?: string;
|
|
2563
|
+
limit?: number;
|
|
2564
|
+
onPage?: (p: CmsReindexPage) => void;
|
|
2565
|
+
}) => Promise<{
|
|
2566
|
+
scanned: number;
|
|
2567
|
+
updated: number;
|
|
2568
|
+
skipped: number;
|
|
2569
|
+
pages: number;
|
|
2570
|
+
}>;
|
|
2255
2571
|
/** Replace the collection's per-record ACTION list (cms.md §17) — the
|
|
2256
2572
|
* `{ actions }` fields-route meta-op; `[]` clears. Each action is ONE
|
|
2257
2573
|
* human-initiated step: the dashboard renders it as a button per record,
|
|
@@ -2273,7 +2589,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
2273
2589
|
version: number;
|
|
2274
2590
|
data: Record<string, unknown>;
|
|
2275
2591
|
}>;
|
|
2276
|
-
|
|
2592
|
+
/** `expand` inlines relation/file fields into `data` (cms.md §6.2): the
|
|
2593
|
+
* full item envelope, a `{ object_id, $ref: 'files' }` stub for a file,
|
|
2594
|
+
* the bare id on a cycle/depth cut, `null` for an invisible target. */
|
|
2595
|
+
get: (collection: string, itemId: string, opts?: {
|
|
2596
|
+
expand?: readonly string[] | string;
|
|
2597
|
+
}) => Promise<Record<string, unknown>>;
|
|
2277
2598
|
/**
|
|
2278
2599
|
* The bounded query DSL. filter ops: $eq $ne $gt $gte $lt $lte $in
|
|
2279
2600
|
* $contains $startsWith (LIKE-escaped; substring-searchable on s*-slotted
|
|
@@ -2289,6 +2610,10 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
2289
2610
|
sort?: string;
|
|
2290
2611
|
limit?: number;
|
|
2291
2612
|
cursor?: string;
|
|
2613
|
+
/** relation/file fields to inline (`$expand`) — see `get`. The server
|
|
2614
|
+
* refuses `$expand` together with `count=true` (422 invalid_query), so
|
|
2615
|
+
* use `count()` for the aggregate. */
|
|
2616
|
+
expand?: readonly string[] | string;
|
|
2292
2617
|
}) => Promise<{
|
|
2293
2618
|
items: Array<Record<string, unknown>>;
|
|
2294
2619
|
next_cursor: string | null;
|
|
@@ -2313,6 +2638,22 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
2313
2638
|
cascaded: number;
|
|
2314
2639
|
set_null: number;
|
|
2315
2640
|
}>;
|
|
2641
|
+
/** N-22 (cms.md §20): BOUNDED FILTERED delete — soft-delete up to `limit`
|
|
2642
|
+
* items (1–100, default 25) matching `filter`, newest id first. A filter
|
|
2643
|
+
* is REQUIRED (an unselected sweep is refused 422). Each matched row runs
|
|
2644
|
+
* the SAME single-item delete path (restrict refusal, cascade budget,
|
|
2645
|
+
* unique-claim freeing, CDC), so this is N deletes in one round trip, not
|
|
2646
|
+
* a set-delete. `dryRun` reports `matched` and mutates nothing. Loop
|
|
2647
|
+
* while `deleted > 0`. A refusal mid-sweep (409 referenced / 422
|
|
2648
|
+
* cascade_too_large) stops the call — rows already deleted STAY deleted,
|
|
2649
|
+
* and the error message names how many. Soft-deleted rows are hard-purged
|
|
2650
|
+
* by the platform 30 days later (there is no per-tenant retention knob). */
|
|
2651
|
+
deleteMany: (collection: string, q: {
|
|
2652
|
+
filter: Record<string, unknown>;
|
|
2653
|
+
limit?: number;
|
|
2654
|
+
cursor?: string;
|
|
2655
|
+
dryRun?: boolean;
|
|
2656
|
+
}) => Promise<CmsBulkDeleteResult>;
|
|
2316
2657
|
publish: (collection: string, itemId: string) => Promise<Record<string, unknown>>;
|
|
2317
2658
|
/** P1-12 (cms.md §11.1): the BOUNDED reverse read — which live items
|
|
2318
2659
|
* reference this one, through which relation field. Owner-scoped in
|
|
@@ -2358,6 +2699,21 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
2358
2699
|
}>;
|
|
2359
2700
|
scanned: number;
|
|
2360
2701
|
}>;
|
|
2702
|
+
/** SERVER-ONLY (403 server_only in end-user mode). Account merge: move
|
|
2703
|
+
* every row `from_user_id` owns — across every collection that declares
|
|
2704
|
+
* an `owner_field`, soft-deleted rows included, the stored owner value
|
|
2705
|
+
* AND its indexed projection — onto `into_user_id`. ONE bounded page (≤500
|
|
2706
|
+
* rows) per call: loop while `done` is false. Idempotent (a second pass
|
|
2707
|
+
* moves 0). Bumps each moved row's `version`. The cms consumer of the
|
|
2708
|
+
* `auth.user.merged { from, into }` event; auth-v1 calls it after a
|
|
2709
|
+
* merge, the event is the backstop. Emits one `cms.items.rekeyed`. */
|
|
2710
|
+
reKey: (input: {
|
|
2711
|
+
from_user_id: string;
|
|
2712
|
+
into_user_id: string;
|
|
2713
|
+
}) => Promise<{
|
|
2714
|
+
moved: number;
|
|
2715
|
+
done: boolean;
|
|
2716
|
+
}>;
|
|
2361
2717
|
};
|
|
2362
2718
|
/** cms-rel B4 (cms.md §13): atomic multi-collection transaction — 1–5
|
|
2363
2719
|
* steps over ≤3 collections, per-step `$where` CAS preconditions (the
|
|
@@ -3195,6 +3551,22 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3195
3551
|
object_id: string;
|
|
3196
3552
|
expires_at: string | null;
|
|
3197
3553
|
}>;
|
|
3554
|
+
/** SERVER-ONLY (403 server_only in end-user mode). Account merge: move
|
|
3555
|
+
* every object `from_user_id` owns (every status, tombstones included) and
|
|
3556
|
+
* the shared links minted on them onto `into_user_id`, which must be a
|
|
3557
|
+
* live tenant user (404 user_not_found otherwise). ONE bounded page (≤500
|
|
3558
|
+
* objects) per call: loop while `done` is false. Idempotent. An erased
|
|
3559
|
+
* `from` user's objects are held back for the GDPR byte sweep (moved 0).
|
|
3560
|
+
* The files consumer of the `auth.user.merged { from, into }` event;
|
|
3561
|
+
* auth-v1 calls it after a merge, the event is the backstop. Emits one
|
|
3562
|
+
* `files.objects.rekeyed`. */
|
|
3563
|
+
reKey: (input: {
|
|
3564
|
+
from_user_id: string;
|
|
3565
|
+
into_user_id: string;
|
|
3566
|
+
}) => Promise<{
|
|
3567
|
+
moved: number;
|
|
3568
|
+
done: boolean;
|
|
3569
|
+
}>;
|
|
3198
3570
|
sharedLinks: {
|
|
3199
3571
|
/** Public (unauthenticated) URL for the object; revocable. Optional
|
|
3200
3572
|
* `max_downloads` (1 = one-time link) and an absolute `expires_at`; the
|
|
@@ -3304,7 +3676,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3304
3676
|
* openrouter) route via BYO keys in tenant_secrets. `images` on the generate
|
|
3305
3677
|
* inputs takes up to 8 vision refs: a public https:// URL, a
|
|
3306
3678
|
* data:image/...;base64 URL, or file:<object_id> (a files-feature object) —
|
|
3307
|
-
* fetched images are capped at 4 MiB each /
|
|
3679
|
+
* fetched images are capped at 4 MiB each; `documents` (pdf/text, ≤10 MiB
|
|
3680
|
+
* each) share one 20 MiB inline ceiling per request with them.
|
|
3308
3681
|
*/
|
|
3309
3682
|
readonly ai: {
|
|
3310
3683
|
templates: {
|
|
@@ -3362,6 +3735,27 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3362
3735
|
* over the template's stored schema. */
|
|
3363
3736
|
response_schema?: Record<string, unknown>;
|
|
3364
3737
|
images?: string[];
|
|
3738
|
+
/** DOCUMENT inputs (pdf / plain text), up to 8 refs, each a public https
|
|
3739
|
+
* URL, a `data:application/pdf;base64,…` (or `text/plain`) URL, or
|
|
3740
|
+
* `file:<object_id>`. Every ref is fetched + inlined server-side, so the
|
|
3741
|
+
* provider only ever sees base64. Needs a provider with the `documents`
|
|
3742
|
+
* capability (anthropic) — others answer 501 with a fixHint. */
|
|
3743
|
+
documents?: string[];
|
|
3744
|
+
/** PROMPT-CACHE breakpoints (anthropic; ignored elsewhere). `system`
|
|
3745
|
+
* caches the system prompt (tool definitions render before it and cache
|
|
3746
|
+
* with it); `messages` asks the provider to cache the transcript prefix;
|
|
3747
|
+
* `ttl` picks the 5-minute default or the 1-hour window. A cache READ
|
|
3748
|
+
* simply lowers the billed `input_tokens` — reported back as
|
|
3749
|
+
* `usage.cache_read_input_tokens` / `usage.cache_creation_input_tokens`
|
|
3750
|
+
* when the provider sends them. The minimum cacheable prefix is
|
|
3751
|
+
* model-dependent (512-4096 tokens); a shorter prompt silently does not
|
|
3752
|
+
* cache. The response cache key ignores this field (it changes billing,
|
|
3753
|
+
* not output). */
|
|
3754
|
+
cache?: {
|
|
3755
|
+
system?: boolean;
|
|
3756
|
+
messages?: boolean;
|
|
3757
|
+
ttl?: "5m" | "1h";
|
|
3758
|
+
};
|
|
3365
3759
|
user_id?: string;
|
|
3366
3760
|
}) => Promise<AiGeneration>;
|
|
3367
3761
|
/** Streamed generation: returns the channel + connect token immediately; open
|
|
@@ -3418,6 +3812,27 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3418
3812
|
* usage frame). */
|
|
3419
3813
|
response_schema?: Record<string, unknown>;
|
|
3420
3814
|
images?: string[];
|
|
3815
|
+
/** DOCUMENT inputs (pdf / plain text), up to 8 refs, each a public https
|
|
3816
|
+
* URL, a `data:application/pdf;base64,…` (or `text/plain`) URL, or
|
|
3817
|
+
* `file:<object_id>`. Every ref is fetched + inlined server-side, so the
|
|
3818
|
+
* provider only ever sees base64. Needs a provider with the `documents`
|
|
3819
|
+
* capability (anthropic) — others answer 501 with a fixHint. */
|
|
3820
|
+
documents?: string[];
|
|
3821
|
+
/** PROMPT-CACHE breakpoints (anthropic; ignored elsewhere). `system`
|
|
3822
|
+
* caches the system prompt (tool definitions render before it and cache
|
|
3823
|
+
* with it); `messages` asks the provider to cache the transcript prefix;
|
|
3824
|
+
* `ttl` picks the 5-minute default or the 1-hour window. A cache READ
|
|
3825
|
+
* simply lowers the billed `input_tokens` — reported back as
|
|
3826
|
+
* `usage.cache_read_input_tokens` / `usage.cache_creation_input_tokens`
|
|
3827
|
+
* when the provider sends them. The minimum cacheable prefix is
|
|
3828
|
+
* model-dependent (512-4096 tokens); a shorter prompt silently does not
|
|
3829
|
+
* cache. The response cache key ignores this field (it changes billing,
|
|
3830
|
+
* not output). */
|
|
3831
|
+
cache?: {
|
|
3832
|
+
system?: boolean;
|
|
3833
|
+
messages?: boolean;
|
|
3834
|
+
ttl?: "5m" | "1h";
|
|
3835
|
+
};
|
|
3421
3836
|
user_id?: string;
|
|
3422
3837
|
}) => Promise<AiJobHandle>;
|
|
3423
3838
|
/** Re-mint a fresh connect token for a LIVE stream (a generation that
|
|
@@ -3648,7 +4063,23 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3648
4063
|
* `quota` inlines one quota, `creditType` inlines the same owner-bound
|
|
3649
4064
|
* balance `getBalance` returns — ONE call for a thin client's paywall.
|
|
3650
4065
|
* A non-2xx answer means UNKNOWN: render the last cached answer, never
|
|
3651
|
-
* free (payments.md §3a).
|
|
4066
|
+
* free (payments.md §3a).
|
|
4067
|
+
*
|
|
4068
|
+
* OVERLAPPING SUBSCRIPTIONS — what `until` means. The WINNER is the
|
|
4069
|
+
* entitled subscription with the highest `tierMap[tier].rank` (default 0);
|
|
4070
|
+
* within a rank, the one with the latest period end (an open-ended manual
|
|
4071
|
+
* grant counts as unbounded), ties broken by subscription id, so the answer
|
|
4072
|
+
* is stable on every read. `entitlements` are the UNION and `quotas` the
|
|
4073
|
+
* MAX across ALL entitled rows, so a cancelled add-on never strips an
|
|
4074
|
+
* entitlement another live row still grants. `until` is the WINNER's OWN
|
|
4075
|
+
* boundary — not `max()` across rows, and not "when all access ends": it
|
|
4076
|
+
* answers *when does the CURRENT tier stop being good*. It is `null` for an
|
|
4077
|
+
* open-ended manual grant and on the `free` baseline. Across ranks a higher
|
|
4078
|
+
* tier wins even when a lower one runs longer, so `until` can report the
|
|
4079
|
+
* higher tier's earlier end; access does not dip (a lapsed winner re-folds
|
|
4080
|
+
* on read and the lower tier takes over), but a client that CACHES `until`
|
|
4081
|
+
* will show an early expiry — re-read instead of trusting a cached value
|
|
4082
|
+
* past it. */
|
|
3652
4083
|
getEntitlements: (userId: string, opts?: {
|
|
3653
4084
|
quota?: string;
|
|
3654
4085
|
creditType?: string;
|
|
@@ -3800,6 +4231,21 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3800
4231
|
* refund the full un-refunded remainder. Idempotency-Key REQUIRED — a retry
|
|
3801
4232
|
* replays the recorded refund (never a second provider refund); a refund can
|
|
3802
4233
|
* NEVER exceed the charge (422 refund_exceeds_charge).
|
|
4234
|
+
*
|
|
4235
|
+
* PADDLE: a PARTIAL refund is issued as a per-line-item adjustment on the
|
|
4236
|
+
* Paddle transaction and typically settles ASYNCHRONOUSLY — it comes back
|
|
4237
|
+
* `pending` until your Paddle account approves the adjustment, and the
|
|
4238
|
+
* approving webhook flips it to `succeeded`. A rejected adjustment flips it
|
|
4239
|
+
* to `failed`, releases the reserved amount back onto the charge and emits
|
|
4240
|
+
* `payments.refund.failed`. While a refund is `pending`, the credits the
|
|
4241
|
+
* charge bought are NOT yet reversed. Stripe and the mock settle inline.
|
|
4242
|
+
*
|
|
4243
|
+
* If the provider has nothing left to refund on the charge — e.g. a refund
|
|
4244
|
+
* was already issued in the provider's own dashboard, which vxil's running
|
|
4245
|
+
* total cannot see — the call returns 422 `refund_not_allocatable` with the
|
|
4246
|
+
* precise reason, and nothing is sent to the provider. It is deterministic:
|
|
4247
|
+
* retrying the same refund fails the same way. A provider call that actually
|
|
4248
|
+
* fails is still 502 `provider_error`.
|
|
3803
4249
|
*/
|
|
3804
4250
|
createRefund: (input: {
|
|
3805
4251
|
charge_id: string;
|
|
@@ -3896,7 +4342,11 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3896
4342
|
tier?: string;
|
|
3897
4343
|
}) => Promise<PaymentsSimulationResult>;
|
|
3898
4344
|
/** The last report-only reconciliation sweep result for this project
|
|
3899
|
-
* (`run: null` before the first daily tick).
|
|
4345
|
+
* (`run: null` before the first daily tick). Finding kinds:
|
|
4346
|
+
* `balance_drift`, `entitlement_lag`, `null_tier`, `refund_pending`,
|
|
4347
|
+
* `stranded_webhook`, `terminal_entitled`, `webhook_secret_missing`.
|
|
4348
|
+
* SERVER-SIDE KEYS ONLY — a thin-client (`end_user_required`) key gets
|
|
4349
|
+
* 403 `server_only`; the sweep is a tenant-wide operator surface. */
|
|
3900
4350
|
reconcile: () => Promise<{
|
|
3901
4351
|
run: {
|
|
3902
4352
|
run_id: string;
|
|
@@ -3935,7 +4385,11 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3935
4385
|
/** Provider webhook event log (payments.md §7 "Event log & replay"):
|
|
3936
4386
|
* operator visibility over every delivery — incl. persisted signature
|
|
3937
4387
|
* failures — plus an idempotent reprocess verb. Needs payments:read
|
|
3938
|
-
* (reprocess: payments:write).
|
|
4388
|
+
* (reprocess: payments:write).
|
|
4389
|
+
* SERVER-SIDE KEYS ONLY — a thin-client (`end_user_required`) key gets
|
|
4390
|
+
* 403 `server_only` on list, detail and reprocess: the event log is a
|
|
4391
|
+
* tenant-wide operator surface and the detail carries the provider
|
|
4392
|
+
* payload (other customers' emails, provider ids and amounts). */
|
|
3939
4393
|
webhookEvents: {
|
|
3940
4394
|
/** List deliveries newest-first (keyset-paginated; pass `cursor` from a
|
|
3941
4395
|
* prior page's next_cursor). List rows omit payload/raw_body. */
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,16 @@ export class VxilError extends Error {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
const DEFAULT_BASE = 'https://api.vxil.com';
|
|
51
|
+
/** Normalize the `expand` option to the `$expand` query value: a comma-joined
|
|
52
|
+
* list (the server's spelling), or `undefined` when nothing is expanded so the
|
|
53
|
+
* emitted URL is BYTE-IDENTICAL to a call without it. `qs()` percent-encodes
|
|
54
|
+
* the `$` in the key, which `url.searchParams.get('$expand')` decodes. */
|
|
55
|
+
function expandParam(e) {
|
|
56
|
+
if (e === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
const list = (typeof e === 'string' ? [e] : e).filter((x) => typeof x === 'string' && x.length > 0);
|
|
59
|
+
return list.length ? list.join(',') : undefined;
|
|
60
|
+
}
|
|
51
61
|
/** Resolve the API base. Precedence: explicit `baseUrl` option > the
|
|
52
62
|
* `VXIL_BASE_URL` env var (Node/server only — guarded via `globalThis`, so it's
|
|
53
63
|
* inert in browsers & Workers) > the production default. This lets an internal /
|
|
@@ -195,13 +205,21 @@ export class Vxil {
|
|
|
195
205
|
...(opts?.guard ? { guard: opts.guard } : {}),
|
|
196
206
|
...(opts?.guards ? { guards: opts.guards } : {}),
|
|
197
207
|
}),
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
// `expand` rides through as `$expand` on the wire; the envelope's `.data`
|
|
209
|
+
// already CARRIES the expanded members (cms-v1 replaces the stored id
|
|
210
|
+
// inside `data`), so the unwrap is unchanged and the overloads above are
|
|
211
|
+
// purely a type-level narrowing.
|
|
212
|
+
// (the two `as` casts pick the OVERLOADED member type: one runtime
|
|
213
|
+
// implementation serves both the plain and the `expand` overload, which
|
|
214
|
+
// differ only in the result TYPE — the wire call is the same.)
|
|
215
|
+
get: ((itemId, opts) => this.cms.items.get(c, itemId, opts).then(unwrap)),
|
|
216
|
+
query: ((q) => this.cms.items.query(c, q)
|
|
217
|
+
.then((r) => ({ items: r.items.map(unwrap), next_cursor: r.next_cursor }))),
|
|
201
218
|
count: (filter) => this.cms.items.count(c, filter),
|
|
202
219
|
patch: (itemId, data, opts) => this.cms.items.patch(c, itemId, data, opts).then(unwrap),
|
|
203
220
|
inc: (itemId, incs, opts) => this.cms.items.inc(c, itemId, incs, opts).then(unwrap),
|
|
204
221
|
delete: (itemId, opts) => this.cms.items.delete(c, itemId, opts),
|
|
222
|
+
deleteMany: (q) => this.cms.items.deleteMany(c, q),
|
|
205
223
|
publish: (itemId) => this.cms.items.publish(c, itemId).then(unwrap),
|
|
206
224
|
aggregate: (q) => this.cms.items.aggregate(c, q),
|
|
207
225
|
rank: (q) => this.cms.items.rank(c, q),
|
|
@@ -344,6 +362,50 @@ export class Vxil {
|
|
|
344
362
|
remove: async (email) => {
|
|
345
363
|
await this.call('DELETE', `/v1/notifications/suppressions/${encodeURIComponent(email)}`);
|
|
346
364
|
},
|
|
365
|
+
/** Bounce/complaint AGGREGATE over a 1–90 day window (default 30; an
|
|
366
|
+
* out-of-range or non-numeric `days` clamps rather than erroring).
|
|
367
|
+
* `counts` always carries all four reasons with explicit zeros; `rate` is
|
|
368
|
+
* `counts.total / sent` to 4 dp, or null when nothing was sent. `sent`
|
|
369
|
+
* counts deliveries the provider ACCEPTED, not inbox-delivered mail —
|
|
370
|
+
* this is a suppressions-per-accepted-send ratio, not an RFC bounce rate.
|
|
371
|
+
* Operator surface: a verified end-user token gets 403. */
|
|
372
|
+
stats: async (q) => {
|
|
373
|
+
const s = qs({ days: q?.days || undefined });
|
|
374
|
+
return (await this.call('GET', `/v1/notifications/suppressions/stats${s}`)).data;
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
/** Per-end-user MUTES — the opt-out table every send channel checks before
|
|
378
|
+
* it delivers. Absence of a row means "not muted". A row with no
|
|
379
|
+
* `template_id` is the ALL-TEMPLATES row.
|
|
380
|
+
*
|
|
381
|
+
* MUTE WINS. Enforcement is `(template_id = <template> OR template_id IS
|
|
382
|
+
* NULL) AND muted`, so the all-templates row SHADOWS a per-template
|
|
383
|
+
* `muted: false` — to re-enable one template after a master mute, clear the
|
|
384
|
+
* all-templates row (`{ muted: false }`, no `template_id`) and mute the
|
|
385
|
+
* others individually. `get` returns a computed `effective` per row, which
|
|
386
|
+
* is the answer enforcement gives; render THAT, not the stored `muted`.
|
|
387
|
+
*
|
|
388
|
+
* AUTH MAIL IS NEVER MUTEABLE. `magic-link` and `otp-code` carry sign-in
|
|
389
|
+
* links and codes: `set` rejects them with a 422 and the all-templates row
|
|
390
|
+
* does not cover them, so an "unsubscribe from everything" click can never
|
|
391
|
+
* lock a user out of its own account. Muteable ids: `welcome`,
|
|
392
|
+
* `transactional`. (Password-reset mail rides `transactional`, which stays
|
|
393
|
+
* muteable — see the notifications feature doc §6c.)
|
|
394
|
+
*
|
|
395
|
+
* SCOPES: with an end-user token both calls are confined to the verified
|
|
396
|
+
* principal and take `notifications:read` (a browser key that can mute its
|
|
397
|
+
* own mail must not also be able to send mail). With a server key
|
|
398
|
+
* `user_id` is required and `set` takes `notifications:send`. */
|
|
399
|
+
preferences: {
|
|
400
|
+
get: async (q) => {
|
|
401
|
+
const s = qs({ user_id: q.user_id || undefined });
|
|
402
|
+
return (await this.call('GET', `/v1/notifications/preferences${s}`)).data;
|
|
403
|
+
},
|
|
404
|
+
/** Idempotent upsert. OMIT `template_id` for the all-templates row; pass
|
|
405
|
+
* a MUTEABLE id to mute just that template. Auth mail (`magic-link`,
|
|
406
|
+
* `otp-code`) is a 422 — it always sends. There is no delete —
|
|
407
|
+
* `muted: false` is the un-mute. */
|
|
408
|
+
set: async (input) => (await this.call('PUT', '/v1/notifications/preferences', input)).data,
|
|
347
409
|
},
|
|
348
410
|
/** Deliveries the retry budget could not save (the dashboard "Resend" surface). */
|
|
349
411
|
deadLetters: {
|
|
@@ -400,13 +462,18 @@ export class Vxil {
|
|
|
400
462
|
};
|
|
401
463
|
/** The planner (config-architect): describe an app in plain English and get an
|
|
402
464
|
* ADVISORY vxil plan — which features to enable, a materialized `config_draft`,
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
465
|
+
* which truly-unique logic needs a tenant function, and an architecture brief
|
|
466
|
+
* (identity, data, money path, background work, limits, and the older patterns
|
|
467
|
+
* vxil replaces). It PROPOSES a plan, it never applies anything (review it,
|
|
468
|
+
* then push the config). Pass `{ brief: false }` to skip the brief.
|
|
469
|
+
* Needs `features:read`. (POST /v1/plan; https://vxil.com/docs/guide/10-agents-and-mcp) */
|
|
406
470
|
planner = {
|
|
407
|
-
create: async (description, name) => (await this.call('POST', '/v1/plan', {
|
|
471
|
+
create: async (description, name, opts) => (await this.call('POST', '/v1/plan', {
|
|
408
472
|
description,
|
|
409
473
|
...(name ? { name } : {}),
|
|
474
|
+
// sent ONLY when opting out, so every shipped call site's request body
|
|
475
|
+
// stays byte-identical
|
|
476
|
+
...(opts?.brief === false ? { brief: false } : {}),
|
|
410
477
|
})).data,
|
|
411
478
|
};
|
|
412
479
|
audit = {
|
|
@@ -557,10 +624,19 @@ export class Vxil {
|
|
|
557
624
|
auth = {
|
|
558
625
|
signUp: async (input) => (await this.call('POST', '/v1/auth/sign-up', input)).data,
|
|
559
626
|
signIn: async (input) => (await this.call('POST', '/v1/auth/sign-in', input)).data,
|
|
627
|
+
/** Magic-link sign-in: vxil mails a single-use link to the address and the
|
|
628
|
+
* click returns here with `?token=`. The request is always a 202 —
|
|
629
|
+
* identical for a known and an unknown address (anti-enumeration). A
|
|
630
|
+
* second request for the SAME address inside the per-identifier cooldown
|
|
631
|
+
* (auth config `magicLink.resendCooldownSec`, default 60s, 0 disables) is
|
|
632
|
+
* `429 magic_link_rate_limited` with a `Retry-After` header. */
|
|
560
633
|
magicLink: {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
634
|
+
/** `test_link` is present ONLY when the address matches auth config
|
|
635
|
+
* `otp.testRecipients` (store-review / CI accounts): no mail is sent and
|
|
636
|
+
* the full sign-in link comes back instead. `captcha_token` is required
|
|
637
|
+
* when the tenant configured `security.captchaSecretRef`; `locale`
|
|
638
|
+
* chooses the mail's language. */
|
|
639
|
+
request: async (input) => (await this.call('POST', '/v1/auth/magic-link/request', input)).data,
|
|
564
640
|
verify: async (token) => (await this.call('POST', '/v1/auth/magic-link/verify', { token })).data,
|
|
565
641
|
},
|
|
566
642
|
/** Email OTP sign-in: a 6-digit single-use code (distinct from magic-link).
|
|
@@ -588,6 +664,22 @@ export class Vxil {
|
|
|
588
664
|
verify: async (input) => (await this.call('POST', '/v1/auth/anonymous/link/verify', input)).data,
|
|
589
665
|
},
|
|
590
666
|
},
|
|
667
|
+
/** E-mail CHANGE for a signed-in (non-anonymous) user: a 6-digit code is
|
|
668
|
+
* mailed to the NEW address; verify swaps the account's address (both the
|
|
669
|
+
* auth row and the shared identity row, in one transaction), marks it
|
|
670
|
+
* verified, and revokes every OTHER session of the user (reason
|
|
671
|
+
* `email_changed`) — the presenting `token` keeps working. `request` is
|
|
672
|
+
* always 202 whether or not the address is taken (no enumeration);
|
|
673
|
+
* a taken address surfaces at `verify` as 409 `email_in_use`. A guest
|
|
674
|
+
* session is 409 `anonymous_user` — guests use `anonymous.link`. Shares
|
|
675
|
+
* the OTP knobs (cooldown → 429 `otp_rate_limited`, attempts, TTL,
|
|
676
|
+
* `test_code` for `otp.testRecipients`) but does NOT need `otp.enabled`. */
|
|
677
|
+
email: {
|
|
678
|
+
change: {
|
|
679
|
+
request: async (input) => (await this.call('POST', '/v1/auth/email/change/request', input)).data,
|
|
680
|
+
verify: async (input) => (await this.call('POST', '/v1/auth/email/change/verify', input)).data,
|
|
681
|
+
},
|
|
682
|
+
},
|
|
591
683
|
/** Step-up re-auth: an OTP challenge on the CURRENT session. On verify the
|
|
592
684
|
* bearer token ROTATES (the returned session.token replaces the old one —
|
|
593
685
|
* swap it client-side) and the JWT gains an `elv` claim. */
|
|
@@ -748,6 +840,59 @@ export class Vxil {
|
|
|
748
840
|
setPublic: async (collection, isPublic) => {
|
|
749
841
|
await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { public: isPublic });
|
|
750
842
|
},
|
|
843
|
+
/** Re-project the collection's index slots after an `index_slot` move
|
|
844
|
+
* (cms.md §19). Slots are projected on WRITE only, so until this runs,
|
|
845
|
+
* stored rows keep their OLD projection: the new slot is NULL and the
|
|
846
|
+
* VACATED slot still holds the old field's values — range/sort on the
|
|
847
|
+
* moved field returns the WRONG rows, not merely missing ones. ONE page
|
|
848
|
+
* per call (≤2000 rows, default 500), resumable via `next_cursor`,
|
|
849
|
+
* idempotent (a second full pass reports `updated: 0`), and never a
|
|
850
|
+
* version bump / CDC event (slots are derived columns). Server-key only.
|
|
851
|
+
* `field` narrows the write to that field's slot plus any unowned slot. */
|
|
852
|
+
reindex: async (collection, opts) => (await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/reindex`, {
|
|
853
|
+
...(opts?.field ? { field: opts.field } : {}),
|
|
854
|
+
...(opts?.cursor ? { cursor: opts.cursor } : {}),
|
|
855
|
+
...(opts?.limit ? { limit: opts.limit } : {}),
|
|
856
|
+
})).data,
|
|
857
|
+
/** Drive `reindex` to completion, page by page. Returns the totals.
|
|
858
|
+
* A page that reports `skipped > 0` lost rows to a concurrent writer, so
|
|
859
|
+
* it is re-run ONCE before the loop advances (the write is idempotent and
|
|
860
|
+
* the retry is bounded — a permanently hot collection never spins). The
|
|
861
|
+
* residual `skipped` is returned, not swallowed. */
|
|
862
|
+
reindexAll: async (collection, opts) => {
|
|
863
|
+
let cursor;
|
|
864
|
+
let scanned = 0;
|
|
865
|
+
let updated = 0;
|
|
866
|
+
let skipped = 0;
|
|
867
|
+
let pages = 0;
|
|
868
|
+
for (;;) {
|
|
869
|
+
let page = await this.cms.collections.reindex(collection, {
|
|
870
|
+
...(opts?.field ? { field: opts.field } : {}),
|
|
871
|
+
...(opts?.limit ? { limit: opts.limit } : {}),
|
|
872
|
+
...(cursor ? { cursor } : {}),
|
|
873
|
+
});
|
|
874
|
+
scanned += page.scanned;
|
|
875
|
+
updated += page.updated;
|
|
876
|
+
pages += 1;
|
|
877
|
+
opts?.onPage?.(page);
|
|
878
|
+
if ((page.skipped ?? 0) > 0) {
|
|
879
|
+
page = await this.cms.collections.reindex(collection, {
|
|
880
|
+
...(opts?.field ? { field: opts.field } : {}),
|
|
881
|
+
...(opts?.limit ? { limit: opts.limit } : {}),
|
|
882
|
+
...(cursor ? { cursor } : {}),
|
|
883
|
+
});
|
|
884
|
+
scanned += page.scanned;
|
|
885
|
+
updated += page.updated;
|
|
886
|
+
pages += 1;
|
|
887
|
+
opts?.onPage?.(page);
|
|
888
|
+
}
|
|
889
|
+
skipped += page.skipped ?? 0;
|
|
890
|
+
if (page.complete || !page.next_cursor)
|
|
891
|
+
break;
|
|
892
|
+
cursor = page.next_cursor;
|
|
893
|
+
}
|
|
894
|
+
return { scanned, updated, skipped, pages };
|
|
895
|
+
},
|
|
751
896
|
/** Replace the collection's per-record ACTION list (cms.md §17) — the
|
|
752
897
|
* `{ actions }` fields-route meta-op; `[]` clears. Each action is ONE
|
|
753
898
|
* human-initiated step: the dashboard renders it as a button per record,
|
|
@@ -760,7 +905,13 @@ export class Vxil {
|
|
|
760
905
|
/** `lock` serializes same-key writers (per-tenant advisory lock); `guard`
|
|
761
906
|
* is the declarative capacity/overlap invariant (requires lock) — cms.md §10. */
|
|
762
907
|
create: async (collection, input) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}`, input)).data,
|
|
763
|
-
|
|
908
|
+
/** `expand` inlines relation/file fields into `data` (cms.md §6.2): the
|
|
909
|
+
* full item envelope, a `{ object_id, $ref: 'files' }` stub for a file,
|
|
910
|
+
* the bare id on a cycle/depth cut, `null` for an invisible target. */
|
|
911
|
+
get: async (collection, itemId, opts) => {
|
|
912
|
+
const s = qs({ $expand: expandParam(opts?.expand) });
|
|
913
|
+
return (await this.call('GET', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}${s}`)).data;
|
|
914
|
+
},
|
|
764
915
|
/**
|
|
765
916
|
* The bounded query DSL. filter ops: $eq $ne $gt $gte $lt $lte $in
|
|
766
917
|
* $contains $startsWith (LIKE-escaped; substring-searchable on s*-slotted
|
|
@@ -777,6 +928,7 @@ export class Vxil {
|
|
|
777
928
|
sort: q?.sort || undefined,
|
|
778
929
|
limit: q?.limit || undefined,
|
|
779
930
|
cursor: q?.cursor || undefined,
|
|
931
|
+
$expand: expandParam(q?.expand),
|
|
780
932
|
});
|
|
781
933
|
return (await this.call('GET', `/v1/cms/items/${encodeURIComponent(collection)}${s}`)).data;
|
|
782
934
|
},
|
|
@@ -804,6 +956,22 @@ export class Vxil {
|
|
|
804
956
|
/** Returns the cascade tally (cms.md §11); `ifVersion` rides If-Match and
|
|
805
957
|
* a conflict aborts BEFORE any cascade side-effect. */
|
|
806
958
|
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,
|
|
959
|
+
/** N-22 (cms.md §20): BOUNDED FILTERED delete — soft-delete up to `limit`
|
|
960
|
+
* items (1–100, default 25) matching `filter`, newest id first. A filter
|
|
961
|
+
* is REQUIRED (an unselected sweep is refused 422). Each matched row runs
|
|
962
|
+
* the SAME single-item delete path (restrict refusal, cascade budget,
|
|
963
|
+
* unique-claim freeing, CDC), so this is N deletes in one round trip, not
|
|
964
|
+
* a set-delete. `dryRun` reports `matched` and mutates nothing. Loop
|
|
965
|
+
* while `deleted > 0`. A refusal mid-sweep (409 referenced / 422
|
|
966
|
+
* cascade_too_large) stops the call — rows already deleted STAY deleted,
|
|
967
|
+
* and the error message names how many. Soft-deleted rows are hard-purged
|
|
968
|
+
* by the platform 30 days later (there is no per-tenant retention knob). */
|
|
969
|
+
deleteMany: async (collection, q) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}/delete`, {
|
|
970
|
+
filter: q.filter,
|
|
971
|
+
...(q.limit !== undefined ? { limit: q.limit } : {}),
|
|
972
|
+
...(q.cursor ? { cursor: q.cursor } : {}),
|
|
973
|
+
...(q.dryRun !== undefined ? { dry_run: q.dryRun } : {}),
|
|
974
|
+
})).data,
|
|
807
975
|
publish: async (collection, itemId) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}/publish`)).data,
|
|
808
976
|
/** P1-12 (cms.md §11.1): the BOUNDED reverse read — which live items
|
|
809
977
|
* reference this one, through which relation field. Owner-scoped in
|
|
@@ -831,6 +999,15 @@ export class Vxil {
|
|
|
831
999
|
* row_number|rank|percent_rank, computed over ≤500 aggregated groups
|
|
832
1000
|
* (never raw rows), optional partitionBy. Same scan cap as aggregate. */
|
|
833
1001
|
rank: async (collection, body) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}/rank`, body)).data,
|
|
1002
|
+
/** SERVER-ONLY (403 server_only in end-user mode). Account merge: move
|
|
1003
|
+
* every row `from_user_id` owns — across every collection that declares
|
|
1004
|
+
* an `owner_field`, soft-deleted rows included, the stored owner value
|
|
1005
|
+
* AND its indexed projection — onto `into_user_id`. ONE bounded page (≤500
|
|
1006
|
+
* rows) per call: loop while `done` is false. Idempotent (a second pass
|
|
1007
|
+
* moves 0). Bumps each moved row's `version`. The cms consumer of the
|
|
1008
|
+
* `auth.user.merged { from, into }` event; auth-v1 calls it after a
|
|
1009
|
+
* merge, the event is the backstop. Emits one `cms.items.rekeyed`. */
|
|
1010
|
+
reKey: async (input) => (await this.call('POST', '/v1/cms/items/re-key', input)).data,
|
|
834
1011
|
},
|
|
835
1012
|
/** cms-rel B4 (cms.md §13): atomic multi-collection transaction — 1–5
|
|
836
1013
|
* steps over ≤3 collections, per-step `$where` CAS preconditions (the
|
|
@@ -1213,6 +1390,16 @@ export class Vxil {
|
|
|
1213
1390
|
/** Set / extend / clear an object's TTL. `expiresInSeconds: null` clears it
|
|
1214
1391
|
* (else a future auto-delete after the given seconds; minimum 60). */
|
|
1215
1392
|
setTtl: async (objectId, expiresInSeconds) => (await this.call('PUT', `/v1/files/${encodeURIComponent(objectId)}/ttl`, { expiresInSeconds })).data,
|
|
1393
|
+
/** SERVER-ONLY (403 server_only in end-user mode). Account merge: move
|
|
1394
|
+
* every object `from_user_id` owns (every status, tombstones included) and
|
|
1395
|
+
* the shared links minted on them onto `into_user_id`, which must be a
|
|
1396
|
+
* live tenant user (404 user_not_found otherwise). ONE bounded page (≤500
|
|
1397
|
+
* objects) per call: loop while `done` is false. Idempotent. An erased
|
|
1398
|
+
* `from` user's objects are held back for the GDPR byte sweep (moved 0).
|
|
1399
|
+
* The files consumer of the `auth.user.merged { from, into }` event;
|
|
1400
|
+
* auth-v1 calls it after a merge, the event is the backstop. Emits one
|
|
1401
|
+
* `files.objects.rekeyed`. */
|
|
1402
|
+
reKey: async (input) => (await this.call('POST', '/v1/files/objects/re-key', input)).data,
|
|
1216
1403
|
sharedLinks: {
|
|
1217
1404
|
/** Public (unauthenticated) URL for the object; revocable. Optional
|
|
1218
1405
|
* `max_downloads` (1 = one-time link) and an absolute `expires_at`; the
|
|
@@ -1280,7 +1467,8 @@ export class Vxil {
|
|
|
1280
1467
|
* openrouter) route via BYO keys in tenant_secrets. `images` on the generate
|
|
1281
1468
|
* inputs takes up to 8 vision refs: a public https:// URL, a
|
|
1282
1469
|
* data:image/...;base64 URL, or file:<object_id> (a files-feature object) —
|
|
1283
|
-
* fetched images are capped at 4 MiB each /
|
|
1470
|
+
* fetched images are capped at 4 MiB each; `documents` (pdf/text, ≤10 MiB
|
|
1471
|
+
* each) share one 20 MiB inline ceiling per request with them.
|
|
1284
1472
|
*/
|
|
1285
1473
|
ai = {
|
|
1286
1474
|
templates: {
|
|
@@ -1455,7 +1643,23 @@ export class Vxil {
|
|
|
1455
1643
|
* `quota` inlines one quota, `creditType` inlines the same owner-bound
|
|
1456
1644
|
* balance `getBalance` returns — ONE call for a thin client's paywall.
|
|
1457
1645
|
* A non-2xx answer means UNKNOWN: render the last cached answer, never
|
|
1458
|
-
* free (payments.md §3a).
|
|
1646
|
+
* free (payments.md §3a).
|
|
1647
|
+
*
|
|
1648
|
+
* OVERLAPPING SUBSCRIPTIONS — what `until` means. The WINNER is the
|
|
1649
|
+
* entitled subscription with the highest `tierMap[tier].rank` (default 0);
|
|
1650
|
+
* within a rank, the one with the latest period end (an open-ended manual
|
|
1651
|
+
* grant counts as unbounded), ties broken by subscription id, so the answer
|
|
1652
|
+
* is stable on every read. `entitlements` are the UNION and `quotas` the
|
|
1653
|
+
* MAX across ALL entitled rows, so a cancelled add-on never strips an
|
|
1654
|
+
* entitlement another live row still grants. `until` is the WINNER's OWN
|
|
1655
|
+
* boundary — not `max()` across rows, and not "when all access ends": it
|
|
1656
|
+
* answers *when does the CURRENT tier stop being good*. It is `null` for an
|
|
1657
|
+
* open-ended manual grant and on the `free` baseline. Across ranks a higher
|
|
1658
|
+
* tier wins even when a lower one runs longer, so `until` can report the
|
|
1659
|
+
* higher tier's earlier end; access does not dip (a lapsed winner re-folds
|
|
1660
|
+
* on read and the lower tier takes over), but a client that CACHES `until`
|
|
1661
|
+
* will show an early expiry — re-read instead of trusting a cached value
|
|
1662
|
+
* past it. */
|
|
1459
1663
|
getEntitlements: async (userId, opts) => {
|
|
1460
1664
|
const s = qs({
|
|
1461
1665
|
user_id: userId,
|
|
@@ -1527,6 +1731,21 @@ export class Vxil {
|
|
|
1527
1731
|
* refund the full un-refunded remainder. Idempotency-Key REQUIRED — a retry
|
|
1528
1732
|
* replays the recorded refund (never a second provider refund); a refund can
|
|
1529
1733
|
* NEVER exceed the charge (422 refund_exceeds_charge).
|
|
1734
|
+
*
|
|
1735
|
+
* PADDLE: a PARTIAL refund is issued as a per-line-item adjustment on the
|
|
1736
|
+
* Paddle transaction and typically settles ASYNCHRONOUSLY — it comes back
|
|
1737
|
+
* `pending` until your Paddle account approves the adjustment, and the
|
|
1738
|
+
* approving webhook flips it to `succeeded`. A rejected adjustment flips it
|
|
1739
|
+
* to `failed`, releases the reserved amount back onto the charge and emits
|
|
1740
|
+
* `payments.refund.failed`. While a refund is `pending`, the credits the
|
|
1741
|
+
* charge bought are NOT yet reversed. Stripe and the mock settle inline.
|
|
1742
|
+
*
|
|
1743
|
+
* If the provider has nothing left to refund on the charge — e.g. a refund
|
|
1744
|
+
* was already issued in the provider's own dashboard, which vxil's running
|
|
1745
|
+
* total cannot see — the call returns 422 `refund_not_allocatable` with the
|
|
1746
|
+
* precise reason, and nothing is sent to the provider. It is deterministic:
|
|
1747
|
+
* retrying the same refund fails the same way. A provider call that actually
|
|
1748
|
+
* fails is still 502 `provider_error`.
|
|
1530
1749
|
*/
|
|
1531
1750
|
createRefund: async (input, opts) => (await this.call('POST', '/v1/payments/refunds', input, { 'idempotency-key': opts.idempotencyKey })).data,
|
|
1532
1751
|
/** Where the user MANAGES their subscription, resolved by FUNDING SOURCE
|
|
@@ -1569,7 +1788,11 @@ export class Vxil {
|
|
|
1569
1788
|
* webhook path and judge it with the conformance oracle. */
|
|
1570
1789
|
simulate: async (input) => (await this.call('POST', '/v1/payments/simulate', input)).data,
|
|
1571
1790
|
/** The last report-only reconciliation sweep result for this project
|
|
1572
|
-
* (`run: null` before the first daily tick).
|
|
1791
|
+
* (`run: null` before the first daily tick). Finding kinds:
|
|
1792
|
+
* `balance_drift`, `entitlement_lag`, `null_tier`, `refund_pending`,
|
|
1793
|
+
* `stranded_webhook`, `terminal_entitled`, `webhook_secret_missing`.
|
|
1794
|
+
* SERVER-SIDE KEYS ONLY — a thin-client (`end_user_required`) key gets
|
|
1795
|
+
* 403 `server_only`; the sweep is a tenant-wide operator surface. */
|
|
1573
1796
|
reconcile: async () => (await this.call('GET', '/v1/payments/reconcile')).data,
|
|
1574
1797
|
/** List charges newest-first (the refund enabler — discover the charge_id).
|
|
1575
1798
|
* Filters: user_id, status, limit (clamped 1..100, default 50). */
|
|
@@ -1584,7 +1807,11 @@ export class Vxil {
|
|
|
1584
1807
|
/** Provider webhook event log (payments.md §7 "Event log & replay"):
|
|
1585
1808
|
* operator visibility over every delivery — incl. persisted signature
|
|
1586
1809
|
* failures — plus an idempotent reprocess verb. Needs payments:read
|
|
1587
|
-
* (reprocess: payments:write).
|
|
1810
|
+
* (reprocess: payments:write).
|
|
1811
|
+
* SERVER-SIDE KEYS ONLY — a thin-client (`end_user_required`) key gets
|
|
1812
|
+
* 403 `server_only` on list, detail and reprocess: the event log is a
|
|
1813
|
+
* tenant-wide operator surface and the detail carries the provider
|
|
1814
|
+
* payload (other customers' emails, provider ids and amounts). */
|
|
1588
1815
|
webhookEvents: {
|
|
1589
1816
|
/** List deliveries newest-first (keyset-paginated; pass `cursor` from a
|
|
1590
1817
|
* prior page's next_cursor). List rows omit payload/raw_body. */
|
package/package.json
CHANGED