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.
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/agents-md.ts +6 -4
- package/src/ai-setup/apply.test.ts +278 -0
- package/src/ai-setup/apply.ts +430 -52
- package/src/ai-setup/catalog.ts +250 -1343
- package/src/ai-setup/from-pref.ts +3 -71
- package/src/ai-setup/prompts.ts +60 -443
- package/src/cli.test.ts +21 -15
- package/src/cli.ts +20 -11
- package/src/create-defaults.test.ts +4 -4
- package/src/create-defaults.ts +3 -3
- package/src/customize-flow.test.ts +9 -15
- package/src/customize-flow.ts +59 -66
- package/src/drivers-catalog.ts +18 -23
- package/src/transform.test.ts +69 -10
- package/src/transform.ts +10 -35
- package/templates/advanced/.env.example +28 -11
- package/templates/advanced/.github/workflows/ci.yml +1 -1
- package/templates/advanced/oke.config.ts +3 -4
- package/templates/advanced/package.json +11 -10
- package/templates/advanced/src/app.ts +36 -1
- package/templates/advanced/src/core.ts +12 -11
- package/templates/advanced/src/db/schema.decl.ts +6 -2
- package/templates/advanced/src/db/seed/index.ts +4 -4
- package/templates/advanced/src/flows/main/route.ts +3 -2
- package/templates/advanced/src/flows/notes/[id]/archive.ts +5 -8
- package/templates/advanced/src/flows/notes/[id]/attach.ts +1 -4
- package/templates/advanced/src/flows/notes/[id]/get.ts +4 -7
- package/templates/advanced/src/flows/notes/[id]/summarize.ts +3 -4
- package/templates/advanced/src/flows/notes/create.ts +4 -6
- package/templates/advanced/src/flows/notes/digest.ts +6 -5
- package/templates/advanced/src/flows/notes/list.ts +4 -5
- package/templates/advanced/src/flows/notes/shapes.ts +13 -3
- package/templates/advanced/src/flows/notes/signals.ts +1 -2
- package/templates/advanced/src/vault.ts +138 -0
- package/templates/advanced/tests/advanced.test.ts +11 -8
- package/templates/advanced/web/src/App.css +7 -0
- package/templates/advanced/web/src/App.tsx +101 -1
- package/templates/advanced/web/src/client.ts +16 -3
- package/templates/advanced/web/vite.config.ts +1 -0
- package/templates/standard/.env.example +22 -11
- package/templates/standard/.github/workflows/ci.yml +1 -1
- package/templates/standard/oke.config.ts +3 -3
- package/templates/standard/package.json +11 -10
- package/templates/standard/src/app.ts +6 -1
- package/templates/standard/src/core.ts +10 -11
- package/templates/standard/src/db/schema.decl.ts +6 -2
- package/templates/standard/src/db/seed/index.ts +3 -3
- package/templates/standard/src/flows/main/route.ts +3 -2
- package/templates/standard/src/flows/notes/[id]/archive.ts +5 -8
- package/templates/standard/src/flows/notes/[id]/get.ts +4 -7
- package/templates/standard/src/flows/notes/create.ts +4 -6
- package/templates/standard/src/flows/notes/list.ts +4 -5
- package/templates/standard/src/flows/notes/shapes.ts +12 -2
- package/templates/standard/src/flows/notes/signals.ts +1 -2
- package/templates/standard/src/vault.ts +123 -0
- package/templates/standard/tests/standard.test.ts +3 -1
- package/templates/standard/web/src/client.ts +2 -2
- package/templates/standard/web/vite.config.ts +1 -0
- package/src/ai-setup/detect-ollama.ts +0 -228
- 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 →
|
|
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
|
-
|
|
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();
|
|
@@ -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
|
-
|
|
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:
|
|
16
|
-
readonly createdAt:
|
|
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.
|
|
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 (
|
|
7
|
-
#
|
|
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":
|
|
11
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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:
|
|
120
|
-
# OKE_AI_MODEL=
|
|
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=
|
|
@@ -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.
|
|
25
|
+
files: "rustfs/rustfs:1.0.0-rc.5",
|
|
26
26
|
},
|
|
27
27
|
channel: {
|
|
28
|
-
email: "axllent/mailpit:v1.
|
|
28
|
+
email: "axllent/mailpit:v1.31.1",
|
|
29
29
|
},
|
|
30
|
-
// pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.
|
|
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.
|
|
8
|
+
"@duckdb/node-api": "^1.5.5-r.4",
|
|
9
9
|
"drizzle-orm": "1.0.0-rc.5-169397b",
|
|
10
|
-
"
|
|
11
|
-
"react
|
|
12
|
-
"
|
|
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.
|
|
16
|
-
"@electric-sql/pglite-pgvector": "^0.0.
|
|
16
|
+
"@electric-sql/pglite": "^0.5.8",
|
|
17
|
+
"@electric-sql/pglite-pgvector": "^0.0.9",
|
|
17
18
|
"@types/bun": "latest",
|
|
18
|
-
"@types/react": "^19.
|
|
19
|
-
"@types/react-dom": "^19.
|
|
20
|
-
"@vitejs/plugin-react": "^6.
|
|
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.
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
21
|
+
archivedAt: toIsoInstant(archivedAt),
|
|
22
|
+
createdAt: toIsoInstant(row.createdAt),
|
|
26
23
|
};
|
|
27
24
|
},
|
|
28
25
|
}),
|