@mediamonks/canopy-cli 0.7.20 → 0.7.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/{dev-DtEwiNYi.mjs → dev-CNqx_53I.mjs} +3 -3
  2. package/dist/dev-D7v7WK9O.mjs +2 -0
  3. package/dist/{dev-worker-IWnHvQ1f.mjs → dev-worker-Bij9OUMk.mjs} +1 -1
  4. package/dist/docs/cli-dev.md +1 -1
  5. package/dist/docs/cli-logs.md +1 -1
  6. package/dist/docs/data.md +2 -2
  7. package/dist/docs/dev.md +1 -1
  8. package/dist/docs/developers-architecture.md +26 -18
  9. package/dist/docs/index.json +1 -6
  10. package/dist/docs/migrations.md +17 -14
  11. package/dist/docs/platform-egress.md +2 -2
  12. package/dist/docs/platform-project-data.md +3 -3
  13. package/dist/docs/platform-schedules.md +1 -1
  14. package/dist/docs/platform-sites-and-profiles.md +1 -1
  15. package/dist/docs/sdk-files.md +11 -1
  16. package/dist/docs/static-serving.md +58 -300
  17. package/dist/{docs-C2D7__Iz.mjs → docs-fiC6q9Ce.mjs} +1 -1
  18. package/dist/{feedback-BvkCjS-g.mjs → feedback-CAwUHwWa.mjs} +1 -1
  19. package/dist/index.mjs +9 -9
  20. package/dist/{local-CKRZur_5.mjs → local-CsSP4Vpt.mjs} +2 -2
  21. package/dist/login-DRYaJcfT.mjs +285 -0
  22. package/dist/{package-B-aXVcQu.mjs → package-C_DLlB96.mjs} +1 -1
  23. package/dist/{skill-CqiSkac0.mjs → skill-CmgMU2Iq.mjs} +1 -1
  24. package/dist/{src-Bu4Ze32I.mjs → src-BKKAEkbH.mjs} +25 -2
  25. package/dist/templates/spa/package.json +1 -1
  26. package/dist/templates/worker/AGENTS.md +3 -0
  27. package/dist/templates/worker/package.json +1 -1
  28. package/dist/templates/worker/worker/index.ts +20 -3
  29. package/dist/{upgrade-CpUCVTrN.mjs → upgrade-D1j0WV4l.mjs} +1 -1
  30. package/package.json +2 -2
  31. package/dist/dev-DjkK-50R.mjs +0 -2
  32. package/dist/docs/infrastructure.md +0 -406
  33. package/dist/login-D-Ac_kFW.mjs +0 -285
@@ -1,406 +0,0 @@
1
- ---
2
- title: Infrastructure as code
3
- description: Terraform for the account-level Cloudflare pieces Canopy needs.
4
- ---
5
-
6
- Canopy's Cloudflare footprint is small on purpose: almost everything
7
- per-project is provisioned at runtime by canopy-api (`apps/api/src/cf.ts`,
8
- reached through the Cloudflare `PlatformAdapter` in
9
- `apps/api/src/platform/cloudflare.ts`), so the only things worth managing
10
- declaratively are a handful of platform-wide resources. Those live in
11
- [`infra/`](../../infra) as OpenTofu; the rest is bootstrap scripts and this
12
- runbook.
13
-
14
- **Live platform (2026-08-01):** Monks Explore account
15
- `1c826c7b063fd9ca5e1b1bdaabd05feb`, zone `canopy.website`. The steps below
16
- are the verified path used for that move (personal account
17
- `9513cbfa5b182546f737501b10e1ed85` / `thanarie.dev` → company). Re-run them
18
- to stand the platform up in another fresh account, or to rebuild after a
19
- wipe.
20
-
21
- Scope of a fresh stand-up:
22
-
23
- 1. the **platform workers** — `canopy-api` and `canopy-gateway` — deployed and
24
- healthy (these aren't optional; nothing works without them), and
25
- 2. the **four core projects** — `dashboard`, `www`, `docs`, `kitchen-sink` —
26
- re-created and deployed _on_ the platform like any other project.
27
-
28
- Everyone else's projects are **not** carried over — a new account starts with
29
- an empty control-plane registry (see [Non-goals](#non-goals)).
30
-
31
- See decision **D14** in [`02-decisions.md`](../02-decisions.md) for the why.
32
-
33
- ## What's code vs. what's clicked
34
-
35
- **OpenTofu** (`infra/*.tf`): zone, proxied wildcard DNS record, control-plane
36
- D1, R2 bucket, AI Gateway shell, optional ACM cert pack.
37
-
38
- **wrangler** (`apps/{api,gateway}/wrangler.{main,staging}.jsonc`): the two
39
- platform Worker scripts, their bindings, and the `*.<zone>/*` route — one
40
- config file per Cloudflare site (same names as CLI profiles). Deployed with
41
- `pnpm run deploy:main` / `deploy:staging`, not Tofu. Plain `wrangler.jsonc`
42
- is local `wrangler dev` only.
43
-
44
- **`canopy deploy`** (`canopy.json` in each project): the core projects
45
- (`dashboard`, `www`, `docs`, `kitchen-sink`) — ordinary platform projects
46
- that dogfood the product, not infrastructure.
47
-
48
- **Bootstrap scripts** (`infra/scripts/`): runtime API tokens, worker secrets,
49
- AI Gateway BYOK keys + spend rule.
50
-
51
- **Manual / billing (clicked once in the dashboard):** create the account &
52
- attach it to the org; add the domain; mint bootstrap API token(s); optionally
53
- enable paid Workers products. Plus OneLogin: redirect URIs for the new host
54
- (if they aren't already set).
55
-
56
- ## Non-goals
57
-
58
- - **No project-data migration.** Per-project D1s, R2 blobs, and deploy history
59
- stay in the old account. The new control-plane D1 is empty; the core
60
- projects are re-created from scratch (they hold no precious data — `www`,
61
- `dashboard`, and `docs` are stateless, `kitchen-sink` is a demo), and everyone else's
62
- projects are re-created as needed. (If that ever changes, the export/import
63
- primitives already exist in `apps/api/src/cf.ts` and `canopy db snapshot|restore`.)
64
- - **No Tofu management of Workers/secrets/per-project resources** — see the
65
- table in [`infra/README.md`](../../infra/README.md).
66
- - **No per-project Workers on the Cloudflare target today.** The CF
67
- `PlatformAdapter` reports `capabilities.workers = false` (and
68
- schedules/secrets likewise). Project backends, schedules, and project
69
- secrets live on targets that support them (e.g. GCP). Kitchen-sink's
70
- `worker` field in `canopy.json` must be omitted for a CF deploy — see
71
- [step 10](#10-log-in-grant-yourself-admin-then-deploy-the-core-projects).
72
-
73
- ---
74
-
75
- ## Rollout — step by step
76
-
77
- Prereqs on your machine: `tofu` (or `terraform`), `wrangler` (via `pnpm` in
78
- the repo), `curl`, `jq`, `openssl`. Confirm wrangler can see the target
79
- account after `wrangler login`:
80
-
81
- ```bash
82
- pnpm --filter api exec wrangler whoami
83
- # expect the new account listed (e.g. "Monks Explore")
84
- export CLOUDFLARE_ACCOUNT_ID="<account_id>" # required when you belong to >1 account
85
- ```
86
-
87
- ### 1. Account & billing (dashboard)
88
-
89
- 1. Create / choose the new account; attach it to the company org; ensure your
90
- user is a member with rights to manage Workers / D1 / R2 / DNS.
91
- 2. Add `canopy.website` to the account and repoint its nameservers at
92
- Cloudflare. Wait for the zone to go **Active**. Note the **Account ID**
93
- (`tofu` var `account_id`) and the **Zone ID**.
94
- 3. **Delete any auto-created `www` record.** Cloudflare's onboarding scan often
95
- adds a `www` A/CNAME. An explicit record beats the wildcard, and if it's
96
- DNS-only (grey cloud) the request never reaches Workers — which would break
97
- the `www` core project. The wildcard `*` alone must serve `www.canopy.website`.
98
- (You can confirm via API once the bootstrap token exists — an empty zone is
99
- fine.)
100
- 4. **R2** must work (creating a probe bucket is enough to confirm). **Workers
101
- Paid + Workers for Platforms** are required for per-project workers
102
- (Phase 2 item 1, D18): once the subscriptions are active, run
103
- `infra/scripts/create-dispatch-namespace.sh` to create the
104
- `canopy-projects` dispatch namespace (the `dispatch_namespaces` bindings in
105
- `apps/api/wrangler.main.jsonc` and `apps/gateway/wrangler.main.jsonc` fail
106
- to deploy without it), and make sure `CF_API_TOKEN` carries **Workers
107
- Scripts Write** (re-mint with `create-api-tokens.sh` if it predates this).
108
- A site without the binding still runs — workers/schedules/secrets simply
109
- answer 501. **No Advanced Certificate Manager** — `*.canopy.website`
110
- is a single-level wildcard covered by free Universal SSL. (ACM would only
111
- be needed if nesting as `*.canopy.<some-other-domain>`.)
112
-
113
- > **Apex is not covered.** `*` matches only one label, so `canopy.website`
114
- > itself isn't served by the gateway (by design — see
115
- > `apps/gateway/wrangler.main.jsonc`).
116
- > Decide separately whether the apex should redirect to `www.canopy.website`
117
- > (a Cloudflare Redirect Rule, cheap to add later); nothing in this runbook
118
- > depends on it.
119
-
120
- ### 2. Bootstrap API token(s) (dashboard)
121
-
122
- You need credentials that can (a) run OpenTofu + wrangler ops, and (b) mint
123
- the least-privilege runtime tokens. In practice that is often **two**
124
- account-owned tokens — a single token that mixes zone DNS with **Account API
125
- Tokens · Write** has been flaky when edited in place.
126
-
127
- **Token A — `canopy-bootstrap` (Tofu + deploy ops)**
128
-
129
- | Resource | Permissions |
130
- | ------------------------------------------------------------ | ----------------------------------------------------------------------- |
131
- | Account | D1 Edit, Workers R2 Storage Edit, AI Gateway Edit, Workers Scripts Edit |
132
- | Zone `canopy.website` (nested under the account — see below) | Zone Edit, DNS Edit, Workers Routes Edit |
133
-
134
- **Token B — mint token (for `create-api-tokens.sh` only)**
135
-
136
- | Resource | Permissions |
137
- | -------- | ----------------------------------------- |
138
- | Account | **Account API Tokens · Write** (and Read) |
139
-
140
- **Pitfalls we hit:**
141
-
142
- - The dashboard's "DNS" picks are easy to get wrong. You need zone-scoped
143
- **DNS Write** / **Zone Write**, _not_ Account DNS Settings, DNS Views,
144
- DNS Firewall, or Registrar Domains.
145
- - For **account-owned** tokens, zone resources must be **nested under the
146
- account** in the policy JSON shape
147
- `com.cloudflare.api.account.<account_id> → com.cloudflare.api.account.zone.<zone_id>`.
148
- A top-level zone resource can look correct in the UI and still 10000-auth on
149
- `/zones/:id/dns_records`.
150
- - `/user/tokens/verify` returns "Invalid API Token" for account-owned tokens —
151
- use `GET /accounts/<account_id>/tokens/verify` instead.
152
- - Wrangler **OAuth** can list/create D1 and R2 and deploy Workers, but it
153
- cannot manage DNS (and often cannot call AI Gateway). Don't rely on OAuth
154
- alone for step 3.
155
-
156
- Store tokens outside git. Convenience pattern used in the move:
157
-
158
- ```bash
159
- cd infra
160
- # gitignored via infra/.gitignore (.env / .env.*)
161
- cat > .env <<'EOF'
162
- CLOUDFLARE_API_TOKEN="<token A — canopy-bootstrap>"
163
- CLOUDFLARE_TOKEN_MINT_TOKEN="<token B — API Tokens Write>"
164
- EOF
165
- set -a && source .env && set +a
166
- ```
167
-
168
- ### 3. Provision static infra (OpenTofu)
169
-
170
- ```bash
171
- cd infra
172
- cp terraform.tfvars.example terraform.tfvars # set account_id + zone_name
173
- set -a && source .env && set +a # CLOUDFLARE_API_TOKEN = token A
174
- tofu init
175
- tofu import cloudflare_zone.platform <zone_id> # adopt the domain from step 1
176
- tofu plan # review — should be creates only
177
- tofu apply
178
- tofu output wrangler_patch_hint
179
- ```
180
-
181
- Expect four creates: wildcard DNS `*`, D1 `canopy-control`, R2 `canopy-assets`,
182
- AI Gateway `canopy`. (If the AI Gateway resource fails schema validation,
183
- check `cache_invalidate_on_update` and friends against the pinned
184
- `cloudflare` provider minor — it shifted in ~> 5.22.)
185
-
186
- The R2 remote-state backend in `versions.tf` is commented out; for a solo
187
- stand-up, local state is fine. If you want shared state, create a
188
- `canopy-tfstate` bucket first, then uncomment and `tofu init -migrate-state`.
189
-
190
- ### 4. Point the apps at the new resources
191
-
192
- Edit the **site** wrangler configs (not the local `wrangler.jsonc`) using
193
- `tofu output wrangler_patch_hint`. For production that is
194
- `wrangler.main.jsonc` in both apps; for the personal/staging account use
195
- `wrangler.staging.jsonc`. Keep both files in git — never comment-flip a
196
- single file to switch accounts.
197
-
198
- **`apps/api/wrangler.<site>.jsonc`:**
199
-
200
- - `account_id` / `vars.CF_ACCOUNT_ID` → account id
201
- - `d1_databases[0].database_id` → control-plane D1 UUID
202
- - `vars.AI_GATEWAY_ID` → unchanged (`canopy`) unless renamed
203
- - `r2_buckets[0].bucket_name` → unchanged (`canopy-assets`) unless renamed
204
-
205
- **`apps/gateway/wrangler.<site>.jsonc`:**
206
-
207
- - `account_id` → same account id
208
- - `routes[0]` → `{ "pattern": "*.canopy.website/*", "zone_name": "canopy.website" }`
209
- (or `*.thanarie.dev/*` for staging)
210
- - `vars.COOKIE_DOMAIN` / `AUTH_HOST` / `API_HOST` → that site's hosts
211
-
212
- Also point the CLI at the site (`canopy-dev --profile main` / `staging`, or
213
- `CANOPY_API_URL=https://api.<domain>`). Until the workers exist, skip the
214
- CLI bits and come back in step 10.
215
-
216
- ### 5. Runtime tokens (script)
217
-
218
- Mint with **token B** (API Tokens · Write). Token A cannot create other
219
- tokens if it lacks that scope (and a token that _has_ Tokens·Write cannot
220
- always be used as a "sub-token" parent for another Tokens·Write policy):
221
-
222
- ```bash
223
- cd infra
224
- set -a && source .env && set +a
225
- export CLOUDFLARE_API_TOKEN="$CLOUDFLARE_TOKEN_MINT_TOKEN"
226
- CANOPY_ACCOUNT_ID="$(tofu output -raw account_id)" \
227
- ./scripts/create-api-tokens.sh | tee .env.runtime
228
- ```
229
-
230
- Prints `CF_API_TOKEN` and `AI_GATEWAY_TOKEN` once. Keep them for secrets
231
- (`.env.runtime` is gitignored via `.env.*`).
232
-
233
- ### 6. Apply the control-plane D1 schema
234
-
235
- The Tofu-created D1 is empty. Load the schema into the remote (production) DB —
236
- the `db:setup` script targets `--local`, so pass `--remote` explicitly:
237
-
238
- ```bash
239
- cd ../apps/api
240
- # Use the site config that matches the account (main = canopy.website).
241
- pnpm exec wrangler d1 execute canopy-control --remote --file schema.sql \
242
- -c wrangler.main.jsonc
243
- ```
244
-
245
- Do **not** run `seed.local.sql` against remote — it's local dev seed data. Your
246
- user row doesn't exist yet either; it's auto-provisioned on first login, and
247
- admin is granted in step 10.
248
-
249
- ### 7. Deploy the platform workers
250
-
251
- `wrangler secret put` needs the Worker script to exist, so deploy **before**
252
- setting secrets. Use each package's `run deploy` script — bare
253
- `pnpm --filter api deploy` is pnpm's own `deploy` command and will fail with
254
- `ERR_PNPM_INVALID_DEPLOY_TARGET`:
255
-
256
- ```bash
257
- # from repo root — pick the site (deploy aliases deploy:main)
258
- pnpm --filter api run deploy:main # or deploy:staging
259
- pnpm --filter gateway run deploy:main # same site
260
- ```
261
-
262
- Sanity check: `curl -I https://auth.canopy.website` should 302 toward
263
- `/auth/login` (gateway alive). `curl -I https://auth.canopy.website/auth/login`
264
- should 302 to OneLogin with
265
- `redirect_uri=https://auth.canopy.website/auth/callback`.
266
-
267
- ### 8. Worker secrets (script)
268
-
269
- ```bash
270
- cd infra
271
- set -a && source .env.runtime && set +a # CF_API_TOKEN + AI_GATEWAY_TOKEN
272
- # OIDC_CLIENT_SECRET: same OneLogin confidential-client secret as before if
273
- # the app is unchanged. SESSION_SECRET: blank prompt = generate a fresh one.
274
- OIDC_CLIENT_SECRET="…" CF_API_TOKEN="…" AI_GATEWAY_TOKEN="…" \
275
- ./scripts/bootstrap-secrets.sh main # or: staging
276
- ```
277
-
278
- Sets secrets on both workers; **`SESSION_SECRET` is written identically to api
279
- and gateway** — the script handles that. Secrets take effect immediately (no
280
- redeploy required).
281
-
282
- ### 9. OneLogin (external, dashboard)
283
-
284
- Only if the host changed and the apps aren't already pointed at it. OneLogin
285
- matches redirect URIs exactly (no wildcards):
286
-
287
- - Gateway confidential client → `https://auth.canopy.website/auth/callback`
288
- - CLI PKCE client → unchanged (`http://127.0.0.1:8976/callback`, local)
289
-
290
- If you already configured these for `canopy.website` before the move, skip.
291
-
292
- ### 10. Log in, grant yourself admin, then deploy the core projects
293
-
294
- The four core projects are ordinary platform projects that dogfood the product.
295
- Three of their slugs (`dashboard`, `www`, `docs`) are in `RESERVED_SLUGS`
296
- (`apps/api/src/routes/projects.ts`), so **only an admin can create them** —
297
- hence the ordering here.
298
-
299
- Point the local CLI at the new platform (and rebuild if you just changed
300
- defaults):
301
-
302
- ```bash
303
- # canopy-dev = this repo's CLI (packages/cli → vp pack), not the npm `canopy`
304
- export CANOPY_API_URL="https://canopy-api.monks.workers.dev"
305
- export CANOPY_PROJECT_DOMAIN="canopy.website"
306
- export CANOPY_PROJECT_PROTOCOL="https"
307
- ```
308
-
309
- 1. `canopy-dev login` → browser PKCE against OneLogin; your user row is
310
- auto-provisioned in the new control DB. Confirm with `canopy-dev whoami`
311
- (apiUrl should be the new workers.dev host).
312
- 2. Grant admin (the row only exists after step 1). The first admin on a fresh
313
- control DB still needs SQL; after that, admins can promote others from the
314
- dashboard **Users** page:
315
- ```bash
316
- pnpm --filter api exec wrangler d1 execute canopy-control --remote \
317
- --command "UPDATE users SET is_admin = 1 WHERE email = 'you@monks.com'"
318
- ```
319
- 3. Register each project remotely, then deploy it. **`--no-scaffold` is
320
- required** — `create` otherwise writes a starter template into `./<slug>/`
321
- (and prompts for a template picker), which inside an existing app means a
322
- junk `apps/dashboard/dashboard/` directory. `--no-scaffold` registers only,
323
- no files written. The `--slug` must match the `slug` in that directory's
324
- existing `canopy.json`, since that's what `deploy` publishes to:
325
-
326
- ```bash
327
- # Always pass --profile <site> for the destination (main / staging / gcp-pilot).
328
- # Examples below use main (production dogfood); swap the id when targeting another site.
329
- cd apps/dashboard
330
- canopy-dev --profile main create "Canopy Dashboard" --slug dashboard --no-scaffold
331
- canopy-dev --profile main deploy
332
-
333
- cd ../www
334
- canopy-dev --profile main create "Canopy" --slug www --no-scaffold
335
- canopy-dev --profile main deploy
336
-
337
- cd ../docs
338
- canopy-dev --profile main create "Canopy Docs" --slug docs --no-scaffold
339
- canopy-dev --profile main deploy
340
-
341
- cd ../../examples/kitchen-sink
342
- canopy-dev --profile main create "Kitchen Sink" --slug kitchen-sink --no-scaffold
343
- canopy-dev --profile main deploy
344
- ```
345
-
346
- `canopy.json` already carries the slug, build command, dist dir, and a
347
- usual `"profile": "main"` pin, so `deploy` builds them — no separate
348
- `pnpm build` needed. Still pass `--profile` for the intended site so a
349
- stray active profile cannot send the deploy elsewhere
350
- (docs/10-cli-sites.md). On a site without worker support
351
- (`capabilities.workers = false`), a deploy that keeps
352
- `"worker": "worker/index.ts"` will 501 `not_supported_on_target` — strip
353
- the `worker` key for an assets-only deploy there; that's still enough to
354
- exercise `db` / `blob` / `ai.chat` through the platform data plane.
355
-
356
- ### 11. AI Gateway keys + spend rule (script)
357
-
358
- ```bash
359
- cd infra # adjust if you're coming from examples/kitchen-sink
360
- set -a && source .env.runtime && set +a
361
- # CF_API_TOKEN has AI Gateway Read (use that for the logs check);
362
- # the script's prompt is about dashboard BYOK / spend-limit clicks.
363
- export CLOUDFLARE_API_TOKEN="$CF_API_TOKEN"
364
- CANOPY_ACCOUNT_ID="$(tofu output -raw account_id)" DEFAULT_BUDGET_USD=25 \
365
- ./scripts/bootstrap-ai-gateway.sh
366
- ```
367
-
368
- Verifies the gateway, then walks you through adding BYOK provider keys and the
369
- per-project spend-limit rule (both dashboard steps — see the script's notes).
370
- `AI_GATEWAY_TOKEN` alone is **Run**-scoped and cannot read logs; use
371
- `CF_API_TOKEN` (D1 Write + AI Gateway Read) for the read-back check.
372
-
373
- The hard daily budget cap is enforced by canopy-api's own `ai_spend` ledger on
374
- every request; the AI Gateway spend rule is belt-and-braces on top.
375
-
376
- ### 12. Smoke test
377
-
378
- Anonymous visits to project hosts **302 to OneLogin** by design (monk SSO;
379
- same as the old `thanarie.dev` setup). Client-password mode is the only
380
- anonymous path. Smoke through a real browser session:
381
-
382
- 1. `https://dashboard.canopy.website` → OneLogin login works; the project list
383
- shows all four under **Core projects**.
384
- 2. `https://www.canopy.website` → marketing site + `/changelog` render (after
385
- SSO).
386
- 3. `https://kitchen-sink.canopy.website` → exercise `db`, `blob`, and `ai.chat`
387
- (proves per-project D1 provisioning, R2 prefixes, and the AI Gateway path —
388
- BYOK must be set first for `ai.chat`).
389
- 4. Confirm kitchen-sink's LLM spend appears in its dashboard usage view (the
390
- view reads the platform's own `ai_spend` ledger, written at call time — the
391
- same rows the daily budget cap enforces).
392
- 5. `canopy-dev create scratch-…` then `canopy-dev delete <slug> --yes` →
393
- confirms provisioning and full teardown.
394
-
395
- CLI sanity (no browser): `curl -sS https://auth.canopy.website/auth/login`
396
- should 302 to OneLogin; `curl -sS https://canopy-api.<subdomain>.workers.dev/control/v1/docs`
397
- should 401 `unauthenticated` (API up, auth required).
398
-
399
- ---
400
-
401
- ## Teardown of the old account
402
-
403
- Once the new account is verified and you no longer need the old projects'
404
- data, delete resources in the old account (or leave it to lapse). Site
405
- bindings live in `wrangler.main.jsonc` / `wrangler.staging.jsonc`; local
406
- `wrangler.jsonc` is not a deploy target.