@vxil/sdk 0.5.2 → 0.5.3

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 CHANGED
@@ -169,6 +169,17 @@ export interface Delivery {
169
169
  delivered_at?: string | null;
170
170
  opened_at?: string | null;
171
171
  clicked_at?: string | null;
172
+ /** The instant the first delivery attempt may fire (a `send_at` /
173
+ * `delay_seconds` schedule, a campaign quiet-hours deferral, a dead-letter
174
+ * replay); null = at `queued_at`. A `queued` row with no attempt 30 min past
175
+ * this is checked against its jobs run by a platform sweep and becomes
176
+ * `failed` / `last_error_code: 'never_attempted'` only when that run is gone
177
+ * or terminal (a run still waiting — e.g. behind the jobs concurrency cap —
178
+ * moves this instant forward instead) — send it again with the same
179
+ * Idempotency-Key. (The key itself is never on the row on the wire; a
180
+ * replay is the `x-vxil-idempotent-replay: true` header plus the identical
181
+ * `delivery_id` on the 202.) */
182
+ deliver_after?: string | null;
172
183
  }
173
184
  /** An ADDITIVE, non-fatal note on a 202 send — today only `mock_provider`
174
185
  * (the send is recorded but no email leaves vxil). */
@@ -713,6 +724,62 @@ export interface AiJobHandle {
713
724
  status: string;
714
725
  resume_path: string;
715
726
  }
727
+ /** The `data` of `job.generation.queued` (jobs.md §11): the run was accepted. */
728
+ export interface JobGenerationQueuedEventPayload {
729
+ run_id: string;
730
+ job_name: string;
731
+ /** the `202` handle's id from `POST /v1/ai/generate` (a tenant-authored
732
+ * `POST /v1/jobs/generation` run echoes its own `payload.generation_id`) */
733
+ generation_id: string | null;
734
+ /** your own `correlation_id` from that request (≤128 chars), or null */
735
+ correlation_id: string | null;
736
+ /** the completion mode the run was enqueued with (the ai lane enqueues `poll`) */
737
+ mode: 'poll' | 'webhook';
738
+ /** when the run is failed as expired if the provider never settles it */
739
+ expires_at: string;
740
+ }
741
+ /** The `data` of `job.generation.completed` / `job.generation.failed`. */
742
+ export interface JobGenerationSettledEventPayload {
743
+ run_id: string;
744
+ generation_id: string | null;
745
+ correlation_id: string | null;
746
+ /** `completed` on the healthy terminal, `failed` on the broken one */
747
+ status: 'completed' | 'failed';
748
+ /** the run's SETTLED error class (`provider_error`, `PollExhausted`, …);
749
+ * always null on `completed` */
750
+ error_class: string | null;
751
+ /** the provider hint alone (`Upstream said: gemini 400: …`); null on
752
+ * `completed` and whenever the failure carried none */
753
+ error_hint: string | null;
754
+ /** the failure-event vocabulary every `*.failed` carries (guide 12) */
755
+ level: 'info' | 'error';
756
+ state: 'ok' | 'broken';
757
+ }
758
+ /** The union a handler subscribed to `job.generation.` receives as `data`:
759
+ * narrow on `'status' in data` (queued carries none). */
760
+ export type JobGenerationEventPayload = JobGenerationQueuedEventPayload | JobGenerationSettledEventPayload;
761
+ /** The `data` of `job.succeeded` / `job.dead_lettered` (every run kind). The
762
+ * dead letter is the one that carries `level: 'error'` / `state: 'broken'`;
763
+ * `job.succeeded` carries neither. */
764
+ export interface JobRunEventPayload {
765
+ run_id: string;
766
+ job_name: string;
767
+ /** the attempt that settled the run (1-based) */
768
+ attempt: number;
769
+ level?: 'error';
770
+ state?: 'broken';
771
+ }
772
+ /** Event name → typed `data`, for the events that settle work you started.
773
+ * `VxilEventPayload<'job.generation.failed'>` names one; everything else on
774
+ * the catalog is `Record<string, unknown>` until typed here. */
775
+ export interface VxilEventPayloads {
776
+ 'job.generation.queued': JobGenerationQueuedEventPayload;
777
+ 'job.generation.completed': JobGenerationSettledEventPayload;
778
+ 'job.generation.failed': JobGenerationSettledEventPayload;
779
+ 'job.succeeded': JobRunEventPayload;
780
+ 'job.dead_lettered': JobRunEventPayload;
781
+ }
782
+ export type VxilEventPayload<E extends string> = E extends keyof VxilEventPayloads ? VxilEventPayloads[E] : Record<string, unknown>;
716
783
  /** A re-minted mid-stream connect token (GET /v1/ai/generations/{id}/token). */
717
784
  export interface AiStreamToken {
718
785
  generation_id: string;
@@ -1152,6 +1219,35 @@ export interface VxilSchemaShape {
1152
1219
  Output: unknown;
1153
1220
  }>;
1154
1221
  }
1222
+ /** Per-call options of `vx.fn.<name>(input, opts)`. */
1223
+ export interface FnInvokeOptions {
1224
+ /** `true` ⇒ the ASYNC http lane (`x-vxil-async: 1`): the call resolves to the
1225
+ * `202 { run_id, status:'queued' }` ack at once; the run is delivered through
1226
+ * the jobs lane with exactly one attempt and records the function's real
1227
+ * status — poll `vx.jobs.run(run_id)` or listen for `job.*` events.
1228
+ * Server-mode only (an end-user client gets `403 server_only`). */
1229
+ async?: boolean;
1230
+ /** Sent as `Idempotency-Key`: rides the envelope as `idempotency_key` (the
1231
+ * function dedupes on it); on the async lane it is also the run's dedupe key,
1232
+ * so a repeat with the same key returns the SAME `run_id`. */
1233
+ idempotencyKey?: string;
1234
+ }
1235
+ /** The async lane's 202 ack. */
1236
+ export interface FnAsyncAck {
1237
+ run_id: string;
1238
+ status: 'queued';
1239
+ /** present (true) when the Idempotency-Key matched an earlier run */
1240
+ deduplicated?: boolean;
1241
+ }
1242
+ /** `vx.fn.<name>`: the function's Output by default, the 202 ack with `{ async: true }`. */
1243
+ export interface FnInvoker<I, O> {
1244
+ (input: I, opts?: FnInvokeOptions & {
1245
+ async?: false;
1246
+ }): Promise<O>;
1247
+ (input: I, opts: FnInvokeOptions & {
1248
+ async: true;
1249
+ }): Promise<FnAsyncAck>;
1250
+ }
1155
1251
  /** feature client property → the feature key (in S['features']) that enables it.
1156
1252
  * The renamed namespaces map to their real feature ids. Used by EnabledVxil to
1157
1253
  * disable namespaces a tenant hasn't enabled. */
@@ -1540,8 +1636,12 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
1540
1636
  /** Typed function invoke (design §4.5). `vx.fn.<name>(payload)` POSTs to
1541
1637
  * /v1/fn/<name>; the generated `VxilSchema` types Input/Output (Level-0
1542
1638
  * opaque until a function declares a signature). A Proxy gives the
1543
- * `vx.fn.<name>` accessor shape without enumerating names at runtime. */
1544
- readonly fn: { [K in keyof S["functions"] & string]: (input: S["functions"][K]["Input"]) => Promise<S["functions"][K]["Output"]>; };
1639
+ * `vx.fn.<name>` accessor shape without enumerating names at runtime.
1640
+ * `vx.fn.<name>(payload, { async: true })` takes the ASYNC lane
1641
+ * (`x-vxil-async: 1`): it resolves to the `202 { run_id, status:'queued' }`
1642
+ * ack at once and the run is delivered through the jobs lane — poll
1643
+ * `vx.jobs.run(run_id)` or subscribe to `job.*` events. */
1644
+ readonly fn: { [K in keyof S["functions"] & string]: FnInvoker<S["functions"][K]["Input"], S["functions"][K]["Output"]>; };
1545
1645
  private call;
1546
1646
  readonly users: {
1547
1647
  upsert: (user: {
@@ -2148,7 +2248,13 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2148
2248
  /** `opts.anonymous_token` is REQUIRED for a link that was requested with
2149
2249
  * one (the guest claim above): the same guest session must present it,
2150
2250
  * else `401 invalid_session`. `merged` is true only when the guest was
2151
- * folded into an existing account (then `user_id` is that account). */
2251
+ * folded into an existing account (then `user_id` is that account), and
2252
+ * `rekeyed` rides beside it: true ⇒ the guest's payments / cms / files
2253
+ * rows were already moved onto `user_id` when this returned; false ⇒
2254
+ * the move continues in the background — wait for the
2255
+ * `auth.user.rekeyed` event before reading owner-scoped rows as the
2256
+ * merged user. The event is written when the pass finishes and is
2257
+ * best-effort past this response (see `anonymous.link.verify`). */
2152
2258
  verify: (token: string, opts?: {
2153
2259
  anonymous_token?: string;
2154
2260
  }) => Promise<{
@@ -2156,6 +2262,7 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2156
2262
  session: AuthSession;
2157
2263
  verified: boolean;
2158
2264
  merged?: boolean;
2265
+ rekeyed?: boolean;
2159
2266
  }>;
2160
2267
  };
2161
2268
  /** Email OTP sign-in: a 6-digit single-use code (distinct from magic-link).
@@ -2204,7 +2311,20 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2204
2311
  * If the claimed email ALREADY has an account, the guest is MERGED
2205
2312
  * into it: `user_id` is the existing account, `merged: true`, and a
2206
2313
  * fresh `session.token` (same session, re-signed for the merged
2207
- * identity — swap it client-side; other guest sessions are revoked). */
2314
+ * identity — swap it client-side; other guest sessions are revoked).
2315
+ * `rekeyed` (present on a merge): true ⇒ the guest's payments / cms /
2316
+ * files rows were already moved onto `user_id` when this returned, so
2317
+ * the merged user's next request finds them; false ⇒ the move ran
2318
+ * past its in-request budget and continues in the background — wait
2319
+ * for the `auth.user.rekeyed { from_user_id, into_user_id, moved,
2320
+ * done, failed }` event before reading owner-scoped rows as `user_id`.
2321
+ * That event is written when the pass finishes and is BEST-EFFORT past
2322
+ * this response: the continuation is not re-run if the instance
2323
+ * serving it is evicted, and a failed event write is logged, not
2324
+ * retried — treat `rekeyed: false` with no event within a minute or
2325
+ * so as "poll your own rows", or re-run the idempotent
2326
+ * `cms.items.reKey` / `files.reKey` / `payments.reKeySubscriptions`
2327
+ * routes from the `auth.user.merged` backstop. */
2208
2328
  verify: (input: {
2209
2329
  token: string;
2210
2330
  email: string;
@@ -2214,6 +2334,7 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2214
2334
  email: string;
2215
2335
  verified: boolean;
2216
2336
  merged?: true;
2337
+ rekeyed?: boolean;
2217
2338
  session?: {
2218
2339
  token: string;
2219
2340
  expires_at: string;
@@ -2391,6 +2512,10 @@ export declare class Vxil<S extends VxilSchemaShape = VxilSchemaShape> {
2391
2512
  session: AuthSession;
2392
2513
  verified: boolean;
2393
2514
  linked: "created" | "existing" | "promoted" | "merged";
2515
+ /** present exactly when `linked === 'merged'`: true ⇒ the guest's rows
2516
+ * are already under `user_id`; false ⇒ wait for `auth.user.rekeyed`
2517
+ * (best-effort past this response — see `anonymous.link.verify`) */
2518
+ rekeyed?: boolean;
2394
2519
  /** sessions taken over by auth config session.maxConcurrent (present once opted in) */
2395
2520
  took_over?: string[];
2396
2521
  }>;
package/dist/index.js CHANGED
@@ -228,7 +228,11 @@ export class Vxil {
228
228
  /** Typed function invoke (design §4.5). `vx.fn.<name>(payload)` POSTs to
229
229
  * /v1/fn/<name>; the generated `VxilSchema` types Input/Output (Level-0
230
230
  * opaque until a function declares a signature). A Proxy gives the
231
- * `vx.fn.<name>` accessor shape without enumerating names at runtime. */
231
+ * `vx.fn.<name>` accessor shape without enumerating names at runtime.
232
+ * `vx.fn.<name>(payload, { async: true })` takes the ASYNC lane
233
+ * (`x-vxil-async: 1`): it resolves to the `202 { run_id, status:'queued' }`
234
+ * ack at once and the run is delivered through the jobs lane — poll
235
+ * `vx.jobs.run(run_id)` or subscribe to `job.*` events. */
232
236
  fn = new Proxy({}, {
233
237
  get: (_t, prop) => {
234
238
  if (typeof prop !== 'string')
@@ -236,10 +240,14 @@ export class Vxil {
236
240
  // /v1/fn/<name> returns the function's RAW Response (not a {data,meta}
237
241
  // envelope), so bypass call()'s unwrap — read the body directly, like
238
242
  // audit.export. Errors still surface as VxilError.
239
- return async (payload) => {
243
+ return async (payload, opts) => {
240
244
  const { response: res, text } = await this.transport.send(`${this.base}${this.path(`/v1/fn/${encodeURIComponent(prop)}`)}`, {
241
245
  method: 'POST',
242
- headers: { authorization: `Bearer ${this.key}`, ...this.authHeaders(), 'content-type': 'application/json' },
246
+ headers: {
247
+ authorization: `Bearer ${this.key}`, ...this.authHeaders(), 'content-type': 'application/json',
248
+ ...(opts?.async ? { 'x-vxil-async': '1' } : {}),
249
+ ...(opts?.idempotencyKey ? { 'idempotency-key': opts.idempotencyKey } : {}),
250
+ },
243
251
  body: JSON.stringify(payload ?? {}),
244
252
  });
245
253
  if (!res.ok) {
@@ -650,7 +658,13 @@ export class Vxil {
650
658
  /** `opts.anonymous_token` is REQUIRED for a link that was requested with
651
659
  * one (the guest claim above): the same guest session must present it,
652
660
  * else `401 invalid_session`. `merged` is true only when the guest was
653
- * folded into an existing account (then `user_id` is that account). */
661
+ * folded into an existing account (then `user_id` is that account), and
662
+ * `rekeyed` rides beside it: true ⇒ the guest's payments / cms / files
663
+ * rows were already moved onto `user_id` when this returned; false ⇒
664
+ * the move continues in the background — wait for the
665
+ * `auth.user.rekeyed` event before reading owner-scoped rows as the
666
+ * merged user. The event is written when the pass finishes and is
667
+ * best-effort past this response (see `anonymous.link.verify`). */
654
668
  verify: async (token, opts = {}) => (await this.call('POST', '/v1/auth/magic-link/verify', { token, ...(opts.anonymous_token !== undefined ? { anonymous_token: opts.anonymous_token } : {}) })).data,
655
669
  },
656
670
  /** Email OTP sign-in: a 6-digit single-use code (distinct from magic-link).
@@ -674,7 +688,20 @@ export class Vxil {
674
688
  * If the claimed email ALREADY has an account, the guest is MERGED
675
689
  * into it: `user_id` is the existing account, `merged: true`, and a
676
690
  * fresh `session.token` (same session, re-signed for the merged
677
- * identity — swap it client-side; other guest sessions are revoked). */
691
+ * identity — swap it client-side; other guest sessions are revoked).
692
+ * `rekeyed` (present on a merge): true ⇒ the guest's payments / cms /
693
+ * files rows were already moved onto `user_id` when this returned, so
694
+ * the merged user's next request finds them; false ⇒ the move ran
695
+ * past its in-request budget and continues in the background — wait
696
+ * for the `auth.user.rekeyed { from_user_id, into_user_id, moved,
697
+ * done, failed }` event before reading owner-scoped rows as `user_id`.
698
+ * That event is written when the pass finishes and is BEST-EFFORT past
699
+ * this response: the continuation is not re-run if the instance
700
+ * serving it is evicted, and a failed event write is logged, not
701
+ * retried — treat `rekeyed: false` with no event within a minute or
702
+ * so as "poll your own rows", or re-run the idempotent
703
+ * `cms.items.reKey` / `files.reKey` / `payments.reKeySubscriptions`
704
+ * routes from the `auth.user.merged` backstop. */
678
705
  verify: async (input) => (await this.call('POST', '/v1/auth/anonymous/link/verify', input)).data,
679
706
  },
680
707
  },
@@ -782,7 +809,9 @@ export class Vxil {
782
809
  /** `anonymous_token`: the caller's CURRENT guest session bearer — the
783
810
  * guest is PROMOTED to this identity (same user id, `linked: 'promoted'`)
784
811
  * or, if the identity already has an account, MERGED into it
785
- * (`linked: 'merged'`, `user_id` = that account). */
812
+ * (`linked: 'merged'`, `user_id` = that account, `rekeyed` says whether
813
+ * the guest's owner-scoped rows already moved — see
814
+ * `anonymous.link.verify`). */
786
815
  input) => (await this.call('POST', `/v1/auth/oauth/${encodeURIComponent(provider)}/native`, input)).data,
787
816
  },
788
817
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/sdk",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Typed client for the Vxil REST API (notifications, auth, jobs, files, cms, comments, webhooks, realtime, orgs, rate-limits).",