@mediamonks/canopy-cli 0.7.19 → 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 (36) 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 +8 -8
  10. package/dist/docs/migrations.md +17 -14
  11. package/dist/docs/notifications.md +14 -1
  12. package/dist/docs/platform-egress.md +2 -2
  13. package/dist/docs/platform-project-data.md +3 -3
  14. package/dist/docs/platform-schedules.md +1 -1
  15. package/dist/docs/platform-sites-and-profiles.md +1 -1
  16. package/dist/docs/sdk-email.md +114 -0
  17. package/dist/docs/sdk-files.md +11 -1
  18. package/dist/docs/sdk.md +4 -4
  19. package/dist/docs/static-serving.md +58 -300
  20. package/dist/{docs-CKZV1X-B.mjs → docs-fiC6q9Ce.mjs} +1 -1
  21. package/dist/{feedback-DBgUpmDC.mjs → feedback-CAwUHwWa.mjs} +1 -1
  22. package/dist/index.mjs +9 -9
  23. package/dist/{local-CKRZur_5.mjs → local-CsSP4Vpt.mjs} +2 -2
  24. package/dist/login-DRYaJcfT.mjs +285 -0
  25. package/dist/{package-qe7a3VEV.mjs → package-C_DLlB96.mjs} +1 -1
  26. package/dist/{skill-DW68KWTY.mjs → skill-CmgMU2Iq.mjs} +1 -1
  27. package/dist/{src-Bu4Ze32I.mjs → src-BKKAEkbH.mjs} +25 -2
  28. package/dist/templates/spa/package.json +1 -1
  29. package/dist/templates/worker/AGENTS.md +3 -0
  30. package/dist/templates/worker/package.json +1 -1
  31. package/dist/templates/worker/worker/index.ts +20 -3
  32. package/dist/{upgrade-DNOIy4g3.mjs → upgrade-D1j0WV4l.mjs} +1 -1
  33. package/package.json +5 -4
  34. package/dist/dev-DjkK-50R.mjs +0 -2
  35. package/dist/docs/infrastructure.md +0 -406
  36. package/dist/login-D9eO1s7W.mjs +0 -401
@@ -1,323 +1,81 @@
1
1
  ---
2
2
  title: How static files are served
3
- description: The full path a browser request takes to a deployed file.
3
+ description: Why every page is a file, and what that means for routing, caching, and deploys.
4
4
  ---
5
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.
6
+ Everything a visitor sees is a **file from your last deploy**. Canopy stores
7
+ the build you shipped and serves it directly — a request for `/`, `/about`, or
8
+ `/reports/42` never runs your code. If your project has a backend, requests
9
+ under `/api/*` go to it; everything else is a file.
9
10
 
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.)
11
+ That one fact explains most of how the platform behaves, so it's worth
12
+ understanding before you fight it.
14
13
 
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.
14
+ ## Nothing is served to a stranger
19
15
 
20
- ## The request path
16
+ Every request passes the sign-in wall before a single byte of your project is
17
+ read. A visitor arrives with a company login, or — if you've turned on
18
+ [client access](https://docs.canopy.website/platform/client-access) — the project's password. There is no
19
+ public URL, no "unlisted" link, and no way to opt a single file out. A project
20
+ is readable by any signed-in colleague unless you set it to collaborators only,
21
+ in [members and access](https://docs.canopy.website/platform/members-and-access).
21
22
 
22
- A request to `https://<slug>.canopy.<domain>/some/path` flows through two
23
- Workers and one R2 bucket:
23
+ ## Deep links work — routing is yours
24
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; directory index, else SPA fallback
38
- │ 6. R2 GET deploys/<project-id>/<version>/<path>
39
- ▼
40
- R2 bucket "canopy-assets"
41
- ```
25
+ When a path doesn't match a file, Canopy serves your `index.html` instead of a 404. That's what makes client-side routing work: someone can bookmark
26
+ `/reports/42`, reload it, and your app boots and handles the route.
42
27
 
43
- ### 1–3. Gateway: hostname, auth, path rewrite
28
+ The trade-off is that a **typo'd asset also gets `index.html`** rather than an
29
+ error. If a script or image comes back as your homepage HTML, you're looking at
30
+ a wrong path, not a broken server.
44
31
 
45
- [`apps/gateway/src/index.ts`](../../apps/gateway/src/index.ts) is the front door
46
- for every `*.canopy.<domain>` request.
32
+ ## Caching, and why a new deploy shows up immediately
47
33
 
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:
34
+ Two different rules, picked automatically:
66
35
 
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
- ```
36
+ - **HTML always revalidates.** Browsers re-check it on every visit, so the
37
+ moment you deploy, people get the new build — no cache-busting dance, no
38
+ waiting.
39
+ - **Assets cache for an hour.** JS, CSS, images, and fonts sit in the visitor's
40
+ browser, which is what keeps repeat visits fast.
77
41
 
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.
42
+ This is the right shape for a normal build tool that puts a content hash in
43
+ asset filenames (Vite does this by default) — the HTML that names them is
44
+ always fresh, so a new deploy points at new files.
81
45
 
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.
46
+ ## Deploys and rollbacks are instant
86
47
 
87
- ### 4–6. API: version resolution, path sanitizing, R2 read
48
+ Shipping a version doesn't copy or rebuild anything at serve time — the
49
+ platform just points at the new one. A [rollback](https://docs.canopy.website/platform/deploys-and-rollbacks)
50
+ is the same move in reverse, which is why it takes effect immediately rather
51
+ than propagating for minutes.
88
52
 
89
- The serving handler is `GET /data/v1/assets/*` in
90
- [`apps/api/src/routes/assets.ts`](../../apps/api/src/routes/assets.ts).
53
+ You can also preview an **older frontend against current data** by putting the
54
+ version in the hostname — `v12-dot-<project>` serves build 12 while auth and all
55
+ data still resolve to the real, current project. Handy for "when did this break?"
56
+ without deploying anything.
91
57
 
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
- and mapped to the R2 key `deploys/<project-id>/<version>/<path>`; the root
107
- path serves `index.html`.
108
- - **Directory index + SPA fallback**: the exact asset is read first. On a
109
- miss, an extensionless path is probed for its own directory index — `/feeds/`
110
- serves `feeds/index.html` (multi-page static exports), and the slash-less
111
- `/feeds` 301-redirects to `/feeds/` so the page's relative URLs resolve.
112
- Only when no directory index exists does the request fall back to the root
113
- `index.html`, so client-side routes (`/settings`, `/projects/42`) still all
114
- resolve to the SPA entry point. Both the fallback `index.html` and the
115
- per-path probe results are served from per-version caches (see
116
- [Performance](#performance-review)), so a deep-linked route doesn't pay a
117
- second R2 read on every request.
58
+ ## What a deploy may contain
118
59
 
119
- ### How assets are stored & versioned
60
+ - Up to **500 files** and **25 MB** in total.
61
+ - An `index.html` — a bundle without one is rejected.
62
+ - Each file's type is fixed at deploy time from its extension. Unknown
63
+ extensions are sent as a download rather than run as code.
120
64
 
121
- `canopy deploy` uses a two-step signed upload
122
- ([`docs/09-plane-split.md`](../09-plane-split.md) D-d): the **control plane**
123
- authorizes the deploy (`POST /control/v1/projects/:slug/deploys`,
124
- [`deploys.ts`](../../apps/api/src/routes/deploys.ts)) and issues a short-lived
125
- signed grant; the CLI PUTs each file to the **data plane**
126
- (`PUT /data/v1/uploads/:grant`,
127
- [`data-plane.ts`](../../apps/api/src/routes/data-plane.ts)); a completion call
128
- (`POST …/deploys/:version/complete`) validates the staged bundle and records
129
- the version. Bundle bytes never transit the control plane.
65
+ If you're over the file count, it's almost always un-bundled assets that a build
66
+ step would combine; over the size limit, it's usually images or fonts that
67
+ belong in [file storage](https://docs.canopy.website/sdk/files) instead of the bundle.
130
68
 
131
- - Each file is written to R2 at `deploys/<project-id>/<version>/<path>` with its
132
- `contentType` stored as R2 `httpMetadata` by the upload endpoint.
133
- - The bucket is `canopy-assets`, bound as `ASSETS`
134
- ([`apps/api/wrangler.main.jsonc`](../../apps/api/wrangler.main.jsonc)).
135
- - **Immutable, append-only versioning.** A deploy bumps the version and writes a
136
- new prefix; the `deployments` row commits at completion _before_
137
- `live_asset_version` moves, so a half-uploaded deploy is never served (an
138
- abandoned grant leaves only unreferenced staged bytes). Rollback
139
- (`POST …/deploys/:version/rollback`,
140
- [`deploys.ts`](../../apps/api/src/routes/deploys.ts)) just repoints
141
- `live_asset_version` at an existing version — instant, nothing copied, nothing
142
- deleted, and any version can be made live again. Old versions keep their bytes
143
- and their identity, which is what makes `v<n>-dot-` preview hosts work.
144
- - Project deletion purges the whole `deploys/<project-id>/` prefix (plus blobs
145
- and snapshots) so no R2 residue remains
146
- ([`projects.ts`](../../apps/api/src/routes/projects.ts)).
69
+ ## What this rules out
147
70
 
148
- The browser SDK itself is served by the platform, not from a deploy bundle:
149
- `GET /_api/data/v1/sdk.js` returns an inlined bundle
150
- ([`data-plane.ts`](../../apps/api/src/routes/data-plane.ts)) so scaffolded
151
- projects need no `npm install`.
71
+ - **Server-side rendering.** No per-request HTML, no server components
72
+ resolved at request time, no incremental regeneration. Render in the browser,
73
+ or move the work into a backend route under `/api/*`.
74
+ - **Redirects and rewrites as configuration.** There's no routing config file;
75
+ handle it in your app, or in your backend.
76
+ - **Serving from anywhere but your build output.** If you want a file to be
77
+ reachable, deploy it, or put it in [file storage](https://docs.canopy.website/sdk/files) and hand out a
78
+ signed link.
152
79
 
153
- ---
154
-
155
- ## Security review
156
-
157
- ### What's solid
158
-
159
- - **Nothing is public.** Every asset request passes the gateway auth wall
160
- (OneLogin session or client-password cookie) before any project bytes are
161
- read, and the gateway→API hop is a private service binding, not an internet
162
- URL. This is the north-star "never public URLs" guarantee.
163
- - **Path-traversal defense.** `safeAssetPath()`
164
- ([`assets.ts`](../../apps/api/src/routes/assets.ts)) rejects on both
165
- ingest (the upload endpoint) and serve: strips a leading `./` or `/`, then
166
- rejects empty paths, paths over 512 chars, and any path containing `\`, `//`,
167
- `\0`, or a `.`/`..`/empty segment. A malicious key can't escape the
168
- `deploys/<project-id>/<version>/` prefix, and one project can never read
169
- another's bytes because the prefix is keyed by the resolved project id.
170
- - **Tenant isolation by key prefix and by stamp.** The R2 key always embeds the
171
- resolved `project.id`, so cross-project reads are structurally impossible even
172
- if a path slipped through. Serving resolves its project from the gateway's
173
- vouched `X-Canopy-Project` stamp alone — never a query parameter — so a
174
- crafted query string can't render one project's pages under another's host.
175
- - **Content-Type is fixed at deploy time**, from an allowlist keyed by extension
176
- (`contentTypeFor()`, [`assets.ts`](../../apps/api/src/routes/assets.ts)),
177
- and stored as R2 metadata by the upload endpoint. Serving echoes that stored
178
- type; unknown extensions become `application/octet-stream` (download, not
179
- execute). The type is never derived from request input at serve time.
180
- - **Deploy bundle limits.** Max 500 files and 25 MB total
181
- ([`storage-keys.ts`](../../apps/api/src/storage-keys.ts)), enforced per file
182
- at upload and re-checked over the staged inventory at completion; a bundle
183
- must contain `index.html` or completion is rejected — bounds the blast radius
184
- of a bad upload. Upload grants are HMAC-signed, project- and version-bound,
185
- and expire after 15 minutes ([`upload-grant.ts`](../../apps/api/src/upload-grant.ts)).
186
- - **Password page escapes untrusted input.** `escapeHtml()` and the same-site
187
- `safeLocalPath()`/`safeReturnTo()` guards prevent reflected-XSS and open-redirect
188
- via the `returnTo` parameter
189
- ([`client.ts`](../../apps/gateway/src/client.ts),
190
- [`index.ts`](../../apps/gateway/src/index.ts)).
191
- - **Version pinning can't leak data.** A `v<n>-dot-` host pins only _which
192
- frontend build_ is served; auth, client-access, and all `/_api` data-plane
193
- calls resolve to the real project (the stamp names the real slug),
194
- so an old build cannot be used to read a different project's data.
195
-
196
- ### Gaps / things to weigh
197
-
198
- These are observations, not known incidents — worth a conscious decision:
199
-
200
- 1. **No hardening response headers.** Served assets carry `content-type`, `etag`,
201
- and `cache-control` only ([`assets.ts`](../../apps/api/src/routes/assets.ts)).
202
- There is **no** `X-Content-Type-Options: nosniff`, no `Content-Security-Policy`,
203
- no `X-Frame-Options`/`frame-ancestors`, no `Referrer-Policy`, and no HSTS. For
204
- an app platform serving arbitrary user HTML/JS on shared `*.canopy.<domain>`
205
- subdomains, the most impactful additions are:
206
- - `X-Content-Type-Options: nosniff` — cheap, prevents MIME-sniffing of e.g. a
207
- user-uploaded file into script.
208
- - `frame-ancestors`/`X-Frame-Options` — clickjacking, and stops one canopy
209
- subdomain from framing another.
210
- - A default `Content-Security-Policy` (even a permissive one) as a baseline.
211
- Note projects run against a shared cookie domain, so cross-subdomain
212
- concerns (cookie scoping, framing) matter more here than on an isolated
213
- site.
214
- 2. **`text/html` is served for deployed HTML with the session cookie present.**
215
- That's by design (auth-gated apps), but it means any XSS in a deployed project
216
- runs on a `*.canopy.<domain>` origin that shares the session-cookie domain.
217
- The session cookie's flags (HttpOnly/SameSite/Secure) in
218
- [`session.ts`](../../apps/gateway/src/session.ts) are the mitigation to keep an
219
- eye on; per-project origin isolation would be the stronger fix.
220
- 3. **SPA fallback returns `index.html` with 200 for unknown paths**, including
221
- things that look like missing assets (`/assets/typo.js` → HTML). That's normal
222
- SPA behavior but can mask 404s and, combined with #1, is another reason
223
- `nosniff` matters (an HTML body served where a script was expected).
224
- 4. **Serving is company-wide readable by default.** Any authenticated monk can
225
- view any active project unless it opts into collaborators-only
226
- (`members_only`, enforced in [`assets.ts`](../../apps/api/src/routes/assets.ts)
227
- via `checkAppAccess`). The default is an intentional product decision (see
228
- docs/02-decisions.md), not a bug — but it means "authenticated" ≠
229
- "authorized for this specific project" on the read path unless the project
230
- tightens it.
231
-
232
- ---
233
-
234
- ## Performance review
235
-
236
- ### What helps
237
-
238
- - **Split caching policy by type** ([`assets.ts`](../../apps/api/src/routes/assets.ts)):
239
-
240
- ```ts
241
- "cache-control": type.startsWith("text/html")
242
- ? "no-cache" // HTML always revalidates → new deploys show up
243
- : "private, max-age=3600", // hashed subresources cache for 1h
244
- ```
245
-
246
- HTML revalidates so a fresh deploy is picked up immediately; hashed JS/CSS/font
247
- assets cache in the browser for an hour. This is the right shape for a
248
- content-hashed SPA build.
249
-
250
- - **ETags** (`"v<version>-<path>"`, [`assets.ts`](../../apps/api/src/routes/assets.ts))
251
- give a stable validator per version+path, so revalidation of `no-cache` HTML
252
- can come back as a cheap `304`. Because the etag encodes the version, a rollback
253
- or new deploy changes the validator and busts caches correctly.
254
- - **Instant deploys and rollbacks.** Serving reads a pointer
255
- (`live_asset_version`); deploy/rollback move the pointer without copying bytes,
256
- so there's no propagation delay and no rebuild.
257
- - **Service-binding hop.** Gateway→API is an in-Cloudflare service binding, not a
258
- public fetch, so there's no extra internet round-trip or TLS handshake between
259
- the two Workers.
260
-
261
- ### Bottlenecks / opportunities
262
-
263
- 1. **`max-age=3600` is modest for hashed assets.** Content-hashed filenames (the
264
- Vite default) can safely use `public, max-age=31536000, immutable`. The current
265
- value is `private` (see #2) and only an hour, so returning visitors refetch
266
- more than necessary. Consider keying on filename hash pattern or a deploy-time
267
- flag to mark truly immutable assets.
268
- 2. **`private` prevents any shared/edge caching.** Because assets are auth-gated
269
- and served `private`, they can't be cached by Cloudflare's CDN or any shared
270
- proxy — every cache miss goes Worker→R2. For an auth-walled platform that's a
271
- deliberate trade (privacy over edge-cache hit rate), but it means R2 latency is
272
- on the critical path for every uncached request. If per-project caching is ever
273
- wanted, the Cache API keyed by (project, version, path) inside the Worker would
274
- let you cache without making anything public.
275
- 3. **No compression / `Content-Encoding`.** The handler streams R2 bytes as-is
276
- with no gzip/brotli negotiation and ignores `Accept-Encoding`. Cloudflare may
277
- apply compression at the edge for eligible content types, but nothing is stored
278
- pre-compressed and the Worker doesn't negotiate it. For large JS bundles,
279
- storing brotli-precompressed variants (or relying on and verifying edge
280
- compression) is worth measuring.
281
- 4. **Range requests / streaming media.** The handler returns the full R2 object
282
- body and doesn't honor `Range`. Fine for a bundled SPA; if projects ever serve
283
- large video/audio, add `Range` support (R2 supports ranged `get`).
284
-
285
- ### Addressed — in-worker caches
286
-
287
- Two former bottlenecks are now handled by best-effort, per-isolate caches
288
- (a performance layer only — never a source of truth; a cold isolate just reads
289
- D1/R2):
290
-
291
- - **SPA fallback no longer double-reads R2.** The serve path reads the exact
292
- asset once; on a miss it falls back to `index.html` served from a per-`(project,
293
- version)` cache ([`asset-cache.ts`](../../apps/api/src/asset-cache.ts)). Because a
294
- deployed version's bytes are immutable, that entry is cached indefinitely
295
- (count-bounded), so the second R2 read is paid **once per version**, not once
296
- per deep-linked route. Verified by a test that serves three client routes and
297
- asserts a single `index.html` read.
298
- - **Project state is cached per slug** as a `ProjectSnapshot` in the
299
- `ProjectDirectory` ([`src/directory.ts`](../../apps/api/src/directory.ts) —
300
- the generalization of the old metadata cache), sparing the hot path its
301
- control-DB queries. Since this data _does_ change, deploy, rollback, and
302
- delete call `invalidateSnapshot()` so a write is reflected immediately on the
303
- same isolate; a short TTL (5 s) is only a backstop for a write that lands on
304
- a different isolate than the next read.
305
-
306
- ---
307
-
308
- ## Quick reference
309
-
310
- | Concern | Where | Notes |
311
- | --------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
312
- | Front door / auth / routing | [`apps/gateway/src/index.ts`](../../apps/gateway/src/index.ts) | Hostname→slug, auth wall, `/data/v1/assets` rewrite + project stamp |
313
- | Client-password page | [`apps/gateway/src/client.ts`](../../apps/gateway/src/client.ts) | HTML-escaped, same-site returnTo |
314
- | Asset serve handler | [`apps/api/src/routes/assets.ts`](../../apps/api/src/routes/assets.ts) | Version resolve, path sanitize, SPA fallback, headers |
315
- | Project snapshots | [`apps/api/src/directory.ts`](../../apps/api/src/directory.ts) | `ProjectDirectory` cache (5 s TTL), invalidated on write |
316
- | index.html cache | [`apps/api/src/asset-cache.ts`](../../apps/api/src/asset-cache.ts) | Per-version, immutable bytes, count-bounded |
317
- | Path sanitizing | [`assets.ts`](../../apps/api/src/routes/assets.ts) | `safeAssetPath()` — traversal defense, shared with the upload endpoint |
318
- | Content-Type allowlist | [`assets.ts`](../../apps/api/src/routes/assets.ts) | Fixed at deploy, stored in R2 metadata |
319
- | Deploy authorize / complete | [`apps/api/src/routes/deploys.ts`](../../apps/api/src/routes/deploys.ts) | Signed grant, 500 files / 25 MB, pointer moves last |
320
- | 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 |
321
- | Rollback | [`deploys.ts`](../../apps/api/src/routes/deploys.ts) | Repoints `live_asset_version` |
322
- | R2 bucket binding | [`apps/api/wrangler.main.jsonc`](../../apps/api/wrangler.main.jsonc) | `ASSETS` → `canopy-assets` |
323
- | 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` |
80
+ Porting an app that assumed a server? [Porting a Next.js app](https://docs.canopy.website/guides/porting-nextjs)
81
+ walks through the restructure.
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-qe7a3VEV.mjs";
1
+ import { t as version } from "./package-C_DLlB96.mjs";
2
2
  import { n as siteConfig } from "./site-BFaWfSVu.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
4
  import { r as reportErrors } from "./ui--atbZ-TM.mjs";
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-qe7a3VEV.mjs";
1
+ import { t as version } from "./package-C_DLlB96.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BFaWfSVu.mjs";
3
3
  import { c as apiDownload, l as apiRequest, r as reportErrors, t as formatDateTime, u as apiUpload } from "./ui--atbZ-TM.mjs";
4
4
  import { defineCommand } from "citty";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-qe7a3VEV.mjs";
2
+ import { t as version } from "./package-C_DLlB96.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BFaWfSVu.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -60,14 +60,14 @@ const main = defineCommand({
60
60
  description: "canopy — create and deploy vibecoded projects"
61
61
  },
62
62
  subCommands: {
63
- login: () => import("./login-D9eO1s7W.mjs").then((m) => m.loginCommand),
64
- whoami: () => import("./login-D9eO1s7W.mjs").then((m) => m.whoamiCommand),
65
- logout: () => import("./login-D9eO1s7W.mjs").then((m) => m.logoutCommand),
63
+ login: () => import("./login-DRYaJcfT.mjs").then((m) => m.loginCommand),
64
+ whoami: () => import("./login-DRYaJcfT.mjs").then((m) => m.whoamiCommand),
65
+ logout: () => import("./login-DRYaJcfT.mjs").then((m) => m.logoutCommand),
66
66
  create: () => import("./projects-1r5387ui.mjs").then((m) => m.createCommand),
67
67
  init: () => import("./projects-1r5387ui.mjs").then((m) => m.initCommand),
68
68
  projects: () => import("./projects-1r5387ui.mjs").then((m) => m.projectsCommand),
69
69
  delete: () => import("./projects-1r5387ui.mjs").then((m) => m.deleteCommand),
70
- dev: () => import("./dev-DjkK-50R.mjs").then((m) => m.devCommand),
70
+ dev: () => import("./dev-D7v7WK9O.mjs").then((m) => m.devCommand),
71
71
  deploy: () => import("./deploy-DCggsLD7.mjs").then((m) => m.deployCommand),
72
72
  promote: () => import("./deploy-DCggsLD7.mjs").then((m) => m.promoteCommand),
73
73
  staging: () => import("./staging-CdpV2YHB.mjs").then((m) => m.stagingCommand),
@@ -76,13 +76,13 @@ const main = defineCommand({
76
76
  token: () => import("./token-C8Bmzwfl.mjs").then((m) => m.tokenCommand),
77
77
  egress: () => import("./egress-BhbQfjjV.mjs").then((m) => m.egressCommand),
78
78
  schedule: () => import("./schedule-DK_Cv6Xf.mjs").then((m) => m.scheduleCommand),
79
- feedback: () => import("./feedback-DBgUpmDC.mjs").then((m) => m.feedbackCommand),
79
+ feedback: () => import("./feedback-CAwUHwWa.mjs").then((m) => m.feedbackCommand),
80
80
  db: () => import("./db-TaSLpQZ5.mjs").then((m) => m.dbCommand),
81
81
  usage: () => import("./usage-CRpVLLi6.mjs").then((m) => m.usageCommand),
82
82
  open: () => import("./open-D7f1wNn3.mjs").then((m) => m.openCommand),
83
- docs: () => import("./docs-CKZV1X-B.mjs").then((m) => m.docsCommand),
84
- skill: () => import("./skill-DW68KWTY.mjs").then((m) => m.skillCommand),
85
- upgrade: () => import("./upgrade-DNOIy4g3.mjs").then((m) => m.upgradeCommand),
83
+ docs: () => import("./docs-fiC6q9Ce.mjs").then((m) => m.docsCommand),
84
+ skill: () => import("./skill-CmgMU2Iq.mjs").then((m) => m.skillCommand),
85
+ upgrade: () => import("./upgrade-D1j0WV4l.mjs").then((m) => m.upgradeCommand),
86
86
  logs: () => import("./logs-Beqz--Df.mjs").then((m) => m.logsCommand)
87
87
  }
88
88
  });
@@ -1,6 +1,6 @@
1
1
  import { s as apiDataRequest } from "./ui--atbZ-TM.mjs";
2
- import { n as META_COLUMNS, r as blobRouter, t as dbRouter } from "./src-Bu4Ze32I.mjs";
3
- import { n as rewriteApiPath } from "./dev-DtEwiNYi.mjs";
2
+ import { n as META_COLUMNS, r as blobRouter, t as dbRouter } from "./src-BKKAEkbH.mjs";
3
+ import { n as rewriteApiPath } from "./dev-CNqx_53I.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join, relative, sep } from "node:path";