@mediamonks/canopy-cli 0.7.10 → 0.7.12

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 (108) hide show
  1. package/dist/{deploy-BKFICJT1.mjs → deploy-B1WrPq5W.mjs} +12 -2
  2. package/dist/{dev-DtoZrexp.mjs → dev-BnUYGQQm.mjs} +5 -4
  3. package/dist/dev-CySBxW7I.mjs +2 -0
  4. package/dist/{dev-worker-BZgtEM9V.mjs → dev-worker-IWnHvQ1f.mjs} +42 -7
  5. package/dist/{docs-Ck9xogEV.mjs → docs-DxB7NhZs.mjs} +1 -1
  6. package/dist/{feedback-BpiCvItH.mjs → feedback-B-GXUBPs.mjs} +1 -1
  7. package/dist/index.mjs +10 -10
  8. package/dist/{local-C5AhA4GM.mjs → local-BGRd_FMZ.mjs} +6 -469
  9. package/dist/{package-DRp7sI6i.mjs → package-CDLufecy.mjs} +1 -1
  10. package/dist/{skill-B2sDMAmv.mjs → skill-BOWfqwr2.mjs} +1 -1
  11. package/dist/src-Bu4Ze32I.mjs +470 -0
  12. package/dist/{upgrade-tOfSmAcc.mjs → upgrade-fhfrid_O.mjs} +1 -1
  13. package/package.json +3 -3
  14. package/dist/dev-BYxSHUH6.mjs +0 -2
  15. package/dist/docs/ci.md +0 -50
  16. package/dist/docs/cli-create.md +0 -46
  17. package/dist/docs/cli-db.md +0 -86
  18. package/dist/docs/cli-delete.md +0 -44
  19. package/dist/docs/cli-deploy.md +0 -58
  20. package/dist/docs/cli-dev.md +0 -82
  21. package/dist/docs/cli-docs.md +0 -47
  22. package/dist/docs/cli-download.md +0 -42
  23. package/dist/docs/cli-egress.md +0 -37
  24. package/dist/docs/cli-feedback.md +0 -75
  25. package/dist/docs/cli-index.md +0 -61
  26. package/dist/docs/cli-init.md +0 -46
  27. package/dist/docs/cli-login.md +0 -43
  28. package/dist/docs/cli-logout.md +0 -32
  29. package/dist/docs/cli-logs.md +0 -57
  30. package/dist/docs/cli-open.md +0 -41
  31. package/dist/docs/cli-projects.md +0 -43
  32. package/dist/docs/cli-promote.md +0 -44
  33. package/dist/docs/cli-rollback.md +0 -45
  34. package/dist/docs/cli-skill.md +0 -39
  35. package/dist/docs/cli-staging.md +0 -54
  36. package/dist/docs/cli-token.md +0 -47
  37. package/dist/docs/cli-upgrade.md +0 -43
  38. package/dist/docs/cli-usage.md +0 -43
  39. package/dist/docs/cli-whoami.md +0 -46
  40. package/dist/docs/cli.md +0 -86
  41. package/dist/docs/data.md +0 -85
  42. package/dist/docs/deploy.md +0 -81
  43. package/dist/docs/dev.md +0 -72
  44. package/dist/docs/developers-agents-and-ai.md +0 -95
  45. package/dist/docs/developers-architecture.md +0 -106
  46. package/dist/docs/getting-started.md +0 -108
  47. package/dist/docs/index.json +0 -331
  48. package/dist/docs/infrastructure.md +0 -406
  49. package/dist/docs/migrations.md +0 -183
  50. package/dist/docs/overview-faq.md +0 -105
  51. package/dist/docs/overview-how-it-works.md +0 -82
  52. package/dist/docs/overview-use-cases.md +0 -76
  53. package/dist/docs/overview-what-is-canopy.md +0 -75
  54. package/dist/docs/platform-ai-usage.md +0 -65
  55. package/dist/docs/platform-client-access.md +0 -54
  56. package/dist/docs/platform-deploys-and-rollbacks.md +0 -93
  57. package/dist/docs/platform-egress.md +0 -75
  58. package/dist/docs/platform-feedback.md +0 -114
  59. package/dist/docs/platform-members-and-access.md +0 -71
  60. package/dist/docs/platform-project-data.md +0 -76
  61. package/dist/docs/platform-schedules.md +0 -90
  62. package/dist/docs/platform-sites-and-profiles.md +0 -61
  63. package/dist/docs/porting-nextjs.md +0 -233
  64. package/dist/docs/sdk-ai.md +0 -103
  65. package/dist/docs/sdk-auth.md +0 -57
  66. package/dist/docs/sdk-database.md +0 -126
  67. package/dist/docs/sdk-files.md +0 -82
  68. package/dist/docs/sdk-services.md +0 -58
  69. package/dist/docs/sdk.md +0 -126
  70. package/dist/docs/staging.md +0 -106
  71. package/dist/docs/static-serving.md +0 -318
  72. package/dist/docs/troubleshooting.md +0 -102
  73. package/dist/skill/canopy/SKILL.md +0 -160
  74. package/dist/templates/spa/.github/workflows/canopy-deploy.yml +0 -34
  75. package/dist/templates/spa/AGENTS.md +0 -47
  76. package/dist/templates/spa/README.md +0 -14
  77. package/dist/templates/spa/_gitignore +0 -4
  78. package/dist/templates/spa/canopy.json +0 -5
  79. package/dist/templates/spa/index.html +0 -12
  80. package/dist/templates/spa/package.json +0 -21
  81. package/dist/templates/spa/src/app.tsx +0 -25
  82. package/dist/templates/spa/src/canopy.ts +0 -5
  83. package/dist/templates/spa/src/main.tsx +0 -10
  84. package/dist/templates/spa/src/style.css +0 -29
  85. package/dist/templates/spa/tsconfig.json +0 -16
  86. package/dist/templates/spa/vite.config.ts +0 -7
  87. package/dist/templates/static/AGENTS.md +0 -45
  88. package/dist/templates/static/README.md +0 -9
  89. package/dist/templates/static/_gitignore +0 -2
  90. package/dist/templates/static/canopy.json +0 -4
  91. package/dist/templates/static/index.html +0 -21
  92. package/dist/templates/static/main.js +0 -9
  93. package/dist/templates/static/style.css +0 -29
  94. package/dist/templates/worker/.dev.vars.example +0 -9
  95. package/dist/templates/worker/.github/workflows/canopy-deploy.yml +0 -34
  96. package/dist/templates/worker/AGENTS.md +0 -63
  97. package/dist/templates/worker/README.md +0 -17
  98. package/dist/templates/worker/_gitignore +0 -5
  99. package/dist/templates/worker/canopy.json +0 -6
  100. package/dist/templates/worker/index.html +0 -12
  101. package/dist/templates/worker/package.json +0 -22
  102. package/dist/templates/worker/src/app.tsx +0 -25
  103. package/dist/templates/worker/src/canopy.ts +0 -5
  104. package/dist/templates/worker/src/main.tsx +0 -10
  105. package/dist/templates/worker/src/style.css +0 -29
  106. package/dist/templates/worker/tsconfig.json +0 -16
  107. package/dist/templates/worker/vite.config.ts +0 -7
  108. package/dist/templates/worker/worker/index.ts +0 -52
@@ -1,318 +0,0 @@
1
- ---
2
- title: How static files are served
3
- description: The full path a browser request takes to a deployed file.
4
- ---
5
-
6
- This explains the full path a browser request takes to receive a deployed
7
- project's `index.html` and its assets (JS/CSS/images/fonts), and reviews that
8
- path from a **security** and a **performance** angle.
9
-
10
- Everything a visitor sees is a _deployed_ build stored in R2 — the platform
11
- serves pre-built static files. (When a project has a backend worker, `/api/*`
12
- requests dispatch to it — Workers for Platforms on Cloudflare, Cloud Run on
13
- GCP; everything else is still static serving.)
14
-
15
- Asset serving is the hottest **data-plane** path (see
16
- [`docs/09-plane-split.md`](../09-plane-split.md)): it lives at
17
- `/data/v1/assets/*`, and the URL carries no project slug — the gateway stamps
18
- which project the request is for.
19
-
20
- ## The request path
21
-
22
- A request to `https://<slug>.canopy.<domain>/some/path` flows through two
23
- Workers and one R2 bucket:
24
-
25
- ```
26
- Browser
27
- │ GET https://dashboard.canopy.example.com/assets/app-a1b2.js
28
- ▼
29
- canopy-gateway (apps/gateway/src/index.ts)
30
- │ 1. Parse <slug> (and optional pinned version) from the hostname
31
- │ 2. Enforce auth — OneLogin session cookie, or client-password cookie
32
- │ 3. Rewrite path → /data/v1/assets/<path> (+ ?v=<n> if pinned),
33
- │ stamping X-Canopy-Project: <slug> + an internal JWT vouching for it
34
- ▼ env.API.fetch(...) (service binding, no public hop)
35
- canopy-api (apps/api/src/routes/assets.ts)
36
- │ 4. Resolve the project from the stamp + which version is live
37
- │ 5. Sanitize the asset path, SPA-fallback to index.html
38
- │ 6. R2 GET deploys/<project-id>/<version>/<path>
39
- ▼
40
- R2 bucket "canopy-assets"
41
- ```
42
-
43
- ### 1–3. Gateway: hostname, auth, path rewrite
44
-
45
- [`apps/gateway/src/index.ts`](../../apps/gateway/src/index.ts) is the front door
46
- for every `*.canopy.<domain>` request.
47
-
48
- - **Hostname → slug + version.** `parseHost()`
49
- ([`index.ts`](../../apps/gateway/src/index.ts)) splits the first DNS label.
50
- A plain `dashboard` resolves to that project's _live_ version; a
51
- `v12-dot-dashboard` prefix pins version 12 of the **frontend only** — auth,
52
- client access, and the data plane still resolve to the real project, so an old
53
- build previews against _current_ data rather than forking it.
54
- - **Auth before any project bytes.** No asset is served unauthenticated. A
55
- request must carry either a valid OneLogin session cookie (verified via a
56
- shared `SESSION_SECRET`) or — on client-enabled projects — a scope-limited
57
- client-password cookie. Missing both: client-enabled projects get the password
58
- page ([`client.ts`](../../apps/gateway/src/client.ts)), everything else
59
- redirects to OneLogin. Collaborators-only projects additionally turn
60
- non-members away with a members-only page before any dispatch.
61
- - **Path rewrite + project stamp.** Once authorized, the gateway rewrites the
62
- request to the data plane and forwards it over a **service binding**
63
- (`env.API.fetch`), never a public URL
64
- ([`index.ts`](../../apps/gateway/src/index.ts)). The URL carries no slug —
65
- the project travels as a header the gateway vouches for:
66
-
67
- ```ts
68
- const assetUrl = new URL(request.url);
69
- assetUrl.pathname = `/data/v1/assets${url.pathname}`;
70
- if (version !== null) assetUrl.searchParams.set("v", String(version));
71
- const assetRequest = new Request(assetUrl, request);
72
- assetRequest.headers.delete("x-canopy-project");
73
- assetRequest.headers.set("x-canopy-project", slug);
74
- assetRequest.headers.set("x-canopy-internal", await createInternalJwt(env.SESSION_SECRET));
75
- return env.API.fetch(assetRequest);
76
- ```
77
-
78
- The caller's cookie travels along, so canopy-api re-verifies it independently,
79
- and canopy-api trusts the `X-Canopy-Project` stamp only when the accompanying
80
- internal JWT verifies.
81
-
82
- `/_api/*` requests are handled separately, forwarded to the API before the
83
- session wall so the SDK gets JSON 401s instead of login HTML. The plane is the
84
- path: `/_api/control/v1/*` forwards untouched, `/_api/data/v1/*` gets the same
85
- project stamp as asset requests.
86
-
87
- ### 4–6. API: version resolution, path sanitizing, R2 read
88
-
89
- The serving handler is `GET /data/v1/assets/*` in
90
- [`apps/api/src/routes/assets.ts`](../../apps/api/src/routes/assets.ts).
91
-
92
- - **Project + status.** Resolves the project from the gateway's vouched stamp
93
- only (`vouchedProject` — never a query string), via a cached
94
- `ProjectSnapshot` from the `ProjectDirectory`
95
- ([`src/directory.ts`](../../apps/api/src/directory.ts)) — the data plane
96
- never queries the control database directly. Unknown → 404 placeholder;
97
- paused (`status !== "active"`) → a "paused" placeholder page. Viewability is
98
- company-wide by default; projects marked **collaborators-only**
99
- (`members_only`) turn non-members away with a 403 placeholder
100
- (`checkAppAccess`, [`src/access.ts`](../../apps/api/src/access.ts)).
101
- - **Which version.** `?v=<n>` pins an exact version (must exist in the
102
- project's deploy history, else 404 — it never silently falls back to live);
103
- otherwise the snapshot's `liveAssetVersion` is served, which falls back to
104
- the max deployed version for legacy rows with a NULL pointer.
105
- - **Path → R2 key.** The tail after `/assets/` is sanitized (see Security),
106
- defaulting to `index.html`, and mapped to the R2 key
107
- `deploys/<project-id>/<version>/<path>`.
108
- - **SPA fallback + read**: the exact asset is read first, and any miss falls
109
- back to `index.html`, so client-side routes (`/settings`, `/projects/42`) all
110
- resolve to the SPA entry point. The fallback `index.html` is served from a
111
- per-version cache (see [Performance](#performance-review)), so a deep-linked
112
- route doesn't pay a second R2 read on every request.
113
-
114
- ### How assets are stored & versioned
115
-
116
- `canopy deploy` uses a two-step signed upload
117
- ([`docs/09-plane-split.md`](../09-plane-split.md) D-d): the **control plane**
118
- authorizes the deploy (`POST /control/v1/projects/:slug/deploys`,
119
- [`deploys.ts`](../../apps/api/src/routes/deploys.ts)) and issues a short-lived
120
- signed grant; the CLI PUTs each file to the **data plane**
121
- (`PUT /data/v1/uploads/:grant`,
122
- [`data-plane.ts`](../../apps/api/src/routes/data-plane.ts)); a completion call
123
- (`POST …/deploys/:version/complete`) validates the staged bundle and records
124
- the version. Bundle bytes never transit the control plane.
125
-
126
- - Each file is written to R2 at `deploys/<project-id>/<version>/<path>` with its
127
- `contentType` stored as R2 `httpMetadata` by the upload endpoint.
128
- - The bucket is `canopy-assets`, bound as `ASSETS`
129
- ([`apps/api/wrangler.main.jsonc`](../../apps/api/wrangler.main.jsonc)).
130
- - **Immutable, append-only versioning.** A deploy bumps the version and writes a
131
- new prefix; the `deployments` row commits at completion _before_
132
- `live_asset_version` moves, so a half-uploaded deploy is never served (an
133
- abandoned grant leaves only unreferenced staged bytes). Rollback
134
- (`POST …/deploys/:version/rollback`,
135
- [`deploys.ts`](../../apps/api/src/routes/deploys.ts)) just repoints
136
- `live_asset_version` at an existing version — instant, nothing copied, nothing
137
- deleted, and any version can be made live again. Old versions keep their bytes
138
- and their identity, which is what makes `v<n>-dot-` preview hosts work.
139
- - Project deletion purges the whole `deploys/<project-id>/` prefix (plus blobs
140
- and snapshots) so no R2 residue remains
141
- ([`projects.ts`](../../apps/api/src/routes/projects.ts)).
142
-
143
- The browser SDK itself is served by the platform, not from a deploy bundle:
144
- `GET /_api/data/v1/sdk.js` returns an inlined bundle
145
- ([`data-plane.ts`](../../apps/api/src/routes/data-plane.ts)) so scaffolded
146
- projects need no `npm install`.
147
-
148
- ---
149
-
150
- ## Security review
151
-
152
- ### What's solid
153
-
154
- - **Nothing is public.** Every asset request passes the gateway auth wall
155
- (OneLogin session or client-password cookie) before any project bytes are
156
- read, and the gateway→API hop is a private service binding, not an internet
157
- URL. This is the north-star "never public URLs" guarantee.
158
- - **Path-traversal defense.** `safeAssetPath()`
159
- ([`assets.ts`](../../apps/api/src/routes/assets.ts)) rejects on both
160
- ingest (the upload endpoint) and serve: strips a leading `./` or `/`, then
161
- rejects empty paths, paths over 512 chars, and any path containing `\`, `//`,
162
- `\0`, or a `.`/`..`/empty segment. A malicious key can't escape the
163
- `deploys/<project-id>/<version>/` prefix, and one project can never read
164
- another's bytes because the prefix is keyed by the resolved project id.
165
- - **Tenant isolation by key prefix and by stamp.** The R2 key always embeds the
166
- resolved `project.id`, so cross-project reads are structurally impossible even
167
- if a path slipped through. Serving resolves its project from the gateway's
168
- vouched `X-Canopy-Project` stamp alone — never a query parameter — so a
169
- crafted query string can't render one project's pages under another's host.
170
- - **Content-Type is fixed at deploy time**, from an allowlist keyed by extension
171
- (`contentTypeFor()`, [`assets.ts`](../../apps/api/src/routes/assets.ts)),
172
- and stored as R2 metadata by the upload endpoint. Serving echoes that stored
173
- type; unknown extensions become `application/octet-stream` (download, not
174
- execute). The type is never derived from request input at serve time.
175
- - **Deploy bundle limits.** Max 500 files and 25 MB total
176
- ([`storage-keys.ts`](../../apps/api/src/storage-keys.ts)), enforced per file
177
- at upload and re-checked over the staged inventory at completion; a bundle
178
- must contain `index.html` or completion is rejected — bounds the blast radius
179
- of a bad upload. Upload grants are HMAC-signed, project- and version-bound,
180
- and expire after 15 minutes ([`upload-grant.ts`](../../apps/api/src/upload-grant.ts)).
181
- - **Password page escapes untrusted input.** `escapeHtml()` and the same-site
182
- `safeLocalPath()`/`safeReturnTo()` guards prevent reflected-XSS and open-redirect
183
- via the `returnTo` parameter
184
- ([`client.ts`](../../apps/gateway/src/client.ts),
185
- [`index.ts`](../../apps/gateway/src/index.ts)).
186
- - **Version pinning can't leak data.** A `v<n>-dot-` host pins only _which
187
- frontend build_ is served; auth, client-access, and all `/_api` data-plane
188
- calls resolve to the real project (the stamp names the real slug),
189
- so an old build cannot be used to read a different project's data.
190
-
191
- ### Gaps / things to weigh
192
-
193
- These are observations, not known incidents — worth a conscious decision:
194
-
195
- 1. **No hardening response headers.** Served assets carry `content-type`, `etag`,
196
- and `cache-control` only ([`assets.ts`](../../apps/api/src/routes/assets.ts)).
197
- There is **no** `X-Content-Type-Options: nosniff`, no `Content-Security-Policy`,
198
- no `X-Frame-Options`/`frame-ancestors`, no `Referrer-Policy`, and no HSTS. For
199
- an app platform serving arbitrary user HTML/JS on shared `*.canopy.<domain>`
200
- subdomains, the most impactful additions are:
201
- - `X-Content-Type-Options: nosniff` — cheap, prevents MIME-sniffing of e.g. a
202
- user-uploaded file into script.
203
- - `frame-ancestors`/`X-Frame-Options` — clickjacking, and stops one canopy
204
- subdomain from framing another.
205
- - A default `Content-Security-Policy` (even a permissive one) as a baseline.
206
- Note projects run against a shared cookie domain, so cross-subdomain
207
- concerns (cookie scoping, framing) matter more here than on an isolated
208
- site.
209
- 2. **`text/html` is served for deployed HTML with the session cookie present.**
210
- That's by design (auth-gated apps), but it means any XSS in a deployed project
211
- runs on a `*.canopy.<domain>` origin that shares the session-cookie domain.
212
- The session cookie's flags (HttpOnly/SameSite/Secure) in
213
- [`session.ts`](../../apps/gateway/src/session.ts) are the mitigation to keep an
214
- eye on; per-project origin isolation would be the stronger fix.
215
- 3. **SPA fallback returns `index.html` with 200 for unknown paths**, including
216
- things that look like missing assets (`/assets/typo.js` → HTML). That's normal
217
- SPA behavior but can mask 404s and, combined with #1, is another reason
218
- `nosniff` matters (an HTML body served where a script was expected).
219
- 4. **Serving is company-wide readable by default.** Any authenticated monk can
220
- view any active project unless it opts into collaborators-only
221
- (`members_only`, enforced in [`assets.ts`](../../apps/api/src/routes/assets.ts)
222
- via `checkAppAccess`). The default is an intentional product decision (see
223
- docs/02-decisions.md), not a bug — but it means "authenticated" ≠
224
- "authorized for this specific project" on the read path unless the project
225
- tightens it.
226
-
227
- ---
228
-
229
- ## Performance review
230
-
231
- ### What helps
232
-
233
- - **Split caching policy by type** ([`assets.ts`](../../apps/api/src/routes/assets.ts)):
234
-
235
- ```ts
236
- "cache-control": type.startsWith("text/html")
237
- ? "no-cache" // HTML always revalidates → new deploys show up
238
- : "private, max-age=3600", // hashed subresources cache for 1h
239
- ```
240
-
241
- HTML revalidates so a fresh deploy is picked up immediately; hashed JS/CSS/font
242
- assets cache in the browser for an hour. This is the right shape for a
243
- content-hashed SPA build.
244
-
245
- - **ETags** (`"v<version>-<path>"`, [`assets.ts`](../../apps/api/src/routes/assets.ts))
246
- give a stable validator per version+path, so revalidation of `no-cache` HTML
247
- can come back as a cheap `304`. Because the etag encodes the version, a rollback
248
- or new deploy changes the validator and busts caches correctly.
249
- - **Instant deploys and rollbacks.** Serving reads a pointer
250
- (`live_asset_version`); deploy/rollback move the pointer without copying bytes,
251
- so there's no propagation delay and no rebuild.
252
- - **Service-binding hop.** Gateway→API is an in-Cloudflare service binding, not a
253
- public fetch, so there's no extra internet round-trip or TLS handshake between
254
- the two Workers.
255
-
256
- ### Bottlenecks / opportunities
257
-
258
- 1. **`max-age=3600` is modest for hashed assets.** Content-hashed filenames (the
259
- Vite default) can safely use `public, max-age=31536000, immutable`. The current
260
- value is `private` (see #2) and only an hour, so returning visitors refetch
261
- more than necessary. Consider keying on filename hash pattern or a deploy-time
262
- flag to mark truly immutable assets.
263
- 2. **`private` prevents any shared/edge caching.** Because assets are auth-gated
264
- and served `private`, they can't be cached by Cloudflare's CDN or any shared
265
- proxy — every cache miss goes Worker→R2. For an auth-walled platform that's a
266
- deliberate trade (privacy over edge-cache hit rate), but it means R2 latency is
267
- on the critical path for every uncached request. If per-project caching is ever
268
- wanted, the Cache API keyed by (project, version, path) inside the Worker would
269
- let you cache without making anything public.
270
- 3. **No compression / `Content-Encoding`.** The handler streams R2 bytes as-is
271
- with no gzip/brotli negotiation and ignores `Accept-Encoding`. Cloudflare may
272
- apply compression at the edge for eligible content types, but nothing is stored
273
- pre-compressed and the Worker doesn't negotiate it. For large JS bundles,
274
- storing brotli-precompressed variants (or relying on and verifying edge
275
- compression) is worth measuring.
276
- 4. **Range requests / streaming media.** The handler returns the full R2 object
277
- body and doesn't honor `Range`. Fine for a bundled SPA; if projects ever serve
278
- large video/audio, add `Range` support (R2 supports ranged `get`).
279
-
280
- ### Addressed — in-worker caches
281
-
282
- Two former bottlenecks are now handled by best-effort, per-isolate caches
283
- (a performance layer only — never a source of truth; a cold isolate just reads
284
- D1/R2):
285
-
286
- - **SPA fallback no longer double-reads R2.** The serve path reads the exact
287
- asset once; on a miss it falls back to `index.html` served from a per-`(project,
288
- version)` cache ([`asset-cache.ts`](../../apps/api/src/asset-cache.ts)). Because a
289
- deployed version's bytes are immutable, that entry is cached indefinitely
290
- (count-bounded), so the second R2 read is paid **once per version**, not once
291
- per deep-linked route. Verified by a test that serves three client routes and
292
- asserts a single `index.html` read.
293
- - **Project state is cached per slug** as a `ProjectSnapshot` in the
294
- `ProjectDirectory` ([`src/directory.ts`](../../apps/api/src/directory.ts) —
295
- the generalization of the old metadata cache), sparing the hot path its
296
- control-DB queries. Since this data _does_ change, deploy, rollback, and
297
- delete call `invalidateSnapshot()` so a write is reflected immediately on the
298
- same isolate; a short TTL (5 s) is only a backstop for a write that lands on
299
- a different isolate than the next read.
300
-
301
- ---
302
-
303
- ## Quick reference
304
-
305
- | Concern | Where | Notes |
306
- | --------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
307
- | Front door / auth / routing | [`apps/gateway/src/index.ts`](../../apps/gateway/src/index.ts) | Hostname→slug, auth wall, `/data/v1/assets` rewrite + project stamp |
308
- | Client-password page | [`apps/gateway/src/client.ts`](../../apps/gateway/src/client.ts) | HTML-escaped, same-site returnTo |
309
- | Asset serve handler | [`apps/api/src/routes/assets.ts`](../../apps/api/src/routes/assets.ts) | Version resolve, path sanitize, SPA fallback, headers |
310
- | Project snapshots | [`apps/api/src/directory.ts`](../../apps/api/src/directory.ts) | `ProjectDirectory` cache (5 s TTL), invalidated on write |
311
- | index.html cache | [`apps/api/src/asset-cache.ts`](../../apps/api/src/asset-cache.ts) | Per-version, immutable bytes, count-bounded |
312
- | Path sanitizing | [`assets.ts`](../../apps/api/src/routes/assets.ts) | `safeAssetPath()` — traversal defense, shared with the upload endpoint |
313
- | Content-Type allowlist | [`assets.ts`](../../apps/api/src/routes/assets.ts) | Fixed at deploy, stored in R2 metadata |
314
- | Deploy authorize / complete | [`apps/api/src/routes/deploys.ts`](../../apps/api/src/routes/deploys.ts) | Signed grant, 500 files / 25 MB, pointer moves last |
315
- | Upload bytes | [`apps/api/src/routes/data-plane.ts`](../../apps/api/src/routes/data-plane.ts) | `PUT /data/v1/uploads/:grant`, grant is the credential |
316
- | Rollback | [`deploys.ts`](../../apps/api/src/routes/deploys.ts) | Repoints `live_asset_version` |
317
- | R2 bucket binding | [`apps/api/wrangler.main.jsonc`](../../apps/api/wrangler.main.jsonc) | `ASSETS` → `canopy-assets` |
318
- | SDK serving | [`apps/api/src/routes/data-plane.ts`](../../apps/api/src/routes/data-plane.ts) | `/_api/data/v1/sdk.js`, `public, max-age=300` |
@@ -1,102 +0,0 @@
1
- ---
2
- title: Troubleshooting
3
- description: Common errors and how to resolve them.
4
- ---
5
-
6
- ## `command not found: canopy`
7
-
8
- The CLI isn't installed or isn't on your PATH. Install it:
9
-
10
- ```bash
11
- npm install -g @mediamonks/canopy-cli
12
- ```
13
-
14
- If it installs but still isn't found, your npm global bin directory isn't on your
15
- PATH. Run `npm bin -g` to find it and add that directory to your PATH.
16
-
17
- ## `Not logged in — run canopy login`
18
-
19
- You have no valid CLI token. Run `canopy login` and finish the sign-in in the
20
- browser. Check status with `canopy whoami`.
21
-
22
- ## `Session expired — run canopy login again`
23
-
24
- Your token expired. Just run `canopy login` again.
25
-
26
- ## The browser didn't open during login
27
-
28
- `canopy login` prints the sign-in URL as a fallback — open it manually in a
29
- browser on the **same machine** as the CLI (login uses a local loopback
30
- redirect, so it must be the same machine).
31
-
32
- ## `Port 8976 is in use`
33
-
34
- A previous login attempt is still holding the callback port. Close it (or wait a
35
- moment) and retry `canopy login`.
36
-
37
- ## Login hangs and never completes
38
-
39
- Login only works when the CLI runs on the machine whose browser you're using
40
- (the redirect goes to `127.0.0.1`). It cannot complete on a remote/headless box.
41
- Run it on your own laptop.
42
-
43
- ## `Deploy token rejected`
44
-
45
- The `CANOPY_DEPLOY_TOKEN` was revoked or is wrong. Create a new one with
46
- `canopy token create` (or in the dashboard) and update your CI secret.
47
-
48
- ## `not_supported_on_target` (501)
49
-
50
- Some features are platform-target-specific: a site whose platform target
51
- doesn't support a feature (for example per-project worker backends, dashboard
52
- schedules, or secret injection) answers this error, and the dashboard hides
53
- those panels. It's not a failure of your project — the feature doesn't exist
54
- on the target your platform runs on.
55
-
56
- ## `ai_budget_exceeded` (429)
57
-
58
- The project hit its daily AI budget cap. The cap is enforced before every AI
59
- call; it resets the next day, or a maintainer can raise the project's budget in
60
- the dashboard.
61
-
62
- ## `egress_denied` (403)
63
-
64
- An outbound `fetch` from your worker was blocked by the platform's egress
65
- guard: `host <hostname> is not on the project allowlist`. Outbound requests
66
- are deny-by-default — only platform infrastructure and the hosts on your
67
- project's own allowlist get through. Add the host and retry (running workers
68
- pick the change up without a deploy):
69
-
70
- ```bash
71
- canopy egress allow api.example.com
72
- ```
73
-
74
- or use dashboard Settings → Outbound hosts. Details and entry rules:
75
- [Outbound requests](https://docs.canopy.website/platform/egress). Calls made through the SDK /
76
- `env.CANOPY.fetch` don't count as egress and are unaffected.
77
-
78
- ## The worker returns errors and you can't see why
79
-
80
- Read its console output instead of adding diagnostic endpoints:
81
-
82
- ```bash
83
- canopy logs --since 15m
84
- ```
85
-
86
- `console.log` / `console.error` from your deployed worker, straight in the
87
- terminal; `--follow` tails it live while you reproduce the problem. See
88
- [canopy logs](https://docs.canopy.website/cli/logs). (On sites without worker log capture the command
89
- answers `not_supported_on_target`.)
90
-
91
- ## Docs are stale
92
-
93
- `canopy docs` caches the platform's docs. Force a refresh:
94
-
95
- ```bash
96
- canopy docs <topic> --refresh
97
- ```
98
-
99
- ## Still stuck?
100
-
101
- Run the failing command again and share its full output. `canopy whoami` and
102
- `canopy --version` are useful to include.
@@ -1,160 +0,0 @@
1
- ---
2
- name: canopy
3
- description: >-
4
- Build, run, and deploy projects on Canopy — Monks' managed platform for
5
- vibecoded apps (hosting + database + blob storage + AI). Use this whenever the
6
- user wants to create a Canopy project, run/deploy one, manage its data or
7
- deploy tokens, or asks how to use the `canopy` CLI. All platform docs are
8
- available offline via `canopy docs`.
9
- ---
10
-
11
- # Canopy
12
-
13
- Canopy is Monks' managed platform for vibecoded projects. Everything is driven
14
- by one CLI: **`canopy`**. This skill tells you how to drive it; the CLI itself
15
- carries the full, always-current documentation.
16
-
17
- ## First: is the CLI installed and signed in?
18
-
19
- Run these before anything else, in order:
20
-
21
- ```bash
22
- canopy --version # is the CLI installed?
23
- canopy whoami # is there a valid login?
24
- ```
25
-
26
- - **`canopy --version` fails / command not found** → install it:
27
-
28
- ```bash
29
- npm install -g @mediamonks/canopy-cli
30
- ```
31
-
32
- (Needs Node.js 20+. If `npm` is missing, tell the user to install Node first.)
33
-
34
- - **`canopy whoami` says not logged in / expired** → run:
35
- ```bash
36
- canopy login
37
- ```
38
- This opens the user's browser. **Then STOP and tell the user to complete the
39
- sign-in in their browser.** The command waits for them and finishes on its own.
40
- - Never ask for or type the user's password, OneLogin codes, or any
41
- credentials yourself.
42
- - Login only works on the user's own machine (it uses a `127.0.0.1` redirect).
43
- If you're on a remote/headless box, tell the user — it can't complete there.
44
- - `canopy login --json` gives machine-readable status if you need to parse it.
45
-
46
- Skip login if `canopy whoami` already shows a valid, unexpired token.
47
-
48
- ## Then: offer to build their first project
49
-
50
- Once the CLI is installed and signed in, **do not just print a list of
51
- commands and stop.** The person following the getting-started prompt is often
52
- non-technical and expects you to drive. Take the lead:
53
-
54
- 1. Ask them, in plain language, whether they'd like to create their first
55
- Canopy project now — and **what it's about** (what should it do, who's it
56
- for). Ask one short question at a time; wait for the answer.
57
- 2. From their answer, pick a sensible name and template (see the template
58
- guidance below — default to `spa` for anything beyond a trivial page), then
59
- scaffold it for them non-interactively, e.g.
60
- `canopy create "Their App Name" --template spa`.
61
- 3. `cd` into the new folder, then keep going — build the thing they described,
62
- running `canopy dev` to preview and `canopy docs`/`canopy <cmd> --help`
63
- whenever you need platform details.
64
-
65
- Only fall back to handing them raw commands if they say they'd rather do it
66
- themselves. The goal is a working first project, not a command cheat-sheet.
67
-
68
- ## Then: read the docs for whatever you're doing
69
-
70
- The CLI ships the full documentation and keeps it fresh from the platform. List
71
- the topics and read the relevant one _before_ running unfamiliar commands:
72
-
73
- ```bash
74
- canopy docs # list all topics
75
- canopy docs deploy # print one topic
76
- ```
77
-
78
- The listing is grouped by section: guides (`getting-started`, `deploy`, `dev`,
79
- `data`, `ci`, `troubleshooting`, …), platform feature pages (`platform-*`), a
80
- per-command CLI reference (`cli-<command>`, plus `cli` for the one-page
81
- overview), and the SDK reference (`sdk-*`). Always prefer `canopy docs` and
82
- `canopy <command> --help` over guessing flags — they are the source of truth
83
- and never go stale.
84
-
85
- ## Command map (quick orientation — see `canopy docs cli` for detail)
86
-
87
- - Create a **new** project: `canopy create`
88
- - Set up a folder for an **existing** project (e.g. made in the dashboard):
89
- `canopy init <slug>` — never use `canopy create` for a project that already
90
- exists; that would register a duplicate. `canopy init <slug> .` targets the
91
- current directory; add `--no-scaffold` to link an app you already have.
92
- - Run locally: `canopy dev` (live data) or `canopy dev --local` (offline sandbox)
93
- - Deploy: `canopy deploy` (add `--build` if the project has a build step)
94
- - Test first on staging: `canopy deploy --staging` → verify → `canopy promote`
95
- (own database cloned from live; `canopy staging status | sync | destroy`)
96
- - Roll back: `canopy rollback`
97
- - Open the live site: `canopy open`
98
- - Manage data: `canopy db pull | snapshot | snapshots | restore | migrate`
99
- - Work bug reports & feedback: `canopy feedback list | show | comment | status | report`
100
- — see `canopy docs platform-feedback` for the triage and reporting loops
101
- - CI credentials: `canopy token create | list | revoke`
102
-
103
- ## Custom tables & migrations (devs; Drizzle recommended)
104
-
105
- The document API (`canopy.db.collection(…)`) needs no schema — prefer it. When
106
- a project defines its **own tables** (raw `canopy.db.sql()` / Drizzle), use the
107
- migrations flow — read `canopy docs migrations` first. The rules that prevent
108
- confusing failures:
109
-
110
- - Pin `"dialect"` and `"migrations"` in `canopy.json`; put ordered `.sql`
111
- files in that directory (Drizzle: `npx drizzle-kit generate` writes them).
112
- - **There is no `DATABASE_URL` and never will be.** Never run
113
- `drizzle-kit push`/`migrate`/`studio` or Drizzle's runtime `migrate()` —
114
- generating is local, applying is Canopy's job: `canopy deploy` applies
115
- pending migrations automatically (`canopy db migrate` applies without a
116
- deploy; `canopy dev --local` applies to the sandbox on boot).
117
- - Never edit an applied migration (the platform refuses edited history) and
118
- never reference `_canopy_*` tables.
119
- - **Schema change? Rehearse it on staging.** `canopy deploy --staging` applies
120
- pending migrations to the staging database (a clone of live) — verify the
121
- app there, then `canopy promote` replays them against live and flips the
122
- version. Never promote with failing checks.
123
-
124
- ## Choosing a starter template (`canopy create` / `canopy init`)
125
-
126
- Both `canopy create` and `canopy init` scaffold from the same three templates.
127
- **`static` is the default, but reach
128
- for it only when the app is genuinely trivial** — a page or two with light SDK
129
- use. Its SDK is served untyped from the platform (`/_api/data/v1/sdk.js`): no
130
- autocomplete, no type-checking, mistakes only show up in the browser.
131
-
132
- Prefer **`spa`** the moment either is true — which covers most real projects:
133
-
134
- - **You lean on the SDK** (`db` queries, `blob`, `ai` — anything past a stray
135
- `auth.user()`). `spa` installs the SDK from npm (`@mediamonks/canopy`), fully
136
- typed, so the compiler catches mistakes as you write them.
137
- - **The app will grow.** `spa` is Vite + React + TypeScript, giving components,
138
- routing, and state a real home; `static` has no build step to organise into.
139
-
140
- Use **`worker`** when you need server-side backend code — e.g. calling an
141
- external API with a secret the browser can't hold, or running scheduled jobs.
142
- It's `spa` plus Hono-style handlers in `worker/`; `canopy deploy` publishes
143
- the frontend and the worker together.
144
-
145
- Non-interactive: `canopy create "My App" --template spa`. **When unsure between
146
- `static` and `spa`, pick `spa`.**
147
-
148
- ## Safety rules
149
-
150
- - **Destructive commands** — `canopy db restore` and `canopy delete` wipe data.
151
- Confirm intent with the user first, and take a `canopy db snapshot` before
152
- restores.
153
- - **Deploys are public.** `canopy deploy` makes a version live. Confirm before
154
- deploying unless the user clearly asked you to.
155
- - Keep the CLI current: `canopy upgrade` (this also refreshes this skill).
156
- - **A `canopy` command itself failing?** First rule out user error (re-read
157
- `canopy docs troubleshooting` and the command's `--help`). If the platform
158
- really misbehaved, check `canopy feedback mine` for a duplicate, then file it:
159
- `canopy feedback report "<what happened>" --command "<the command>" --error "<output>"`
160
- — it reaches the platform team directly (no `--project` needed).
@@ -1,34 +0,0 @@
1
- # Deploy to Canopy on every push to main. GitHub's runners are the build
2
- # farm — the platform owns no build infrastructure.
3
- #
4
- # Needs a CANOPY_DEPLOY_TOKEN repo secret: create a deploy token in the
5
- # dashboard (project → Settings → Deploy tokens) or with `canopy token
6
- # create`, then add it via `gh secret set CANOPY_DEPLOY_TOKEN` or the repo's
7
- # Settings → Secrets. Until the secret is set, this workflow skips itself
8
- # instead of failing the build.
9
- name: Deploy to Canopy
10
-
11
- on:
12
- push:
13
- branches: [main]
14
-
15
- jobs:
16
- deploy:
17
- runs-on: ubuntu-latest
18
- env:
19
- CANOPY_DEPLOY_TOKEN: ${{ secrets.CANOPY_DEPLOY_TOKEN }}
20
- steps:
21
- - uses: actions/checkout@v4
22
- - uses: actions/setup-node@v4
23
- with:
24
- node-version: 22
25
- - name: Install dependencies
26
- if: env.CANOPY_DEPLOY_TOKEN != ''
27
- run: |
28
- if [ -f package-lock.json ]; then npm ci; else npm install; fi
29
- - name: Deploy
30
- if: env.CANOPY_DEPLOY_TOKEN != ''
31
- run: npm exec --yes @mediamonks/canopy-cli deploy
32
- - name: Skipped (no deploy token)
33
- if: env.CANOPY_DEPLOY_TOKEN == ''
34
- run: echo "CANOPY_DEPLOY_TOKEN secret not set — skipping deploy."
@@ -1,47 +0,0 @@
1
- # {{name}} — a Canopy React SPA
2
-
3
- A Vite + React single-page app on [Canopy](https://dashboard.thanarie.dev),
4
- Monks' managed platform for internal projects. Everything is served behind
5
- company login; there is no infrastructure to manage and none should be added.
6
-
7
- ## Canopy skill
8
-
9
- The full SDK reference (auth, db, blob, ai) and platform guardrails ship as an
10
- agent skill, kept out of this scaffold so it stays current. Install it before
11
- building features:
12
-
13
- ```
14
- npx skills add mediamonks/canopy
15
- ```
16
-
17
- If that fails the skill isn't published yet — the SDK surface is documented on
18
- the [Canopy dashboard](https://dashboard.thanarie.dev).
19
-
20
- ## Working on this project
21
-
22
- - A standard Vite + React + TypeScript app; `npm install` once, then develop
23
- with `canopy dev` (it runs Vite with this project's `vite.config.ts` and
24
- proxies `/_api` to the platform).
25
- - The SDK comes from npm (`@mediamonks/canopy`) — the shared instance lives in
26
- `src/canopy.ts`.
27
- - `canopy dev` uses LIVE project data: read freely, but take a
28
- `canopy db snapshot` before risky data changes.
29
- - `canopy dev --local [--pull]` — offline sandbox with local data instead;
30
- writes never reach the platform. Prefer it for destructive experiments.
31
- - `canopy deploy` — builds (`npm run build`) and publishes `dist/`; live
32
- (behind company login) at https://{{slug}}.thanarie.dev.
33
- - Risky change (schema migration, backend rework)? `canopy deploy --staging`
34
- first — a private copy with its own database cloned from live — verify, then
35
- `canopy promote` ships the exact tested version. Never promote with failing
36
- checks.
37
-
38
- ## Boundaries
39
-
40
- - Do not add auth — the platform authenticates every request before it
41
- reaches this app.
42
- - Do not store secrets anywhere in this project, and never call external APIs
43
- that need them from the browser — all backend needs go through the Canopy
44
- SDK (`canopy.auth`, `canopy.db`, `canopy.blob`, `canopy.ai`).
45
- - Frontend only: no SSR, no custom servers, no databases outside `canopy.db`.
46
- (Need backend code? Recreate on the `worker` template, or ask the Canopy
47
- team.)