@vxil/sdk 0.16.1 → 0.18.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 +253 -26
- package/dist/index.js +99 -16
- package/dist/retry.d.ts +8 -2
- package/dist/retry.js +11 -4
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -160,7 +160,11 @@ export interface Delivery {
|
|
|
160
160
|
template_id: string;
|
|
161
161
|
locale: string;
|
|
162
162
|
to_email: string;
|
|
163
|
-
|
|
163
|
+
/** `test_sink` (2026-10-04): the recipient matched the project's
|
|
164
|
+
* `notifications.suppression.testRecipients` — rendered in full and recorded,
|
|
165
|
+
* but no provider was called and the send is not billed. Read what would
|
|
166
|
+
* have gone out with `notifications.delivery(id)` (`rendered`). */
|
|
167
|
+
status: 'queued' | 'sent' | 'failed' | 'suppressed' | 'test_sink';
|
|
164
168
|
attempts: number;
|
|
165
169
|
provider: string;
|
|
166
170
|
provider_message_id: string | null;
|
|
@@ -185,6 +189,27 @@ export interface Delivery {
|
|
|
185
189
|
* `delivery_id` on the 202.) */
|
|
186
190
|
deliver_after?: string | null;
|
|
187
191
|
}
|
|
192
|
+
/** One delivery by id (`notifications.delivery(id)`): the row plus a render.
|
|
193
|
+
* For a `test_sink` row `rendered` is the FULL render the send produced (its
|
|
194
|
+
* data, its locale — an `ar` template carries `dir="rtl"`) and `rendered_from`
|
|
195
|
+
* is `'send'` — EXCEPT auth mail (the `magic-link` / `otp-code` templates and
|
|
196
|
+
* the auth feature's password-reset / e-mail-verification mail) and a row
|
|
197
|
+
* sunk at delivery time (an address added to `testRecipients` after the send
|
|
198
|
+
* was queued): those are rendered WITHOUT the send's data (`rendered_from:
|
|
199
|
+
* 'send_redacted'`), so a sign-in link, code or reset link is never stored or
|
|
200
|
+
* readable. For every other row the template variables were never stored, so
|
|
201
|
+
* `rendered` is the template re-rendered with no data (`rendered_from:
|
|
202
|
+
* 'template'`), or null when it cannot be rendered. */
|
|
203
|
+
export interface DeliveryDetail extends Delivery {
|
|
204
|
+
last_error_msg?: string | null;
|
|
205
|
+
rendered: {
|
|
206
|
+
subject: string;
|
|
207
|
+
html: string;
|
|
208
|
+
text: string;
|
|
209
|
+
locale: string;
|
|
210
|
+
} | null;
|
|
211
|
+
rendered_from?: 'send' | 'send_redacted' | 'template';
|
|
212
|
+
}
|
|
188
213
|
/** An ADDITIVE, non-fatal note on a 202 send — today only `mock_provider`
|
|
189
214
|
* (the send is recorded but no email leaves vxil). */
|
|
190
215
|
export interface NotificationWarning {
|
|
@@ -403,9 +428,20 @@ export interface JobRun {
|
|
|
403
428
|
* problem (400 / 413 / 422 — e.g. a callback key the collection does not
|
|
404
429
|
* declare) is retried with only the status column (a progress update tries
|
|
405
430
|
* the declared `progress_fields` first), so an undeclared key no longer
|
|
406
|
-
* strands the row; this says what was dropped
|
|
407
|
-
*
|
|
431
|
+
* strands the row; this says what was dropped (`status_written`: whether
|
|
432
|
+
* the record holds the status word). Never flips the run. A later TERMINAL
|
|
433
|
+
* mirror that lands cleanly moves it to `last_mirror_error` (null here
|
|
434
|
+
* again); a non-terminal success leaves it. A write the record refuses
|
|
435
|
+
* because it already holds a terminal word, or because its
|
|
436
|
+
* `status_mirror.fence_field` names another run, is skipped quietly —
|
|
437
|
+
* never a mirror_error. */
|
|
408
438
|
mirror_error?: JobRunMirrorError | null;
|
|
439
|
+
/** the single-run read, generation runs only: the `mirror_error` a later
|
|
440
|
+
* clean terminal mirror healed; null otherwise */
|
|
441
|
+
last_mirror_error?: JobRunMirrorError | null;
|
|
442
|
+
/** the single-run read, generation runs only: when that healing terminal
|
|
443
|
+
* mirror landed (ISO); null otherwise */
|
|
444
|
+
mirror_ok_at?: string | null;
|
|
409
445
|
/** the single-run read only: the enqueue `concurrency_key` this run holds
|
|
410
446
|
* while running or waiting; null for an unkeyed run */
|
|
411
447
|
concurrency_key?: string | null;
|
|
@@ -458,6 +494,7 @@ export interface JobEnqueueResult {
|
|
|
458
494
|
export interface JobRunsQuery {
|
|
459
495
|
job_name?: string;
|
|
460
496
|
state?: JobRun['state'] | string;
|
|
497
|
+
/** exact run_ids, at most 50 distinct (more answers 422 — page in chunks of 50) */
|
|
461
498
|
ids?: string[];
|
|
462
499
|
batch_id?: string;
|
|
463
500
|
/** ISO: runs created at or after */
|
|
@@ -501,13 +538,25 @@ export interface JobRunMirrorError {
|
|
|
501
538
|
code: string | null;
|
|
502
539
|
/** the target's message (≤ 200 chars), e.g. "unknown field 'video_url'" */
|
|
503
540
|
message: string | null;
|
|
541
|
+
/** the target's hint (≤ 200 chars) when it sent one — e.g. the engine
|
|
542
|
+
* detail of a cms validate hook that could not evaluate */
|
|
543
|
+
hint?: string;
|
|
504
544
|
/** when it was refused (ISO) */
|
|
505
545
|
at: string;
|
|
546
|
+
/** whether the record holds `generation_status` after this write: true
|
|
547
|
+
* when a narrower retry carrying the status word landed (or the write left
|
|
548
|
+
* the word out because the record already held it); false when no write
|
|
549
|
+
* carrying it landed — the full write was already the status column alone,
|
|
550
|
+
* the refusal was not a body problem (401 / 403 / 404 / 409 / 429 / 5xx,
|
|
551
|
+
* a timeout), or every narrower retry was refused. Absent on errors
|
|
552
|
+
* recorded before it existed. */
|
|
553
|
+
status_written?: boolean;
|
|
506
554
|
/** present when a narrower retry LANDED: the keys that were not written */
|
|
507
555
|
fields_dropped?: string[];
|
|
508
|
-
/** present when
|
|
509
|
-
* status
|
|
510
|
-
*
|
|
556
|
+
/** present when narrower retries ran and none landed: the LAST one's HTTP
|
|
557
|
+
* status (0 = no answer). The ladder stops at the first refusal that is not
|
|
558
|
+
* a body problem, so read `status_written`, not this, for "did the status
|
|
559
|
+
* word land" */
|
|
511
560
|
retry_status?: number;
|
|
512
561
|
}
|
|
513
562
|
/** A run's latest progress report (`JobRun.progress`): only these keys pass. */
|
|
@@ -748,6 +797,28 @@ export interface PaymentsSimulationResult {
|
|
|
748
797
|
pass: boolean;
|
|
749
798
|
};
|
|
750
799
|
}
|
|
800
|
+
/** `payments.moveSubscriptionUntil` (the test clock) — the moved row and the
|
|
801
|
+
* user's entitlement snapshot after the refold. */
|
|
802
|
+
export interface PaymentsTestClockResult {
|
|
803
|
+
subscription_id: string;
|
|
804
|
+
user_id: string;
|
|
805
|
+
tier: string | null;
|
|
806
|
+
/** 'lapsed' after a move into the past, 'active' after one into the future */
|
|
807
|
+
status: string;
|
|
808
|
+
since: string | null;
|
|
809
|
+
until: string;
|
|
810
|
+
previous_until: string | null;
|
|
811
|
+
/** true when THIS move ended access (payments.subscription.lapsed emitted) */
|
|
812
|
+
lapsed: boolean;
|
|
813
|
+
/** why the project may use the test clock */
|
|
814
|
+
lane: 'mock' | 'sandbox' | 'dev';
|
|
815
|
+
entitlement: {
|
|
816
|
+
tier: string;
|
|
817
|
+
entitlements: string[];
|
|
818
|
+
quotas: Record<string, number>;
|
|
819
|
+
source_sub_id: string | null;
|
|
820
|
+
};
|
|
821
|
+
}
|
|
751
822
|
/** The ONE complete entitlement read (GET /v1/payments/entitlements; guide ch. 6,
|
|
752
823
|
* payments). `until` is the winning subscription's current_period_end (+ the
|
|
753
824
|
* configured grace window when it is past_due); null on the free baseline. */
|
|
@@ -880,6 +951,10 @@ export interface FileUploadUrl {
|
|
|
880
951
|
expires_in: number;
|
|
881
952
|
/** When the OBJECT expires (null = never). */
|
|
882
953
|
expires_at: string | null;
|
|
954
|
+
/** The headers the PUT to `upload_url` must send, exactly — each one is
|
|
955
|
+
* signed into the URL: `content-type`, plus `x-amz-checksum-sha256` when
|
|
956
|
+
* the request declared `checksum_sha256`. */
|
|
957
|
+
upload_headers?: Record<string, string>;
|
|
883
958
|
}
|
|
884
959
|
/** `files.usage` answer. */
|
|
885
960
|
export interface FilesUsage {
|
|
@@ -893,7 +968,15 @@ export interface FilesUsage {
|
|
|
893
968
|
maxObjectBytes?: number;
|
|
894
969
|
maxTotalBytes?: number;
|
|
895
970
|
};
|
|
896
|
-
|
|
971
|
+
/** Headroom left under the quotas. Never negative: when usage sits ABOVE a
|
|
972
|
+
* lowered quota (or a plan downgrade) both read 0 and `over_quota` is true
|
|
973
|
+
* — new uploads answer 422 quota_exceeded until usage drops back under. */
|
|
974
|
+
available: {
|
|
975
|
+
object_slots: number;
|
|
976
|
+
bytes_remaining: number;
|
|
977
|
+
over_quota?: boolean;
|
|
978
|
+
[key: string]: number | boolean | null | undefined;
|
|
979
|
+
};
|
|
897
980
|
/** Your plan's storage ceiling, and whether `quotas.maxTotalBytes` is that
|
|
898
981
|
* ceiling (`plan`) or your own lower value (`config`). */
|
|
899
982
|
storage_plan?: {
|
|
@@ -1361,7 +1444,8 @@ export interface PaymentsSubscriptionStatusEventPayload {
|
|
|
1361
1444
|
current_period_end: string | null;
|
|
1362
1445
|
environment: PaymentsEventEnvironment;
|
|
1363
1446
|
/** what wrote it: the provider event type, `subscription.synced` /
|
|
1364
|
-
* `subscription.restored`, or `
|
|
1447
|
+
* `subscription.restored`, `period_end_enforced`, or `test_clock` (a
|
|
1448
|
+
* `moveSubscriptionUntil` on a non-production project ended the row) */
|
|
1365
1449
|
event_type: string;
|
|
1366
1450
|
slack_hours: number | null;
|
|
1367
1451
|
level: 'info';
|
|
@@ -1502,6 +1586,24 @@ export interface PaymentsConformanceEventPayload {
|
|
|
1502
1586
|
level: 'info' | 'error';
|
|
1503
1587
|
state: 'ok' | 'broken';
|
|
1504
1588
|
}
|
|
1589
|
+
/** `payments.test_clock.moved` — the test clock moved a manual grant's or a
|
|
1590
|
+
* purchase pass's end (`moveSubscriptionUntil`; non-production projects only).
|
|
1591
|
+
* `lapsed` is true when this move ended access (a `payments.subscription.lapsed`
|
|
1592
|
+
* with `event_type: 'test_clock'` follows); `lane` names why the project was
|
|
1593
|
+
* allowed: `mock` provider, a `sandbox` provider environment, or a `dev`
|
|
1594
|
+
* (preview / `vxil dev up`) project. */
|
|
1595
|
+
export interface PaymentsTestClockMovedEventPayload {
|
|
1596
|
+
end_user_id: string;
|
|
1597
|
+
subscription_id: string;
|
|
1598
|
+
provider_sub_id: string;
|
|
1599
|
+
tier: string | null;
|
|
1600
|
+
previous_until: string | null;
|
|
1601
|
+
until: string;
|
|
1602
|
+
lapsed: boolean;
|
|
1603
|
+
lane: 'mock' | 'sandbox' | 'dev';
|
|
1604
|
+
level: 'info';
|
|
1605
|
+
state: 'ok';
|
|
1606
|
+
}
|
|
1505
1607
|
/** Event name → typed `data`, for the events that settle work you started and
|
|
1506
1608
|
* (since 2026-10-01) every `payments.*` event. `VxilEventPayload<'job.generation.failed'>`
|
|
1507
1609
|
* names one; everything else on the catalog is `Record<string, unknown>` until
|
|
@@ -1536,6 +1638,7 @@ export interface VxilEventPayloads {
|
|
|
1536
1638
|
'payments.reconcile.discrepancy': PaymentsReconcileDiscrepancyEventPayload;
|
|
1537
1639
|
'payments.reconcile.ok': PaymentsReconcileOkEventPayload;
|
|
1538
1640
|
'payments.conformance': PaymentsConformanceEventPayload;
|
|
1641
|
+
'payments.test_clock.moved': PaymentsTestClockMovedEventPayload;
|
|
1539
1642
|
}
|
|
1540
1643
|
export type VxilEventPayload<E extends string> = E extends keyof VxilEventPayloads ? VxilEventPayloads[E] : Record<string, unknown>;
|
|
1541
1644
|
/** The `trigger` label on the envelope. The config spells two of them in
|
|
@@ -2266,6 +2369,11 @@ type DisabledFeatures<S extends VxilSchemaShape> = {
|
|
|
2266
2369
|
export type EnabledVxil<S extends VxilSchemaShape> = Omit<Vxil<S>, DisabledFeatures<S>> & {
|
|
2267
2370
|
[P in DisabledFeatures<S>]: DisabledFeature<FeatureMap[P] & string>;
|
|
2268
2371
|
};
|
|
2372
|
+
/** A cms collection's END-USER ACCESS mode (guide ch. 9): what a verified end
|
|
2373
|
+
* user (thin-client key + session) may do on it — `'readwrite'` (default),
|
|
2374
|
+
* `'read'` (writes are `403 server_only`) or `'none'` (reads and writes are
|
|
2375
|
+
* `403 server_only`). Server keys are never affected. */
|
|
2376
|
+
export type CmsEndUserAccess = 'readwrite' | 'read' | 'none';
|
|
2269
2377
|
/** One config-declared per-record ACTION (guide ch. 4): a button on a record row
|
|
2270
2378
|
* that invokes the deployed tenant function `fn` ONCE with `{ collection,
|
|
2271
2379
|
* item_id, action, actor, item }`. Exactly one human-initiated step — no
|
|
@@ -2753,6 +2861,15 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
2753
2861
|
get: (id: string, opts?: {
|
|
2754
2862
|
includeDeleted?: boolean;
|
|
2755
2863
|
}) => Promise<VxilUser>;
|
|
2864
|
+
/**
|
|
2865
|
+
* Update a user. `email` / `display_name` / `avatar_url` are replaced
|
|
2866
|
+
* (null clears them). `attributes` is DEEP-MERGED into the stored
|
|
2867
|
+
* attributes (RFC 7396 merge patch): objects merge key by key, arrays and
|
|
2868
|
+
* scalars replace, and `null` deletes a key AT ANY DEPTH — including inside
|
|
2869
|
+
* a subtree the row does not have yet, where the null is simply dropped
|
|
2870
|
+
* (`{ attributes: { prefs: { phone: null } } }` never stores a null).
|
|
2871
|
+
* Scope users:write.
|
|
2872
|
+
*/
|
|
2756
2873
|
patch: (id: string, patch: {
|
|
2757
2874
|
email?: string | null;
|
|
2758
2875
|
display_name?: string | null;
|
|
@@ -2775,7 +2892,7 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
2775
2892
|
* Merge a REGISTRY-ONLY user (a row you created with upsert that never
|
|
2776
2893
|
* signed in) INTO another user of the tenant: its attributes fill the
|
|
2777
2894
|
* survivor's gaps (the survivor wins on conflicts), the merged id is
|
|
2778
|
-
* soft-deleted, its owner-scoped cms / files / payments rows move to the
|
|
2895
|
+
* soft-deleted, its owner-scoped cms / files / payments / jobs rows move to the
|
|
2779
2896
|
* survivor, and `auth.user.merged { from, into, method: 'registry_merge' }`
|
|
2780
2897
|
* is audited. `rekeyed: false` means a re-key could not finish inside the
|
|
2781
2898
|
* request — the event is the backstop (call the feature's re-key route
|
|
@@ -2994,8 +3111,9 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
2994
3111
|
/** Reset the row and re-enqueue the original message. */
|
|
2995
3112
|
replay: (deliveryId: string) => Promise<void>;
|
|
2996
3113
|
};
|
|
2997
|
-
/** A single delivery by id (the list is `deliveries()`)
|
|
2998
|
-
|
|
3114
|
+
/** A single delivery by id (the list is `deliveries()`), with its render —
|
|
3115
|
+
* the full one for a `test_sink` row (see `DeliveryDetail`). */
|
|
3116
|
+
delivery: (deliveryId: string) => Promise<DeliveryDetail>;
|
|
2999
3117
|
/** Email broadcast campaigns (guide ch. 6, notifications): audience-ref fan-out
|
|
3000
3118
|
* with quiet-hours + frequency-cap policy. `schedule_cron` sets a recurring
|
|
3001
3119
|
* send (status `scheduled`); omit it for a `draft`. */
|
|
@@ -3048,6 +3166,9 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3048
3166
|
skipped_freq_cap: number;
|
|
3049
3167
|
skipped_no_email: number;
|
|
3050
3168
|
already_sent: number;
|
|
3169
|
+
/** recipients in `suppression.testRecipients` on this page: recorded
|
|
3170
|
+
* `test_sink`, never sent, never billed */
|
|
3171
|
+
test_sink?: number;
|
|
3051
3172
|
}>;
|
|
3052
3173
|
};
|
|
3053
3174
|
};
|
|
@@ -3220,7 +3341,16 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3220
3341
|
*
|
|
3221
3342
|
* The answer's `generation_status` is `pending` on a fresh enqueue; a replay
|
|
3222
3343
|
* of the same `idempotency_key` (`deduplicated: true`) carries the run's
|
|
3223
|
-
* status NOW (`processing`, `completed` or `failed` too).
|
|
3344
|
+
* status NOW (`processing`, `completed` or `failed` too). The key is bound
|
|
3345
|
+
* to the request BODY: re-sending it with a different body (another
|
|
3346
|
+
* payload — a rebuilt `deadline_at` included —, provider, completion,
|
|
3347
|
+
* timeout, reserve…) will answer 422 `idempotency_key_reused` naming the
|
|
3348
|
+
* original run (the ai-v1 rule). That check is being rolled out: today a
|
|
3349
|
+
* mismatch is only logged, on every project, and the send is still
|
|
3350
|
+
* deduplicated to the first run; a changelog entry will announce
|
|
3351
|
+
* enforcement — so keep the body deterministic now. A 409 `generation_in_progress`
|
|
3352
|
+
* (Retry-After 5) means the run that key names is still placing its
|
|
3353
|
+
* credit hold.
|
|
3224
3354
|
*
|
|
3225
3355
|
* `reserve_credits` takes a PROVISIONAL held credit debit at enqueue
|
|
3226
3356
|
* (linked to the run), committed on `completed` and reversed on
|
|
@@ -3228,7 +3358,8 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3228
3358
|
* `config.generation.maxReserveCredits` cap; in end-user mode `user_id` is
|
|
3229
3359
|
* FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
|
|
3230
3360
|
* the enqueue (insufficient balance; the run ends with job.generation.failed
|
|
3231
|
-
* `ReserveInsufficient` and the 402 names its `run_id
|
|
3361
|
+
* `ReserveInsufficient` and the 402 names its `run_id`; the key is released,
|
|
3362
|
+
* so the same request after a top-up starts a new run); a runaway over the
|
|
3232
3363
|
* per-tenant outstanding-holds ceiling → 429 (Retry-After 5). Payments
|
|
3233
3364
|
* unreachable at the hold → 503 `payments_unavailable` (Retry-After 15;
|
|
3234
3365
|
* nothing started, nothing held — re-send the same request), unless
|
|
@@ -3249,8 +3380,9 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3249
3380
|
*
|
|
3250
3381
|
* `opts.retryOnCapacity: { maxWaitMs }` retries a CAPACITY 429
|
|
3251
3382
|
* (`generation_concurrency_exceeded`, `reserve_holds_exceeded`,
|
|
3252
|
-
* `queue_full`)
|
|
3253
|
-
*
|
|
3383
|
+
* `queue_full`), a 503 `payments_unavailable` / `enqueue_interrupted` and a
|
|
3384
|
+
* 409 `generation_in_progress` (the key's run is still placing its credit
|
|
3385
|
+
* hold), honouring Retry-After with jitter, with the SAME
|
|
3254
3386
|
* idempotency_key (one is generated when the input has none), until
|
|
3255
3387
|
* maxWaitMs has passed — then the last 429 is thrown. */
|
|
3256
3388
|
generation: (input: {
|
|
@@ -3281,13 +3413,26 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3281
3413
|
};
|
|
3282
3414
|
/** `progress_fields`: the progress keys (`progress` / `stage` /
|
|
3283
3415
|
* `message`) a webhook-mode `processing` callback also writes onto the
|
|
3284
|
-
* mirrored record — realtime clients of that record see them live
|
|
3416
|
+
* mirrored record — realtime clients of that record see them live.
|
|
3417
|
+
* In END-USER mode (`asEndUser`, or a function invoked for a signed-in
|
|
3418
|
+
* user) the target must be a cms record that user can read: another
|
|
3419
|
+
* user's row, a server-only collection or a non-cms feature answers
|
|
3420
|
+
* 403 `mirror_target_forbidden` and nothing starts (503
|
|
3421
|
+
* `mirror_check_unavailable` if the check could not run — retry).
|
|
3422
|
+
* Mirror onto a server-only row from server-mode code instead. */
|
|
3285
3423
|
status_mirror?: {
|
|
3286
3424
|
feature: string;
|
|
3287
3425
|
collection: string;
|
|
3288
3426
|
record_id: string;
|
|
3289
3427
|
column?: string;
|
|
3290
3428
|
progress_fields?: Array<"progress" | "stage" | "message">;
|
|
3429
|
+
/** cms only: a declared string field the mirror stamps with this
|
|
3430
|
+
* run's `run_id` on its first write; every later write of the run
|
|
3431
|
+
* requires it (a cms `if` precondition), so an older run's late
|
|
3432
|
+
* writes never land on a row a newer run has claimed. A write the
|
|
3433
|
+
* record refuses that way — or because it already holds
|
|
3434
|
+
* `completed` / `failed` — is skipped, not a `mirror_error`. */
|
|
3435
|
+
fence_field?: string;
|
|
3291
3436
|
};
|
|
3292
3437
|
timeout?: {
|
|
3293
3438
|
after_ms: number;
|
|
@@ -3365,9 +3510,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3365
3510
|
/** Cancel a run that has not started its current attempt (`queued`,
|
|
3366
3511
|
* `delayed`, `retrying`) or a plain run handed off to its signed callback
|
|
3367
3512
|
* (`waiting` after its handler answered 202 — the callback URL is
|
|
3368
|
-
* consumed). A
|
|
3369
|
-
*
|
|
3370
|
-
*
|
|
3513
|
+
* consumed). A generation's credit hold is released — a STARTED
|
|
3514
|
+
* generation's too (its provider accepted it): it ends `failed`
|
|
3515
|
+
* (`Cancelled`) and a late provider callback is a no-op, but the provider
|
|
3516
|
+
* is NOT told, so its work and its charge may still complete. A plain
|
|
3517
|
+
* `running` run (a delivery in flight), a run waiting on an event, or a
|
|
3518
|
+
* terminal run answers `409 not_cancellable`. */
|
|
3371
3519
|
cancel: (runId: string) => Promise<{
|
|
3372
3520
|
run_id: string;
|
|
3373
3521
|
state: string;
|
|
@@ -3378,6 +3526,24 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3378
3526
|
run_id: string;
|
|
3379
3527
|
replayed_from: string;
|
|
3380
3528
|
}>;
|
|
3529
|
+
/** SERVER-ONLY (403 server_only in end-user mode). Account merge: move the
|
|
3530
|
+
* credit-hold owner (`reserve_credits.user_id`) of every run of
|
|
3531
|
+
* `from_user_id`, in any state, onto `into_user_id`, so the surviving
|
|
3532
|
+
* account reads, cancels and replays the generation runs it started as a
|
|
3533
|
+
* guest. ONE bounded page (≤500 runs) per call — call again until `done`
|
|
3534
|
+
* (idempotent: a second call moves 0). The jobs consumer of
|
|
3535
|
+
* `auth.user.merged { from, into }`; auth and `users.merge` call it after
|
|
3536
|
+
* a merge, the event is the backstop. Emits one `jobs.runs.rekeyed` when
|
|
3537
|
+
* a call moved at least one run (none on a zero-move call). Unlike the
|
|
3538
|
+
* files re-key it does not check that `into_user_id` exists.
|
|
3539
|
+
* Scope jobs:write. */
|
|
3540
|
+
reKey: (input: {
|
|
3541
|
+
from_user_id: string;
|
|
3542
|
+
into_user_id: string;
|
|
3543
|
+
}) => Promise<{
|
|
3544
|
+
moved: number;
|
|
3545
|
+
done: boolean;
|
|
3546
|
+
}>;
|
|
3381
3547
|
/**
|
|
3382
3548
|
* Suspend the RUNNING run until an event (call from the executing
|
|
3383
3549
|
* handler, then return 200 — the suspension wins). `state: 'resumed'` =
|
|
@@ -3502,9 +3668,10 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3502
3668
|
sent: true;
|
|
3503
3669
|
test_link?: string;
|
|
3504
3670
|
}>;
|
|
3505
|
-
/**
|
|
3506
|
-
*
|
|
3507
|
-
*
|
|
3671
|
+
/** An unknown, expired or already-used link answers `401 invalid_token`
|
|
3672
|
+
* (request a new one with `request`). `opts.anonymous_token` is REQUIRED
|
|
3673
|
+
* for a link that was requested with one (the guest claim above): the
|
|
3674
|
+
* same guest session must present it, else `401 invalid_session`. `merged` is true only when the guest was
|
|
3508
3675
|
* folded into an existing account (then `user_id` is that account), and
|
|
3509
3676
|
* `rekeyed` rides beside it: true ⇒ the guest's payments / cms / files
|
|
3510
3677
|
* rows were already moved onto `user_id` when this returned; false ⇒
|
|
@@ -3689,7 +3856,7 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3689
3856
|
}) => Promise<AuthImportResult>;
|
|
3690
3857
|
/** END-USER MODE ONLY (`endUserToken` / `asEndUser`): the signed-in user
|
|
3691
3858
|
* deep-merges a bounded `attributes` object into its OWN shared identity
|
|
3692
|
-
* row (null deletes a key; ≤8 KB, ≤64 top-level keys, depth ≤8). Profile
|
|
3859
|
+
* row (null deletes a key at any depth — RFC 7396; ≤8 KB, ≤64 top-level keys, depth ≤8). Profile
|
|
3693
3860
|
* fields are not patchable here. In server mode the call throws 422
|
|
3694
3861
|
* `end_user_mode_required` — use `users.patch(id, …)` instead. */
|
|
3695
3862
|
patchMe: (attributes: Record<string, unknown>) => Promise<{
|
|
@@ -3933,7 +4100,11 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3933
4100
|
};
|
|
3934
4101
|
readonly cms: {
|
|
3935
4102
|
collections: {
|
|
3936
|
-
/** Define a content type. The model is data — not config.
|
|
4103
|
+
/** Define a content type. The model is data — not config.
|
|
4104
|
+
* Model management (`create`, `addField` and every fields-route meta-op
|
|
4105
|
+
* in this namespace) is SERVER-ONLY: a client acting as a signed-in end
|
|
4106
|
+
* user (`asEndUser`, a thin-client key) gets `403 server_only`. Call
|
|
4107
|
+
* these with a server key — your backend, `vxil push`, or the dashboard. */
|
|
3937
4108
|
create: (input: {
|
|
3938
4109
|
collection: string;
|
|
3939
4110
|
singular?: string;
|
|
@@ -3949,6 +4120,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3949
4120
|
* and the owner_field are never exposed. Optional; defaults false. Use the
|
|
3950
4121
|
* top-level `cmsPublicUrl` / `listCmsPublic` helpers for the reader side. */
|
|
3951
4122
|
public?: boolean;
|
|
4123
|
+
/** What a VERIFIED end user may do here (guide ch. 9, "Read-only and
|
|
4124
|
+
* server-only collections"): `'readwrite'` (default), `'read'` (every
|
|
4125
|
+
* end-user write is `403 server_only`; reads unchanged) or `'none'`
|
|
4126
|
+
* (end-user reads and writes are `403 server_only`, even with an
|
|
4127
|
+
* owner_field). Server keys are never affected. */
|
|
4128
|
+
end_user_access?: CmsEndUserAccess;
|
|
3952
4129
|
fields?: Array<{
|
|
3953
4130
|
field: string;
|
|
3954
4131
|
type: "string" | "text" | "int" | "float" | "bool" | "datetime" | "json" | "relation" | "file";
|
|
@@ -3996,12 +4173,16 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
3996
4173
|
owner_field?: string;
|
|
3997
4174
|
public?: boolean;
|
|
3998
4175
|
actions?: CmsActionDef[];
|
|
4176
|
+
/** present when not the default 'readwrite' */
|
|
4177
|
+
end_user_access?: CmsEndUserAccess;
|
|
3999
4178
|
}>;
|
|
4000
4179
|
list: () => Promise<Array<{
|
|
4001
4180
|
collection: string;
|
|
4002
4181
|
singular: string;
|
|
4003
4182
|
fields: unknown[];
|
|
4004
4183
|
owner_field?: string | null;
|
|
4184
|
+
/** the collection's end-user access mode (absent from an older server = 'readwrite') */
|
|
4185
|
+
end_user_access?: CmsEndUserAccess;
|
|
4005
4186
|
}>>;
|
|
4006
4187
|
addField: (collection: string, field: Record<string, unknown>) => Promise<void>;
|
|
4007
4188
|
/** Set (or clear, with `null`/`[]`) a field's READ-ROLE gate (guide ch. 4) —
|
|
@@ -4033,6 +4214,17 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
4033
4214
|
* the owner_field are never exposed. `false` closes the lane (and purges the
|
|
4034
4215
|
* edge cache). Read the public side with `cmsPublicUrl` / `listCmsPublic`. */
|
|
4035
4216
|
setPublic: (collection: string, isPublic: boolean) => Promise<void>;
|
|
4217
|
+
/** Set the collection's END-USER ACCESS mode (guide ch. 9, "Read-only and
|
|
4218
|
+
* server-only collections"). `'read'`: a verified end user (thin-client
|
|
4219
|
+
* key + session) may still read — owner-scoped when the collection has an
|
|
4220
|
+
* owner_field — but every end-user write answers `403 server_only`;
|
|
4221
|
+
* `'none'`: end-user reads and writes are both refused; `'readwrite'`
|
|
4222
|
+
* (default) restores today's behaviour. Server keys are never affected.
|
|
4223
|
+
* Audited; takes effect on the next request. */
|
|
4224
|
+
setEndUserAccess: (collection: string, access: CmsEndUserAccess) => Promise<{
|
|
4225
|
+
collection: string;
|
|
4226
|
+
end_user_access: CmsEndUserAccess;
|
|
4227
|
+
}>;
|
|
4036
4228
|
/** Re-project the collection's index slots after an `index_slot` move
|
|
4037
4229
|
* (guide ch. 4). Slots are projected on WRITE only, so until this runs,
|
|
4038
4230
|
* stored rows keep their OLD projection: the new slot is NULL and the
|
|
@@ -5071,16 +5263,27 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
5071
5263
|
* long after the mint, whether or not `files.ttl` is enabled; `null` =
|
|
5072
5264
|
* never. The answer's `expires_in` is the UPLOAD URL's life in seconds;
|
|
5073
5265
|
* `expires_at` is when the object expires (null = never). `user_id` is
|
|
5074
|
-
* required with a server key and omitted in end-user mode.
|
|
5266
|
+
* required with a server key and omitted in end-user mode.
|
|
5267
|
+
* `checksum_sha256` (the SHA-256 of the bytes, 64 hex or 44 base64
|
|
5268
|
+
* characters) is signed into the URL (send the answer's `upload_headers`
|
|
5269
|
+
* on the PUT) and kept with the upload: `complete` compares it with the
|
|
5270
|
+
* SHA-256 the store reports for the bytes — 409 `checksum_mismatch` when
|
|
5271
|
+
* they differ, 409 `checksum_unverified` when the store reports none —
|
|
5272
|
+
* and on success answers the verified `checksum_sha256`. */
|
|
5075
5273
|
createUploadUrl: (input: {
|
|
5076
5274
|
user_id?: string;
|
|
5077
5275
|
filename: string;
|
|
5078
5276
|
content_type: string;
|
|
5079
5277
|
size_bytes: number;
|
|
5080
5278
|
expiresInSeconds?: number | null;
|
|
5279
|
+
checksum_sha256?: string;
|
|
5081
5280
|
}) => Promise<FileUploadUrl>;
|
|
5082
5281
|
/** Confirm the PUT (bytes verified, object → available). Answers the
|
|
5083
|
-
* object's `expires_at` (null = never)
|
|
5282
|
+
* object's `expires_at` (null = never) and `checksum_sha256` (hex; null
|
|
5283
|
+
* when the store reported none) — a repeated complete answers the same.
|
|
5284
|
+
* When the upload declared `checksum_sha256`: 409 `checksum_mismatch` if
|
|
5285
|
+
* the bytes hash differently, 409 `checksum_unverified` if the store
|
|
5286
|
+
* reports no checksum (the object stays pending either way). */
|
|
5084
5287
|
complete: (objectId: string) => Promise<{
|
|
5085
5288
|
object_id: string;
|
|
5086
5289
|
status: string;
|
|
@@ -6047,6 +6250,30 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
|
|
|
6047
6250
|
user_id: string;
|
|
6048
6251
|
tier?: string;
|
|
6049
6252
|
}) => Promise<PaymentsSimulationResult>;
|
|
6253
|
+
/** SERVER-ONLY TEST CLOCK — move the end (`until`, past or future, within
|
|
6254
|
+
* 5 years of now) of a MANUAL grant or a purchase pass, to test expired /
|
|
6255
|
+
* export-window / renew-soon states on demand. Allowed only where no live
|
|
6256
|
+
* money can be involved: a `mock`-provider project, a project whose
|
|
6257
|
+
* provider runs in its sandbox (`paddle.sandbox`, `paypal.sandbox`, a
|
|
6258
|
+
* Stripe `sk_test_` key), or a dev/preview project — elsewhere 403
|
|
6259
|
+
* `test_clock_forbidden` (also for a pass linked to a non-sandbox charge).
|
|
6260
|
+
* 409 `provider_managed` for a provider subscription (use the provider's
|
|
6261
|
+
* own test clock); 409 `subscription_ended` for a revoked/refunded row.
|
|
6262
|
+
* The effect is a natural lapse's: the entitlements refold
|
|
6263
|
+
* (`payments.entitlement.changed`), a move into the past turns an active
|
|
6264
|
+
* row `lapsed` and emits `payments.subscription.lapsed` (`event_type:
|
|
6265
|
+
* 'test_clock'`, `environment: 'sandbox'`) once; a move back into the
|
|
6266
|
+
* future revives it (as `active` — a `trialing` row does not return to
|
|
6267
|
+
* `trialing`; a `past_due` row's status is left as it is). Only the end
|
|
6268
|
+
* moves: `since` (the period start) is never rewritten, so after a move
|
|
6269
|
+
* before it `since` > `until`. Every move emits
|
|
6270
|
+
* `payments.test_clock.moved`. Both events are emitted after the move
|
|
6271
|
+
* commits, best-effort (as the period-end sweep's are): if one fails to
|
|
6272
|
+
* record, the move still stands and is not re-announced on a retry, so
|
|
6273
|
+
* assert on the returned `status` / `lapsed` rather than only on the
|
|
6274
|
+
* event. Same-tier passes stacked behind the moved one are not
|
|
6275
|
+
* re-chained. No credits are granted or reversed. */
|
|
6276
|
+
moveSubscriptionUntil: (subscriptionId: string, until: string | Date) => Promise<PaymentsTestClockResult>;
|
|
6050
6277
|
/** The last report-only reconciliation sweep result for this project
|
|
6051
6278
|
* (`run: null` before the first daily tick). Finding kinds:
|
|
6052
6279
|
* `balance_drift`, `entitlement_lag`, `null_tier`, `refund_pending`,
|
package/dist/index.js
CHANGED
|
@@ -432,6 +432,15 @@ export class Vxil {
|
|
|
432
432
|
upsert: async (user) => (await this.call('POST', '/v1/users', user)).data,
|
|
433
433
|
bulk: async (users) => (await this.call('POST', '/v1/users/bulk', { users })).data.upserted,
|
|
434
434
|
get: async (id, opts) => (await this.call('GET', `/v1/users/${encodeURIComponent(id)}${opts?.includeDeleted ? '?include_deleted=true' : ''}`)).data,
|
|
435
|
+
/**
|
|
436
|
+
* Update a user. `email` / `display_name` / `avatar_url` are replaced
|
|
437
|
+
* (null clears them). `attributes` is DEEP-MERGED into the stored
|
|
438
|
+
* attributes (RFC 7396 merge patch): objects merge key by key, arrays and
|
|
439
|
+
* scalars replace, and `null` deletes a key AT ANY DEPTH — including inside
|
|
440
|
+
* a subtree the row does not have yet, where the null is simply dropped
|
|
441
|
+
* (`{ attributes: { prefs: { phone: null } } }` never stores a null).
|
|
442
|
+
* Scope users:write.
|
|
443
|
+
*/
|
|
435
444
|
patch: async (id, patch) => (await this.call('PATCH', `/v1/users/${encodeURIComponent(id)}`, patch)).data,
|
|
436
445
|
/**
|
|
437
446
|
* Delete a user. By default this is a SOFT delete (sets deleted_at; the PII
|
|
@@ -449,7 +458,7 @@ export class Vxil {
|
|
|
449
458
|
* Merge a REGISTRY-ONLY user (a row you created with upsert that never
|
|
450
459
|
* signed in) INTO another user of the tenant: its attributes fill the
|
|
451
460
|
* 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
|
|
461
|
+
* soft-deleted, its owner-scoped cms / files / payments / jobs rows move to the
|
|
453
462
|
* survivor, and `auth.user.merged { from, into, method: 'registry_merge' }`
|
|
454
463
|
* is audited. `rekeyed: false` means a re-key could not finish inside the
|
|
455
464
|
* request — the event is the backstop (call the feature's re-key route
|
|
@@ -567,7 +576,8 @@ export class Vxil {
|
|
|
567
576
|
await this.call('POST', `/v1/notifications/dead-letters/${encodeURIComponent(deliveryId)}/replay`);
|
|
568
577
|
},
|
|
569
578
|
},
|
|
570
|
-
/** A single delivery by id (the list is `deliveries()`)
|
|
579
|
+
/** A single delivery by id (the list is `deliveries()`), with its render —
|
|
580
|
+
* the full one for a `test_sink` row (see `DeliveryDetail`). */
|
|
571
581
|
delivery: async (deliveryId) => (await this.call('GET', `/v1/notifications/deliveries/${encodeURIComponent(deliveryId)}`)).data,
|
|
572
582
|
/** Email broadcast campaigns (guide ch. 6, notifications): audience-ref fan-out
|
|
573
583
|
* with quiet-hours + frequency-cap policy. `schedule_cron` sets a recurring
|
|
@@ -756,7 +766,16 @@ export class Vxil {
|
|
|
756
766
|
*
|
|
757
767
|
* The answer's `generation_status` is `pending` on a fresh enqueue; a replay
|
|
758
768
|
* of the same `idempotency_key` (`deduplicated: true`) carries the run's
|
|
759
|
-
* status NOW (`processing`, `completed` or `failed` too).
|
|
769
|
+
* status NOW (`processing`, `completed` or `failed` too). The key is bound
|
|
770
|
+
* to the request BODY: re-sending it with a different body (another
|
|
771
|
+
* payload — a rebuilt `deadline_at` included —, provider, completion,
|
|
772
|
+
* timeout, reserve…) will answer 422 `idempotency_key_reused` naming the
|
|
773
|
+
* original run (the ai-v1 rule). That check is being rolled out: today a
|
|
774
|
+
* mismatch is only logged, on every project, and the send is still
|
|
775
|
+
* deduplicated to the first run; a changelog entry will announce
|
|
776
|
+
* enforcement — so keep the body deterministic now. A 409 `generation_in_progress`
|
|
777
|
+
* (Retry-After 5) means the run that key names is still placing its
|
|
778
|
+
* credit hold.
|
|
760
779
|
*
|
|
761
780
|
* `reserve_credits` takes a PROVISIONAL held credit debit at enqueue
|
|
762
781
|
* (linked to the run), committed on `completed` and reversed on
|
|
@@ -764,7 +783,8 @@ export class Vxil {
|
|
|
764
783
|
* `config.generation.maxReserveCredits` cap; in end-user mode `user_id` is
|
|
765
784
|
* FORCED to the verified end-user (a mismatched user_id → 400). A 402 aborts
|
|
766
785
|
* the enqueue (insufficient balance; the run ends with job.generation.failed
|
|
767
|
-
* `ReserveInsufficient` and the 402 names its `run_id
|
|
786
|
+
* `ReserveInsufficient` and the 402 names its `run_id`; the key is released,
|
|
787
|
+
* so the same request after a top-up starts a new run); a runaway over the
|
|
768
788
|
* per-tenant outstanding-holds ceiling → 429 (Retry-After 5). Payments
|
|
769
789
|
* unreachable at the hold → 503 `payments_unavailable` (Retry-After 15;
|
|
770
790
|
* nothing started, nothing held — re-send the same request), unless
|
|
@@ -785,8 +805,9 @@ export class Vxil {
|
|
|
785
805
|
*
|
|
786
806
|
* `opts.retryOnCapacity: { maxWaitMs }` retries a CAPACITY 429
|
|
787
807
|
* (`generation_concurrency_exceeded`, `reserve_holds_exceeded`,
|
|
788
|
-
* `queue_full`)
|
|
789
|
-
*
|
|
808
|
+
* `queue_full`), a 503 `payments_unavailable` / `enqueue_interrupted` and a
|
|
809
|
+
* 409 `generation_in_progress` (the key's run is still placing its credit
|
|
810
|
+
* hold), honouring Retry-After with jitter, with the SAME
|
|
790
811
|
* idempotency_key (one is generated when the input has none), until
|
|
791
812
|
* maxWaitMs has passed — then the last 429 is thrown. */
|
|
792
813
|
generation: async (input, opts) => {
|
|
@@ -835,13 +856,28 @@ export class Vxil {
|
|
|
835
856
|
/** Cancel a run that has not started its current attempt (`queued`,
|
|
836
857
|
* `delayed`, `retrying`) or a plain run handed off to its signed callback
|
|
837
858
|
* (`waiting` after its handler answered 202 — the callback URL is
|
|
838
|
-
* consumed). A
|
|
839
|
-
*
|
|
840
|
-
*
|
|
859
|
+
* consumed). A generation's credit hold is released — a STARTED
|
|
860
|
+
* generation's too (its provider accepted it): it ends `failed`
|
|
861
|
+
* (`Cancelled`) and a late provider callback is a no-op, but the provider
|
|
862
|
+
* is NOT told, so its work and its charge may still complete. A plain
|
|
863
|
+
* `running` run (a delivery in flight), a run waiting on an event, or a
|
|
864
|
+
* terminal run answers `409 not_cancellable`. */
|
|
841
865
|
cancel: async (runId) => (await this.call('POST', `/v1/jobs/runs/${encodeURIComponent(runId)}/cancel`)).data,
|
|
842
866
|
/** Clone a terminal run into a fresh queued run. A generation run answers
|
|
843
867
|
* `409 not_replayable` — submit the generation again instead. */
|
|
844
868
|
replay: async (runId) => (await this.call('POST', `/v1/jobs/runs/${encodeURIComponent(runId)}/replay`)).data,
|
|
869
|
+
/** SERVER-ONLY (403 server_only in end-user mode). Account merge: move the
|
|
870
|
+
* credit-hold owner (`reserve_credits.user_id`) of every run of
|
|
871
|
+
* `from_user_id`, in any state, onto `into_user_id`, so the surviving
|
|
872
|
+
* account reads, cancels and replays the generation runs it started as a
|
|
873
|
+
* guest. ONE bounded page (≤500 runs) per call — call again until `done`
|
|
874
|
+
* (idempotent: a second call moves 0). The jobs consumer of
|
|
875
|
+
* `auth.user.merged { from, into }`; auth and `users.merge` call it after
|
|
876
|
+
* a merge, the event is the backstop. Emits one `jobs.runs.rekeyed` when
|
|
877
|
+
* a call moved at least one run (none on a zero-move call). Unlike the
|
|
878
|
+
* files re-key it does not check that `into_user_id` exists.
|
|
879
|
+
* Scope jobs:write. */
|
|
880
|
+
reKey: async (input) => (await this.call('POST', '/v1/jobs/runs/re-key', input)).data,
|
|
845
881
|
/**
|
|
846
882
|
* Suspend the RUNNING run until an event (call from the executing
|
|
847
883
|
* handler, then return 200 — the suspension wins). `state: 'resumed'` =
|
|
@@ -926,9 +962,10 @@ export class Vxil {
|
|
|
926
962
|
* existing account; swap tokens). Needs `anonymous.enabled`; a non-guest
|
|
927
963
|
* bearer is `409 not_anonymous`. */
|
|
928
964
|
request: async (input) => (await this.call('POST', '/v1/auth/magic-link/request', input)).data,
|
|
929
|
-
/**
|
|
930
|
-
*
|
|
931
|
-
*
|
|
965
|
+
/** An unknown, expired or already-used link answers `401 invalid_token`
|
|
966
|
+
* (request a new one with `request`). `opts.anonymous_token` is REQUIRED
|
|
967
|
+
* for a link that was requested with one (the guest claim above): the
|
|
968
|
+
* same guest session must present it, else `401 invalid_session`. `merged` is true only when the guest was
|
|
932
969
|
* folded into an existing account (then `user_id` is that account), and
|
|
933
970
|
* `rekeyed` rides beside it: true ⇒ the guest's payments / cms / files
|
|
934
971
|
* rows were already moved onto `user_id` when this returned; false ⇒
|
|
@@ -1026,7 +1063,7 @@ export class Vxil {
|
|
|
1026
1063
|
import: async (input) => (await this.call('POST', '/v1/auth/users/import', input)).data,
|
|
1027
1064
|
/** END-USER MODE ONLY (`endUserToken` / `asEndUser`): the signed-in user
|
|
1028
1065
|
* 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
|
|
1066
|
+
* row (null deletes a key at any depth — RFC 7396; ≤8 KB, ≤64 top-level keys, depth ≤8). Profile
|
|
1030
1067
|
* fields are not patchable here. In server mode the call throws 422
|
|
1031
1068
|
* `end_user_mode_required` — use `users.patch(id, …)` instead. */
|
|
1032
1069
|
patchMe: async (attributes) => (await this.call('PATCH', '/v1/auth/users/me', { attributes })).data,
|
|
@@ -1210,7 +1247,11 @@ export class Vxil {
|
|
|
1210
1247
|
};
|
|
1211
1248
|
cms = {
|
|
1212
1249
|
collections: {
|
|
1213
|
-
/** Define a content type. The model is data — not config.
|
|
1250
|
+
/** Define a content type. The model is data — not config.
|
|
1251
|
+
* Model management (`create`, `addField` and every fields-route meta-op
|
|
1252
|
+
* in this namespace) is SERVER-ONLY: a client acting as a signed-in end
|
|
1253
|
+
* user (`asEndUser`, a thin-client key) gets `403 server_only`. Call
|
|
1254
|
+
* these with a server key — your backend, `vxil push`, or the dashboard. */
|
|
1214
1255
|
create: async (input) => (await this.call('POST', '/v1/cms/collections', input)).data,
|
|
1215
1256
|
list: async () => (await this.call('GET', '/v1/cms/collections')).data.collections,
|
|
1216
1257
|
addField: async (collection, field) => {
|
|
@@ -1262,6 +1303,14 @@ export class Vxil {
|
|
|
1262
1303
|
setPublic: async (collection, isPublic) => {
|
|
1263
1304
|
await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { public: isPublic });
|
|
1264
1305
|
},
|
|
1306
|
+
/** Set the collection's END-USER ACCESS mode (guide ch. 9, "Read-only and
|
|
1307
|
+
* server-only collections"). `'read'`: a verified end user (thin-client
|
|
1308
|
+
* key + session) may still read — owner-scoped when the collection has an
|
|
1309
|
+
* owner_field — but every end-user write answers `403 server_only`;
|
|
1310
|
+
* `'none'`: end-user reads and writes are both refused; `'readwrite'`
|
|
1311
|
+
* (default) restores today's behaviour. Server keys are never affected.
|
|
1312
|
+
* Audited; takes effect on the next request. */
|
|
1313
|
+
setEndUserAccess: async (collection, access) => (await this.call('POST', `/v1/cms/collections/${encodeURIComponent(collection)}/fields`, { end_user_access: access })).data,
|
|
1265
1314
|
/** Re-project the collection's index slots after an `index_slot` move
|
|
1266
1315
|
* (guide ch. 4). Slots are projected on WRITE only, so until this runs,
|
|
1267
1316
|
* stored rows keep their OLD projection: the new slot is NULL and the
|
|
@@ -1852,10 +1901,20 @@ export class Vxil {
|
|
|
1852
1901
|
* long after the mint, whether or not `files.ttl` is enabled; `null` =
|
|
1853
1902
|
* never. The answer's `expires_in` is the UPLOAD URL's life in seconds;
|
|
1854
1903
|
* `expires_at` is when the object expires (null = never). `user_id` is
|
|
1855
|
-
* required with a server key and omitted in end-user mode.
|
|
1904
|
+
* required with a server key and omitted in end-user mode.
|
|
1905
|
+
* `checksum_sha256` (the SHA-256 of the bytes, 64 hex or 44 base64
|
|
1906
|
+
* characters) is signed into the URL (send the answer's `upload_headers`
|
|
1907
|
+
* on the PUT) and kept with the upload: `complete` compares it with the
|
|
1908
|
+
* SHA-256 the store reports for the bytes — 409 `checksum_mismatch` when
|
|
1909
|
+
* they differ, 409 `checksum_unverified` when the store reports none —
|
|
1910
|
+
* and on success answers the verified `checksum_sha256`. */
|
|
1856
1911
|
createUploadUrl: async (input) => (await this.call('POST', '/v1/files/upload-url', input)).data,
|
|
1857
1912
|
/** Confirm the PUT (bytes verified, object → available). Answers the
|
|
1858
|
-
* object's `expires_at` (null = never)
|
|
1913
|
+
* object's `expires_at` (null = never) and `checksum_sha256` (hex; null
|
|
1914
|
+
* when the store reported none) — a repeated complete answers the same.
|
|
1915
|
+
* When the upload declared `checksum_sha256`: 409 `checksum_mismatch` if
|
|
1916
|
+
* the bytes hash differently, 409 `checksum_unverified` if the store
|
|
1917
|
+
* reports no checksum (the object stays pending either way). */
|
|
1859
1918
|
complete: async (objectId) => (await this.call('POST', `/v1/files/${encodeURIComponent(objectId)}/complete`)).data,
|
|
1860
1919
|
downloadUrl: async (objectId) => (await this.call('GET', `/v1/files/${encodeURIComponent(objectId)}/download-url`)).data.download_url,
|
|
1861
1920
|
/** Mint up to 100 presigned GETs in ONE call (`GET /v1/files/download-urls`)
|
|
@@ -2371,6 +2430,30 @@ export class Vxil {
|
|
|
2371
2430
|
* past-due-grace, cross-platform-unlock, transfer) through the mock
|
|
2372
2431
|
* webhook path and judge it with the conformance oracle. */
|
|
2373
2432
|
simulate: async (input) => (await this.call('POST', '/v1/payments/simulate', input)).data,
|
|
2433
|
+
/** SERVER-ONLY TEST CLOCK — move the end (`until`, past or future, within
|
|
2434
|
+
* 5 years of now) of a MANUAL grant or a purchase pass, to test expired /
|
|
2435
|
+
* export-window / renew-soon states on demand. Allowed only where no live
|
|
2436
|
+
* money can be involved: a `mock`-provider project, a project whose
|
|
2437
|
+
* provider runs in its sandbox (`paddle.sandbox`, `paypal.sandbox`, a
|
|
2438
|
+
* Stripe `sk_test_` key), or a dev/preview project — elsewhere 403
|
|
2439
|
+
* `test_clock_forbidden` (also for a pass linked to a non-sandbox charge).
|
|
2440
|
+
* 409 `provider_managed` for a provider subscription (use the provider's
|
|
2441
|
+
* own test clock); 409 `subscription_ended` for a revoked/refunded row.
|
|
2442
|
+
* The effect is a natural lapse's: the entitlements refold
|
|
2443
|
+
* (`payments.entitlement.changed`), a move into the past turns an active
|
|
2444
|
+
* row `lapsed` and emits `payments.subscription.lapsed` (`event_type:
|
|
2445
|
+
* 'test_clock'`, `environment: 'sandbox'`) once; a move back into the
|
|
2446
|
+
* future revives it (as `active` — a `trialing` row does not return to
|
|
2447
|
+
* `trialing`; a `past_due` row's status is left as it is). Only the end
|
|
2448
|
+
* moves: `since` (the period start) is never rewritten, so after a move
|
|
2449
|
+
* before it `since` > `until`. Every move emits
|
|
2450
|
+
* `payments.test_clock.moved`. Both events are emitted after the move
|
|
2451
|
+
* commits, best-effort (as the period-end sweep's are): if one fails to
|
|
2452
|
+
* record, the move still stands and is not re-announced on a retry, so
|
|
2453
|
+
* assert on the returned `status` / `lapsed` rather than only on the
|
|
2454
|
+
* event. Same-tier passes stacked behind the moved one are not
|
|
2455
|
+
* re-chained. No credits are granted or reversed. */
|
|
2456
|
+
moveSubscriptionUntil: async (subscriptionId, until) => (await this.call('POST', `/v1/payments/subscriptions/${encodeURIComponent(subscriptionId)}/until`, { until: typeof until === 'string' ? until : until.toISOString() })).data,
|
|
2374
2457
|
/** The last report-only reconciliation sweep result for this project
|
|
2375
2458
|
* (`run: null` before the first daily tick). Finding kinds:
|
|
2376
2459
|
* `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