create-oke 0.18.4 → 0.19.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 (62) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/agents-md.ts +6 -4
  4. package/src/ai-setup/apply.test.ts +278 -0
  5. package/src/ai-setup/apply.ts +430 -52
  6. package/src/ai-setup/catalog.ts +250 -1343
  7. package/src/ai-setup/from-pref.ts +3 -71
  8. package/src/ai-setup/prompts.ts +60 -443
  9. package/src/cli.test.ts +21 -15
  10. package/src/cli.ts +20 -11
  11. package/src/create-defaults.test.ts +4 -4
  12. package/src/create-defaults.ts +3 -3
  13. package/src/customize-flow.test.ts +9 -15
  14. package/src/customize-flow.ts +59 -66
  15. package/src/drivers-catalog.ts +18 -23
  16. package/src/transform.test.ts +69 -10
  17. package/src/transform.ts +10 -35
  18. package/templates/advanced/.env.example +28 -11
  19. package/templates/advanced/.github/workflows/ci.yml +1 -1
  20. package/templates/advanced/oke.config.ts +3 -4
  21. package/templates/advanced/package.json +11 -10
  22. package/templates/advanced/src/app.ts +36 -1
  23. package/templates/advanced/src/core.ts +12 -11
  24. package/templates/advanced/src/db/schema.decl.ts +6 -2
  25. package/templates/advanced/src/db/seed/index.ts +4 -4
  26. package/templates/advanced/src/flows/main/route.ts +3 -2
  27. package/templates/advanced/src/flows/notes/[id]/archive.ts +5 -8
  28. package/templates/advanced/src/flows/notes/[id]/attach.ts +1 -4
  29. package/templates/advanced/src/flows/notes/[id]/get.ts +4 -7
  30. package/templates/advanced/src/flows/notes/[id]/summarize.ts +3 -4
  31. package/templates/advanced/src/flows/notes/create.ts +4 -6
  32. package/templates/advanced/src/flows/notes/digest.ts +6 -5
  33. package/templates/advanced/src/flows/notes/list.ts +4 -5
  34. package/templates/advanced/src/flows/notes/shapes.ts +13 -3
  35. package/templates/advanced/src/flows/notes/signals.ts +1 -2
  36. package/templates/advanced/src/vault.ts +138 -0
  37. package/templates/advanced/tests/advanced.test.ts +11 -8
  38. package/templates/advanced/web/src/App.css +7 -0
  39. package/templates/advanced/web/src/App.tsx +101 -1
  40. package/templates/advanced/web/src/client.ts +16 -3
  41. package/templates/advanced/web/vite.config.ts +1 -0
  42. package/templates/standard/.env.example +22 -11
  43. package/templates/standard/.github/workflows/ci.yml +1 -1
  44. package/templates/standard/oke.config.ts +3 -3
  45. package/templates/standard/package.json +11 -10
  46. package/templates/standard/src/app.ts +6 -1
  47. package/templates/standard/src/core.ts +10 -11
  48. package/templates/standard/src/db/schema.decl.ts +6 -2
  49. package/templates/standard/src/db/seed/index.ts +3 -3
  50. package/templates/standard/src/flows/main/route.ts +3 -2
  51. package/templates/standard/src/flows/notes/[id]/archive.ts +5 -8
  52. package/templates/standard/src/flows/notes/[id]/get.ts +4 -7
  53. package/templates/standard/src/flows/notes/create.ts +4 -6
  54. package/templates/standard/src/flows/notes/list.ts +4 -5
  55. package/templates/standard/src/flows/notes/shapes.ts +12 -2
  56. package/templates/standard/src/flows/notes/signals.ts +1 -2
  57. package/templates/standard/src/vault.ts +123 -0
  58. package/templates/standard/tests/standard.test.ts +3 -1
  59. package/templates/standard/web/src/client.ts +2 -2
  60. package/templates/standard/web/vite.config.ts +1 -0
  61. package/src/ai-setup/detect-ollama.ts +0 -228
  62. package/src/ai-setup/recommend.ts +0 -220
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Notes vault contracts — secrets and cleartext config.
3
+ *
4
+ * Values resolve through the driver chain (built-in store → process.env →
5
+ * `.env.local` → `dev:` / `vault.fromDocker`). Declare every stack / app name
6
+ * here so Console Vault lists it; put values in `.env.local`, `oke vault set`,
7
+ * or leave the local fallback for Docker-first `oke dev`.
8
+ */
9
+
10
+ import { vault } from "okengine";
11
+
12
+ // --- Secrets (fingerprinted) -------------------------------------------------
13
+
14
+ /** HMAC secret for outbound note webhooks (`fx.vault.get` on create). */
15
+ export const webhookSecret = vault.secret("APP_WEBHOOK_SECRET", {
16
+ description: "HMAC secret for outbound note webhooks",
17
+ rotate: "never",
18
+ dev: "dev-webhook-secret-change-me",
19
+ });
20
+
21
+ /** Console operator secret. */
22
+ export const okeConsoleSecret = vault.secret("OKE_CONSOLE_SECRET", {
23
+ description: "Console operator secret",
24
+ rotate: "90d",
25
+ dev: "oke-dev-notes-console",
26
+ });
27
+
28
+ /** Mailpit SMTP URL. */
29
+ export const channelEmailUrl = vault.secret("OKE_CHANNEL_EMAIL_URL", {
30
+ description: "Mailpit SMTP URL",
31
+ rotate: "never",
32
+ dev: vault.fromDocker("channel.email"),
33
+ });
34
+
35
+ /** SMTP alias. */
36
+ export const smtpUrl = vault.secret("SMTP_URL", {
37
+ description: "SMTP URL",
38
+ rotate: "never",
39
+ dev: vault.fromDocker("channel.email"),
40
+ });
41
+
42
+ /** Object storage URL. */
43
+ export const storeFilesUrl = vault.secret("OKE_STORE_FILES_URL", {
44
+ description: "Object storage URL",
45
+ rotate: "never",
46
+ dev: vault.fromDocker("store.files"),
47
+ });
48
+
49
+ /** Redis URL. */
50
+ export const storeKvUrl = vault.secret("OKE_STORE_KV_URL", {
51
+ description: "Redis URL",
52
+ rotate: "never",
53
+ dev: vault.fromDocker("store.kv"),
54
+ });
55
+
56
+ /** Redis alias. */
57
+ export const redisUrl = vault.secret("REDIS_URL", {
58
+ description: "Redis URL",
59
+ rotate: "never",
60
+ dev: vault.fromDocker("store.kv"),
61
+ });
62
+
63
+ /** Direct Postgres URL. */
64
+ export const storeSqlUrl = vault.secret("OKE_STORE_SQL_URL", {
65
+ description: "Direct Postgres URL",
66
+ rotate: "never",
67
+ dev: vault.fromDocker("store.sql"),
68
+ });
69
+
70
+ /** Postgres URL (compose / PgDog may rewrite this). */
71
+ export const databaseUrl = vault.secret("DATABASE_URL", {
72
+ description: "Postgres URL",
73
+ rotate: "never",
74
+ dev: vault.fromDocker("store.sql"),
75
+ });
76
+
77
+ /** Meilisearch master key (when `store.index` is meilisearch). */
78
+ export const meiliMasterKey = vault.secret("MEILI_MASTER_KEY", {
79
+ description: "Meilisearch master key",
80
+ rotate: "90d",
81
+ dev: "dev-notes-meili",
82
+ });
83
+
84
+ // --- Config (shown in the clear) ---------------------------------------------
85
+
86
+ /** App listen origin. */
87
+ export const okeAppUrl = vault.config("OKE_APP_URL", {
88
+ description: "App listen origin",
89
+ dev: "http://127.0.0.1:6530",
90
+ });
91
+
92
+ /** Public API origin (Vite web keeps `VITE_API_URL` empty for same-origin proxy). */
93
+ export const publicApiUrl = vault.config("PUBLIC_API_URL", {
94
+ description: "Public API origin",
95
+ dev: "http://127.0.0.1:6530",
96
+ });
97
+
98
+ /** Mailpit UI origin. */
99
+ export const mailpitUiUrl = vault.config("MAILPIT_UI_URL", {
100
+ description: "Mailpit UI origin",
101
+ dev: "http://127.0.0.1:8025",
102
+ });
103
+
104
+ /** Meilisearch origin. */
105
+ export const meiliUrl = vault.config("MEILI_URL", {
106
+ description: "Meilisearch origin",
107
+ dev: "http://127.0.0.1:7700",
108
+ });
109
+
110
+ /** Maintenance flag (`1` / `0`). */
111
+ export const maintenanceMode = vault.config("MAINTENANCE_MODE", {
112
+ description: "Maintenance mode flag",
113
+ dev: "0",
114
+ });
115
+
116
+ /**
117
+ * Full contract list for `oke({ secrets })`.
118
+ *
119
+ * `vault.secret` auto-registers; `vault.config` does not — pass this array so
120
+ * configs resolve in boot / Console the same way secrets do.
121
+ */
122
+ export const NOTES_VAULT = [
123
+ webhookSecret,
124
+ okeConsoleSecret,
125
+ channelEmailUrl,
126
+ smtpUrl,
127
+ storeFilesUrl,
128
+ storeKvUrl,
129
+ redisUrl,
130
+ storeSqlUrl,
131
+ databaseUrl,
132
+ meiliMasterKey,
133
+ okeAppUrl,
134
+ publicApiUrl,
135
+ mailpitUiUrl,
136
+ meiliUrl,
137
+ maintenanceMode,
138
+ ] as const;
@@ -5,7 +5,15 @@ import { app, type App } from "@/app";
5
5
  let t: TestApp<App>;
6
6
 
7
7
  beforeAll(async () => {
8
- t = await createTestApp(app);
8
+ t = await createTestApp(app, {
9
+ boot: {
10
+ config: {
11
+ drivers: {
12
+ store: { sql: { test: "pglite" } },
13
+ },
14
+ },
15
+ },
16
+ });
9
17
  });
10
18
 
11
19
  afterAll(async () => {
@@ -18,7 +26,7 @@ test("boots — health flow is named main.health", async () => {
18
26
  expect(data).toEqual({ ok: true });
19
27
  });
20
28
 
21
- test("notes create → attach → summarize → archive", async () => {
29
+ test("notes create → attach → archive", async () => {
22
30
  const created = await t.api.notes!.create!({
23
31
  title: "Advanced",
24
32
  body: "Body long enough to exercise attach and summarize paths in the advanced starter.",
@@ -32,12 +40,7 @@ test("notes create → attach → summarize → archive", async () => {
32
40
  expect(attached.error).toBeNull();
33
41
  expect((attached.data as { key: string }).key).toBe(`notes/${id}/attachment.txt`);
34
42
 
35
- t.ai.mock("summarize-note", { summary: "Advanced starter summary." });
36
- const summary = await t.api.notes!.summarize!({ id });
37
- expect(summary.error).toBeNull();
38
- const out = summary.data as { via: string; summary: string };
39
- expect(out.via.length).toBeGreaterThan(0);
40
- expect(out.summary.length).toBeGreaterThan(0);
43
+ // `notes.summarize` needs `oke ai setup` (summarize-note prompt) exercised there.
41
44
 
42
45
  const archived = await t.api.notes!.archive!({ id });
43
46
  expect(archived.error).toBeNull();
@@ -57,6 +57,13 @@
57
57
  color: #b91c1c;
58
58
  }
59
59
 
60
+ .hint,
61
+ .session {
62
+ margin: 0.5rem 0;
63
+ font-size: 0.875rem;
64
+ color: #64748b;
65
+ }
66
+
60
67
  .list {
61
68
  list-style: none;
62
69
  padding: 0;
@@ -1,10 +1,12 @@
1
1
  import { useCallback, useEffect, useState, type FormEvent, type JSX } from "react";
2
2
  import { isOk } from "okengine/client";
3
+ import { Can } from "okengine/client-react";
3
4
  import { api, type Note } from "./client.ts";
4
5
  import "./App.css";
5
6
 
6
7
  /**
7
8
  * Notes SPA — health + list + create + archive against the oke app.
9
+ * Auth chrome uses Can (UI-only); Gate on Flows remains real authz.
8
10
  */
9
11
  export function App(): JSX.Element {
10
12
  const [health, setHealth] = useState<"unknown" | "ok" | "down">("unknown");
@@ -12,6 +14,18 @@ export function App(): JSX.Element {
12
14
  const [title, setTitle] = useState("");
13
15
  const [body, setBody] = useState("");
14
16
  const [message, setMessage] = useState<string | null>(null);
17
+ const [email, setEmail] = useState("demo@localhost");
18
+ const [password, setPassword] = useState("password-demo-1");
19
+ const [sessionLabel, setSessionLabel] = useState<string>("…");
20
+
21
+ const refreshSession = useCallback(async () => {
22
+ if (!api.auth) {
23
+ setSessionLabel("no auth");
24
+ return;
25
+ }
26
+ const user = await api.auth.getSession();
27
+ setSessionLabel(user ? `${user.email ?? user.userId}` : "signed out");
28
+ }, []);
15
29
 
16
30
  const refresh = useCallback(async () => {
17
31
  const live = await api.main.health({});
@@ -27,7 +41,48 @@ export function App(): JSX.Element {
27
41
 
28
42
  useEffect(() => {
29
43
  void refresh();
30
- }, [refresh]);
44
+ void refreshSession();
45
+ }, [refresh, refreshSession]);
46
+
47
+ async function onSignIn(event: FormEvent<HTMLFormElement>): Promise<void> {
48
+ event.preventDefault();
49
+ if (!api.auth) return;
50
+ const result = await api.auth.signIn.email({ email, password });
51
+ if (!result.ok) {
52
+ setMessage("sign-in failed — try sign-up first");
53
+ return;
54
+ }
55
+ setMessage(null);
56
+ await refreshSession();
57
+ }
58
+
59
+ async function onSignUp(): Promise<void> {
60
+ if (!api.auth) return;
61
+ const result = await api.auth.signUp.email({ email, password, name: "Demo" });
62
+ if (!result.ok) {
63
+ setMessage("sign-up failed");
64
+ return;
65
+ }
66
+ setMessage(null);
67
+ await refreshSession();
68
+ }
69
+
70
+ async function onPasskey(): Promise<void> {
71
+ if (!api.auth) return;
72
+ const result = await api.auth.signIn.passkey({ email });
73
+ if (!result.ok) {
74
+ setMessage("passkey failed — register a passkey while signed in");
75
+ return;
76
+ }
77
+ setMessage(null);
78
+ await refreshSession();
79
+ }
80
+
81
+ async function onSignOut(): Promise<void> {
82
+ if (!api.auth) return;
83
+ await api.auth.signOut();
84
+ await refreshSession();
85
+ }
31
86
 
32
87
  async function onCreate(event: FormEvent<HTMLFormElement>): Promise<void> {
33
88
  event.preventDefault();
@@ -57,8 +112,38 @@ export function App(): JSX.Element {
57
112
  <p className={health === "ok" ? "ok" : "down"}>
58
113
  {health === "unknown" ? "checking…" : health === "ok" ? "app up" : "app down — start oke dev"}
59
114
  </p>
115
+ <p className="session">{sessionLabel}</p>
60
116
  </header>
61
117
 
118
+ <form className="compose" onSubmit={(event) => void onSignIn(event)}>
119
+ <input
120
+ name="email"
121
+ type="email"
122
+ placeholder="Email"
123
+ value={email}
124
+ onChange={(event) => setEmail(event.target.value)}
125
+ required
126
+ />
127
+ <input
128
+ name="password"
129
+ type="password"
130
+ placeholder="Password"
131
+ value={password}
132
+ onChange={(event) => setPassword(event.target.value)}
133
+ required
134
+ />
135
+ <button type="submit">Sign in</button>
136
+ <button type="button" onClick={() => void onSignUp()}>
137
+ Sign up
138
+ </button>
139
+ <button type="button" onClick={() => void onPasskey()}>
140
+ Passkey
141
+ </button>
142
+ <button type="button" onClick={() => void onSignOut()}>
143
+ Sign out
144
+ </button>
145
+ </form>
146
+
62
147
  <form className="compose" onSubmit={(event) => void onCreate(event)}>
63
148
  <input
64
149
  name="title"
@@ -80,6 +165,21 @@ export function App(): JSX.Element {
80
165
  <button type="submit">Create</button>
81
166
  </form>
82
167
 
168
+ {api.auth ? (
169
+ <Can
170
+ auth={api.auth}
171
+ all={["notes:write"]}
172
+ fallback={
173
+ <p className="hint">
174
+ UI chrome: missing notes:write (authorize is UI-only — Gate on Flows is real authz).
175
+ </p>
176
+ }
177
+ loading={<p className="hint">Checking session…</p>}
178
+ >
179
+ <p className="hint">UI chrome: notes:write — editor affordances can show here.</p>
180
+ </Can>
181
+ ) : null}
182
+
83
183
  {message ? <p className="err">{message}</p> : null}
84
184
 
85
185
  <ul className="list">
@@ -12,8 +12,8 @@ export type Note = {
12
12
  readonly id: string;
13
13
  readonly title: string;
14
14
  readonly body: string;
15
- readonly archivedAt: number | null;
16
- readonly createdAt: number;
15
+ readonly archivedAt: string | null;
16
+ readonly createdAt: string;
17
17
  };
18
18
 
19
19
  const $routes = {
@@ -26,13 +26,26 @@ const $routes = {
26
26
  get: { method: "GET", path: "/notes/:id" },
27
27
  archive: { method: "POST", path: "/notes/:id/archive" },
28
28
  },
29
+ auth: {
30
+ me: { method: "GET", path: "/auth/me" },
31
+ signInEmail: { method: "POST", path: "/auth/sign-in/email" },
32
+ signUpEmail: { method: "POST", path: "/auth/sign-up/email" },
33
+ revoke: { method: "POST", path: "/auth/revoke" },
34
+ passkeyAuthenticateOptions: {
35
+ method: "POST",
36
+ path: "/auth/passkey/authenticate/options",
37
+ },
38
+ passkeyAuthenticate: { method: "POST", path: "/auth/passkey/authenticate" },
39
+ },
29
40
  } as const;
30
41
 
31
42
  type AppRoutes = { readonly $routes: typeof $routes };
32
43
 
33
44
  /**
34
- * Typed caller for starter Flows. `VITE_API_URL` is empty in dev (proxy).
45
+ * Typed caller for starter Flows. Cookie session + `api.auth` helpers.
46
+ * `VITE_API_URL` is empty in dev (proxy).
35
47
  */
36
48
  export const api = createClient<AppRoutes>(import.meta.env.VITE_API_URL ?? "", {
37
49
  $routes,
50
+ auth: { mode: "cookie", csrfConfigured: true },
38
51
  });
@@ -20,6 +20,7 @@ const APP_ORIGIN = "http://127.0.0.1:6530";
20
20
  const proxy: Record<string, ProxyOptions> = {
21
21
  "/health": { target: APP_ORIGIN, changeOrigin: true },
22
22
  "/notes": { target: APP_ORIGIN, changeOrigin: true },
23
+ "/auth": { target: APP_ORIGIN, changeOrigin: true },
23
24
  "/_oke": { target: APP_ORIGIN, changeOrigin: true },
24
25
  };
25
26
 
@@ -1,15 +1,16 @@
1
1
  # Environment template — create-oke copies this to `.env.local` when you scaffold.
2
2
  #
3
3
  # Resolution order (first hit wins):
4
- # vault driver → process.env → .env.local → dev fallback
4
+ # vault driver → process.env → .env.local → dev / fromDocker fallback
5
5
  #
6
- # --- Vault (app secrets) ---
7
- # Declare secrets in src/vault.ts with `vault.secret(...)`, then uncomment lines here.
6
+ # --- Vault (contracts in src/vault.ts) ---
7
+ # `vault.secret` / `vault.config` names are declared in source so Console Vault
8
+ # lists them. Put values here, in process.env, via `oke vault set`, or Console —
9
+ # same contract either way. Uncomment a line to pin a dotenv value.
8
10
  #
9
11
  # ── vault — built-in encrypted-at-rest store ────────────────
10
- # `drivers.vault` is "vault": secrets live encrypted in Postgres, no extra service.
11
- # Nothing below is required to start — a value already in the vault backend wins
12
- # over leftover process.env / dotenv pins.
12
+ # `drivers.vault` is "vault": encrypted values live in Postgres, no extra service.
13
+ # A value already in the vault backend wins over leftover process.env / dotenv.
13
14
  #
14
15
  # oke vault init # creates the store, prints the master key ONCE
15
16
  # export OKE_VAULT_MASTER_KEY=… # every later command / boot unseals with this
@@ -36,10 +37,11 @@
36
37
  # Uncomment to override compose credentials for a host-managed service.
37
38
  # Host ports under docker mode are unique per project — do not assume defaults.
38
39
 
39
- # ── App / Console ───────────────────────────────────────────
40
+ # ── App / Console (declared in src/vault.ts) ────────────────
40
41
  # PORT=6530
41
42
  # OKE_APP_URL=http://127.0.0.1:6530
42
- # Vite web (`bun run web`): empty = same-origin proxy to oke dev
43
+ # PUBLIC_API_URL=http://127.0.0.1:6530
44
+ # Vite web (`bun run web`): leave VITE_API_URL unset = same-origin proxy to oke dev
43
45
  # VITE_API_URL=
44
46
  # OKE_CONSOLE_SECRET=
45
47
  # MAINTENANCE_MODE=0
@@ -114,9 +116,18 @@
114
116
  # POSTGRES_INITDB_ARGS=--data-checksums
115
117
 
116
118
  # ── AI ──────────────────────────────────────────────────────
117
- # Default local: llama.cpp openai-compatible (Ollama remains supported)
119
+ # Prefer `oke ai setup` writes ai.model() with registry providers.
120
+ # Zero Docker: OpenRouter (baseUrl auto-resolved)
121
+ # OPENROUTER_API_KEY=
122
+ # OKE_AI_CLOUD_MODEL=openrouter/free
123
+ # BYO OpenAI-compatible `/v1` — set OKE_AI_URL yourself (Compose does not manage inference)
118
124
  # OKE_AI_DRIVER=openai-compatible
119
- # OKE_AI_URL=http://127.0.0.1:8080/v1
120
- # OKE_AI_MODEL=granite3.3:2b
125
+ # OKE_AI_URL=http://127.0.0.1:1234/v1
126
+ # OKE_AI_MODEL=your-model-id
127
+ # Native Anthropic (production Claude)
121
128
  # ANTHROPIC_API_KEY=
129
+ # Other registry keys (when selected in setup): GROQ_API_KEY, TOGETHER_API_KEY,
130
+ # DEEPSEEK_API_KEY, MISTRAL_API_KEY, XAI_API_KEY, GEMINI_API_KEY, …
122
131
  # OPENAI_API_KEY=
132
+ # Optional proxy override for any registry provider:
133
+ # OPENAI_BASE_URL=
@@ -15,7 +15,7 @@ jobs:
15
15
  - uses: actions/checkout@v6
16
16
  - uses: oven-sh/setup-bun@v2.2.0
17
17
  with:
18
- bun-version: "1.4.0"
18
+ bun-version: "1.4.2"
19
19
  - name: Install
20
20
  run: bun install
21
21
  - name: Typecheck
@@ -22,12 +22,12 @@ export default defineConfig({
22
22
  store: {
23
23
  sql: "postgres:18-alpine",
24
24
  kv: "redis:8-alpine",
25
- files: "rustfs/rustfs:1.0.0-rc.2",
25
+ files: "rustfs/rustfs:1.0.0-rc.5",
26
26
  },
27
27
  channel: {
28
- email: "axllent/mailpit:v1.30.7",
28
+ email: "axllent/mailpit:v1.31.1",
29
29
  },
30
- // pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.53", // create-oke wizard / --pgdog
30
+ // pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.57", // create-oke wizard / --pgdog
31
31
  // proxy: "caddy:2-alpine", // or traefik:v3.7 / nginx:1.31-alpine
32
32
  },
33
33
  i18n: { locales: ["en"], default: "en" },
@@ -5,22 +5,23 @@
5
5
  "type": "module",
6
6
  "dependencies": {
7
7
  "okengine": "file:../../../..",
8
- "@duckdb/node-api": "^1.5.5-r.2",
8
+ "@duckdb/node-api": "^1.5.5-r.4",
9
9
  "drizzle-orm": "1.0.0-rc.5-169397b",
10
- "react": "^19.2.8",
11
- "react-dom": "^19.2.8",
12
- "zod": "^4.4.3"
10
+ "oxc-parser": "^0.149.0",
11
+ "react": "^19.3.0",
12
+ "react-dom": "^19.3.0",
13
+ "zod": "^4.6.1"
13
14
  },
14
15
  "devDependencies": {
15
- "@electric-sql/pglite": "^0.5.4",
16
- "@electric-sql/pglite-pgvector": "^0.0.5",
16
+ "@electric-sql/pglite": "^0.5.8",
17
+ "@electric-sql/pglite-pgvector": "^0.0.9",
17
18
  "@types/bun": "latest",
18
- "@types/react": "^19.2.18",
19
- "@types/react-dom": "^19.2.4",
20
- "@vitejs/plugin-react": "^6.0.5",
19
+ "@types/react": "^19.3.0",
20
+ "@types/react-dom": "^19.3.0",
21
+ "@vitejs/plugin-react": "^6.1.1",
21
22
  "drizzle-kit": "1.0.0-rc.5-ab785fc",
22
23
  "typescript": "^7.0.2",
23
- "vite": "^8.2.0"
24
+ "vite": "^8.3.0"
24
25
  },
25
26
  "trustedDependencies": ["@duckdb/node-api"],
26
27
  "scripts": {
@@ -2,7 +2,12 @@ import "@/core";
2
2
  import "@/flows/generated";
3
3
 
4
4
  import { oke } from "okengine/http";
5
+ import { NOTES_VAULT } from "@/vault";
5
6
 
6
- export const app = oke({ name: "notes" });
7
+ /**
8
+ * Notes app — `vault.config` is not auto-registered (only `vault.secret` is),
9
+ * so pass {@link NOTES_VAULT} for configs + secrets to resolve together.
10
+ */
11
+ export const app = oke({ name: "notes", secrets: NOTES_VAULT });
7
12
 
8
13
  export type App = typeof app;
@@ -3,15 +3,17 @@
3
3
  *
4
4
  * Order matches how you usually extend the starter:
5
5
  * locales → store → gate → vault → channel → (AI via `oke ai setup`).
6
- * Keep this file until a section outgrows it, then split by element.
6
+ * Vault contracts live in `src/vault.ts` (re-exported below).
7
7
  */
8
8
 
9
9
  import "@/locales";
10
10
 
11
- import { channel, gate, store, vault } from "okengine";
11
+ import { channel, gate, store } from "okengine";
12
12
  import { z } from "zod";
13
13
  import * as schema from "@/db/schema.decl";
14
14
 
15
+ export * from "@/vault";
16
+
15
17
  // --- Store -------------------------------------------------------------------
16
18
 
17
19
  /** SQL store for Notes (`schema.decl`). Drivers: pglite locally · postgres in docker. */
@@ -42,14 +44,6 @@ export const notesWriteRate = gate.rate({
42
44
  /** Reuse on every notes mutate route. */
43
45
  export const notesMutate = gate.all(notesWrite, notesWriteRate);
44
46
 
45
- // --- Vault -------------------------------------------------------------------
46
-
47
- /** HMAC secret for outbound note webhooks (`fx.vault.get` on create). */
48
- export const webhookSecret = vault.secret("APP_WEBHOOK_SECRET", {
49
- description: "HMAC secret for outbound note webhooks",
50
- dev: "dev-webhook-secret-change-me",
51
- });
52
-
53
47
  // --- Channel -----------------------------------------------------------------
54
48
 
55
49
  const mail = channel.email({ from: "Notes <notes@localhost>" });
@@ -64,4 +58,9 @@ export const noteCreatedMail = mail.template("note-created", {
64
58
  });
65
59
 
66
60
  // --- AI ----------------------------------------------------------------------
67
- // `oke ai setup` / create-oke --ai appends models here.
61
+ // Appended by `oke ai setup` / `create-oke --ai`.
62
+ // When an embed model is chosen, setup also wires Notes hybrid search:
63
+ // oke({ store: { search: { embed: { model: embedModel, dims: 768 } } } })
64
+ // body: field.text().searchable().embed()
65
+ // Registry cloud: provider "openrouter" + OPENROUTER_API_KEY (no baseUrl).
66
+ // Local self-host still needs an explicit baseUrl (or OKE_AI_URL on the binding).
@@ -3,11 +3,15 @@ import { store, field } from "okengine";
3
3
  /**
4
4
  * Notes domain — abstract declarations, emitted to
5
5
  * `src/db/schema.drizzle.ts` for the active dialect by `oke db` / `oke dev`.
6
+ *
7
+ * `.searchable()` is free BM25. With `oke ai setup` (embed model chosen),
8
+ * body also gets bare `.embed()` and `oke({ store: { search: { embed } } })`
9
+ * stamps the project default (model + dims).
6
10
  */
7
11
  export const notes = store.schema.table("notes", {
8
12
  id: field.id().primaryKey(),
9
- title: field.text().notNull(),
10
- body: field.text().notNull(),
13
+ title: field.text().searchable({ weight: 2 }).notNull(),
14
+ body: field.text().searchable().notNull(),
11
15
  archivedAt: field.timestamp(),
12
16
  createdAt: field.timestamp().notNull().now(),
13
17
  });
@@ -19,7 +19,7 @@ async function welcomeNote(fx: Fx) {
19
19
  title: "Welcome",
20
20
  body: "Your Notes API is ready. Create, list, and archive notes over HTTP.",
21
21
  archivedAt: null,
22
- createdAt: 1,
22
+ createdAt: new Date("2026-01-15T10:00:00.000Z"),
23
23
  },
24
24
  );
25
25
  }
@@ -33,7 +33,7 @@ async function sampleNotes(fx: Fx) {
33
33
  title: "Shipping checklist",
34
34
  body: "Confirm schema with oke db push, then seed with oke db seed.",
35
35
  archivedAt: null,
36
- createdAt: 2,
36
+ createdAt: new Date("2026-01-15T10:01:00.000Z"),
37
37
  },
38
38
  );
39
39
  await fx.store(db).upsert(
@@ -44,7 +44,7 @@ async function sampleNotes(fx: Fx) {
44
44
  title: "Ideas",
45
45
  body: "Replace these seed rows with your own domain data.",
46
46
  archivedAt: null,
47
- createdAt: 3,
47
+ createdAt: new Date("2026-01-15T10:02:00.000Z"),
48
48
  },
49
49
  );
50
50
  }
@@ -3,14 +3,15 @@ import { z } from "zod";
3
3
 
4
4
  /** First-run welcome — visit :6530/ after `oke dev` (browser code block; curl stays JSON). */
5
5
  export const root = on(
6
- http.get().public(),
7
- flow({
6
+ http.get({
8
7
  out: z.object({
9
8
  ok: z.literal(true),
10
9
  app: z.string(),
11
10
  try: z.array(z.string()),
12
11
  console: z.string(),
13
12
  }),
13
+ }).public(),
14
+ flow({
14
15
  do: () => ({
15
16
  ok: true as const,
16
17
  app: "notes",
@@ -3,26 +3,23 @@ import { eq } from "drizzle-orm";
3
3
 
4
4
  import { db, notesMutate } from "@/core";
5
5
  import { notes } from "@/db/schema.decl";
6
- import { NoteIdIn, NoteOut, NotFound } from "../shapes";
6
+ import { NoteIdIn, NoteOut, NotFound, toIsoInstant } from "../shapes";
7
7
 
8
8
  /** Soft-archive a note. */
9
9
  export const archive = on(
10
- http.post().gate(notesMutate),
10
+ http.post({ in: NoteIdIn, out: NoteOut, errors: { NotFound } }).gate(notesMutate),
11
11
  flow({
12
- in: NoteIdIn,
13
- out: NoteOut,
14
- errors: { NotFound },
15
12
  do: async (input, fx) => {
16
13
  const row = await fx.store(db).findById(notes, input.id);
17
14
  if (!row) return fail("NotFound", { id: input.id });
18
- const archivedAt = fx.clock.now();
15
+ const archivedAt = new Date(fx.clock.now());
19
16
  await fx.store(db).update(notes).set({ archivedAt }).where(eq(notes.id, input.id));
20
17
  return {
21
18
  id: String(row.id),
22
19
  title: String(row.title),
23
20
  body: String(row.body),
24
- archivedAt,
25
- createdAt: Number(row.createdAt),
21
+ archivedAt: toIsoInstant(archivedAt),
22
+ createdAt: toIsoInstant(row.createdAt),
26
23
  };
27
24
  },
28
25
  }),