okengine 0.21.0 → 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 (160) 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 -25
  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/dev.test.ts +105 -2
  33. package/src/cli/dev.ts +37 -2
  34. package/src/cli/docker-cli.test.ts +1 -1
  35. package/src/cli/index.ts +9 -1
  36. package/src/cli/load-config.images.test.ts +10 -10
  37. package/src/cli/load-config.ts +1 -1
  38. package/src/cli/vault-cmd.test.ts +23 -0
  39. package/src/cli/vault-cmd.ts +7 -0
  40. package/src/client/create.ts +31 -27
  41. package/src/client/live.ts +44 -101
  42. package/src/client/sse.ts +20 -66
  43. package/src/client/stream.ts +25 -67
  44. package/src/client/transport.test.ts +225 -0
  45. package/src/client/transport.ts +158 -90
  46. package/src/client/types.ts +29 -3
  47. package/src/client/wire.ts +119 -0
  48. package/src/compiler/aot.ts +3 -32
  49. package/src/compiler/dynamic.ts +13 -11
  50. package/src/compiler/effects-infer.ts +726 -16
  51. package/src/compiler/extract.test.ts +37 -0
  52. package/src/compiler/extract.ts +113 -12
  53. package/src/compiler/fixtures/skyport.expected.json +24 -0
  54. package/src/compiler/fx-follow.test.ts +164 -0
  55. package/src/compiler/fx-index.ts +399 -0
  56. package/src/compiler/interpret.ts +45 -0
  57. package/src/console/server/flows-invoke.test.ts +2 -2
  58. package/src/console/ui-next/dist/assets/{access-page-tIbsiphz.js → access-page-Ze6ckjdd.js} +2 -2
  59. package/src/console/ui-next/dist/assets/agent-disclosure-Bkohc_8X.js +1 -0
  60. package/src/console/ui-next/dist/assets/{cache-glyph-BCC-DxKT.js → cache-glyph-rL0lHxat.js} +1 -1
  61. package/src/console/ui-next/dist/assets/{call-pii-button-jOmYISdc.js → call-pii-button-Deo4PP18.js} +1 -1
  62. package/src/console/ui-next/dist/assets/{collapsible-DC2xNaAb.js → collapsible-CoJ6amHf.js} +1 -1
  63. package/src/console/ui-next/dist/assets/{copy-inline-button-CAYD18cr.js → copy-inline-button-CWG5_ZOh.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{detail-header-DVWNjWjg.js → detail-header-b_ZzIsmU.js} +1 -1
  65. package/src/console/ui-next/dist/assets/{dropdown-menu-4h2LOVXM.js → dropdown-menu-nwXEO1Ac.js} +1 -1
  66. package/src/console/ui-next/dist/assets/duration-tone-BbQ_8z50.js +9 -0
  67. package/src/console/ui-next/dist/assets/element-icons-CwyLpVXz.js +1 -0
  68. package/src/console/ui-next/dist/assets/{explorer-empty-CJs5A-wm.js → explorer-empty-C8sSCqYT.js} +1 -1
  69. package/src/console/ui-next/dist/assets/{flows-page-gT1lsWQK.js → flows-page-D2E7BtKK.js} +1 -1
  70. package/src/console/ui-next/dist/assets/{highlighted-json-C2GZEJNI.js → highlighted-json-CPYBzDh0.js} +1 -1
  71. package/src/console/ui-next/dist/assets/{http-method-BdYjcIrD.js → http-method-DTPevKmM.js} +1 -1
  72. package/src/console/ui-next/dist/assets/index-vTuwmeQz.js +58 -0
  73. package/src/console/ui-next/dist/assets/observability-page-SJTSzHFJ.js +4 -0
  74. package/src/console/ui-next/dist/assets/{replica-lag-C4QdAF7J.js → replica-lag-BM27lXld.js} +3 -3
  75. package/src/console/ui-next/dist/assets/{request-meta-C43DHyld.js → request-meta-BjFT7DNl.js} +1 -1
  76. package/src/console/ui-next/dist/assets/shortcut-keys-CUNegEV4.js +1 -0
  77. package/src/console/ui-next/dist/assets/store-page-Qj9ThRE-.js +41 -0
  78. package/src/console/ui-next/dist/assets/{trace-detail-sheet-Dcpo4Us_.js → trace-detail-sheet-Cmitq3da.js} +2 -2
  79. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CKJTuv43.js → tree-expand-toggle-CbDIB-7x.js} +2 -2
  80. package/src/console/ui-next/dist/assets/units-page-_PuYqFty.js +1 -0
  81. package/src/console/ui-next/dist/assets/{use-vault-list-CT4-gajj.js → use-vault-list-Cs45Czkt.js} +1 -1
  82. package/src/console/ui-next/dist/assets/{vault-page-BwZ9YjTW.js → vault-page-BOMr0go9.js} +2 -2
  83. package/src/console/ui-next/dist/index.html +2 -2
  84. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +9 -0
  85. package/src/docker/docker.test.ts +13 -13
  86. package/src/docker/images-config.test.ts +12 -12
  87. package/src/docker/stack-id.test.ts +1 -1
  88. package/src/drivers/journal-postgres.ts +168 -3
  89. package/src/drivers/meilisearch.ts +2 -0
  90. package/src/drivers/vault-builtin.test.ts +10 -0
  91. package/src/drivers/vault-builtin.ts +13 -1
  92. package/src/drivers/vault-remote-bag.ts +5 -1
  93. package/src/drivers/vault-types.ts +6 -1
  94. package/src/elements/store/emit-drizzle.ts +2 -2
  95. package/src/elements/store/live-default.test.ts +2 -0
  96. package/src/elements/store/live-http.test.ts +1 -0
  97. package/src/elements/store/resource-list-docs.test.ts +2 -2
  98. package/src/elements/store/resource.test.ts +3 -3
  99. package/src/elements/store/schema-decl.test.ts +1 -0
  100. package/src/elements/store/sql-condition.ts +24 -7
  101. package/src/elements/store/sql-session.ts +20 -4
  102. package/src/elements/store/upsert-app.test.ts +1 -1
  103. package/src/elements/vault/audit.test.ts +137 -0
  104. package/src/elements/vault/audit.ts +106 -13
  105. package/src/elements/vault/boot-chain.ts +9 -1
  106. package/src/elements/vault/builtin-adapter.ts +33 -3
  107. package/src/i18n/catalogs/ar.ts +4 -0
  108. package/src/i18n/catalogs/en.ts +4 -0
  109. package/src/kernel/abort-scope.ts +33 -2
  110. package/src/kernel/app.ts +200 -20
  111. package/src/kernel/auto-cache.test.ts +6 -6
  112. package/src/kernel/boot-bind/vault.ts +1 -0
  113. package/src/kernel/boot.ts +19 -1
  114. package/src/kernel/builtin-errors.ts +8 -0
  115. package/src/kernel/client-descriptor.test.ts +78 -0
  116. package/src/kernel/client-descriptor.ts +24 -0
  117. package/src/kernel/concurrency.test.ts +33 -0
  118. package/src/kernel/effects-stamping.test.ts +2 -2
  119. package/src/kernel/errors-compiler.ts +20 -0
  120. package/src/kernel/errors-text.ts +99 -0
  121. package/src/kernel/errors.ts +90 -138
  122. package/src/kernel/external-effects.test.ts +36 -0
  123. package/src/kernel/flow.ts +24 -0
  124. package/src/kernel/fx-fetch.ts +5 -1
  125. package/src/kernel/fx-sql-handle.ts +305 -0
  126. package/src/kernel/fx.ts +42 -330
  127. package/src/kernel/idempotency-store.ts +578 -0
  128. package/src/kernel/idempotency.test.ts +556 -0
  129. package/src/kernel/idempotency.ts +304 -0
  130. package/src/kernel/index.ts +1 -0
  131. package/src/kernel/journal.ts +56 -0
  132. package/src/kernel/json-result.ts +59 -0
  133. package/src/kernel/project-out.ts +6 -1
  134. package/src/manifest/types.ts +8 -0
  135. package/src/okid-extended.ts +175 -0
  136. package/src/okid-shared.ts +103 -0
  137. package/src/okid.ts +30 -213
  138. package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
  139. package/src/plugins/otp.test.ts +65 -1
  140. package/src/plugins/otp.ts +59 -7
  141. package/src/release/absolute-regression.test.ts +63 -0
  142. package/src/release/build-lib.ts +9 -0
  143. package/src/release/http-graph.test.ts +25 -0
  144. package/src/release/http-graph.ts +90 -0
  145. package/src/release/index.ts +3 -0
  146. package/src/release/limits.ts +17 -2
  147. package/src/release/measure.ts +105 -14
  148. package/src/test/create-test-app.test.ts +3 -1
  149. package/src/test/create-test-app.ts +40 -0
  150. package/src/test/live-signals.test.ts +1 -0
  151. package/src/test/provisions.integration.test.ts +1 -0
  152. package/src/test/tenant-isolation.test.ts +1 -0
  153. package/src/console/ui-next/dist/assets/agent-disclosure-CSKumwS2.js +0 -1
  154. package/src/console/ui-next/dist/assets/duration-tone-CwoV56jn.js +0 -9
  155. package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
  156. package/src/console/ui-next/dist/assets/index-Cul17AcV.js +0 -63
  157. package/src/console/ui-next/dist/assets/observability-page-oY9vdYBk.js +0 -4
  158. package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
  159. package/src/console/ui-next/dist/assets/store-page-CL0D9dOq.js +0 -41
  160. package/src/console/ui-next/dist/assets/units-page-1PlT19ft.js +0 -1
@@ -7,6 +7,19 @@
7
7
  * completed journal steps replay instead of re-running).
8
8
  */
9
9
 
10
+ import {
11
+ createPostgresIdempotencyStore,
12
+ IDEM_ATTACH_SQL,
13
+ IDEM_COMPLETE_SQL,
14
+ IDEM_DELETE_EXPIRED_ONE_SQL,
15
+ IDEM_FORFEIT_SQL,
16
+ IDEM_INSERT_SQL,
17
+ IDEM_PURGE_SQL,
18
+ IDEM_RECLAIM_SQL,
19
+ IDEM_REMOVE_SQL,
20
+ IDEM_RENEW_SQL,
21
+ IDEM_SELECT_SQL,
22
+ } from "../kernel/idempotency-store.ts";
10
23
  import {
11
24
  JOURNAL_DEFAULT_LEASE_MS,
12
25
  type JournalEntry,
@@ -199,6 +212,26 @@ async function ensureSchema(sql: PostgresJournalSql): Promise<void> {
199
212
  await sql.exec(
200
213
  `CREATE INDEX IF NOT EXISTS oke_journal_runs_lease ON oke_journal_runs (status, lease_expires_at)`,
201
214
  );
215
+ await sql.exec(`CREATE TABLE IF NOT EXISTS oke_idempotency (
216
+ tenant TEXT NOT NULL DEFAULT '',
217
+ principal TEXT NOT NULL,
218
+ flow TEXT NOT NULL,
219
+ key TEXT NOT NULL,
220
+ fingerprint TEXT NOT NULL,
221
+ status TEXT NOT NULL,
222
+ claim_token TEXT NOT NULL,
223
+ lease_expires_at BIGINT NOT NULL,
224
+ run_id TEXT,
225
+ response_status INTEGER,
226
+ response_headers TEXT,
227
+ response_body TEXT,
228
+ created_at BIGINT NOT NULL,
229
+ expires_at BIGINT NOT NULL,
230
+ PRIMARY KEY (tenant, principal, flow, key)
231
+ )`);
232
+ await sql.exec(
233
+ `CREATE INDEX IF NOT EXISTS oke_idempotency_expires ON oke_idempotency (expires_at)`,
234
+ );
202
235
  }
203
236
 
204
237
  /**
@@ -208,9 +241,25 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
208
241
  /** Force-kill mid-transaction (drops uncommitted state). */
209
242
  killActiveTransaction(): void;
210
243
  } {
211
- type State = { rows: JournalDbRow[] };
244
+ type IdemDbRow = {
245
+ tenant: string;
246
+ principal: string;
247
+ flow: string;
248
+ key: string;
249
+ fingerprint: string;
250
+ status: string;
251
+ claim_token: string;
252
+ lease_expires_at: number;
253
+ run_id: string | null;
254
+ response_status: number | null;
255
+ response_headers: string | null;
256
+ response_body: string | null;
257
+ created_at: number;
258
+ expires_at: number;
259
+ };
260
+ type State = { rows: JournalDbRow[]; idem: IdemDbRow[] };
212
261
 
213
- let committed: State = { rows: [] };
262
+ let committed: State = { rows: [], idem: [] };
214
263
  let active: { state: State; locked: Set<string>; done: boolean } | null = null;
215
264
  /** Run ids held by other active transactions (SKIP LOCKED). */
216
265
  const heldByTxn = new Set<string>();
@@ -222,7 +271,19 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
222
271
  }
223
272
 
224
273
  function cloneState(s: State): State {
225
- return { rows: s.rows.map((r) => ({ ...r })) };
274
+ return {
275
+ rows: s.rows.map((r) => ({ ...r })),
276
+ idem: s.idem.map((r) => ({ ...r })),
277
+ };
278
+ }
279
+
280
+ function idemPk(row: IdemDbRow, params: readonly unknown[], offset = 0): boolean {
281
+ return (
282
+ row.tenant === String(params[offset]) &&
283
+ row.principal === String(params[offset + 1]) &&
284
+ row.flow === String(params[offset + 2]) &&
285
+ row.key === String(params[offset + 3])
286
+ );
226
287
  }
227
288
 
228
289
  function claimable(r: JournalDbRow, holder: string, cutoff: number): boolean {
@@ -309,6 +370,12 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
309
370
  return state.rows.map((r) => ({ ...r }));
310
371
  }
311
372
 
373
+ if (text === IDEM_SELECT_SQL) {
374
+ return state.idem
375
+ .filter((row) => idemPk(row, params))
376
+ .map((row) => ({ ...row }) as Record<string, unknown>);
377
+ }
378
+
312
379
  throw new Error(`postgres journal fake: unsupported query: ${sql}`);
313
380
  },
314
381
  async exec(sql, params = []) {
@@ -388,6 +455,103 @@ export function createPostgresJournalFake(): PostgresJournalSql & {
388
455
  return { changes: 0 };
389
456
  }
390
457
 
458
+ if (text === IDEM_PURGE_SQL) {
459
+ const now = Number(params[0]);
460
+ const before = state.idem.length;
461
+ state.idem = state.idem.filter((row) => row.expires_at > now);
462
+ return { changes: before - state.idem.length };
463
+ }
464
+ if (text === IDEM_DELETE_EXPIRED_ONE_SQL) {
465
+ const now = Number(params[4]);
466
+ const idx = state.idem.findIndex((row) => idemPk(row, params) && row.expires_at <= now);
467
+ if (idx < 0) return { changes: 0 };
468
+ state.idem.splice(idx, 1);
469
+ return { changes: 1 };
470
+ }
471
+ if (text === IDEM_INSERT_SQL) {
472
+ if (state.idem.some((row) => idemPk(row, params))) return { changes: 0 };
473
+ state.idem.push({
474
+ tenant: String(params[0]),
475
+ principal: String(params[1]),
476
+ flow: String(params[2]),
477
+ key: String(params[3]),
478
+ fingerprint: String(params[4]),
479
+ status: "in_progress",
480
+ claim_token: String(params[5]),
481
+ lease_expires_at: Number(params[6]),
482
+ run_id: null,
483
+ response_status: null,
484
+ response_headers: null,
485
+ response_body: null,
486
+ created_at: Number(params[7]),
487
+ expires_at: Number(params[8]),
488
+ });
489
+ return { changes: 1 };
490
+ }
491
+ if (text === IDEM_RECLAIM_SQL) {
492
+ const row = state.idem.find(
493
+ (candidate) =>
494
+ idemPk(candidate, params, 2) && candidate.claim_token === String(params[6]),
495
+ );
496
+ if (row === undefined || row.status !== "in_progress") return { changes: 0 };
497
+ row.claim_token = String(params[0]);
498
+ row.lease_expires_at = Number(params[1]);
499
+ return { changes: 1 };
500
+ }
501
+ if (text === IDEM_COMPLETE_SQL) {
502
+ const row = state.idem.find(
503
+ (candidate) =>
504
+ idemPk(candidate, params, 4) && candidate.claim_token === String(params[8]),
505
+ );
506
+ if (row === undefined) return { changes: 0 };
507
+ row.status = "completed";
508
+ row.response_status = Number(params[0]);
509
+ row.response_headers = params[1] === null ? null : String(params[1]);
510
+ row.response_body = params[2] === null ? null : String(params[2]);
511
+ if (params[3] !== null && params[3] !== undefined) row.run_id = String(params[3]);
512
+ return { changes: 1 };
513
+ }
514
+ if (text === IDEM_REMOVE_SQL) {
515
+ const idx = state.idem.findIndex(
516
+ (candidate) => idemPk(candidate, params) && candidate.claim_token === String(params[4]),
517
+ );
518
+ if (idx < 0) return { changes: 0 };
519
+ state.idem.splice(idx, 1);
520
+ return { changes: 1 };
521
+ }
522
+ if (text === IDEM_RENEW_SQL) {
523
+ const row = state.idem.find(
524
+ (candidate) =>
525
+ idemPk(candidate, params, 1) &&
526
+ candidate.claim_token === String(params[5]) &&
527
+ candidate.status === "in_progress",
528
+ );
529
+ if (row === undefined) return { changes: 0 };
530
+ row.lease_expires_at = Number(params[0]);
531
+ return { changes: 1 };
532
+ }
533
+ if (text === IDEM_ATTACH_SQL) {
534
+ const row = state.idem.find(
535
+ (candidate) =>
536
+ idemPk(candidate, params, 1) && candidate.claim_token === String(params[5]),
537
+ );
538
+ if (row === undefined) return { changes: 0 };
539
+ row.run_id = String(params[0]);
540
+ return { changes: 1 };
541
+ }
542
+ if (text === IDEM_FORFEIT_SQL) {
543
+ const row = state.idem.find(
544
+ (candidate) =>
545
+ idemPk(candidate, params, 1) &&
546
+ candidate.claim_token === String(params[5]) &&
547
+ candidate.status === "in_progress",
548
+ );
549
+ if (row === undefined) return { changes: 0 };
550
+ row.claim_token = String(params[0]);
551
+ row.lease_expires_at = 0;
552
+ return { changes: 1 };
553
+ }
554
+
391
555
  throw new Error(`postgres journal fake: unsupported exec: ${sql}`);
392
556
  },
393
557
  async begin(fn) {
@@ -460,6 +624,7 @@ export async function createPostgresJournalStore(
460
624
 
461
625
  const store: PostgresJournalStore = {
462
626
  sql,
627
+ idempotency: createPostgresIdempotencyStore(sql),
463
628
  async get(runId) {
464
629
  const rows = await sql.query(`SELECT * FROM oke_journal_runs WHERE id = ?`, [runId]);
465
630
  if (!rows[0]) return undefined;
@@ -22,6 +22,7 @@ import type {
22
22
  TextIndexStore,
23
23
  } from "./types.ts";
24
24
  import { hostFromUrl } from "./external.ts";
25
+ import { requestSignal } from "../kernel/abort-scope.ts";
25
26
 
26
27
  /** Error thrown when the remote Meilisearch is unreachable / unhealthy / errors. */
27
28
  export class MeilisearchUnavailableError extends Error {
@@ -134,6 +135,7 @@ export async function openMeilisearchIndex(
134
135
  res = await fetchFn(`${url}${path}`, {
135
136
  ...init,
136
137
  headers: { ...headers, ...(init?.headers ?? {}) },
138
+ signal: requestSignal(init?.signal ?? null),
137
139
  });
138
140
  } catch (err) {
139
141
  throw new MeilisearchUnavailableError(
@@ -115,4 +115,14 @@ describe("builtin vault driver", () => {
115
115
  await rm(dir, { recursive: true, force: true });
116
116
  }
117
117
  }, 15_000);
118
+
119
+ test("webhook audit without a URL fails open instead of degrading", async () => {
120
+ await expect(
121
+ builtinVaultDriver.open({
122
+ connection: sharedConn,
123
+ env: {},
124
+ audit: { sink: "webhook" },
125
+ }),
126
+ ).rejects.toThrow(/webhookUrl/);
127
+ });
118
128
  });
@@ -18,6 +18,7 @@ import {
18
18
  sqlConnectionAsExec,
19
19
  type BuiltinVaultAdapter,
20
20
  } from "../elements/vault/builtin-adapter.ts";
21
+ import { isVaultError } from "../elements/vault/errors.ts";
21
22
  import type { SqlConnection } from "./types.ts";
22
23
  import type { VaultBag, VaultDriver, VaultOpenOptions } from "./vault-types.ts";
23
24
 
@@ -39,6 +40,10 @@ export interface OpenBuiltinVaultOptions {
39
40
  readonly env?: Readonly<Record<string, string | undefined>>;
40
41
  /** DEKs re-wrapped per master-rotation batch. */
41
42
  readonly kekRewrapBatchSize?: number;
43
+ /** `vault.audit` from config. Omitted means the SQL hash chain. */
44
+ readonly audit?: import("../elements/vault/types.ts").VaultAuditConfig;
45
+ /** Fetch override for the webhook audit sink (tests). */
46
+ readonly fetch?: typeof globalThis.fetch;
42
47
  }
43
48
 
44
49
  /** An adapter plus the connection this module opened for it, if any. */
@@ -76,6 +81,8 @@ export async function openBuiltinVaultAdapter(
76
81
  ...(options.kekRewrapBatchSize === undefined
77
82
  ? {}
78
83
  : { kekRewrapBatchSize: options.kekRewrapBatchSize }),
84
+ ...(options.audit === undefined ? {} : { audit: options.audit }),
85
+ ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
79
86
  });
80
87
 
81
88
  const masterKey = options.masterKey ?? env[VAULT_MASTER_KEY_ENV];
@@ -114,6 +121,8 @@ export const builtinVaultDriver: VaultDriver = {
114
121
  opened = await openBuiltinVaultAdapter({
115
122
  env,
116
123
  ...(options.connection !== undefined ? { connection: options.connection } : { url }),
124
+ ...(options.audit === undefined ? {} : { audit: options.audit }),
125
+ ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
117
126
  });
118
127
  if (opened.adapter.getUnsealer() !== null) {
119
128
  for (const entry of await opened.adapter.list()) {
@@ -121,7 +130,10 @@ export const builtinVaultDriver: VaultDriver = {
121
130
  if (secret) map.set(entry.path, secret.value);
122
131
  }
123
132
  }
124
- } catch {
133
+ } catch (err) {
134
+ // A bad audit sink is a config error — do not hide it behind the
135
+ // uninitialized-backend fallback.
136
+ if (isVaultError(err, "MISSING_PEER")) throw err;
125
137
  // Uninitialized / sealed / unreachable: fall back to the seed bag so
126
138
  // boot reports missing contracts instead of a driver stack trace.
127
139
  await opened?.close().catch(() => undefined);
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { VaultError } from "../elements/vault/errors.ts";
11
+ import { requestSignal } from "../kernel/abort-scope.ts";
11
12
  import type { VaultBag } from "./vault-types.ts";
12
13
 
13
14
  /**
@@ -183,7 +184,10 @@ export async function vaultHttpJson(
183
184
  ): Promise<VaultHttpJsonResult> {
184
185
  let response: Response;
185
186
  try {
186
- response = await fetchFn(url, init);
187
+ response = await fetchFn(url, {
188
+ ...init,
189
+ signal: requestSignal(init.signal),
190
+ });
187
191
  } catch (error) {
188
192
  throw asRemoteVaultError(error, message);
189
193
  }
@@ -46,8 +46,13 @@ export interface VaultOpenOptions {
46
46
  readonly region?: string;
47
47
  /** Seed map for `memory` / `managed` drivers. */
48
48
  readonly secrets?: Readonly<Record<string, string>>;
49
- /** HTTP fetch override for remote drivers (tests). */
49
+ /** HTTP fetch override for remote drivers and the webhook audit sink (tests). */
50
50
  readonly fetch?: typeof globalThis.fetch;
51
+ /**
52
+ * Built-in vault audit block (`vault.audit`). Honored by the `vault` driver.
53
+ * Omitted means the SQL hash chain.
54
+ */
55
+ readonly audit?: import("../elements/vault/types.ts").VaultAuditConfig;
51
56
  }
52
57
 
53
58
  /** Resolved secret bag — names → cleartext values. Never logged. */
@@ -519,12 +519,12 @@ function emitTableBlock(table: SchemaTableDecl, dialect: SqlDialect): string {
519
519
  const exportName = exportNameForTable(sqlName);
520
520
  const cols = `{\n${lines.join("\n")}\n}`;
521
521
  const policies = table.policies ?? [];
522
+ const ctor = table.rls === true ? "pgTable.withRLS" : "pgTable";
522
523
  if (policies.length === 0) {
523
- const ctor = table.rls === true ? "pgTable.withRLS" : "pgTable";
524
524
  return `export const ${exportName} = ${ctor}(${JSON.stringify(sqlName)}, ${cols});`;
525
525
  }
526
526
  const extras = policies.map((policy) => ` ${emitPolicyExtra(policy, sqlName)},`).join("\n");
527
- return `export const ${exportName} = pgTable(${JSON.stringify(sqlName)}, ${cols}, (_t) => [\n${extras}\n]);`;
527
+ return `export const ${exportName} = ${ctor}(${JSON.stringify(sqlName)}, ${cols}, (_t) => [\n${extras}\n]);`;
528
528
  }
529
529
 
530
530
  function emitPolicyExtra(policy: SchemaPolicyDecl, table: string): string {
@@ -88,6 +88,7 @@ describe("project-wide store.live default — real boot", () => {
88
88
  });
89
89
  try {
90
90
  await createTestApp(app, {
91
+ capability: "open",
91
92
  boot: {
92
93
  stores: [db],
93
94
  config: { drivers: { store: { sql: { test: "pglite" } } } },
@@ -127,6 +128,7 @@ describe("project-wide store.live default — real boot", () => {
127
128
  });
128
129
  try {
129
130
  await createTestApp(app, {
131
+ capability: "open",
130
132
  boot: {
131
133
  stores: [db],
132
134
  config: { drivers: { store: { sql: { test: "pglite" } } } },
@@ -124,6 +124,7 @@ describe("manual live flow (.live(table) + liveQuery) — real boot", () => {
124
124
  // SQL unless boot.config.drivers.store.sql is set explicitly.
125
125
  try {
126
126
  await createTestApp(app, {
127
+ capability: "open",
127
128
  boot: {
128
129
  stores: [db],
129
130
  config: { drivers: { store: { sql: { test: "pglite" } } } },
@@ -40,7 +40,7 @@ describe("resource-list-docs fixtures — documented behaviors stay real", () =>
40
40
  // (and cleared by the global afterEach registry reset).
41
41
  Object.assign(app.$options, { env: "test", stores: [docsListItemsStore] });
42
42
 
43
- const t = await createTestApp(app);
43
+ const t = await createTestApp(app, { capability: "open" });
44
44
 
45
45
  const bad = await app.fetch(new Request("http://localhost/docs-restricted?secret=eq.x"));
46
46
  expect(bad.status).toBe(422);
@@ -70,7 +70,7 @@ describe("resource-list-docs fixtures — documented behaviors stay real", () =>
70
70
  const app = oke({ name: "docs-admin-test" }).adopt({ docs: exactMounted });
71
71
  Object.assign(app.$options, { env: "test", stores: [docsListItemsStore] });
72
72
 
73
- const t = await createTestApp(app);
73
+ const t = await createTestApp(app, { capability: "open" });
74
74
  const created = await app.fetch(
75
75
  new Request("http://localhost/docs-admin", {
76
76
  method: "POST",
@@ -156,7 +156,7 @@ describe("resource HTTP — exact keys + PII with differing TS/SQL names", () =>
156
156
 
157
157
  test("app.fetch list/get/create/update: only createdAt + masked email, never raw SQL keys", async () => {
158
158
  const app = buildApp();
159
- const t = await createTestApp(app);
159
+ const t = await createTestApp(app, { capability: "open" });
160
160
 
161
161
  // create → 201
162
162
  const created = await app.fetch(
@@ -234,7 +234,7 @@ describe("resource HTTP — exact keys + PII with differing TS/SQL names", () =>
234
234
 
235
235
  test("list cursor meta walks next and previous", async () => {
236
236
  const app = buildApp();
237
- const t = await createTestApp(app);
237
+ const t = await createTestApp(app, { capability: "open" });
238
238
  for (const email of ["a@example.com", "b@example.com", "c@example.com"]) {
239
239
  const created = await app.fetch(
240
240
  new Request("http://localhost/contacts", {
@@ -317,7 +317,7 @@ describe("resource HTTP — exact keys + PII with differing TS/SQL names", () =>
317
317
  config: { drivers: { store: { sql: { test: "pglite" } } } },
318
318
  });
319
319
 
320
- const t = await createTestApp(app);
320
+ const t = await createTestApp(app, { capability: "open" });
321
321
  const created = await app.fetch(
322
322
  new Request("http://localhost/people", {
323
323
  method: "POST",
@@ -289,6 +289,7 @@ describe("store.schema RLS extras", () => {
289
289
  "scope_booking_create_insert",
290
290
  ]);
291
291
  const src = emitDrizzleSource([bookings], "postgres");
292
+ expect(src).toContain("pgTable.withRLS");
292
293
  expect(src).toContain("pgPolicy");
293
294
  expect(src).toContain("oke.gate() = ");
294
295
  expect(src).toContain("oke.user()");
@@ -215,6 +215,8 @@ function compileSqlNode(node: unknown): CompiledNode {
215
215
 
216
216
  // Group level: only structural parens and ` and ` / ` or ` separators may
217
217
  // appear next to nested conditions — anything else is a loud failure.
218
+ // `1.0.0-rc.5-5935859` glues the parens onto the separator (`"(("`,
219
+ // `") and ("`, `"))`).
218
220
  let joiner: "AND" | "OR" | undefined;
219
221
  for (const chunk of chunks) {
220
222
  if (isDrizzleSql(chunk)) continue;
@@ -222,14 +224,13 @@ function compileSqlNode(node: unknown): CompiledNode {
222
224
  if (text === undefined) {
223
225
  throw new TypeError("sql where: unsupported mixed condition");
224
226
  }
225
- const token = text.trim().toLowerCase();
226
- if (token === "" || token === "(" || token === ")") continue;
227
- if (token === "and" || token === "or") {
228
- const next = token.toUpperCase() as "AND" | "OR";
229
- if (joiner !== undefined && joiner !== next) {
227
+ const sep = groupSeparator(text);
228
+ if (sep === "skip") continue;
229
+ if (sep === "AND" || sep === "OR") {
230
+ if (joiner !== undefined && joiner !== sep) {
230
231
  throw new TypeError("sql where: mixed AND/OR at one level — parenthesize explicitly");
231
232
  }
232
- joiner = next;
233
+ joiner = sep;
233
234
  continue;
234
235
  }
235
236
  throw new TypeError(`sql where: unsupported fragment ${JSON.stringify(text)}`);
@@ -329,6 +330,22 @@ function compileLeaf(chunks: readonly unknown[]): CompiledNode {
329
330
  };
330
331
  }
331
332
 
333
+ /**
334
+ * Classify a group-level string chunk.
335
+ *
336
+ * Empty or parenthesis-only text is structural. `and` / `or` may sit inside
337
+ * the parentheses (`") and ("`). Anything else is not a separator.
338
+ *
339
+ * @param text - Raw `StringChunk` text
340
+ */
341
+ function groupSeparator(text: string): "skip" | "AND" | "OR" | undefined {
342
+ const token = text.trim().toLowerCase().replace(/[()]/g, "").trim();
343
+ if (token === "") return "skip";
344
+ if (token === "and") return "AND";
345
+ if (token === "or") return "OR";
346
+ return undefined;
347
+ }
348
+
332
349
  function chunksOf(node: unknown): readonly unknown[] {
333
350
  return (node as { queryChunks: unknown[] }).queryChunks;
334
351
  }
@@ -353,7 +370,7 @@ function isStringChunk(chunk: object): boolean {
353
370
  /**
354
371
  * Joined text of a Drizzle `StringChunk`.
355
372
  *
356
- * drizzle-orm `1.0.0-rc.5-169397b` stores `value` as a `string`. Earlier
373
+ * drizzle-orm `1.0.0-rc.5-5935859` stores `value` as a `string`. Earlier
357
374
  * snapshots (and the synthetic chunks in `resource.ts`) store `string[]`.
358
375
  */
359
376
  function chunkText(chunk: unknown): string | undefined {
@@ -15,6 +15,7 @@ import {
15
15
  } from "../../drivers/pg-rls.ts";
16
16
  import { throwOke } from "../../kernel/errors.ts";
17
17
  import { isFlowFailure } from "../../kernel/hooks.ts";
18
+ import { lazyRequire } from "../../kernel/lazy-require.ts";
18
19
  import { isRetryableSqlError, sqlErrorToFailure } from "./sql-errors.ts";
19
20
  import { AsyncLocalStorage } from "node:async_hooks";
20
21
  import { maskRows, tableFromSql } from "./classify.ts";
@@ -36,9 +37,8 @@ import {
36
37
  resolveTableName,
37
38
  type TableHandle,
38
39
  } from "./table.ts";
39
- import { isSchemaTableDecl } from "./schema-decl.ts";
40
40
  import { SearchConfigError } from "./search-errors.ts";
41
- import { runSqlSearch, type SearchColumnMeta, type SqlSearchOptions } from "./search-runtime.ts";
41
+ import type { SearchColumnMeta, SqlSearchOptions } from "./search-runtime.ts";
42
42
 
43
43
  export type { WhereMap } from "./sql-condition.ts";
44
44
 
@@ -1212,7 +1212,7 @@ export function createSqlStoreHandle(
1212
1212
  await ensureFromMeta(table);
1213
1213
  const columns = searchColumnsFromTable(table);
1214
1214
  const pk = resolvePkColumn(table);
1215
- return runSqlSearch({
1215
+ return loadSearchRuntime().runSqlSearch({
1216
1216
  conn: connection,
1217
1217
  table,
1218
1218
  columns,
@@ -1257,13 +1257,29 @@ export function resolvePkColumn(table: unknown): string {
1257
1257
  return "id";
1258
1258
  }
1259
1259
 
1260
+ /**
1261
+ * Hybrid search (BM25 / LSH / fusion). First `search()` loads it.
1262
+ * A static import pins that graph on every HTTP cold start.
1263
+ */
1264
+ function loadSearchRuntime(): typeof import("./search-runtime.ts") {
1265
+ return lazyRequire(import.meta.dir, ["search", "runtime"].join("-"));
1266
+ }
1267
+
1268
+ /**
1269
+ * Schema-table predicate. Only `search()` needs it.
1270
+ * `schema-decl` is the declare-site module and stays off the HTTP import graph.
1271
+ */
1272
+ function loadSchemaDecl(): typeof import("./schema-decl.ts") {
1273
+ return lazyRequire(import.meta.dir, ["schema", "decl"].join("-"));
1274
+ }
1275
+
1260
1276
  /**
1261
1277
  * Collect `.searchable()` / `.embed()` column meta from a schema table.
1262
1278
  *
1263
1279
  * @param table - Table handle
1264
1280
  */
1265
1281
  function searchColumnsFromTable(table: unknown): SearchColumnMeta[] {
1266
- if (!isSchemaTableDecl(table)) {
1282
+ if (!loadSchemaDecl().isSchemaTableDecl(table)) {
1267
1283
  throw new Error(
1268
1284
  "search(): table must be a store.schema.table() declaration with .searchable() columns",
1269
1285
  );
@@ -65,7 +65,7 @@ describe("fx.store(db).upsert via app.fetch (real boot)", () => {
65
65
  gate: { policies: [gate.public] },
66
66
  }).adopt({ seed });
67
67
  Object.assign(app.$options, { env: "test", stores: [db], unguardedHttp: "allow" });
68
- await createTestApp(app);
68
+ await createTestApp(app, { capability: "open" });
69
69
 
70
70
  async function post(body: unknown) {
71
71
  const res = await app.fetch(