@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/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.1.0)
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.1.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.