@vxil/sdk 0.13.0 → 0.14.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/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // (code/message/hint/fixUrl + request id) so callers — humans or agents —
4
4
  // can self-correct.
5
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
6
+ * guide ch. 5). `'v1'` is the only released major, so pinning
7
7
  * anything else (`apiVersion: 'v2'`) is a COMPILE error until a v2 GAs and
8
8
  * widens this union. Every path the SDK builds is major-versioned; the default
9
9
  * is the compile-time constant `'v1'` (never a floating `latest` alias). */
@@ -136,7 +136,7 @@ function resolveBase(explicit) {
136
136
  return (explicit ?? envBase ?? DEFAULT_BASE).replace(/\/$/, '');
137
137
  }
138
138
  /** Build the anonymous, KEYLESS public-delivery URL for a collection's PUBLISHED
139
- * items (roadmap §4.4) — `{base}/v1/cms/public/:tenantId/:collection[?…]`. This is
139
+ * items (guide ch. 4, "Draft and publish") — `{base}/v1/cms/public/:tenantId/:collection[?…]`. This is
140
140
  * the reader path a public blog/storefront/docs site hits with NO api key: the
141
141
  * edge mints a restricted read-only inner token bound to the URL tenant, forces
142
142
  * `status='published'`, and strips the collection's owner_field. PURE (no
@@ -155,7 +155,7 @@ export function cmsPublicUrl(tenantId, collection, query, opts) {
155
155
  return `${base}${path}${s}`;
156
156
  }
157
157
  /** Fetch a page of a collection's PUBLISHED items over the KEYLESS public lane
158
- * (roadmap §4.4) — NO api key, no `Vxil` client, no auth of any kind. This is the
158
+ * (guide ch. 4) — NO api key, no `Vxil` client, no auth of any kind. This is the
159
159
  * anonymous reader path (a blog/storefront/docs front-end). Returns the same
160
160
  * `{ items, next_cursor }` envelope the authed list does, but rows are stripped
161
161
  * to the public surface (`CmsPublicRow`) — drafts and the owner_field are never
@@ -182,6 +182,36 @@ export async function listCmsPublic(tenantId, collection, query, opts) {
182
182
  }
183
183
  return parsed.data ?? { items: [], next_cursor: null };
184
184
  }
185
+ /** POST a run's single-use signed `callback_url` (guide ch. 6, "Hand a run to
186
+ * an external worker") — KEYLESS: no API key, no `Vxil` client; the URL is the
187
+ * credential, so call this from the worker that finishes the job (a render
188
+ * farm, a GPU box, another queue's task). Idempotent: a repeat of a used URL
189
+ * answers `deduplicated: true`. A non-2xx throws `VxilError` (401 a forged or
190
+ * altered URL, 410 `callback_expired`, 413 `result_too_large`, 404 a run that
191
+ * did not opt in). */
192
+ export async function postRunCallback(callbackUrl, body, opts) {
193
+ const fetchImpl = opts?.fetch ?? fetch;
194
+ const res = await fetchImpl(callbackUrl, {
195
+ method: 'POST',
196
+ headers: { 'content-type': 'application/json' },
197
+ body: JSON.stringify(body),
198
+ });
199
+ const text = await res.text();
200
+ let parsed = {};
201
+ if (text.length > 0) {
202
+ try {
203
+ parsed = JSON.parse(text);
204
+ }
205
+ catch {
206
+ throw new VxilError(res.status, `http_${res.status}`, text.slice(0, 200), undefined, undefined, undefined, parseRetryAfter(res.headers.get('retry-after')));
207
+ }
208
+ }
209
+ if (!res.ok || parsed.error || !parsed.data) {
210
+ const e = parsed.error ?? { code: `http_${res.status}`, message: text.slice(0, 200) };
211
+ throw new VxilError(res.status, e.code, e.message, e.hint, e.fixUrl, parsed.meta?.request_id, parseRetryAfter(res.headers.get('retry-after')));
212
+ }
213
+ return parsed.data;
214
+ }
185
215
  export class Vxil {
186
216
  base;
187
217
  key;
@@ -254,7 +284,7 @@ export class Vxil {
254
284
  static connect(opts) {
255
285
  return new Vxil(opts);
256
286
  }
257
- /** Typed per-collection CMS handle (design §4.8). A thin wrapper over the
287
+ /** Typed per-collection CMS handle (guide ch. 5). A thin wrapper over the
258
288
  * generic `cms.items.*` methods — the wire calls are identical; the generated
259
289
  * `VxilSchema` supplies the field types. `vx.cms.items.*` stays as the
260
290
  * always-available un-generic fallback. */
@@ -291,7 +321,7 @@ export class Vxil {
291
321
  rank: (q) => this.cms.items.rank(c, q),
292
322
  };
293
323
  }
294
- /** Typed function invoke (design §4.5). `vx.fn.<name>(payload)` POSTs to
324
+ /** Typed function invoke (guide ch. 8). `vx.fn.<name>(payload)` POSTs to
295
325
  * /v1/fn/<name>; the generated `VxilSchema` types Input/Output (Level-0
296
326
  * opaque until a function declares a signature). A Proxy gives the
297
327
  * `vx.fn.<name>` accessor shape without enumerating names at runtime.
@@ -359,7 +389,7 @@ export class Vxil {
359
389
  });
360
390
  // An empty body on a 2xx is a valid "no content" success (e.g. 204 from
361
391
  // DELETE/markRead) — never an error. Only parse when there are bytes; a
362
- // void-returning caller ignores `data` anyway. (audit #115)
392
+ // void-returning caller ignores `data` anyway.
363
393
  let parsed = {};
364
394
  if (text.length > 0) {
365
395
  try {
@@ -395,7 +425,7 @@ export class Vxil {
395
425
  * (email/display_name/avatar_url/attributes) is scrubbed, while the id is
396
426
  * kept so cross-feature references stay intact. NOTE: a full account-delete
397
427
  * flow also calls the auth half — POST /v1/auth/users/:id/erase — to erase
398
- * the credential/session identity (see features/tenant-users.md §3).
428
+ * the credential/session identity (see guide ch. 6, auth).
399
429
  */
400
430
  delete: async (id, opts) => {
401
431
  await this.call('DELETE', `/v1/users/${encodeURIComponent(id)}${opts?.erase ? '?erase=true' : ''}`);
@@ -497,7 +527,7 @@ export class Vxil {
497
527
  * does not cover them, so an "unsubscribe from everything" click can never
498
528
  * lock a user out of its own account. Muteable ids: `welcome`,
499
529
  * `transactional`. (Password-reset mail rides `transactional`, which stays
500
- * muteable — see the notifications feature doc §6c.)
530
+ * muteable — see guide ch. 6, notifications.)
501
531
  *
502
532
  * SCOPES: with an end-user token both calls are confined to the verified
503
533
  * principal and take `notifications:read` (a browser key that can mute its
@@ -524,7 +554,7 @@ export class Vxil {
524
554
  },
525
555
  /** A single delivery by id (the list is `deliveries()`). */
526
556
  delivery: async (deliveryId) => (await this.call('GET', `/v1/notifications/deliveries/${encodeURIComponent(deliveryId)}`)).data,
527
- /** Email broadcast campaigns (notifications.md §11b): audience-ref fan-out
557
+ /** Email broadcast campaigns (guide ch. 6, notifications): audience-ref fan-out
528
558
  * with quiet-hours + frequency-cap policy. `schedule_cron` sets a recurring
529
559
  * send (status `scheduled`); omit it for a `draft`. */
530
560
  campaigns: {
@@ -557,12 +587,12 @@ export class Vxil {
557
587
  /** The tenant's enabled-feature summary (GET /v1/features). Privilege-
558
588
  * independent: any valid key may read it — it leaks no secrets, only which
559
589
  * features the tenant has turned on. Mirrors what the MCP tool-list
560
- * aggregation sees. (audit #113) */
590
+ * aggregation sees. */
561
591
  list: async () => (await this.call('GET', '/v1/features')).data.features,
562
592
  /** The FULL enabled-feature summary: `features` + `api_versions` (released
563
593
  * API majors per feature) + the additive `key` block — the CALLING key's
564
594
  * identity and per-tool MCP permissions (allowed_tools/denied_tools
565
- * patterns, api_keys 0057; mcp.md §6.7). `key` is absent for non-key
595
+ * patterns; guide ch. 10). `key` is absent for non-key
566
596
  * callers and for keys without explicit tool perms. `list()` stays the
567
597
  * stable flat-array shorthand. */
568
598
  summary: async () => (await this.call('GET', '/v1/features')).data,
@@ -610,7 +640,7 @@ export class Vxil {
610
640
  if (!res.ok) {
611
641
  // Preserve the structured error envelope on failure — the export path
612
642
  // is hand-rolled (no this.call), so without this a 403/429 would lose
613
- // its code/hint/requestId. (audit #119)
643
+ // its code/hint/requestId.
614
644
  let env = {};
615
645
  try {
616
646
  env = JSON.parse(text);
@@ -621,7 +651,7 @@ export class Vxil {
621
651
  }
622
652
  // Skip blank lines AND tolerate a single malformed NDJSON line rather
623
653
  // than aborting the whole batch (e.g. a truncated final line on a large
624
- // page) — collect parse failures so the caller can decide. (audit #116)
654
+ // page) — collect parse failures so the caller can decide.
625
655
  const events = [];
626
656
  const parseErrors = [];
627
657
  text.split('\n').forEach((l, i) => {
@@ -651,13 +681,20 @@ export class Vxil {
651
681
  jobs = {
652
682
  /** Enqueue a one-off job; Vxil POSTs a signed callback to target_url with
653
683
  * retries. deliver_after (ISO) / delay_seconds (≤ 30 d, at most one of the
654
- * two) defer the first delivery; > 12 h returns state 'delayed'. */
684
+ * two) defer the first delivery; > 12 h returns state 'delayed'.
685
+ *
686
+ * `callback: true` (or `{ ttl_seconds }`, 60 s..31 d; default 24 h) opts
687
+ * the run in to a KEYLESS signed `callback_url` (in the answer and on every
688
+ * delivery): an external worker POSTs it to complete / fail the run, report
689
+ * progress, or wake a `wait`. A handler that answers 202 HANDS the run off
690
+ * — it waits for that callback (dead-lettered as `CallbackTimeout` when the
691
+ * lifetime passes). See `postRunCallback`. */
655
692
  enqueue: async (input) => (await this.call('POST', '/v1/jobs/enqueue', input)).data,
656
693
  /** Atomic multi-enqueue (≤100 items; any invalid item rejects the whole
657
694
  * batch). Each item = the enqueue input, incl. per-item idempotency_key
658
695
  * and deliver_after/delay_seconds. Results align with the input order. */
659
696
  enqueueBatch: async (items) => (await this.call('POST', '/v1/jobs/enqueue-batch', { jobs: items })).data,
660
- /** Enqueue a long-running EXTERNAL generation run (jobs.md §11): Vxil calls
697
+ /** Enqueue a long-running EXTERNAL generation run (guide ch. 6, jobs): Vxil calls
661
698
  * the provider (BYO key), tracks completion via poll/webhook, mirrors a typed
662
699
  * generation_status onto a tenant record, enforces a built-in timeout, and
663
700
  * (on failure) fires the payments credit-reversal.
@@ -666,7 +703,7 @@ export class Vxil {
666
703
  * of the same `idempotency_key` (`deduplicated: true`) carries the run's
667
704
  * status NOW (`processing`, `completed` or `failed` too).
668
705
  *
669
- * `reserve_credits` (§11.8) takes a PROVISIONAL held credit debit at enqueue
706
+ * `reserve_credits` takes a PROVISIONAL held credit debit at enqueue
670
707
  * (linked to the run), committed on `completed` and reversed on
671
708
  * failed/timeout/DLQ. `amount` is positive-only and CLAMPED to the platform
672
709
  * `config.generation.maxReserveCredits` cap; in end-user mode `user_id` is
@@ -717,11 +754,12 @@ export class Vxil {
717
754
  replay: async (runId) => (await this.call('POST', `/v1/jobs/runs/${encodeURIComponent(runId)}/replay`)).data,
718
755
  /**
719
756
  * Suspend the RUNNING run until an event (call from the executing
720
- * handler, then return 200 — the suspension wins).
757
+ * handler, then return 200 — the suspension wins). `state: 'resumed'` =
758
+ * the event already fired (continue; `wakeup` is its payload). A run
759
+ * enqueued with `callback` also gets its `callback_url`: an external worker
760
+ * POSTing it wakes this wait with no API key.
721
761
  */
722
- wait: async (runId, input) => {
723
- await this.call('POST', `/v1/jobs/runs/${encodeURIComponent(runId)}/wait`, input);
724
- },
762
+ wait: async (runId, input) => (await this.call('POST', `/v1/jobs/runs/${encodeURIComponent(runId)}/wait`, input)).data,
725
763
  /** Wake every run waiting on the event. */
726
764
  emitEvent: async (event, payload) => (await this.call('POST', '/v1/jobs/events', { event, ...(payload ? { payload } : {}) })).data.woken,
727
765
  /** Secret for verifying X-Vxil-Jobs-Signature on your callback endpoints. */
@@ -1059,7 +1097,7 @@ export class Vxil {
1059
1097
  check: async (input) => (await this.call('POST', '/v1/rate-limits/check', input)).data,
1060
1098
  /** Per-identifier overrides layered over a policy: `pattern` matches the
1061
1099
  * RENDERED key (exact, or a `*`-glob where the longest literal prefix
1062
- * wins). Propagates to the check path within ≤30s (KV cacheTtl). */
1100
+ * wins). Propagates to the check path within ≤30s (cache TTL). */
1063
1101
  overrides: {
1064
1102
  list: async (policyId) => (await this.call('GET', `/v1/rate-limits/policies/${encodeURIComponent(policyId)}/overrides`)).data.overrides,
1065
1103
  create: async (policyId, input) => (await this.call('POST', `/v1/rate-limits/policies/${encodeURIComponent(policyId)}/overrides`, input)).data,
@@ -1078,7 +1116,7 @@ export class Vxil {
1078
1116
  addField: async (collection, field) => {
1079
1117
  await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, field);
1080
1118
  },
1081
- /** Set (or clear, with `null`/`[]`) a field's READ-ROLE gate (cms.md §18) —
1119
+ /** Set (or clear, with `null`/`[]`) a field's READ-ROLE gate (guide ch. 4) —
1082
1120
  * the same-type in-place alter on the fields route. Re-sends the field's
1083
1121
  * `type` (required by the alter path); every OTHER attribute the field
1084
1122
  * carries is re-sent from `rest`, because the alter overwrites the whole
@@ -1088,7 +1126,7 @@ export class Vxil {
1088
1126
  setFieldReadRoles: async (collection, field, type, roles, rest = {}) => {
1089
1127
  await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { ...rest, field, type, read_roles: roles ?? [] });
1090
1128
  },
1091
- /** Turn a field's EQUALITY INDEX on or off (cms.md §3 "Indexed equality")
1129
+ /** Turn a field's EQUALITY INDEX on or off (guide ch. 4 "Indexed equality")
1092
1130
  * — the same-type in-place alter on the fields route; `indexed` is
1093
1131
  * present-key, so no other attribute moves. Only for UNSLOTTED,
1094
1132
  * non-computed fields, ≤4 per collection (422 `eq_index_budget`).
@@ -1112,11 +1150,11 @@ export class Vxil {
1112
1150
  }
1113
1151
  },
1114
1152
  /** Set (or clear, with `null`) the collection's end-user owner-scope flag
1115
- * (design §5.1). Names an existing `string` field that holds the owner id. */
1153
+ * (guide ch. 4, "`ownerField`"). Names an existing `string` field that holds the owner id. */
1116
1154
  setOwnerField: async (collection, ownerField) => {
1117
1155
  await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { owner_field: ownerField });
1118
1156
  },
1119
- /** Toggle the collection's PUBLIC-delivery flag (roadmap §4.4). When `true`,
1157
+ /** Toggle the collection's PUBLIC-delivery flag (guide ch. 4). When `true`,
1120
1158
  * its PUBLISHED items become KEYLESS-readable via the anonymous public lane
1121
1159
  * (`GET {base}/v1/cms/public/:tenantId/:collection` — no api key); drafts and
1122
1160
  * the owner_field are never exposed. `false` closes the lane (and purges the
@@ -1125,7 +1163,7 @@ export class Vxil {
1125
1163
  await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { public: isPublic });
1126
1164
  },
1127
1165
  /** Re-project the collection's index slots after an `index_slot` move
1128
- * (cms.md §19). Slots are projected on WRITE only, so until this runs,
1166
+ * (guide ch. 4). Slots are projected on WRITE only, so until this runs,
1129
1167
  * stored rows keep their OLD projection: the new slot is NULL and the
1130
1168
  * VACATED slot still holds the old field's values — range/sort on the
1131
1169
  * moved field returns the WRONG rows, not merely missing ones. ONE page
@@ -1180,7 +1218,7 @@ export class Vxil {
1180
1218
  }
1181
1219
  return { scanned, updated, skipped, pages, ...(indexCertify ? { index_certify: indexCertify } : {}) };
1182
1220
  },
1183
- /** Replace the collection's per-record ACTION list (cms.md §17) — the
1221
+ /** Replace the collection's per-record ACTION list (guide ch. 4) — the
1184
1222
  * `{ actions }` fields-route meta-op; `[]` clears. Each action is ONE
1185
1223
  * human-initiated step: the dashboard renders it as a button per record,
1186
1224
  * and `items.runAction` invokes its deployed function. */
@@ -1189,10 +1227,10 @@ export class Vxil {
1189
1227
  },
1190
1228
  },
1191
1229
  items: {
1192
- /** `lock` serializes same-key writers (per-tenant advisory lock); `guard`
1193
- * is the declarative capacity/overlap invariant (requires lock) — cms.md §10. */
1230
+ /** `lock` serializes same-key writers (a per-key lock); `guard`
1231
+ * is the declarative capacity/overlap invariant (requires lock) — guide ch. 4. */
1194
1232
  create: async (collection, input) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}`, input)).data,
1195
- /** `expand` inlines relation/file fields into `data` (cms.md §6.2): the
1233
+ /** `expand` inlines relation/file fields into `data` (guide ch. 4): the
1196
1234
  * full item envelope, a `{ object_id, $ref: 'files' }` stub for a file,
1197
1235
  * the bare id on a cycle/depth cut, `null` for an invisible target. */
1198
1236
  get: async (collection, itemId, opts) => {
@@ -1224,9 +1262,9 @@ export class Vxil {
1224
1262
  });
1225
1263
  return (await this.call('GET', `/v1/cms/items/${encodeURIComponent(collection)}${s}`)).data;
1226
1264
  },
1227
- /** Merge-patch data keys; null clears a key. Concurrency opts (cms.md §9):
1265
+ /** Merge-patch data keys; null clears a key. Concurrency opts (guide ch. 4):
1228
1266
  * `ifVersion` → If-Match CAS; `if` → bounded field precondition against
1229
- * the locked row; `lock`/`guard` → the §10 serialization primitives. */
1267
+ * the locked row; `lock`/`guard` → the write-serialization primitives (guide ch. 4). */
1230
1268
  patch: async (collection, itemId, data, opts) => (await this.call('PATCH', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}`, {
1231
1269
  data,
1232
1270
  ...(opts?.if ? { if: opts.if } : {}),
@@ -1237,18 +1275,18 @@ export class Vxil {
1237
1275
  /** Atomic in-database increment — PATCH `{ $inc: {field: delta} }`, ONE
1238
1276
  * conditional UPDATE guarded by the field's validation min/max (the quota
1239
1277
  * shape), the optional `if` precondition, and If-Match. 409
1240
- * inc_out_of_bounds when the guard refuses (cms.md §9.3). */
1278
+ * inc_out_of_bounds when the guard refuses (guide ch. 4). */
1241
1279
  inc: async (collection, itemId, incs, opts) => (await this.call('PATCH', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}`, { $inc: incs, ...(opts?.if ? { if: opts.if } : {}) }, opts?.ifVersion !== undefined ? { 'if-match': String(opts.ifVersion) } : {})).data,
1242
- /** `{ count }` under the same bounded filter grammar (cms.md §9.4). 422
1280
+ /** `{ count }` under the same bounded filter grammar (guide ch. 4). 422
1243
1281
  * count_unavailable_with_read_hooks on beforeRead-hooked collections. */
1244
1282
  count: async (collection, filter) => {
1245
1283
  const s = qs({ count: 'true', filter: filter ? JSON.stringify(filter) : undefined });
1246
1284
  return (await this.call('GET', `/v1/cms/items/${encodeURIComponent(collection)}${s}`)).data.count;
1247
1285
  },
1248
- /** Returns the cascade tally (cms.md §11); `ifVersion` rides If-Match and
1286
+ /** Returns the cascade tally (guide ch. 4); `ifVersion` rides If-Match and
1249
1287
  * a conflict aborts BEFORE any cascade side-effect. */
1250
1288
  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,
1251
- /** N-22 (cms.md §20): BOUNDED FILTERED delete — soft-delete up to `limit`
1289
+ /** BOUNDED FILTERED delete — soft-delete up to `limit`
1252
1290
  * items (1–100, default 25) matching `filter`, newest id first. A filter
1253
1291
  * is REQUIRED (an unselected sweep is refused 422). Each matched row runs
1254
1292
  * the SAME single-item delete path (restrict refusal, cascade budget,
@@ -1268,7 +1306,7 @@ export class Vxil {
1268
1306
  ...(q.dryRun !== undefined ? { dry_run: q.dryRun } : {}),
1269
1307
  })).data,
1270
1308
  publish: async (collection, itemId) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}/publish`)).data,
1271
- /** P1-12 (cms.md §11.1): the BOUNDED reverse read — which live items
1309
+ /** The BOUNDED reverse read — which live items
1272
1310
  * reference this one, through which relation field. Owner-scoped in
1273
1311
  * end-user mode like `get`. `count` is the page returned (≤ limit, max
1274
1312
  * 100), never a total; `has_more` says the cap was hit. A DELETE refused
@@ -1278,19 +1316,19 @@ export class Vxil {
1278
1316
  const qs = opts?.limit !== undefined ? `?limit=${encodeURIComponent(String(opts.limit))}` : '';
1279
1317
  return (await this.call('GET', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}/backlinks${qs}`)).data;
1280
1318
  },
1281
- /** P1-7 (cms.md §17): run ONE declared per-record action — invokes the
1319
+ /** Run ONE declared per-record action — invokes the
1282
1320
  * action's deployed function with `{ collection, item_id, action, actor,
1283
1321
  * item }` and returns its result. 404 when the key is not declared;
1284
1322
  * 502 `action_failed` (with `upstream.code` = the function's error
1285
1323
  * class) when the function fails. Requires cms:write. */
1286
1324
  runAction: async (collection, itemId, key) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}/${encodeURIComponent(itemId)}/actions/${encodeURIComponent(key)}`)).data,
1287
- /** cms-rel B2 (cms.md §12): bounded group-by aggregate. fns count|sum|
1325
+ /** Bounded group-by aggregate. fns count|sum|
1288
1326
  * min|max|avg over index-slot-bound fields; groupBy ≤2 slot-bound
1289
1327
  * fields; filter = the full query DSL incl. ONE-hop dotted join terms
1290
1328
  * ({"channel.visibility":"public"}); scan capped at 50k rows → 422
1291
1329
  * window_too_large (narrow the window or materialize a read-model). */
1292
1330
  aggregate: async (collection, body) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}/aggregate`, body)).data,
1293
- /** cms-rel B3 (cms.md §12): window ranking over the aggregate — rank ∈
1331
+ /** Window ranking over the aggregate — rank ∈
1294
1332
  * row_number|rank|percent_rank, computed over ≤500 aggregated groups
1295
1333
  * (never raw rows), optional partitionBy. Same scan cap as aggregate. */
1296
1334
  rank: async (collection, body) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}/rank`, body)).data,
@@ -1304,13 +1342,13 @@ export class Vxil {
1304
1342
  * merge, the event is the backstop. Emits one `cms.items.rekeyed`. */
1305
1343
  reKey: async (input) => (await this.call('POST', '/v1/cms/items/re-key', input)).data,
1306
1344
  },
1307
- /** cms-rel B4 (cms.md §13): atomic multi-collection transaction — 1–5
1345
+ /** Atomic multi-collection transaction — 1–5
1308
1346
  * steps over ≤3 collections, per-step `$where` CAS preconditions (the
1309
1347
  * PATCH `if` grammar). All-or-nothing: any failed precondition/validation
1310
1348
  * rolls the WHOLE transaction back (409 precondition_failed names the
1311
1349
  * step). cms-internal only — no cross-feature effects inside the tx. */
1312
1350
  transaction: async (steps) => (await this.call('POST', '/v1/cms/transactions', { steps })).data,
1313
- /** P1-1 (cms.md §21): several get / query / create / patch / delete ops in
1351
+ /** Several get / query / create / patch / delete ops in
1314
1352
  * ONE round trip — the function-chain shape ("read 3 rows → patch 2 →
1315
1353
  * create 1" is one call, not six). ≤25 ops, each run through the SAME path
1316
1354
  * its single route uses (scopes, owner-scoping, hooks, guards, `if`,
@@ -1325,7 +1363,7 @@ export class Vxil {
1325
1363
  ops: ops.map((o) => ('expand' in o && o.expand !== undefined ? { ...o, expand: expandParam(o.expand) } : o)),
1326
1364
  ...(opts?.atomic !== undefined ? { atomic: opts.atomic } : {}),
1327
1365
  })).data,
1328
- /** cms-rel B5 (cms.md §12.4): declared read-models (config `readModels`
1366
+ /** Declared read-models (config `readModels`
1329
1367
  * bag) — list with last-run status, and "run now" materialization into
1330
1368
  * the rollup collection. */
1331
1369
  readModels: {
@@ -1373,7 +1411,7 @@ export class Vxil {
1373
1411
  /** Read the resolved DM config (defaults merged with the stored partial). */
1374
1412
  getConfig: async () => (await this.call('GET', '/v1/dm/config')).data,
1375
1413
  /** Write the DM master config — a partial merge (omitted leaves keep their
1376
- * current value), version-bumped and republished to the gate's KV key. */
1414
+ * current value), version-bumped and republished to the gate's cache. */
1377
1415
  setConfig: async (patch) => (await this.call('PUT', '/v1/dm/config', patch)).data,
1378
1416
  conversations: {
1379
1417
  /** Open (or, for a direct pair, return the existing) conversation. A
@@ -1436,7 +1474,7 @@ export class Vxil {
1436
1474
  /** The MCP aggregation surface (the `mcp` feature). */
1437
1475
  mcp = {
1438
1476
  /** Per-tenant secret for verifying the X-Vxil-Mcp-Signature header on custom
1439
- * MCP tool calls (mcp.md §6.5) — the mirror of jobs.signingSecret(). */
1477
+ * MCP tool calls (guide ch. 10) — the mirror of jobs.signingSecret(). */
1440
1478
  signingSecret: async () => (await this.call('GET', '/v1/mcp/signing-secret')).data.signing_secret,
1441
1479
  };
1442
1480
  /**
@@ -1521,7 +1559,7 @@ export class Vxil {
1521
1559
  markAll: async (userId) => this.feedsMark(userId, 'read'),
1522
1560
  /** Archive/dismiss groups (hidden from inbox + dropped from the badge). */
1523
1561
  archive: async (userId, scope) => this.feedsMark(userId, 'archive', scope),
1524
- /** The badge: { unseen, unread, total } (KV-cached over the authority). */
1562
+ /** The badge: { unseen, unread, total } (cached, recomputed on a miss). */
1525
1563
  unreadCount: async (userId) => (await this.call('GET', `/v1/feeds/notification/${encodeURIComponent(userId)}/count`)).data,
1526
1564
  },
1527
1565
  /**
@@ -1649,7 +1687,7 @@ export class Vxil {
1649
1687
  /** List an org's pending invitations. */
1650
1688
  list: async (orgId) => (await this.call('GET', `/v1/orgs/${encodeURIComponent(orgId)}/invitations`)).data.invitations,
1651
1689
  },
1652
- /** Custom tenant roles (permission-sets; migration orgs/0019). A custom role
1690
+ /** Custom tenant roles (permission-sets). A custom role
1653
1691
  * is a named set of permissions assignable like any built-in; the four
1654
1692
  * built-ins reproduce the fixed owner>admin>member>viewer lattice. */
1655
1693
  roles: {
@@ -1662,7 +1700,7 @@ export class Vxil {
1662
1700
  await this.call('DELETE', `/v1/orgs/roles/${encodeURIComponent(roleKey)}`);
1663
1701
  },
1664
1702
  },
1665
- /** Per-org/user resource ACL grants (migration orgs/0019): grant a permission
1703
+ /** Per-org/user resource ACL grants: grant a permission
1666
1704
  * on a specific resource string; `check(...,{ resource })` consults them. */
1667
1705
  acl: {
1668
1706
  grant: async (orgId, input) => (await this.call('POST', `/v1/orgs/${encodeURIComponent(orgId)}/acl`, input)).data,
@@ -1733,9 +1771,38 @@ export class Vxil {
1733
1771
  delete: async (objectId) => {
1734
1772
  await this.call('DELETE', `/v1/files/${encodeURIComponent(objectId)}`);
1735
1773
  },
1736
- /** Aggregate storage usage vs quotas (the FilesManager Storage panel). */
1774
+ /** Aggregate storage usage vs quotas (the FilesManager Storage panel).
1775
+ * `public_assets` = published copies vs the plan's published-bytes ceiling
1776
+ * (identical bytes count once). */
1737
1777
  usage: async () => (await this.call('GET', '/v1/files/usage')).data,
1738
- /** OCR / text extraction (files.md §1.1, BYO-key add-on). Small/mock inputs
1778
+ /** SERVER-ONLY (403 server_only in end-user mode). Publish an available
1779
+ * object to vxil's public asset host: a stable, content-addressed URL
1780
+ * (`https://cdn.vxil.app/<tenant>/<sha256>.<ext>`) served from a global
1781
+ * edge cache with `Cache-Control: public, max-age=31536000, immutable`,
1782
+ * playable video/audio and CORS from `publicAssets.corsOrigins`. Needs
1783
+ * `files.publicAssets.enabled`. Publishable: png/jpeg/webp/avif/gif,
1784
+ * mp4/webm, mp3/m4a/ogg, woff2/woff, json — never HTML or SVG (422
1785
+ * `content_type_not_publishable`). Counts against the plan's
1786
+ * published-bytes ceiling (422 `quota_exceeded`). Idempotent: an already
1787
+ * published object answers its existing URL. Emits
1788
+ * `files.object.published`. */
1789
+ publish: async (objectId) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/publish`)).data,
1790
+ /** SERVER-ONLY. Publish up to 100 objects (about 2 GiB of bytes) in one
1791
+ * call; a bad id lands in `errors[]` without failing the rest (ids past
1792
+ * the byte budget as `batch_budget_exceeded` — send them again),
1793
+ * `published[]` keeps your order. */
1794
+ publishMany: async (objectIds) => (await this.call('POST', '/v1/files/publish', { object_ids: objectIds })).data,
1795
+ /** SERVER-ONLY. Take an object off the public asset host. Its public copy
1796
+ * is deleted once no other object of yours has the same bytes, and the
1797
+ * edge cache stops serving it within about a minute and a half (a
1798
+ * browser that already downloaded it keeps its copy). Deleting the object
1799
+ * unpublishes it too. 404 when it is not published; 503
1800
+ * `unpublish_failed` when the public copy could not be removed right
1801
+ * then (it stays published — retry). Emits `files.object.unpublished`. */
1802
+ unpublish: async (objectId) => {
1803
+ await this.call('DELETE', `/v1/files/${encodeURIComponent(objectId)}/publish`);
1804
+ },
1805
+ /** OCR / text extraction (guide ch. 6, files, BYO-key add-on). Small/mock inputs
1739
1806
  * extract inline (status `available`); large inputs (or `async:true`) return
1740
1807
  * 202 with a `job_id` — poll `getText()`. */
1741
1808
  extractText: async (objectId, opts) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/extract-text`, opts ?? {})).data,
@@ -1818,7 +1885,7 @@ export class Vxil {
1818
1885
  * The AI substrate (the `ai` feature): store prompt TEMPLATES (config-as-code),
1819
1886
  * then generate (sync or streamed) and embed across providers. 'mock' is the
1820
1887
  * deterministic default; the real providers (openai/anthropic/gemini/azure/
1821
- * openrouter) route via BYO keys in tenant_secrets. `images` on the generate
1888
+ * openrouter) route via BYO keys in your project secrets. `images` on the generate
1822
1889
  * inputs takes up to 8 vision refs: a public https:// URL, a
1823
1890
  * data:image/...;base64 URL, or file:<object_id> (a files-feature object) —
1824
1891
  * fetched images are capped at 4 MiB each; `documents` (pdf/text, ≤10 MiB
@@ -1878,7 +1945,7 @@ export class Vxil {
1878
1945
  * outlives the ≤300s realtime token TTL); reconnect with `?since=<seq>`. */
1879
1946
  remintToken: async (generationId) => (await this.call('GET', `/v1/ai/generations/${encodeURIComponent(generationId)}/token`)).data,
1880
1947
  /** Resume a streamed generation after a dropped socket: every recorded frame
1881
- * with seq > `since` plus `done` (the §2a replay buffer — plain JSON, not
1948
+ * with seq > `since` plus `done` (the replay buffer — plain JSON, not
1882
1949
  * an SSE stream; the buffer lives 1 h). A settled JOB-lane generation is
1883
1950
  * served from its stored answer instead (30 days). `status` says where the
1884
1951
  * generation is; `expired: true` means it settled but its frames are gone
@@ -1929,7 +1996,7 @@ export class Vxil {
1929
1996
  /** Streamed grounded answer: citations + the channel handle up front, tokens
1930
1997
  * over the realtime channel. */
1931
1998
  stream: async (input) => (await this.call('POST', '/v1/rag/answer', { ...input, stream: true })).data,
1932
- /** Retrieval-only grounding preview (rag.md §1c): the exact chunks `answer`
1999
+ /** Retrieval-only grounding preview (guide ch. 6, rag): the exact chunks `answer`
1933
2000
  * would ground on, with rerank + metadata boosts applied — no generation,
1934
2001
  * no token spend. `boosts`/`rerank`/`min_score` override the rag config.
1935
2002
  * `min_score` floors the EFFECTIVE `score`, which is a RANK value (about
@@ -2028,7 +2095,7 @@ export class Vxil {
2028
2095
  * `quota` inlines one quota, `creditType` inlines the same owner-bound
2029
2096
  * balance `getBalance` returns — ONE call for a thin client's paywall.
2030
2097
  * A non-2xx answer means UNKNOWN: render the last cached answer, never
2031
- * free (payments.md §3a).
2098
+ * free (guide ch. 6, payments).
2032
2099
  *
2033
2100
  * OVERLAPPING SUBSCRIPTIONS — what `until` means. The WINNER is the
2034
2101
  * entitled subscription with the highest `tierMap[tier].rank` (default 0);
@@ -2114,14 +2181,14 @@ export class Vxil {
2114
2181
  return (await this.call('GET', `/v1/payments/subscriptions${s}`)).data.subscriptions;
2115
2182
  },
2116
2183
  /**
2117
- * Create a hosted-checkout session (payments.md §3). Redirect the buyer to
2184
+ * Create a hosted-checkout session (guide ch. 6, payments). Redirect the buyer to
2118
2185
  * the returned `url`; completion lands server-side via the provider webhook
2119
2186
  * (the matching session flips to completed, the charge/grant is folded).
2120
2187
  * Idempotency-Key REQUIRED — a retry replays the SAME session verbatim.
2121
2188
  */
2122
2189
  createCheckoutSession: async (input, opts) => (await this.call('POST', '/v1/payments/checkout-sessions', input, { 'idempotency-key': opts.idempotencyKey })).data,
2123
2190
  /**
2124
- * Refund a charge at-most-once (payments.md §6a). Omit `amount_cents` to
2191
+ * Refund a charge at-most-once (guide ch. 6, payments). Omit `amount_cents` to
2125
2192
  * refund the full un-refunded remainder. Idempotency-Key REQUIRED — a retry
2126
2193
  * replays the recorded refund (never a second provider refund); a refund can
2127
2194
  * NEVER exceed the charge (422 refund_exceeds_charge).
@@ -2241,7 +2308,7 @@ export class Vxil {
2241
2308
  });
2242
2309
  return (await this.call('GET', `/v1/payments/refunds${suffix}`)).data;
2243
2310
  },
2244
- /** Provider webhook event log (payments.md §7 "Event log & replay"):
2311
+ /** Provider webhook event log (guide ch. 6, payments "Event log & replay"):
2245
2312
  * operator visibility over every delivery — incl. persisted signature
2246
2313
  * failures — plus an idempotent reprocess verb. Needs payments:read
2247
2314
  * (reprocess: payments:write).
@@ -2276,5 +2343,5 @@ export class Vxil {
2276
2343
  };
2277
2344
  }
2278
2345
  // Failure reporting for tenant functions — the Sentry-envelope forwarder
2279
- // (roadmap §4.11 P0-4e). Zero dependencies; see reporting.ts.
2346
+ // (guide ch. 8). Zero dependencies; see reporting.ts.
2280
2347
  export { withReporting, report, buildEnvelope, parseDsn, exceptionEvent, reportServerErrors, REPORT_TIMEOUT_MS, } from './reporting.js';
package/dist/qs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Query-string builder — the SDK's replacement for the WHATWG `URLSearchParams`
2
- // at every call site (F7-44, the React-Native-clean SDK).
2
+ // at every call site (the React-Native-clean SDK).
3
3
  //
4
4
  // WHY (implementation note; `//` comments never reach the shipped .d.ts):
5
5
  // React Native supplies its own URLSearchParams polyfill, and up to RN 0.79
package/dist/reporting.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // withReporting — forward a tenant function's failure to the tenant's OWN
2
- // error reporter over the Sentry envelope HTTP format (roadmap §4.11 P0-4e,
3
- // techmaker evaluation P0-5's "SDK half", 2026-09-23).
2
+ // error reporter over the Sentry envelope HTTP format.
4
3
  //
5
4
  // NOT a Sentry SDK and NOT a dependency on one: the function sandbox is
6
5
  // Web-standard fetch/crypto only, so this is the plain ingestion envelope
package/dist/retry.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // The request seam: retry + timeout + hooks around the ONE `fetch` every SDK
2
- // request goes through (parity P1 #9), and the `Retry-After` parser
3
- // `VxilError.retryAfter` shares (F7-44). DEFAULT OFF — with no `retry`,
2
+ // request goes through, and the `Retry-After` parser
3
+ // `VxilError.retryAfter` shares. DEFAULT OFF — with no `retry`,
4
4
  // `timeoutMs` or `hooks` option the transport is a single `fetch` + `text()`,
5
5
  // exactly what `call()` did before.
6
6
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/sdk",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Typed client for the Vxil REST API (notifications, auth, jobs, files, cms, comments, webhooks, realtime, orgs, rate-limits).",