@odla-ai/cli 0.4.0 → 0.5.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.
@@ -6,6 +6,9 @@ description: >
6
6
  wants to create/scaffold an odla app, add odla-db/ai/auth/o11y to a repo, or
7
7
  "get started with odla". For moving an existing static or GitHub Pages site to
8
8
  odla, use the odla-migrate skill instead.
9
+ runbookOrder:
10
+ - references/build.md
11
+ - references/sdks.md
9
12
  ---
10
13
 
11
14
  # odla
@@ -34,32 +37,56 @@ State which path you're taking and what you'll build in one line; get a nod.
34
37
  `.odla/credentials.local.json`, or `.odla/dev-token.json` — use `ls -l` to
35
38
  confirm they exist (and are `0600`). The only value a human ever pastes is a
36
39
  Clerk **publishable** key (`pk_…`) — public by design.
37
- 2. **Dev before prod.** Keep `envs: ["dev"]` (or `["dev","prod"]`) and verify the
38
- tenant (`<appId>--dev` in dev) before any write or deploy. The first prod
39
- deploy and `secrets push --env prod --yes` are human checkpoints.
40
+ 2. **Dev before prod.** Keep `envs: ["dev"]` and verify the tenant
41
+ (`<appId>--dev`) before any write or deploy. Add `prod` only at the explicit
42
+ production checkpoint. The first prod
43
+ `provision --dry-run` review, `provision --yes --push-secrets`, and deploy
44
+ are human checkpoints.
40
45
  3. **Rules are default-deny.** A namespace with no rule is invisible; a write
41
46
  with no rule is rejected. Never widen a rule just to clear a 403 — edit
42
47
  `src/odla/rules.mjs` deliberately, and flag rule changes to the human.
43
48
  4. Never `git add -A` without reading `git status` first. Never run
44
- `provision --rotate-keys` unless the human explicitly asks.
49
+ `provision --rotate-keys` or `provision --rotate-o11y-token` unless the human
50
+ explicitly asks.
45
51
 
46
52
  ## The flow
47
53
 
48
54
  You reached this skill because the human ran `npx @odla-ai/cli setup`. Then:
49
55
 
56
+ First run `npx @odla-ai/cli capabilities --json`; use that contract instead of
57
+ guessing which platform/credential steps need manual work. Then:
58
+
50
59
  1. **init** — `npx @odla-ai/cli init --app-id <id> --name "<Name>"` scaffolds
51
60
  `odla.config.mjs`, `src/odla/schema.mjs`, `src/odla/rules.mjs` (deny-all).
52
- 2. **provision** `npx odla-ai provision --write-dev-vars`. A device code
53
- prints; the human approves it at https://odla.ai. It creates the app, enables
54
- services, mints keys (db key + o11y ingest token if enabled), pushes schema +
55
- rules, and writes `.dev.vars`.
56
- 3. **build** — install the SDKs and write the app (`references/sdks.md`).
61
+ 2. **build the Worker shell** — install the SDKs, write the app, and create its
62
+ Wrangler config (`references/sdks.md`). With o11y, add `nodejs_compat` and
63
+ `withObservability`. The CLI will refuse secret delivery until the Wrangler
64
+ target exists.
65
+ 3. **provision**
66
+ `npx @odla-ai/cli provision --write-dev-vars --push-secrets`. A device code
67
+ prints; the human approves it at https://odla.ai/studio. It creates the app,
68
+ enables services, issues or reuses configured credentials (db key + o11y
69
+ ingest token when enabled), pushes schema + rules, writes `.dev.vars`, and
70
+ transfers Worker secrets through Wrangler stdin.
57
71
  4. **run** — `npx wrangler dev` (auto-loads `.dev.vars`); verify locally.
58
- 5. **ship** `npx odla-ai secrets push --env prod --yes`, then
72
+ 5. **security** — run the passive `@odla-ai/security` odla profile;
73
+ inspect every lead and keep critical candidate gating enabled.
74
+ 6. **ship** ⏸ — after adding `prod`, run `npx @odla-ai/cli provision --dry-run`,
75
+ show the human, then `npx @odla-ai/cli provision --yes --push-secrets` and
59
76
  `npx wrangler deploy`.
60
77
 
61
- `npx odla-ai doctor` is an offline config check anytime; `smoke --env dev`
62
- verifies a live deployment.
78
+ `npx @odla-ai/cli doctor` is an offline config check anytime;
79
+ `npx @odla-ai/cli smoke --env dev` verifies a live deployment.
80
+
81
+ The CLI owns deterministic platform work from `odla.config.mjs`: service
82
+ enablement, credential issuance/storage, `.dev.vars`, and Wrangler secret
83
+ transfer. You own source changes — install `@odla-ai/o11y`, add
84
+ `withObservability`, create the Wrangler config, and choose application-specific
85
+ signals. The human owns device approval, production consent, and explicit
86
+ destructive rotation. If an
87
+ o11y token must be replaced, use
88
+ `provision --rotate-o11y-token --push-secrets` so the cached and deployed values
89
+ move together; Studio rotation is manual recovery only.
63
90
 
64
91
  ## Checkpoint protocol
65
92
 
@@ -15,24 +15,40 @@ deny-all rules from the schema. To turn on auth/observability, add `"o11y"` to
15
15
  `services` and an `auth.clerk` block in `odla.config.mjs` (init leaves it
16
16
  commented).
17
17
 
18
- ## 2. Provision ⏸ device-code approval
18
+ ## 2. Build the Worker shell
19
+
20
+ `npm i` only the SDKs you need and use the local `references/sdks.md` as the
21
+ integration map. Create the Worker entrypoint and `wrangler.jsonc` before any
22
+ deployed-secret transfer. With o11y, install `@odla-ai/o11y`, add
23
+ `"nodejs_compat"` to `compatibility_flags`, and wrap the entrypoint with
24
+ `withObservability`.
25
+
26
+ Each installed package's `node_modules/@odla-ai/<pkg>/llms.txt` is the full API
27
+ contract. The npm artifacts contain everything required for this flow; private
28
+ service source or online documentation is not setup context.
29
+
30
+ ## 3. Provision ⏸ device-code approval
19
31
 
20
32
  ```
21
- npx odla-ai provision --write-dev-vars
33
+ npx @odla-ai/cli provision --write-dev-vars --push-secrets
22
34
  ```
23
35
 
24
- Prints a short device code and a link. ⏸ The human opens https://odla.ai, signs
36
+ Prints a short device code and a link. ⏸ The human opens https://odla.ai/studio, signs
25
37
  in, and approves the code — no secret passes through the chat. Provision then:
26
- creates the app, enables its services, mints the db key (and the o11y ingest
27
- token when o11y is enabled), pushes schema + rules, and writes `.dev.vars`
28
- (`0600`, gitignored). Verify with `npx odla-ai doctor` it prints the app,
29
- envs, services, and flags anything unset.
30
-
31
- ## 3. Build
32
-
33
- `npm i` only the SDKs you need (see `references/sdks.md`) and write the app. Read
34
- each package's `node_modules/@odla-ai/<pkg>/llms.txt` for its full API. The
35
- `examples/saas` app in the odla-ai repo is a complete realtime app to mirror.
38
+ creates the app, enables its services, issues or reuses the db key (and the
39
+ o11y ingest token when o11y is enabled), pushes schema + rules, writes
40
+ `.dev.vars`, and transfers configured Worker secrets through Wrangler stdin.
41
+ Local credential files are `0600` and gitignored. Verify with
42
+ `npx @odla-ai/cli doctor` — it prints the app, envs, services, and flags
43
+ anything unset. `--push-secrets` preflights the Wrangler config and login before
44
+ issuing or rotating a shown-once credential.
45
+
46
+ The CLI stops at the source boundary: it verifies but does not invent your
47
+ application semantics. Do not use Studio to mint a routine o11y token. Manual
48
+ Studio rotation is recovery-only;
49
+ normal replacement is
50
+ `npx @odla-ai/cli provision --rotate-o11y-token --push-secrets` after explicit
51
+ human approval.
36
52
 
37
53
  ## 4. Run locally
38
54
 
@@ -45,14 +61,33 @@ means the namespace has no rule yet — add one in `src/odla/rules.mjs`
45
61
  (deliberately; e.g. `{ notes: { view: "auth.signedIn", create: "auth.signedIn" } }`),
46
62
  re-provision to push it, and retry.
47
63
 
48
- ## 5. Ship ⏸ human checkpoint
64
+ ## 5. Security preflight
65
+
66
+ Install the passive harness and scan before any production secret or deploy:
67
+
68
+ Before installing the exact release, first verify
69
+ `npm view @odla-ai/security@0.1.0 version`. An exact-version `E404` means the
70
+ release is unavailable and blocks this preflight; it is not a clean scan and
71
+ does not prove the package name is absent.
72
+
73
+ ```
74
+ npm i -D --save-exact @odla-ai/security@0.1.0
75
+ npx odla-security scan . --profile odla --out .odla/security/pre-ship --fail-on high --fail-on-candidates critical
76
+ ```
77
+
78
+ Read `.odla/security/pre-ship/REPORT.md`. The CLI makes no model calls and does
79
+ not execute target code. A `candidate` is a review lead, not a confirmed
80
+ vulnerability; do not suppress one merely to clear the gate. Baseline entries
81
+ need a fingerprint, concrete reason, owner, and expiry.
82
+
83
+ ## 6. Ship ⏸ human checkpoint
49
84
 
50
85
  Add `"prod"` to `envs`, provision again (prod tenant = the bare `appId`), then:
51
86
 
52
87
  ```
53
- npx odla-ai secrets push --env prod --yes # pushes ODLA_API_KEY (+ ODLA_O11Y_TOKEN if o11y)
88
+ npx @odla-ai/cli provision --yes --push-secrets # prod mutation + configured Worker secrets
54
89
  npx wrangler deploy
55
90
  ```
56
91
 
57
- Verify with `npx odla-ai smoke --env prod`. Point env vars at the service custom
58
- domains, never `*.workers.dev` (Workers can't fetch same-account workers.dev).
92
+ Verify with `npx @odla-ai/cli smoke --env prod`. Point env vars at the service
93
+ custom domains, never `*.workers.dev` (Workers can't fetch same-account workers.dev).
@@ -49,9 +49,39 @@ count("http.requests", 1, { "http.route": path }); // metrics
49
49
  recordError(err, { route, code }); // structured errors
50
50
  ```
51
51
 
52
- Set `ODLA_O11Y_ENDPOINT` + `ODLA_O11Y_SERVICE` (vars) and `ODLA_O11Y_TOKEN`
53
- (secret) `provision` mints the token when `o11y` is in `services`. First-party
54
- hosting binds `ODLA_O11Y_COLLECTOR` instead (no endpoint/token).
52
+ When `o11y` is in `services`, run
53
+ `npx @odla-ai/cli provision --write-dev-vars --push-secrets`. The CLI enables
54
+ the service, issues or reuses its token, persists it locally, writes
55
+ `.dev.vars`, and transfers `ODLA_O11Y_TOKEN` to the Worker over Wrangler stdin.
56
+ With a token present, the SDK defaults `ODLA_O11Y_ENDPOINT` to
57
+ `https://o11y.odla.ai` and the service label to
58
+ `ODLA_O11Y_SERVICE ?? ODLA_APP_ID`; both remain overrides. First-party hosting
59
+ binds `ODLA_O11Y_COLLECTOR` instead (no public endpoint/token).
60
+
61
+ Source instrumentation remains your job: install the package, wrap with
62
+ `withObservability`, and select useful signals. Never rotate through Studio as
63
+ routine setup. On explicit human request, use
64
+ `provision --rotate-o11y-token --push-secrets` so local and deployed values stay
65
+ in sync.
66
+
67
+ ## @odla-ai/security — pre-ship vulnerability harness
68
+
69
+ Run the passive CLI in local/CI development; do not import it into the running
70
+ Worker:
71
+
72
+ Before installing the exact release, require
73
+ `npm view @odla-ai/security@0.1.0 version` to succeed before installing it. An
74
+ exact-version `E404` means the release is unavailable, not that the preflight
75
+ passed, and does not prove the package name is absent.
76
+
77
+ ```
78
+ npm i -D --save-exact @odla-ai/security@0.1.0
79
+ npx odla-security scan . --profile odla --out .odla/security/pre-ship --fail-on high --fail-on-candidates critical
80
+ ```
81
+
82
+ Deterministic rules emit leads. Autonomous recon/hunt/validation is an optional
83
+ library flow with separate models, persistent state, and an explicitly injected
84
+ isolated executor; it never falls back to the host shell.
55
85
 
56
86
  ## Others
57
87
 
@@ -5,6 +5,16 @@ description: >
5
5
  phases, then add a database, Clerk login, and AI. Use when the user wants to
6
6
  move a static or GitHub Pages site to Cloudflare/odla, or to add a backend,
7
7
  database, login/auth, or AI features to a static site via odla.
8
+ runbookOrder:
9
+ - references/phase-0-preflight.md
10
+ - references/phase-1-static.md
11
+ - references/phase-2-db.md
12
+ - references/phase-3-auth.md
13
+ - references/phase-3b-user-sync.md
14
+ - references/phase-4-ai.md
15
+ - references/phase-5-cutover.md
16
+ - references/troubleshooting.md
17
+ - references/secrets-map.md
8
18
  ---
9
19
 
10
20
  # odla-migrate
@@ -15,8 +25,8 @@ handshake approval, DNS). You do everything else.
15
25
 
16
26
  ## When NOT to use this skill
17
27
 
18
- Greenfield odla apps (no existing site to migrate): follow the "Typical
19
- agent flow" in https://odla.ai/llms.txt instead.
28
+ Greenfield odla apps (no existing site to migrate): read and follow the sibling
29
+ installed skill at `../odla/SKILL.md` instead.
20
30
 
21
31
  ## Non-negotiable rules
22
32
 
@@ -30,7 +40,11 @@ agent flow" in https://odla.ai/llms.txt instead.
30
40
  4. Never `git add -A` without reading `git status` first.
31
41
  5. Never widen a db rule to silence a 403 — default-deny is the design.
32
42
  Any rules change is a human checkpoint.
33
- 6. Never run `provision --rotate-keys` unless the human explicitly asks.
43
+ 6. Never run `provision --rotate-keys` or
44
+ `provision --rotate-o11y-token` unless the human explicitly asks. Pair any
45
+ approved o11y rotation with `--push-secrets` so the Worker is updated in the
46
+ same run. If the final Wrangler transfer fails, use the CLI's printed
47
+ non-rotating `secrets push` retry; never rotate again just to retry delivery.
34
48
 
35
49
  ## Phase state machine
36
50
 
@@ -42,7 +56,14 @@ Phases run strictly in order; each has a verification gate:
42
56
  `MIGRATION.md` at the user's repo root is the durable state: create it in
43
57
  Phase 0, update it at every gate (phase, what changed, what was verified).
44
58
  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.
59
+ phase; `npx @odla-ai/cli doctor` confirms the current config state.
60
+
61
+ ## Auth model (one line)
62
+
63
+ Clerk is the **source of truth** for users; odla-db keeps a mirror in the
64
+ app's reserved `$users` namespace (Phase 3 ships login; Phase 3b enables the
65
+ mirror). That per-app `$users` is distinct from odla.ai's own operator
66
+ access list — don't conflate them.
46
67
 
47
68
  ## Checkpoint protocol (every phase boundary)
48
69
 
@@ -52,18 +73,21 @@ phase. If the CLI offers `odla-ai status`, run it to confirm.
52
73
  approve, value to paste, command to run themselves).
53
74
  3. Wait for explicit approval before entering the next phase.
54
75
 
55
- At the very first checkpoint, have the human sign in at https://odla.ai and
76
+ At the very first checkpoint, have the human sign in at https://odla.ai/studio and
56
77
  open Docs → "Moving your site to odla" — it explains their side of the whole
57
78
  journey in plain language. They need an odla account for the migration anyway,
58
79
  so this is a natural first step.
59
80
 
60
81
  ## Verification tools
61
82
 
62
- - `npx odla-ai doctor` — offline config/schema/rules validation; run
83
+ - `npx @odla-ai/cli capabilities --json` — authoritative automation boundary;
84
+ run once when orienting so source edits and human checkpoints are not confused
85
+ with CLI-owned platform/credential work.
86
+ - `npx @odla-ai/cli doctor` — offline config/schema/rules validation; run
63
87
  after any config edit.
64
- - `npx odla-ai provision --dry-run` — the plan, zero network/file I/O;
88
+ - `npx @odla-ai/cli provision --dry-run` — the plan, zero network/file I/O;
65
89
  show it to the human before the first real provision.
66
- - `npx odla-ai smoke --env dev` — live, read-only: public-config, live
90
+ - `npx @odla-ai/cli smoke --env dev` — live, read-only: public-config, live
67
91
  schema diff, a count aggregate. Run after every provision.
68
92
  - `wrangler dev` + curl — exercise routes locally before deploying.
69
93
  - Parity curls — compare the old and new site on representative paths
@@ -77,6 +101,7 @@ Read the current phase's file when you enter it — not before:
77
101
  - references/phase-1-static.md
78
102
  - references/phase-2-db.md
79
103
  - references/phase-3-auth.md
104
+ - references/phase-3b-user-sync.md (optional: mirror Clerk users into $users)
80
105
  - references/phase-4-ai.md
81
106
  - references/phase-5-cutover.md
82
107
 
@@ -84,8 +109,8 @@ On any failure, read references/troubleshooting.md before improvising.
84
109
 
85
110
  ## Context bootstrap
86
111
 
87
- Before Phase 1, fetch https://odla.ai/llms.txt and
88
- https://odla.ai/llms-migrate.txt (the served copy of this procedure's
89
- reference text). After `npm install`, prefer
90
- `node_modules/@odla-ai/*/llms.txt` over training memory for any
91
- @odla-ai package the packages are newer than you think.
112
+ This installed skill and its `references/` directory are the complete migration
113
+ runbook; do not fetch a website or private source repository to reconstruct it.
114
+ Read only the current phase file as directed above. After `npm install`, prefer
115
+ `node_modules/@odla-ai/*/llms.txt` over training memory for package APIs — the
116
+ installed artifact is the version this project actually uses.
@@ -16,8 +16,6 @@ Human obligation: none.
16
16
  `.github/workflows/*` using deploy-pages.
17
17
  - Dynamic wishes: forms posting to third parties, `mailto:` contact,
18
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
19
  2. Ensure the build outputs to a DEDICATED directory (`dist/`, `_site/`,
22
20
  `build/`). If the site is served from the repo root, restructure so a
23
21
  build step (even a copy script) produces a clean output dir first.
@@ -10,9 +10,7 @@ in this session).
10
10
  ## Steps
11
11
 
12
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:
13
+ 2. Write `wrangler.jsonc` by hand using this documented shape:
16
14
  - `name`: kebab-case app id; `main`: `src/worker.ts`
17
15
  - `compatibility_date` (today), `compatibility_flags: ["nodejs_compat"]`
18
16
  - `assets: { "directory": "<buildDir>", "binding": "ASSETS",
@@ -45,4 +43,4 @@ Rollback: delete the dev worker in the Cloudflare dashboard. Pages was
45
43
  never touched.
46
44
 
47
45
  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).
46
+ obligation: sign in at https://odla.ai/studio and approve a handshake code).
@@ -4,15 +4,22 @@ Goal: the app registered on the platform, a dev odla-db tenant with
4
4
  schema + deny-all rules, the db key in the dev worker, and first
5
5
  `/api/*` routes live in dev.
6
6
 
7
- Human obligation: sign in at https://odla.ai and approve the handshake
7
+ Human obligation: sign in at https://odla.ai/studio and approve the handshake
8
8
  code when the provision run prints it (it also opens the approval page
9
9
  in the browser in interactive terminals).
10
10
 
11
11
  ## Steps
12
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.
13
+ 1. Require `npm view @odla-ai/cli@0.5.0 version` to succeed, then run
14
+ `npm i -D --save-exact @odla-ai/cli@0.5.0` and `npm i @odla-ai/db`.
15
+ 2. `npx @odla-ai/cli init --app-id <id> --name "<Name>" --env dev --services db`
16
+ Review `odla.config.mjs`. Keep `envs: ["dev"]` — prod is Phase 5. Set
17
+ `links.dev` to the URL the Phase 1 `wrangler deploy` **actually printed** —
18
+ copy it from that output and `curl` it (200) before pasting. NEVER predict or
19
+ remember a workers.dev URL; the worker name and account subdomain must match
20
+ exactly or Studio links to a dead URL. Provision records it so Studio and
21
+ public-config show where the app runs. (`links.prod` is set the same way at
22
+ Phase 5, from the prod deploy's printed URL.)
16
23
  3. STOP before touching schema: read "Porting relational code" in
17
24
  `node_modules/@odla-ai/db/llms.txt`. The traps are silent: entity ids
18
25
  are not attrs (mirror an id attr), there is no NULL (omit on write,
@@ -23,16 +30,15 @@ in the browser in interactive terminals).
23
30
  generated deny-all `src/odla/rules.mjs`: the worker mediates all
24
31
  access with its app key (which bypasses rules); browsers get nothing
25
32
  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).
33
+ 5. `npx @odla-ai/cli doctor` until clean.
34
+ 6. `npx @odla-ai/cli provision --dry-run` — show the plan to the human.
35
+ 7. `npx @odla-ai/cli provision --write-dev-vars --push-secrets` — handshake,
36
+ app registration, dev db key, schema + rules push; writes
37
+ `.odla/credentials.local.json` (0600) and `.dev.vars`, then pipes configured
38
+ Worker secrets to Wrangler over stdin. Both local files are gitignored by
39
+ init confirm with `git status`. Use
40
+ `npx @odla-ai/cli secrets push --env dev` only to retry the secret-transfer
41
+ portion (details and manual fallback: references/secrets-map.md).
36
42
  9. Add to `wrangler.jsonc` `env.dev.vars`: `ODLA_ENDPOINT`
37
43
  ("https://db.odla.ai"), `ODLA_TENANT` ("<appId>--dev"),
38
44
  `ODLA_PLATFORM` ("https://odla.ai"), `ODLA_APP_ID`, `ODLA_ENV`
@@ -40,15 +46,17 @@ in the browser in interactive terminals).
40
46
  Phase 5. These are vars, not secrets.
41
47
  10. Add `/api/*` routes before the assets fall-through, using
42
48
  `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`.
49
+ endpoint: env.ODLA_ENDPOINT })` from `@odla-ai/db`. Read the installed
50
+ package's `node_modules/@odla-ai/db/llms.txt` for query, transaction, and
51
+ error contracts.
45
52
  11. `wrangler dev` + curl each route; then `npm run deploy:app:dev` and
46
53
  curl the workers.dev URL.
47
54
 
48
55
  ## Verification checklist
49
56
 
50
- - [ ] `npx odla-ai smoke --env dev` passes (public-config, schema, aggregate)
57
+ - [ ] `npx @odla-ai/cli smoke --env dev` passes (public-config, schema, aggregate)
51
58
  - [ ] Routes work locally and on the deployed dev worker
59
+ - [ ] `links.dev` set — public-config carries the URL and Studio shows it
52
60
  - [ ] `git status` shows no credential files staged
53
61
  - [ ] MIGRATION.md updated with tenant id + route list
54
62
 
@@ -3,31 +3,48 @@
3
3
  Goal: Clerk sign-in on the site, the worker verifying session JWTs
4
4
  itself, at least one route gated. Dev only.
5
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_…`.
6
+ Human obligation: **run `npx clerk auth login`** a one-time browser
7
+ OAuth into their Clerk account. This is the ONE interactive step and the
8
+ whole reason to surface it early: the agent cannot authenticate as the
9
+ user. After login, credentials are stored locally and the agent drives
10
+ the rest (`apps create`, `link`, `env pull`, `config patch`) from Bash.
11
+ There is no `pk_` to hand-paste — the CLI pulls it. (Never handle `sk_…`
12
+ or `whsec_…` in client mode; `env pull` writes an `sk_` into a gitignored
13
+ `.env.local` that stays local and unused.)
14
+
15
+ Use the Clerk CLI (`npx clerk`) to create the instance rather than the
16
+ dashboard. Do NOT assume an app exists: a stale publishable key can linger
17
+ on the registry's app_auth record from a deleted/recreated app —
18
+ `clerk apps list` is the source of truth for what's real.
10
19
 
11
20
  ## Steps
12
21
 
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`):
22
+ 1. Provision the Clerk app with the CLI (after the human's `clerk auth
23
+ login`):
24
+ - `npx clerk apps list` confirm what exists (catch stale/leftover
25
+ instances); create if absent.
26
+ - `npx clerk apps create "<App Name>"` creates the app + dev
27
+ instance (`pk_test_…`).
28
+ - `npx clerk link` (or `--app <id>`), then `npx clerk env pull` — pull
29
+ the dev `pk_test_…` into `.env.local`. Take only the publishable key.
30
+ 2. Add the key to `odla.config.mjs`: `auth: { clerk: { dev: "<pk_test_…>" } }`
31
+ (inline is fine — it's public), then `npx @odla-ai/cli provision`
32
+ (idempotent) — calls setAuth for the dev env; the issuer is derived
33
+ from the key. **provision OVERWRITES any stale key** on public-config.
34
+ The worker fetches auth config from public-config at runtime, so a key
35
+ change propagates without a redeploy.
36
+ 3. `npm i jose`. In the worker, use this documented verification pattern:
23
37
  - fetch `<ODLA_PLATFORM>/registry/apps/<ODLA_APP_ID>/public-config?env=<ODLA_ENV>`,
24
38
  cache ~5 min per isolate
25
39
  - `createRemoteJWKSet(new URL(issuer + "/.well-known/jwks.json"))`,
26
40
  cached per issuer
27
41
  - `jwtVerify(token, jwks, { issuer })` on the `Authorization: Bearer`
28
42
  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
43
+ - ensure the Clerk session token includes an email claim if routes
44
+ need email: try `npx clerk config schema --keys session` then
45
+ `npx clerk config patch --json '{…}'`; else set it in the dashboard
46
+ (Sessions → customize session token) to
47
+ `{"email": "{{user.primary_email_address}}"}`
31
48
  4. Gate at least one `/api/*` route on a verified user; return 401
32
49
  otherwise.
33
50
  5. Add Clerk sign-in to the site pages. For a React/Preact site, use
@@ -36,17 +53,86 @@ they will ever paste into chat. Never ask for `sk_…` or `whsec_…`.
36
53
  clerk-js (no clerk-react), loads clerk-js as a lazy chunk, and themes
37
54
  to odla-ui via `clerkAppearanceFromTokens()`. Otherwise drive ClerkJS
38
55
  directly with the publishable key from public-config, or use Clerk's
39
- hosted pages.
56
+ hosted pages. For a **vanilla/static site**, load Clerk's browser bundle
57
+ with the key on the tag — it hot-loads `window.Clerk` as a ready
58
+ *instance* (not a constructor): `<script src="https://<frontend-api>/
59
+ npm/@clerk/clerk-js@5/dist/clerk.browser.js" data-clerk-publishable-key=
60
+ "<pk>">` then `await window.Clerk.load()`. The `<frontend-api>` is the
61
+ host encoded in the pk. Then mount `Clerk.mountSignIn(el, { appearance })`
62
+ and attach `Clerk.session.getToken()` as the `Bearer` on `/api/*` calls.
40
63
  6. Deploy dev (`npm run deploy:app:dev`).
41
64
 
42
- Auth mode "full" (webhook-synced $users via svix, whsec in the tenant
43
- vault) is a later upgrade not needed to ship login.
65
+ Clerk is the **source of truth** for identity; odla-db keeps a mirror in
66
+ `$users`. Shipping login (this phase) is enough to gate routes. Mirroring
67
+ users into `$users` (svix webhook, "full" mode) is a **separate, optional
68
+ next step — see phase-3b-user-sync.md** — not needed to ship login.
69
+
70
+ ## Shape the instance with the Clerk CLI (optional, for invite-only sites)
71
+
72
+ The same `clerk` CLI configures the instance as code (per app, from any
73
+ directory with `--app <id>`) — no dashboard clicking. For a members-only
74
+ site you typically want email-only + no social SSO + an allowlist:
75
+
76
+ - Inspect: `npx clerk config schema --keys session auth_email auth_password
77
+ auth_access_control` ; pull current with `npx clerk config pull`.
78
+ - Apply a partial patch (dry-run first with `--dry-run`):
79
+ `npx clerk config patch --json '{"auth_password":{"enabled":false},
80
+ "connection_oauth_google":{"enabled":false},
81
+ "auth_access_control":{"allowlist_enabled":true,
82
+ "allowlist_blocklist_enforced_on_sign_in":true}}'`
83
+ (email-code sign-in stays on via `auth_email`). A committed
84
+ `clerk-config.json` + `clerk config patch --file` keeps it reproducible and
85
+ reusable across the dev and prod instances.
86
+ - Invite-gate sign-ups by adding allowed emails to the allowlist:
87
+ `npx clerk api /allowlist_identifiers -X POST -d
88
+ '{"identifier":"<email>","notify":false}'` (per app with `--app <id>`).
89
+ - The email session-token claim (Phase 3 step 3) is just another patch:
90
+ `clerk config patch --json '{"session":{"claims":{"email":
91
+ "{{user.primary_email_address}}"}}}'`.
92
+
93
+ ## Match the site's existing look and feel (do NOT invent a style)
94
+
95
+ Every visible element you add — the sign-in gate, any nav/top bar, an
96
+ admin console, cross-links — MUST look like it was always part of the
97
+ site. A bolted-on or default-styled element is a defect even if it
98
+ works. Before writing any markup:
99
+
100
+ 1. **Read the site's design system first.** Find its stylesheet(s) and
101
+ shared components (header/nav/footer). Note the exact tokens: fonts
102
+ (heading vs body), color palette, logo/wordmark markup, spacing,
103
+ button styles, radii. Reuse them verbatim — same classes, or copy the
104
+ exact values. Never approximate; "close enough" reads as wrong (a
105
+ 32px logo next to the real 36px one, the accent colour off by a shade,
106
+ a heading in the body font — all instantly visible).
107
+ 2. **Reuse existing components.** If the site has a `<site-header>`/nav
108
+ component, extend or replicate it exactly rather than inventing a new
109
+ bar. New surfaces (admin, member area) should share ONE bar with the
110
+ rest of the site — identical logo size, wordmark size, padding, max
111
+ width.
112
+ 3. **Theme third-party widgets to the brand.** A vendor's default chrome
113
+ (Clerk's card + purple user-button avatar, Stripe's default inputs)
114
+ is off-brand by definition. Use its theming API — Clerk `appearance`:
115
+ strip the card to blend into a host card, set `colorPrimary`/fonts/
116
+ radii to the site tokens, hide redundant headers, `overflow:visible`
117
+ so it can't clip its own labels. Don't ship a raw vendor widget, and
118
+ don't show an off-brand account avatar when a plain "Sign out" fits.
119
+ 4. **Bump the cache-bust version** (`?v=`) on any shared asset you change
120
+ (stylesheet, header script), on every page that embeds it — otherwise
121
+ cached visitors keep the old copy and "your fix didn't deploy".
122
+ 5. **You cannot see the render — so never claim a visual match.** State
123
+ what you changed and ask the human to confirm; expect a round or two
124
+ of "the logo's too small / it clips / that colour is wrong" and treat
125
+ that as the normal loop, not something to argue with.
44
126
 
45
127
  ## Verification checklist
46
128
 
47
129
  - [ ] Unauthenticated curl to the gated route → 401
48
130
  - [ ] Signed-in browser session reaches the gated route
49
- - [ ] `npx odla-ai smoke --env dev` still passes
131
+ - [ ] `npx @odla-ai/cli smoke --env dev` still passes
132
+ - [ ] Every added UI element matches the site's existing look (fonts,
133
+ palette, logo, spacing) — human-confirmed, not self-asserted
134
+ - [ ] Post-login redirect lands on a real URL (mind trailing-slash /
135
+ directory-index routing), not a 404
50
136
  - [ ] MIGRATION.md updated (gated routes, Clerk instance name)
51
137
 
52
138
  Rollback: remove the auth layer / ungate the route; nothing outside dev
@@ -0,0 +1,57 @@
1
+ # Phase 3b — Mirror Clerk users into odla-db (`$users`)
2
+
3
+ Optional, and staged: ship login first (Phase 3), then turn this on. **Clerk is
4
+ the source of truth for identity.** odla-db keeps a *mirror* of your users in the
5
+ reserved `$users` namespace so rules and app data can reference them. You do not
6
+ build the mirror yourself — the platform does it. This phase enables it.
7
+
8
+ Goal: signed-in users appear as rows in the app's `$users`, kept in sync with
9
+ Clerk (create/update/delete). Dev first, then prod.
10
+
11
+ Human obligation: create a Clerk webhook endpoint and paste its signing secret
12
+ (`whsec_…`) into Studio (write-only). That plus the Phase-3 `clerk auth login`
13
+ are the only human Clerk steps.
14
+
15
+ ## How `$users` fills — two paths
16
+
17
+ 1. **Browser talks to odla-db directly with its Clerk session token.** The
18
+ platform verifies the JWT and auto-creates a basic `$users` row (`id` = the
19
+ Clerk `sub`). No setup. This only happens for *user-token* access.
20
+ 2. **The Clerk webhook (full profile).** Clerk pushes `user.created/updated/
21
+ deleted` to odla-db, which upserts the full profile (`email`, `name`,
22
+ `imageUrl`) and tombstones on delete. Works for **any** app.
23
+
24
+ **If your app is worker-mediated** (the worker holds the app's admin/scoped key
25
+ and calls odla-db itself — the Phase-2/3 pattern), that scoped credential is
26
+ **not projected into `$users`**. So the webhook (path 2) is how you get `$users`.
27
+ Enable it here.
28
+
29
+ ## Enable the webhook
30
+
31
+ 1. **Create the endpoint in Clerk** (per instance — dev instance → dev tenant;
32
+ prod instance → prod tenant). URL:
33
+ `https://db.odla.ai/webhooks/clerk/<ODLA_TENANT>`
34
+ (the odla tenant, e.g. `<appId>--dev` in dev, `<appId>` in prod). Subscribe to
35
+ `user.created`, `user.updated`, `user.deleted`. Clerk returns a signing secret
36
+ `whsec_…`. Prefer the Clerk CLI if it exposes webhook creation; otherwise the
37
+ Clerk dashboard → Webhooks → Add Endpoint. (`whsec_` is a real secret — never
38
+ print/commit it; it is the only thing you paste, and it goes into the vault,
39
+ not chat.)
40
+ 2. **Store the secret in the tenant vault** as `clerk_webhook_secret`, write-only,
41
+ via Studio's secret UI (`odla.ai/studio` → the app → the env → secrets). The server
42
+ decrypts it only to verify inbound events (svix HMAC over `id.timestamp.body`).
43
+ 3. **(Optional) `sk_…`** — the webhook payload already carries email/name/image,
44
+ so the basic mirror needs only `whsec_`. Add the Clerk backend key `sk_…` to
45
+ the vault only if you later need to resolve data the webhook doesn't send.
46
+
47
+ ## Verify
48
+
49
+ - Sign in (or edit the user in Clerk) → a `$users` row appears (check Studio, or
50
+ a worker route that runs `db.query({ $users: {} })`).
51
+ - Delete the Clerk user → the row is tombstoned, not hard-deleted.
52
+ - Rules can now gate on it: `$users` is platform-managed (a browser may view its
53
+ own row but cannot write `$users`). Your app's own tables (members, roles,
54
+ status) layer on top, keyed by the Clerk `sub` or `email`.
55
+
56
+ Done when: a real sign-in produces a `$users` row in the dev tenant. Repeat the
57
+ endpoint + secret for the prod instance/tenant at Phase 5.