@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.
- package/dist/{ai-DpQ1nLka.mjs → ai-FBHTSCLZ.mjs} +2 -2
- package/dist/{api-Do9c8tZ_.mjs → api-Bl6SG8Oq.mjs} +2 -2
- package/dist/{browse-Dp5HytqK.mjs → browse-B8-0dweq.mjs} +2 -2
- package/dist/{db-BNsX7sXo.mjs → db-BJJGydgL.mjs} +3 -3
- package/dist/{deploy-CXMLABvH.mjs → deploy-ExrIeX_m.mjs} +22 -3
- package/dist/dev-CUJKgaqJ.mjs +2 -0
- package/dist/{dev-CPR-9slK.mjs → dev-Cq98yblV.mjs} +98 -16
- package/dist/{dev-worker-D78leKBJ.mjs → dev-worker-CcYwQjxz.mjs} +23 -10
- package/dist/docs/agent-browsing.md +2 -2
- package/dist/docs/agent-tools.md +2 -2
- package/dist/docs/ci.md +1 -1
- package/dist/docs/cli-browse.md +1 -1
- package/dist/docs/cli-dev.md +1 -0
- package/dist/docs/cli-mcp.md +1 -1
- package/dist/docs/cli-members.md +41 -0
- package/dist/docs/cli-roles.md +39 -0
- package/dist/docs/cli-token.md +1 -1
- package/dist/docs/developers-architecture.md +12 -0
- package/dist/docs/index.json +16 -1
- package/dist/docs/overview-faq.md +6 -4
- package/dist/docs/overview-how-it-works.md +3 -2
- package/dist/docs/overview-use-cases.md +1 -1
- package/dist/docs/overview-what-is-canopy.md +1 -1
- package/dist/docs/platform-client-access.md +1 -1
- package/dist/docs/platform-egress.md +14 -7
- package/dist/docs/platform-members-and-access.md +66 -42
- package/dist/docs/roles.md +100 -0
- package/dist/docs/sdk-auth.md +24 -10
- package/dist/docs/sdk-realtime.md +1 -1
- package/dist/docs/static-serving.md +14 -5
- package/dist/{docs-CkJGcCpR.mjs → docs-KmN6jD1S.mjs} +3 -3
- package/dist/{egress-D6qyaaAn.mjs → egress-C2amvqmE.mjs} +2 -2
- package/dist/{feedback-DMgUUl0V.mjs → feedback-B5-95neo.mjs} +3 -3
- package/dist/index.mjs +34 -32
- package/dist/{local-w2wXfE1d.mjs → local-CIkL7tXP.mjs} +5 -4
- package/dist/{login-CeNvHLUv.mjs → login-CREcZfLH.mjs} +2 -2
- package/dist/{logs-BzgJfWNP.mjs → logs-BnrNZvLc.mjs} +2 -2
- package/dist/{mcp-DFLaXiY_.mjs → mcp-DZdMNTWr.mjs} +3 -3
- package/dist/{migrations-Bsu4quya.mjs → migrations-D8NqjC5I.mjs} +1 -1
- package/dist/{open-D71Ms1nG.mjs → open-Bpz4C2Sr.mjs} +1 -1
- package/dist/{package-CVDpzGgA.mjs → package-Bvly8-rB.mjs} +1 -1
- package/dist/project-template-715H_lnu.mjs +2 -0
- package/dist/{project-template-BXfHpsI8.mjs → project-template-Cdr1uwwE.mjs} +2 -2
- package/dist/{projects-BWTIk80a.mjs → projects-BRG2HnBs.mjs} +4 -4
- package/dist/{rename-BdIFrqvG.mjs → rename-BGKa25Rq.mjs} +2 -2
- package/dist/{repo-LSOxnmUk.mjs → repo-CwT2RDoW.mjs} +2 -2
- package/dist/repo-DmFs6H9m.mjs +2 -0
- package/dist/roles-BDZaN3vi.mjs +2 -0
- package/dist/roles-UM0Iiws6.mjs +379 -0
- package/dist/{schedule-DpMIpCGC.mjs → schedule-BcqBspn3.mjs} +2 -2
- package/dist/{secrets-BVx8L0Gj.mjs → secrets-BjbIi7Gp.mjs} +2 -2
- package/dist/skill/canopy/SKILL.md +9 -0
- package/dist/{skill-D58yV6K8.mjs → skill-DRLniv-I.mjs} +2 -2
- package/dist/{src-RUf3Gfuh.mjs → src-8vxOBOxW.mjs} +763 -2
- package/dist/{staging-ynPlMiHW.mjs → staging-_2Js0Fkc.mjs} +2 -2
- package/dist/{token-CxiPrKn_.mjs → token-7vSNuz-X.mjs} +2 -2
- package/dist/{ui-CDUJq-X-.mjs → ui-CLnICOne.mjs} +1 -1
- package/dist/{upgrade-Dll5rhOQ.mjs → upgrade-D_yrD7zp.mjs} +2 -2
- package/dist/{usage-BtPsb7Iu.mjs → usage-DGGmTwjJ.mjs} +2 -2
- package/package.json +2 -2
- package/dist/dev-Dj3wFKHe.mjs +0 -2
- package/dist/project-template-BbmSmraT.mjs +0 -2
- 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
|
|
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
|
-
| **
|
|
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
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
52
|
-
|
|
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.
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
65
|
-
- [Client password access](https://docs.canopy.website/platform/client-access), if you've
|
|
66
|
-
|
|
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:**
|
|
71
|
-
>
|
|
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.
|
package/dist/docs/sdk-auth.md
CHANGED
|
@@ -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 `
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
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 **
|
|
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
|
-
|
|
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-
|
|
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-
|
|
5
|
-
import { r as reportErrors } from "./ui-
|
|
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-
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-
|
|
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-
|
|
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-
|
|
4
|
-
import { r as reportErrors, t as formatDateTime } from "./ui-
|
|
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-
|
|
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-
|
|
67
|
-
whoami: () => import("./login-
|
|
68
|
-
logout: () => import("./login-
|
|
69
|
-
create: () => import("./projects-
|
|
70
|
-
init: () => import("./projects-
|
|
71
|
-
projects: () => import("./projects-
|
|
72
|
-
delete: () => import("./projects-
|
|
73
|
-
rename: () => import("./rename-
|
|
74
|
-
dev: () => import("./dev-
|
|
75
|
-
deploy: () => import("./deploy-
|
|
76
|
-
promote: () => import("./deploy-
|
|
77
|
-
staging: () => import("./staging-
|
|
78
|
-
rollback: () => import("./deploy-
|
|
79
|
-
download: () => import("./deploy-
|
|
80
|
-
token: () => import("./token-
|
|
81
|
-
egress: () => import("./egress-
|
|
82
|
-
secrets: () => import("./secrets-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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-
|
|
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-
|
|
3
|
-
import { a as rewriteApiPath } from "./dev-
|
|
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-
|
|
3
|
-
import { n as openBrowser, r as reportErrors } from "./ui-
|
|
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-
|
|
3
|
-
import { r as reportErrors } from "./ui-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
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
|