@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.
Files changed (38) hide show
  1. package/dist/{ai-EyrXJC5z.mjs → ai-PXXMv7kO.mjs} +2 -2
  2. package/dist/{api-ArfD6-t5.mjs → api-BHzCvDcY.mjs} +12 -0
  3. package/dist/{browse-DEhnJAPT.mjs → browse-DRz1DowL.mjs} +1 -1
  4. package/dist/{db-DGrCgj-R.mjs → db-5LfBG6N7.mjs} +3 -3
  5. package/dist/{deploy-B7KItUnh.mjs → deploy-6rYq1Iq8.mjs} +3 -3
  6. package/dist/dev-D38FQ2LD.mjs +2 -0
  7. package/dist/{dev-03LA_Etx.mjs → dev-D_gyIeUq.mjs} +5 -5
  8. package/dist/docs/platform-egress.md +69 -12
  9. package/dist/docs/troubleshooting.md +16 -0
  10. package/dist/{docs-B0-IEPjd.mjs → docs-B9xGnLSR.mjs} +3 -3
  11. package/dist/egress-CCpOsYpS.mjs +397 -0
  12. package/dist/{feedback-BsJ_6ke2.mjs → feedback-CdjtPumG.mjs} +3 -3
  13. package/dist/index.mjs +31 -31
  14. package/dist/{local-CkS1XhRu.mjs → local-Dshy0NsJ.mjs} +2 -2
  15. package/dist/{login-B2FJGIrD.mjs → login-CKNFCp7c.mjs} +2 -2
  16. package/dist/{logs-wSKh_FmL.mjs → logs-D3WakFME.mjs} +2 -2
  17. package/dist/{mcp-DtRIDtaL.mjs → mcp-CoVS5Mj3.mjs} +2 -2
  18. package/dist/{migrations-CG_O5Ghw.mjs → migrations-DDJ5JJqj.mjs} +1 -1
  19. package/dist/{open-COFMburt.mjs → open-BPYozPQo.mjs} +1 -1
  20. package/dist/{package-C1xCnyOB.mjs → package-SWgTmctH.mjs} +1 -1
  21. package/dist/project-template-CMgD5hnV.mjs +2 -0
  22. package/dist/{project-template-DtTfXyLI.mjs → project-template-DACdMdpn.mjs} +2 -2
  23. package/dist/{projects-B3Wi01lw.mjs → projects-naWQ89Qj.mjs} +4 -4
  24. package/dist/{repo-4x-CJwCp.mjs → repo-BCua4L-I.mjs} +2 -2
  25. package/dist/repo-Un_p617X.mjs +2 -0
  26. package/dist/{schedule-CbaDIJXr.mjs → schedule-DjHcaxyt.mjs} +2 -2
  27. package/dist/{secrets-CmY4dUHz.mjs → secrets-COwRvCev.mjs} +2 -2
  28. package/dist/{skill-AqZYYsE6.mjs → skill-DSYQA4Ls.mjs} +2 -2
  29. package/dist/{staging-ypR79MlD.mjs → staging-CYMTZyyM.mjs} +2 -2
  30. package/dist/{token-CDrHQ5Xb.mjs → token-CCpJ1ES4.mjs} +2 -2
  31. package/dist/{ui-CaHgvvkJ.mjs → ui-SBUdC9rX.mjs} +1 -1
  32. package/dist/{upgrade-DSjmifH_.mjs → upgrade-B_tLCpc4.mjs} +2 -2
  33. package/dist/{usage-CP8rBVZi.mjs → usage-BSPnsMXd.mjs} +9 -2
  34. package/package.json +1 -1
  35. package/dist/dev-dJ88L73Y.mjs +0 -2
  36. package/dist/egress-CzxRD6Tp.mjs +0 -188
  37. package/dist/project-template-DlzMx8Oj.mjs +0 -2
  38. package/dist/repo-CmbakFVi.mjs +0 -2
@@ -1,5 +1,5 @@
1
- import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
2
- import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
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-ArfD6-t5.mjs";
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-ArfD6-t5.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CaHgvvkJ.mjs";
4
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-CG_O5Ghw.mjs";
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-ArfD6-t5.mjs";
3
- import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
4
- import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-CG_O5Ghw.mjs";
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";
@@ -0,0 +1,2 @@
1
+ import { n as devCommand } from "./dev-D_gyIeUq.mjs";
2
+ export { devCommand };
@@ -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-ArfD6-t5.mjs";
3
- import { r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
4
- import { r as readMigrationFiles } from "./migrations-CG_O5Ghw.mjs";
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-CkS1XhRu.mjs");
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-CkS1XhRu.mjs");
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
- ## Allowlisting Canopy on the other side
115
+ ## Fixed outbound addresses
116
116
 
117
117
  Some external services only accept calls from IP addresses you register with
118
- them first — a partner API, a corporate database, a payment provider's
119
- allowlist. Projects hosted on **Google Cloud** (the `gcp-pilot` site,
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 your allowlist. These
130
- addresses are shared by every project on that site, so registering them lets
131
- any Canopy project on Google Cloud reach the service — the other service's own
132
- authentication (API keys, tokens) still decides who gets in.
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. Projects hosted on Cloudflare have **no** fixed
136
- outbound address (requests leave from Cloudflare's shared edge), so if a
137
- service you depend on insists on IP allowlisting, run that project on the
138
- Google Cloud site.
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-C1xCnyOB.mjs";
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-ArfD6-t5.mjs";
5
- import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
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-C1xCnyOB.mjs";
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-ArfD6-t5.mjs";
4
- import { r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
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-C1xCnyOB.mjs";
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-B2FJGIrD.mjs").then((m) => m.loginCommand),
67
- whoami: () => import("./login-B2FJGIrD.mjs").then((m) => m.whoamiCommand),
68
- logout: () => import("./login-B2FJGIrD.mjs").then((m) => m.logoutCommand),
69
- create: () => import("./projects-B3Wi01lw.mjs").then((m) => m.createCommand),
70
- init: () => import("./projects-B3Wi01lw.mjs").then((m) => m.initCommand),
71
- projects: () => import("./projects-B3Wi01lw.mjs").then((m) => m.projectsCommand),
72
- delete: () => import("./projects-B3Wi01lw.mjs").then((m) => m.deleteCommand),
73
- dev: () => import("./dev-dJ88L73Y.mjs").then((m) => m.devCommand),
74
- deploy: () => import("./deploy-B7KItUnh.mjs").then((m) => m.deployCommand),
75
- promote: () => import("./deploy-B7KItUnh.mjs").then((m) => m.promoteCommand),
76
- staging: () => import("./staging-ypR79MlD.mjs").then((m) => m.stagingCommand),
77
- rollback: () => import("./deploy-B7KItUnh.mjs").then((m) => m.rollbackCommand),
78
- download: () => import("./deploy-B7KItUnh.mjs").then((m) => m.downloadCommand),
79
- token: () => import("./token-CDrHQ5Xb.mjs").then((m) => m.tokenCommand),
80
- egress: () => import("./egress-CzxRD6Tp.mjs").then((m) => m.egressCommand),
81
- secrets: () => import("./secrets-CmY4dUHz.mjs").then((m) => m.secretsCommand),
82
- schedule: () => import("./schedule-CbaDIJXr.mjs").then((m) => m.scheduleCommand),
83
- feedback: () => import("./feedback-BsJ_6ke2.mjs").then((m) => m.feedbackCommand),
84
- db: () => import("./db-DGrCgj-R.mjs").then((m) => m.dbCommand),
85
- usage: () => import("./usage-CP8rBVZi.mjs").then((m) => m.usageCommand),
86
- ai: () => import("./ai-EyrXJC5z.mjs").then((m) => m.aiCommand),
87
- open: () => import("./open-COFMburt.mjs").then((m) => m.openCommand),
88
- browse: () => import("./browse-DEhnJAPT.mjs").then((m) => m.browseCommand),
89
- docs: () => import("./docs-B0-IEPjd.mjs").then((m) => m.docsCommand),
90
- skill: () => import("./skill-AqZYYsE6.mjs").then((m) => m.skillCommand),
91
- upgrade: () => import("./upgrade-DSjmifH_.mjs").then((m) => m.upgradeCommand),
92
- logs: () => import("./logs-wSKh_FmL.mjs").then((m) => m.logsCommand),
93
- mcp: () => import("./mcp-DtRIDtaL.mjs").then((m) => m.mcpCommand),
94
- repo: () => import("./repo-CmbakFVi.mjs").then((m) => m.repoCommand),
95
- template: () => import("./project-template-DlzMx8Oj.mjs").then((m) => m.templateCommand)
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-ArfD6-t5.mjs";
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-03LA_Etx.mjs";
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-ArfD6-t5.mjs";
3
- import { n as openBrowser, r as reportErrors } from "./ui-CaHgvvkJ.mjs";
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-ArfD6-t5.mjs";
3
- import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
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-C1xCnyOB.mjs";
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-ArfD6-t5.mjs";
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-ArfD6-t5.mjs";
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-CaHgvvkJ.mjs";
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,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.40";
2
+ var version = "0.7.41";
3
3
  //#endregion
4
4
  export { version as t };
@@ -0,0 +1,2 @@
1
+ import { a as templateCommand } from "./project-template-DACdMdpn.mjs";
2
+ export { templateCommand };
@@ -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-ArfD6-t5.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CaHgvvkJ.mjs";
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-ArfD6-t5.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CaHgvvkJ.mjs";
4
- import { i as nextSteps, n as describeCopy, r as fetchTemplate, t as TEMPLATE_FIXES } from "./project-template-DtTfXyLI.mjs";
5
- import { i as offerRepoOnCreate, n as cloneProjectRepo } from "./repo-4x-CJwCp.mjs";
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-ArfD6-t5.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
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";
@@ -0,0 +1,2 @@
1
+ import { a as repoCommand } from "./repo-BCua4L-I.mjs";
2
+ export { repoCommand };
@@ -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-ArfD6-t5.mjs";
3
- import { r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
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-ArfD6-t5.mjs";
3
- import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
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-C1xCnyOB.mjs";
2
- import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
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-ArfD6-t5.mjs";
3
- import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.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
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-ArfD6-t5.mjs";
3
- import { r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
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,4 +1,4 @@
1
- import { t as ApiError } from "./api-ArfD6-t5.mjs";
1
+ import { t as ApiError } from "./api-BHzCvDcY.mjs";
2
2
  import { spawn } from "node:child_process";
3
3
  import { cancel, isCancel } from "@clack/prompts";
4
4
  //#region src/ui.ts
@@ -1,5 +1,5 @@
1
1
  import { t as PACKAGE_NAME } from "./version-check-CcsA1QjI.mjs";
2
- import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
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-AqZYYsE6.mjs");
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-ArfD6-t5.mjs";
3
- import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.7.40",
3
+ "version": "0.7.41",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -1,2 +0,0 @@
1
- import { n as devCommand } from "./dev-03LA_Etx.mjs";
2
- export { devCommand };
@@ -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 };
@@ -1,2 +0,0 @@
1
- import { a as templateCommand } from "./project-template-DtTfXyLI.mjs";
2
- export { templateCommand };
@@ -1,2 +0,0 @@
1
- import { a as repoCommand } from "./repo-4x-CJwCp.mjs";
2
- export { repoCommand };