@mediamonks/canopy-cli 0.7.13 → 0.7.15
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/docs/cli-egress.md +9 -0
- package/dist/docs/cli-feedback.md +13 -3
- package/dist/docs/cli-index.md +3 -20
- package/dist/docs/cli-schedule.md +43 -0
- package/dist/docs/index.json +12 -2
- package/dist/docs/platform-egress.md +52 -1
- package/dist/docs/platform-feedback.md +5 -0
- package/dist/docs/platform-google.md +2 -1
- package/dist/docs/platform-schedules.md +17 -4
- package/dist/docs/sdk-google.md +139 -0
- package/dist/docs/sdk.md +6 -8
- package/dist/{docs-BBMVxVta.mjs → docs-BlzdYmou.mjs} +1 -1
- package/dist/egress-PJp0SZy2.mjs +184 -0
- package/dist/{feedback-BMX_d4KZ.mjs → feedback-h_j6DBBZ.mjs} +25 -6
- package/dist/index.mjs +6 -6
- package/dist/{package-vJXq4RNN.mjs → package-CF0dX7UA.mjs} +1 -1
- package/dist/{skill-Zq9aIPKf.mjs → skill-DaaxgqEk.mjs} +1 -1
- package/dist/{upgrade-ES8u8_Pn.mjs → upgrade-DG-7xwOs.mjs} +1 -1
- package/package.json +3 -3
- package/dist/egress-BFSwa8Uu.mjs +0 -88
package/dist/docs/cli-egress.md
CHANGED
|
@@ -14,6 +14,9 @@ canopy egress list
|
|
|
14
14
|
canopy egress allow api.example.com
|
|
15
15
|
canopy egress allow "*.googleapis.com"
|
|
16
16
|
canopy egress remove api.example.com
|
|
17
|
+
canopy egress mode open --accept-risk
|
|
18
|
+
canopy egress mode allowlist
|
|
19
|
+
canopy egress log
|
|
17
20
|
```
|
|
18
21
|
|
|
19
22
|
## Arguments & flags
|
|
@@ -26,6 +29,12 @@ target directory, or `--project <slug>` to override — and honor the global
|
|
|
26
29
|
- `allow <host>` — add a hostname (or `*.` subdomain wildcard; quote it so the
|
|
27
30
|
shell doesn't expand the `*`).
|
|
28
31
|
- `remove <host>` — remove an entry (as shown by `list`).
|
|
32
|
+
- `mode [allowlist|open]` — show or switch the
|
|
33
|
+
[egress mode](https://docs.canopy.website/platform/egress#open-mode). Switching to `open` prints a risk
|
|
34
|
+
summary and asks you to type the project slug; pass `--accept-risk` to
|
|
35
|
+
acknowledge it non-interactively. Switching back needs no ceremony.
|
|
36
|
+
- `log` — print the [outbound traffic log](https://docs.canopy.website/platform/egress#the-outbound-traffic-log):
|
|
37
|
+
hosts the worker actually fetched, with counts and last-seen times.
|
|
29
38
|
|
|
30
39
|
## How it works
|
|
31
40
|
|
|
@@ -37,11 +37,15 @@ machine-readable output.
|
|
|
37
37
|
URL, deploy version, console errors — or CLI context), the screenshots on
|
|
38
38
|
the report, and the comment trail. `--download` saves the screenshots into
|
|
39
39
|
a directory so you can actually look at them.
|
|
40
|
-
- `comment <id> <text
|
|
41
|
-
|
|
40
|
+
- `comment <id> <text> [--internal]` — append a comment (findings, questions,
|
|
41
|
+
updates). `--internal` (members only) keeps the note off the reporter's
|
|
42
|
+
view — for stack traces, table names, and other working detail that would
|
|
43
|
+
only confuse them.
|
|
44
|
+
- `attach <id> <file> [--attach <more>] [--caption <text>] [--comment <text>] [--internal]` —
|
|
42
45
|
add screenshots to an existing report. With `--comment`, the comment is
|
|
43
46
|
posted first and the screenshots hang off that trail entry — how you show
|
|
44
|
-
"here's what it looks like after the fix".
|
|
47
|
+
"here's what it looks like after the fix". `--internal` makes that comment a
|
|
48
|
+
members-only note, screenshots included.
|
|
45
49
|
- `status <id> <new-status> [--comment <note>]` — move the ticket; the
|
|
46
50
|
transition is recorded on the trail. Moving to `investigated` needs the
|
|
47
51
|
findings: pass `--comment`, or comment first.
|
|
@@ -65,6 +69,12 @@ yet. Reproduce with the context in `show` (including the deploy version that
|
|
|
65
69
|
served the page), then `status <id> investigated --comment "<what you found>"`.
|
|
66
70
|
Stop there — say what it would take to fix and let the person decide.
|
|
67
71
|
|
|
72
|
+
Split your findings by audience: the status comment is what the **reporter**
|
|
73
|
+
reads, so keep it plain ("the upload fails for files over 5 MB — looking into
|
|
74
|
+
it"). The tech-loaded version — stack traces, table names, reproduction
|
|
75
|
+
queries — goes in `comment <id> "<detail>" --internal`, which only project
|
|
76
|
+
members see. An internal note counts as findings for `investigated`.
|
|
77
|
+
|
|
68
78
|
**Build:** `list --status accepted` is what a person has asked for. Do the
|
|
69
79
|
work, then `resolve` with a note, or `declined` if it turns out to be a bad
|
|
70
80
|
idea. Set `accepted` yourself only when the person you are working with told
|
package/dist/docs/cli-index.md
CHANGED
|
@@ -39,23 +39,6 @@ Canopy authenticates through Monks' OneLogin. Run [`canopy login`](https://docs.
|
|
|
39
39
|
|
|
40
40
|
## Commands
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- [canopy create](https://docs.canopy.website/cli/create) — scaffold and register a new project
|
|
46
|
-
- [canopy init](https://docs.canopy.website/cli/init) — set up a folder for an existing project
|
|
47
|
-
- [canopy projects](https://docs.canopy.website/cli/projects) — list your projects
|
|
48
|
-
- [canopy delete](https://docs.canopy.website/cli/delete) — delete a project and its data
|
|
49
|
-
- [canopy dev](https://docs.canopy.website/cli/dev) — run a project locally
|
|
50
|
-
- [canopy deploy](https://docs.canopy.website/cli/deploy) — build and deploy a new version
|
|
51
|
-
- [canopy rollback](https://docs.canopy.website/cli/rollback) — make a previous version live again
|
|
52
|
-
- [canopy download](https://docs.canopy.website/cli/download) — download a deployed version's files
|
|
53
|
-
- [canopy token](https://docs.canopy.website/cli/token) — manage deploy tokens for CI
|
|
54
|
-
- [canopy db](https://docs.canopy.website/cli/db) — pull, snapshot, and restore project data
|
|
55
|
-
- [canopy usage](https://docs.canopy.website/cli/usage) — AI spend and storage for a project
|
|
56
|
-
- [canopy open](https://docs.canopy.website/cli/open) — open the live project in your browser
|
|
57
|
-
- [canopy docs](https://docs.canopy.website/cli/docs) — read the Canopy guides, offline-capable
|
|
58
|
-
- [canopy skill](https://docs.canopy.website/cli/skill) — install the Canopy agent skill into a repo
|
|
59
|
-
- [canopy upgrade](https://docs.canopy.website/cli/upgrade) — update the CLI itself
|
|
60
|
-
|
|
61
|
-
`canopy logs` (tail a project's worker logs) exists as a stub for a planned feature and currently exits with a pointer to the roadmap.
|
|
42
|
+
{/* Generated from this folder's meta.json + each page's frontmatter description — never hand-edit a list here. */}
|
|
43
|
+
|
|
44
|
+
<SectionIndex folder="/cli" />
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: canopy schedule
|
|
3
|
+
description: List, add, and remove the project's cron schedules from the terminal.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy schedule` manages the project's named cron jobs — the same schedules
|
|
7
|
+
as the dashboard's **Settings → Schedules**, so a migration or an agent can
|
|
8
|
+
set up recurring jobs without leaving the terminal. See
|
|
9
|
+
[Schedules](https://docs.canopy.website/platform/schedules) for how schedules work and how your worker
|
|
10
|
+
handles them.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
canopy schedule list
|
|
16
|
+
canopy schedule add refresh-feeds "0 5 * * *"
|
|
17
|
+
canopy schedule remove refresh-feeds
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Arguments & flags
|
|
21
|
+
|
|
22
|
+
All subcommands resolve the project the usual way — `canopy.json` in the
|
|
23
|
+
target directory, or `--project <slug>` to override — and honor the global
|
|
24
|
+
`--profile <site>` / `CANOPY_PROFILE`.
|
|
25
|
+
|
|
26
|
+
- `list` — print each schedule with its cron expression, enabled state, and
|
|
27
|
+
last run (including the error when the last run failed). `--json` prints
|
|
28
|
+
the raw JSON instead.
|
|
29
|
+
- `add <name> <cron>` — create a schedule, or update the cron of an existing
|
|
30
|
+
one. Names are lowercase letters, digits, and hyphens; the cron is a
|
|
31
|
+
standard five-field expression in **UTC** — quote it so the shell doesn't
|
|
32
|
+
expand the `*`s.
|
|
33
|
+
- `remove <name>` — delete a schedule (as shown by `list`).
|
|
34
|
+
|
|
35
|
+
## How it works
|
|
36
|
+
|
|
37
|
+
Schedules live on the platform, per project — not in `canopy.json` — so
|
|
38
|
+
changing a cadence never needs a deploy. A project can have up to **5
|
|
39
|
+
schedules** with a shortest interval of one minute; names and cron
|
|
40
|
+
expressions are validated server-side. Your worker receives ticks in its
|
|
41
|
+
`scheduled` handler and switches on `event.name` — see
|
|
42
|
+
[Schedules](https://docs.canopy.website/platform/schedules). Enabling/disabling without deleting, and
|
|
43
|
+
firing a job manually to test it, remain dashboard-only for now.
|
package/dist/docs/index.json
CHANGED
|
@@ -112,12 +112,12 @@
|
|
|
112
112
|
{
|
|
113
113
|
"slug": "platform-schedules",
|
|
114
114
|
"title": "Schedules",
|
|
115
|
-
"summary": "Run named cron jobs in your project's backend — configured in the dashboard, so you can change the cadence without redeploying."
|
|
115
|
+
"summary": "Run named cron jobs in your project's backend — configured in the dashboard or the CLI, so you can change the cadence without redeploying."
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
"slug": "platform-egress",
|
|
119
119
|
"title": "Outbound requests",
|
|
120
|
-
"summary": "Which external hosts your project's backend may fetch — a per-project allowlist you manage yourself."
|
|
120
|
+
"summary": "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."
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
"slug": "platform-ai-usage",
|
|
@@ -236,6 +236,11 @@
|
|
|
236
236
|
"title": "canopy egress",
|
|
237
237
|
"summary": "Manage which outbound hosts the project's backend worker may fetch."
|
|
238
238
|
},
|
|
239
|
+
{
|
|
240
|
+
"slug": "cli-schedule",
|
|
241
|
+
"title": "canopy schedule",
|
|
242
|
+
"summary": "List, add, and remove the project's cron schedules from the terminal."
|
|
243
|
+
},
|
|
239
244
|
{
|
|
240
245
|
"slug": "cli-db",
|
|
241
246
|
"title": "canopy db",
|
|
@@ -308,6 +313,11 @@
|
|
|
308
313
|
"title": "AI",
|
|
309
314
|
"summary": "Metered LLM access through the project's budget-capped gateway key — chat, streaming, and an OpenAI-compatible endpoint."
|
|
310
315
|
},
|
|
316
|
+
{
|
|
317
|
+
"slug": "sdk-google",
|
|
318
|
+
"title": "Google",
|
|
319
|
+
"summary": "Call Google APIs as the project's connected account or as the visitor's own — the platform holds the OAuth."
|
|
320
|
+
},
|
|
311
321
|
{
|
|
312
322
|
"slug": "sdk-services",
|
|
313
323
|
"title": "Services",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Outbound requests
|
|
3
|
-
description: Which external hosts your project's backend may fetch — a per-project allowlist you manage yourself.
|
|
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
6
|
Outbound `fetch` from a project's backend worker is **deny-by-default**: the
|
|
@@ -73,3 +73,54 @@ how recently each was hit. One click on **Allow** puts the host on the list
|
|
|
73
73
|
and clears it from the blocked section. Hosts that keep a low profile drop
|
|
74
74
|
off — the list remembers the 50 most recent — and allowing a host removes its
|
|
75
75
|
blocked entry immediately.
|
|
76
|
+
|
|
77
|
+
## Open mode
|
|
78
|
+
|
|
79
|
+
Some apps exist to fetch **arbitrary user-provided URLs** — link collectors,
|
|
80
|
+
feed readers, scrapers. For those, an allowlist is the wrong shape: the long
|
|
81
|
+
tail of domains never ends, and every new one is a denied request. Instead of
|
|
82
|
+
growing the list forever, switch the project to **open egress mode**:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
canopy egress mode open
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
or in the dashboard: **Settings → Outbound hosts → Egress mode**. In open mode
|
|
89
|
+
the worker may fetch **any public hostname**. Three guardrails replace the
|
|
90
|
+
allowlist:
|
|
91
|
+
|
|
92
|
+
- **A built-in blocklist**, no exceptions: IP-literal URLs (v4 and v6),
|
|
93
|
+
`localhost`, private networks, and cloud metadata endpoints
|
|
94
|
+
(`metadata.google.internal` and friends) are always denied — in both modes.
|
|
95
|
+
- **An outbound rate limit** — generous (hundreds of fetches per minute; it
|
|
96
|
+
exists to stop runaway loops and hostile scraping, not to meter normal use).
|
|
97
|
+
Exceeding it answers 403 with `egress_rate_limited`, and the hits show up
|
|
98
|
+
under Recently blocked. If your app legitimately needs more, tell us —
|
|
99
|
+
the budget is meant to be raised when real use bumps into it.
|
|
100
|
+
- **The outbound traffic log** (below) records every host the worker talks to.
|
|
101
|
+
|
|
102
|
+
Because open mode means _users of your app can decide where it connects_,
|
|
103
|
+
enabling it is a deliberate act: the dashboard asks you to type the project
|
|
104
|
+
slug under a plain-language risk summary, and the CLI wants an interactive
|
|
105
|
+
confirmation or the explicit `--accept-risk` flag. Extra care applies if the
|
|
106
|
+
project has [client access](https://docs.canopy.website/platform/client-access) enabled — then untrusted
|
|
107
|
+
visitors drive the fetches.
|
|
108
|
+
|
|
109
|
+
Switching back is one command (`canopy egress mode allowlist`), and the
|
|
110
|
+
dashboard offers to **seed the allowlist from the observed hosts** so
|
|
111
|
+
tightening up later doesn't break the app.
|
|
112
|
+
|
|
113
|
+
## The outbound traffic log
|
|
114
|
+
|
|
115
|
+
In both modes, the platform keeps a per-project log of the hosts the worker
|
|
116
|
+
**actually fetched** — hostnames only, never paths, query strings, headers, or
|
|
117
|
+
bodies. Per host it tracks a fetch count and first/last seen. See it under
|
|
118
|
+
**Outbound traffic** in Settings → Outbound hosts, or:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
canopy egress log
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
In allowlist mode it doubles as a shortcut: any observed host can be promoted
|
|
125
|
+
to the allowlist with one click. The log keeps up to 1000 distinct hosts per
|
|
126
|
+
project; beyond that the least-recently-seen entry makes room.
|
|
@@ -65,6 +65,11 @@ Project members triage reports on the **Feedback tab**:
|
|
|
65
65
|
- The order is a suggestion, not a track. Accept something straight away, skip
|
|
66
66
|
the investigation, or decline it at any point.
|
|
67
67
|
- Every status change and comment is recorded on the item's trail.
|
|
68
|
+
- A comment can be marked **internal** (the toggle next to the comment box):
|
|
69
|
+
visible to project members and admins only, never the reporter. That's the
|
|
70
|
+
place for stack traces, query plans, and other working notes that would only
|
|
71
|
+
confuse the person who filed the report — screenshots attached to an
|
|
72
|
+
internal note stay internal too.
|
|
68
73
|
- Reporters follow their submissions on the **Reports page** (dashboard
|
|
69
74
|
header) and can comment on or withdraw their own items; only project
|
|
70
75
|
members change statuses.
|
|
@@ -9,7 +9,8 @@ own account**.
|
|
|
9
9
|
|
|
10
10
|
Either way there is no OAuth setup to do. No Cloud project, no consent screen,
|
|
11
11
|
no client ID or secret, no redirect URLs, no tokens to store or refresh. Canopy
|
|
12
|
-
holds all of that.
|
|
12
|
+
holds all of that. This page covers the concepts and permissions; the calling
|
|
13
|
+
surface is documented in [the SDK reference](https://docs.canopy.website/sdk/google).
|
|
13
14
|
|
|
14
15
|
## The project's own account
|
|
15
16
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Schedules
|
|
3
|
-
description: Run named cron jobs in your project's backend — configured in the dashboard, so you can change the cadence without redeploying.
|
|
3
|
+
description: Run named cron jobs in your project's backend — configured in the dashboard or the CLI, so you can change the cadence without redeploying.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Schedules let a project do things on a clock: send a daily digest, refresh a
|
|
7
|
-
cache every hour, tick a counter every minute. You define **named jobs**
|
|
8
|
-
the dashboard
|
|
9
|
-
|
|
7
|
+
cache every hour, tick a counter every minute. You define **named jobs** —
|
|
8
|
+
in the dashboard or with [`canopy schedule`](https://docs.canopy.website/cli/schedule) — and the
|
|
9
|
+
platform wakes your project's backend at the right times; your code only
|
|
10
|
+
decides what each job does.
|
|
10
11
|
|
|
11
12
|
> **Note:** Schedules need a project with a custom backend. Per-project backends run on the **main** site and
|
|
12
13
|
> the **GCP pilot** site. On sites without backend support the Schedules section doesn't appear in
|
|
@@ -28,6 +29,18 @@ you can see at a glance whether a job is healthy.
|
|
|
28
29
|
You can also trigger a schedule manually from the dashboard, which is the
|
|
29
30
|
quickest way to test a handler without waiting for the clock.
|
|
30
31
|
|
|
32
|
+
The same jobs are manageable from the terminal — handy when a migration or a
|
|
33
|
+
coding agent sets up a project end to end:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
canopy schedule list
|
|
37
|
+
canopy schedule add refresh-feeds "0 5 * * *"
|
|
38
|
+
canopy schedule remove refresh-feeds
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
See [canopy schedule](https://docs.canopy.website/cli/schedule) for the details. Pausing a job and
|
|
42
|
+
firing one manually are dashboard-only.
|
|
43
|
+
|
|
31
44
|
## Handling schedules in your code
|
|
32
45
|
|
|
33
46
|
Your project's worker exports a `scheduled` handler next to its normal
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Google
|
|
3
|
+
description: Call Google APIs as the project's connected account or as the visitor's own — the platform holds the OAuth.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy.google` calls Google APIs through the platform — no client IDs,
|
|
7
|
+
secrets, redirect URLs, or tokens in project code. There are two grants,
|
|
8
|
+
chosen explicitly because they act as different accounts:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
canopy.google.project(); // the account the owner connected — backend only
|
|
12
|
+
canopy.google.user(); // the calling visitor's own account
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
What each grant is for, who connects one, and every permission bundle is
|
|
16
|
+
covered in [Google](https://docs.canopy.website/platform/google) on the platform side — this page is the
|
|
17
|
+
calling surface. Both grants share the same three calls; the user grant adds
|
|
18
|
+
the connect flow.
|
|
19
|
+
|
|
20
|
+
## grant.status()
|
|
21
|
+
|
|
22
|
+
Whether this grant is usable right now — check it before offering a feature:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const status = await canopy.google.user().status();
|
|
26
|
+
if (!status.connected) {
|
|
27
|
+
// show a connect button
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Field | Type | Meaning |
|
|
32
|
+
| ------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| `configured` | `boolean` | Whether this Canopy site has Google set up at all. |
|
|
34
|
+
| `connected` | `boolean` | Whether an account is connected on this grant. |
|
|
35
|
+
| `available` | `boolean?` | Whether this caller could use the grant at all — `false` for the project grant in a browser, or the user grant in a scheduled job. A fact to branch on, not an error. |
|
|
36
|
+
| `reason` | `string?` | Why the grant is unavailable, when it is. |
|
|
37
|
+
| `email` | `string?` | Which account acts when this grant is used. |
|
|
38
|
+
| `bundles` | `string[]?` | Permission bundles the grant can actually use. Google lets people approve a subset of what was asked, so branch on this, not on what you requested. |
|
|
39
|
+
| `requested` | `string[]?` | What was asked for. Anything here but missing from `bundles` needs a reconnect. |
|
|
40
|
+
| `catalog` | `GoogleBundle[]?` | Everything that could be connected — key, label, description, tier — for building your own connect prompt. |
|
|
41
|
+
|
|
42
|
+
## grant.fetch(url, init?)
|
|
43
|
+
|
|
44
|
+
Call a Google API URL and get its JSON response back, typed with the generic
|
|
45
|
+
you provide:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const sheet = await canopy.google
|
|
49
|
+
.project()
|
|
50
|
+
.fetch("https://sheets.googleapis.com/v4/spreadsheets/<id>/values/A1:C10");
|
|
51
|
+
|
|
52
|
+
const event = await canopy.google
|
|
53
|
+
.user()
|
|
54
|
+
.fetch<CalendarEvent>("https://www.googleapis.com/calendar/v3/calendars/primary/events", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
body: { summary: "Sync", start, end },
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- `url` — a full Google API URL, straight from Google's own API reference.
|
|
61
|
+
- `init.method` — `GET` (default), `POST`, `PUT`, `PATCH`, or `DELETE`.
|
|
62
|
+
- `init.headers` — optional; only `content-type`, `accept`, and `accept-language` reach Google.
|
|
63
|
+
- `init.body` — objects are JSON-encoded; strings are sent as-is.
|
|
64
|
+
|
|
65
|
+
Which URLs work depends on the permission bundles the grant holds — a call
|
|
66
|
+
outside them (or a write on a reading-only bundle) is refused before it
|
|
67
|
+
reaches Google and throws `CanopyError`.
|
|
68
|
+
|
|
69
|
+
## grant.raw(url, init?)
|
|
70
|
+
|
|
71
|
+
The same call, returning the raw `Response` — for endpoints that answer with
|
|
72
|
+
bytes rather than JSON, like a Drive file download or an exported document:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const res = await canopy.google
|
|
76
|
+
.project()
|
|
77
|
+
.raw("https://www.googleapis.com/drive/v3/files/<id>?alt=media");
|
|
78
|
+
const pdf = await res.blob();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Connecting a visitor (user grant only)
|
|
82
|
+
|
|
83
|
+
The project grant is connected once, by the owner, on the dashboard's
|
|
84
|
+
**Services** tab — there is no code involved. Visitors connect themselves,
|
|
85
|
+
from your pages:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const google = canopy.google.user();
|
|
89
|
+
|
|
90
|
+
// On load: did we just come back from a connect trip?
|
|
91
|
+
const result = google.consumeConnectResult(); // toast this, if not null
|
|
92
|
+
|
|
93
|
+
const status = await google.status();
|
|
94
|
+
if (!status.connected) {
|
|
95
|
+
google.connect(["calendar-read"]); // full-page trip to Google, then back here
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### user.connect(bundles, returnTo?)
|
|
100
|
+
|
|
101
|
+
Sends the browser to Google's consent screen — a full-page navigation, not a
|
|
102
|
+
fetch, so put the button somewhere without unsaved work. `bundles` is the list
|
|
103
|
+
of permission bundle keys to ask for (e.g. `"sheets-read"`, `"calendar"`,
|
|
104
|
+
`"gmail-send"` — the picker on the Services tab shows every key, and
|
|
105
|
+
`status().catalog` lists them at runtime). `returnTo` defaults to the current
|
|
106
|
+
page. Browser only — calling it in a worker throws.
|
|
107
|
+
|
|
108
|
+
### user.connectUrl(bundles, returnTo?)
|
|
109
|
+
|
|
110
|
+
The URL `connect()` navigates to, when you want to render it yourself as a
|
|
111
|
+
plain link.
|
|
112
|
+
|
|
113
|
+
### user.consumeConnectResult()
|
|
114
|
+
|
|
115
|
+
Reads the one-shot outcome the connect flow left in the URL and strips it, so
|
|
116
|
+
a refresh doesn't replay it. Returns `null` when this page load wasn't a
|
|
117
|
+
return trip:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const result = google.consumeConnectResult();
|
|
121
|
+
// { ok: true, email: "…" } | { ok: false, reason: "cancelled" | … } | null
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Call it on load and show a message when it isn't null. After the trip,
|
|
125
|
+
`status()` is the reliable answer to "are they connected" — this only tells
|
|
126
|
+
you how the trip that just happened went.
|
|
127
|
+
|
|
128
|
+
### user.disconnect()
|
|
129
|
+
|
|
130
|
+
Drops the visitor's connection and revokes it at Google, so access stops
|
|
131
|
+
immediately:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
await canopy.google.user().disconnect();
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
> **Note:** The user grant also works in worker code that is handling that visitor's request — same account,
|
|
138
|
+
> same calls. It cannot work in a scheduled job, because then nobody is signed in; that is what the
|
|
139
|
+
> project grant is for.
|
package/dist/docs/sdk.md
CHANGED
|
@@ -4,8 +4,8 @@ description: One typed client for auth, data, files, AI, and services — no key
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
`@mediamonks/canopy` is a thin, typed client over the platform's project API.
|
|
7
|
-
One `createCanopy()` call gives you
|
|
8
|
-
and `services` — and that is the entire surface. There are no API keys,
|
|
7
|
+
One `createCanopy()` call gives you six modules — `auth`, `db`, `blob`, `ai`,
|
|
8
|
+
`google`, and `services` — and that is the entire surface. There are no API keys,
|
|
9
9
|
connection strings, or environment variables to manage: every call rides the
|
|
10
10
|
session the platform already established before your code ran.
|
|
11
11
|
|
|
@@ -35,7 +35,7 @@ import { createCanopy } from "/_api/data/v1/sdk.js";
|
|
|
35
35
|
import { createCanopy } from "@mediamonks/canopy";
|
|
36
36
|
|
|
37
37
|
const canopy = createCanopy();
|
|
38
|
-
// canopy.auth, canopy.db, canopy.blob, canopy.ai, canopy.services
|
|
38
|
+
// canopy.auth, canopy.db, canopy.blob, canopy.ai, canopy.google, canopy.services
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
| Option | Type | Default | What it does |
|
|
@@ -119,8 +119,6 @@ environments:
|
|
|
119
119
|
|
|
120
120
|
## Modules
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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.
|
|
122
|
+
{/* Generated from this folder's meta.json + each page's frontmatter description — never hand-edit a list here. */}
|
|
123
|
+
|
|
124
|
+
<SectionIndex folder="/sdk" />
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-CF0dX7UA.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
4
|
import { n as reportErrors } from "./ui-B1Px5y7q.mjs";
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { a as projectDirArgs, c as resolveProject } from "./site-BUlQR117.mjs";
|
|
2
|
+
import { c as apiRequest, i as unwrap, n as reportErrors, r as requireTty } from "./ui-B1Px5y7q.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
//#region src/egress.ts
|
|
6
|
+
/**
|
|
7
|
+
* canopy egress — manage the project's outbound-host policy: the allowlist
|
|
8
|
+
* (docs/16-egress-allowlist.md) and the egress mode (docs/25-egress-modes.md).
|
|
9
|
+
* In allowlist mode, worker fetches to hosts outside the list are denied
|
|
10
|
+
* (`egress_denied`) on targets that enforce egress control; open mode allows
|
|
11
|
+
* any public hostname behind a built-in blocklist and an outbound rate limit.
|
|
12
|
+
* Policy lives on the platform, not in canopy.json, so a repo file can never
|
|
13
|
+
* widen egress silently on deploy.
|
|
14
|
+
*/
|
|
15
|
+
function printList({ mode, hosts, denied, enforced }, slug) {
|
|
16
|
+
if (mode === "open") console.log(`${slug} is in OPEN egress mode — the worker may fetch any public host (the allowlist below is kept but not enforced; \`canopy egress mode allowlist\` restores it).`);
|
|
17
|
+
if (hosts.length === 0) console.log(`No extra egress hosts for ${slug} — the worker can reach platform hosts only.`);
|
|
18
|
+
else for (const host of hosts) console.log(host);
|
|
19
|
+
if (denied.length > 0) {
|
|
20
|
+
console.log(`\nRecently blocked (allow with \`canopy egress allow <host>\`):`);
|
|
21
|
+
for (const d of denied) {
|
|
22
|
+
const why = d.reason === "rate_limited" ? "rate limited" : "blocked";
|
|
23
|
+
console.log(` ${d.host} ×${d.count} ${why}, last ${d.last_seen}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!enforced) console.error("(note: this site's target does not enforce egress yet — the policy is recorded for when it does)");
|
|
27
|
+
}
|
|
28
|
+
const listCommand = defineCommand({
|
|
29
|
+
meta: {
|
|
30
|
+
name: "list",
|
|
31
|
+
description: "List the project's allowed outbound hosts"
|
|
32
|
+
},
|
|
33
|
+
args: { ...projectDirArgs },
|
|
34
|
+
run: reportErrors("egress list", async ({ args }) => {
|
|
35
|
+
const { slug } = resolveProject(args);
|
|
36
|
+
printList(await apiRequest("GET", `/projects/${slug}/egress`), slug);
|
|
37
|
+
})
|
|
38
|
+
});
|
|
39
|
+
const allowCommand = defineCommand({
|
|
40
|
+
meta: {
|
|
41
|
+
name: "allow",
|
|
42
|
+
description: "Allow the worker to fetch a host (e.g. api.example.com or *.googleapis.com)"
|
|
43
|
+
},
|
|
44
|
+
args: {
|
|
45
|
+
host: {
|
|
46
|
+
type: "positional",
|
|
47
|
+
description: "Hostname to allow",
|
|
48
|
+
required: true
|
|
49
|
+
},
|
|
50
|
+
...projectDirArgs
|
|
51
|
+
},
|
|
52
|
+
run: reportErrors("egress allow", async ({ args }) => {
|
|
53
|
+
const { slug } = resolveProject(args);
|
|
54
|
+
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
55
|
+
const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: [...current.hosts, args.host] });
|
|
56
|
+
console.log(`Allowed ${args.host} for ${slug} — running workers pick it up now.\n`);
|
|
57
|
+
printList(updated, slug);
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
const removeCommand = defineCommand({
|
|
61
|
+
meta: {
|
|
62
|
+
name: "remove",
|
|
63
|
+
description: "Remove a host from the allowlist"
|
|
64
|
+
},
|
|
65
|
+
args: {
|
|
66
|
+
host: {
|
|
67
|
+
type: "positional",
|
|
68
|
+
description: "Hostname to remove",
|
|
69
|
+
required: true
|
|
70
|
+
},
|
|
71
|
+
...projectDirArgs
|
|
72
|
+
},
|
|
73
|
+
run: reportErrors("egress remove", async ({ args }) => {
|
|
74
|
+
const { slug } = resolveProject(args);
|
|
75
|
+
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
76
|
+
const wanted = args.host.trim().toLowerCase();
|
|
77
|
+
if (!current.hosts.includes(wanted)) {
|
|
78
|
+
console.error(`${wanted} is not on the allowlist for ${slug} (see \`canopy egress list\`).`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: current.hosts.filter((h) => h !== wanted) });
|
|
82
|
+
console.log(`Removed ${wanted} from ${slug} — running workers stop reaching it now.\n`);
|
|
83
|
+
printList(updated, slug);
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
/** The plain-language risk summary shown before open mode is enabled (docs/25 §2). */
|
|
87
|
+
function openModeRiskSummary(slug, clientAccess) {
|
|
88
|
+
const lines = [
|
|
89
|
+
`Open egress mode lets ${slug}'s worker contact ANY public host:`,
|
|
90
|
+
"",
|
|
91
|
+
" - If the URLs it fetches come from user input, users control where it connects.",
|
|
92
|
+
" - Abusive outbound traffic can get the project rate-limited or paused.",
|
|
93
|
+
" - A built-in blocklist (private IPs, localhost, cloud metadata) and an",
|
|
94
|
+
" outbound rate limit still apply, and every visited host is logged",
|
|
95
|
+
" (`canopy egress log`)."
|
|
96
|
+
];
|
|
97
|
+
if (clientAccess) lines.push("", " ! This project has client access enabled — untrusted visitors can drive", " what the worker fetches.");
|
|
98
|
+
lines.push("", "It's the right choice for apps that fetch arbitrary user-provided URLs", "(link collectors, feed readers, scrapers). Everything else is better served", "by the allowlist.");
|
|
99
|
+
return lines.join("\n");
|
|
100
|
+
}
|
|
101
|
+
const egressCommand = defineCommand({
|
|
102
|
+
meta: {
|
|
103
|
+
name: "egress",
|
|
104
|
+
description: "Manage which outbound hosts the project's worker may fetch"
|
|
105
|
+
},
|
|
106
|
+
subCommands: {
|
|
107
|
+
list: listCommand,
|
|
108
|
+
allow: allowCommand,
|
|
109
|
+
remove: removeCommand,
|
|
110
|
+
mode: defineCommand({
|
|
111
|
+
meta: {
|
|
112
|
+
name: "mode",
|
|
113
|
+
description: "Show or switch the egress mode (allowlist | open)"
|
|
114
|
+
},
|
|
115
|
+
args: {
|
|
116
|
+
mode: {
|
|
117
|
+
type: "positional",
|
|
118
|
+
description: "allowlist | open (omit to show)",
|
|
119
|
+
required: false
|
|
120
|
+
},
|
|
121
|
+
"accept-risk": {
|
|
122
|
+
type: "boolean",
|
|
123
|
+
description: "Acknowledge the open-mode risk summary without an interactive prompt"
|
|
124
|
+
},
|
|
125
|
+
...projectDirArgs
|
|
126
|
+
},
|
|
127
|
+
run: reportErrors("egress mode", async ({ args }) => {
|
|
128
|
+
const { slug } = resolveProject(args);
|
|
129
|
+
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
130
|
+
const wanted = args.mode?.trim().toLowerCase();
|
|
131
|
+
if (!wanted) {
|
|
132
|
+
console.log(current.mode);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (wanted !== "allowlist" && wanted !== "open") {
|
|
136
|
+
console.error(`unknown mode "${wanted}" — expected allowlist or open.`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
if (wanted === current.mode) {
|
|
140
|
+
console.log(`${slug} is already in ${wanted} mode.`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (wanted === "open") {
|
|
144
|
+
const access = await apiRequest("GET", `/projects/${slug}/client-password`).catch(() => ({ enabled: false }));
|
|
145
|
+
console.log(openModeRiskSummary(slug, access.enabled));
|
|
146
|
+
console.log("");
|
|
147
|
+
if (!args["accept-risk"]) {
|
|
148
|
+
requireTty("pass --accept-risk to enable open mode without confirmation.");
|
|
149
|
+
if (unwrap(await p.text({ message: `Type the project slug ("${slug}") to enable open mode.` })).trim() !== slug) {
|
|
150
|
+
p.cancel("Aborted — egress mode unchanged.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { mode: wanted });
|
|
156
|
+
if (wanted === "open") {
|
|
157
|
+
console.log(`Open egress mode is ON for ${slug} — running workers pick it up now.`);
|
|
158
|
+
console.log(`Switch back anytime with \`canopy egress mode allowlist\`.`);
|
|
159
|
+
} else {
|
|
160
|
+
console.log(`Allowlist mode restored for ${slug} — only listed hosts are reachable.\n`);
|
|
161
|
+
printList(updated, slug);
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
}),
|
|
165
|
+
log: defineCommand({
|
|
166
|
+
meta: {
|
|
167
|
+
name: "log",
|
|
168
|
+
description: "Show the outbound traffic log — hosts the worker actually fetched"
|
|
169
|
+
},
|
|
170
|
+
args: { ...projectDirArgs },
|
|
171
|
+
run: reportErrors("egress log", async ({ args }) => {
|
|
172
|
+
const { slug } = resolveProject(args);
|
|
173
|
+
const { visits } = await apiRequest("GET", `/projects/${slug}/egress/log`);
|
|
174
|
+
if (visits.length === 0) {
|
|
175
|
+
console.log(`No outbound traffic recorded for ${slug} yet.`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
for (const v of visits) console.log(`${v.host} ×${v.count} last ${v.last_seen}`);
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
//#endregion
|
|
184
|
+
export { egressCommand };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-CF0dX7UA.mjs";
|
|
2
2
|
import { a as projectDirArgs, c as resolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
|
|
3
3
|
import { c as apiRequest, l as apiUpload, n as reportErrors, s as apiDownload } from "./ui-B1Px5y7q.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
@@ -165,7 +165,8 @@ const showCommand = defineCommand({
|
|
|
165
165
|
for (const entry of comments) {
|
|
166
166
|
const author = entry.author_name || entry.author_email || "unknown";
|
|
167
167
|
const move = entry.status_to ? ` [${entry.status_from} → ${entry.status_to}]` : "";
|
|
168
|
-
|
|
168
|
+
const internal = entry.internal ? " [internal]" : "";
|
|
169
|
+
console.log(`--- ${author} · ${entry.created_at}${move}${internal}`);
|
|
169
170
|
if (entry.body) console.log(entry.body);
|
|
170
171
|
}
|
|
171
172
|
if (args.download && shots.length > 0) {
|
|
@@ -196,12 +197,19 @@ const commentCommand = defineCommand({
|
|
|
196
197
|
description: "Comment text",
|
|
197
198
|
required: true
|
|
198
199
|
},
|
|
200
|
+
internal: {
|
|
201
|
+
type: "boolean",
|
|
202
|
+
description: "Triager-only note, hidden from the reporter — for tech detail (stack traces, table names) that would only confuse them (members only)"
|
|
203
|
+
},
|
|
199
204
|
...projectDirArgs
|
|
200
205
|
},
|
|
201
206
|
run: reportErrors("feedback comment", async ({ args }) => {
|
|
202
207
|
const { slug } = resolveProject(args);
|
|
203
|
-
await apiRequest("POST", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}/comments`, {
|
|
204
|
-
|
|
208
|
+
await apiRequest("POST", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}/comments`, {
|
|
209
|
+
body: args.text,
|
|
210
|
+
...args.internal ? { internal: true } : {}
|
|
211
|
+
});
|
|
212
|
+
console.log(args.internal ? "Internal note added (hidden from the reporter)." : "Comment added.");
|
|
205
213
|
})
|
|
206
214
|
});
|
|
207
215
|
const statusCommand = defineCommand({
|
|
@@ -366,17 +374,28 @@ const feedbackCommand = defineCommand({
|
|
|
366
374
|
type: "string",
|
|
367
375
|
description: "Post this comment first and hang the screenshots off it"
|
|
368
376
|
},
|
|
377
|
+
internal: {
|
|
378
|
+
type: "boolean",
|
|
379
|
+
description: "Make the --comment a triager-only note; its screenshots are hidden from the reporter too (members only)"
|
|
380
|
+
},
|
|
369
381
|
...projectDirArgs
|
|
370
382
|
},
|
|
371
383
|
run: reportErrors("feedback attach", async ({ args }) => {
|
|
372
384
|
const { slug } = resolveProject(args);
|
|
373
385
|
const paths = [args.file, ...attachedPaths(args.attach)];
|
|
374
386
|
for (const path of paths) await readFile(path);
|
|
387
|
+
if (args.internal && !args.comment) {
|
|
388
|
+
console.error("--internal needs a --comment to hang the screenshots off.");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
375
391
|
let commentId;
|
|
376
392
|
if (args.comment) {
|
|
377
|
-
const { comment } = await apiRequest("POST", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}/comments`, {
|
|
393
|
+
const { comment } = await apiRequest("POST", `/projects/${slug}/feedback/${encodeURIComponent(args.id)}/comments`, {
|
|
394
|
+
body: args.comment,
|
|
395
|
+
...args.internal ? { internal: true } : {}
|
|
396
|
+
});
|
|
378
397
|
commentId = comment.id;
|
|
379
|
-
console.log("Comment added.");
|
|
398
|
+
console.log(args.internal ? "Internal note added (hidden from the reporter)." : "Comment added.");
|
|
380
399
|
}
|
|
381
400
|
await attachFiles(slug, args.id, paths, args.caption, commentId);
|
|
382
401
|
})
|
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-CF0dX7UA.mjs";
|
|
3
3
|
import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BUlQR117.mjs";
|
|
4
4
|
import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
@@ -74,15 +74,15 @@ const main = defineCommand({
|
|
|
74
74
|
rollback: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.rollbackCommand),
|
|
75
75
|
download: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.downloadCommand),
|
|
76
76
|
token: () => import("./token-BUxgRJVA.mjs").then((m) => m.tokenCommand),
|
|
77
|
-
egress: () => import("./egress-
|
|
77
|
+
egress: () => import("./egress-PJp0SZy2.mjs").then((m) => m.egressCommand),
|
|
78
78
|
schedule: () => import("./schedule-Cq9K0rnE.mjs").then((m) => m.scheduleCommand),
|
|
79
|
-
feedback: () => import("./feedback-
|
|
79
|
+
feedback: () => import("./feedback-h_j6DBBZ.mjs").then((m) => m.feedbackCommand),
|
|
80
80
|
db: () => import("./db-DKnJQPm5.mjs").then((m) => m.dbCommand),
|
|
81
81
|
usage: () => import("./usage-DjNngyvy.mjs").then((m) => m.usageCommand),
|
|
82
82
|
open: () => import("./open-C5x3tX6I.mjs").then((m) => m.openCommand),
|
|
83
|
-
docs: () => import("./docs-
|
|
84
|
-
skill: () => import("./skill-
|
|
85
|
-
upgrade: () => import("./upgrade-
|
|
83
|
+
docs: () => import("./docs-BlzdYmou.mjs").then((m) => m.docsCommand),
|
|
84
|
+
skill: () => import("./skill-DaaxgqEk.mjs").then((m) => m.skillCommand),
|
|
85
|
+
upgrade: () => import("./upgrade-DG-7xwOs.mjs").then((m) => m.upgradeCommand),
|
|
86
86
|
logs: () => import("./logs-C53y2eAS.mjs").then((m) => m.logsCommand)
|
|
87
87
|
}
|
|
88
88
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-CF0dX7UA.mjs";
|
|
2
2
|
import { n as reportErrors } from "./ui-B1Px5y7q.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
@@ -112,7 +112,7 @@ const upgradeCommand = defineCommand({
|
|
|
112
112
|
const spec = `${PACKAGE_NAME}@${args.version ?? "latest"}`;
|
|
113
113
|
const command = pmCommand(pm);
|
|
114
114
|
const cmd = installArgs(pm, spec);
|
|
115
|
-
const { skillInstalled } = await import("./skill-
|
|
115
|
+
const { skillInstalled } = await import("./skill-DaaxgqEk.mjs");
|
|
116
116
|
const refreshSkill = skillInstalled(process.cwd());
|
|
117
117
|
console.log(`Upgrading via ${command}: ${command} ${cmd.join(" ")}`);
|
|
118
118
|
await run(command, cmd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mediamonks/canopy-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
4
4
|
"description": "Canopy CLI — create, develop and deploy Canopy projects.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"canopy": "./dist/index.mjs"
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"esbuild": "^0.28.1",
|
|
21
21
|
"hono": "^4.8.0",
|
|
22
22
|
"vite-server": "npm:vite@^7",
|
|
23
|
-
"@mediamonks/canopy": "0.0.
|
|
24
|
-
"@mediamonks/canopy
|
|
23
|
+
"@mediamonks/canopy-data-plane": "0.0.4",
|
|
24
|
+
"@mediamonks/canopy": "0.0.5"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.1",
|
package/dist/egress-BFSwa8Uu.mjs
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { a as projectDirArgs, c as resolveProject } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { c as apiRequest, n as reportErrors } from "./ui-B1Px5y7q.mjs";
|
|
3
|
-
import { defineCommand } from "citty";
|
|
4
|
-
//#region src/egress.ts
|
|
5
|
-
/**
|
|
6
|
-
* canopy egress — manage the project's outbound-host allowlist
|
|
7
|
-
* (docs/16-egress-allowlist.md). Worker fetches to hosts outside the list are
|
|
8
|
-
* denied (`egress_denied`) on targets that enforce egress control; the list
|
|
9
|
-
* lives on the platform, not in canopy.json, so a repo file can never widen
|
|
10
|
-
* egress silently on deploy.
|
|
11
|
-
*/
|
|
12
|
-
function printList({ hosts, denied, enforced }, slug) {
|
|
13
|
-
if (hosts.length === 0) console.log(`No extra egress hosts for ${slug} — the worker can reach platform hosts only.`);
|
|
14
|
-
else for (const host of hosts) console.log(host);
|
|
15
|
-
if (denied.length > 0) {
|
|
16
|
-
console.log(`\nRecently blocked (allow with \`canopy egress allow <host>\`):`);
|
|
17
|
-
for (const d of denied) console.log(` ${d.host} ×${d.count} last ${d.last_seen}`);
|
|
18
|
-
}
|
|
19
|
-
if (!enforced) console.error("(note: this site's target does not enforce egress yet — the list is recorded for when it does)");
|
|
20
|
-
}
|
|
21
|
-
const egressCommand = defineCommand({
|
|
22
|
-
meta: {
|
|
23
|
-
name: "egress",
|
|
24
|
-
description: "Manage which outbound hosts the project's worker may fetch"
|
|
25
|
-
},
|
|
26
|
-
subCommands: {
|
|
27
|
-
list: defineCommand({
|
|
28
|
-
meta: {
|
|
29
|
-
name: "list",
|
|
30
|
-
description: "List the project's allowed outbound hosts"
|
|
31
|
-
},
|
|
32
|
-
args: { ...projectDirArgs },
|
|
33
|
-
run: reportErrors("egress list", async ({ args }) => {
|
|
34
|
-
const { slug } = resolveProject(args);
|
|
35
|
-
printList(await apiRequest("GET", `/projects/${slug}/egress`), slug);
|
|
36
|
-
})
|
|
37
|
-
}),
|
|
38
|
-
allow: defineCommand({
|
|
39
|
-
meta: {
|
|
40
|
-
name: "allow",
|
|
41
|
-
description: "Allow the worker to fetch a host (e.g. api.example.com or *.googleapis.com)"
|
|
42
|
-
},
|
|
43
|
-
args: {
|
|
44
|
-
host: {
|
|
45
|
-
type: "positional",
|
|
46
|
-
description: "Hostname to allow",
|
|
47
|
-
required: true
|
|
48
|
-
},
|
|
49
|
-
...projectDirArgs
|
|
50
|
-
},
|
|
51
|
-
run: reportErrors("egress allow", async ({ args }) => {
|
|
52
|
-
const { slug } = resolveProject(args);
|
|
53
|
-
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
54
|
-
const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: [...current.hosts, args.host] });
|
|
55
|
-
console.log(`Allowed ${args.host} for ${slug} — running workers pick it up now.\n`);
|
|
56
|
-
printList(updated, slug);
|
|
57
|
-
})
|
|
58
|
-
}),
|
|
59
|
-
remove: defineCommand({
|
|
60
|
-
meta: {
|
|
61
|
-
name: "remove",
|
|
62
|
-
description: "Remove a host from the allowlist"
|
|
63
|
-
},
|
|
64
|
-
args: {
|
|
65
|
-
host: {
|
|
66
|
-
type: "positional",
|
|
67
|
-
description: "Hostname to remove",
|
|
68
|
-
required: true
|
|
69
|
-
},
|
|
70
|
-
...projectDirArgs
|
|
71
|
-
},
|
|
72
|
-
run: reportErrors("egress remove", async ({ args }) => {
|
|
73
|
-
const { slug } = resolveProject(args);
|
|
74
|
-
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
75
|
-
const wanted = args.host.trim().toLowerCase();
|
|
76
|
-
if (!current.hosts.includes(wanted)) {
|
|
77
|
-
console.error(`${wanted} is not on the allowlist for ${slug} (see \`canopy egress list\`).`);
|
|
78
|
-
process.exit(1);
|
|
79
|
-
}
|
|
80
|
-
const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: current.hosts.filter((h) => h !== wanted) });
|
|
81
|
-
console.log(`Removed ${wanted} from ${slug} — running workers stop reaching it now.\n`);
|
|
82
|
-
printList(updated, slug);
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
//#endregion
|
|
88
|
-
export { egressCommand };
|