@odla-ai/cli 0.1.0 → 0.2.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 +27 -0
- package/dist/bin.cjs +338 -37
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-AXCZKIVY.js → chunk-5J4LKP37.js} +331 -27
- package/dist/chunk-5J4LKP37.js.map +1 -0
- package/dist/index.cjs +344 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -1
- package/dist/index.d.ts +57 -1
- package/dist/index.js +7 -1
- package/llms.txt +85 -2
- package/package.json +2 -1
- package/skills/odla-migrate/SKILL.md +89 -0
- package/skills/odla-migrate/references/phase-0-preflight.md +45 -0
- package/skills/odla-migrate/references/phase-1-static.md +48 -0
- package/skills/odla-migrate/references/phase-2-db.md +59 -0
- package/skills/odla-migrate/references/phase-3-auth.md +52 -0
- package/skills/odla-migrate/references/phase-4-ai.md +44 -0
- package/skills/odla-migrate/references/phase-5-cutover.md +53 -0
- package/skills/odla-migrate/references/secrets-map.md +43 -0
- package/skills/odla-migrate/references/troubleshooting.md +69 -0
- package/dist/chunk-AXCZKIVY.js.map +0 -1
package/llms.txt
CHANGED
|
@@ -48,6 +48,8 @@ npx odla-ai doctor
|
|
|
48
48
|
npx odla-ai provision --dry-run
|
|
49
49
|
npx odla-ai provision --write-dev-vars
|
|
50
50
|
npx odla-ai smoke --env dev
|
|
51
|
+
npx odla-ai secrets push --env dev
|
|
52
|
+
npx odla-ai skill install
|
|
51
53
|
npx odla-ai version
|
|
52
54
|
```
|
|
53
55
|
|
|
@@ -83,6 +85,30 @@ odla-db schema with the tenant key, compares expected schema entities, and runs
|
|
|
83
85
|
a count aggregate against the first entity. It fails early with a clear message
|
|
84
86
|
when provisioning has not written `.odla/credentials.local.json`.
|
|
85
87
|
|
|
88
|
+
`secrets push --env <env>` moves the env's odla-db key from
|
|
89
|
+
`.odla/credentials.local.json` into the deployed Worker by piping it over
|
|
90
|
+
stdin to `wrangler secret put ODLA_API_KEY` — the value never appears on
|
|
91
|
+
argv, in output, or in an agent's transcript. It preflights `wrangler whoami`
|
|
92
|
+
and the presence of a wrangler config file. The env `prod`/`production`
|
|
93
|
+
targets the top-level wrangler environment (no `--env` flag is passed to
|
|
94
|
+
wrangler) and requires `--yes`; every other env maps to wrangler's
|
|
95
|
+
`--env <name>`. `--dry-run` prints a redacted plan without spawning anything.
|
|
96
|
+
|
|
97
|
+
`skill install` copies the Claude Code skills bundled with this package
|
|
98
|
+
(currently `odla-migrate`, the phased GitHub Pages → odla migration
|
|
99
|
+
procedure) into the project's `.claude/skills/`, or into `~/.claude/skills/`
|
|
100
|
+
with `--global`. Re-runs are idempotent; a locally modified skill file is
|
|
101
|
+
never overwritten without `--force`.
|
|
102
|
+
|
|
103
|
+
`doctor` also lints for common footguns: permission rules that are literally
|
|
104
|
+
`"true"` (public writes always warn; a public `view` warns unless the
|
|
105
|
+
namespace is listed in `db.publicRead`), schema entities missing from the
|
|
106
|
+
rules map, credential files (`.dev.vars`, `.odla/*`) tracked by git, a
|
|
107
|
+
wrangler assets directory pointed at the project root or at a directory
|
|
108
|
+
containing `node_modules` (the `wrangler dev` "spawn EBADF" footgun),
|
|
109
|
+
secret-shaped values in wrangler `vars`, and a package.json script literally
|
|
110
|
+
named `deploy` (CI may auto-deploy it).
|
|
111
|
+
|
|
86
112
|
## Config
|
|
87
113
|
|
|
88
114
|
```js
|
|
@@ -96,6 +122,7 @@ export default {
|
|
|
96
122
|
schema: "./src/odla/schema.mjs",
|
|
97
123
|
rules: "./src/odla/rules.mjs",
|
|
98
124
|
defaultRules: "deny",
|
|
125
|
+
publicRead: [], // namespaces where a `view: "true"` rule is intentional
|
|
99
126
|
},
|
|
100
127
|
ai: {
|
|
101
128
|
provider: process.env.ODLA_AI_PROVIDER ?? "anthropic",
|
|
@@ -121,7 +148,7 @@ If schema is present and rules are omitted, `defaultRules: "deny"` generates
|
|
|
121
148
|
deny-all rules for every schema entity. That is the safe default for Workers
|
|
122
149
|
that mediate reads and writes with an app key.
|
|
123
150
|
|
|
124
|
-
## API reference (generated from dist/index.d.ts, v0.
|
|
151
|
+
## API reference (generated from dist/index.d.ts, v0.2.0)
|
|
125
152
|
|
|
126
153
|
```ts
|
|
127
154
|
declare function runCli(argv?: string[]): Promise<void>;
|
|
@@ -173,6 +200,8 @@ interface OdlaProjectConfig {
|
|
|
173
200
|
rules?: AppRules | string;
|
|
174
201
|
/** Generate deny-all rules from schema when rules are omitted. Defaults to true when schema exists. */
|
|
175
202
|
defaultRules?: "deny" | false;
|
|
203
|
+
/** Namespaces where a literal `view: "true"` rule is intentional (public content). */
|
|
204
|
+
publicRead?: string[];
|
|
176
205
|
};
|
|
177
206
|
ai?: {
|
|
178
207
|
provider?: string;
|
|
@@ -254,7 +283,61 @@ interface SmokeOptions {
|
|
|
254
283
|
|
|
255
284
|
declare function provision(options: ProvisionOptions): Promise<void>;
|
|
256
285
|
|
|
286
|
+
declare function redactSecrets(value: string): string;
|
|
287
|
+
|
|
288
|
+
interface RunResult {
|
|
289
|
+
code: number;
|
|
290
|
+
stdout: string;
|
|
291
|
+
stderr: string;
|
|
292
|
+
}
|
|
293
|
+
/** Subprocess seam: injectable in tests, `defaultRunner` in production. */
|
|
294
|
+
type CommandRunner = (cmd: string, args: string[], opts?: {
|
|
295
|
+
input?: string;
|
|
296
|
+
cwd?: string;
|
|
297
|
+
}) => Promise<RunResult>;
|
|
298
|
+
|
|
299
|
+
interface SecretsPushOptions {
|
|
300
|
+
configPath: string;
|
|
301
|
+
env: string;
|
|
302
|
+
dryRun?: boolean;
|
|
303
|
+
/** Required to push to a prod-named env; the visible consent token in agent transcripts. */
|
|
304
|
+
yes?: boolean;
|
|
305
|
+
runner?: CommandRunner;
|
|
306
|
+
stdout?: Pick<typeof console, "log" | "error">;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Moves the env's odla-db key from `.odla/credentials.local.json` into the
|
|
310
|
+
* Worker via `wrangler secret put ODLA_API_KEY`, piping the value over stdin
|
|
311
|
+
* so it never appears on argv, in output, or in the conversation transcript.
|
|
312
|
+
*/
|
|
313
|
+
declare function secretsPush(options: SecretsPushOptions): Promise<void>;
|
|
314
|
+
|
|
315
|
+
interface SkillInstallOptions {
|
|
316
|
+
/** Project directory receiving `.claude/skills/`. Defaults to cwd. Ignored with `global`. */
|
|
317
|
+
dir?: string;
|
|
318
|
+
/** Install into `~/.claude/skills/` instead of the project. */
|
|
319
|
+
global?: boolean;
|
|
320
|
+
/** Overwrite locally modified skill files. */
|
|
321
|
+
force?: boolean;
|
|
322
|
+
/** Test/embedding override for the home directory. */
|
|
323
|
+
homeDir?: string;
|
|
324
|
+
/** Test/embedding override for the bundled skills directory. */
|
|
325
|
+
sourceDir?: string;
|
|
326
|
+
stdout?: Pick<typeof console, "log" | "error">;
|
|
327
|
+
}
|
|
328
|
+
interface SkillInstallResult {
|
|
329
|
+
targetDir: string;
|
|
330
|
+
written: string[];
|
|
331
|
+
unchanged: string[];
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Copies the skills bundled with this package into `.claude/skills/` so the
|
|
335
|
+
* user's agent picks them up. Idempotent: unchanged files are skipped, and a
|
|
336
|
+
* locally modified file is never overwritten without `force`.
|
|
337
|
+
*/
|
|
338
|
+
declare function installSkill(options?: SkillInstallOptions): SkillInstallResult;
|
|
339
|
+
|
|
257
340
|
declare function smoke(options: SmokeOptions): Promise<void>;
|
|
258
341
|
|
|
259
|
-
export { type LoadedProjectConfig, type LocalCredentials, type OdlaProjectConfig, type ProvisionOptions, type ProvisionPlan, type SmokeOptions, doctor, initProject, provision, runCli, smoke };
|
|
342
|
+
export { type LoadedProjectConfig, type LocalCredentials, type OdlaProjectConfig, type ProvisionOptions, type ProvisionPlan, type SecretsPushOptions, type SkillInstallOptions, type SkillInstallResult, type SmokeOptions, doctor, initProject, installSkill, provision, redactSecrets, runCli, secretsPush, smoke };
|
|
260
343
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@odla-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Project-neutral CLI for provisioning odla apps, database schemas, AI settings, and local runtime files.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
+
"skills",
|
|
22
23
|
"README.md",
|
|
23
24
|
"REQUIREMENTS.md",
|
|
24
25
|
"llms.txt"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: odla-migrate
|
|
3
|
+
description: >
|
|
4
|
+
Migrate a static site (e.g. GitHub Pages) to odla on Cloudflare in safe
|
|
5
|
+
phases, then add a database, Clerk login, and AI. Use when the user wants to
|
|
6
|
+
move a static or GitHub Pages site to Cloudflare/odla, or to add a backend,
|
|
7
|
+
database, login/auth, or AI features to a static site via odla.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# odla-migrate
|
|
11
|
+
|
|
12
|
+
You are driving a phased migration of a static site to odla + Cloudflare.
|
|
13
|
+
The human approves checkpoints and performs browser-only steps (accounts,
|
|
14
|
+
handshake approval, DNS). You do everything else.
|
|
15
|
+
|
|
16
|
+
## When NOT to use this skill
|
|
17
|
+
|
|
18
|
+
Greenfield odla apps (no existing site to migrate): follow the "Typical
|
|
19
|
+
agent flow" in https://odla.ai/llms.txt instead.
|
|
20
|
+
|
|
21
|
+
## Non-negotiable rules
|
|
22
|
+
|
|
23
|
+
1. The old site (GitHub Pages) stays live and untouched until Phase 5
|
|
24
|
+
sign-off. Before that, rollback is always "do nothing."
|
|
25
|
+
2. Dev only until Phase 5: `envs: ["dev"]` in odla.config.mjs; the dev
|
|
26
|
+
tenant is `<appId>--dev`. Verify the tenant before any write or deploy.
|
|
27
|
+
3. Never print, paste, or commit a secret. Never `cat` .dev.vars,
|
|
28
|
+
.odla/credentials.local.json, or .odla/dev-token.json. Read
|
|
29
|
+
references/secrets-map.md BEFORE any command that touches a credential.
|
|
30
|
+
4. Never `git add -A` without reading `git status` first.
|
|
31
|
+
5. Never widen a db rule to silence a 403 — default-deny is the design.
|
|
32
|
+
Any rules change is a human checkpoint.
|
|
33
|
+
6. Never run `provision --rotate-keys` unless the human explicitly asks.
|
|
34
|
+
|
|
35
|
+
## Phase state machine
|
|
36
|
+
|
|
37
|
+
Phases run strictly in order; each has a verification gate:
|
|
38
|
+
|
|
39
|
+
P0 preflight -> P1 static-on-cloudflare -> P2 database -> P3 login
|
|
40
|
+
-> P4 ai (optional) -> P5 prod + DNS cutover
|
|
41
|
+
|
|
42
|
+
`MIGRATION.md` at the user's repo root is the durable state: create it in
|
|
43
|
+
Phase 0, update it at every gate (phase, what changed, what was verified).
|
|
44
|
+
In a fresh session, read MIGRATION.md first and resume from the recorded
|
|
45
|
+
phase. If the CLI offers `odla-ai status`, run it to confirm.
|
|
46
|
+
|
|
47
|
+
## Checkpoint protocol (every phase boundary)
|
|
48
|
+
|
|
49
|
+
1. Run the phase's verification checklist (in its reference file).
|
|
50
|
+
2. Give the human a 3-line summary: what changed / what was verified /
|
|
51
|
+
what the NEXT phase will ask of them (account to create, code to
|
|
52
|
+
approve, value to paste, command to run themselves).
|
|
53
|
+
3. Wait for explicit approval before entering the next phase.
|
|
54
|
+
|
|
55
|
+
At the very first checkpoint, send the human https://odla.ai/migrate —
|
|
56
|
+
it explains their side of the whole journey in plain language.
|
|
57
|
+
|
|
58
|
+
## Verification tools
|
|
59
|
+
|
|
60
|
+
- `npx odla-ai doctor` — offline config/schema/rules validation; run
|
|
61
|
+
after any config edit.
|
|
62
|
+
- `npx odla-ai provision --dry-run` — the plan, zero network/file I/O;
|
|
63
|
+
show it to the human before the first real provision.
|
|
64
|
+
- `npx odla-ai smoke --env dev` — live, read-only: public-config, live
|
|
65
|
+
schema diff, a count aggregate. Run after every provision.
|
|
66
|
+
- `wrangler dev` + curl — exercise routes locally before deploying.
|
|
67
|
+
- Parity curls — compare the old and new site on representative paths
|
|
68
|
+
(Phase 1, again from the public domain in Phase 5).
|
|
69
|
+
|
|
70
|
+
## Phase files
|
|
71
|
+
|
|
72
|
+
Read the current phase's file when you enter it — not before:
|
|
73
|
+
|
|
74
|
+
- references/phase-0-preflight.md
|
|
75
|
+
- references/phase-1-static.md
|
|
76
|
+
- references/phase-2-db.md
|
|
77
|
+
- references/phase-3-auth.md
|
|
78
|
+
- references/phase-4-ai.md
|
|
79
|
+
- references/phase-5-cutover.md
|
|
80
|
+
|
|
81
|
+
On any failure, read references/troubleshooting.md before improvising.
|
|
82
|
+
|
|
83
|
+
## Context bootstrap
|
|
84
|
+
|
|
85
|
+
Before Phase 1, fetch https://odla.ai/llms.txt and
|
|
86
|
+
https://odla.ai/llms-migrate.txt (the served copy of this procedure's
|
|
87
|
+
reference text). After `npm install`, prefer
|
|
88
|
+
`node_modules/@odla-ai/*/llms.txt` over training memory for any
|
|
89
|
+
@odla-ai package — the packages are newer than you think.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Phase 0 — Preflight
|
|
2
|
+
|
|
3
|
+
Goal: understand the repo and make it migratable. No accounts, no
|
|
4
|
+
platform state, no changes to the live site.
|
|
5
|
+
|
|
6
|
+
Human obligation: none.
|
|
7
|
+
|
|
8
|
+
## Steps
|
|
9
|
+
|
|
10
|
+
1. Inventory the repo and record findings:
|
|
11
|
+
- Static generator? (`_config.yml` = Jekyll, astro/eleventy/vite
|
|
12
|
+
configs, plain HTML?) Build command? Output directory?
|
|
13
|
+
- Custom domain? (`CNAME` file at root or in the publish source —
|
|
14
|
+
capture the domain; it becomes `links.prod` in Phase 5.)
|
|
15
|
+
- How Pages deploys: `gh-pages` branch, `docs/` folder, or a
|
|
16
|
+
`.github/workflows/*` using deploy-pages.
|
|
17
|
+
- Dynamic wishes: forms posting to third parties, `mailto:` contact,
|
|
18
|
+
localStorage used as a database, TODOs mentioning login/db.
|
|
19
|
+
- If your installed CLI has `npx odla-ai inspect`, run it instead of
|
|
20
|
+
hand-checking; it also does the secret scan below.
|
|
21
|
+
2. Ensure the build outputs to a DEDICATED directory (`dist/`, `_site/`,
|
|
22
|
+
`build/`). If the site is served from the repo root, restructure so a
|
|
23
|
+
build step (even a copy script) produces a clean output dir first.
|
|
24
|
+
This is a hard blocker: pointing wrangler's assets at a directory
|
|
25
|
+
containing node_modules kills `wrangler dev` with "spawn EBADF".
|
|
26
|
+
3. Confirm the output dir is gitignored if it is generated.
|
|
27
|
+
4. Secret scan: grep tracked files for key-shaped strings (`sk-`,
|
|
28
|
+
`sk_live_`, `whsec_`, `ghp_`, `github_pat_`, `AKIA`, `-----BEGIN`).
|
|
29
|
+
Any hit: STOP, show the human file:line (never the value), and
|
|
30
|
+
resolve before continuing.
|
|
31
|
+
5. Create `MIGRATION.md` at the repo root: the six-phase checklist with
|
|
32
|
+
P0 marked in progress, the inventory findings, and the chosen build
|
|
33
|
+
dir. Commit it (review `git status` first).
|
|
34
|
+
|
|
35
|
+
## Verification checklist
|
|
36
|
+
|
|
37
|
+
- [ ] Build runs clean and populates only the dedicated output dir
|
|
38
|
+
- [ ] Output dir contains index.html and the site's assets
|
|
39
|
+
- [ ] No committed secrets found (or resolved with the human)
|
|
40
|
+
- [ ] MIGRATION.md committed
|
|
41
|
+
|
|
42
|
+
Rollback: nothing to roll back.
|
|
43
|
+
|
|
44
|
+
Done when: all boxes checked and the human approves entering Phase 1
|
|
45
|
+
(their next obligation: a Cloudflare account + `wrangler login`).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Phase 1 — Same site, served by a Cloudflare Worker
|
|
2
|
+
|
|
3
|
+
Goal: the exact same site, deployed to a workers.dev URL via a Worker
|
|
4
|
+
with an assets binding. No odla yet. GitHub Pages untouched.
|
|
5
|
+
|
|
6
|
+
Human obligation: create a Cloudflare account (free plan) and run
|
|
7
|
+
`wrangler login` (browser flow — they run it, or run `! wrangler login`
|
|
8
|
+
in this session).
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. `npm i -D wrangler`
|
|
13
|
+
2. If your installed CLI has `npx odla-ai scaffold worker`, use it.
|
|
14
|
+
Otherwise write `wrangler.jsonc` by hand, modeled on
|
|
15
|
+
`examples/demo-app/wrangler.jsonc` in the odla-ai monorepo:
|
|
16
|
+
- `name`: kebab-case app id; `main`: `src/worker.ts`
|
|
17
|
+
- `compatibility_date` (today), `compatibility_flags: ["nodejs_compat"]`
|
|
18
|
+
- `assets: { "directory": "<buildDir>", "binding": "ASSETS",
|
|
19
|
+
"not_found_handling": "404-page" }` — match the site's current 404
|
|
20
|
+
behavior; the directory is the Phase 0 build dir, NEVER the repo root
|
|
21
|
+
- `env.dev` block: `name` = "<name>-dev", same assets
|
|
22
|
+
3. Minimal `src/worker.ts`:
|
|
23
|
+
- `GET /api/health` returns JSON `{ ok: true }` and sets an
|
|
24
|
+
`x-odla-worker: <name>` response header
|
|
25
|
+
- everything else: `return env.ASSETS.fetch(req)`
|
|
26
|
+
4. Build the site, then `wrangler dev` and spot-check pages locally.
|
|
27
|
+
5. Deploy ONLY dev: `npx wrangler deploy --env dev`. Do not deploy the
|
|
28
|
+
top-level (prod) config before Phase 5.
|
|
29
|
+
6. Parity check: pick 5–10 representative paths (home, a deep page, an
|
|
30
|
+
asset, a missing path for 404). Curl each on BOTH the Pages URL and
|
|
31
|
+
the workers.dev URL; compare status, content-type, and title.
|
|
32
|
+
7. Add non-`deploy` npm scripts so CI never auto-deploys:
|
|
33
|
+
`"deploy:app:dev": "<build> && wrangler deploy --env dev"` (and later
|
|
34
|
+
`"deploy:app"` for prod). Never name a script exactly `deploy`.
|
|
35
|
+
8. Record the workers.dev URL and parity results in MIGRATION.md.
|
|
36
|
+
|
|
37
|
+
## Verification checklist
|
|
38
|
+
|
|
39
|
+
- [ ] `wrangler dev` serves the site with no EBADF / watcher errors
|
|
40
|
+
- [ ] Parity table recorded (status + content-type + title per path)
|
|
41
|
+
- [ ] `/api/health` returns `{ ok: true }` on the deployed dev worker
|
|
42
|
+
- [ ] GitHub Pages site still serving, untouched
|
|
43
|
+
|
|
44
|
+
Rollback: delete the dev worker in the Cloudflare dashboard. Pages was
|
|
45
|
+
never touched.
|
|
46
|
+
|
|
47
|
+
Done when: parity recorded and the human approves Phase 2 (their next
|
|
48
|
+
obligation: sign in at https://odla.ai and approve a handshake code).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Phase 2 — Database (dev tenant only)
|
|
2
|
+
|
|
3
|
+
Goal: the app registered on the platform, a dev odla-db tenant with
|
|
4
|
+
schema + deny-all rules, the db key in the dev worker, and first
|
|
5
|
+
`/api/*` routes live in dev.
|
|
6
|
+
|
|
7
|
+
Human obligation: sign in at https://odla.ai and approve the handshake
|
|
8
|
+
code when the provision run prints it (it also opens the approval page
|
|
9
|
+
in the browser in interactive terminals).
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. `npm i -D @odla-ai/cli` and `npm i @odla-ai/db`
|
|
14
|
+
2. `npx odla-ai init --app-id <id> --name "<Name>" --env dev --services db`
|
|
15
|
+
Review `odla.config.mjs`. Keep `envs: ["dev"]` — prod is Phase 5.
|
|
16
|
+
3. STOP before touching schema: read "Porting relational code" in
|
|
17
|
+
`node_modules/@odla-ai/db/llms.txt`. The traps are silent: entity ids
|
|
18
|
+
are not attrs (mirror an id attr), there is no NULL (omit on write,
|
|
19
|
+
re-project on read), lists need explicit `order`, uniques are
|
|
20
|
+
single-attr (derive composite keys), ON CONFLICT maps to `mutationId`
|
|
21
|
+
dedupe.
|
|
22
|
+
4. Write `src/odla/schema.mjs` for the app's entities. KEEP the
|
|
23
|
+
generated deny-all `src/odla/rules.mjs`: the worker mediates all
|
|
24
|
+
access with its app key (which bypasses rules); browsers get nothing
|
|
25
|
+
directly. Loosening a rule is a human checkpoint.
|
|
26
|
+
5. `npx odla-ai doctor` until clean.
|
|
27
|
+
6. `npx odla-ai provision --dry-run` — show the plan to the human.
|
|
28
|
+
7. `npx odla-ai provision --write-dev-vars` — handshake, app
|
|
29
|
+
registration, dev db key, schema + rules push; writes
|
|
30
|
+
`.odla/credentials.local.json` (0600) and `.dev.vars`. Both are
|
|
31
|
+
gitignored by init — confirm with `git status`.
|
|
32
|
+
8. Push the key to the dev worker without echoing it:
|
|
33
|
+
`npx odla-ai secrets push --env dev` — it pipes the value from the
|
|
34
|
+
credentials file straight into wrangler over stdin (details and the
|
|
35
|
+
manual fallback: references/secrets-map.md).
|
|
36
|
+
9. Add to `wrangler.jsonc` `env.dev.vars`: `ODLA_ENDPOINT`
|
|
37
|
+
("https://db.odla.ai"), `ODLA_TENANT` ("<appId>--dev"),
|
|
38
|
+
`ODLA_PLATFORM` ("https://odla.ai"), `ODLA_APP_ID`, `ODLA_ENV`
|
|
39
|
+
("dev"). Mirror at top level with tenant "<appId>" / env "prod" for
|
|
40
|
+
Phase 5. These are vars, not secrets.
|
|
41
|
+
10. Add `/api/*` routes before the assets fall-through, using
|
|
42
|
+
`init({ appId: env.ODLA_TENANT, adminToken: env.ODLA_API_KEY,
|
|
43
|
+
endpoint: env.ODLA_ENDPOINT })` from `@odla-ai/db`. Reference:
|
|
44
|
+
`examples/demo-app/src/routes.ts`.
|
|
45
|
+
11. `wrangler dev` + curl each route; then `npm run deploy:app:dev` and
|
|
46
|
+
curl the workers.dev URL.
|
|
47
|
+
|
|
48
|
+
## Verification checklist
|
|
49
|
+
|
|
50
|
+
- [ ] `npx odla-ai smoke --env dev` passes (public-config, schema, aggregate)
|
|
51
|
+
- [ ] Routes work locally and on the deployed dev worker
|
|
52
|
+
- [ ] `git status` shows no credential files staged
|
|
53
|
+
- [ ] MIGRATION.md updated with tenant id + route list
|
|
54
|
+
|
|
55
|
+
Rollback: the dev tenant is disposable; the live site never depended on
|
|
56
|
+
it. Pages untouched.
|
|
57
|
+
|
|
58
|
+
Done when: smoke passes, routes live in dev, human approves Phase 3
|
|
59
|
+
(their next obligation: a Clerk account + pasting a publishable key).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Phase 3 — Login (Clerk, client mode)
|
|
2
|
+
|
|
3
|
+
Goal: Clerk sign-in on the site, the worker verifying session JWTs
|
|
4
|
+
itself, at least one route gated. Dev only.
|
|
5
|
+
|
|
6
|
+
Human obligation: create a Clerk application (dev instance) and paste
|
|
7
|
+
its PUBLISHABLE key (`pk_test_…`) when asked. Tell them explicitly:
|
|
8
|
+
publishable keys are public by design — this is the only key-like value
|
|
9
|
+
they will ever paste into chat. Never ask for `sk_…` or `whsec_…`.
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. Add to `odla.config.mjs`:
|
|
14
|
+
`auth: { clerk: { dev: "<pk_test_…>" } }`
|
|
15
|
+
(Inline is fine — the value is public. The `"$ENV_VAR"` indirection
|
|
16
|
+
the config template shows is optional.)
|
|
17
|
+
2. `npx odla-ai provision` (idempotent) — calls setAuth for the dev env;
|
|
18
|
+
the issuer is derived from the key. The worker fetches auth config
|
|
19
|
+
from the registry's public-config at runtime, so a key rotation
|
|
20
|
+
propagates without a redeploy.
|
|
21
|
+
3. `npm i jose`. In the worker (lift the exact pattern from
|
|
22
|
+
`examples/demo-app/src/worker.ts`):
|
|
23
|
+
- fetch `<ODLA_PLATFORM>/registry/apps/<ODLA_APP_ID>/public-config?env=<ODLA_ENV>`,
|
|
24
|
+
cache ~5 min per isolate
|
|
25
|
+
- `createRemoteJWKSet(new URL(issuer + "/.well-known/jwks.json"))`,
|
|
26
|
+
cached per issuer
|
|
27
|
+
- `jwtVerify(token, jwks, { issuer })` on the `Authorization: Bearer`
|
|
28
|
+
header; pass `{ sub, email }` to routes
|
|
29
|
+
- ensure the Clerk session token includes an email claim (Clerk
|
|
30
|
+
dashboard → session token customization) if routes need email
|
|
31
|
+
4. Gate at least one `/api/*` route on a verified user; return 401
|
|
32
|
+
otherwise.
|
|
33
|
+
5. Add Clerk sign-in to the site pages (ClerkJS with the publishable
|
|
34
|
+
key from public-config, or Clerk's hosted pages).
|
|
35
|
+
6. Deploy dev (`npm run deploy:app:dev`).
|
|
36
|
+
|
|
37
|
+
Auth mode "full" (webhook-synced $users via svix, whsec in the tenant
|
|
38
|
+
vault) is a later upgrade — not needed to ship login.
|
|
39
|
+
|
|
40
|
+
## Verification checklist
|
|
41
|
+
|
|
42
|
+
- [ ] Unauthenticated curl to the gated route → 401
|
|
43
|
+
- [ ] Signed-in browser session reaches the gated route
|
|
44
|
+
- [ ] `npx odla-ai smoke --env dev` still passes
|
|
45
|
+
- [ ] MIGRATION.md updated (gated routes, Clerk instance name)
|
|
46
|
+
|
|
47
|
+
Rollback: remove the auth layer / ungate the route; nothing outside dev
|
|
48
|
+
changed.
|
|
49
|
+
|
|
50
|
+
Done when: both auth outcomes verified and the human approves Phase 4
|
|
51
|
+
(their next obligation: a provider API key, used in THEIR shell only)
|
|
52
|
+
— or Phase 5 directly if they don't want AI.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Phase 4 — AI (optional)
|
|
2
|
+
|
|
3
|
+
Goal: the worker calls an LLM via `initFromPlatform` — provider/model
|
|
4
|
+
from the registry, the key from the tenant vault. The key never enters
|
|
5
|
+
this conversation, wrangler config, or git.
|
|
6
|
+
|
|
7
|
+
Human obligation: obtain a provider API key and run ONE command in their
|
|
8
|
+
own terminal so you never see the value.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. Add `"ai"` to `services` and configure in `odla.config.mjs`:
|
|
13
|
+
`ai: { provider: "<anthropic|openai|google>", keyEnv: "<PROVIDER>_API_KEY" }`
|
|
14
|
+
2. Ask the human to run, in their own terminal (NOT pasted to you, not
|
|
15
|
+
via this session):
|
|
16
|
+
|
|
17
|
+
export <PROVIDER>_API_KEY=... # their key
|
|
18
|
+
npx odla-ai provision
|
|
19
|
+
|
|
20
|
+
provision stores the key in the tenant vault and sets provider/model
|
|
21
|
+
in the registry. Their shell forgets it when closed; wrangler and git
|
|
22
|
+
never see it. If they use `! npx odla-ai provision` in-session, the
|
|
23
|
+
export must still happen in a terminal you don't read.
|
|
24
|
+
3. `npm i @odla-ai/ai`. In the worker, use `initFromPlatform` — it reads
|
|
25
|
+
provider/model from public-config (cached ~60s) and resolves the key
|
|
26
|
+
from the vault at call time using only the app's db key. Provider and
|
|
27
|
+
model are switchable in Studio with no redeploy. Reference:
|
|
28
|
+
`examples/kg-demo` in the odla-ai monorepo.
|
|
29
|
+
4. Add one `/api/*` route that round-trips the model; deploy dev.
|
|
30
|
+
|
|
31
|
+
## Verification checklist
|
|
32
|
+
|
|
33
|
+
- [ ] `npx odla-ai smoke --env dev` passes
|
|
34
|
+
- [ ] The AI route returns a model response on the deployed dev worker
|
|
35
|
+
- [ ] `wrangler.jsonc` `vars` contain NO provider key; `git grep` for the
|
|
36
|
+
key's prefix finds nothing
|
|
37
|
+
- [ ] MIGRATION.md updated (provider, model, route)
|
|
38
|
+
|
|
39
|
+
Rollback: remove the route; the vault key can be rotated/removed in
|
|
40
|
+
Studio.
|
|
41
|
+
|
|
42
|
+
Done when: a dev route returns a model response and the human approves
|
|
43
|
+
Phase 5 (their next obligations: domain into Cloudflare, DNS clicks,
|
|
44
|
+
prod Clerk instance if using login).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Phase 5 — Production + DNS cutover
|
|
2
|
+
|
|
3
|
+
Goal: prod env provisioned and deployed, custom domain on the prod
|
|
4
|
+
worker, DNS cut over — with GitHub Pages kept live as the rollback for
|
|
5
|
+
at least 72 hours.
|
|
6
|
+
|
|
7
|
+
Human obligations: add the domain to Cloudflare; click through the DNS
|
|
8
|
+
changes (you supply exact values); create the prod Clerk instance if
|
|
9
|
+
the app uses login; final go/no-go at each step below.
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. Update `odla.config.mjs`: add `"prod"` to `envs`; add
|
|
14
|
+
`auth.clerk.prod` (`pk_live_…` from the prod Clerk instance) if using
|
|
15
|
+
login; add `links: { prod: "https://<domain>" }` (the CNAME domain
|
|
16
|
+
captured in Phase 0). If using AI, the human re-runs the Phase 4
|
|
17
|
+
export + provision so the PROD tenant's vault gets the key.
|
|
18
|
+
2. `npx odla-ai provision --dry-run`, show the human, then
|
|
19
|
+
`npx odla-ai provision` — provisions the prod tenant (`<appId>`),
|
|
20
|
+
idempotent for everything already done in dev.
|
|
21
|
+
3. Push the prod db key: `npx odla-ai secrets push --env prod --yes`
|
|
22
|
+
(targets the top-level wrangler env; `--yes` is the explicit prod
|
|
23
|
+
consent — see references/secrets-map.md).
|
|
24
|
+
4. Build, then `npx wrangler deploy` (first prod deploy). Verify
|
|
25
|
+
`/api/health` and the parity paths on the prod workers.dev URL.
|
|
26
|
+
5. `npx odla-ai smoke --env prod`.
|
|
27
|
+
6. Human: add the domain to Cloudflare, then attach it to the prod
|
|
28
|
+
worker (Workers & Pages → the worker → Domains & Routes). Supply
|
|
29
|
+
them the exact hostname values to enter.
|
|
30
|
+
7. Cut DNS. GitHub Pages STAYS PUBLISHED — if anything looks wrong,
|
|
31
|
+
the rollback is pointing DNS back at Pages.
|
|
32
|
+
8. Verify from the public domain: parity paths, the gated route (signed
|
|
33
|
+
in and out), `/api/*` routes, the AI route if present.
|
|
34
|
+
9. Update MIGRATION.md: cutover timestamp, verification results, and a
|
|
35
|
+
dated reminder ≥ 72 hours out to decommission Pages.
|
|
36
|
+
10. After ≥ 72 hours of clean parallel-run and explicit human
|
|
37
|
+
confirmation: disable GitHub Pages in the repo settings. KEEP the
|
|
38
|
+
repo — it is still the source of the site.
|
|
39
|
+
|
|
40
|
+
## Verification checklist
|
|
41
|
+
|
|
42
|
+
- [ ] `npx odla-ai smoke --env prod` passes
|
|
43
|
+
- [ ] Public domain serves from the worker (check the `x-odla-worker`
|
|
44
|
+
header) on every parity path
|
|
45
|
+
- [ ] Auth and AI routes verified from the public domain
|
|
46
|
+
- [ ] Pages still enabled until the 72-hour confirmation
|
|
47
|
+
|
|
48
|
+
Rollback: point DNS back at GitHub Pages (minutes). Nothing on the
|
|
49
|
+
odla/Cloudflare side needs to be torn down to roll back.
|
|
50
|
+
|
|
51
|
+
Done when: the 72-hour confirmation is done and MIGRATION.md is closed
|
|
52
|
+
out. Congratulate the human — and mention Studio (https://odla.ai) as
|
|
53
|
+
where they watch their app from now on.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Secrets map — read before ANY command that touches a credential
|
|
2
|
+
|
|
3
|
+
## Where each value lives (and the ONLY place it lives)
|
|
4
|
+
|
|
5
|
+
| Value | Lives in | Handling |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Clerk publishable key (`pk_test_`/`pk_live_`) | registry, via provision → setAuth | public by design; the one value a human may paste into chat; fine inline in odla.config.mjs |
|
|
8
|
+
| Clerk webhook secret (`whsec_…`) | tenant vault (`clerk_webhook_secret`) | only for auth mode "full"; entered in Studio, never wrangler, never chat |
|
|
9
|
+
| Clerk secret key (`sk_test_`/`sk_live_`) | not used in this journey | never ask for it |
|
|
10
|
+
| LLM provider key | tenant vault | env var in the HUMAN's shell for one provision run; never wrangler vars, never git, never chat |
|
|
11
|
+
| `odla_sk_…` (tenant db key) | wrangler secret `ODLA_API_KEY` + `.odla/credentials.local.json` (0600) + `.dev.vars` | the only wrangler secret in this journey; move it only with the pipeline below |
|
|
12
|
+
| `odla_dev_…` (developer token) | `.odla/dev-token.json` (0600) | ~24h lifetime, provision-time only; never deployed |
|
|
13
|
+
| `ODLA_ENDPOINT` / `ODLA_TENANT` / `ODLA_PLATFORM` / `ODLA_APP_ID` / `ODLA_ENV` | wrangler `vars` | not secrets; keep them set in every env block |
|
|
14
|
+
|
|
15
|
+
## Moving the db key into the Worker (never through the transcript)
|
|
16
|
+
|
|
17
|
+
Use the CLI — it reads the 0600 credentials file and pipes the value to
|
|
18
|
+
wrangler over stdin, with preflights (`wrangler whoami`, config present)
|
|
19
|
+
and redacted output:
|
|
20
|
+
|
|
21
|
+
npx odla-ai secrets push --env dev
|
|
22
|
+
npx odla-ai secrets push --env prod --yes # Phase 5; --yes is the prod consent
|
|
23
|
+
|
|
24
|
+
Manual fallback (identical mechanics, if the CLI is unavailable):
|
|
25
|
+
|
|
26
|
+
node -e 'const c=require("./.odla/credentials.local.json");process.stdout.write(c.envs.dev.dbKey)' \
|
|
27
|
+
| npx wrangler secret put ODLA_API_KEY --env dev
|
|
28
|
+
|
|
29
|
+
(For prod, use `c.envs.prod.dbKey` and drop `--env` — the top-level
|
|
30
|
+
wrangler env is prod.)
|
|
31
|
+
|
|
32
|
+
## Standing rules
|
|
33
|
+
|
|
34
|
+
- Never `cat`, `head`, `grep -v`, or otherwise display `.dev.vars`,
|
|
35
|
+
`.odla/credentials.local.json`, or `.odla/dev-token.json`. To check
|
|
36
|
+
they exist, use `ls -l` (also confirms 0600).
|
|
37
|
+
- Before every commit: read `git status`; the files above and the build
|
|
38
|
+
output dir must not be staged. `odla-ai init` gitignores them — trust
|
|
39
|
+
but verify.
|
|
40
|
+
- If a secret value ever does land in the conversation or a committed
|
|
41
|
+
file: tell the human immediately, treat it as burned, and rotate it
|
|
42
|
+
(`provision --rotate-keys` for db keys — with their explicit
|
|
43
|
+
approval; provider dashboard for LLM/Clerk values).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Troubleshooting — symptom → cause → fix
|
|
2
|
+
|
|
3
|
+
## `spawn EBADF` when running `wrangler dev`
|
|
4
|
+
|
|
5
|
+
Cause: the assets `directory` points at the repo root or any directory
|
|
6
|
+
containing `node_modules`; wrangler's file watcher exhausts file
|
|
7
|
+
descriptors and workerd dies with this unhelpful error.
|
|
8
|
+
Fix: point `assets.directory` at the dedicated build dir from Phase 0.
|
|
9
|
+
Never "fix" it by raising ulimits.
|
|
10
|
+
|
|
11
|
+
## 403 / permission denied from odla-db (usually from the browser)
|
|
12
|
+
|
|
13
|
+
Cause: default-deny rules doing their job. The deny-all rules are
|
|
14
|
+
correct for a worker-mediated app.
|
|
15
|
+
Fix: route the access through the worker's `/api/*` (the app key
|
|
16
|
+
bypasses rules). Do NOT widen a rule to make the error disappear —
|
|
17
|
+
any rules change is a human checkpoint.
|
|
18
|
+
|
|
19
|
+
## Data appearing in the wrong place / writes not visible
|
|
20
|
+
|
|
21
|
+
Cause: tenant confusion — the code ran against `<appId>` (prod) instead
|
|
22
|
+
of `<appId>--dev`, or vice versa. Typical trigger: `wrangler deploy`
|
|
23
|
+
without `--env dev`, or `wrangler dev` picking up top-level vars.
|
|
24
|
+
Fix: check `ODLA_TENANT`/`ODLA_ENV` in the relevant `wrangler.jsonc`
|
|
25
|
+
env block and which deploy command ran. `npx odla-ai smoke --env dev`
|
|
26
|
+
prints what it verified against.
|
|
27
|
+
|
|
28
|
+
## Provision fails with an auth/token error
|
|
29
|
+
|
|
30
|
+
Cause: the `odla_dev_…` token expired (~24h) or the handshake was never
|
|
31
|
+
approved.
|
|
32
|
+
Fix: re-run `npx odla-ai provision` — it starts a fresh handshake; the
|
|
33
|
+
human approves the new code at the printed URL. Use `--no-open` in
|
|
34
|
+
non-interactive shells if the browser launch misbehaves.
|
|
35
|
+
|
|
36
|
+
## `smoke` fails: missing credentials
|
|
37
|
+
|
|
38
|
+
Cause: `.odla/credentials.local.json` absent, for a different app id,
|
|
39
|
+
or lacking a db key for the requested env.
|
|
40
|
+
Fix: run `npx odla-ai provision` for the configured envs first; confirm
|
|
41
|
+
`envs` in odla.config.mjs includes the env you're smoking.
|
|
42
|
+
|
|
43
|
+
## `smoke` fails: schema mismatch
|
|
44
|
+
|
|
45
|
+
Cause: local `src/odla/schema.mjs` changed since the last push.
|
|
46
|
+
Fix: re-run `npx odla-ai provision` (schema re-push is the migration
|
|
47
|
+
mechanism; additive changes are safe on live tenants), then re-run
|
|
48
|
+
smoke.
|
|
49
|
+
|
|
50
|
+
## Clerk-verified requests return no email
|
|
51
|
+
|
|
52
|
+
Cause: the Clerk session token doesn't include an email claim.
|
|
53
|
+
Fix: Clerk dashboard → session token customization → add the email
|
|
54
|
+
claim, then re-test. The worker only forwards what the JWT carries.
|
|
55
|
+
|
|
56
|
+
## Re-running provision — is it safe?
|
|
57
|
+
|
|
58
|
+
Yes: re-runs are idempotent (existing app registration and db keys are
|
|
59
|
+
reused; schema/rules re-push). The only destructive flag is
|
|
60
|
+
`--rotate-keys`, which mints new db keys — deployed workers keep
|
|
61
|
+
working only after you push the new key to them. Use it only on
|
|
62
|
+
explicit human request (e.g. a burned key).
|
|
63
|
+
|
|
64
|
+
## Something not covered here
|
|
65
|
+
|
|
66
|
+
Check, in order: `npx odla-ai doctor` output; the field notes in
|
|
67
|
+
https://odla.ai/llms.txt; the relevant
|
|
68
|
+
`node_modules/@odla-ai/*/llms.txt`. Do not improvise around a safety
|
|
69
|
+
rule to unblock yourself — surface the blocker to the human instead.
|