@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.
Files changed (45) hide show
  1. package/dist/{ai-EyrXJC5z.mjs → ai-DpQ1nLka.mjs} +2 -2
  2. package/dist/{api-ArfD6-t5.mjs → api-Do9c8tZ_.mjs} +73 -3
  3. package/dist/{browse-DEhnJAPT.mjs → browse-Dp5HytqK.mjs} +2 -2
  4. package/dist/{db-DGrCgj-R.mjs → db-BNsX7sXo.mjs} +4 -4
  5. package/dist/{deploy-B7KItUnh.mjs → deploy-CXMLABvH.mjs} +8 -8
  6. package/dist/{dev-03LA_Etx.mjs → dev-CPR-9slK.mjs} +6 -6
  7. package/dist/dev-Dj3wFKHe.mjs +2 -0
  8. package/dist/docs/ci.md +19 -1
  9. package/dist/docs/cli-rename.md +51 -0
  10. package/dist/docs/git.md +5 -0
  11. package/dist/docs/index.json +11 -1
  12. package/dist/docs/platform-egress.md +69 -12
  13. package/dist/docs/rename.md +65 -0
  14. package/dist/docs/troubleshooting.md +16 -0
  15. package/dist/{docs-B0-IEPjd.mjs → docs-CkJGcCpR.mjs} +4 -4
  16. package/dist/egress-D6qyaaAn.mjs +397 -0
  17. package/dist/{feedback-BsJ_6ke2.mjs → feedback-DMgUUl0V.mjs} +4 -4
  18. package/dist/index.mjs +33 -32
  19. package/dist/{local-CkS1XhRu.mjs → local-w2wXfE1d.mjs} +2 -2
  20. package/dist/{login-B2FJGIrD.mjs → login-CeNvHLUv.mjs} +3 -3
  21. package/dist/{logs-wSKh_FmL.mjs → logs-BzgJfWNP.mjs} +3 -3
  22. package/dist/{mcp-DtRIDtaL.mjs → mcp-DFLaXiY_.mjs} +8 -6
  23. package/dist/{migrations-CG_O5Ghw.mjs → migrations-Bsu4quya.mjs} +2 -2
  24. package/dist/{open-COFMburt.mjs → open-D71Ms1nG.mjs} +2 -2
  25. package/dist/{package-C1xCnyOB.mjs → package-CVDpzGgA.mjs} +1 -1
  26. package/dist/{project-template-DtTfXyLI.mjs → project-template-BXfHpsI8.mjs} +3 -3
  27. package/dist/project-template-BbmSmraT.mjs +2 -0
  28. package/dist/{projects-B3Wi01lw.mjs → projects-BWTIk80a.mjs} +5 -5
  29. package/dist/rename-BdIFrqvG.mjs +81 -0
  30. package/dist/repo-JBKJqyoS.mjs +2 -0
  31. package/dist/{repo-4x-CJwCp.mjs → repo-LSOxnmUk.mjs} +5 -3
  32. package/dist/{schedule-CbaDIJXr.mjs → schedule-DpMIpCGC.mjs} +3 -3
  33. package/dist/{secrets-CmY4dUHz.mjs → secrets-BVx8L0Gj.mjs} +3 -3
  34. package/dist/{site-DM2J10CB.mjs → site-0i7wAJqg.mjs} +24 -1
  35. package/dist/{skill-AqZYYsE6.mjs → skill-D58yV6K8.mjs} +2 -2
  36. package/dist/{staging-ypR79MlD.mjs → staging-ynPlMiHW.mjs} +3 -3
  37. package/dist/{token-CDrHQ5Xb.mjs → token-CxiPrKn_.mjs} +3 -3
  38. package/dist/{ui-CaHgvvkJ.mjs → ui-CDUJq-X-.mjs} +1 -1
  39. package/dist/{upgrade-DSjmifH_.mjs → upgrade-Dll5rhOQ.mjs} +2 -2
  40. package/dist/{usage-CP8rBVZi.mjs → usage-BtPsb7Iu.mjs} +10 -3
  41. package/package.json +1 -1
  42. package/dist/dev-dJ88L73Y.mjs +0 -2
  43. package/dist/egress-CzxRD6Tp.mjs +0 -188
  44. package/dist/project-template-DlzMx8Oj.mjs +0 -2
  45. 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, f as withContext } from "./api-Do9c8tZ_.mjs";
2
+ import { r as reportErrors } from "./ui-CDUJq-X-.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/ai.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { n as siteConfig } from "./site-DM2J10CB.mjs";
1
+ import { r as siteConfig, t as projectDir, u as rewriteCanopyJsonSlug } from "./site-0i7wAJqg.mjs";
2
2
  //#region src/api.ts
3
3
  /**
4
4
  * Authenticated requests to the Canopy control plane. Credentials, in order:
@@ -29,7 +29,7 @@ const ERROR_COPY = {
29
29
  fix: "Choose a different slug."
30
30
  },
31
31
  invalid_slug: {
32
- why: "the slug is not a valid format",
32
+ why: "the slug breaks one of the naming rules",
33
33
  fix: "Use lowercase letters, digits and hyphens only (e.g. my-project)."
34
34
  },
35
35
  project_paused: {
@@ -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."
@@ -179,6 +191,26 @@ const ERROR_COPY = {
179
191
  worker_secret_sync_failed: {
180
192
  why: "the secret was saved but redeploying the running worker with it failed",
181
193
  fix: "Retry, or redeploy the project (`canopy deploy`) to apply it."
194
+ },
195
+ slug_unchanged: {
196
+ why: "the project already has that slug",
197
+ fix: "Pass the new slug you want, e.g. `canopy rename my-new-name`."
198
+ },
199
+ rename_not_allowed: {
200
+ why: "Canopy's own projects (dashboard, www, docs) keep their names",
201
+ fix: "Nothing to do — those addresses are fixed."
202
+ },
203
+ rename_too_soon: {
204
+ why: "a project can be renamed once a day",
205
+ fix: "Try again after the time shown, or ask a Canopy admin if it can't wait."
206
+ },
207
+ too_many_old_slugs: {
208
+ why: "a project keeps at most 5 old slugs, and each one stays reserved for it",
209
+ fix: "Rename back to one of its old slugs instead (`canopy projects` shows them in the dashboard's Settings), or ask a Canopy admin."
210
+ },
211
+ rename_conflict: {
212
+ why: "the project changed while the rename ran",
213
+ fix: "Run the command again."
182
214
  }
183
215
  };
184
216
  /** Generic explanation for a status code when we have no per-code copy. */
@@ -248,6 +280,40 @@ function commonHeaders() {
248
280
  ...agentHeaders()
249
281
  };
250
282
  }
283
+ /**
284
+ * Renamed projects (OpenSpec project-slug-rename). A request that names an
285
+ * old slug still reaches the project, and the answer carries its current
286
+ * slug in `Canopy-Project-Slug`. By default the CLI then points canopy.json
287
+ * at it (when that file named the old slug) and says so once; `canopy mcp
288
+ * serve` and the git credential helper only say so, because what names the
289
+ * slug there is the user's own configuration, not ours to edit.
290
+ */
291
+ const RENAMED_HEADER = "canopy-project-slug";
292
+ const renames = /* @__PURE__ */ new Map();
293
+ let renameHandling = "rewrite";
294
+ /** Report renames without editing files (mcp serve, the git credential helper). */
295
+ function noticeRenamesOnly() {
296
+ renameHandling = "notice";
297
+ }
298
+ /** The current slug of a project this process saw renamed; `slug` when it saw none. */
299
+ function currentSlug(slug) {
300
+ return renames.get(slug) ?? slug;
301
+ }
302
+ /** Act on a response that says `named` is an old slug. Exported for the credential helper. */
303
+ function followRename(response, named) {
304
+ const current = response.headers.get(RENAMED_HEADER);
305
+ if (!named || !current || current === named || renames.has(named)) return;
306
+ renames.set(named, current);
307
+ if (renameHandling === "rewrite" && rewriteCanopyJsonSlug(projectDir(), named, current)) {
308
+ console.error(`canopy: project "${named}" is now "${current}" — updated canopy.json.`);
309
+ return;
310
+ }
311
+ console.error(`canopy: project "${named}" is now "${current}" — use "${current}" where you name it (MCP client config, scripts); the old name keeps working for now.`);
312
+ }
313
+ /** The slug a control-plane path names (`/projects/<slug>/…`), if any. */
314
+ function slugInPath(path) {
315
+ return /^\/projects\/([^/?#]+)/.exec(path)?.[1];
316
+ }
251
317
  var ApiError = class extends Error {
252
318
  status;
253
319
  code;
@@ -339,6 +405,7 @@ async function apiRequest(method, path, body, rawContentType, headers) {
339
405
  },
340
406
  body: body === void 0 ? void 0 : isForm ? body : rawContentType !== void 0 ? body : JSON.stringify(body)
341
407
  });
408
+ followRename(response, slugInPath(path));
342
409
  if (!response.ok) return fail(response, isDeployToken, method, path);
343
410
  if (response.status === 204) return void 0;
344
411
  return await response.json();
@@ -358,6 +425,7 @@ async function apiUpload(path, bytes, contentType) {
358
425
  },
359
426
  body: bytes
360
427
  });
428
+ followRename(response, slugInPath(path));
361
429
  if (!response.ok) return fail(response, isDeployToken, "POST", path);
362
430
  return await response.json();
363
431
  }
@@ -368,6 +436,7 @@ async function apiDownload(path) {
368
436
  ...commonHeaders(),
369
437
  authorization: `Bearer ${token}`
370
438
  } });
439
+ followRename(response, slugInPath(path));
371
440
  if (!response.ok) return fail(response, isDeployToken, "GET", path);
372
441
  return Buffer.from(await response.arrayBuffer());
373
442
  }
@@ -389,9 +458,10 @@ async function apiDataRequest(method, path, projectSlug, body, rawContentType, c
389
458
  },
390
459
  body: body === void 0 ? void 0 : rawContentType !== void 0 ? body : JSON.stringify(body)
391
460
  });
461
+ followRename(response, projectSlug);
392
462
  if (!response.ok) return fail(response, isDeployToken, method, path);
393
463
  if (response.status === 204) return void 0;
394
464
  return await response.json();
395
465
  }
396
466
  //#endregion
397
- export { apiRequest as a, credentials as c, apiDownload as i, withContext as l, agentHeaders as n, apiUpload as o, apiDataRequest as r, commonHeaders as s, ApiError as t };
467
+ export { apiRequest as a, credentials as c, noticeRenamesOnly as d, withContext as f, apiDownload as i, currentSlug as l, agentHeaders as n, apiUpload as o, apiDataRequest as r, commonHeaders as s, ApiError as t, followRename as u };
@@ -1,5 +1,5 @@
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";
1
+ import { p as tryResolveProject, r as siteConfig } from "./site-0i7wAJqg.mjs";
2
+ import { a as apiRequest, t as ApiError } from "./api-Do9c8tZ_.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  //#region src/browse.ts
5
5
  /**
@@ -1,7 +1,7 @@
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";
1
+ import { l as resolveProject, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
2
+ import { a as apiRequest, f as withContext, r as apiDataRequest } from "./api-Do9c8tZ_.mjs";
3
+ import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CDUJq-X-.mjs";
4
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-Bsu4quya.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
- 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";
1
+ import { c as projectUrl, d as stagingUrl, f as tryReadCanopyJson, l as resolveProject, o as projectDirArgs, r as siteConfig, s as projectFlagArgs } from "./site-0i7wAJqg.mjs";
2
+ import { a as apiRequest, f as withContext, i as apiDownload, l as currentSlug, r as apiDataRequest } from "./api-Do9c8tZ_.mjs";
3
+ import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-CDUJq-X-.mjs";
4
+ import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-Bsu4quya.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";
@@ -314,10 +314,10 @@ const deployCommand = defineCommand({
314
314
  }
315
315
  const backend = result.worker?.serviceUrl ? ` · backend ${result.worker.serviceUrl}` : "";
316
316
  if (channel === "staging") {
317
- spin.stop(`Staging: ${stagingUrl(slug)} (v${result.version})${backend}`);
317
+ spin.stop(`Staging: ${stagingUrl(currentSlug(slug))} (v${result.version})${backend}`);
318
318
  if (stagingCreated) console.log(" staging channel created — database cloned from live");
319
319
  console.log(` happy with it? \`canopy promote\` makes v${result.version} live`);
320
- } else spin.stop(`Live: ${projectUrl(slug)} (v${result.version})${backend}`);
320
+ } else spin.stop(`Live: ${projectUrl(currentSlug(slug))} (v${result.version})${backend}`);
321
321
  if (migrateResult !== void 0) for (const line of describeMigrateResult(migrateResult)) console.log(` ${line}`);
322
322
  })
323
323
  });
@@ -371,7 +371,7 @@ const promoteCommand = defineCommand({
371
371
  }
372
372
  const backend = result.worker?.serviceUrl ? ` · backend ${result.worker.serviceUrl}` : "";
373
373
  const was = result.previous === null ? "" : `, was v${result.previous}`;
374
- spin.stop(`Live: ${projectUrl(slug)} (v${result.version}${was})${backend}`);
374
+ spin.stop(`Live: ${projectUrl(currentSlug(slug))} (v${result.version}${was})${backend}`);
375
375
  for (const name of result.migrationsApplied) console.log(` applied ${name}`);
376
376
  if (result.snapshot !== void 0) console.log(` pre-promote snapshot: ${result.snapshot} (canopy db restore --snapshot …)`);
377
377
  })
@@ -445,7 +445,7 @@ const rollbackCommand = defineCommand({
445
445
  console.log(`Rolling back ${slug} to v${target}`);
446
446
  const { live: result } = await apiRequest("POST", `/projects/${slug}/deploys/${target}/rollback`);
447
447
  const worker = result.worker ? ` · worker v${result.worker.version}` : "";
448
- console.log(`Live: ${projectUrl(slug)} (v${result.version}, was v${result.previous ?? "?"})${worker}`);
448
+ console.log(`Live: ${projectUrl(currentSlug(slug))} (v${result.version}, was v${result.previous ?? "?"})${worker}`);
449
449
  })
450
450
  });
451
451
  const downloadCommand = defineCommand({
@@ -1,7 +1,7 @@
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";
1
+ import { a as projectDialect, f as tryReadCanopyJson, l as resolveProject, o as projectDirArgs, r as siteConfig } from "./site-0i7wAJqg.mjs";
2
+ import { a as apiRequest, f as withContext, t as ApiError } from "./api-Do9c8tZ_.mjs";
3
+ import { r as reportErrors, t as formatDateTime } from "./ui-CDUJq-X-.mjs";
4
+ import { r as readMigrationFiles } from "./migrations-Bsu4quya.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-w2wXfE1d.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-w2wXfE1d.mjs");
271
271
  const sandbox = await createSandbox({
272
272
  slug,
273
273
  persistDir: join(flags.dir, ".canopy", "local"),
@@ -0,0 +1,2 @@
1
+ import { n as devCommand } from "./dev-CPR-9slK.mjs";
2
+ export { devCommand };
package/dist/docs/ci.md CHANGED
@@ -1,8 +1,26 @@
1
1
  ---
2
2
  title: CI and deploy tokens
3
- description: Push-to-main deploys using a project-scoped deploy token.
3
+ description: Push-to-main deploys using a project-scoped deploy token, and why we recommend deploying from your own machine instead.
4
4
  ---
5
5
 
6
+ ## Our recommendation: deploy manually
7
+
8
+ Most Canopy users don't have a GitHub Enterprise seat, so they can't see or
9
+ interact with GitHub Actions. That is why Canopy repositories are created with
10
+ Actions turned **off**: pushing a workflow file works, but nothing runs, and
11
+ there is no Actions tab for you to look at. Turning Actions on for a repository
12
+ is something only a GitHub administrator can do, and you would still not see
13
+ the runs.
14
+
15
+ Deploy from your machine (or let your coding agent do it): run
16
+ `canopy deploy --staging`, check the result, then `canopy promote`. It is
17
+ usually quicker than waiting for a pipeline, and your agent can verify the
18
+ deploy right afterwards. Run your typecheck, lint and tests locally before you
19
+ deploy.
20
+
21
+ The rest of this page is for teams that run their own CI somewhere they can
22
+ see it.
23
+
6
24
  Interactive `canopy login` is for humans at a terminal. For automated deploys
7
25
  (GitHub Actions, etc.) use a **deploy token**: a project-scoped credential that
8
26
  can deploy that one project and nothing else.
@@ -0,0 +1,51 @@
1
+ ---
2
+ title: canopy rename
3
+ description: Change a project's slug — its address — and update canopy.json.
4
+ ---
5
+
6
+ `canopy rename` gives a project a new slug, and with it a new address. The old
7
+ slug stays with the project: old links redirect, and everything that still
8
+ names it keeps working. See [Rename a project](https://docs.canopy.website/guides/rename) for what changes
9
+ and what doesn't.
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ canopy rename my-new-name # in the project's folder
15
+ canopy rename my-new-name --project old-name # name the project yourself
16
+ canopy rename my-new-name --yes # skip the confirmation
17
+ ```
18
+
19
+ ## Arguments & flags
20
+
21
+ - `<slug>` — the new slug. Same rules as `canopy create`: 2–40 lowercase letters,
22
+ digits and dashes, starting with a letter.
23
+ - `--project <slug>` — the project to rename. Defaults to the `slug` in
24
+ `canopy.json`.
25
+ - `[dir]` — the project folder (defaults to the current directory). Its
26
+ `canopy.json` is updated after the rename.
27
+ - `--yes` — rename without asking. Required on non-interactive runs.
28
+ - `--json` — print the result as JSON: the project, its old slugs, and whether
29
+ the backend refresh and Slack app update are still to come.
30
+
31
+ ## Details
32
+
33
+ Before renaming, the CLI lists what changes for people: the address, the email
34
+ sender, client-password visitors signing in again, and visitors' browser storage
35
+ starting empty. Only the owner or a Canopy admin can rename. A project can be
36
+ renamed once a day and keeps at most 5 old slugs; renaming back to one of them
37
+ is always allowed.
38
+
39
+ Other commands notice a rename too. When `canopy.json` still names the old
40
+ slug, the next command that talks to the project updates it and says so in one
41
+ line. `canopy mcp serve` and the git credential helper only print a notice,
42
+ since they're started from your own configuration.
43
+
44
+ ## Examples
45
+
46
+ ```bash
47
+ canopy rename q4-dashboard
48
+ ```
49
+
50
+ Shows what changes, asks to confirm, renames, and points `canopy.json` at
51
+ `q4-dashboard`.
package/dist/docs/git.md CHANGED
@@ -79,6 +79,11 @@ sign in to a Canopy repository; use the terminal for those.
79
79
  - **Deleting is for good.** `canopy repo delete` (owner only) removes the
80
80
  repository and all its history. Deleting the project deletes its repository
81
81
  too.
82
+ - **No CI on the repository.** GitHub Actions are turned off on Canopy
83
+ repositories, so workflow files you push don't run, and you can't see
84
+ Actions runs without a GitHub seat. Run your checks locally and deploy with
85
+ `canopy deploy --staging` then `canopy promote` (see
86
+ [CI and deploy tokens](https://docs.canopy.website/guides/ci)).
82
87
  - **Moving to your own GitHub later** is ordinary git: create an empty
83
88
  repository there, then `git remote add github <its url>` and
84
89
  `git push github --all`.
@@ -65,7 +65,7 @@
65
65
  {
66
66
  "slug": "ci",
67
67
  "title": "CI and deploy tokens",
68
- "summary": "Push-to-main deploys using a project-scoped deploy token."
68
+ "summary": "Push-to-main deploys using a project-scoped deploy token, and why we recommend deploying from your own machine instead."
69
69
  },
70
70
  {
71
71
  "slug": "git",
@@ -77,6 +77,11 @@
77
77
  "title": "Start from a template",
78
78
  "summary": "Begin a project from someone else's working one — a copy of their code and setup, never their data."
79
79
  },
80
+ {
81
+ "slug": "rename",
82
+ "title": "Rename a project",
83
+ "summary": "Change a project's slug — its address — without breaking links, checkouts or integrations."
84
+ },
80
85
  {
81
86
  "slug": "agent-tools",
82
87
  "title": "Agent tools (MCP)",
@@ -231,6 +236,11 @@
231
236
  "title": "canopy delete",
232
237
  "summary": "Permanently delete a Canopy project — its deploys, database, and files."
233
238
  },
239
+ {
240
+ "slug": "cli-rename",
241
+ "title": "canopy rename",
242
+ "summary": "Change a project's slug — its address — and update canopy.json."
243
+ },
234
244
  {
235
245
  "slug": "cli-dev",
236
246
  "title": "canopy dev",
@@ -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
 
@@ -0,0 +1,65 @@
1
+ ---
2
+ title: Rename a project
3
+ description: Change a project's slug — its address — without breaking links, checkouts or integrations.
4
+ ---
5
+
6
+ A project's **slug** is its address: `<slug>.canopy.website`. You can change it
7
+ from the dashboard or the CLI. Nothing needs to be redeployed, and everything
8
+ that still uses the old slug keeps reaching the project.
9
+
10
+ ## How to rename
11
+
12
+ In the dashboard, open the project's **Settings** and use **Rename address**
13
+ on the _Project address_ card. Type the new slug, read what changes, and type
14
+ it again to confirm.
15
+
16
+ From the CLI, run it inside the project's folder:
17
+
18
+ ```bash
19
+ canopy rename my-new-name
20
+ ```
21
+
22
+ The CLI shows what changes and asks before it renames. It also updates
23
+ `canopy.json` in that folder. See [`canopy rename`](https://docs.canopy.website/cli/rename) for the flags.
24
+
25
+ Only the project's **owner** (or a Canopy admin) can rename it.
26
+
27
+ ## What changes
28
+
29
+ - **The address.** The project is now at `my-new-name.canopy.website`. The old
30
+ address sends visitors there, keeping the rest of the link
31
+ (`old-name.canopy.website/reports/42` opens `my-new-name.canopy.website/reports/42`).
32
+ The same goes for `staging-dot-` and pinned `v12-dot-` addresses.
33
+ - **The email sender.** Mail your project sends comes from `my-new-name@` instead
34
+ of `old-name@`.
35
+ - **Client-password visitors sign in again** on the new address. Their sign-in
36
+ belonged to the old one.
37
+ - **Browser storage starts empty** on the new address. Anything your app keeps
38
+ in the visitor's browser (localStorage, IndexedDB) stays with the old one.
39
+ - **New report keys** use the new slug: `my-new-name-18`.
40
+
41
+ ## What keeps working
42
+
43
+ The old slug stays with your project. Nobody else can create a project with it.
44
+
45
+ - Old links and bookmarks redirect, as above. Pages that were already open keep
46
+ working until they're reloaded.
47
+ - `canopy.json` files, CI deploy tokens, scripts and MCP client configs that
48
+ still say the old slug keep working. The CLI updates a `canopy.json` it finds
49
+ with the old slug, and tells you when anything else should be updated.
50
+ - Your backend keeps running, and the rename updates the `PROJECT_SLUG` it
51
+ sees. If that update fails, Settings says so and has a retry button; Canopy
52
+ also retries on its own, and the next deploy updates it too.
53
+ - Report keys with the old slug (`old-name-17`) still open the report.
54
+ - Your Slack app keeps receiving events and slash commands.
55
+ - `git push` and `git pull` keep working. The repository itself keeps its name.
56
+ - Database, files, deploys, schedules, secrets and members don't change.
57
+
58
+ ## Limits
59
+
60
+ - A project can be renamed **once a day**.
61
+ - A project keeps at most **5 old slugs**. Renaming back to one of them is always
62
+ allowed, and doesn't count as a new one.
63
+ - The new slug follows the same rules as a new project: 2–40 lowercase letters,
64
+ digits and dashes, starting with a letter.
65
+ - Canopy's own projects (`dashboard`, `www`, `docs`) can't be renamed.
@@ -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";
2
- import { n as siteConfig } from "./site-DM2J10CB.mjs";
1
+ import { t as version } from "./package-CVDpzGgA.mjs";
2
+ import { r as siteConfig } from "./site-0i7wAJqg.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-Do9c8tZ_.mjs";
5
+ import { r as reportErrors } from "./ui-CDUJq-X-.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";