@mediamonks/canopy-cli 0.7.42 → 0.7.44

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 (63) hide show
  1. package/dist/{ai-DpQ1nLka.mjs → ai-FBHTSCLZ.mjs} +2 -2
  2. package/dist/{api-Do9c8tZ_.mjs → api-Bl6SG8Oq.mjs} +2 -2
  3. package/dist/{browse-Dp5HytqK.mjs → browse-B8-0dweq.mjs} +2 -2
  4. package/dist/{db-BNsX7sXo.mjs → db-BJJGydgL.mjs} +3 -3
  5. package/dist/{deploy-CXMLABvH.mjs → deploy-ExrIeX_m.mjs} +22 -3
  6. package/dist/dev-CUJKgaqJ.mjs +2 -0
  7. package/dist/{dev-CPR-9slK.mjs → dev-Cq98yblV.mjs} +98 -16
  8. package/dist/{dev-worker-D78leKBJ.mjs → dev-worker-CcYwQjxz.mjs} +23 -10
  9. package/dist/docs/agent-browsing.md +2 -2
  10. package/dist/docs/agent-tools.md +2 -2
  11. package/dist/docs/ci.md +1 -1
  12. package/dist/docs/cli-browse.md +1 -1
  13. package/dist/docs/cli-dev.md +1 -0
  14. package/dist/docs/cli-mcp.md +1 -1
  15. package/dist/docs/cli-members.md +41 -0
  16. package/dist/docs/cli-roles.md +39 -0
  17. package/dist/docs/cli-token.md +1 -1
  18. package/dist/docs/developers-architecture.md +12 -0
  19. package/dist/docs/index.json +16 -1
  20. package/dist/docs/overview-faq.md +6 -4
  21. package/dist/docs/overview-how-it-works.md +3 -2
  22. package/dist/docs/overview-use-cases.md +1 -1
  23. package/dist/docs/overview-what-is-canopy.md +1 -1
  24. package/dist/docs/platform-client-access.md +1 -1
  25. package/dist/docs/platform-egress.md +14 -7
  26. package/dist/docs/platform-members-and-access.md +66 -42
  27. package/dist/docs/roles.md +100 -0
  28. package/dist/docs/sdk-auth.md +24 -10
  29. package/dist/docs/sdk-realtime.md +1 -1
  30. package/dist/docs/static-serving.md +14 -5
  31. package/dist/{docs-CkJGcCpR.mjs → docs-KmN6jD1S.mjs} +3 -3
  32. package/dist/{egress-D6qyaaAn.mjs → egress-C2amvqmE.mjs} +2 -2
  33. package/dist/{feedback-DMgUUl0V.mjs → feedback-B5-95neo.mjs} +3 -3
  34. package/dist/index.mjs +34 -32
  35. package/dist/{local-w2wXfE1d.mjs → local-CIkL7tXP.mjs} +5 -4
  36. package/dist/{login-CeNvHLUv.mjs → login-CREcZfLH.mjs} +2 -2
  37. package/dist/{logs-BzgJfWNP.mjs → logs-BnrNZvLc.mjs} +2 -2
  38. package/dist/{mcp-DFLaXiY_.mjs → mcp-DZdMNTWr.mjs} +3 -3
  39. package/dist/{migrations-Bsu4quya.mjs → migrations-D8NqjC5I.mjs} +1 -1
  40. package/dist/{open-D71Ms1nG.mjs → open-Bpz4C2Sr.mjs} +1 -1
  41. package/dist/{package-CVDpzGgA.mjs → package-Bvly8-rB.mjs} +1 -1
  42. package/dist/project-template-715H_lnu.mjs +2 -0
  43. package/dist/{project-template-BXfHpsI8.mjs → project-template-Cdr1uwwE.mjs} +2 -2
  44. package/dist/{projects-BWTIk80a.mjs → projects-BRG2HnBs.mjs} +4 -4
  45. package/dist/{rename-BdIFrqvG.mjs → rename-BGKa25Rq.mjs} +2 -2
  46. package/dist/{repo-LSOxnmUk.mjs → repo-CwT2RDoW.mjs} +2 -2
  47. package/dist/repo-DmFs6H9m.mjs +2 -0
  48. package/dist/roles-BDZaN3vi.mjs +2 -0
  49. package/dist/roles-UM0Iiws6.mjs +379 -0
  50. package/dist/{schedule-DpMIpCGC.mjs → schedule-BcqBspn3.mjs} +2 -2
  51. package/dist/{secrets-BVx8L0Gj.mjs → secrets-BjbIi7Gp.mjs} +2 -2
  52. package/dist/skill/canopy/SKILL.md +9 -0
  53. package/dist/{skill-D58yV6K8.mjs → skill-DRLniv-I.mjs} +2 -2
  54. package/dist/{src-RUf3Gfuh.mjs → src-8vxOBOxW.mjs} +763 -2
  55. package/dist/{staging-ynPlMiHW.mjs → staging-_2Js0Fkc.mjs} +2 -2
  56. package/dist/{token-CxiPrKn_.mjs → token-7vSNuz-X.mjs} +2 -2
  57. package/dist/{ui-CDUJq-X-.mjs → ui-CLnICOne.mjs} +1 -1
  58. package/dist/{upgrade-Dll5rhOQ.mjs → upgrade-D_yrD7zp.mjs} +2 -2
  59. package/dist/{usage-BtPsb7Iu.mjs → usage-DGGmTwjJ.mjs} +2 -2
  60. package/package.json +2 -2
  61. package/dist/dev-Dj3wFKHe.mjs +0 -2
  62. package/dist/project-template-BbmSmraT.mjs +0 -2
  63. package/dist/repo-JBKJqyoS.mjs +0 -2
@@ -28,7 +28,7 @@ address exists as soon as the project does.
28
28
 
29
29
  Visitors to your project's URL hit OneLogin before they see anything. By
30
30
  default any Monk can open it. From the project's Settings you can tighten that
31
- to collaborators only, or add a shared client password so people outside Monks
31
+ to members only, or add a shared client password so people outside Monks
32
32
  can view it. The password stays visible in Settings, so you can look it up and
33
33
  share it again later. There is no "make it public" switch — that's by design.
34
34
 
@@ -76,7 +76,8 @@ loop gone wrong stops at the cap instead of running up a bill.
76
76
  | **Snapshot** | A saved copy of a project's database you can restore later. |
77
77
  | **Slug** | The project's short name — the first part of its URL. |
78
78
  | **Site** (or **profile**) | An installation of the Canopy platform. Most people only ever use the main one; the CLI can point at others (like staging) and always shows which one you're on. |
79
- | **Collaborator** | A teammate invited to a project through the dashboard's people picker. |
79
+ | **Member** | Someone on a project's roster: an editor, who works on the project, or a viewer, who can only open the app. |
80
+ | **App role** | A label your app gives people, like `approver`, read as `roles` on the signed-in user. |
80
81
 
81
82
  - [Getting started](https://docs.canopy.website/guides/getting-started) — From nothing to a live project, step by step.
82
83
  - [Local development](https://docs.canopy.website/guides/dev) — Preview changes on your machine against your real project data.
@@ -40,7 +40,7 @@ content is a normal use, not an exception.
40
40
  - A strategist vibecoding a pitch prototype over a weekend, sharing it Monday
41
41
  behind a client password.
42
42
  - A producer building a tracker the whole team uses daily, with teammates
43
- invited as collaborators.
43
+ invited as members.
44
44
  - A developer prototyping an AI-assisted workflow with a custom backend,
45
45
  scheduled jobs, and secrets — still inside platform auth and budgets.
46
46
 
@@ -35,7 +35,7 @@ behind the scenes, so for you it looks like this:
35
35
  don't set up servers, domains, or certificates.
36
36
  - **Sign-in, always on.** Every project sits behind Monks OneLogin
37
37
  automatically. Any Monk can open it by default; you can tighten access to
38
- collaborators only, or add a shared password so clients without a Monks
38
+ members only, or add a shared password so clients without a Monks
39
39
  account can see work-in-progress. Nothing on Canopy is ever truly public.
40
40
  - **Data and files.** Each project has its own private database and file
41
41
  storage. No tables to design, no migrations, no connection strings — and no
@@ -57,7 +57,7 @@ route that should serve clients checks `env.CALLER`, not `env.USER`. See
57
57
  ## Playing well with other access controls
58
58
 
59
59
  Client access is independent of
60
- [collaborators-only mode](https://docs.canopy.website/platform/members-and-access): you can lock
60
+ [members-only mode](https://docs.canopy.website/platform/members-and-access): you can lock
61
61
  the app down for Monks outside the team while a client with the password
62
62
  still gets in. That combination — "the client and the project team, nobody
63
63
  else" — is a common setup during a review phase.
@@ -3,11 +3,11 @@ title: Outbound requests
3
3
  description: Which external hosts your project's backend may fetch — a per-project allowlist you manage yourself, or open mode for apps that fetch arbitrary URLs.
4
4
  ---
5
5
 
6
- Outbound `fetch` from a project's backend worker is **deny-by-default**: the
7
- platform only lets requests through to platform infrastructure (your database,
8
- files, AI, the Canopy API) and to the hosts on your project's own
9
- **egress allowlist**. Anything else is answered locally with a 403
10
- `egress_denied` — the request never leaves the runtime.
6
+ Outbound requests from a project's backend worker are **deny-by-default**,
7
+ whatever the code uses to make them: `fetch`, `node:https`, or a raw socket.
8
+ The platform only lets them through to platform infrastructure (your
9
+ database, files, AI, the Canopy API) and to the hosts on your project's own
10
+ **egress allowlist**. Anything else is stopped before it leaves the runtime.
11
11
 
12
12
  That default is deliberate: a thousand idle vibecoded projects should not be
13
13
  able to call arbitrary internet hosts. When your project genuinely needs an
@@ -48,8 +48,8 @@ punycode form. A project can list up to **32** hosts.
48
48
 
49
49
  Everything you reach through the SDK — `canopy.db`, `canopy.blob`,
50
50
  `canopy.ai`, and `env.CANOPY.fetch` — goes through the platform and is always
51
- allowed. The allowlist only governs direct `fetch` calls from your worker code
52
- to the outside world. (Frontend code in the browser is unaffected — this is
51
+ allowed. The allowlist governs every connection your worker code opens to the
52
+ outside world. (Frontend code in the browser is unaffected — this is
53
53
  about the worker.)
54
54
 
55
55
  ## When a request is denied
@@ -64,6 +64,13 @@ body:
64
64
  Allow the host and retry — see the
65
65
  [troubleshooting entry](https://docs.canopy.website/guides/troubleshooting#egress_denied-403).
66
66
 
67
+ That answer is for `fetch` and the libraries built on it. Other clients get a
68
+ plain connection failure instead: a `node:http` or `node:https` request, or a
69
+ raw TCP socket (`connect()` from `cloudflare:sockets`, `node:net`). Raw sockets
70
+ to allowed hosts work on sites that support them and fail on the rest. A host
71
+ on your list that resolves to a private or internal address is refused too,
72
+ so an allowed name can't lead your worker into the platform's own network.
73
+
67
74
  ## Recently blocked hosts
68
75
 
69
76
  You don't have to guess what got denied: the runtime reports every blocked
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Members & access
3
- description: Everything on Canopy sits behind Monks sign-in. Collaborators manage a project; other Monks can view the live app — unless you tighten it.
3
+ description: Everything on Canopy sits behind Monks sign-in. Editors manage a project, viewers open it, and your app can hand out its own roles on top.
4
4
  ---
5
5
 
6
6
  Nothing on Canopy is reachable by the anonymous internet. Every project URL
@@ -23,52 +23,76 @@ nobody outside the team can touch its data or settings.
23
23
 
24
24
  ## Roles
25
25
 
26
- - **Owner** — the person who created the project. Owners govern it: they
27
- invite and remove collaborators, change access settings, and can delete
28
- the project.
29
- - **Collaborator** — invited teammates. They can do day-to-day work — deploy,
30
- roll back, browse and edit data, manage schedules and secrets — but not
31
- delete the project or manage its roster.
32
- - **Platform admin** — can manage any project from the dashboard. Admins keep
33
- governance access, but they do not silently bypass a collaborators-only
34
- live app (see below).
35
-
36
- ## Inviting collaborators
37
-
38
- Open the project's **Settings** tab and use the **Collaborators** card. Start
39
- typing a name or email: colleagues who have signed in to Canopy before show up
40
- as suggestions, and picking one adds them immediately.
41
-
42
- Someone who has never opened Canopy can be added too — type their full
43
- company email and choose **Invite by email**. They appear on the roster as
44
- **invited** and become a collaborator automatically the first time they sign
45
- in, whether that's opening the project's URL or running `canopy login`. Until
46
- then the invite grants nothing. You can withdraw a pending invite from the
47
- same card. Canopy doesn't send them a message, so let them know yourself.
48
-
49
- Only company addresses that can sign in to Canopy can be invited. Owners (and
50
- admins) can also remove people from the same card.
51
-
52
- ## Collaborators-only mode
53
-
54
- Sometimes "any Monk can look" is too open — payroll prototypes, sensitive
55
- client work, unfinished things you'd rather not demo by accident. Flip on
56
- **Settings → Who at Monks can open it → Restrict to collaborators** and the
57
- live app is limited to the project's roster:
58
-
59
- - Other Monks who open the URL get a clear "limited to its collaborators"
60
- page instead of the app.
61
- - The project still appears in the company list — it isn't hidden, just
26
+ Everyone on a project's roster has one Canopy role:
27
+
28
+ - **Owner**: the person who created the project. Owners govern it: they add
29
+ and remove members, decide who is an editor or a viewer, change access
30
+ settings, and can delete the project.
31
+ - **Editor**: does the day-to-day work. Editors deploy, roll back, browse and
32
+ edit data, and manage schedules, secrets and app roles. They can't delete
33
+ the project or change who is on it.
34
+ - **Viewer**: can open the app, and nothing else. Viewers don't see the
35
+ project's management pages. Adding someone as a viewer only makes a
36
+ difference when the project is limited to members, or when you give them
37
+ [app roles](#app-roles).
38
+ - **Platform admin**: can manage any project from the dashboard. Admins don't
39
+ quietly get past a members-only app (see below).
40
+
41
+ ## Adding members
42
+
43
+ Open the project's **Settings → Access** page and use the **Members** card.
44
+ Start typing a name or email: colleagues who have signed in to Canopy before
45
+ show up as suggestions. Pick **Editor** or **Viewer** and add them.
46
+
47
+ Someone who has never opened Canopy can be added too. Type their full company
48
+ email and choose **Invite by email**. They show up as **invited** and join
49
+ automatically the first time they sign in, whether that's opening the
50
+ project's URL or running `canopy login`. Until then the invite grants nothing.
51
+ Canopy doesn't send them a message, so let them know yourself.
52
+
53
+ The owner can switch anyone between editor and viewer, remove people, or
54
+ withdraw an invite from the same card. From a terminal,
55
+ [`canopy members`](https://docs.canopy.website/cli/members) does the same.
56
+
57
+ ## App roles
58
+
59
+ Many apps need their own permissions: "everyone can look, but only a few
60
+ people approve", or "only these people get in, each with their own role".
61
+ App roles cover that without a table of emails in your code.
62
+
63
+ Under **Settings → Access → App roles**, create the roles your app needs. Each
64
+ has a name and a slug, like `approver`. Then give members their roles from the
65
+ Members card. Your app reads them as `roles` on the signed-in user, from
66
+ [`auth.user()`](https://docs.canopy.website/sdk/auth) or `env.CALLER` in a backend worker.
67
+
68
+ One role is always the **default**. Everyone who can open the app has it,
69
+ client-password visitors included, so you don't have to hand it out. The first
70
+ role you create becomes the default, and you can make another one the default
71
+ at any time.
72
+
73
+ Canopy hands roles to your app but never checks them for you. Read
74
+ [Roles and permissions](https://docs.canopy.website/guides/roles) for how to use them safely.
75
+
76
+ ## Members-only mode
77
+
78
+ Sometimes "any Monk can look" is too open: payroll prototypes, sensitive
79
+ client work, unfinished things you'd rather not demo by accident. Turn on
80
+ **Settings → Access → Who at Monks can open it → Restrict to members** and the
81
+ live app is limited to the project's roster, editors and viewers alike:
82
+
83
+ - Other Monks who open the URL get a "limited to its members" page instead of
84
+ the app.
85
+ - The project still appears in the company list. It isn't hidden, just
62
86
  closed.
63
87
  - Platform admins can still manage the project in the dashboard, but they
64
- must add themselves as collaborators if they need to open the live app.
65
- - [Client password access](https://docs.canopy.website/platform/client-access), if you've enabled
66
- it, is unaffected — the two controls are independent.
88
+ must add themselves as members to open the live app.
89
+ - [Client password access](https://docs.canopy.website/platform/client-access), if you've turned it on,
90
+ is unaffected. The two controls are independent.
67
91
 
68
92
  Turning it off again is the same toggle.
69
93
 
70
- > **Note:** Collaborators-only gates the **live app**. Management access (settings, data, deploys) is always
71
- > members-only, regardless of this toggle.
94
+ > **Note:** Members-only gates the **live app**. Management pages (settings, data, deploys) are always for the
95
+ > owner and editors only, whatever this toggle says.
72
96
 
73
97
  ## Platform admins and the Admin page
74
98
 
@@ -0,0 +1,100 @@
1
+ ---
2
+ title: Roles and permissions
3
+ description: Give people different powers inside your app with app roles, and check them where it counts.
4
+ ---
5
+
6
+ Canopy decides who can open your app. App roles decide what each person can
7
+ do once they're in. You create the roles, give them to people, and your code
8
+ reads them. Canopy never checks them for you.
9
+
10
+ ## Two kinds of role
11
+
12
+ - **Canopy roles** (owner, editor, viewer) are about the project itself: who
13
+ can deploy, see the data, change settings. Viewers can only open the app.
14
+ See [Members and access](https://docs.canopy.website/platform/members-and-access).
15
+ - **App roles** are your app's own labels, like `approver` or `admin`. They
16
+ mean whatever your code makes them mean.
17
+
18
+ The two are independent. A viewer can be your app's `admin`, and an editor
19
+ can have no app roles at all.
20
+
21
+ ## Set roles up
22
+
23
+ In the dashboard, open **Settings → Access**:
24
+
25
+ 1. Under **App roles**, add a role. Give it a name ("Approver") and check the
26
+ slug (`approver`). The slug is what your code checks, and it can't change
27
+ later. The name can.
28
+ 2. Under **Members**, open the tag button next to someone and tick their
29
+ roles. To give roles to a colleague who only needs to use the app, add
30
+ them as a **Viewer** first.
31
+
32
+ Or from a terminal, which is also how an agent sets them up:
33
+
34
+ ```bash
35
+ canopy roles add "Approver"
36
+ canopy members add jane@monks.com --viewer --app-role approver
37
+ canopy roles list
38
+ ```
39
+
40
+ ## The default role
41
+
42
+ The first role you create is the **default**. Everyone who can open the app
43
+ has it: members, any other Monk on an open project, and client-password
44
+ visitors. That lets your code treat everyone the same way ("everyone is at
45
+ least a `user`") without listing every person. Make another role the default
46
+ whenever you like; everyone's roles change with it. The default role can't be
47
+ deleted while other roles exist.
48
+
49
+ ## Read roles in your code
50
+
51
+ `roles` is a list of slugs, the default first:
52
+
53
+ ```ts
54
+ const user = await canopy.auth.user();
55
+ user.roles; // ["user", "approver"]
56
+ ```
57
+
58
+ In a backend worker, `env.CALLER.roles` holds the same list without a round
59
+ trip, and so does `env.USER.roles` for a signed-in Monk.
60
+
61
+ ## Check roles where it counts
62
+
63
+ A check that only runs in the browser hides buttons. It doesn't protect
64
+ anything, because a visitor can call your data and routes directly. Put the
65
+ check that matters in your backend worker:
66
+
67
+ ```ts
68
+ app.post("/api/approve/:id", async (c) => {
69
+ if (!c.env.CALLER?.roles?.includes("approver")) {
70
+ return c.json({ error: "approvers only" }, 403);
71
+ }
72
+ // only approvers get here
73
+ });
74
+ ```
75
+
76
+ Keep data that only some roles may see in tables or collections whose names
77
+ start with `private_`. Pages can't reach those, only your worker can. See
78
+ [Database](https://docs.canopy.website/sdk/database).
79
+
80
+ ## Try another role locally
81
+
82
+ `canopy dev` runs with your own roles. To see the app the way someone else
83
+ does, pass `--as-role`:
84
+
85
+ ```bash
86
+ canopy dev --as-role reviewer
87
+ ```
88
+
89
+ You keep the project's default role, as a real visitor would, and get
90
+ `reviewer` instead of your own roles. It only changes your local session.
91
+ Nothing changes on the platform.
92
+
93
+ ## Good to know
94
+
95
+ - Role changes reach your app within a few seconds.
96
+ - A backend deployed before app roles existed sees no `roles` on
97
+ `env.CALLER` until you redeploy it. `auth.user()` has them right away.
98
+ - Client-password visitors all share one password, so they only ever get the
99
+ default role.
100
+ - Scheduled runs have no caller and no roles.
@@ -15,15 +15,16 @@ Returns the authenticated user for the current session as a `CanopyUser`:
15
15
 
16
16
  ```ts
17
17
  const user = await canopy.auth.user();
18
- // { id: "…", email: "ada@monks.com", name: "Ada Lovelace", kind: "monk" }
18
+ // { id: "…", email: "ada@monks.com", name: "Ada Lovelace", kind: "monk", roles: ["user"] }
19
19
  ```
20
20
 
21
- | Field | Type | Meaning |
22
- | ------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
23
- | `id` | `string` | Stable subject from OneLogin — safe to key user data on. |
24
- | `email` | `string` | The user's email address. |
25
- | `name` | `string` | Display name. |
26
- | `kind` | `"monk" \| "external"` | `"monk"` = signed in through OneLogin; `"external"` = a visitor who got in with the project's client password instead. |
21
+ | Field | Type | Meaning |
22
+ | ------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
23
+ | `id` | `string` | Stable subject from OneLogin — safe to key user data on. |
24
+ | `email` | `string` | The user's email address. |
25
+ | `name` | `string` | Display name. |
26
+ | `kind` | `"monk" \| "external"` | `"monk"` = signed in through OneLogin; `"external"` = a visitor who got in with the project's client password instead. |
27
+ | `roles` | `string[]` | The project's [app roles](https://docs.canopy.website/guides/roles) this person has: the default role first, then their own. Empty when the project has none. |
27
28
 
28
29
  `kind: "external"` sessions come from client-password access — clients viewing
29
30
  a shared project without a Monks account. Branch on it when externals should
@@ -46,8 +47,8 @@ if (user.kind === "external") {
46
47
  ### In a backend worker
47
48
 
48
49
  A worker route already knows who is calling, without asking: `env.CALLER`
49
- holds the same `id`, `email`, `name` and `kind` that `auth.user()` returns,
50
- plus `agent`, which is `true` when an AI agent is acting for a Monk.
50
+ holds the same `id`, `email`, `name`, `kind` and `roles` that `auth.user()`
51
+ returns, plus `agent`, which is `true` when an AI agent is acting for a Monk.
51
52
 
52
53
  ```ts
53
54
  import type { CanopyCaller } from "@mediamonks/canopy";
@@ -67,7 +68,20 @@ A client-password visitor has no email address and is named `"Client"`. Its
67
68
  `id` changes with every password unlock, so don't key data on it that has to
68
69
  last.
69
70
 
70
- A backend gets `env.CALLER` from its next deploy. Until it is redeployed,
71
+ This is the place to check app roles, because a visitor can't change what
72
+ your worker sees:
73
+
74
+ ```ts
75
+ app.post("/api/approve/:id", (c) => {
76
+ if (!c.env.CALLER?.roles?.includes("approver")) {
77
+ return c.json({ error: "approvers only" }, 403);
78
+ }
79
+ // …
80
+ });
81
+ ```
82
+
83
+ A backend gets `env.CALLER` from its next deploy, and `roles` on it from its
84
+ first deploy after app roles shipped. Until it is redeployed,
71
85
  `auth.user()` through the `CANOPY` binding gives the same answer:
72
86
 
73
87
  ```ts
@@ -121,7 +121,7 @@ right away and `onOpen` runs, so the page catches up from the database. A
121
121
  quick switch away and back keeps the connection.
122
122
 
123
123
  Every connection also renews itself about every quarter of an hour, and
124
- whenever access to the project changes (the client password, collaborators
124
+ whenever access to the project changes (the client password, members
125
125
  only, someone removed). Each renewal runs `onOpen` again, which is what
126
126
  keeps the reload-on-open pattern correct.
127
127
 
@@ -17,13 +17,21 @@ Every request passes the sign-in wall before a single byte of your project is
17
17
  read. A visitor arrives with a company login, or — if you've turned on
18
18
  [client access](https://docs.canopy.website/platform/client-access) — the project's password. There is no
19
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,
20
+ is readable by any signed-in colleague unless you set it to members only,
21
21
  in [members and access](https://docs.canopy.website/platform/members-and-access).
22
22
 
23
23
  ## Deep links work — routing is yours
24
24
 
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.
25
+ A path is looked up in this order:
26
+
27
+ 1. **The exact file.** `/logo.svg` serves `logo.svg`.
28
+ 2. **A folder's `index.html`.** `/widgets/foo/` serves `widgets/foo/index.html`,
29
+ and `/widgets/foo` redirects to `/widgets/foo/` first. This is how
30
+ prerendered and multi-page builds work: every page is its own file, and
31
+ reloading any of them gets that page, not your homepage.
32
+ 3. **Your root `index.html`.** Anything else gets the app's entry point instead
33
+ of a 404. That's what makes client-side routing work: someone can bookmark
34
+ `/reports/42`, reload it, and your app boots and handles the route.
27
35
 
28
36
  The trade-off is that a **typo'd asset also gets `index.html`** rather than an
29
37
  error. If a script or image comes back as your homepage HTML, you're looking at
@@ -57,12 +65,13 @@ without deploying anything.
57
65
 
58
66
  ## What a deploy may contain
59
67
 
60
- - Up to **500 files** and **25 MB** in total.
68
+ - Up to **1000 files** and **50 MB** in total.
61
69
  - An `index.html` — a bundle without one is rejected.
62
70
  - Each file's type is fixed at deploy time from its extension. Unknown
63
71
  extensions are sent as a download rather than run as code.
64
72
 
65
- If you're over the file count, it's almost always un-bundled assets that a build
73
+ `canopy deploy` checks both limits before it uploads anything. If you're over
74
+ the file count, it's almost always un-bundled assets that a build
66
75
  step would combine; over the size limit, it's usually images or fonts that
67
76
  belong in [file storage](https://docs.canopy.website/sdk/files) instead of the bundle.
68
77
 
@@ -1,8 +1,8 @@
1
- import { t as version } from "./package-CVDpzGgA.mjs";
1
+ import { t as version } from "./package-Bvly8-rB.mjs";
2
2
  import { r as siteConfig } from "./site-0i7wAJqg.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
- import { n as agentHeaders } from "./api-Do9c8tZ_.mjs";
5
- import { r as reportErrors } from "./ui-CDUJq-X-.mjs";
4
+ import { n as agentHeaders } from "./api-Bl6SG8Oq.mjs";
5
+ import { r as reportErrors } from "./ui-CLnICOne.mjs";
6
6
  import { defineCommand } from "citty";
7
7
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
@@ -1,6 +1,6 @@
1
1
  import { l as resolveProject, o as projectDirArgs } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, f as withContext } from "./api-Do9c8tZ_.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CDUJq-X-.mjs";
2
+ import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import * as p from "@clack/prompts";
6
6
  //#region src/egress.ts
@@ -1,7 +1,7 @@
1
- import { t as version } from "./package-CVDpzGgA.mjs";
1
+ import { t as version } from "./package-Bvly8-rB.mjs";
2
2
  import { f as tryReadCanopyJson, l as resolveProject, o as projectDirArgs, p as tryResolveProject, r as siteConfig } from "./site-0i7wAJqg.mjs";
3
- import { a as apiRequest, c as credentials, i as apiDownload, o as apiUpload, s as commonHeaders } from "./api-Do9c8tZ_.mjs";
4
- import { r as reportErrors, t as formatDateTime } from "./ui-CDUJq-X-.mjs";
3
+ import { a as apiRequest, c as credentials, i as apiDownload, o as apiUpload, s as commonHeaders } from "./api-Bl6SG8Oq.mjs";
4
+ import { r as reportErrors, t as formatDateTime } from "./ui-CLnICOne.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { basename, extname, join } from "node:path";
7
7
  import { readFile, writeFile } from "node:fs/promises";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-CVDpzGgA.mjs";
2
+ import { t as version } from "./package-Bvly8-rB.mjs";
3
3
  import { i as siteHeaderLine, n as setInvocationProfile } from "./site-0i7wAJqg.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -63,37 +63,39 @@ const main = defineCommand({
63
63
  description: "canopy — create and deploy vibecoded projects"
64
64
  },
65
65
  subCommands: {
66
- login: () => import("./login-CeNvHLUv.mjs").then((m) => m.loginCommand),
67
- whoami: () => import("./login-CeNvHLUv.mjs").then((m) => m.whoamiCommand),
68
- logout: () => import("./login-CeNvHLUv.mjs").then((m) => m.logoutCommand),
69
- create: () => import("./projects-BWTIk80a.mjs").then((m) => m.createCommand),
70
- init: () => import("./projects-BWTIk80a.mjs").then((m) => m.initCommand),
71
- projects: () => import("./projects-BWTIk80a.mjs").then((m) => m.projectsCommand),
72
- delete: () => import("./projects-BWTIk80a.mjs").then((m) => m.deleteCommand),
73
- rename: () => import("./rename-BdIFrqvG.mjs").then((m) => m.renameCommand),
74
- dev: () => import("./dev-Dj3wFKHe.mjs").then((m) => m.devCommand),
75
- deploy: () => import("./deploy-CXMLABvH.mjs").then((m) => m.deployCommand),
76
- promote: () => import("./deploy-CXMLABvH.mjs").then((m) => m.promoteCommand),
77
- staging: () => import("./staging-ynPlMiHW.mjs").then((m) => m.stagingCommand),
78
- rollback: () => import("./deploy-CXMLABvH.mjs").then((m) => m.rollbackCommand),
79
- download: () => import("./deploy-CXMLABvH.mjs").then((m) => m.downloadCommand),
80
- token: () => import("./token-CxiPrKn_.mjs").then((m) => m.tokenCommand),
81
- egress: () => import("./egress-D6qyaaAn.mjs").then((m) => m.egressCommand),
82
- secrets: () => import("./secrets-BVx8L0Gj.mjs").then((m) => m.secretsCommand),
83
- schedule: () => import("./schedule-DpMIpCGC.mjs").then((m) => m.scheduleCommand),
84
- feedback: () => import("./feedback-DMgUUl0V.mjs").then((m) => m.feedbackCommand),
85
- db: () => import("./db-BNsX7sXo.mjs").then((m) => m.dbCommand),
86
- usage: () => import("./usage-BtPsb7Iu.mjs").then((m) => m.usageCommand),
87
- ai: () => import("./ai-DpQ1nLka.mjs").then((m) => m.aiCommand),
88
- open: () => import("./open-D71Ms1nG.mjs").then((m) => m.openCommand),
89
- browse: () => import("./browse-Dp5HytqK.mjs").then((m) => m.browseCommand),
90
- docs: () => import("./docs-CkJGcCpR.mjs").then((m) => m.docsCommand),
91
- skill: () => import("./skill-D58yV6K8.mjs").then((m) => m.skillCommand),
92
- upgrade: () => import("./upgrade-Dll5rhOQ.mjs").then((m) => m.upgradeCommand),
93
- logs: () => import("./logs-BzgJfWNP.mjs").then((m) => m.logsCommand),
94
- mcp: () => import("./mcp-DFLaXiY_.mjs").then((m) => m.mcpCommand),
95
- repo: () => import("./repo-JBKJqyoS.mjs").then((m) => m.repoCommand),
96
- template: () => import("./project-template-BbmSmraT.mjs").then((m) => m.templateCommand)
66
+ login: () => import("./login-CREcZfLH.mjs").then((m) => m.loginCommand),
67
+ whoami: () => import("./login-CREcZfLH.mjs").then((m) => m.whoamiCommand),
68
+ logout: () => import("./login-CREcZfLH.mjs").then((m) => m.logoutCommand),
69
+ create: () => import("./projects-BRG2HnBs.mjs").then((m) => m.createCommand),
70
+ init: () => import("./projects-BRG2HnBs.mjs").then((m) => m.initCommand),
71
+ projects: () => import("./projects-BRG2HnBs.mjs").then((m) => m.projectsCommand),
72
+ delete: () => import("./projects-BRG2HnBs.mjs").then((m) => m.deleteCommand),
73
+ rename: () => import("./rename-BGKa25Rq.mjs").then((m) => m.renameCommand),
74
+ dev: () => import("./dev-CUJKgaqJ.mjs").then((m) => m.devCommand),
75
+ deploy: () => import("./deploy-ExrIeX_m.mjs").then((m) => m.deployCommand),
76
+ promote: () => import("./deploy-ExrIeX_m.mjs").then((m) => m.promoteCommand),
77
+ staging: () => import("./staging-_2Js0Fkc.mjs").then((m) => m.stagingCommand),
78
+ rollback: () => import("./deploy-ExrIeX_m.mjs").then((m) => m.rollbackCommand),
79
+ download: () => import("./deploy-ExrIeX_m.mjs").then((m) => m.downloadCommand),
80
+ token: () => import("./token-7vSNuz-X.mjs").then((m) => m.tokenCommand),
81
+ egress: () => import("./egress-C2amvqmE.mjs").then((m) => m.egressCommand),
82
+ secrets: () => import("./secrets-BjbIi7Gp.mjs").then((m) => m.secretsCommand),
83
+ roles: () => import("./roles-BDZaN3vi.mjs").then((m) => m.rolesCommand),
84
+ members: () => import("./roles-BDZaN3vi.mjs").then((m) => m.membersCommand),
85
+ schedule: () => import("./schedule-BcqBspn3.mjs").then((m) => m.scheduleCommand),
86
+ feedback: () => import("./feedback-B5-95neo.mjs").then((m) => m.feedbackCommand),
87
+ db: () => import("./db-BJJGydgL.mjs").then((m) => m.dbCommand),
88
+ usage: () => import("./usage-DGGmTwjJ.mjs").then((m) => m.usageCommand),
89
+ ai: () => import("./ai-FBHTSCLZ.mjs").then((m) => m.aiCommand),
90
+ open: () => import("./open-Bpz4C2Sr.mjs").then((m) => m.openCommand),
91
+ browse: () => import("./browse-B8-0dweq.mjs").then((m) => m.browseCommand),
92
+ docs: () => import("./docs-KmN6jD1S.mjs").then((m) => m.docsCommand),
93
+ skill: () => import("./skill-DRLniv-I.mjs").then((m) => m.skillCommand),
94
+ upgrade: () => import("./upgrade-D_yrD7zp.mjs").then((m) => m.upgradeCommand),
95
+ logs: () => import("./logs-BnrNZvLc.mjs").then((m) => m.logsCommand),
96
+ mcp: () => import("./mcp-DZdMNTWr.mjs").then((m) => m.mcpCommand),
97
+ repo: () => import("./repo-DmFs6H9m.mjs").then((m) => m.repoCommand),
98
+ template: () => import("./project-template-715H_lnu.mjs").then((m) => m.templateCommand)
97
99
  }
98
100
  });
99
101
  const quiet = process.argv[2] === "mcp" && process.argv[3] === "serve" || process.argv[2] === "repo" && process.argv[3] === "credential";
@@ -1,6 +1,6 @@
1
- import { r as apiDataRequest } from "./api-Do9c8tZ_.mjs";
2
- import { a as realtimeRouter, c as META_COLUMNS, l as blobRouter, n as WORKER_ATTEST_HEADER, o as dbRouter, s as hasPrivateNames } from "./src-RUf3Gfuh.mjs";
3
- import { a as rewriteApiPath } from "./dev-CPR-9slK.mjs";
1
+ import { r as apiDataRequest } from "./api-Bl6SG8Oq.mjs";
2
+ import { a as realtimeRouter, c as META_COLUMNS, l as blobRouter, n as WORKER_ATTEST_HEADER, o as dbRouter, s as hasPrivateNames } from "./src-8vxOBOxW.mjs";
3
+ import { a as rewriteApiPath } from "./dev-Cq98yblV.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";
@@ -942,7 +942,8 @@ async function createSandbox(options) {
942
942
  id: "local-dev",
943
943
  email: options.email ?? "dev@localhost",
944
944
  name: "Local Dev",
945
- kind: "monk"
945
+ kind: "monk",
946
+ roles: options.roles ?? []
946
947
  }));
947
948
  app.all("/data/v1/ai/*", (c) => c.json({
948
949
  error: "not_available_in_local_dev",
@@ -1,6 +1,6 @@
1
1
  import { _ as setActiveProfile, b as getBakedProfile, g as readConfigFile, h as listBakedProfiles, m as clearAllProfileTokens, r as siteConfig, v as upsertProfileAuth, x as isProfileId, y as writeConfigFile } from "./site-0i7wAJqg.mjs";
2
- import { n as agentHeaders } from "./api-Do9c8tZ_.mjs";
3
- import { n as openBrowser, r as reportErrors } from "./ui-CDUJq-X-.mjs";
2
+ import { n as agentHeaders } from "./api-Bl6SG8Oq.mjs";
3
+ import { n as openBrowser, r as reportErrors } from "./ui-CLnICOne.mjs";
4
4
  import { n as clearRepoTokenCache } from "./repo-credential-CNDRM-tG.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { hostname } from "node:os";
@@ -1,6 +1,6 @@
1
1
  import { l as resolveProject, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
2
- import { a as apiRequest, f as withContext } from "./api-Do9c8tZ_.mjs";
3
- import { r as reportErrors } from "./ui-CDUJq-X-.mjs";
2
+ import { a as apiRequest, f as withContext } from "./api-Bl6SG8Oq.mjs";
3
+ import { r as reportErrors } from "./ui-CLnICOne.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  //#region src/logs.ts
6
6
  /**
@@ -1,6 +1,6 @@
1
- import { t as version } from "./package-CVDpzGgA.mjs";
1
+ import { t as version } from "./package-Bvly8-rB.mjs";
2
2
  import { c as projectUrl, d as stagingUrl, p as tryResolveProject, r as siteConfig } from "./site-0i7wAJqg.mjs";
3
- import { a as apiRequest, d as noticeRenamesOnly, l as currentSlug, t as ApiError } from "./api-Do9c8tZ_.mjs";
3
+ import { a as apiRequest, d as noticeRenamesOnly, l as currentSlug, t as ApiError } from "./api-Bl6SG8Oq.mjs";
4
4
  import { t as liveDevServer } from "./dev-server-file-Dk9pk4AR.mjs";
5
5
  import { defineCommand } from "citty";
6
6
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
@@ -171,7 +171,7 @@ function explainRefusal(status, body, project) {
171
171
  case "agent_token_required":
172
172
  case "agent_token_invalid": return "Canopy refused the sign-in for this agent session — run `canopy login` and restart the MCP server.";
173
173
  case "agent_token_scope": return `The agent credential does not cover ${project} on this channel.`;
174
- case "members_only": return `${project} is limited to its collaborators — ask the owner to add you.`;
174
+ case "members_only": return `${project} is limited to its members — ask the owner to add you.`;
175
175
  case "project_not_found": return `There is no project ${project} on this site — check the slug and --profile.`;
176
176
  case "project_paused": return `${project} is paused.`;
177
177
  case "project_moved": return `${project} has moved${body.moved_to ? ` to ${body.moved_to}` : ""}.`;
@@ -1,5 +1,5 @@
1
1
  import { f as tryReadCanopyJson } from "./site-0i7wAJqg.mjs";
2
- import { r as apiDataRequest } from "./api-Do9c8tZ_.mjs";
2
+ import { r as apiDataRequest } from "./api-Bl6SG8Oq.mjs";
3
3
  import { readFileSync, readdirSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  //#region src/migrations.ts