@mediamonks/canopy-cli 0.7.14 → 0.7.16
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-docs.md +10 -0
- package/dist/docs/cli-egress.md +9 -0
- 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-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-3pQHiuTn.mjs → docs-D7Ey1ZMQ.mjs} +92 -2
- package/dist/egress-PJp0SZy2.mjs +184 -0
- package/dist/{feedback-DReTDDzD.mjs → feedback-B0iX3iZ1.mjs} +1 -1
- package/dist/index.mjs +6 -6
- package/dist/{package-CuyFMXam.mjs → package-BGDIsRxe.mjs} +1 -1
- package/dist/skill/canopy/SKILL.md +3 -2
- package/dist/{skill-M4RHvuUB.mjs → skill-Be_pqqdH.mjs} +1 -1
- package/dist/{upgrade-C8Xox-P-.mjs → upgrade-ChwehW_q.mjs} +1 -1
- package/package.json +3 -3
- package/dist/egress-BFSwa8Uu.mjs +0 -88
package/dist/docs/cli-docs.md
CHANGED
|
@@ -10,12 +10,14 @@ description: Read the Canopy docs from the terminal — live from the platform,
|
|
|
10
10
|
```bash
|
|
11
11
|
canopy docs # list all topics
|
|
12
12
|
canopy docs <topic> # print one topic
|
|
13
|
+
canopy docs search <terms…> # keyword search across all docs
|
|
13
14
|
canopy docs <topic> --refresh
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
## Arguments & flags
|
|
17
18
|
|
|
18
19
|
- `[topic]` — positional: the topic slug to print. Omit it to list every topic with a one-line summary, grouped by section. Guides keep bare slugs (`deploy`, `staging`); other sections prefix theirs (`sdk-database`, `cli-deploy`, `platform-schedules`).
|
|
20
|
+
- `search <terms…>` — search every doc for the given terms instead of printing one. Case-insensitive; a doc matches only when every term appears somewhere in its title, summary, or body. Results come back best-first (title and summary hits outrank body mentions), each with the matching lines, capped at ten.
|
|
19
21
|
- `--refresh` — re-fetch from the platform, bypassing the local cache. Default `false`.
|
|
20
22
|
|
|
21
23
|
## How it works
|
|
@@ -30,6 +32,8 @@ That last step is why `canopy docs` works with no network and no login: a fresh
|
|
|
30
32
|
|
|
31
33
|
Asking for an unknown topic errors with the list of available slugs. Everything prints as plain Markdown to stdout, so it pipes cleanly into a pager or an agent's context.
|
|
32
34
|
|
|
35
|
+
Search runs entirely on the local corpus — the cache plus the bundled snapshot — so it's instant and works offline. It's a locator, not a reader: find the topic, then print it with `canopy docs <topic>` (which fetches fresh content when it can).
|
|
36
|
+
|
|
33
37
|
## Examples
|
|
34
38
|
|
|
35
39
|
```bash
|
|
@@ -38,6 +42,12 @@ canopy docs
|
|
|
38
42
|
|
|
39
43
|
Lists every topic with its summary — the starting point when you're not sure which guide covers what you're doing.
|
|
40
44
|
|
|
45
|
+
```bash
|
|
46
|
+
canopy docs search egress allowlist
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Finds the docs that mention both terms — useful when the summaries don't tell you which doc covers a detail, like a specific error code or environment variable.
|
|
50
|
+
|
|
41
51
|
```bash
|
|
42
52
|
canopy docs deploy --refresh
|
|
43
53
|
```
|
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
|
|
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.
|
|
@@ -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-BGDIsRxe.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";
|
|
@@ -14,6 +14,10 @@ import { fileURLToPath } from "node:url";
|
|
|
14
14
|
* never goes stale, with a local cache and a bundled snapshot as offline
|
|
15
15
|
* fallbacks. The listing is grouped per docs-site section.
|
|
16
16
|
*
|
|
17
|
+
* `canopy docs search <terms…>` greps the locally available corpus (cache +
|
|
18
|
+
* bundled snapshot) — the corpus is small enough (~60 docs) that keyword
|
|
19
|
+
* matching beats any index, and it works fully offline.
|
|
20
|
+
*
|
|
17
21
|
* The platform requires a login for docs (internal platform), so fetches carry
|
|
18
22
|
* the CLI token when there is one; without it we simply fall back to the
|
|
19
23
|
* bundled snapshot, so `canopy docs` still works before sign-in.
|
|
@@ -140,6 +144,75 @@ function bodyFromFallback(slug) {
|
|
|
140
144
|
}
|
|
141
145
|
}
|
|
142
146
|
}
|
|
147
|
+
const SEARCH_LIMIT = 10;
|
|
148
|
+
const LINE_LIMIT = 2;
|
|
149
|
+
const LINE_WIDTH = 120;
|
|
150
|
+
/** A matching body line, trimmed and windowed around the first term hit. */
|
|
151
|
+
function excerpt(line, matchIndex) {
|
|
152
|
+
const clean = line.replace(/\s+/g, " ").trim();
|
|
153
|
+
if (clean.length <= LINE_WIDTH) return clean;
|
|
154
|
+
const start = Math.max(0, Math.min(matchIndex - Math.floor(LINE_WIDTH / 3), clean.length - LINE_WIDTH));
|
|
155
|
+
const slice = clean.slice(start, start + LINE_WIDTH);
|
|
156
|
+
return `${start > 0 ? "…" : ""}${slice}${start + LINE_WIDTH < clean.length ? "…" : ""}`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Keyword search over the docs corpus: case-insensitive, every term must match
|
|
160
|
+
* somewhere in a doc (title, summary, or body). Title and summary hits weigh
|
|
161
|
+
* more than body occurrences; results come back best-first.
|
|
162
|
+
*/
|
|
163
|
+
function searchDocs(entries, query) {
|
|
164
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
165
|
+
if (terms.length === 0) return [];
|
|
166
|
+
const hits = [];
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
const title = entry.title.toLowerCase();
|
|
169
|
+
const summary = entry.summary.toLowerCase();
|
|
170
|
+
const body = entry.body.toLowerCase();
|
|
171
|
+
let score = 0;
|
|
172
|
+
let matchesAll = true;
|
|
173
|
+
for (const term of terms) {
|
|
174
|
+
let termScore = 0;
|
|
175
|
+
if (title.includes(term)) termScore += 10;
|
|
176
|
+
if (summary.includes(term)) termScore += 5;
|
|
177
|
+
let count = 0;
|
|
178
|
+
for (let i = body.indexOf(term); i !== -1 && count < 20; i = body.indexOf(term, i + term.length)) count++;
|
|
179
|
+
termScore += count;
|
|
180
|
+
if (termScore === 0) {
|
|
181
|
+
matchesAll = false;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
score += termScore;
|
|
185
|
+
}
|
|
186
|
+
if (!matchesAll) continue;
|
|
187
|
+
const lines = [];
|
|
188
|
+
for (const line of entry.body.split("\n")) {
|
|
189
|
+
const lower = line.toLowerCase();
|
|
190
|
+
const at = terms.map((t) => lower.indexOf(t)).find((i) => i !== -1);
|
|
191
|
+
if (at !== void 0 && line.trim()) {
|
|
192
|
+
lines.push(excerpt(line, at));
|
|
193
|
+
if (lines.length >= LINE_LIMIT) break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
hits.push({
|
|
197
|
+
slug: entry.slug,
|
|
198
|
+
title: entry.title,
|
|
199
|
+
score,
|
|
200
|
+
lines
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return hits.sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Every topic paired with its locally available body (cache first, bundled
|
|
207
|
+
* snapshot second). A topic with no local body still gets searched by title
|
|
208
|
+
* and summary — search never hits the network per doc.
|
|
209
|
+
*/
|
|
210
|
+
function searchEntries(index) {
|
|
211
|
+
return index.topics.map((t) => ({
|
|
212
|
+
...t,
|
|
213
|
+
body: bodyFromFallback(t.slug) ?? ""
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
143
216
|
async function listIndex(refresh) {
|
|
144
217
|
const { apiUrl } = siteConfig();
|
|
145
218
|
if (refresh || !cacheFresh()) {
|
|
@@ -174,7 +247,7 @@ const docsCommand = defineCommand({
|
|
|
174
247
|
topic: {
|
|
175
248
|
type: "positional",
|
|
176
249
|
required: false,
|
|
177
|
-
description: "Topic to print (omit to list)"
|
|
250
|
+
description: "Topic to print (omit to list), or `search <terms…>`"
|
|
178
251
|
},
|
|
179
252
|
refresh: {
|
|
180
253
|
type: "boolean",
|
|
@@ -187,6 +260,23 @@ const docsCommand = defineCommand({
|
|
|
187
260
|
const topic = args.topic;
|
|
188
261
|
const latest = latestKnownVersion();
|
|
189
262
|
if (latest && compareSemver(version, latest) < 0) console.error(`Note: these docs describe CLI ${latest}; you have ${version} — commands or flags they mention may not exist here. Run \`canopy upgrade\`.\n`);
|
|
263
|
+
if (topic === "search") {
|
|
264
|
+
const query = args._.slice(1).join(" ").trim();
|
|
265
|
+
if (!query) throw new Error("Usage: canopy docs search <terms…>");
|
|
266
|
+
const hits = searchDocs(searchEntries(await listIndex(refresh)), query);
|
|
267
|
+
if (hits.length === 0) {
|
|
268
|
+
console.log(`No docs match "${query}". Run \`canopy docs\` to list all topics.`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const shown = hits.slice(0, SEARCH_LIMIT);
|
|
272
|
+
console.log(`${hits.length} doc${hits.length === 1 ? "" : "s"} match "${query}" — run \`canopy docs <topic>\` to read one:\n`);
|
|
273
|
+
for (const hit of shown) {
|
|
274
|
+
console.log(`${hit.slug} — ${hit.title}`);
|
|
275
|
+
for (const line of hit.lines) console.log(` ${line}`);
|
|
276
|
+
}
|
|
277
|
+
if (hits.length > shown.length) console.log(`\n…and ${hits.length - shown.length} more — add terms to narrow it down.`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
190
280
|
if (!topic) {
|
|
191
281
|
const index = await listIndex(refresh);
|
|
192
282
|
if (index.topics.length === 0) throw new Error("No docs available — check your connection, or reinstall the CLI.");
|
|
@@ -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-BGDIsRxe.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";
|
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-BGDIsRxe.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-B0iX3iZ1.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-D7Ey1ZMQ.mjs").then((m) => m.docsCommand),
|
|
84
|
+
skill: () => import("./skill-Be_pqqdH.mjs").then((m) => m.skillCommand),
|
|
85
|
+
upgrade: () => import("./upgrade-ChwehW_q.mjs").then((m) => m.upgradeCommand),
|
|
86
86
|
logs: () => import("./logs-C53y2eAS.mjs").then((m) => m.logsCommand)
|
|
87
87
|
}
|
|
88
88
|
});
|
|
@@ -71,8 +71,9 @@ The CLI ships the full documentation and keeps it fresh from the platform. List
|
|
|
71
71
|
the topics and read the relevant one _before_ running unfamiliar commands:
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
|
-
canopy docs
|
|
75
|
-
canopy docs deploy
|
|
74
|
+
canopy docs # list all topics
|
|
75
|
+
canopy docs deploy # print one topic
|
|
76
|
+
canopy docs search egress allowlist # find the topics that mention all terms
|
|
76
77
|
```
|
|
77
78
|
|
|
78
79
|
The listing is grouped by section: guides (`getting-started`, `deploy`, `dev`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-BGDIsRxe.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-Be_pqqdH.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.16",
|
|
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 };
|