@mediamonks/canopy-cli 0.7.40 → 0.7.42
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-DpQ1nLka.mjs} +2 -2
- package/dist/{api-ArfD6-t5.mjs → api-Do9c8tZ_.mjs} +73 -3
- package/dist/{browse-DEhnJAPT.mjs → browse-Dp5HytqK.mjs} +2 -2
- package/dist/{db-DGrCgj-R.mjs → db-BNsX7sXo.mjs} +4 -4
- package/dist/{deploy-B7KItUnh.mjs → deploy-CXMLABvH.mjs} +8 -8
- package/dist/{dev-03LA_Etx.mjs → dev-CPR-9slK.mjs} +6 -6
- package/dist/dev-Dj3wFKHe.mjs +2 -0
- package/dist/docs/ci.md +19 -1
- package/dist/docs/cli-rename.md +51 -0
- package/dist/docs/git.md +5 -0
- package/dist/docs/index.json +11 -1
- package/dist/docs/platform-egress.md +69 -12
- package/dist/docs/rename.md +65 -0
- package/dist/docs/troubleshooting.md +16 -0
- package/dist/{docs-B0-IEPjd.mjs → docs-CkJGcCpR.mjs} +4 -4
- package/dist/egress-D6qyaaAn.mjs +397 -0
- package/dist/{feedback-BsJ_6ke2.mjs → feedback-DMgUUl0V.mjs} +4 -4
- package/dist/index.mjs +33 -32
- package/dist/{local-CkS1XhRu.mjs → local-w2wXfE1d.mjs} +2 -2
- package/dist/{login-B2FJGIrD.mjs → login-CeNvHLUv.mjs} +3 -3
- package/dist/{logs-wSKh_FmL.mjs → logs-BzgJfWNP.mjs} +3 -3
- package/dist/{mcp-DtRIDtaL.mjs → mcp-DFLaXiY_.mjs} +8 -6
- package/dist/{migrations-CG_O5Ghw.mjs → migrations-Bsu4quya.mjs} +2 -2
- package/dist/{open-COFMburt.mjs → open-D71Ms1nG.mjs} +2 -2
- package/dist/{package-C1xCnyOB.mjs → package-CVDpzGgA.mjs} +1 -1
- package/dist/{project-template-DtTfXyLI.mjs → project-template-BXfHpsI8.mjs} +3 -3
- package/dist/project-template-BbmSmraT.mjs +2 -0
- package/dist/{projects-B3Wi01lw.mjs → projects-BWTIk80a.mjs} +5 -5
- package/dist/rename-BdIFrqvG.mjs +81 -0
- package/dist/repo-JBKJqyoS.mjs +2 -0
- package/dist/{repo-4x-CJwCp.mjs → repo-LSOxnmUk.mjs} +5 -3
- package/dist/{schedule-CbaDIJXr.mjs → schedule-DpMIpCGC.mjs} +3 -3
- package/dist/{secrets-CmY4dUHz.mjs → secrets-BVx8L0Gj.mjs} +3 -3
- package/dist/{site-DM2J10CB.mjs → site-0i7wAJqg.mjs} +24 -1
- package/dist/{skill-AqZYYsE6.mjs → skill-D58yV6K8.mjs} +2 -2
- package/dist/{staging-ypR79MlD.mjs → staging-ynPlMiHW.mjs} +3 -3
- package/dist/{token-CDrHQ5Xb.mjs → token-CxiPrKn_.mjs} +3 -3
- package/dist/{ui-CaHgvvkJ.mjs → ui-CDUJq-X-.mjs} +1 -1
- package/dist/{upgrade-DSjmifH_.mjs → upgrade-Dll5rhOQ.mjs} +2 -2
- package/dist/{usage-CP8rBVZi.mjs → usage-BtPsb7Iu.mjs} +10 -3
- 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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { c as projectUrl, l as resolveProject, o as projectDirArgs, u as rewriteCanopyJsonSlug } from "./site-0i7wAJqg.mjs";
|
|
2
|
+
import { a as apiRequest, f as withContext } from "./api-Do9c8tZ_.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CDUJq-X-.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
//#region src/rename.ts
|
|
7
|
+
/**
|
|
8
|
+
* canopy rename <new-slug> — change a project's slug, its address on the
|
|
9
|
+
* site (OpenSpec project-slug-rename). The old slug stays the project's: its
|
|
10
|
+
* hostnames redirect, and everything that still names it (canopy.json
|
|
11
|
+
* elsewhere, CI, MCP configs, git remotes, report keys) keeps reaching the
|
|
12
|
+
* project. The canopy.json in this directory is pointed at the new slug.
|
|
13
|
+
*/
|
|
14
|
+
/** What changes for people outside the platform — said before anything happens. */
|
|
15
|
+
function renameConsequences(from, to) {
|
|
16
|
+
return [
|
|
17
|
+
`The address becomes ${projectUrl(to)}; ${projectUrl(from)} redirects there.`,
|
|
18
|
+
`Email your project sends comes from ${to}@ instead of ${from}@.`,
|
|
19
|
+
"Visitors who signed in with the client password sign in again on the new address.",
|
|
20
|
+
"Visitors' browser storage for the project (localStorage, IndexedDB) starts empty on the new address.",
|
|
21
|
+
`Scripts, CI, MCP client configs and git remotes that say "${from}" keep working; update them when convenient.`
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
const renameCommand = defineCommand({
|
|
25
|
+
meta: {
|
|
26
|
+
name: "rename",
|
|
27
|
+
description: "Change a project's slug (its address)"
|
|
28
|
+
},
|
|
29
|
+
args: {
|
|
30
|
+
slug: {
|
|
31
|
+
type: "positional",
|
|
32
|
+
description: "The new slug",
|
|
33
|
+
required: true
|
|
34
|
+
},
|
|
35
|
+
...projectDirArgs,
|
|
36
|
+
yes: {
|
|
37
|
+
type: "boolean",
|
|
38
|
+
default: false,
|
|
39
|
+
description: "Skip the confirmation"
|
|
40
|
+
},
|
|
41
|
+
json: {
|
|
42
|
+
type: "boolean",
|
|
43
|
+
default: false,
|
|
44
|
+
description: "Print the result as JSON"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
run: reportErrors("rename", async ({ args }) => {
|
|
48
|
+
const { dir, slug: from } = resolveProject(args);
|
|
49
|
+
const to = String(args.slug).trim().toLowerCase();
|
|
50
|
+
if (!args.yes) {
|
|
51
|
+
requireTty("pass --yes to rename without confirmation.");
|
|
52
|
+
p.note(renameConsequences(from, to).join("\n"), `Rename ${from} → ${to}`);
|
|
53
|
+
if (!unwrap(await p.confirm({ message: `Rename "${from}" to "${to}"?` }))) {
|
|
54
|
+
p.cancel("Aborted.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const result = await withContext(apiRequest("POST", `/projects/${from}/rename`, { slug: to }), {
|
|
59
|
+
action: `rename "${from}"`,
|
|
60
|
+
fixes: {
|
|
61
|
+
forbidden: "Only the project's owner or a Canopy admin can rename it — ask the owner.",
|
|
62
|
+
invalid_slug: "Use 2–40 lowercase letters, digits and dashes, starting with a letter — not a name Canopy reserves (www, docs, api, …).",
|
|
63
|
+
slug_taken: "Pick another slug: this one belongs to another project, now or in the past."
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const updated = rewriteCanopyJsonSlug(dir, from, result.project.slug);
|
|
67
|
+
if (args.json) {
|
|
68
|
+
console.log(JSON.stringify({
|
|
69
|
+
...result,
|
|
70
|
+
canopy_json_updated: updated
|
|
71
|
+
}, null, 2));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
p.log.success(`Renamed to ${result.project.slug}: ${projectUrl(result.project.slug)}`);
|
|
75
|
+
if (updated) p.log.info("Updated canopy.json.");
|
|
76
|
+
if (result.refresh.pending) p.log.warn("The backend still reports the old slug as PROJECT_SLUG (it keeps working). Canopy retries; `canopy deploy` or Settings → Retry refreshes it now.");
|
|
77
|
+
if (result.slack === "no_config_token" || result.slack === "rejected") p.log.info("The project's Slack app keeps its old URLs, which still work. They move on its next manifest update.");
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
//#endregion
|
|
81
|
+
export { renameCommand };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as apiRequest,
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-
|
|
1
|
+
import { f as tryReadCanopyJson, l as resolveProject, o as projectDirArgs, r as siteConfig } from "./site-0i7wAJqg.mjs";
|
|
2
|
+
import { a as apiRequest, d as noticeRenamesOnly, f as withContext, n as agentHeaders, u as followRename } from "./api-Do9c8tZ_.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CDUJq-X-.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";
|
|
@@ -328,6 +328,8 @@ async function mintRepoToken(profile, slug) {
|
|
|
328
328
|
}
|
|
329
329
|
}).catch(() => null);
|
|
330
330
|
if (!res) throw new RepoCredentialError(`could not reach Canopy (${config.apiUrl}).`);
|
|
331
|
+
noticeRenamesOnly();
|
|
332
|
+
followRename(res, slug);
|
|
331
333
|
if (res.status === 201) return await res.json();
|
|
332
334
|
const body = await res.json().catch(() => ({}));
|
|
333
335
|
if (res.status === 401) throw new RepoCredentialError(`your ${profile} sign-in has expired — ${login}.`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as apiRequest,
|
|
3
|
-
import { r as reportErrors, t as formatDateTime } from "./ui-
|
|
1
|
+
import { l as resolveProject, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
|
|
2
|
+
import { a as apiRequest, f as withContext } from "./api-Do9c8tZ_.mjs";
|
|
3
|
+
import { r as reportErrors, t as formatDateTime } from "./ui-CDUJq-X-.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
//#region src/schedule.ts
|
|
6
6
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as apiRequest,
|
|
3
|
-
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-
|
|
1
|
+
import { l as resolveProject, o as projectDirArgs } from "./site-0i7wAJqg.mjs";
|
|
2
|
+
import { a as apiRequest, f as withContext } from "./api-Do9c8tZ_.mjs";
|
|
3
|
+
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-CDUJq-X-.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
//#region src/secrets.ts
|
|
@@ -198,6 +198,25 @@ function projectDialect(dir) {
|
|
|
198
198
|
if (dialect !== "sqlite" && dialect !== "postgres") throw new Error(`canopy.json "dialect" must be "sqlite" or "postgres", not ${JSON.stringify(dialect)}.`);
|
|
199
199
|
return dialect;
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Point canopy.json at a project's new slug after a rename (OpenSpec
|
|
203
|
+
* project-slug-rename) — only when it still names `from`, and changing
|
|
204
|
+
* nothing else in the file. True when it was rewritten.
|
|
205
|
+
*/
|
|
206
|
+
function rewriteCanopyJsonSlug(dir, from, to) {
|
|
207
|
+
let config;
|
|
208
|
+
try {
|
|
209
|
+
config = tryReadCanopyJson(dir);
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if (config?.slug !== from) return false;
|
|
214
|
+
writeFileSync(join(dir, "canopy.json"), `${JSON.stringify({
|
|
215
|
+
...config,
|
|
216
|
+
slug: to
|
|
217
|
+
}, null, 2)}\n`);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
201
220
|
function readCanopyJson(dir) {
|
|
202
221
|
const config = tryReadCanopyJson(dir);
|
|
203
222
|
if (config === void 0) throw new Error(`No canopy.json in ${dir} — run from a project created by \`canopy create\`, add one ({"slug": "<slug>", "dist": "dist"}), or pass --project <slug>.`);
|
|
@@ -255,6 +274,10 @@ function setInvocationProfile(profile) {
|
|
|
255
274
|
function setProjectDirForPin(dir) {
|
|
256
275
|
projectDirForPin = dir;
|
|
257
276
|
}
|
|
277
|
+
/** The project directory this invocation resolved (cwd when none was named). */
|
|
278
|
+
function projectDir() {
|
|
279
|
+
return projectDirForPin ?? ".";
|
|
280
|
+
}
|
|
258
281
|
function projectProfileFromCwd() {
|
|
259
282
|
return tryReadCanopyJson(projectDirForPin ?? ".")?.profile;
|
|
260
283
|
}
|
|
@@ -277,4 +300,4 @@ function siteHeaderLine(cliVersion) {
|
|
|
277
300
|
return `canopy ${cliVersion} · ${c.envOverride || !c.profileId ? c.apiUrl : `${c.profileId} (${c.projectDomain ?? new URL(c.apiUrl).hostname})`}`;
|
|
278
301
|
}
|
|
279
302
|
//#endregion
|
|
280
|
-
export {
|
|
303
|
+
export { setActiveProfile as _, projectDialect as a, getBakedProfile as b, projectUrl as c, stagingUrl as d, tryReadCanopyJson as f, readConfigFile as g, listBakedProfiles as h, siteHeaderLine as i, resolveProject as l, clearAllProfileTokens as m, setInvocationProfile as n, projectDirArgs as o, tryResolveProject as p, siteConfig as r, projectFlagArgs as s, projectDir as t, rewriteCanopyJsonSlug as u, upsertProfileAuth as v, isProfileId as x, writeConfigFile as y };
|
|
@@ -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-CVDpzGgA.mjs";
|
|
2
|
+
import { r as reportErrors } from "./ui-CDUJq-X-.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
|
-
import { c as
|
|
2
|
-
import { a as apiRequest,
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-
|
|
1
|
+
import { c as projectUrl, d as stagingUrl, l as resolveProject, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
|
|
2
|
+
import { a as apiRequest, f as withContext } from "./api-Do9c8tZ_.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CDUJq-X-.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
|
-
import {
|
|
2
|
-
import { a as apiRequest } from "./api-
|
|
3
|
-
import { r as reportErrors, t as formatDateTime } from "./ui-
|
|
1
|
+
import { l as resolveProject, o as projectDirArgs, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
|
|
2
|
+
import { a as apiRequest } from "./api-Do9c8tZ_.mjs";
|
|
3
|
+
import { r as reportErrors, t as formatDateTime } from "./ui-CDUJq-X-.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-CDUJq-X-.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-D58yV6K8.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
|
-
import {
|
|
2
|
-
import { a as apiRequest,
|
|
3
|
-
import { r as reportErrors } from "./ui-
|
|
1
|
+
import { l as resolveProject, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
|
|
2
|
+
import { a as apiRequest, f as withContext } from "./api-Do9c8tZ_.mjs";
|
|
3
|
+
import { r as reportErrors } from "./ui-CDUJq-X-.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