okengine 0.21.1 → 0.22.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.
Files changed (140) hide show
  1. package/AGENTS.md +4 -4
  2. package/LICENSE +202 -0
  3. package/NOTICE +2 -0
  4. package/README.md +15 -9
  5. package/manifest.v1.schema.json +10 -0
  6. package/package.json +22 -20
  7. package/site/content/docs/ai/skills.mdx +1 -1
  8. package/site/content/docs/client/calling.mdx +27 -19
  9. package/site/content/docs/client/index.mdx +1 -1
  10. package/site/content/docs/elements/channel/email.mdx +1 -1
  11. package/site/content/docs/elements/channel/index.mdx +1 -1
  12. package/site/content/docs/elements/flow/http.mdx +4 -4
  13. package/site/content/docs/elements/flow/index.mdx +3 -3
  14. package/site/content/docs/elements/flow/meta.json +1 -1
  15. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  16. package/site/content/docs/elements/vault/rotation.mdx +15 -0
  17. package/site/content/docs/plugins/otp.mdx +31 -31
  18. package/site/content/docs/recipes/mailpit.mdx +1 -1
  19. package/site/content/docs/recipes/meilisearch.mdx +1 -1
  20. package/site/content/docs/recipes/pgdog.mdx +1 -1
  21. package/site/content/docs/reference/configuration.mdx +2 -2
  22. package/site/content/docs/reference/errors.mdx +12 -2
  23. package/site/content/docs/reference/fx.mdx +28 -15
  24. package/site/content/docs/reference/idempotency.mdx +192 -0
  25. package/site/content/docs/reference/index.mdx +6 -1
  26. package/site/content/docs/reference/meta.json +2 -1
  27. package/site/content/docs/understand/meta.json +1 -1
  28. package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
  29. package/site/content/docs/understand/the-architecture.mdx +14 -12
  30. package/site/content/docs/understand/try-it.mdx +3 -3
  31. package/src/cli/competitor-mention-removal.test.ts +3 -3
  32. package/src/cli/docker-cli.test.ts +1 -1
  33. package/src/cli/index.ts +9 -1
  34. package/src/cli/load-config.images.test.ts +10 -10
  35. package/src/cli/load-config.ts +1 -1
  36. package/src/cli/vault-cmd.test.ts +23 -0
  37. package/src/cli/vault-cmd.ts +7 -0
  38. package/src/client/create.ts +18 -1
  39. package/src/client/transport.test.ts +225 -0
  40. package/src/client/transport.ts +132 -19
  41. package/src/client/types.ts +29 -3
  42. package/src/compiler/effects-infer.ts +726 -16
  43. package/src/compiler/extract.test.ts +37 -0
  44. package/src/compiler/extract.ts +113 -12
  45. package/src/compiler/fixtures/skyport.expected.json +24 -0
  46. package/src/compiler/fx-follow.test.ts +164 -0
  47. package/src/compiler/fx-index.ts +399 -0
  48. package/src/console/server/flows-invoke.test.ts +2 -2
  49. package/src/console/ui-next/dist/assets/{access-page-Bgt9bq2r.js → access-page-Ze6ckjdd.js} +2 -2
  50. package/src/console/ui-next/dist/assets/agent-disclosure-Bkohc_8X.js +1 -0
  51. package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-rL0lHxat.js} +1 -1
  52. package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-Deo4PP18.js} +1 -1
  53. package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-CoJ6amHf.js} +1 -1
  54. package/src/console/ui-next/dist/assets/{copy-inline-button-CAYD18cr.js → copy-inline-button-CWG5_ZOh.js} +1 -1
  55. package/src/console/ui-next/dist/assets/{detail-header-DVWNjWjg.js → detail-header-b_ZzIsmU.js} +1 -1
  56. package/src/console/ui-next/dist/assets/{dropdown-menu-4h2LOVXM.js → dropdown-menu-nwXEO1Ac.js} +1 -1
  57. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +9 -0
  58. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +1 -0
  59. package/src/console/ui-next/dist/assets/{explorer-empty-CJs5A-wm.js → explorer-empty-C8sSCqYT.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{flows-page-BMHs-IzK.js → flows-page-D2E7BtKK.js} +1 -1
  61. package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-CPYBzDh0.js} +1 -1
  62. package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DTPevKmM.js} +1 -1
  63. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +58 -0
  64. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +4 -0
  65. package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-BM27lXld.js} +3 -3
  66. package/src/console/ui-next/dist/assets/{request-meta-DMbnAe3f.js → request-meta-BjFT7DNl.js} +1 -1
  67. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +1 -0
  68. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +41 -0
  69. package/src/console/ui-next/dist/assets/{trace-detail-sheet-Htk8Cm9t.js → trace-detail-sheet-Cmitq3da.js} +2 -2
  70. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CFMPWX4f.js → tree-expand-toggle-CbDIB-7x.js} +2 -2
  71. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +1 -0
  72. package/src/console/ui-next/dist/assets/{use-vault-list-CT4-gajj.js → use-vault-list-Cs45Czkt.js} +1 -1
  73. package/src/console/ui-next/dist/assets/{vault-page-CBYl0LW_.js → vault-page-BOMr0go9.js} +2 -2
  74. package/src/console/ui-next/dist/index.html +2 -2
  75. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +9 -0
  76. package/src/docker/docker.test.ts +13 -13
  77. package/src/docker/images-config.test.ts +12 -12
  78. package/src/docker/stack-id.test.ts +1 -1
  79. package/src/drivers/journal-postgres.ts +168 -3
  80. package/src/drivers/meilisearch.ts +2 -0
  81. package/src/drivers/vault-builtin.test.ts +10 -0
  82. package/src/drivers/vault-builtin.ts +13 -1
  83. package/src/drivers/vault-remote-bag.ts +5 -1
  84. package/src/drivers/vault-types.ts +6 -1
  85. package/src/elements/store/emit-drizzle.ts +2 -2
  86. package/src/elements/store/live-default.test.ts +2 -0
  87. package/src/elements/store/live-http.test.ts +1 -0
  88. package/src/elements/store/resource-list-docs.test.ts +2 -2
  89. package/src/elements/store/resource.test.ts +3 -3
  90. package/src/elements/store/schema-decl.test.ts +1 -0
  91. package/src/elements/store/sql-condition.ts +24 -7
  92. package/src/elements/store/upsert-app.test.ts +1 -1
  93. package/src/elements/vault/audit.test.ts +137 -0
  94. package/src/elements/vault/audit.ts +106 -13
  95. package/src/elements/vault/boot-chain.ts +9 -1
  96. package/src/elements/vault/builtin-adapter.ts +33 -3
  97. package/src/i18n/catalogs/ar.ts +4 -0
  98. package/src/i18n/catalogs/en.ts +4 -0
  99. package/src/kernel/abort-scope.ts +33 -2
  100. package/src/kernel/app.ts +191 -18
  101. package/src/kernel/auto-cache.test.ts +6 -6
  102. package/src/kernel/boot-bind/vault.ts +1 -0
  103. package/src/kernel/boot.ts +19 -1
  104. package/src/kernel/builtin-errors.ts +8 -0
  105. package/src/kernel/client-descriptor.ts +1 -0
  106. package/src/kernel/concurrency.test.ts +33 -0
  107. package/src/kernel/effects-stamping.test.ts +2 -2
  108. package/src/kernel/errors-compiler.ts +20 -0
  109. package/src/kernel/external-effects.test.ts +36 -0
  110. package/src/kernel/flow.ts +24 -0
  111. package/src/kernel/fx-fetch.ts +5 -1
  112. package/src/kernel/fx.ts +5 -1
  113. package/src/kernel/idempotency-store.ts +578 -0
  114. package/src/kernel/idempotency.test.ts +556 -0
  115. package/src/kernel/idempotency.ts +304 -0
  116. package/src/kernel/index.ts +1 -0
  117. package/src/kernel/journal.ts +56 -0
  118. package/src/manifest/types.ts +8 -0
  119. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  120. package/src/plugins/otp.test.ts +65 -1
  121. package/src/plugins/otp.ts +59 -7
  122. package/src/release/absolute-regression.test.ts +63 -0
  123. package/src/release/http-graph.test.ts +25 -0
  124. package/src/release/http-graph.ts +90 -0
  125. package/src/release/index.ts +3 -0
  126. package/src/release/limits.ts +17 -2
  127. package/src/release/measure.ts +105 -14
  128. package/src/test/create-test-app.test.ts +3 -1
  129. package/src/test/create-test-app.ts +40 -0
  130. package/src/test/live-signals.test.ts +1 -0
  131. package/src/test/provisions.integration.test.ts +1 -0
  132. package/src/test/tenant-isolation.test.ts +1 -0
  133. package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
  134. package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
  135. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  136. package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
  137. package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
  138. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  139. package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
  140. package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
@@ -20,11 +20,18 @@ import {
20
20
  toQuery,
21
21
  } from "./wire.ts";
22
22
 
23
- /** Per-call transport options (binary decode, abort). */
23
+ /** Per-call transport options (binary decode, abort, retry opt-in). */
24
24
  export interface TransportCallOptions {
25
25
  readonly headers?: ClientHeaders;
26
26
  readonly response?: "json" | "blob" | "arrayBuffer";
27
27
  readonly signal?: AbortSignal;
28
+ /**
29
+ * Repeat this call under the client `retry` policy even when it sends
30
+ * no idempotency key.
31
+ */
32
+ readonly retry?: boolean;
33
+ /** Stable key, or `false` to send none. */
34
+ readonly idempotencyKey?: string | false;
28
35
  }
29
36
 
30
37
  /** Internal transport handle. */
@@ -63,10 +70,25 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
63
70
  let delay = delay0;
64
71
  const callClientOpts: ClientOptions =
65
72
  callOpts.signal !== undefined ? { ...opts, signal: callOpts.signal } : opts;
73
+ const method = (opts.routes?.[key.replace("/", ".")]?.method ?? "POST").toUpperCase();
74
+ const idempotencyKey = resolveIdempotencyKey(method, callOpts.idempotencyKey);
75
+ const allowRetry =
76
+ callOpts.retry === true ||
77
+ method === "GET" ||
78
+ method === "QUERY" ||
79
+ idempotencyKey !== undefined;
66
80
 
67
81
  for (;;) {
68
82
  try {
69
- const res = await once(base, key, input, callClientOpts, fetchFn, callOpts.headers);
83
+ const res = await once(
84
+ base,
85
+ key,
86
+ input,
87
+ callClientOpts,
88
+ fetchFn,
89
+ callOpts.headers,
90
+ idempotencyKey,
91
+ );
70
92
  if (
71
93
  res.status === 401 &&
72
94
  opts.auth &&
@@ -78,9 +100,22 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
78
100
  await opts.auth.refresh();
79
101
  continue;
80
102
  }
103
+ if (res.status === 409 && allowRetry && attempt < retries) {
104
+ const seconds = Number(res.headers.get("retry-after"));
105
+ const wait = seconds > 0 ? seconds * 1000 : delay;
106
+ const structured = await decodeIfEnvelope(res);
107
+ if (structured?.error?.code === "IdempotencyInProgress") {
108
+ await sleep(wait);
109
+ delay *= backoff;
110
+ attempt += 1;
111
+ continue;
112
+ }
113
+ if (structured) return stampReplay(structured, res);
114
+ return transportEnvelope("HTTP 409", 409);
115
+ }
81
116
  if (res.status >= 500) {
82
117
  const structured = await decodeIfEnvelope(res);
83
- if (structured) return structured;
118
+ if (structured) return stampReplay(structured, res);
84
119
  throw new Error(`HTTP ${res.status}`);
85
120
  }
86
121
  if (callOpts.response === "blob" || callOpts.response === "arrayBuffer") {
@@ -89,7 +124,7 @@ export function createTransport(base: string, opts: ClientOptions = {}): Transpo
89
124
  return decode(res);
90
125
  } catch (err) {
91
126
  const transient = isTransient(err);
92
- if (!transient || attempt >= retries) {
127
+ if (!transient || !allowRetry || attempt >= retries) {
93
128
  return transportEnvelope(err instanceof Error ? err.message : String(err));
94
129
  }
95
130
  await sleep(delay);
@@ -127,10 +162,20 @@ function normalizeCallOpts(
127
162
  if (Array.isArray(headersOrOpts)) return { headers: headersOrOpts };
128
163
  if (
129
164
  typeof headersOrOpts === "object" &&
130
- ("response" in headersOrOpts || "signal" in headersOrOpts || "headers" in headersOrOpts)
165
+ ("response" in headersOrOpts ||
166
+ "signal" in headersOrOpts ||
167
+ "headers" in headersOrOpts ||
168
+ "retry" in headersOrOpts ||
169
+ "idempotencyKey" in headersOrOpts)
131
170
  ) {
132
171
  const o = headersOrOpts as TransportCallOptions;
133
- if (o.response !== undefined || o.signal !== undefined || o.headers !== undefined) {
172
+ if (
173
+ o.response !== undefined ||
174
+ o.signal !== undefined ||
175
+ o.headers !== undefined ||
176
+ o.retry !== undefined ||
177
+ o.idempotencyKey !== undefined
178
+ ) {
134
179
  return o;
135
180
  }
136
181
  }
@@ -138,7 +183,9 @@ function normalizeCallOpts(
138
183
  }
139
184
 
140
185
  /**
141
- * Single HTTP attempt. Throws on network / abort / 5xx (retryable).
186
+ * Single HTTP attempt. Throws on network / abort / non-envelope 5xx.
187
+ * A client timeout is rethrown as a plain `Error` so the retry loop can
188
+ * repeat it. A caller `AbortSignal` stays an `AbortError` and is not retried.
142
189
  *
143
190
  * @param base - Origin
144
191
  * @param key - `unit/flow`
@@ -154,6 +201,7 @@ async function once(
154
201
  opts: ClientOptions,
155
202
  fetchFn: ClientFetch,
156
203
  callHeaders?: ClientHeaders,
204
+ idempotencyKey?: string,
157
205
  ): Promise<Response> {
158
206
  const route = opts.routes?.[key.replace("/", ".")];
159
207
  const { url, method, body } = route
@@ -163,6 +211,7 @@ async function once(
163
211
  const headers = new Headers();
164
212
  applyHeaderBag(headers, await resolveHeaders(opts));
165
213
  applyHeaderBag(headers, callHeaders);
214
+ if (idempotencyKey !== undefined) headers.set("Idempotency-Key", idempotencyKey);
166
215
  if (body !== undefined && !headers.has("content-type") && typeof body === "string") {
167
216
  headers.set("content-type", "application/json");
168
217
  }
@@ -172,13 +221,20 @@ async function once(
172
221
  const signal =
173
222
  opts.signal && timeout ? AbortSignal.any([opts.signal, timeout]) : (opts.signal ?? timeout);
174
223
 
175
- return await fetchFn(url, {
176
- method,
177
- headers,
178
- body: body as RequestInit["body"],
179
- signal,
180
- ...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}),
181
- });
224
+ try {
225
+ return await fetchFn(url, {
226
+ method,
227
+ headers,
228
+ body: body as RequestInit["body"],
229
+ signal,
230
+ ...(opts.credentials !== undefined ? { credentials: opts.credentials } : {}),
231
+ });
232
+ } catch (err) {
233
+ if (isAbortError(err) && timeout?.aborted === true && opts.signal?.aborted !== true) {
234
+ throw new Error(err instanceof Error ? err.message : "timeout");
235
+ }
236
+ throw err;
237
+ }
182
238
  }
183
239
 
184
240
  /**
@@ -253,12 +309,12 @@ function isRawBody(input: unknown): input is ClientBodyInit {
253
309
 
254
310
  async function decode(res: Response): Promise<ClientEnvelope> {
255
311
  if (res.status === 204) {
256
- return { data: undefined, error: null };
312
+ return stampReplay({ data: undefined, error: null }, res);
257
313
  }
258
314
 
259
315
  const text = await res.text();
260
316
  if (!text) {
261
- if (res.ok) return { data: undefined, error: null };
317
+ if (res.ok) return stampReplay({ data: undefined, error: null }, res);
262
318
  return transportEnvelope(`HTTP ${res.status}`, res.status);
263
319
  }
264
320
 
@@ -270,11 +326,11 @@ async function decode(res: Response): Promise<ClientEnvelope> {
270
326
  }
271
327
 
272
328
  if (json !== null && typeof json === "object" && "data" in json && "error" in json) {
273
- return json as ClientEnvelope;
329
+ return stampReplay(json as ClientEnvelope, res);
274
330
  }
275
331
 
276
332
  if (res.ok) {
277
- return { data: json, error: null };
333
+ return stampReplay({ data: json, error: null }, res);
278
334
  }
279
335
 
280
336
  return transportEnvelope(`HTTP ${res.status}`, res.status);
@@ -287,7 +343,64 @@ async function decodeBinary(res: Response, mode: "blob" | "arrayBuffer"): Promis
287
343
  return transportEnvelope(`HTTP ${res.status}`, res.status);
288
344
  }
289
345
  const data = mode === "blob" ? await res.blob() : await res.arrayBuffer();
290
- return { data, error: null };
346
+ return stampReplay({ data, error: null }, res);
347
+ }
348
+
349
+ /**
350
+ * Default `okid()` alphabet, inlined.
351
+ *
352
+ * Same 64-character Base64URL order as {@link OKID_ALPHABET}. Importing
353
+ * `okid` would pull `lazyRequire` and every alphabet constant into the client.
354
+ */
355
+ export const IDEMPOTENCY_KEY_ALPHABET =
356
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
357
+
358
+ /**
359
+ * 21-char key from `crypto.getRandomValues`.
360
+ *
361
+ * `getRandomValues` works on plain HTTP. When `crypto` is missing entirely,
362
+ * returns `undefined` so the call sends no key and runs once.
363
+ */
364
+ export function mintIdempotencyKey(): string | undefined {
365
+ const webCrypto = globalThis.crypto;
366
+ if (webCrypto?.getRandomValues === undefined) return undefined;
367
+ const bytes = new Uint8Array(21);
368
+ webCrypto.getRandomValues(bytes);
369
+ let out = "";
370
+ for (let i = 0; i < 21; i++) out += IDEMPOTENCY_KEY_ALPHABET[bytes[i]! & 63]!;
371
+ return out;
372
+ }
373
+
374
+ /**
375
+ * Key for this logical call, or `undefined` when none is sent.
376
+ *
377
+ * @param method - HTTP method, already uppercased
378
+ * @param opt - Per-call override
379
+ */
380
+ function resolveIdempotencyKey(
381
+ method: string,
382
+ opt: string | false | undefined,
383
+ ): string | undefined {
384
+ if (opt === false) return undefined;
385
+ if (typeof opt === "string") return opt;
386
+ if (method === "GET") return undefined;
387
+ return mintIdempotencyKey();
388
+ }
389
+
390
+ /**
391
+ * Copy `Idempotent-Replayed: true` onto result meta.
392
+ *
393
+ * @param envelope - Decoded envelope
394
+ * @param res - HTTP response
395
+ */
396
+ function stampReplay(envelope: ClientEnvelope, res: Response): ClientEnvelope {
397
+ if (res.headers.get("idempotent-replayed") !== "true") return envelope;
398
+ const prev = envelope.meta;
399
+ return { ...envelope, meta: { ...prev, idempotentReplayed: true } };
400
+ }
401
+
402
+ function isAbortError(err: unknown): boolean {
403
+ return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
291
404
  }
292
405
 
293
406
  function isTransient(err: unknown): boolean {
@@ -146,6 +146,8 @@ export type ClientListMeta = {
146
146
  readonly prev?: ClientPagerLink | null;
147
147
  readonly total?: number;
148
148
  readonly offset?: number;
149
+ /** Set when the server replayed a completed idempotent call. */
150
+ readonly idempotentReplayed?: true;
149
151
  };
150
152
 
151
153
  /**
@@ -163,7 +165,11 @@ export type ClientEnvelope<
163
165
  readonly error: null;
164
166
  readonly meta?: ClientListMeta;
165
167
  }
166
- | { readonly data: null; readonly error: ClientError<E> | TransportError };
168
+ | {
169
+ readonly data: null;
170
+ readonly error: ClientError<E> | TransportError;
171
+ readonly meta?: ClientListMeta;
172
+ };
167
173
 
168
174
  /**
169
175
  * Client call result — envelope plus always-callable `next` / `prev`.
@@ -251,7 +257,15 @@ export interface ClientOptions {
251
257
  readonly headers?: ClientHeaders | (() => ClientHeaders | Promise<ClientHeaders>);
252
258
  /** Abort the request after this many milliseconds. */
253
259
  readonly timeout?: number;
254
- /** Retry transient failures (network / 5xx). */
260
+ /**
261
+ * Retry transient failures (network errors, client timeouts, and
262
+ * non-envelope 5xx).
263
+ *
264
+ * A call retries when it is `GET` or `QUERY`, or when it carries an
265
+ * idempotency key. Keys are sent automatically on every non-GET call.
266
+ * `idempotencyKey: false` runs once unless that call passes `{ retry: true }`.
267
+ * `409 IdempotencyInProgress` waits for `Retry-After` and counts as an attempt.
268
+ */
255
269
  readonly retry?: {
256
270
  /** Extra attempts after the first (default 0). */
257
271
  readonly retries?: number;
@@ -296,11 +310,22 @@ export interface ClientOptions {
296
310
  export type ClientThenableIterable<T> = PromiseLike<T> & AsyncIterable<T>;
297
311
 
298
312
  /**
299
- * Per-call options on a Flow invoke (binary decode / abort).
313
+ * Per-call options on a Flow invoke (binary decode, abort, retry opt-in).
300
314
  */
301
315
  export interface ClientCallOpts {
302
316
  readonly response?: "json" | "blob" | "arrayBuffer";
303
317
  readonly signal?: AbortSignal;
318
+ /**
319
+ * Repeat this call under the client `retry` policy even when it sends
320
+ * no idempotency key.
321
+ */
322
+ readonly retry?: boolean;
323
+ /**
324
+ * Stable `Idempotency-Key` for this logical call. A string is sent as-is
325
+ * (one key per form render dedupes double submits). `false` sends no key.
326
+ * Omitted on a non-GET call generates one key and reuses it across retries.
327
+ */
328
+ readonly idempotencyKey?: string | false;
304
329
  }
305
330
 
306
331
  /**
@@ -503,6 +528,7 @@ export interface ClientDescriptor {
503
528
  readonly stream?: true;
504
529
  readonly matchKey?: readonly string[];
505
530
  readonly gates?: readonly string[];
531
+ readonly idempotency?: { readonly mode: "off" | "auto" | "required"; readonly ttl: string };
506
532
  };
507
533
  };
508
534
  };