@vxil/sdk 0.17.0 → 0.19.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.d.ts +482 -42
- package/dist/index.js +226 -29
- package/dist/retry.d.ts +8 -2
- package/dist/retry.js +11 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -429,9 +429,23 @@ export class Vxil {
|
|
|
429
429
|
return { data: parsed.data, meta: parsed.meta ?? { request_id: '' }, response: res };
|
|
430
430
|
}
|
|
431
431
|
users = {
|
|
432
|
+
/** Create or replace a tenant user by id. Upserting a deleted id brings it
|
|
433
|
+
* back; upserting an ERASED id re-creates it as a new, empty user
|
|
434
|
+
* (`erased_at` cleared — the erase already removed the old data). While
|
|
435
|
+
* the erased user's files are still being erased the call fails with 409
|
|
436
|
+
* `user_erase_pending`; retry later. `bulk` is all-or-nothing. */
|
|
432
437
|
upsert: async (user) => (await this.call('POST', '/v1/users', user)).data,
|
|
433
438
|
bulk: async (users) => (await this.call('POST', '/v1/users/bulk', { users })).data.upserted,
|
|
434
439
|
get: async (id, opts) => (await this.call('GET', `/v1/users/${encodeURIComponent(id)}${opts?.includeDeleted ? '?include_deleted=true' : ''}`)).data,
|
|
440
|
+
/**
|
|
441
|
+
* Update a user. `email` / `display_name` / `avatar_url` are replaced
|
|
442
|
+
* (null clears them). `attributes` is DEEP-MERGED into the stored
|
|
443
|
+
* attributes (RFC 7396 merge patch): objects merge key by key, arrays and
|
|
444
|
+
* scalars replace, and `null` deletes a key AT ANY DEPTH — including inside
|
|
445
|
+
* a subtree the row does not have yet, where the null is simply dropped
|
|
446
|
+
* (`{ attributes: { prefs: { phone: null } } }` never stores a null).
|
|
447
|
+
* Scope users:write.
|
|
448
|
+
*/
|
|
435
449
|
patch: async (id, patch) => (await this.call('PATCH', `/v1/users/${encodeURIComponent(id)}`, patch)).data,
|
|
436
450
|
/**
|
|
437
451
|
* Delete a user. By default this is a SOFT delete (sets deleted_at; the PII
|
|
@@ -449,7 +463,7 @@ export class Vxil {
|
|
|
449
463
|
* Merge a REGISTRY-ONLY user (a row you created with upsert that never
|
|
450
464
|
* signed in) INTO another user of the tenant: its attributes fill the
|
|
451
465
|
* survivor's gaps (the survivor wins on conflicts), the merged id is
|
|
452
|
-
* soft-deleted, its owner-scoped cms / files / payments rows move to the
|
|
466
|
+
* soft-deleted, its owner-scoped cms / files / payments / jobs rows move to the
|
|
453
467
|
* survivor, and `auth.user.merged { from, into, method: 'registry_merge' }`
|
|
454
468
|
* is audited. `rekeyed: false` means a re-key could not finish inside the
|
|
455
469
|
* request — the event is the backstop (call the feature's re-key route
|
|
@@ -567,7 +581,8 @@ export class Vxil {
|
|
|
567
581
|
await this.call('POST', `/v1/notifications/dead-letters/${encodeURIComponent(deliveryId)}/replay`);
|
|
568
582
|
},
|
|
569
583
|
},
|
|
570
|
-
/** A single delivery by id (the list is `deliveries()`)
|
|
584
|
+
/** A single delivery by id (the list is `deliveries()`), with its render —
|
|
585
|
+
* the full one for a `test_sink` row (see `DeliveryDetail`). */
|
|
571
586
|
delivery: async (deliveryId) => (await this.call('GET', `/v1/notifications/deliveries/${encodeURIComponent(deliveryId)}`)).data,
|
|
572
587
|
/** Email broadcast campaigns (guide ch. 6, notifications): audience-ref fan-out
|
|
573
588
|
* with quiet-hours + frequency-cap policy. `schedule_cron` sets a recurring
|
|
@@ -628,12 +643,24 @@ export class Vxil {
|
|
|
628
643
|
...(opts?.brief === false ? { brief: false } : {}),
|
|
629
644
|
})).data,
|
|
630
645
|
};
|
|
646
|
+
/** The project audit log. Needs `audit:read` (the narrow read-only scope —
|
|
647
|
+
* declare it on a function to read the log from its callback token) or
|
|
648
|
+
* `features:read`. Server-only: a public / thin-client key cannot read it. */
|
|
631
649
|
audit = {
|
|
650
|
+
/**
|
|
651
|
+
* The audit trail, newest first. `subject` narrows to the events about ONE
|
|
652
|
+
* thing: a user id (`payload.user_id` / `payload.id`), a cms record
|
|
653
|
+
* (`payload.item_id` — the record's history), a verified end user
|
|
654
|
+
* (`payload.end_user_id` — what that person changed through a thin-client
|
|
655
|
+
* key or a function acting for them, and their payments events), or a
|
|
656
|
+
* principal (`actor`). Server keys only.
|
|
657
|
+
*/
|
|
632
658
|
list: async (q) => {
|
|
633
659
|
const s = qs({
|
|
634
660
|
since: q?.since || undefined,
|
|
635
661
|
cursor: q?.cursor || undefined,
|
|
636
662
|
limit: q?.limit || undefined,
|
|
663
|
+
subject: q?.subject || undefined,
|
|
637
664
|
});
|
|
638
665
|
return (await this.call('GET', `/v1/audit${s}`)).data.events;
|
|
639
666
|
},
|
|
@@ -647,6 +674,7 @@ export class Vxil {
|
|
|
647
674
|
until: q?.until || undefined,
|
|
648
675
|
after_id: q?.after_id || undefined,
|
|
649
676
|
limit: q?.limit || undefined,
|
|
677
|
+
subject: q?.subject || undefined,
|
|
650
678
|
});
|
|
651
679
|
const { response: res, text } = await this.transport.send(`${this.base}${this.path(`/v1/audit/export${s}`)}`, {
|
|
652
680
|
method: 'GET',
|
|
@@ -756,7 +784,16 @@ export class Vxil {
|
|
|
756
784
|
*
|
|
757
785
|
* The answer's `generation_status` is `pending` on a fresh enqueue; a replay
|
|
758
786
|
* of the same `idempotency_key` (`deduplicated: true`) carries the run's
|
|
759
|
-
* status NOW (`processing`, `completed` or `failed` too).
|
|
787
|
+
* status NOW (`processing`, `completed` or `failed` too). The key is bound
|
|
788
|
+
* to the request BODY: re-sending it with a different body (another
|
|
789
|
+
* payload — a rebuilt `deadline_at` included —, provider, completion,
|
|
790
|
+
* timeout, reserve…) will answer 422 `idempotency_key_reused` naming the
|
|
791
|
+
* original run (the ai-v1 rule). That check is being rolled out: today a
|
|
792
|
+
* mismatch is only logged, on every project, and the send is still
|
|
793
|
+
* deduplicated to the first run; a changelog entry will announce
|
|
794
|
+
* enforcement — so keep the body deterministic now. A 409 `generation_in_progress`
|
|
795
|
+
* (Retry-After 5) means the run that key names is still placing its
|
|
796
|
+
* credit hold.
|
|
760
797
|
*
|
|
761
798
|
* `reserve_credits` takes a PROVISIONAL held credit debit at enqueue
|
|
762
799
|
* (linked to the run), committed on `completed` and reversed on
|
|
@@ -764,7 +801,8 @@ export class Vxil {
|
|
|
764
801
|
* `config.generation.maxReserveCredits` cap; in end-user mode `user_id` is
|
|
765
802
|
* FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
|
|
766
803
|
* the enqueue (insufficient balance; the run ends with job.generation.failed
|
|
767
|
-
* `ReserveInsufficient` and the 402 names its `run_id
|
|
804
|
+
* `ReserveInsufficient` and the 402 names its `run_id`; the key is released,
|
|
805
|
+
* so the same request after a top-up starts a new run); a runaway over the
|
|
768
806
|
* per-tenant outstanding-holds ceiling → 429 (Retry-After 5). Payments
|
|
769
807
|
* unreachable at the hold → 503 `payments_unavailable` (Retry-After 15;
|
|
770
808
|
* nothing started, nothing held — re-send the same request), unless
|
|
@@ -785,8 +823,9 @@ export class Vxil {
|
|
|
785
823
|
*
|
|
786
824
|
* `opts.retryOnCapacity: { maxWaitMs }` retries a CAPACITY 429
|
|
787
825
|
* (`generation_concurrency_exceeded`, `reserve_holds_exceeded`,
|
|
788
|
-
* `queue_full`)
|
|
789
|
-
*
|
|
826
|
+
* `queue_full`), a 503 `payments_unavailable` / `enqueue_interrupted` and a
|
|
827
|
+
* 409 `generation_in_progress` (the key's run is still placing its credit
|
|
828
|
+
* hold), honouring Retry-After with jitter, with the SAME
|
|
790
829
|
* idempotency_key (one is generated when the input has none), until
|
|
791
830
|
* maxWaitMs has passed — then the last 429 is thrown. */
|
|
792
831
|
generation: async (input, opts) => {
|
|
@@ -835,13 +874,45 @@ export class Vxil {
|
|
|
835
874
|
/** Cancel a run that has not started its current attempt (`queued`,
|
|
836
875
|
* `delayed`, `retrying`) or a plain run handed off to its signed callback
|
|
837
876
|
* (`waiting` after its handler answered 202 — the callback URL is
|
|
838
|
-
* consumed). A
|
|
839
|
-
*
|
|
840
|
-
*
|
|
877
|
+
* consumed). A generation's credit hold is released — a STARTED
|
|
878
|
+
* generation's too (its provider accepted it): it ends `failed`
|
|
879
|
+
* (`Cancelled`) and a late provider callback is a no-op, but the provider
|
|
880
|
+
* is NOT told, so its work and its charge may still complete. A plain
|
|
881
|
+
* `running` run (a delivery in flight), a run waiting on an event, or a
|
|
882
|
+
* terminal run answers `409 not_cancellable`. */
|
|
841
883
|
cancel: async (runId) => (await this.call('POST', `/v1/jobs/runs/${encodeURIComponent(runId)}/cancel`)).data,
|
|
842
884
|
/** Clone a terminal run into a fresh queued run. A generation run answers
|
|
843
|
-
* `409 not_replayable` — submit the generation again instead
|
|
885
|
+
* `409 not_replayable` — submit the generation again instead; a redacted
|
|
886
|
+
* or erased run answers `409 run_redacted` (its payload is gone). */
|
|
844
887
|
replay: async (runId) => (await this.call('POST', `/v1/jobs/runs/${encodeURIComponent(runId)}/replay`)).data,
|
|
888
|
+
/** SERVER-ONLY (403 server_only in end-user mode). Account merge: move the
|
|
889
|
+
* credit-hold owner (`reserve_credits.user_id`) of every run of
|
|
890
|
+
* `from_user_id`, in any state, onto `into_user_id`, so the surviving
|
|
891
|
+
* account reads, cancels and replays the generation runs it started as a
|
|
892
|
+
* guest. ONE bounded page (≤500 runs) per call — call again until `done`
|
|
893
|
+
* (idempotent: a second call moves 0). The jobs consumer of
|
|
894
|
+
* `auth.user.merged { from, into }`; auth and `users.merge` call it after
|
|
895
|
+
* a merge, the event is the backstop. Emits one `jobs.runs.rekeyed` when
|
|
896
|
+
* a call moved at least one run (none on a zero-move call). Unlike the
|
|
897
|
+
* files re-key it does not check that `into_user_id` exists.
|
|
898
|
+
* Scope jobs:write. */
|
|
899
|
+
reKey: async (input) => (await this.call('POST', '/v1/jobs/runs/re-key', input)).data,
|
|
900
|
+
/** SERVER-ONLY (403 server_only in end-user mode). Redact FINISHED runs
|
|
901
|
+
* that hold personal data: the payload becomes `{ erased: true }`, the
|
|
902
|
+
* result, progress, callback body and error text are cleared, a
|
|
903
|
+
* generation's provider request body/headers are dropped and its credit
|
|
904
|
+
* hold's user id is pseudonymized (unless its settle is still owed). The
|
|
905
|
+
* row itself stays (state, timings, job_name). The same fields a GDPR
|
|
906
|
+
* erase clears on the runs it can link to a user — use this for runs
|
|
907
|
+
* nothing links to one (a plain queue run, an async function call).
|
|
908
|
+
* `{ run_ids }` (≤500, one call; open runs are left alone and counted in
|
|
909
|
+
* `skipped_open`) or `{ user_id }` (the generation runs whose credit hold
|
|
910
|
+
* names that user — ONE page of ≤500 per call, call again until `done`).
|
|
911
|
+
* Idempotent. Audited as `jobs.runs.redacted` (counts only; none when a
|
|
912
|
+
* call redacted nothing). NOT cleared: concurrency_key, debounce_key,
|
|
913
|
+
* idempotency_key and target_url — keep user ids out of those (hash them).
|
|
914
|
+
* A redacted run does not replay. Scope jobs:write. */
|
|
915
|
+
redact: async (input) => (await this.call('POST', '/v1/jobs/runs/redact', input)).data,
|
|
845
916
|
/**
|
|
846
917
|
* Suspend the RUNNING run until an event (call from the executing
|
|
847
918
|
* handler, then return 200 — the suspension wins). `state: 'resumed'` =
|
|
@@ -926,9 +997,10 @@ export class Vxil {
|
|
|
926
997
|
* existing account; swap tokens). Needs `anonymous.enabled`; a non-guest
|
|
927
998
|
* bearer is `409 not_anonymous`. */
|
|
928
999
|
request: async (input) => (await this.call('POST', '/v1/auth/magic-link/request', input)).data,
|
|
929
|
-
/**
|
|
930
|
-
*
|
|
931
|
-
*
|
|
1000
|
+
/** An unknown, expired or already-used link answers `401 invalid_token`
|
|
1001
|
+
* (request a new one with `request`). `opts.anonymous_token` is REQUIRED
|
|
1002
|
+
* for a link that was requested with one (the guest claim above): the
|
|
1003
|
+
* same guest session must present it, else `401 invalid_session`. `merged` is true only when the guest was
|
|
932
1004
|
* folded into an existing account (then `user_id` is that account), and
|
|
933
1005
|
* `rekeyed` rides beside it: true ⇒ the guest's payments / cms / files
|
|
934
1006
|
* rows were already moved onto `user_id` when this returned; false ⇒
|
|
@@ -958,8 +1030,10 @@ export class Vxil {
|
|
|
958
1030
|
/** On success the guest keeps its user id (`user_id` unchanged).
|
|
959
1031
|
* If the claimed email ALREADY has an account, the guest is MERGED
|
|
960
1032
|
* into it: `user_id` is the existing account, `merged: true`, and a
|
|
961
|
-
* fresh `session.token` (same session
|
|
962
|
-
* identity — swap it client-side
|
|
1033
|
+
* fresh `session.token` (the same session carried over to the merged
|
|
1034
|
+
* identity under a new session id — swap it client-side: the old
|
|
1035
|
+
* guest token is revoked, the refresh token keeps working; other
|
|
1036
|
+
* guest sessions are revoked).
|
|
963
1037
|
* `rekeyed` (present on a merge): true ⇒ the guest's payments / cms /
|
|
964
1038
|
* files rows were already moved onto `user_id` when this returned, so
|
|
965
1039
|
* the merged user's next request finds them; false ⇒ the move ran
|
|
@@ -1026,7 +1100,7 @@ export class Vxil {
|
|
|
1026
1100
|
import: async (input) => (await this.call('POST', '/v1/auth/users/import', input)).data,
|
|
1027
1101
|
/** END-USER MODE ONLY (`endUserToken` / `asEndUser`): the signed-in user
|
|
1028
1102
|
* deep-merges a bounded `attributes` object into its OWN shared identity
|
|
1029
|
-
* row (null deletes a key; ≤8 KB, ≤64 top-level keys, depth ≤8). Profile
|
|
1103
|
+
* row (null deletes a key at any depth — RFC 7396; ≤8 KB, ≤64 top-level keys, depth ≤8). Profile
|
|
1030
1104
|
* fields are not patchable here. In server mode the call throws 422
|
|
1031
1105
|
* `end_user_mode_required` — use `users.patch(id, …)` instead. */
|
|
1032
1106
|
patchMe: async (attributes) => (await this.call('PATCH', '/v1/auth/users/me', { attributes })).data,
|
|
@@ -1145,15 +1219,33 @@ export class Vxil {
|
|
|
1145
1219
|
},
|
|
1146
1220
|
confirm: async (token) => (await this.call('POST', '/v1/auth/email/verify/confirm', { token })).data,
|
|
1147
1221
|
},
|
|
1148
|
-
/** Social sign-in. The web `
|
|
1149
|
-
* (
|
|
1150
|
-
* `GET /v1/auth/oauth/{provider}/start
|
|
1151
|
-
*
|
|
1222
|
+
/** Social sign-in. The web flow: `startUrl` fetches the provider authorize
|
|
1223
|
+
* URL with your key (a browser cannot follow the header-carrying 302 of
|
|
1224
|
+
* `GET /v1/auth/oauth/{provider}/start` itself), you navigate the browser
|
|
1225
|
+
* there, and hand the returned `code`+`state` to `…/callback`; `native`
|
|
1152
1226
|
* is the mobile / broker token-exchange the SDK wraps. `oidc` is the
|
|
1153
1227
|
* tenant's generic OIDC / SSO issuer (auth config `providers.oidc` —
|
|
1154
1228
|
* Okta / Entra / Auth0 / any OpenID Connect IdP, or a SAML broker that
|
|
1155
1229
|
* speaks OIDC); it rides the same three routes. */
|
|
1156
1230
|
oauth: {
|
|
1231
|
+
/** Start the web sign-in from a browser SPA: answers the provider
|
|
1232
|
+
* `authorize_url` (PKCE challenge, one-shot `state`, and for `oidc` a
|
|
1233
|
+
* nonce bound to the flow — all held server-side) for you to navigate
|
|
1234
|
+
* to: `location.assign((await vx.auth.oauth.startUrl('oidc',
|
|
1235
|
+
* { redirect_uri })).authorize_url)`. The provider returns to
|
|
1236
|
+
* `redirect_uri` with `?code&state`, which your page sends to
|
|
1237
|
+
* `GET /v1/auth/oauth/{provider}/callback` for the session. The same
|
|
1238
|
+
* rules as the redirect form apply: `redirect_uri` must match the auth
|
|
1239
|
+
* config `security.allowedRedirectOrigins` when set (else
|
|
1240
|
+
* `422 redirect_not_allowed`), and the state expires after
|
|
1241
|
+
* `expires_in` seconds (600). `anonymous_token` (a guest bearer)
|
|
1242
|
+
* promotes or merges that guest at the callback. */
|
|
1243
|
+
startUrl: async (provider, input) => {
|
|
1244
|
+
// plain encodeURIComponent (no URLSearchParams — React Native ≤ 0.79)
|
|
1245
|
+
const q = `redirect_uri=${encodeURIComponent(input.redirect_uri)}`
|
|
1246
|
+
+ (input.anonymous_token ? `&anonymous_token=${encodeURIComponent(input.anonymous_token)}` : '');
|
|
1247
|
+
return (await this.call('GET', `/v1/auth/oauth/${encodeURIComponent(provider)}/start?${q}`, undefined, { accept: 'application/json' })).data;
|
|
1248
|
+
},
|
|
1157
1249
|
/** Native social sign-in: exchange a provider `id_token`/`access_token`
|
|
1158
1250
|
* for a vxil session (`linked` marks whether the user was created or
|
|
1159
1251
|
* matched to an existing identity). */
|
|
@@ -1164,6 +1256,9 @@ export class Vxil {
|
|
|
1164
1256
|
* (`linked: 'merged'`, `user_id` = that account, `rekeyed` says whether
|
|
1165
1257
|
* the guest's owner-scoped rows already moved — see
|
|
1166
1258
|
* `anonymous.link.verify`). */
|
|
1259
|
+
/** `nonce`: the value your app put on the IdP authorize request. For
|
|
1260
|
+
* When sent, the id_token's `nonce` claim must equal it (401
|
|
1261
|
+
* `oauth_token_invalid` otherwise); omitted, it is not checked. */
|
|
1167
1262
|
input) => (await this.call('POST', `/v1/auth/oauth/${encodeURIComponent(provider)}/native`, input)).data,
|
|
1168
1263
|
},
|
|
1169
1264
|
};
|
|
@@ -1274,6 +1369,19 @@ export class Vxil {
|
|
|
1274
1369
|
* (default) restores today's behaviour. Server keys are never affected.
|
|
1275
1370
|
* Audited; takes effect on the next request. */
|
|
1276
1371
|
setEndUserAccess: async (collection, access) => (await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { end_user_access: access })).data,
|
|
1372
|
+
/** Set (or clear, with `[]` / `null`) the collection's END-USER WRITE
|
|
1373
|
+
* ROLES (guide ch. 9): a verified end user must hold one of `roles` (the
|
|
1374
|
+
* session's verified role claims — orgs roles) to write the collection,
|
|
1375
|
+
* otherwise every write door answers `403 role_required`. Reads are
|
|
1376
|
+
* unaffected; server keys are never affected. Audited; takes effect on
|
|
1377
|
+
* the next write. */
|
|
1378
|
+
setWriteRoles: async (collection, roles) => (await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { write_roles: roles ?? [] })).data,
|
|
1379
|
+
/** Set (1..3650 days) or clear (`null`) the collection's RETENTION (guide
|
|
1380
|
+
* ch. 4): the nightly platform sweep soft-deletes live items created more
|
|
1381
|
+
* than `days` ago — bounded per project per night, no restore, purged at
|
|
1382
|
+
* least 30 days later like any delete, one `cms.collection.retention_swept` audit event
|
|
1383
|
+
* per collection per sweep. Audited (`cms.collection.retain_days.set`). */
|
|
1384
|
+
setRetention: async (collection, days) => (await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { retain_days: days })).data,
|
|
1277
1385
|
/** Re-project the collection's index slots after an `index_slot` move
|
|
1278
1386
|
* (guide ch. 4). Slots are projected on WRITE only, so until this runs,
|
|
1279
1387
|
* stored rows keep their OLD projection: the new slot is NULL and the
|
|
@@ -1342,6 +1450,16 @@ export class Vxil {
|
|
|
1342
1450
|
/** `lock` serializes same-key writers (a per-key lock); `guard`
|
|
1343
1451
|
* is the declarative capacity/overlap invariant (requires lock) — guide ch. 4. */
|
|
1344
1452
|
create: async (collection, input) => (await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}`, input)).data,
|
|
1453
|
+
/** Upsert by a declared `unique` field (guide ch. 4): creates the item, or
|
|
1454
|
+
* — when a live item already holds `data[field]` — merges `patch` into
|
|
1455
|
+
* THAT item instead (one transaction; update hooks, guards, version bump
|
|
1456
|
+
* and a `cms.item.updated` audit, exactly like `patch`; null clears a
|
|
1457
|
+
* field). `created` tells which happened (201 vs 200). Concurrent upserts
|
|
1458
|
+
* of one key serialize, so they never create a duplicate. */
|
|
1459
|
+
upsert: async (collection, input) => {
|
|
1460
|
+
const r = await this.call('POST', `/v1/cms/items/${encodeURIComponent(collection)}`, input);
|
|
1461
|
+
return { ...r.data, created: r.response.status === 201 };
|
|
1462
|
+
},
|
|
1345
1463
|
/** `expand` inlines relation/file fields into `data` (guide ch. 4): the
|
|
1346
1464
|
* full item envelope, a `{ object_id, $ref: 'files' }` stub for a file,
|
|
1347
1465
|
* the bare id on a cycle/depth cut, `null` for an invisible target. */
|
|
@@ -1351,7 +1469,9 @@ export class Vxil {
|
|
|
1351
1469
|
},
|
|
1352
1470
|
/**
|
|
1353
1471
|
* The bounded query DSL. filter ops: $eq $ne $gt $gte $lt $lte $in
|
|
1354
|
-
* $
|
|
1472
|
+
* ($ne is NULL-safe: a row whose field is absent or null counts as not
|
|
1473
|
+
* equal; on a dotted join term the row still needs a readable target,
|
|
1474
|
+
* so a null or dangling relation is excluded) $contains $startsWith (LIKE-escaped; substring-searchable on s*-slotted
|
|
1355
1475
|
* fields) $arrayContains/$anyOf (json/relation array containment, indexed);
|
|
1356
1476
|
* range/sort needs slot-indexed fields. This is the
|
|
1357
1477
|
* DELIBERATELY-UNTYPED escape hatch: it accepts everything the server
|
|
@@ -1582,6 +1702,17 @@ export class Vxil {
|
|
|
1582
1702
|
* `features:write`. (DELETE /v1/functions/:name)
|
|
1583
1703
|
*/
|
|
1584
1704
|
delete: async (name, opts) => (await this.call('DELETE', `/v1/functions/${encodeURIComponent(name)}`, undefined, opts?.ifMatch !== undefined ? { 'if-match': String(opts.ifMatch) } : {})).data,
|
|
1705
|
+
/**
|
|
1706
|
+
* Log records of ONE function, one per invocation (outcome, wall/cpu ms,
|
|
1707
|
+
* console lines, exceptions), oldest first. Pass the returned `next_since`
|
|
1708
|
+
* back as `since` to read only newer records (a tail); without it, the most
|
|
1709
|
+
* recent records of the last 24 hours. Kept 14 days. Needs
|
|
1710
|
+
* `functions:read`. (GET /v1/functions/:name/logs)
|
|
1711
|
+
*/
|
|
1712
|
+
logs: async (name, opts) => {
|
|
1713
|
+
const s = qs({ since: opts?.since || undefined });
|
|
1714
|
+
return (await this.call('GET', `/v1/functions/${encodeURIComponent(name)}/logs${s}`)).data;
|
|
1715
|
+
},
|
|
1585
1716
|
};
|
|
1586
1717
|
/** The MCP aggregation surface (the `mcp` feature). */
|
|
1587
1718
|
mcp = {
|
|
@@ -1720,7 +1851,19 @@ export class Vxil {
|
|
|
1720
1851
|
*/
|
|
1721
1852
|
test: async (subId) => (await this.call('POST', `/v1/webhooks/subscriptions/${encodeURIComponent(subId)}/test`)).data,
|
|
1722
1853
|
sources: {
|
|
1723
|
-
/** Register an inbound source; the receiver URL is returned ONCE.
|
|
1854
|
+
/** Register an inbound source; the receiver URL is returned ONCE.
|
|
1855
|
+
*
|
|
1856
|
+
* Destination: `forward_url` (your endpoint) OR `target_function` (a
|
|
1857
|
+
* deployed function that declares a bare `trigger: { kind: 'webhook' }`
|
|
1858
|
+
* binding — it receives `payload.event === 'inbound_webhook.received'`
|
|
1859
|
+
* with the inbound envelope as `payload.data`), never both; neither =
|
|
1860
|
+
* store only (read with `events.list`).
|
|
1861
|
+
*
|
|
1862
|
+
* `provider: 'hmac'` is the parameterized preset for any provider that
|
|
1863
|
+
* signs an HMAC of the raw body (Shopify, Intercom, Linear, Zendesk, …):
|
|
1864
|
+
* `verify` says which header, sha256|sha1, hex|base64, an optional prefix
|
|
1865
|
+
* and an optional timestamp header + tolerance. Like stripe/paddle/slack it
|
|
1866
|
+
* fails closed (401) until the source's signing secret is set. */
|
|
1724
1867
|
create: async (input) => (await this.call('POST', '/v1/webhooks/sources', input)).data,
|
|
1725
1868
|
list: async () => (await this.call('GET', '/v1/webhooks/sources')).data.sources,
|
|
1726
1869
|
delete: async (sourceId) => {
|
|
@@ -1731,11 +1874,21 @@ export class Vxil {
|
|
|
1731
1874
|
test: async (sourceId) => (await this.call('POST', `/v1/webhooks/sources/${encodeURIComponent(sourceId)}/test`)).data,
|
|
1732
1875
|
},
|
|
1733
1876
|
events: {
|
|
1877
|
+
/** Received events. Newest first, paged with `cursor`; or — with `after`
|
|
1878
|
+
* (your watermark, an event id) — OLDEST first from just after it, the
|
|
1879
|
+
* drain shape: store `next_after` and pass it back, `has_more` says
|
|
1880
|
+
* another page waits. The `after` drain answers only events received
|
|
1881
|
+
* at least 10 s ago (ids are not strictly arrival-ordered, so a fresher
|
|
1882
|
+
* event could otherwise commit below a stored watermark and be
|
|
1883
|
+
* skipped); `cursor` and `event_id` reads are not lagged. `event_id`
|
|
1884
|
+
* reads exactly one event. `cursor` and `after` are exclusive. */
|
|
1734
1885
|
list: async (q) => {
|
|
1735
1886
|
const s = qs({
|
|
1736
1887
|
source_id: q?.source_id || undefined,
|
|
1737
1888
|
status: q?.status || undefined,
|
|
1738
1889
|
cursor: q?.cursor || undefined,
|
|
1890
|
+
after: q?.after || undefined,
|
|
1891
|
+
event_id: q?.event_id || undefined,
|
|
1739
1892
|
limit: q?.limit || undefined,
|
|
1740
1893
|
});
|
|
1741
1894
|
return (await this.call('GET', `/v1/webhooks/events${s}`)).data;
|
|
@@ -1838,8 +1991,14 @@ export class Vxil {
|
|
|
1838
1991
|
* matching its TokenProvider type *structurally* (no import — a static
|
|
1839
1992
|
* import would break the single-file served sdk.mjs) that re-mints via
|
|
1840
1993
|
* POST /v1/realtime/tokens with `defaults` merged over the channel the
|
|
1841
|
-
* client asks for.
|
|
1842
|
-
*
|
|
1994
|
+
* client asks for. Two safe ways to use it:
|
|
1995
|
+
* - on a server, with a server key (`user_id` names the subject); or
|
|
1996
|
+
* - in a browser / mobile app, with a PUBLIC `end_user_required` key and
|
|
1997
|
+
* the signed-in user's session (`endUserToken` on the client): the mint
|
|
1998
|
+
* runs in end-user mode and the token's subject is FORCED to the
|
|
1999
|
+
* verified user (any `user_id` you pass is overridden), so no
|
|
2000
|
+
* backend of your own is needed.
|
|
2001
|
+
* Never ship a server key to a browser. */
|
|
1843
2002
|
tokenProvider: (defaults) => (ctx) => this.realtime.mintToken({ channel: ctx.channel, ...defaults }),
|
|
1844
2003
|
/** One-shot: mint a token AND open the WebSocket. Uses the global
|
|
1845
2004
|
* `WebSocket` (browser / Node ≥22). On older Node, pass a constructor —
|
|
@@ -1864,10 +2023,20 @@ export class Vxil {
|
|
|
1864
2023
|
* long after the mint, whether or not `files.ttl` is enabled; `null` =
|
|
1865
2024
|
* never. The answer's `expires_in` is the UPLOAD URL's life in seconds;
|
|
1866
2025
|
* `expires_at` is when the object expires (null = never). `user_id` is
|
|
1867
|
-
* required with a server key and omitted in end-user mode.
|
|
2026
|
+
* required with a server key and omitted in end-user mode.
|
|
2027
|
+
* `checksum_sha256` (the SHA-256 of the bytes, 64 hex or 44 base64
|
|
2028
|
+
* characters) is signed into the URL (send the answer's `upload_headers`
|
|
2029
|
+
* on the PUT) and kept with the upload: `complete` compares it with the
|
|
2030
|
+
* SHA-256 the store reports for the bytes — 409 `checksum_mismatch` when
|
|
2031
|
+
* they differ, 409 `checksum_unverified` when the store reports none —
|
|
2032
|
+
* and on success answers the verified `checksum_sha256`. */
|
|
1868
2033
|
createUploadUrl: async (input) => (await this.call('POST', '/v1/files/upload-url', input)).data,
|
|
1869
2034
|
/** Confirm the PUT (bytes verified, object → available). Answers the
|
|
1870
|
-
* object's `expires_at` (null = never)
|
|
2035
|
+
* object's `expires_at` (null = never) and `checksum_sha256` (hex; null
|
|
2036
|
+
* when the store reported none) — a repeated complete answers the same.
|
|
2037
|
+
* When the upload declared `checksum_sha256`: 409 `checksum_mismatch` if
|
|
2038
|
+
* the bytes hash differently, 409 `checksum_unverified` if the store
|
|
2039
|
+
* reports no checksum (the object stays pending either way). */
|
|
1871
2040
|
complete: async (objectId) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/complete`)).data,
|
|
1872
2041
|
downloadUrl: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/download-url`)).data.download_url,
|
|
1873
2042
|
/** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
|
|
@@ -2123,8 +2292,10 @@ export class Vxil {
|
|
|
2123
2292
|
* would ground on, with rerank + metadata boosts applied — no generation,
|
|
2124
2293
|
* no token spend. `boosts`/`rerank`/`min_score` override the rag config.
|
|
2125
2294
|
* `min_score` floors the EFFECTIVE `score`, which is a RANK value (about
|
|
2126
|
-
* 0.016–0.033 at the top): a floor above ~0.033 drops every hit.
|
|
2127
|
-
* relevance
|
|
2295
|
+
* 0.016–0.033 at the top): a floor above ~0.033 drops every hit. For a
|
|
2296
|
+
* relevance threshold use `min_similarity`, which floors each hit's cosine
|
|
2297
|
+
* `similarity` (-1..1; overrides config `retrieval.minSimilarity`; a hit
|
|
2298
|
+
* with no measured similarity, e.g. keyword mode, is kept). */
|
|
2128
2299
|
search: async (input) => (await this.call('POST', '/v1/rag/search', input)).data,
|
|
2129
2300
|
/** Ingest into the backing vector-search collection (chunk → embed → index). */
|
|
2130
2301
|
ingest: async (collection, input) => (await this.call('POST', `/v1/rag/ingest/${encodeURIComponent(collection)}`, input)).data,
|
|
@@ -2262,7 +2433,9 @@ export class Vxil {
|
|
|
2262
2433
|
* (no oversell). Pass `job_id` to make the debit PROVISIONAL (held, not yet
|
|
2263
2434
|
* committed): a linked jobs run that terminally fails auto-refunds the hold,
|
|
2264
2435
|
* a success settles it. Throws VxilError(402, 'INSUFFICIENT_CREDITS') when the
|
|
2265
|
-
* available balance can't cover `amount`.
|
|
2436
|
+
* available balance can't cover `amount`. Throws VxilError(409,
|
|
2437
|
+
* 'job_released') when `job_id` names a generation run that already ended
|
|
2438
|
+
* (cancelled or failed) before this hold landed: nothing is held.
|
|
2266
2439
|
*
|
|
2267
2440
|
* Ordered credits: pass `credit_types` (1–8, in spend order — e.g.
|
|
2268
2441
|
* `['free', 'subscription', 'topup']`) instead of `credit_type`; the WHOLE
|
|
@@ -2383,6 +2556,30 @@ export class Vxil {
|
|
|
2383
2556
|
* past-due-grace, cross-platform-unlock, transfer) through the mock
|
|
2384
2557
|
* webhook path and judge it with the conformance oracle. */
|
|
2385
2558
|
simulate: async (input) => (await this.call('POST', '/v1/payments/simulate', input)).data,
|
|
2559
|
+
/** SERVER-ONLY TEST CLOCK — move the end (`until`, past or future, within
|
|
2560
|
+
* 5 years of now) of a MANUAL grant or a purchase pass, to test expired /
|
|
2561
|
+
* export-window / renew-soon states on demand. Allowed only where no live
|
|
2562
|
+
* money can be involved: a `mock`-provider project, a project whose
|
|
2563
|
+
* provider runs in its sandbox (`paddle.sandbox`, `paypal.sandbox`, a
|
|
2564
|
+
* Stripe `sk_test_` key), or a dev/preview project — elsewhere 403
|
|
2565
|
+
* `test_clock_forbidden` (also for a pass linked to a non-sandbox charge).
|
|
2566
|
+
* 409 `provider_managed` for a provider subscription (use the provider's
|
|
2567
|
+
* own test clock); 409 `subscription_ended` for a revoked/refunded row.
|
|
2568
|
+
* The effect is a natural lapse's: the entitlements refold
|
|
2569
|
+
* (`payments.entitlement.changed`), a move into the past turns an active
|
|
2570
|
+
* row `lapsed` and emits `payments.subscription.lapsed` (`event_type:
|
|
2571
|
+
* 'test_clock'`, `environment: 'sandbox'`) once; a move back into the
|
|
2572
|
+
* future revives it (as `active` — a `trialing` row does not return to
|
|
2573
|
+
* `trialing`; a `past_due` row's status is left as it is). Only the end
|
|
2574
|
+
* moves: `since` (the period start) is never rewritten, so after a move
|
|
2575
|
+
* before it `since` > `until`. Every move emits
|
|
2576
|
+
* `payments.test_clock.moved`. Both events are emitted after the move
|
|
2577
|
+
* commits, best-effort (as the period-end sweep's are): if one fails to
|
|
2578
|
+
* record, the move still stands and is not re-announced on a retry, so
|
|
2579
|
+
* assert on the returned `status` / `lapsed` rather than only on the
|
|
2580
|
+
* event. Same-tier passes stacked behind the moved one are not
|
|
2581
|
+
* re-chained. No credits are granted or reversed. */
|
|
2582
|
+
moveSubscriptionUntil: async (subscriptionId, until) => (await this.call('POST', `/v1/payments/subscriptions/${encodeURIComponent(subscriptionId)}/until`, { until: typeof until === 'string' ? until : until.toISOString() })).data,
|
|
2386
2583
|
/** The last report-only reconciliation sweep result for this project
|
|
2387
2584
|
* (`run: null` before the first daily tick). Finding kinds:
|
|
2388
2585
|
* `balance_drift`, `entitlement_lag`, `null_tier`, `refund_pending`,
|
package/dist/retry.d.ts
CHANGED
|
@@ -55,6 +55,11 @@ export declare const CAPACITY_ERROR_CODES: ReadonlySet<string>;
|
|
|
55
55
|
* started and nothing held, and the SAME request re-sent after Retry-After
|
|
56
56
|
* starts the run (the refused run released its idempotency key). */
|
|
57
57
|
export declare const UNAVAILABLE_ERROR_CODES: ReadonlySet<string>;
|
|
58
|
+
/** The 409 answers retried the same way: the run this idempotency_key names
|
|
59
|
+
* is still placing its credit hold (`generation_in_progress`, Retry-After 5)
|
|
60
|
+
* — the SAME request re-sent then answers with that run (or, when the
|
|
61
|
+
* earlier enqueue was cut off, starts a fresh one). */
|
|
62
|
+
export declare const IN_PROGRESS_ERROR_CODES: ReadonlySet<string>;
|
|
58
63
|
/** Is this thrown error one `retryOnCapacity` waits out? Duck-typed on
|
|
59
64
|
* `{ status, code }` (VxilError's fields). */
|
|
60
65
|
export declare function isCapacityRetryable(e: unknown): boolean;
|
|
@@ -68,8 +73,9 @@ export interface RetryOnCapacityOptions {
|
|
|
68
73
|
now?: () => number;
|
|
69
74
|
}
|
|
70
75
|
/**
|
|
71
|
-
* Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`)
|
|
72
|
-
* a 503 `payments_unavailable` (`UNAVAILABLE_ERROR_CODES`)
|
|
76
|
+
* Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`),
|
|
77
|
+
* a 503 `payments_unavailable` / `enqueue_interrupted` (`UNAVAILABLE_ERROR_CODES`)
|
|
78
|
+
* or a 409 `generation_in_progress` (`IN_PROGRESS_ERROR_CODES`):
|
|
73
79
|
* wait the server's `Retry-After` (else 5 s) plus up to 20 % jitter (≤ 1 s,
|
|
74
80
|
* so many clients told the same second do not all return on it), and never
|
|
75
81
|
* past `maxWaitMs` since the first attempt — then the last 429 is rethrown.
|
package/dist/retry.js
CHANGED
|
@@ -166,7 +166,12 @@ export const CAPACITY_ERROR_CODES = new Set([
|
|
|
166
166
|
* lane could not reach payments to place its credit hold — nothing was
|
|
167
167
|
* started and nothing held, and the SAME request re-sent after Retry-After
|
|
168
168
|
* starts the run (the refused run released its idempotency key). */
|
|
169
|
-
export const UNAVAILABLE_ERROR_CODES = new Set(['payments_unavailable']);
|
|
169
|
+
export const UNAVAILABLE_ERROR_CODES = new Set(['payments_unavailable', 'enqueue_interrupted']);
|
|
170
|
+
/** The 409 answers retried the same way: the run this idempotency_key names
|
|
171
|
+
* is still placing its credit hold (`generation_in_progress`, Retry-After 5)
|
|
172
|
+
* — the SAME request re-sent then answers with that run (or, when the
|
|
173
|
+
* earlier enqueue was cut off, starts a fresh one). */
|
|
174
|
+
export const IN_PROGRESS_ERROR_CODES = new Set(['generation_in_progress']);
|
|
170
175
|
/** Is this thrown error one `retryOnCapacity` waits out? Duck-typed on
|
|
171
176
|
* `{ status, code }` (VxilError's fields). */
|
|
172
177
|
export function isCapacityRetryable(e) {
|
|
@@ -174,13 +179,15 @@ export function isCapacityRetryable(e) {
|
|
|
174
179
|
if (!err || typeof err.code !== 'string')
|
|
175
180
|
return false;
|
|
176
181
|
return (err.status === 429 && CAPACITY_ERROR_CODES.has(err.code))
|
|
177
|
-
|| (err.status === 503 && UNAVAILABLE_ERROR_CODES.has(err.code))
|
|
182
|
+
|| (err.status === 503 && UNAVAILABLE_ERROR_CODES.has(err.code))
|
|
183
|
+
|| (err.status === 409 && IN_PROGRESS_ERROR_CODES.has(err.code));
|
|
178
184
|
}
|
|
179
185
|
/** Fallback wait (ms) when a capacity 429 carries no Retry-After. */
|
|
180
186
|
const CAPACITY_DEFAULT_WAIT_MS = 5_000;
|
|
181
187
|
/**
|
|
182
|
-
* Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`)
|
|
183
|
-
* a 503 `payments_unavailable` (`UNAVAILABLE_ERROR_CODES`)
|
|
188
|
+
* Re-run `attempt` while it throws a CAPACITY 429 (`CAPACITY_ERROR_CODES`),
|
|
189
|
+
* a 503 `payments_unavailable` / `enqueue_interrupted` (`UNAVAILABLE_ERROR_CODES`)
|
|
190
|
+
* or a 409 `generation_in_progress` (`IN_PROGRESS_ERROR_CODES`):
|
|
184
191
|
* wait the server's `Retry-After` (else 5 s) plus up to 20 % jitter (≤ 1 s,
|
|
185
192
|
* so many clients told the same second do not all return on it), and never
|
|
186
193
|
* past `maxWaitMs` since the first attempt — then the last 429 is rethrown.
|
package/package.json
CHANGED