@mediamonks/canopy-cli 0.7.40 → 0.7.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ai-EyrXJC5z.mjs → ai-PXXMv7kO.mjs} +2 -2
- package/dist/{api-ArfD6-t5.mjs → api-BHzCvDcY.mjs} +12 -0
- package/dist/{browse-DEhnJAPT.mjs → browse-DRz1DowL.mjs} +1 -1
- package/dist/{db-DGrCgj-R.mjs → db-5LfBG6N7.mjs} +3 -3
- package/dist/{deploy-B7KItUnh.mjs → deploy-6rYq1Iq8.mjs} +3 -3
- package/dist/dev-D38FQ2LD.mjs +2 -0
- package/dist/{dev-03LA_Etx.mjs → dev-D_gyIeUq.mjs} +5 -5
- package/dist/docs/platform-egress.md +69 -12
- package/dist/docs/troubleshooting.md +16 -0
- package/dist/{docs-B0-IEPjd.mjs → docs-B9xGnLSR.mjs} +3 -3
- package/dist/egress-CCpOsYpS.mjs +397 -0
- package/dist/{feedback-BsJ_6ke2.mjs → feedback-CdjtPumG.mjs} +3 -3
- package/dist/index.mjs +31 -31
- package/dist/{local-CkS1XhRu.mjs → local-Dshy0NsJ.mjs} +2 -2
- package/dist/{login-B2FJGIrD.mjs → login-CKNFCp7c.mjs} +2 -2
- package/dist/{logs-wSKh_FmL.mjs → logs-D3WakFME.mjs} +2 -2
- package/dist/{mcp-DtRIDtaL.mjs → mcp-CoVS5Mj3.mjs} +2 -2
- package/dist/{migrations-CG_O5Ghw.mjs → migrations-DDJ5JJqj.mjs} +1 -1
- package/dist/{open-COFMburt.mjs → open-BPYozPQo.mjs} +1 -1
- package/dist/{package-C1xCnyOB.mjs → package-SWgTmctH.mjs} +1 -1
- package/dist/project-template-CMgD5hnV.mjs +2 -0
- package/dist/{project-template-DtTfXyLI.mjs → project-template-DACdMdpn.mjs} +2 -2
- package/dist/{projects-B3Wi01lw.mjs → projects-naWQ89Qj.mjs} +4 -4
- package/dist/{repo-4x-CJwCp.mjs → repo-BCua4L-I.mjs} +2 -2
- package/dist/repo-Un_p617X.mjs +2 -0
- package/dist/{schedule-CbaDIJXr.mjs → schedule-DjHcaxyt.mjs} +2 -2
- package/dist/{secrets-CmY4dUHz.mjs → secrets-COwRvCev.mjs} +2 -2
- package/dist/{skill-AqZYYsE6.mjs → skill-DSYQA4Ls.mjs} +2 -2
- package/dist/{staging-ypR79MlD.mjs → staging-CYMTZyyM.mjs} +2 -2
- package/dist/{token-CDrHQ5Xb.mjs → token-CCpJ1ES4.mjs} +2 -2
- package/dist/{ui-CaHgvvkJ.mjs → ui-SBUdC9rX.mjs} +1 -1
- package/dist/{upgrade-DSjmifH_.mjs → upgrade-B_tLCpc4.mjs} +2 -2
- package/dist/{usage-CP8rBVZi.mjs → usage-BSPnsMXd.mjs} +9 -2
- package/package.json +1 -1
- package/dist/dev-dJ88L73Y.mjs +0 -2
- package/dist/egress-CzxRD6Tp.mjs +0 -188
- package/dist/project-template-DlzMx8Oj.mjs +0 -2
- package/dist/repo-CmbakFVi.mjs +0 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as apiRequest, l as withContext } from "./api-
|
|
2
|
-
import { r as reportErrors } from "./ui-
|
|
1
|
+
import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
|
|
2
|
+
import { r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/ai.ts
|
|
5
5
|
/**
|
|
@@ -156,6 +156,18 @@ const ERROR_COPY = {
|
|
|
156
156
|
why: "the schedule was saved but syncing it to the platform scheduler failed",
|
|
157
157
|
fix: "Retry the same command — it re-syncs the saved schedule."
|
|
158
158
|
},
|
|
159
|
+
fixed_ip_hosts_required: {
|
|
160
|
+
why: "routing listed hosts through the fixed addresses needs at least one host",
|
|
161
|
+
fix: "Add a host with `canopy egress fixed-ip add <host>`, or turn it off with `canopy egress fixed-ip off`."
|
|
162
|
+
},
|
|
163
|
+
fixed_ip_not_optional: {
|
|
164
|
+
why: "every request on this site already leaves from the fixed addresses",
|
|
165
|
+
fix: "There is nothing to set here — `canopy egress fixed-ip` shows the addresses to register."
|
|
166
|
+
},
|
|
167
|
+
invalid_fixed_ip_mode: {
|
|
168
|
+
why: "the fixed outbound address mode must be off, hosts or all",
|
|
169
|
+
fix: "Use `canopy egress fixed-ip add <host>`, `canopy egress fixed-ip all` or `canopy egress fixed-ip off`."
|
|
170
|
+
},
|
|
159
171
|
worker_egress_sync_failed: {
|
|
160
172
|
why: "the list was saved but redeploying the running worker with it failed",
|
|
161
173
|
fix: "Retry, or redeploy the project (`canopy deploy`) to apply the new allowlist."
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { d as tryResolveProject, n as siteConfig } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, t as ApiError } from "./api-
|
|
2
|
+
import { a as apiRequest, t as ApiError } from "./api-BHzCvDcY.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/browse.ts
|
|
5
5
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext, r as apiDataRequest } from "./api-
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-
|
|
4
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
2
|
+
import { a as apiRequest, l as withContext, r as apiDataRequest } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
4
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-DDJ5JJqj.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, l as stagingUrl, n as siteConfig, o as projectFlagArgs, s as projectUrl, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, i as apiDownload, l as withContext, r as apiDataRequest } from "./api-
|
|
3
|
-
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-
|
|
4
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
2
|
+
import { a as apiRequest, i as apiDownload, l as withContext, r as apiDataRequest } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
|
|
4
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-DDJ5JJqj.mjs";
|
|
5
5
|
import { builtinModules } from "node:module";
|
|
6
6
|
import { defineCommand } from "citty";
|
|
7
7
|
import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext, t as ApiError } from "./api-
|
|
3
|
-
import { r as reportErrors, t as formatDateTime } from "./ui-
|
|
4
|
-
import { r as readMigrationFiles } from "./migrations-
|
|
2
|
+
import { a as apiRequest, l as withContext, t as ApiError } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
|
|
4
|
+
import { r as readMigrationFiles } from "./migrations-DDJ5JJqj.mjs";
|
|
5
5
|
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER } from "./src-RUf3Gfuh.mjs";
|
|
6
6
|
import { n as writeDevServerRecord } from "./dev-server-file-Dk9pk4AR.mjs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
@@ -251,7 +251,7 @@ async function devRemote(flags, slug) {
|
|
|
251
251
|
async function devLocal(flags, slug) {
|
|
252
252
|
let dump;
|
|
253
253
|
if (flags.pull) {
|
|
254
|
-
const { fetchProjectDump } = await import("./local-
|
|
254
|
+
const { fetchProjectDump } = await import("./local-Dshy0NsJ.mjs");
|
|
255
255
|
console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
|
|
256
256
|
dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
|
|
257
257
|
}
|
|
@@ -267,7 +267,7 @@ async function devLocal(flags, slug) {
|
|
|
267
267
|
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
268
268
|
}
|
|
269
269
|
const workerAttest = randomBytes(24).toString("base64url");
|
|
270
|
-
const { createSandbox } = await import("./local-
|
|
270
|
+
const { createSandbox } = await import("./local-Dshy0NsJ.mjs");
|
|
271
271
|
const sandbox = await createSandbox({
|
|
272
272
|
slug,
|
|
273
273
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
@@ -112,12 +112,11 @@ Switching back is one command (`canopy egress mode allowlist`), and the
|
|
|
112
112
|
dashboard offers to **seed the allowlist from the observed hosts** so
|
|
113
113
|
tightening up later doesn't break the app.
|
|
114
114
|
|
|
115
|
-
##
|
|
115
|
+
## Fixed outbound addresses
|
|
116
116
|
|
|
117
117
|
Some external services only accept calls from IP addresses you register with
|
|
118
|
-
them first
|
|
119
|
-
allowlist.
|
|
120
|
-
`canopy.monks.zone`) make every outbound request from a fixed pair of
|
|
118
|
+
them first: a partner API, a corporate database, a payment provider's
|
|
119
|
+
allowlist. Canopy can send your backend's requests from a fixed pair of
|
|
121
120
|
addresses, so you can hand them over:
|
|
122
121
|
|
|
123
122
|
| Address |
|
|
@@ -126,16 +125,74 @@ addresses, so you can hand them over:
|
|
|
126
125
|
| `34.158.168.43` |
|
|
127
126
|
|
|
128
127
|
Register **both**. Traffic may leave from either one, and the second gives the
|
|
129
|
-
platform room to grow without asking you to update
|
|
130
|
-
addresses are shared by every project
|
|
131
|
-
any Canopy project
|
|
132
|
-
|
|
128
|
+
platform room to grow without asking you to update the other side. These
|
|
129
|
+
addresses are shared by every project that uses them, so registering them lets
|
|
130
|
+
any Canopy project reach the service. The other service's own authentication
|
|
131
|
+
(API keys, tokens) still decides who gets in.
|
|
133
132
|
|
|
134
133
|
The addresses are a platform commitment: they will not change without notice
|
|
135
|
-
and a migration period.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
and a migration period.
|
|
135
|
+
|
|
136
|
+
### Route a partner's host
|
|
137
|
+
|
|
138
|
+
By default requests leave normally, not from these addresses. Route the hosts
|
|
139
|
+
that need them:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
canopy egress fixed-ip add api.partner.com
|
|
143
|
+
canopy egress fixed-ip # show the setting and the addresses
|
|
144
|
+
canopy egress fixed-ip remove api.partner.com
|
|
145
|
+
canopy egress fixed-ip off
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
or in the dashboard: **Backend → Outbound hosts → Fixed outbound addresses**.
|
|
149
|
+
Entries follow the same rules as the allowlist (a bare host or a `*.`
|
|
150
|
+
subdomain wildcard, up to 32). Each request is routed by its own host, so if
|
|
151
|
+
the partner redirects you to a CDN that isn't listed, that second request
|
|
152
|
+
leaves normally. The setting applies to the running worker right away, to
|
|
153
|
+
both channels, and to scheduled runs as well as HTTP requests.
|
|
154
|
+
|
|
155
|
+
Only the **project owner** can turn the setting on or route all traffic. Any
|
|
156
|
+
member can change the listed hosts or turn it off.
|
|
157
|
+
|
|
158
|
+
**Routing a host doesn't allow it.** In allowlist mode the host still has to be
|
|
159
|
+
on the allowlist, or requests to it are denied as usual. The dashboard and
|
|
160
|
+
`canopy egress fixed-ip` flag routed hosts that aren't allowed and offer to
|
|
161
|
+
allow them. The setting works in open mode too.
|
|
162
|
+
|
|
163
|
+
### All traffic
|
|
164
|
+
|
|
165
|
+
`canopy egress fixed-ip all` sends **every** outbound request from the fixed
|
|
166
|
+
addresses. It's there for apps that can't list their partners up front, but
|
|
167
|
+
it's rarely the better choice: every request takes the slower route, counts
|
|
168
|
+
toward the daily quota, and leaves from addresses shared with other projects.
|
|
169
|
+
List the hosts that need it instead when you can.
|
|
170
|
+
|
|
171
|
+
### What to expect
|
|
172
|
+
|
|
173
|
+
- **A little latency.** A routed request makes an extra hop, usually tens of
|
|
174
|
+
milliseconds. The first routed request after a quiet spell (about 15
|
|
175
|
+
minutes) can take a second or two longer while the route wakes up. Give
|
|
176
|
+
tight partner timeouts some slack.
|
|
177
|
+
- **A daily transfer quota.** What a project sends and receives through the
|
|
178
|
+
fixed addresses counts toward 1 GB a day, shown on the **Usage** tab. Once
|
|
179
|
+
it's used up, routed requests are refused until 00:00 UTC; everything else
|
|
180
|
+
keeps working. Ask the platform team if you need more.
|
|
181
|
+
- **`fetch` only.** Raw socket connections (`connect()`) and outbound
|
|
182
|
+
WebSockets don't leave from the fixed addresses.
|
|
183
|
+
- **Uploads up to 32 MB.** A larger request body to a routed host is refused.
|
|
184
|
+
|
|
185
|
+
On some sites every request of every project already leaves from these
|
|
186
|
+
addresses. There the setting shows as always on and there is nothing to set.
|
|
187
|
+
|
|
188
|
+
### Errors from routed requests
|
|
189
|
+
|
|
190
|
+
| Status | `error` | What it means |
|
|
191
|
+
| ------ | -------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
192
|
+
| 429 | `egress_fixed_ip_quota_exceeded` | Today's fixed-address quota is used up. Routed requests work again at 00:00 UTC. |
|
|
193
|
+
| 502 | `egress_fixed_ip_unavailable` | The fixed-address route couldn't send the request. Nothing went out from another address; retry. |
|
|
194
|
+
| 413 | `egress_fixed_ip_body_too_large` | The request body is over 32 MB. |
|
|
195
|
+
| 403 | `egress_denied` | As always: the host isn't allowed, or it resolves to a private address. |
|
|
139
196
|
|
|
140
197
|
## The outbound traffic log
|
|
141
198
|
|
|
@@ -75,6 +75,22 @@ or use dashboard Settings → Outbound hosts. Details and entry rules:
|
|
|
75
75
|
[Outbound requests](https://docs.canopy.website/platform/egress). Calls made through the SDK /
|
|
76
76
|
`env.CANOPY.fetch` don't count as egress and are unaffected.
|
|
77
77
|
|
|
78
|
+
## `egress_fixed_ip_quota_exceeded` (429)
|
|
79
|
+
|
|
80
|
+
The project used up today's transfer through the
|
|
81
|
+
[fixed outbound addresses](https://docs.canopy.website/platform/egress#fixed-outbound-addresses). Routed
|
|
82
|
+
requests are refused until 00:00 UTC; requests to other hosts keep working.
|
|
83
|
+
`canopy usage` and the dashboard's Usage tab show how much was sent. If routing
|
|
84
|
+
all traffic used it up, list only the hosts that need a fixed address
|
|
85
|
+
(`canopy egress fixed-ip add <host>`).
|
|
86
|
+
|
|
87
|
+
## `egress_fixed_ip_unavailable` (502)
|
|
88
|
+
|
|
89
|
+
A request to a host routed through the fixed outbound addresses couldn't be
|
|
90
|
+
sent that way. It never went out from another address, so the partner didn't
|
|
91
|
+
see it. Retry; if it keeps happening, the detail says why (and the platform
|
|
92
|
+
team hears about it).
|
|
93
|
+
|
|
78
94
|
## The worker returns errors and you can't see why
|
|
79
95
|
|
|
80
96
|
Read its console output instead of adding diagnostic endpoints:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-SWgTmctH.mjs";
|
|
2
2
|
import { n as siteConfig } from "./site-DM2J10CB.mjs";
|
|
3
3
|
import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
|
|
4
|
-
import { n as agentHeaders } from "./api-
|
|
5
|
-
import { r as reportErrors } from "./ui-
|
|
4
|
+
import { n as agentHeaders } from "./api-BHzCvDcY.mjs";
|
|
5
|
+
import { r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
6
6
|
import { defineCommand } from "citty";
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
|
|
2
|
+
import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
//#region src/egress.ts
|
|
7
|
+
/**
|
|
8
|
+
* canopy egress — manage the project's outbound-host policy: the allowlist
|
|
9
|
+
* (docs/16-egress-allowlist.md) and the egress mode (docs/25-egress-modes.md).
|
|
10
|
+
* In allowlist mode, worker fetches to hosts outside the list are denied
|
|
11
|
+
* (`egress_denied`) on targets that enforce egress control; open mode allows
|
|
12
|
+
* any public hostname behind a built-in blocklist and an outbound rate limit.
|
|
13
|
+
* Policy lives on the platform, not in canopy.json, so a repo file can never
|
|
14
|
+
* widen egress silently on deploy.
|
|
15
|
+
*/
|
|
16
|
+
function printList({ mode, hosts, denied, enforced }, slug) {
|
|
17
|
+
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).`);
|
|
18
|
+
if (hosts.length === 0) console.log(`No extra egress hosts for ${slug} — the worker can reach platform hosts only.`);
|
|
19
|
+
else for (const host of hosts) console.log(host);
|
|
20
|
+
if (denied.length > 0) {
|
|
21
|
+
console.log(`\nRecently blocked (allow with \`canopy egress allow <host>\`):`);
|
|
22
|
+
for (const d of denied) {
|
|
23
|
+
const why = d.reason === "rate_limited" ? "rate limited" : "blocked";
|
|
24
|
+
console.log(` ${d.host} ×${d.count} ${why}, last ${formatDateTime(d.last_seen)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (!enforced) console.error("(note: this site's target does not enforce egress yet — the policy is recorded for when it does)");
|
|
28
|
+
}
|
|
29
|
+
const listCommand = defineCommand({
|
|
30
|
+
meta: {
|
|
31
|
+
name: "list",
|
|
32
|
+
description: "List the project's allowed outbound hosts"
|
|
33
|
+
},
|
|
34
|
+
args: { ...projectDirArgs },
|
|
35
|
+
run: reportErrors("egress list", async ({ args }) => {
|
|
36
|
+
const { slug } = resolveProject(args);
|
|
37
|
+
printList(await apiRequest("GET", `/projects/${slug}/egress`), slug);
|
|
38
|
+
})
|
|
39
|
+
});
|
|
40
|
+
const allowCommand = defineCommand({
|
|
41
|
+
meta: {
|
|
42
|
+
name: "allow",
|
|
43
|
+
description: "Allow the worker to fetch a host (e.g. api.example.com or *.googleapis.com)"
|
|
44
|
+
},
|
|
45
|
+
args: {
|
|
46
|
+
host: {
|
|
47
|
+
type: "positional",
|
|
48
|
+
description: "Hostname to allow",
|
|
49
|
+
required: true
|
|
50
|
+
},
|
|
51
|
+
...projectDirArgs
|
|
52
|
+
},
|
|
53
|
+
run: reportErrors("egress allow", async ({ args }) => {
|
|
54
|
+
const { slug } = resolveProject(args);
|
|
55
|
+
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
56
|
+
const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: [...current.hosts, args.host] });
|
|
57
|
+
console.log(`Allowed ${args.host} for ${slug} — running workers pick it up now.\n`);
|
|
58
|
+
printList(updated, slug);
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
const removeCommand = defineCommand({
|
|
62
|
+
meta: {
|
|
63
|
+
name: "remove",
|
|
64
|
+
description: "Remove a host from the allowlist"
|
|
65
|
+
},
|
|
66
|
+
args: {
|
|
67
|
+
host: {
|
|
68
|
+
type: "positional",
|
|
69
|
+
description: "Hostname to remove",
|
|
70
|
+
required: true
|
|
71
|
+
},
|
|
72
|
+
...projectDirArgs
|
|
73
|
+
},
|
|
74
|
+
run: reportErrors("egress remove", async ({ args }) => {
|
|
75
|
+
const { slug } = resolveProject(args);
|
|
76
|
+
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
77
|
+
const wanted = args.host.trim().toLowerCase();
|
|
78
|
+
if (!current.hosts.includes(wanted)) {
|
|
79
|
+
console.error(`${wanted} is not on the allowlist for ${slug} (see \`canopy egress list\`).`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: current.hosts.filter((h) => h !== wanted) });
|
|
83
|
+
console.log(`Removed ${wanted} from ${slug} — running workers stop reaching it now.\n`);
|
|
84
|
+
printList(updated, slug);
|
|
85
|
+
})
|
|
86
|
+
});
|
|
87
|
+
/** The plain-language risk summary shown before open mode is enabled (docs/25 §2). */
|
|
88
|
+
function openModeRiskSummary(slug, clientAccess) {
|
|
89
|
+
const lines = [
|
|
90
|
+
`Open egress mode lets ${slug}'s worker contact ANY public host:`,
|
|
91
|
+
"",
|
|
92
|
+
" - If the URLs it fetches come from user input, users control where it connects.",
|
|
93
|
+
" - Abusive outbound traffic can get the project rate-limited or paused.",
|
|
94
|
+
" - A built-in blocklist (private IPs, localhost, cloud metadata) and an",
|
|
95
|
+
" outbound rate limit still apply, and every visited host is logged",
|
|
96
|
+
" (`canopy egress log`)."
|
|
97
|
+
];
|
|
98
|
+
if (clientAccess) lines.push("", " ! This project has client access enabled — untrusted visitors can drive", " what the worker fetches.");
|
|
99
|
+
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.");
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
const modeCommand = defineCommand({
|
|
103
|
+
meta: {
|
|
104
|
+
name: "mode",
|
|
105
|
+
description: "Show or switch the egress mode (allowlist | open)"
|
|
106
|
+
},
|
|
107
|
+
args: {
|
|
108
|
+
mode: {
|
|
109
|
+
type: "positional",
|
|
110
|
+
description: "allowlist | open (omit to show)",
|
|
111
|
+
required: false
|
|
112
|
+
},
|
|
113
|
+
"accept-risk": {
|
|
114
|
+
type: "boolean",
|
|
115
|
+
description: "Acknowledge the open-mode risk summary without an interactive prompt"
|
|
116
|
+
},
|
|
117
|
+
...projectDirArgs
|
|
118
|
+
},
|
|
119
|
+
run: reportErrors("egress mode", async ({ args }) => {
|
|
120
|
+
const { slug } = resolveProject(args);
|
|
121
|
+
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
122
|
+
const wanted = args.mode?.trim().toLowerCase();
|
|
123
|
+
if (!wanted) {
|
|
124
|
+
console.log(current.mode);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (wanted !== "allowlist" && wanted !== "open") {
|
|
128
|
+
console.error(`unknown mode "${wanted}" — expected allowlist or open.`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
if (wanted === current.mode) {
|
|
132
|
+
console.log(`${slug} is already in ${wanted} mode.`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (wanted === "open") {
|
|
136
|
+
const access = await apiRequest("GET", `/projects/${slug}/client-password`).catch(() => ({ enabled: false }));
|
|
137
|
+
console.log(openModeRiskSummary(slug, access.enabled));
|
|
138
|
+
console.log("");
|
|
139
|
+
if (!args["accept-risk"]) {
|
|
140
|
+
requireTty("pass --accept-risk to enable open mode without confirmation.");
|
|
141
|
+
if (unwrap(await p.text({ message: `Type the project slug ("${slug}") to enable open mode.` })).trim() !== slug) {
|
|
142
|
+
p.cancel("Aborted — egress mode unchanged.");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const updated = await withContext(apiRequest("PUT", `/projects/${slug}/egress`, { mode: wanted }), {
|
|
148
|
+
action: `switch ${slug} to ${wanted} egress mode`,
|
|
149
|
+
fixes: { owner_only: "Only the project owner can switch egress to open mode — ask them to run this command or flip it in the dashboard." }
|
|
150
|
+
});
|
|
151
|
+
if (wanted === "open") {
|
|
152
|
+
console.log(`Open egress mode is ON for ${slug} — running workers pick it up now.`);
|
|
153
|
+
console.log(`Switch back anytime with \`canopy egress mode allowlist\`.`);
|
|
154
|
+
} else {
|
|
155
|
+
console.log(`Allowlist mode restored for ${slug} — only listed hosts are reachable.\n`);
|
|
156
|
+
printList(updated, slug);
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
const logCommand = defineCommand({
|
|
161
|
+
meta: {
|
|
162
|
+
name: "log",
|
|
163
|
+
description: "Show the outbound traffic log — hosts the worker actually fetched"
|
|
164
|
+
},
|
|
165
|
+
args: { ...projectDirArgs },
|
|
166
|
+
run: reportErrors("egress log", async ({ args }) => {
|
|
167
|
+
const { slug } = resolveProject(args);
|
|
168
|
+
const { visits } = await apiRequest("GET", `/projects/${slug}/egress/log`);
|
|
169
|
+
if (visits.length === 0) {
|
|
170
|
+
console.log(`No outbound traffic recorded for ${slug} yet.`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const v of visits) console.log(`${v.host} ×${v.count} last ${formatDateTime(v.last_seen)}`);
|
|
174
|
+
})
|
|
175
|
+
});
|
|
176
|
+
/** The note `all` prints: what routing every request costs. */
|
|
177
|
+
const FIXED_IP_ALL_NOTE = [
|
|
178
|
+
"Every outbound request now leaves from the fixed addresses. That means:",
|
|
179
|
+
" - each request takes a slower route (an extra hop, and a cold start after idle),",
|
|
180
|
+
" - it all counts toward the daily fixed-address transfer quota,",
|
|
181
|
+
" - and it leaves from addresses shared with other projects.",
|
|
182
|
+
"Listing only the hosts that need a fixed address (`canopy egress fixed-ip add <host>`)",
|
|
183
|
+
"is usually the better choice."
|
|
184
|
+
].join("\n");
|
|
185
|
+
/** `canopy egress fixed-ip` with no argument: mode, hosts, addresses, flags. */
|
|
186
|
+
function renderFixedIp(view, slug, egressMode) {
|
|
187
|
+
const lines = [];
|
|
188
|
+
const addresses = view.addresses.join(", ");
|
|
189
|
+
if (!view.optional) {
|
|
190
|
+
lines.push(`Every outbound request of ${slug} leaves from the fixed addresses: ${addresses}`);
|
|
191
|
+
lines.push("Ask partners to allow both. There is nothing to set on this site.");
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
194
|
+
if (view.mode === "off") {
|
|
195
|
+
lines.push(`Fixed outbound addresses are off for ${slug} — requests leave normally.`);
|
|
196
|
+
lines.push(`Route a partner's host through ${addresses} with \`canopy egress fixed-ip add <host>\`.`);
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
if (view.mode === "all") lines.push(`Every outbound request of ${slug} leaves from: ${addresses}`);
|
|
200
|
+
else {
|
|
201
|
+
lines.push(`Requests of ${slug} to these hosts leave from ${addresses}:`);
|
|
202
|
+
for (const host of view.hosts) {
|
|
203
|
+
const flag = view.notAllowed.includes(host) ? " (not allowed — requests are denied)" : "";
|
|
204
|
+
lines.push(` ${host}${flag}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (view.notAllowed.length > 0 && egressMode === "allowlist") lines.push(`Routing doesn't allow a host: add it with \`canopy egress allow <host>\` to reach it.`);
|
|
208
|
+
return lines.join("\n");
|
|
209
|
+
}
|
|
210
|
+
/** The PUT body `add` sends; from off it starts a fresh list with just this host. */
|
|
211
|
+
function fixedIpAddBody(view, host) {
|
|
212
|
+
if (view.mode === "off") return {
|
|
213
|
+
mode: "hosts",
|
|
214
|
+
hosts: [host]
|
|
215
|
+
};
|
|
216
|
+
return { hosts: [...view.hosts, host] };
|
|
217
|
+
}
|
|
218
|
+
/** The PUT body `remove` sends, or null when the host isn't routed. */
|
|
219
|
+
function fixedIpRemoveBody(view, host) {
|
|
220
|
+
const wanted = host.trim().toLowerCase();
|
|
221
|
+
if (!view.hosts.includes(wanted)) return null;
|
|
222
|
+
const hosts = view.hosts.filter((h) => h !== wanted);
|
|
223
|
+
return view.mode === "hosts" && hosts.length === 0 ? {
|
|
224
|
+
mode: "off",
|
|
225
|
+
hosts
|
|
226
|
+
} : { hosts };
|
|
227
|
+
}
|
|
228
|
+
const FIXED_IP_FIXES = {
|
|
229
|
+
owner_only: "Only the project owner can turn on fixed outbound addresses or route all traffic — ask them to run this command or turn it on in the dashboard.",
|
|
230
|
+
fixed_ip_not_optional: "Every request on this site already leaves from the fixed addresses — `canopy egress fixed-ip` shows them.",
|
|
231
|
+
not_supported_on_target: "This site has no fixed outbound addresses: it runs no project backends to send from them."
|
|
232
|
+
};
|
|
233
|
+
/** The current setting, or a clear exit where the site offers none. */
|
|
234
|
+
async function loadFixedIp(slug) {
|
|
235
|
+
const egress = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
236
|
+
if (!egress.fixedIp) {
|
|
237
|
+
console.error(egress.fixedIp === void 0 ? "This site's API predates fixed outbound addresses." : "This site has no fixed outbound addresses (it runs no project backends).");
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
egress,
|
|
242
|
+
view: egress.fixedIp
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function putFixedIp(slug, fixedIp, action) {
|
|
246
|
+
return withContext(apiRequest("PUT", `/projects/${slug}/egress`, { fixedIp }), {
|
|
247
|
+
action,
|
|
248
|
+
fixes: FIXED_IP_FIXES
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function requireOptional(view, slug) {
|
|
252
|
+
if (view.optional) return;
|
|
253
|
+
console.log(renderFixedIp(view, slug, "allowlist"));
|
|
254
|
+
process.exit(0);
|
|
255
|
+
}
|
|
256
|
+
let fixedIpSubcommandRan = false;
|
|
257
|
+
const fixedIpAddCommand = defineCommand({
|
|
258
|
+
meta: {
|
|
259
|
+
name: "add",
|
|
260
|
+
description: "Route a host through the fixed addresses (turns the setting on)"
|
|
261
|
+
},
|
|
262
|
+
args: {
|
|
263
|
+
host: {
|
|
264
|
+
type: "positional",
|
|
265
|
+
description: "Hostname (api.partner.com or *.partner.com)",
|
|
266
|
+
required: true
|
|
267
|
+
},
|
|
268
|
+
allow: {
|
|
269
|
+
type: "boolean",
|
|
270
|
+
description: "Also add the host to the allowlist when it isn't on it"
|
|
271
|
+
},
|
|
272
|
+
...projectDirArgs
|
|
273
|
+
},
|
|
274
|
+
run: reportErrors("egress fixed-ip add", async ({ args }) => {
|
|
275
|
+
fixedIpSubcommandRan = true;
|
|
276
|
+
const { slug } = resolveProject(args);
|
|
277
|
+
const { egress, view } = await loadFixedIp(slug);
|
|
278
|
+
requireOptional(view, slug);
|
|
279
|
+
const updated = await putFixedIp(slug, fixedIpAddBody(view, args.host), `route ${args.host} through the fixed addresses for ${slug}`);
|
|
280
|
+
const fixed = updated.fixedIp;
|
|
281
|
+
console.log(`Requests of ${slug} to ${args.host} now leave from the fixed addresses.`);
|
|
282
|
+
console.log(`Ask the partner to allow both: ${fixed.addresses.join(", ")}\n`);
|
|
283
|
+
if (fixed.mode === "all") console.log("(All traffic is routed right now, so this host's requests already were.)\n");
|
|
284
|
+
const host = fixed.hosts.find((h) => h === args.host.trim().toLowerCase()) ?? args.host;
|
|
285
|
+
if (egress.mode === "allowlist" && fixed.notAllowed.includes(host)) {
|
|
286
|
+
let allow = Boolean(args.allow);
|
|
287
|
+
if (!allow && process.stdin.isTTY) allow = unwrap(await p.confirm({
|
|
288
|
+
message: `${host} isn't on the allowlist, so requests to it are denied. Allow it now?`,
|
|
289
|
+
initialValue: true
|
|
290
|
+
}));
|
|
291
|
+
if (allow) {
|
|
292
|
+
const allowed = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: [...egress.hosts, host] });
|
|
293
|
+
console.log(`Allowed ${host} too.\n`);
|
|
294
|
+
console.log(renderFixedIp(allowed.fixedIp, slug, allowed.mode));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
console.log(renderFixedIp(fixed, slug, updated.mode));
|
|
299
|
+
})
|
|
300
|
+
});
|
|
301
|
+
const fixedIpRemoveCommand = defineCommand({
|
|
302
|
+
meta: {
|
|
303
|
+
name: "remove",
|
|
304
|
+
description: "Stop routing a host through the fixed addresses"
|
|
305
|
+
},
|
|
306
|
+
args: {
|
|
307
|
+
host: {
|
|
308
|
+
type: "positional",
|
|
309
|
+
description: "Hostname to stop routing",
|
|
310
|
+
required: true
|
|
311
|
+
},
|
|
312
|
+
...projectDirArgs
|
|
313
|
+
},
|
|
314
|
+
run: reportErrors("egress fixed-ip remove", async ({ args }) => {
|
|
315
|
+
fixedIpSubcommandRan = true;
|
|
316
|
+
const { slug } = resolveProject(args);
|
|
317
|
+
const { view } = await loadFixedIp(slug);
|
|
318
|
+
requireOptional(view, slug);
|
|
319
|
+
const body = fixedIpRemoveBody(view, args.host);
|
|
320
|
+
if (!body) {
|
|
321
|
+
console.error(`${args.host.trim().toLowerCase()} isn't routed through the fixed addresses for ${slug} (see \`canopy egress fixed-ip\`).`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
const updated = await putFixedIp(slug, body, `stop routing ${args.host} for ${slug}`);
|
|
325
|
+
console.log(body.mode === "off" ? `That was the last routed host — fixed outbound addresses are off for ${slug}.` : `Requests of ${slug} to ${args.host} leave normally again.\n`);
|
|
326
|
+
if (body.mode !== "off") console.log(renderFixedIp(updated.fixedIp, slug, updated.mode));
|
|
327
|
+
})
|
|
328
|
+
});
|
|
329
|
+
const fixedIpAllCommand = defineCommand({
|
|
330
|
+
meta: {
|
|
331
|
+
name: "all",
|
|
332
|
+
description: "Send every outbound request from the fixed addresses"
|
|
333
|
+
},
|
|
334
|
+
args: { ...projectDirArgs },
|
|
335
|
+
run: reportErrors("egress fixed-ip all", async ({ args }) => {
|
|
336
|
+
fixedIpSubcommandRan = true;
|
|
337
|
+
const { slug } = resolveProject(args);
|
|
338
|
+
const { view } = await loadFixedIp(slug);
|
|
339
|
+
requireOptional(view, slug);
|
|
340
|
+
console.log(`${FIXED_IP_ALL_NOTE}\n`);
|
|
341
|
+
const updated = await putFixedIp(slug, { mode: "all" }, `route all of ${slug}'s requests through the fixed addresses`);
|
|
342
|
+
console.log(renderFixedIp(updated.fixedIp, slug, updated.mode));
|
|
343
|
+
})
|
|
344
|
+
});
|
|
345
|
+
const fixedIpOffCommand = defineCommand({
|
|
346
|
+
meta: {
|
|
347
|
+
name: "off",
|
|
348
|
+
description: "Turn fixed outbound addresses off — requests leave normally"
|
|
349
|
+
},
|
|
350
|
+
args: { ...projectDirArgs },
|
|
351
|
+
run: reportErrors("egress fixed-ip off", async ({ args }) => {
|
|
352
|
+
fixedIpSubcommandRan = true;
|
|
353
|
+
const { slug } = resolveProject(args);
|
|
354
|
+
const { view } = await loadFixedIp(slug);
|
|
355
|
+
requireOptional(view, slug);
|
|
356
|
+
if (view.mode === "off") {
|
|
357
|
+
console.log(`Fixed outbound addresses are already off for ${slug}.`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
await putFixedIp(slug, { mode: "off" }, `turn fixed outbound addresses off for ${slug}`);
|
|
361
|
+
console.log(`Fixed outbound addresses are off for ${slug} — requests leave normally now.`);
|
|
362
|
+
})
|
|
363
|
+
});
|
|
364
|
+
const showFixedIp = reportErrors("egress fixed-ip", async ({ args }) => {
|
|
365
|
+
const { slug } = resolveProject(args);
|
|
366
|
+
const { egress, view } = await loadFixedIp(slug);
|
|
367
|
+
console.log(renderFixedIp(view, slug, egress.mode));
|
|
368
|
+
});
|
|
369
|
+
const egressCommand = defineCommand({
|
|
370
|
+
meta: {
|
|
371
|
+
name: "egress",
|
|
372
|
+
description: "Manage which outbound hosts the project's worker may fetch"
|
|
373
|
+
},
|
|
374
|
+
subCommands: {
|
|
375
|
+
list: listCommand,
|
|
376
|
+
allow: allowCommand,
|
|
377
|
+
remove: removeCommand,
|
|
378
|
+
mode: modeCommand,
|
|
379
|
+
log: logCommand,
|
|
380
|
+
"fixed-ip": defineCommand({
|
|
381
|
+
meta: {
|
|
382
|
+
name: "fixed-ip",
|
|
383
|
+
description: "Show or change which requests leave from Canopy's fixed outbound addresses"
|
|
384
|
+
},
|
|
385
|
+
args: { ...projectDirArgs },
|
|
386
|
+
subCommands: {
|
|
387
|
+
add: fixedIpAddCommand,
|
|
388
|
+
remove: fixedIpRemoveCommand,
|
|
389
|
+
all: fixedIpAllCommand,
|
|
390
|
+
off: fixedIpOffCommand
|
|
391
|
+
},
|
|
392
|
+
run: (context) => fixedIpSubcommandRan ? void 0 : showFixedIp(context)
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
//#endregion
|
|
397
|
+
export { egressCommand };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-SWgTmctH.mjs";
|
|
2
2
|
import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
3
|
-
import { a as apiRequest, c as credentials, i as apiDownload, o as apiUpload, s as commonHeaders } from "./api-
|
|
4
|
-
import { r as reportErrors, t as formatDateTime } from "./ui-
|
|
3
|
+
import { a as apiRequest, c as credentials, i as apiDownload, o as apiUpload, s as commonHeaders } from "./api-BHzCvDcY.mjs";
|
|
4
|
+
import { r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { basename, extname, join } from "node:path";
|
|
7
7
|
import { readFile, writeFile } from "node:fs/promises";
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as version } from "./package-
|
|
2
|
+
import { t as version } from "./package-SWgTmctH.mjs";
|
|
3
3
|
import { r as siteHeaderLine, t as setInvocationProfile } from "./site-DM2J10CB.mjs";
|
|
4
4
|
import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
@@ -63,36 +63,36 @@ const main = defineCommand({
|
|
|
63
63
|
description: "canopy — create and deploy vibecoded projects"
|
|
64
64
|
},
|
|
65
65
|
subCommands: {
|
|
66
|
-
login: () => import("./login-
|
|
67
|
-
whoami: () => import("./login-
|
|
68
|
-
logout: () => import("./login-
|
|
69
|
-
create: () => import("./projects-
|
|
70
|
-
init: () => import("./projects-
|
|
71
|
-
projects: () => import("./projects-
|
|
72
|
-
delete: () => import("./projects-
|
|
73
|
-
dev: () => import("./dev-
|
|
74
|
-
deploy: () => import("./deploy-
|
|
75
|
-
promote: () => import("./deploy-
|
|
76
|
-
staging: () => import("./staging-
|
|
77
|
-
rollback: () => import("./deploy-
|
|
78
|
-
download: () => import("./deploy-
|
|
79
|
-
token: () => import("./token-
|
|
80
|
-
egress: () => import("./egress-
|
|
81
|
-
secrets: () => import("./secrets-
|
|
82
|
-
schedule: () => import("./schedule-
|
|
83
|
-
feedback: () => import("./feedback-
|
|
84
|
-
db: () => import("./db-
|
|
85
|
-
usage: () => import("./usage-
|
|
86
|
-
ai: () => import("./ai-
|
|
87
|
-
open: () => import("./open-
|
|
88
|
-
browse: () => import("./browse-
|
|
89
|
-
docs: () => import("./docs-
|
|
90
|
-
skill: () => import("./skill-
|
|
91
|
-
upgrade: () => import("./upgrade-
|
|
92
|
-
logs: () => import("./logs-
|
|
93
|
-
mcp: () => import("./mcp-
|
|
94
|
-
repo: () => import("./repo-
|
|
95
|
-
template: () => import("./project-template-
|
|
66
|
+
login: () => import("./login-CKNFCp7c.mjs").then((m) => m.loginCommand),
|
|
67
|
+
whoami: () => import("./login-CKNFCp7c.mjs").then((m) => m.whoamiCommand),
|
|
68
|
+
logout: () => import("./login-CKNFCp7c.mjs").then((m) => m.logoutCommand),
|
|
69
|
+
create: () => import("./projects-naWQ89Qj.mjs").then((m) => m.createCommand),
|
|
70
|
+
init: () => import("./projects-naWQ89Qj.mjs").then((m) => m.initCommand),
|
|
71
|
+
projects: () => import("./projects-naWQ89Qj.mjs").then((m) => m.projectsCommand),
|
|
72
|
+
delete: () => import("./projects-naWQ89Qj.mjs").then((m) => m.deleteCommand),
|
|
73
|
+
dev: () => import("./dev-D38FQ2LD.mjs").then((m) => m.devCommand),
|
|
74
|
+
deploy: () => import("./deploy-6rYq1Iq8.mjs").then((m) => m.deployCommand),
|
|
75
|
+
promote: () => import("./deploy-6rYq1Iq8.mjs").then((m) => m.promoteCommand),
|
|
76
|
+
staging: () => import("./staging-CYMTZyyM.mjs").then((m) => m.stagingCommand),
|
|
77
|
+
rollback: () => import("./deploy-6rYq1Iq8.mjs").then((m) => m.rollbackCommand),
|
|
78
|
+
download: () => import("./deploy-6rYq1Iq8.mjs").then((m) => m.downloadCommand),
|
|
79
|
+
token: () => import("./token-CCpJ1ES4.mjs").then((m) => m.tokenCommand),
|
|
80
|
+
egress: () => import("./egress-CCpOsYpS.mjs").then((m) => m.egressCommand),
|
|
81
|
+
secrets: () => import("./secrets-COwRvCev.mjs").then((m) => m.secretsCommand),
|
|
82
|
+
schedule: () => import("./schedule-DjHcaxyt.mjs").then((m) => m.scheduleCommand),
|
|
83
|
+
feedback: () => import("./feedback-CdjtPumG.mjs").then((m) => m.feedbackCommand),
|
|
84
|
+
db: () => import("./db-5LfBG6N7.mjs").then((m) => m.dbCommand),
|
|
85
|
+
usage: () => import("./usage-BSPnsMXd.mjs").then((m) => m.usageCommand),
|
|
86
|
+
ai: () => import("./ai-PXXMv7kO.mjs").then((m) => m.aiCommand),
|
|
87
|
+
open: () => import("./open-BPYozPQo.mjs").then((m) => m.openCommand),
|
|
88
|
+
browse: () => import("./browse-DRz1DowL.mjs").then((m) => m.browseCommand),
|
|
89
|
+
docs: () => import("./docs-B9xGnLSR.mjs").then((m) => m.docsCommand),
|
|
90
|
+
skill: () => import("./skill-DSYQA4Ls.mjs").then((m) => m.skillCommand),
|
|
91
|
+
upgrade: () => import("./upgrade-B_tLCpc4.mjs").then((m) => m.upgradeCommand),
|
|
92
|
+
logs: () => import("./logs-D3WakFME.mjs").then((m) => m.logsCommand),
|
|
93
|
+
mcp: () => import("./mcp-CoVS5Mj3.mjs").then((m) => m.mcpCommand),
|
|
94
|
+
repo: () => import("./repo-Un_p617X.mjs").then((m) => m.repoCommand),
|
|
95
|
+
template: () => import("./project-template-CMgD5hnV.mjs").then((m) => m.templateCommand)
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
98
|
const quiet = process.argv[2] === "mcp" && process.argv[3] === "serve" || process.argv[2] === "repo" && process.argv[3] === "credential";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { r as apiDataRequest } from "./api-
|
|
1
|
+
import { r as apiDataRequest } from "./api-BHzCvDcY.mjs";
|
|
2
2
|
import { a as realtimeRouter, c as META_COLUMNS, l as blobRouter, n as WORKER_ATTEST_HEADER, o as dbRouter, s as hasPrivateNames } from "./src-RUf3Gfuh.mjs";
|
|
3
|
-
import { a as rewriteApiPath } from "./dev-
|
|
3
|
+
import { a as rewriteApiPath } from "./dev-D_gyIeUq.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join, relative, sep } from "node:path";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { _ as writeConfigFile, f as clearAllProfileTokens, g as upsertProfileAuth, h as setActiveProfile, m as readConfigFile, n as siteConfig, p as listBakedProfiles, v as getBakedProfile, y as isProfileId } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { n as agentHeaders } from "./api-
|
|
3
|
-
import { n as openBrowser, r as reportErrors } from "./ui-
|
|
2
|
+
import { n as agentHeaders } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { n as openBrowser, r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
4
4
|
import { n as clearRepoTokenCache } from "./repo-credential-CNDRM-tG.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { hostname } from "node:os";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext } from "./api-
|
|
3
|
-
import { r as reportErrors } from "./ui-
|
|
2
|
+
import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
//#region src/logs.ts
|
|
6
6
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-SWgTmctH.mjs";
|
|
2
2
|
import { d as tryResolveProject, l as stagingUrl, n as siteConfig, s as projectUrl } from "./site-DM2J10CB.mjs";
|
|
3
|
-
import { a as apiRequest, t as ApiError } from "./api-
|
|
3
|
+
import { a as apiRequest, t as ApiError } from "./api-BHzCvDcY.mjs";
|
|
4
4
|
import { t as liveDevServer } from "./dev-server-file-Dk9pk4AR.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { r as apiDataRequest } from "./api-
|
|
2
|
+
import { r as apiDataRequest } from "./api-BHzCvDcY.mjs";
|
|
3
3
|
import { readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
//#region src/migrations.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, l as stagingUrl, s as projectUrl } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { n as openBrowser, r as reportErrors } from "./ui-
|
|
2
|
+
import { n as openBrowser, r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/open.ts
|
|
5
5
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext } from "./api-
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-
|
|
2
|
+
import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
/** Plain words for the API's template errors. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { n as siteConfig, s as projectUrl, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext } from "./api-
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-
|
|
4
|
-
import { i as nextSteps, n as describeCopy, r as fetchTemplate, t as TEMPLATE_FIXES } from "./project-template-
|
|
5
|
-
import { i as offerRepoOnCreate, n as cloneProjectRepo } from "./repo-
|
|
2
|
+
import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
4
|
+
import { i as nextSteps, n as describeCopy, r as fetchTemplate, t as TEMPLATE_FIXES } from "./project-template-DACdMdpn.mjs";
|
|
5
|
+
import { i as offerRepoOnCreate, n as cloneProjectRepo } from "./repo-BCua4L-I.mjs";
|
|
6
6
|
import { defineCommand } from "citty";
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { join, relative, resolve, sep } from "node:path";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext, n as agentHeaders } from "./api-
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { a as apiRequest, l as withContext, n as agentHeaders } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
|
|
4
4
|
import { a as parseCredentialInput, i as helperCommand, o as runCredentialHelper, r as fileTokenCache, t as RepoCredentialError } from "./repo-credential-CNDRM-tG.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { existsSync, realpathSync } from "node:fs";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext } from "./api-
|
|
3
|
-
import { r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
//#region src/schedule.ts
|
|
6
6
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext } from "./api-
|
|
3
|
-
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
//#region src/secrets.ts
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
2
|
-
import { r as reportErrors } from "./ui-
|
|
1
|
+
import { t as version } from "./package-SWgTmctH.mjs";
|
|
2
|
+
import { r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as resolveProject, l as stagingUrl, o as projectFlagArgs, s as projectUrl } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext } from "./api-
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
//#region src/staging.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest } from "./api-
|
|
3
|
-
import { r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { a as apiRequest } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { r as reportErrors, t as formatDateTime } from "./ui-SBUdC9rX.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
const tokenCommand = defineCommand({
|
|
6
6
|
meta: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
|
|
2
|
-
import { r as reportErrors } from "./ui-
|
|
2
|
+
import { r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -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-DSYQA4Ls.mjs");
|
|
116
116
|
const { homedir } = await import("node:os");
|
|
117
117
|
const refreshGlobal = skillInstalled(homedir());
|
|
118
118
|
const refreshRepo = process.cwd() !== homedir() && skillInstalled(process.cwd());
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext } from "./api-
|
|
3
|
-
import { r as reportErrors } from "./ui-
|
|
2
|
+
import { a as apiRequest, l as withContext } from "./api-BHzCvDcY.mjs";
|
|
3
|
+
import { r as reportErrors } from "./ui-SBUdC9rX.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
//#region src/usage.ts
|
|
6
6
|
/**
|
|
@@ -60,6 +60,13 @@ function renderUsage(slug, usage) {
|
|
|
60
60
|
const deploys = usage.deploys.count;
|
|
61
61
|
const live = usage.deploys.latestVersion != null ? ` · v${usage.deploys.latestVersion} live` : "";
|
|
62
62
|
lines.push(` ${pad("deploys")}${deploys}${live}`);
|
|
63
|
+
const fixed = usage.fixedIp;
|
|
64
|
+
if (fixed && (fixed.mode !== "off" || fixed.bytesToday > 0)) {
|
|
65
|
+
lines.push("");
|
|
66
|
+
lines.push("Fixed outbound addresses");
|
|
67
|
+
const gb = (n) => `${Number((n / 1e9).toFixed(n < 1e8 ? 3 : 2))} GB`;
|
|
68
|
+
lines.push(` ${pad("today")}${gb(fixed.bytesToday)} of ${gb(fixed.dailyQuotaBytes)}`);
|
|
69
|
+
}
|
|
63
70
|
return lines.join("\n");
|
|
64
71
|
}
|
|
65
72
|
const usageCommand = defineCommand({
|
package/package.json
CHANGED
package/dist/dev-dJ88L73Y.mjs
DELETED
package/dist/egress-CzxRD6Tp.mjs
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
|
|
4
|
-
import { defineCommand } from "citty";
|
|
5
|
-
import * as p from "@clack/prompts";
|
|
6
|
-
//#region src/egress.ts
|
|
7
|
-
/**
|
|
8
|
-
* canopy egress — manage the project's outbound-host policy: the allowlist
|
|
9
|
-
* (docs/16-egress-allowlist.md) and the egress mode (docs/25-egress-modes.md).
|
|
10
|
-
* In allowlist mode, worker fetches to hosts outside the list are denied
|
|
11
|
-
* (`egress_denied`) on targets that enforce egress control; open mode allows
|
|
12
|
-
* any public hostname behind a built-in blocklist and an outbound rate limit.
|
|
13
|
-
* Policy lives on the platform, not in canopy.json, so a repo file can never
|
|
14
|
-
* widen egress silently on deploy.
|
|
15
|
-
*/
|
|
16
|
-
function printList({ mode, hosts, denied, enforced }, slug) {
|
|
17
|
-
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).`);
|
|
18
|
-
if (hosts.length === 0) console.log(`No extra egress hosts for ${slug} — the worker can reach platform hosts only.`);
|
|
19
|
-
else for (const host of hosts) console.log(host);
|
|
20
|
-
if (denied.length > 0) {
|
|
21
|
-
console.log(`\nRecently blocked (allow with \`canopy egress allow <host>\`):`);
|
|
22
|
-
for (const d of denied) {
|
|
23
|
-
const why = d.reason === "rate_limited" ? "rate limited" : "blocked";
|
|
24
|
-
console.log(` ${d.host} ×${d.count} ${why}, last ${formatDateTime(d.last_seen)}`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
if (!enforced) console.error("(note: this site's target does not enforce egress yet — the policy is recorded for when it does)");
|
|
28
|
-
}
|
|
29
|
-
const listCommand = defineCommand({
|
|
30
|
-
meta: {
|
|
31
|
-
name: "list",
|
|
32
|
-
description: "List the project's allowed outbound hosts"
|
|
33
|
-
},
|
|
34
|
-
args: { ...projectDirArgs },
|
|
35
|
-
run: reportErrors("egress list", async ({ args }) => {
|
|
36
|
-
const { slug } = resolveProject(args);
|
|
37
|
-
printList(await apiRequest("GET", `/projects/${slug}/egress`), slug);
|
|
38
|
-
})
|
|
39
|
-
});
|
|
40
|
-
const allowCommand = defineCommand({
|
|
41
|
-
meta: {
|
|
42
|
-
name: "allow",
|
|
43
|
-
description: "Allow the worker to fetch a host (e.g. api.example.com or *.googleapis.com)"
|
|
44
|
-
},
|
|
45
|
-
args: {
|
|
46
|
-
host: {
|
|
47
|
-
type: "positional",
|
|
48
|
-
description: "Hostname to allow",
|
|
49
|
-
required: true
|
|
50
|
-
},
|
|
51
|
-
...projectDirArgs
|
|
52
|
-
},
|
|
53
|
-
run: reportErrors("egress allow", async ({ args }) => {
|
|
54
|
-
const { slug } = resolveProject(args);
|
|
55
|
-
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
56
|
-
const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: [...current.hosts, args.host] });
|
|
57
|
-
console.log(`Allowed ${args.host} for ${slug} — running workers pick it up now.\n`);
|
|
58
|
-
printList(updated, slug);
|
|
59
|
-
})
|
|
60
|
-
});
|
|
61
|
-
const removeCommand = defineCommand({
|
|
62
|
-
meta: {
|
|
63
|
-
name: "remove",
|
|
64
|
-
description: "Remove a host from the allowlist"
|
|
65
|
-
},
|
|
66
|
-
args: {
|
|
67
|
-
host: {
|
|
68
|
-
type: "positional",
|
|
69
|
-
description: "Hostname to remove",
|
|
70
|
-
required: true
|
|
71
|
-
},
|
|
72
|
-
...projectDirArgs
|
|
73
|
-
},
|
|
74
|
-
run: reportErrors("egress remove", async ({ args }) => {
|
|
75
|
-
const { slug } = resolveProject(args);
|
|
76
|
-
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
77
|
-
const wanted = args.host.trim().toLowerCase();
|
|
78
|
-
if (!current.hosts.includes(wanted)) {
|
|
79
|
-
console.error(`${wanted} is not on the allowlist for ${slug} (see \`canopy egress list\`).`);
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
const updated = await apiRequest("PUT", `/projects/${slug}/egress`, { hosts: current.hosts.filter((h) => h !== wanted) });
|
|
83
|
-
console.log(`Removed ${wanted} from ${slug} — running workers stop reaching it now.\n`);
|
|
84
|
-
printList(updated, slug);
|
|
85
|
-
})
|
|
86
|
-
});
|
|
87
|
-
/** The plain-language risk summary shown before open mode is enabled (docs/25 §2). */
|
|
88
|
-
function openModeRiskSummary(slug, clientAccess) {
|
|
89
|
-
const lines = [
|
|
90
|
-
`Open egress mode lets ${slug}'s worker contact ANY public host:`,
|
|
91
|
-
"",
|
|
92
|
-
" - If the URLs it fetches come from user input, users control where it connects.",
|
|
93
|
-
" - Abusive outbound traffic can get the project rate-limited or paused.",
|
|
94
|
-
" - A built-in blocklist (private IPs, localhost, cloud metadata) and an",
|
|
95
|
-
" outbound rate limit still apply, and every visited host is logged",
|
|
96
|
-
" (`canopy egress log`)."
|
|
97
|
-
];
|
|
98
|
-
if (clientAccess) lines.push("", " ! This project has client access enabled — untrusted visitors can drive", " what the worker fetches.");
|
|
99
|
-
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.");
|
|
100
|
-
return lines.join("\n");
|
|
101
|
-
}
|
|
102
|
-
const egressCommand = defineCommand({
|
|
103
|
-
meta: {
|
|
104
|
-
name: "egress",
|
|
105
|
-
description: "Manage which outbound hosts the project's worker may fetch"
|
|
106
|
-
},
|
|
107
|
-
subCommands: {
|
|
108
|
-
list: listCommand,
|
|
109
|
-
allow: allowCommand,
|
|
110
|
-
remove: removeCommand,
|
|
111
|
-
mode: defineCommand({
|
|
112
|
-
meta: {
|
|
113
|
-
name: "mode",
|
|
114
|
-
description: "Show or switch the egress mode (allowlist | open)"
|
|
115
|
-
},
|
|
116
|
-
args: {
|
|
117
|
-
mode: {
|
|
118
|
-
type: "positional",
|
|
119
|
-
description: "allowlist | open (omit to show)",
|
|
120
|
-
required: false
|
|
121
|
-
},
|
|
122
|
-
"accept-risk": {
|
|
123
|
-
type: "boolean",
|
|
124
|
-
description: "Acknowledge the open-mode risk summary without an interactive prompt"
|
|
125
|
-
},
|
|
126
|
-
...projectDirArgs
|
|
127
|
-
},
|
|
128
|
-
run: reportErrors("egress mode", async ({ args }) => {
|
|
129
|
-
const { slug } = resolveProject(args);
|
|
130
|
-
const current = await apiRequest("GET", `/projects/${slug}/egress`);
|
|
131
|
-
const wanted = args.mode?.trim().toLowerCase();
|
|
132
|
-
if (!wanted) {
|
|
133
|
-
console.log(current.mode);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (wanted !== "allowlist" && wanted !== "open") {
|
|
137
|
-
console.error(`unknown mode "${wanted}" — expected allowlist or open.`);
|
|
138
|
-
process.exit(1);
|
|
139
|
-
}
|
|
140
|
-
if (wanted === current.mode) {
|
|
141
|
-
console.log(`${slug} is already in ${wanted} mode.`);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (wanted === "open") {
|
|
145
|
-
const access = await apiRequest("GET", `/projects/${slug}/client-password`).catch(() => ({ enabled: false }));
|
|
146
|
-
console.log(openModeRiskSummary(slug, access.enabled));
|
|
147
|
-
console.log("");
|
|
148
|
-
if (!args["accept-risk"]) {
|
|
149
|
-
requireTty("pass --accept-risk to enable open mode without confirmation.");
|
|
150
|
-
if (unwrap(await p.text({ message: `Type the project slug ("${slug}") to enable open mode.` })).trim() !== slug) {
|
|
151
|
-
p.cancel("Aborted — egress mode unchanged.");
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const updated = await withContext(apiRequest("PUT", `/projects/${slug}/egress`, { mode: wanted }), {
|
|
157
|
-
action: `switch ${slug} to ${wanted} egress mode`,
|
|
158
|
-
fixes: { owner_only: "Only the project owner can switch egress to open mode — ask them to run this command or flip it in the dashboard." }
|
|
159
|
-
});
|
|
160
|
-
if (wanted === "open") {
|
|
161
|
-
console.log(`Open egress mode is ON for ${slug} — running workers pick it up now.`);
|
|
162
|
-
console.log(`Switch back anytime with \`canopy egress mode allowlist\`.`);
|
|
163
|
-
} else {
|
|
164
|
-
console.log(`Allowlist mode restored for ${slug} — only listed hosts are reachable.\n`);
|
|
165
|
-
printList(updated, slug);
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
}),
|
|
169
|
-
log: defineCommand({
|
|
170
|
-
meta: {
|
|
171
|
-
name: "log",
|
|
172
|
-
description: "Show the outbound traffic log — hosts the worker actually fetched"
|
|
173
|
-
},
|
|
174
|
-
args: { ...projectDirArgs },
|
|
175
|
-
run: reportErrors("egress log", async ({ args }) => {
|
|
176
|
-
const { slug } = resolveProject(args);
|
|
177
|
-
const { visits } = await apiRequest("GET", `/projects/${slug}/egress/log`);
|
|
178
|
-
if (visits.length === 0) {
|
|
179
|
-
console.log(`No outbound traffic recorded for ${slug} yet.`);
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
for (const v of visits) console.log(`${v.host} ×${v.count} last ${formatDateTime(v.last_seen)}`);
|
|
183
|
-
})
|
|
184
|
-
})
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
//#endregion
|
|
188
|
-
export { egressCommand };
|
package/dist/repo-CmbakFVi.mjs
DELETED