okengine 0.3.6 → 0.4.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.
Files changed (89) hide show
  1. package/AGENTS.md +2 -0
  2. package/package.json +12 -12
  3. package/site/content/docs/ai/skills.mdx +5 -3
  4. package/site/content/docs/elements/ai.mdx +2 -0
  5. package/site/content/docs/elements/channel.mdx +2 -0
  6. package/site/content/docs/elements/clock.mdx +1 -4
  7. package/site/content/docs/elements/flow.mdx +4 -10
  8. package/site/content/docs/elements/gate.mdx +2 -0
  9. package/site/content/docs/elements/signal.mdx +1 -5
  10. package/site/content/docs/elements/store.mdx +27 -6
  11. package/site/content/docs/elements/vault.mdx +10 -11
  12. package/site/content/docs/get-started/basic-usage.mdx +76 -41
  13. package/site/content/docs/get-started/installation.mdx +95 -43
  14. package/site/content/docs/get-started/introduction.mdx +128 -75
  15. package/site/content/docs/get-started/meta.json +1 -1
  16. package/site/content/docs/get-started/why.mdx +141 -0
  17. package/site/content/docs/plugins/ip-allowlist.mdx +1 -2
  18. package/site/content/docs/plugins/security-headers.mdx +1 -1
  19. package/site/content/docs/reference/configuration.mdx +1 -1
  20. package/site/content/docs/reference/environment-variables.mdx +5 -3
  21. package/site/content/docs/reference/fx.mdx +16 -3
  22. package/src/cli/competitor-mention-removal.test.ts +117 -0
  23. package/src/cli/dev.ts +20 -0
  24. package/src/cli/meilisearch-local.test.ts +69 -0
  25. package/src/cli/meilisearch-local.ts +188 -0
  26. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +5 -1
  27. package/src/console/server/store.test.ts +1 -1
  28. package/src/console/server/store.ts +11 -1
  29. package/src/console/ui/dist/assets/index-CjxwRGVv.js +10 -0
  30. package/src/console/ui/dist/assets/panel-access-BGv45snf.js +64 -0
  31. package/src/console/ui/dist/assets/{panel-ai-D_m6WQI8.js → panel-ai-B2S7LEii.js} +1 -1
  32. package/src/console/ui/dist/assets/{panel-architecture-CKnXFyUx.js → panel-architecture-D7UJh91v.js} +1 -1
  33. package/src/console/ui/dist/assets/{panel-channels-DCDd4WAC.js → panel-channels-9T3ybqRu.js} +1 -1
  34. package/src/console/ui/dist/assets/panel-clock-Cb1UXGRQ.js +1 -0
  35. package/src/console/ui/dist/assets/{panel-diff-cdonmH8c.js → panel-diff-DmYbKWmN.js} +1 -1
  36. package/src/console/ui/dist/assets/panel-flows-PiHwT55z.js +48 -0
  37. package/src/console/ui/dist/assets/{panel-gates-B5eTE8XH.js → panel-gates-BQGYXvjT.js} +1 -1
  38. package/src/console/ui/dist/assets/panel-overview-BBnRO18l.js +1 -0
  39. package/src/console/ui/dist/assets/{panel-plugins-Cj7DK1er.js → panel-plugins-D0PsmVw2.js} +1 -1
  40. package/src/console/ui/dist/assets/panel-runs-CWuRDe0r.js +1 -0
  41. package/src/console/ui/dist/assets/{panel-signals-whmDXIg3.js → panel-signals-Bbg4ewpP.js} +1 -1
  42. package/src/console/ui/dist/assets/{panel-store-CEMHLvaw.js → panel-store-CPCbsDRa.js} +1 -1
  43. package/src/console/ui/dist/assets/panel-traces-DVAzuA_S.js +1 -0
  44. package/src/console/ui/dist/assets/{panel-vault-C9wjbki8.js → panel-vault-D1_MvOmo.js} +1 -1
  45. package/src/console/ui/dist/assets/{rolldown-runtime-CNC7AqOf.js → rolldown-runtime-B0Z9INg1.js} +1 -1
  46. package/src/console/ui/dist/index.html +2 -2
  47. package/src/docker/compose.ts +5 -0
  48. package/src/docker/docker.test.ts +41 -0
  49. package/src/docker/recipes/index.ts +10 -2
  50. package/src/docker/recipes/meilisearch.ts +31 -0
  51. package/src/drivers/conformance.test.ts +16 -1
  52. package/src/drivers/conformance.ts +40 -3
  53. package/src/drivers/index.ts +14 -2
  54. package/src/drivers/libsql.ts +4 -4
  55. package/src/drivers/meilisearch.integration.test.ts +77 -0
  56. package/src/drivers/meilisearch.test.ts +181 -0
  57. package/src/drivers/meilisearch.ts +208 -0
  58. package/src/drivers/memory.ts +4 -4
  59. package/src/drivers/pgvector.ts +6 -6
  60. package/src/drivers/types.ts +93 -12
  61. package/src/drivers/vault-driver-removal.test.ts +6 -0
  62. package/src/drivers/vault-types.ts +4 -4
  63. package/src/elements/ai/runtime.ts +6 -0
  64. package/src/elements/ai.test.ts +22 -0
  65. package/src/elements/store/index-boot.test.ts +49 -7
  66. package/src/elements/store/runtime.ts +50 -15
  67. package/src/elements/store.ts +2 -0
  68. package/src/elements/vault.test.ts +27 -4
  69. package/src/elements/vault.ts +1 -1
  70. package/src/index.ts +4 -0
  71. package/src/kernel/boot-bind/store.test.ts +9 -0
  72. package/src/kernel/boot-bind/store.ts +30 -2
  73. package/src/kernel/concurrency.test.ts +58 -0
  74. package/src/kernel/concurrency.ts +48 -0
  75. package/src/kernel/fx.test.ts +11 -2
  76. package/src/kernel/fx.ts +37 -5
  77. package/src/kernel/index.ts +10 -1
  78. package/src/kernel/redacted.ts +74 -0
  79. package/src/kernel/router.ts +3 -3
  80. package/src/test/provisions.integration.test.ts +1 -1
  81. package/site/content/docs/get-started/comparison.mdx +0 -65
  82. package/src/console/ui/dist/assets/index-CrKMmO__.js +0 -10
  83. package/src/console/ui/dist/assets/panel-access-C0J2D-a2.js +0 -64
  84. package/src/console/ui/dist/assets/panel-clock-DjGGFPzr.js +0 -1
  85. package/src/console/ui/dist/assets/panel-flows-DlCU5zjA.js +0 -45
  86. package/src/console/ui/dist/assets/panel-overview-BsFvDdts.js +0 -1
  87. package/src/console/ui/dist/assets/panel-runs-C0gmnoYL.js +0 -1
  88. package/src/console/ui/dist/assets/panel-traces-BDiAuVSK.js +0 -1
  89. package/src/drivers/vault-infisical.ts +0 -57
@@ -44,6 +44,7 @@ See [Store](/docs/elements/store) for the query-builder surface.
44
44
  | `fx.all([...thunks])` | — | Parallel; first rejection aborts siblings |
45
45
  | `fx.race([...thunks])` | — | First settle wins; losers aborted |
46
46
  | `fx.retry(fn, opts?)` | — | Exponential backoff + jitter (plain Promise) |
47
+ | `fx.using(acq, rel, use)` | — | `release` runs once on settle or ambient abort |
47
48
  | `fx.signal` | — | Ambient `AbortSignal` for the current branch |
48
49
  | `fx.fail(code, data, opts?)` | — | Typed failure value (`opts.message` overrides) |
49
50
 
@@ -81,6 +82,18 @@ const charge = await fx.step("charge", () =>
81
82
  `Promise.all`.
82
83
  </Callout>
83
84
 
85
+ `fx.using(acquire, release, use)` scopes a process-local resource to one attempt: `release` runs
86
+ exactly once when `use` settles **or** when the ambient signal aborts (a sibling `fx.race` winner,
87
+ a failing `fx.all` sibling). It is not journaled — do not hold handles across durable park/resume.
88
+
89
+ ```typescript
90
+ const rows = await fx.using(
91
+ () => pool.acquire(),
92
+ (conn) => conn.release(),
93
+ (conn) => conn.query("select …"),
94
+ );
95
+ ```
96
+
84
97
  **Consequence:** put `fx.retry` inside `fx.step` on durable flows so a completed charge never re-runs on resume. Coarse whole-body retry is `flow({ retry: { … } })` on the same journal session.
85
98
 
86
99
  ## Channel
@@ -102,9 +115,9 @@ AI calls are nondeterministic: journaling is forced on and auto-cache disabled a
102
115
 
103
116
  ## Vault
104
117
 
105
- | Signature | Records | Returns / notes |
106
- | -------------------- | ------- | ----------------------------------------------------------------------- |
107
- | `fx.vault(contract)` | `read` | Cleartext, inside this flow only registered with the redactor at boot |
118
+ | Signature | Records | Returns / notes |
119
+ | -------------------- | ------- | ----------------------------------------------------------------------------------------- |
120
+ | `fx.vault(contract)` | `read` | `Redacted<string>` prints/logs as a placeholder; `.reveal()` at the credential boundary |
108
121
 
109
122
  ## Clock
110
123
 
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Gate: named competitor comparisons stay gone from the tracked tree.
3
+ *
4
+ * Mirrors {@link ./doc-staleness.test.ts} — `git grep` must find zero hits
5
+ * (the security-headers decoy header fixture may still use a common
6
+ * `X-Powered-By` value; that path is allow-listed).
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import { join } from "node:path";
11
+
12
+ const ROOT = join(import.meta.dir, "../..");
13
+
14
+ /** Forbidden peer name — split so this file does not match itself. */
15
+ const FORBIDDEN_HONO = ["Ho", "no"].join("");
16
+ /** Forbidden peer name. */
17
+ const FORBIDDEN_ELYSIA = ["Ely", "sia"].join("");
18
+ /** Forbidden peer name (covers Enc + ore.ts). */
19
+ const FORBIDDEN_ENCORE = ["Enc", "ore"].join("");
20
+ /** Forbidden peer name. */
21
+ const FORBIDDEN_NESTJS = ["Nest", "JS"].join("");
22
+ /** Forbidden peer name. */
23
+ const FORBIDDEN_FASTIFY = ["Fast", "ify"].join("");
24
+ /** Forbidden peer site — avoid bare `iii` (matches lockfile hashes). */
25
+ const FORBIDDEN_III_DEV = ["iii", ".", "dev"].join("");
26
+ /** Deleted Comparison page path residue. */
27
+ const FORBIDDEN_COMPARISON_PATH = ["get-started/", "comparison"].join("");
28
+ /** Word-boundary classic Node framework — `-F` false-positives on `CallExpression`. */
29
+ const FORBIDDEN_EXPRESS = ["\\b", "Ex", "press", "\\b"].join("");
30
+
31
+ /** Decoy `X-Powered-By` fixture value in the security-headers plugin tests. */
32
+ const EXPRESS_ALLOW = ["src/plugins/security-headers.test.ts:"];
33
+
34
+ /**
35
+ * Run `git grep -F` and assert zero matches after ignoring allow-listed paths.
36
+ *
37
+ * @param pattern - Fixed-string pattern
38
+ * @param ignorePrefixes - Path prefixes to ignore (`path:line:` hits)
39
+ */
40
+ function assertZeroGitGrepFixed(pattern: string, ignorePrefixes: readonly string[] = []): void {
41
+ const proc = Bun.spawnSync(["git", "grep", "-F", "-n", "-e", pattern, "--"], {
42
+ cwd: ROOT,
43
+ stdout: "pipe",
44
+ stderr: "pipe",
45
+ });
46
+ // git grep exits 1 when there are no matches.
47
+ if (proc.exitCode === 1) {
48
+ expect(proc.stdout.toString()).toBe("");
49
+ return;
50
+ }
51
+ const hits = proc.stdout
52
+ .toString()
53
+ .trim()
54
+ .split("\n")
55
+ .filter((line) => line.length > 0)
56
+ .filter((line) => !ignorePrefixes.some((prefix) => line.startsWith(prefix)));
57
+ expect(hits.join("\n")).toBe("");
58
+ }
59
+
60
+ /**
61
+ * Run `git grep` with a basic regex and assert zero matches after allow-list.
62
+ *
63
+ * @param pattern - Basic regex (no `-F`)
64
+ * @param ignorePrefixes - Path prefixes to ignore (`path:line:` hits)
65
+ */
66
+ function assertZeroGitGrepRegex(pattern: string, ignorePrefixes: readonly string[] = []): void {
67
+ const proc = Bun.spawnSync(["git", "grep", "-n", "-e", pattern, "--"], {
68
+ cwd: ROOT,
69
+ stdout: "pipe",
70
+ stderr: "pipe",
71
+ });
72
+ if (proc.exitCode === 1) {
73
+ expect(proc.stdout.toString()).toBe("");
74
+ return;
75
+ }
76
+ const hits = proc.stdout
77
+ .toString()
78
+ .trim()
79
+ .split("\n")
80
+ .filter((line) => line.length > 0)
81
+ .filter((line) => !ignorePrefixes.some((prefix) => line.startsWith(prefix)));
82
+ expect(hits.join("\n")).toBe("");
83
+ }
84
+
85
+ describe("named competitor mention removal gate", () => {
86
+ test("tracked tree has zero mentions of the thin multi-runtime router peer", () => {
87
+ assertZeroGitGrepFixed(FORBIDDEN_HONO);
88
+ });
89
+
90
+ test("tracked tree has zero mentions of the Bun-first peer framework", () => {
91
+ assertZeroGitGrepFixed(FORBIDDEN_ELYSIA);
92
+ });
93
+
94
+ test("tracked tree has zero mentions of the Rust-core peer platform", () => {
95
+ assertZeroGitGrepFixed(FORBIDDEN_ENCORE);
96
+ });
97
+
98
+ test("tracked tree has zero mentions of the decorator DI peer", () => {
99
+ assertZeroGitGrepFixed(FORBIDDEN_NESTJS);
100
+ });
101
+
102
+ test("tracked tree has zero mentions of the Node HTTP peer", () => {
103
+ assertZeroGitGrepFixed(FORBIDDEN_FASTIFY);
104
+ });
105
+
106
+ test("tracked tree has zero mentions of the polyglot peer site host", () => {
107
+ assertZeroGitGrepFixed(FORBIDDEN_III_DEV);
108
+ });
109
+
110
+ test("tracked tree has zero deleted Comparison path residue", () => {
111
+ assertZeroGitGrepFixed(FORBIDDEN_COMPARISON_PATH);
112
+ });
113
+
114
+ test("tracked tree has zero classic Node framework peer mentions outside decoy fixture", () => {
115
+ assertZeroGitGrepRegex(FORBIDDEN_EXPRESS, EXPRESS_ALLOW);
116
+ });
117
+ });
package/src/cli/dev.ts CHANGED
@@ -339,6 +339,24 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
339
339
  loadedConfig = null;
340
340
  }
341
341
 
342
+ // Local mode + a configured `meilisearch` store.index: bring the server up
343
+ // as a fourth local process (binary on PATH — a documented prerequisite,
344
+ // like Docker for --docker). Docker mode instead relies on the recipe.
345
+ let meili: import("./meilisearch-local.ts").LocalMeilisearchHandle | null = null;
346
+ if (mode === "local") {
347
+ const indexId = resolveDriverId(loadedConfig?.drivers?.store?.index, "local") ?? "memory";
348
+ if (indexId === "meilisearch") {
349
+ try {
350
+ const { startLocalMeilisearch } = await import("./meilisearch-local.ts");
351
+ meili = await startLocalMeilisearch({ cwd });
352
+ write(formatStatusLine(`meilisearch local server on ${meili.url}`));
353
+ } catch (err) {
354
+ console.error(err instanceof Error ? err.message : String(err));
355
+ return { code: 1 };
356
+ }
357
+ }
358
+ }
359
+
342
360
  if (mode === "docker") {
343
361
  let images = options.images;
344
362
  if (!images) {
@@ -567,6 +585,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
567
585
  OKE_KV_DRIVER: stackKvDriver,
568
586
  }
569
587
  : {}),
588
+ ...(meili?.overlay ?? {}),
570
589
  };
571
590
  process.env.OKE_DEV_REQUEST_LOG = "1";
572
591
 
@@ -755,6 +774,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
755
774
  stopped = true;
756
775
  autoPushRunner.cancel();
757
776
  watcher.close();
777
+ meili?.stop();
758
778
  app.stop();
759
779
  consoleServer.stop();
760
780
  mcpServer?.stop();
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Local Meilisearch lifecycle — persisted master key material and fail-loud
3
+ * binary resolution. Never spawns a real server here.
4
+ */
5
+
6
+ import { afterEach, describe, expect, test } from "bun:test";
7
+ import { mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import {
11
+ ensureMasterKey,
12
+ MEILISEARCH_STATE_DIR_REL,
13
+ MeilisearchLocalError,
14
+ startLocalMeilisearch,
15
+ } from "./meilisearch-local.ts";
16
+
17
+ const dirs: string[] = [];
18
+
19
+ function tempStateDir(): string {
20
+ const dir = mkdtempSync(join(tmpdir(), "oke-meili-"));
21
+ dirs.push(dir);
22
+ return dir;
23
+ }
24
+
25
+ afterEach(() => {
26
+ while (dirs.length > 0) {
27
+ const dir = dirs.pop();
28
+ if (dir) rmSync(dir, { recursive: true, force: true });
29
+ }
30
+ });
31
+
32
+ describe("ensureMasterKey", () => {
33
+ test("generates once, persists 0600, reuses on next call", () => {
34
+ const stateDir = tempStateDir();
35
+ const first = ensureMasterKey(stateDir);
36
+ const second = ensureMasterKey(stateDir);
37
+ expect(first).toBe(second);
38
+ expect(first.length).toBeGreaterThanOrEqual(32);
39
+ const keyPath = join(stateDir, "master.key");
40
+ expect(statSync(keyPath).mode & 0o777).toBe(0o600);
41
+ expect(statSync(stateDir).mode & 0o777).toBe(0o700);
42
+ expect(readFileSync(keyPath, "utf8").trim()).toBe(first);
43
+ // No leftover temp files.
44
+ expect(readdirSync(stateDir).filter((f) => f.includes(".tmp-"))).toHaveLength(0);
45
+ });
46
+
47
+ test("a too-short persisted key fails loud", () => {
48
+ const stateDir = tempStateDir();
49
+ const keyPath = join(stateDir, "master.key");
50
+ // Write a weak key directly (bypasses the writer) to simulate corruption.
51
+ writeFileSync(keyPath, "short\n");
52
+ expect(() => ensureMasterKey(stateDir)).toThrow(MeilisearchLocalError);
53
+ });
54
+ });
55
+
56
+ describe("startLocalMeilisearch", () => {
57
+ test("missing binary fails loud with an install hint (never silent memory)", async () => {
58
+ await expect(
59
+ startLocalMeilisearch({
60
+ binary: "meilisearch-definitely-not-on-path-xyz",
61
+ stateDir: tempStateDir(),
62
+ }),
63
+ ).rejects.toThrow(/not found on PATH|never auto-downloads/i);
64
+ });
65
+
66
+ test("state dir rel lives under .oke", () => {
67
+ expect(MEILISEARCH_STATE_DIR_REL).toBe(join(".oke", "meilisearch"));
68
+ });
69
+ });
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Local Meilisearch lifecycle for `oke dev` (local mode).
3
+ *
4
+ * A Meilisearch server is a fourth moving part only when the app actually
5
+ * opts into the `meilisearch` store.index driver. This module:
6
+ *
7
+ * - generates the master key once and persists it under `.oke/meilisearch/`
8
+ * (`0700` dir / `0600` file, atomic write — same material discipline as
9
+ * OpenBao, minus the init/unseal ceremony: a single static key),
10
+ * - resolves the `meilisearch` binary from `PATH` (a documented prerequisite,
11
+ * like Docker for `--docker`; we never auto-download binaries),
12
+ * - spawns it via `Bun.spawn` with its own data dir + port, polls `/health`,
13
+ * and hands back an env overlay (`OKE_STORE_INDEX_URL` / `…_KEY`) plus a
14
+ * `stop()` that kills the child alongside the app.
15
+ *
16
+ * Fail-loud: missing binary / unhealthy server / never-ready all throw
17
+ * {@link MeilisearchLocalError} — never a silent memory fallback.
18
+ */
19
+
20
+ import { randomBytes } from "node:crypto";
21
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
22
+ import { dirname, join, resolve } from "node:path";
23
+
24
+ /** Material directory relative to the project root. */
25
+ export const MEILISEARCH_STATE_DIR_REL = join(".oke", "meilisearch");
26
+ /** Default loopback port for the local server. */
27
+ export const MEILISEARCH_LOCAL_PORT = 7700;
28
+
29
+ /** Fail-loud local Meilisearch error (missing binary / never healthy). */
30
+ export class MeilisearchLocalError extends Error {
31
+ constructor(message: string) {
32
+ super(message);
33
+ this.name = "MeilisearchLocalError";
34
+ }
35
+ }
36
+
37
+ /** Options for {@link startLocalMeilisearch}. */
38
+ export interface LocalMeilisearchOptions {
39
+ readonly cwd?: string;
40
+ /** Loopback port (default {@link MEILISEARCH_LOCAL_PORT}). */
41
+ readonly port?: number;
42
+ /** Override state dir (tests). */
43
+ readonly stateDir?: string;
44
+ /** Binary name/path resolved via `Bun.which` (default `meilisearch`). */
45
+ readonly binary?: string;
46
+ /** Max ms to wait for `/health` to report available (default 30_000). */
47
+ readonly readyTimeoutMs?: number;
48
+ readonly fetch?: typeof globalThis.fetch;
49
+ }
50
+
51
+ /** A running local Meilisearch child. */
52
+ export interface LocalMeilisearchHandle {
53
+ /** Base URL the app should talk to. */
54
+ readonly url: string;
55
+ /** Master key (also persisted on disk). */
56
+ readonly masterKey: string;
57
+ /** Env overlay for the app child. */
58
+ readonly overlay: Record<string, string>;
59
+ /** Kill the child (idempotent). */
60
+ readonly stop: () => void;
61
+ }
62
+
63
+ /** Env overlay for the app child. */
64
+ export function meilisearchStackEnv(handle: LocalMeilisearchHandle): Record<string, string> {
65
+ return handle.overlay;
66
+ }
67
+
68
+ function writeMaterial(path: string, value: string): void {
69
+ mkdirSync(dirname(path), { recursive: true });
70
+ chmodSync(dirname(path), 0o700);
71
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
72
+ writeFileSync(tmp, `${value}\n`, { mode: 0o600 });
73
+ renameSync(tmp, path);
74
+ chmodSync(path, 0o600);
75
+ const back = readFileSync(path, "utf8").trim();
76
+ if (back !== value) {
77
+ throw new MeilisearchLocalError(`meilisearch local: verify failed reading ${path}`);
78
+ }
79
+ }
80
+
81
+ /** Read the persisted master key, or generate + persist a fresh one. */
82
+ export function ensureMasterKey(stateDir: string): string {
83
+ const keyPath = join(stateDir, "master.key");
84
+ if (existsSync(keyPath)) {
85
+ const key = readFileSync(keyPath, "utf8").trim();
86
+ if (key.length >= 16) return key;
87
+ throw new MeilisearchLocalError(
88
+ `meilisearch local: ${keyPath} exists but is too short — delete it to regenerate`,
89
+ );
90
+ }
91
+ const key = randomBytes(32).toString("hex");
92
+ writeMaterial(keyPath, key);
93
+ return key;
94
+ }
95
+
96
+ /**
97
+ * Start a local Meilisearch server and wait for `/health`.
98
+ *
99
+ * @param options - cwd / port / binary / injected fetch
100
+ */
101
+ export async function startLocalMeilisearch(
102
+ options: LocalMeilisearchOptions = {},
103
+ ): Promise<LocalMeilisearchHandle> {
104
+ const cwd = options.cwd ?? process.cwd();
105
+ const port = options.port ?? MEILISEARCH_LOCAL_PORT;
106
+ const stateDir = options.stateDir ?? resolve(cwd, MEILISEARCH_STATE_DIR_REL);
107
+ const dataDir = join(stateDir, "data");
108
+ const fetchFn = options.fetch ?? globalThis.fetch;
109
+ const readyTimeoutMs = options.readyTimeoutMs ?? 30_000;
110
+
111
+ const binary = options.binary ?? "meilisearch";
112
+ const resolved = Bun.which(binary);
113
+ if (!resolved) {
114
+ throw new MeilisearchLocalError(
115
+ "meilisearch local: `meilisearch` not found on PATH — install it to use the store.index FTS driver " +
116
+ "(brew install meilisearch · curl -L https://install.meilisearch.com | sh), " +
117
+ "or run `oke dev --docker` instead. OKE never auto-downloads binaries.",
118
+ );
119
+ }
120
+
121
+ const masterKey = ensureMasterKey(stateDir);
122
+ mkdirSync(dataDir, { recursive: true });
123
+
124
+ const url = `http://127.0.0.1:${port}`;
125
+ const proc = Bun.spawn(
126
+ [
127
+ resolved,
128
+ "--http-addr",
129
+ `127.0.0.1:${port}`,
130
+ "--master-key",
131
+ masterKey,
132
+ "--db-path",
133
+ dataDir,
134
+ "--env",
135
+ "development",
136
+ "--no-analytics",
137
+ ],
138
+ {
139
+ cwd,
140
+ stdout: "ignore",
141
+ stderr: "inherit",
142
+ env: { ...(process.env as Record<string, string>) },
143
+ },
144
+ );
145
+
146
+ let stopped = false;
147
+ const stop = () => {
148
+ if (stopped) return;
149
+ stopped = true;
150
+ try {
151
+ proc.kill("SIGKILL");
152
+ } catch {
153
+ // Already gone.
154
+ }
155
+ };
156
+
157
+ const deadline = Date.now() + readyTimeoutMs;
158
+ for (;;) {
159
+ if (proc.exitCode !== null) {
160
+ throw new MeilisearchLocalError(
161
+ `meilisearch local: server exited (${proc.exitCode}) before becoming healthy`,
162
+ );
163
+ }
164
+ try {
165
+ const res = await fetchFn(`${url}/health`);
166
+ if (res.ok) break;
167
+ } catch {
168
+ // Not up yet.
169
+ }
170
+ if (Date.now() > deadline) {
171
+ stop();
172
+ throw new MeilisearchLocalError(
173
+ `meilisearch local: no /health from ${url} within ${readyTimeoutMs}ms`,
174
+ );
175
+ }
176
+ await new Promise((resolve) => setTimeout(resolve, 25));
177
+ }
178
+
179
+ return {
180
+ url,
181
+ masterKey,
182
+ overlay: {
183
+ OKE_STORE_INDEX_URL: url,
184
+ OKE_STORE_INDEX_KEY: masterKey,
185
+ },
186
+ stop,
187
+ };
188
+ }
@@ -5,7 +5,11 @@ export const chargeBooking = flow({
5
5
  name: "payments.chargeBooking",
6
6
  durable: true,
7
7
  do: async ({ orderId }, fx) => {
8
- const intent = await fx.step("create-intent", () => fx.vault(stripeKey));
8
+ // Journal only serializable step values a Redacted is not journaled.
9
+ const intent = await fx.step("create-intent", async () => ({
10
+ id: `pi_${orderId}`,
11
+ key: fx.vault(stripeKey).reveal(),
12
+ }));
9
13
  await fx.clock.sleep("verify-window", "2m");
10
14
  return fx.step("confirm", () => intent);
11
15
  },
@@ -137,7 +137,7 @@ describe("index driver resolution", () => {
137
137
  expect(decl).toBeDefined();
138
138
  const handle = (await runtime.open(decl!, {
139
139
  effects: { reads: ["index:docs"], writes: ["index:docs"] },
140
- })) as import("../../elements/store.ts").IndexStoreFxHandle;
140
+ })) as import("../../elements/store.ts").VectorIndexStoreFxHandle;
141
141
  expect(handle.driverId).toBe("memory");
142
142
  await handle.upsert("d1", [1, 0, 0], { t: 1 });
143
143
  const hits = await handle.search([1, 0, 0], 1);
@@ -329,6 +329,7 @@ export interface StoreQueryInput {
329
329
  readonly prefix?: string;
330
330
  readonly limit?: number;
331
331
  readonly vector?: readonly number[];
332
+ readonly q?: string;
332
333
  readonly topK?: number;
333
334
  readonly revealPii?: boolean;
334
335
  }
@@ -350,6 +351,7 @@ export interface StoreQueryResult {
350
351
  readonly score: number;
351
352
  readonly meta?: Record<string, unknown>;
352
353
  }>;
354
+ readonly facetDistribution?: Record<string, Record<string, number>>;
353
355
  readonly masked: boolean;
354
356
  readonly routedRole?: "primary" | "replica";
355
357
  }
@@ -438,12 +440,20 @@ export async function queryStore(
438
440
 
439
441
  // index
440
442
  const idx = handle as IndexStoreFxHandle;
443
+ void manifest;
444
+ if (idx.driverId === "meilisearch") {
445
+ const q = input.q ?? "";
446
+ if (q.trim().length === 0) {
447
+ return { facet, hits: [], masked: false };
448
+ }
449
+ const result = await idx.search(q, { topK: input.topK ?? 5 });
450
+ return { facet, hits: result.hits, facetDistribution: result.facetDistribution, masked: false };
451
+ }
441
452
  const vector = input.vector ?? [];
442
453
  if (vector.length === 0) {
443
454
  return { facet, hits: [], masked: false };
444
455
  }
445
456
  const hits = await idx.search(vector, input.topK ?? 5);
446
- void manifest;
447
457
  return { facet, hits, masked: false };
448
458
  }
449
459