@mediamonks/canopy-cli 0.7.12 → 0.7.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{db-8Etjmzxh.mjs → db-DKnJQPm5.mjs} +2 -2
- package/dist/{deploy-B1WrPq5W.mjs → deploy-CnxJ4TE6.mjs} +2 -2
- package/dist/dev-BqLvttKc.mjs +2 -0
- package/dist/{dev-BnUYGQQm.mjs → dev-CyJT6EfQ.mjs} +4 -4
- package/dist/docs/ci.md +50 -0
- package/dist/docs/cli-create.md +46 -0
- package/dist/docs/cli-db.md +86 -0
- package/dist/docs/cli-delete.md +44 -0
- package/dist/docs/cli-deploy.md +58 -0
- package/dist/docs/cli-dev.md +82 -0
- package/dist/docs/cli-docs.md +47 -0
- package/dist/docs/cli-download.md +42 -0
- package/dist/docs/cli-egress.md +37 -0
- package/dist/docs/cli-feedback.md +75 -0
- package/dist/docs/cli-index.md +61 -0
- package/dist/docs/cli-init.md +46 -0
- package/dist/docs/cli-login.md +43 -0
- package/dist/docs/cli-logout.md +32 -0
- package/dist/docs/cli-logs.md +57 -0
- package/dist/docs/cli-open.md +41 -0
- package/dist/docs/cli-projects.md +43 -0
- package/dist/docs/cli-promote.md +44 -0
- package/dist/docs/cli-rollback.md +45 -0
- package/dist/docs/cli-skill.md +39 -0
- package/dist/docs/cli-staging.md +54 -0
- package/dist/docs/cli-token.md +47 -0
- package/dist/docs/cli-upgrade.md +43 -0
- package/dist/docs/cli-usage.md +43 -0
- package/dist/docs/cli-whoami.md +46 -0
- package/dist/docs/cli.md +86 -0
- package/dist/docs/data.md +85 -0
- package/dist/docs/deploy.md +81 -0
- package/dist/docs/dev.md +72 -0
- package/dist/docs/developers-agents-and-ai.md +95 -0
- package/dist/docs/developers-architecture.md +106 -0
- package/dist/docs/getting-started.md +108 -0
- package/dist/docs/index.json +336 -0
- package/dist/docs/infrastructure.md +406 -0
- package/dist/docs/migrations.md +183 -0
- package/dist/docs/overview-faq.md +105 -0
- package/dist/docs/overview-how-it-works.md +82 -0
- package/dist/docs/overview-use-cases.md +76 -0
- package/dist/docs/overview-what-is-canopy.md +75 -0
- package/dist/docs/platform-ai-usage.md +65 -0
- package/dist/docs/platform-client-access.md +54 -0
- package/dist/docs/platform-deploys-and-rollbacks.md +93 -0
- package/dist/docs/platform-egress.md +75 -0
- package/dist/docs/platform-feedback.md +114 -0
- package/dist/docs/platform-google.md +213 -0
- package/dist/docs/platform-members-and-access.md +71 -0
- package/dist/docs/platform-project-data.md +76 -0
- package/dist/docs/platform-schedules.md +90 -0
- package/dist/docs/platform-sites-and-profiles.md +61 -0
- package/dist/docs/porting-nextjs.md +233 -0
- package/dist/docs/sdk-ai.md +103 -0
- package/dist/docs/sdk-auth.md +57 -0
- package/dist/docs/sdk-database.md +126 -0
- package/dist/docs/sdk-files.md +82 -0
- package/dist/docs/sdk-services.md +58 -0
- package/dist/docs/sdk.md +126 -0
- package/dist/docs/staging.md +106 -0
- package/dist/docs/static-serving.md +323 -0
- package/dist/docs/troubleshooting.md +102 -0
- package/dist/{docs-DxB7NhZs.mjs → docs-BBMVxVta.mjs} +2 -2
- package/dist/{egress-CN_axsAN.mjs → egress-BFSwa8Uu.mjs} +1 -1
- package/dist/{feedback-B-GXUBPs.mjs → feedback-BMX_d4KZ.mjs} +2 -2
- package/dist/index.mjs +25 -24
- package/dist/{local-BGRd_FMZ.mjs → local-BQxsp-ZQ.mjs} +2 -2
- package/dist/{login-D7dI8D9N.mjs → login-BDTO3nVw.mjs} +1 -1
- package/dist/{logs-DNiZquki.mjs → logs-C53y2eAS.mjs} +1 -1
- package/dist/{migrations-C-5x6SYf.mjs → migrations-C6EcKD6a.mjs} +1 -1
- package/dist/{open-DRPp4QJg.mjs → open-C5x3tX6I.mjs} +1 -1
- package/dist/{package-CDLufecy.mjs → package-vJXq4RNN.mjs} +1 -1
- package/dist/{projects-C6JQjVqg.mjs → projects-MMbv7MmK.mjs} +1 -1
- package/dist/schedule-Cq9K0rnE.mjs +106 -0
- package/dist/skill/canopy/SKILL.md +160 -0
- package/dist/{skill-BOWfqwr2.mjs → skill-Zq9aIPKf.mjs} +2 -2
- package/dist/{staging-Bw1D_J8M.mjs → staging-D8G4amjO.mjs} +1 -1
- package/dist/templates/spa/.github/workflows/canopy-deploy.yml +34 -0
- package/dist/templates/spa/AGENTS.md +47 -0
- package/dist/templates/spa/README.md +14 -0
- package/dist/templates/spa/_gitignore +4 -0
- package/dist/templates/spa/canopy.json +5 -0
- package/dist/templates/spa/index.html +12 -0
- package/dist/templates/spa/package.json +21 -0
- package/dist/templates/spa/src/app.tsx +25 -0
- package/dist/templates/spa/src/canopy.ts +5 -0
- package/dist/templates/spa/src/main.tsx +10 -0
- package/dist/templates/spa/src/style.css +29 -0
- package/dist/templates/spa/tsconfig.json +16 -0
- package/dist/templates/spa/vite.config.ts +7 -0
- package/dist/templates/static/AGENTS.md +45 -0
- package/dist/templates/static/README.md +9 -0
- package/dist/templates/static/_gitignore +2 -0
- package/dist/templates/static/canopy.json +4 -0
- package/dist/templates/static/index.html +21 -0
- package/dist/templates/static/main.js +9 -0
- package/dist/templates/static/style.css +29 -0
- package/dist/templates/worker/.dev.vars.example +9 -0
- package/dist/templates/worker/.github/workflows/canopy-deploy.yml +34 -0
- package/dist/templates/worker/AGENTS.md +63 -0
- package/dist/templates/worker/README.md +17 -0
- package/dist/templates/worker/_gitignore +5 -0
- package/dist/templates/worker/canopy.json +6 -0
- package/dist/templates/worker/index.html +12 -0
- package/dist/templates/worker/package.json +22 -0
- package/dist/templates/worker/src/app.tsx +25 -0
- package/dist/templates/worker/src/canopy.ts +5 -0
- package/dist/templates/worker/src/main.tsx +10 -0
- package/dist/templates/worker/src/style.css +29 -0
- package/dist/templates/worker/tsconfig.json +16 -0
- package/dist/templates/worker/vite.config.ts +7 -0
- package/dist/templates/worker/worker/index.ts +52 -0
- package/dist/{token-D7zrIF8G.mjs → token-BUxgRJVA.mjs} +1 -1
- package/dist/{ui-2mrJcjYC.mjs → ui-B1Px5y7q.mjs} +17 -0
- package/dist/{upgrade-fhfrid_O.mjs → upgrade-ES8u8_Pn.mjs} +2 -2
- package/dist/{usage-CX1Izj_l.mjs → usage-DjNngyvy.mjs} +1 -1
- package/package.json +1 -1
- package/dist/dev-CySBxW7I.mjs +0 -2
package/dist/docs/sdk.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Using the SDK
|
|
3
|
+
description: One typed client for auth, data, files, AI, and services — no keys, no connection strings, the platform authenticates for you.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`@mediamonks/canopy` is a thin, typed client over the platform's project API.
|
|
7
|
+
One `createCanopy()` call gives you five modules — `auth`, `db`, `blob`, `ai`,
|
|
8
|
+
and `services` — and that is the entire surface. There are no API keys,
|
|
9
|
+
connection strings, or environment variables to manage: every call rides the
|
|
10
|
+
session the platform already established before your code ran.
|
|
11
|
+
|
|
12
|
+
The same code works everywhere your project runs — in the browser during
|
|
13
|
+
`canopy dev`, in the deployed site, and in a project's backend worker.
|
|
14
|
+
|
|
15
|
+
## Getting the SDK
|
|
16
|
+
|
|
17
|
+
You usually don't install anything. The `spa` and `worker` templates ship with
|
|
18
|
+
`@mediamonks/canopy` in `package.json`; the `static` template imports the same
|
|
19
|
+
bundle straight from the platform, untyped but always version-matched to the
|
|
20
|
+
API:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// spa / worker templates (typed, from npm)
|
|
24
|
+
import { createCanopy } from "@mediamonks/canopy";
|
|
25
|
+
|
|
26
|
+
// static template (untyped, served by the platform)
|
|
27
|
+
import { createCanopy } from "/_api/data/v1/sdk.js";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## createCanopy(config)
|
|
31
|
+
|
|
32
|
+
`createCanopy` takes an optional config object and returns the client:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { createCanopy } from "@mediamonks/canopy";
|
|
36
|
+
|
|
37
|
+
const canopy = createCanopy();
|
|
38
|
+
// canopy.auth, canopy.db, canopy.blob, canopy.ai, canopy.services
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Option | Type | Default | What it does |
|
|
42
|
+
| --------- | -------------- | -------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `baseUrl` | `string` | `"/_api"` | Base URL of the platform data API. The default is same-origin, so it works untouched in the browser. |
|
|
44
|
+
| `token` | `string` | none | A project-scoped `cnpy_` token sent as a bearer on every request — for scripts and CI running outside Canopy hosting. |
|
|
45
|
+
| `fetch` | `typeof fetch` | global `fetch` | Override how requests are sent — a backend worker passes its platform service binding here. |
|
|
46
|
+
|
|
47
|
+
In the browser no config is needed. In a backend worker (the `worker`
|
|
48
|
+
template), the platform is reachable through the `CANOPY` service binding, so
|
|
49
|
+
create the client per request with that binding's fetch:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
app.get("/api/hello", async (c) => {
|
|
53
|
+
const canopy = createCanopy({ fetch: c.env.CANOPY.fetch.bind(c.env.CANOPY) });
|
|
54
|
+
const user = await canopy.auth.user();
|
|
55
|
+
return c.json({ message: `Hello from the backend, ${user.name}!` });
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Outside Canopy hosting — a local Node script, a CI job — there is no session
|
|
60
|
+
and no service binding. Mint a dev token (`canopy dev` does this for you, or
|
|
61
|
+
`POST /control/v1/projects/:slug/dev-token`) and pass it with the platform's
|
|
62
|
+
API host:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const canopy = createCanopy({
|
|
66
|
+
baseUrl: "https://api.canopy.website",
|
|
67
|
+
token: process.env.CANOPY_TOKEN, // a project-scoped cnpy_ token
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## One instance, shared
|
|
72
|
+
|
|
73
|
+
In the browser, create the client once and import it everywhere. The scaffolds
|
|
74
|
+
do this in `src/canopy.ts`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// src/canopy.ts
|
|
78
|
+
import { createCanopy } from "@mediamonks/canopy";
|
|
79
|
+
|
|
80
|
+
export const canopy = createCanopy();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Error handling
|
|
84
|
+
|
|
85
|
+
Any call that fails throws a `CanopyError` (exported from the package). It
|
|
86
|
+
extends `Error` with the HTTP `status` of the failed request; `message` carries
|
|
87
|
+
the response body when the platform sent one:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { CanopyError } from "@mediamonks/canopy";
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
await canopy.db.collection("todos").insert({ title: "hi", done: false });
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (err instanceof CanopyError && err.status === 403) {
|
|
96
|
+
// e.g. a viewer hitting a write — show a friendly message
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Two read calls are deliberately soft instead: `collection.get(id)` and
|
|
103
|
+
`blob.get(key)` resolve to `null` for a missing document or object rather than
|
|
104
|
+
throwing.
|
|
105
|
+
|
|
106
|
+
## Where it runs
|
|
107
|
+
|
|
108
|
+
The client is plain `fetch` underneath, so behavior is identical across
|
|
109
|
+
environments:
|
|
110
|
+
|
|
111
|
+
- **Browser, deployed** — calls go to same-origin `/_api`; the platform
|
|
112
|
+
authenticated the visitor before your page loaded, and the session cookie
|
|
113
|
+
rides along automatically.
|
|
114
|
+
- **Browser, `canopy dev`** — the dev server proxies `/_api` to the platform
|
|
115
|
+
with your CLI login, so local code talks to your project's real data.
|
|
116
|
+
- **Backend worker** — pass the `CANOPY` service binding's fetch (shown
|
|
117
|
+
above); the incoming request's identity carries through, so `auth.user()`
|
|
118
|
+
and per-project budgets behave exactly as in the browser.
|
|
119
|
+
|
|
120
|
+
## Modules
|
|
121
|
+
|
|
122
|
+
- [Auth](https://docs.canopy.website/sdk/auth) — who is signed in, and ending sessions.
|
|
123
|
+
- [Database](https://docs.canopy.website/sdk/database) — document collections, plus a raw SQL escape hatch.
|
|
124
|
+
- [Files](https://docs.canopy.website/sdk/files) — private blob storage under the project's prefix.
|
|
125
|
+
- [AI](https://docs.canopy.website/sdk/ai) — metered, budget-capped LLM access.
|
|
126
|
+
- [Services](https://docs.canopy.website/sdk/services) — curated internal data services.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Staging & promoting
|
|
3
|
+
description: Deploy to a safe copy of your project first — own database cloned from live — then promote the exact same version.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Every project can have a **staging channel**: a second, private copy of the
|
|
7
|
+
app with its own database (cloned from live), its own files, and its own
|
|
8
|
+
backend, served at
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
https://staging-dot-<slug>.canopy.website
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
behind the same sign-in as the live site. Deploy there first, click around
|
|
15
|
+
against real-shaped data, then **promote** — the exact version you tested
|
|
16
|
+
becomes live, nothing is rebuilt.
|
|
17
|
+
|
|
18
|
+
You don't set anything up. The first staged deploy creates the channel and
|
|
19
|
+
copies the live database into it.
|
|
20
|
+
|
|
21
|
+
## The workflow
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
canopy deploy --staging # build + deploy to staging (created on first use)
|
|
25
|
+
canopy open --staging # check it in the browser
|
|
26
|
+
canopy promote # happy? make that version live
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`canopy deploy --staging` is the normal deploy in every way — same build, same
|
|
30
|
+
versioning — except the result serves on the staging URL and the live site is
|
|
31
|
+
untouched. Versions share one sequence: if live is at v7 and you stage v8,
|
|
32
|
+
promoting makes that same v8 live.
|
|
33
|
+
|
|
34
|
+
`canopy promote` is not a redeploy: the platform re-uses the exact files and
|
|
35
|
+
backend bundle you staged. If migrations were rehearsed on staging, they replay
|
|
36
|
+
against the live database first (with an automatic snapshot), then the pointer
|
|
37
|
+
flips.
|
|
38
|
+
|
|
39
|
+
## Testing schema migrations
|
|
40
|
+
|
|
41
|
+
This is the staging channel's superpower. Because the staging database starts
|
|
42
|
+
as a copy of live, a migration that works there will behave the same way on
|
|
43
|
+
the real thing:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
canopy deploy --staging # pending migrations apply to the STAGING database
|
|
47
|
+
# … verify the app against the migrated copy …
|
|
48
|
+
canopy promote # the same migrations replay against live, then v goes live
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A migration that fails on staging costs you nothing — live never saw it. See
|
|
52
|
+
[Schema migrations](https://docs.canopy.website/guides/migrations) for the migration system itself.
|
|
53
|
+
|
|
54
|
+
## Staging an older version
|
|
55
|
+
|
|
56
|
+
Any version in the deploy history can be pointed at the staging channel —
|
|
57
|
+
from the dashboard's deploy list (the **Stage** action, next to Rollback) or
|
|
58
|
+
with `canopy staging use v3`. Like rollback, it's a pointer move: nothing is
|
|
59
|
+
rebuilt, and live is untouched. Handy for comparing an old build against the
|
|
60
|
+
staging data, or re-testing a version before promoting it.
|
|
61
|
+
|
|
62
|
+
## Keeping staging fresh
|
|
63
|
+
|
|
64
|
+
Staging data drifts as you test. Re-clone it from live anytime:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
canopy staging sync # replace staging data with a fresh copy of live
|
|
68
|
+
canopy staging status # what's staged vs live
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Writes on staging never flow back to live — promote moves code and schema, not
|
|
72
|
+
data.
|
|
73
|
+
|
|
74
|
+
## What's shared, what's separate
|
|
75
|
+
|
|
76
|
+
Separate per channel: the database, uploaded files, and the backend worker.
|
|
77
|
+
Shared with live: members, secrets, the client password, schedules (they run
|
|
78
|
+
against live only), and the AI budget — staging traffic counts against the
|
|
79
|
+
same cap.
|
|
80
|
+
|
|
81
|
+
## Developing against staging data
|
|
82
|
+
|
|
83
|
+
Remote dev can point at the staging database instead of live — useful while a
|
|
84
|
+
schema is in flux:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
canopy dev --staging
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Cleaning up
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
canopy staging destroy # removes the staging database, files and backend
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Live is untouched, and the next `canopy deploy --staging` recreates the channel
|
|
97
|
+
from a fresh copy. The dashboard's Settings tab has the same controls, the
|
|
98
|
+
Overview tab shows what's staged with a Promote button, and a Live/Staging
|
|
99
|
+
toggle above the Data and Files tabs browses either channel's data.
|
|
100
|
+
|
|
101
|
+
## Staging vs version previews
|
|
102
|
+
|
|
103
|
+
Version preview URLs (`v12-dot-<slug>`) show an old **frontend** against the
|
|
104
|
+
**live** data — good for comparing builds. The staging channel is the full
|
|
105
|
+
package: candidate frontend, candidate backend, and its own database. Use
|
|
106
|
+
previews to look backwards, staging to look forwards.
|
|
@@ -0,0 +1,323 @@
|
|
|
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; directory index, else SPA fallback
|
|
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
|
+
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.
|
|
118
|
+
|
|
119
|
+
### How assets are stored & versioned
|
|
120
|
+
|
|
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.
|
|
130
|
+
|
|
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)).
|
|
147
|
+
|
|
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`.
|
|
152
|
+
|
|
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` |
|
|
@@ -0,0 +1,102 @@
|
|
|
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,7 +1,7 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-vJXq4RNN.mjs";
|
|
2
2
|
import { n as siteConfig } from "./site-BUlQR117.mjs";
|
|
3
3
|
import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
|
|
4
|
-
import { n as reportErrors } from "./ui-
|
|
4
|
+
import { n as reportErrors } from "./ui-B1Px5y7q.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|