@mediamonks/canopy-cli 0.7.33 → 0.7.35
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/browse-Dycwj0V_.mjs +89 -0
- package/dist/dev-CID2LCES.mjs +2 -0
- package/dist/{dev-CkhBnSlv.mjs → dev-oS4nYMSn.mjs} +14 -8
- package/dist/{dev-worker-CaCsGBkn.mjs → dev-worker-CpSB_Okh.mjs} +5 -3
- package/dist/docs/agent-browsing.md +58 -0
- package/dist/docs/cli-browse.md +48 -0
- package/dist/docs/cli-dev.md +2 -2
- package/dist/docs/index.json +10 -0
- package/dist/docs/platform-google.md +8 -0
- package/dist/docs/sdk-database.md +3 -0
- package/dist/{docs-BP5jmD10.mjs → docs-CGrXLl7G.mjs} +1 -1
- package/dist/{feedback-5oPSxAz0.mjs → feedback-CrmDfqmZ.mjs} +1 -1
- package/dist/index.mjs +9 -7
- package/dist/{local-DAYCNUyz.mjs → local-Cc8H3NUw.mjs} +84 -14
- package/dist/{mcp-DXfTYVN_.mjs → mcp-DAaO4SQc.mjs} +1 -1
- package/dist/{package-CQIPmdcO.mjs → package-BOXNJrWP.mjs} +1 -1
- package/dist/skill/canopy/SKILL.md +6 -0
- package/dist/{skill-Dg7dQF0k.mjs → skill-GPJrYL61.mjs} +1 -1
- package/dist/{src-BkACMkol.mjs → src-CxzGOEhP.mjs} +28 -3
- package/dist/{upgrade-huJDjhrV.mjs → upgrade-DSTg2cgu.mjs} +1 -1
- package/package.json +1 -1
- package/dist/dev-MoHHjQh1.mjs +0 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { d as tryResolveProject, n as siteConfig } from "./site-DM2J10CB.mjs";
|
|
2
|
+
import { i as apiRequest, t as ApiError } from "./api-C4rMhE7i.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
//#region src/browse.ts
|
|
5
|
+
/**
|
|
6
|
+
* canopy browse — a one-time sign-in link to a project for an agent's
|
|
7
|
+
* browser (agent-browse-sessions). Opening it within a minute gives that
|
|
8
|
+
* browser an hour on this one project and channel, as the signed-in Monk;
|
|
9
|
+
* nothing else on Canopy. The link is the only thing printed: no cookie, no
|
|
10
|
+
* token, nothing written to disk, and no browser opened (that is `canopy open`).
|
|
11
|
+
*/
|
|
12
|
+
var BrowseError = class extends Error {
|
|
13
|
+
name = "BrowseError";
|
|
14
|
+
};
|
|
15
|
+
/** The control plane's refusal, in words that say what to do about it. */
|
|
16
|
+
function explainBrowseRefusal(code, slug, detail) {
|
|
17
|
+
switch (code) {
|
|
18
|
+
case "members_only": return `${slug} is limited to its collaborators — ask the owner to add you.`;
|
|
19
|
+
case "project_not_found": return `There is no project ${slug} on this site — check the slug and --profile.`;
|
|
20
|
+
case "project_paused": return `${slug} is paused.`;
|
|
21
|
+
case "project_moved": return `${slug} has moved${detail ? ` (${detail})` : ""}.`;
|
|
22
|
+
case "staging_not_enabled": return `${slug} has no staging channel yet — drop --staging, or create it with \`canopy deploy --staging\`.`;
|
|
23
|
+
case "invalid_path": return "--path must be a page on the project, starting with / (for example /settings).";
|
|
24
|
+
case "cli_session_required": return "Browse links need your own CLI sign-in, not a deploy token — run `canopy login`.";
|
|
25
|
+
case "not_configured": return "This site has no public project hostnames, so it can't hand out browse links.";
|
|
26
|
+
default: return detail ? `${code}: ${detail}` : code;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const browseCommand = defineCommand({
|
|
30
|
+
meta: {
|
|
31
|
+
name: "browse",
|
|
32
|
+
description: "Print a one-time sign-in link to a project for an agent's browser"
|
|
33
|
+
},
|
|
34
|
+
args: {
|
|
35
|
+
slug: {
|
|
36
|
+
type: "positional",
|
|
37
|
+
description: "Project slug (defaults to canopy.json in the current directory)",
|
|
38
|
+
required: false
|
|
39
|
+
},
|
|
40
|
+
staging: {
|
|
41
|
+
type: "boolean",
|
|
42
|
+
default: false,
|
|
43
|
+
description: "Link to the staging channel instead of live"
|
|
44
|
+
},
|
|
45
|
+
path: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Page to land on after signing in (default /)"
|
|
48
|
+
},
|
|
49
|
+
json: {
|
|
50
|
+
type: "boolean",
|
|
51
|
+
default: false,
|
|
52
|
+
description: "Print only { url, expires_at } as JSON"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async run({ args }) {
|
|
56
|
+
try {
|
|
57
|
+
const slug = tryResolveProject({ project: args.slug }).slug;
|
|
58
|
+
if (!slug) throw new BrowseError("No project named — pass a slug (`canopy browse <slug>`) or run from a project directory.");
|
|
59
|
+
if (process.env["CANOPY_DEPLOY_TOKEN"]) throw new BrowseError("Browse links use your own sign-in, not a deploy token — unset CANOPY_DEPLOY_TOKEN and run `canopy login`.");
|
|
60
|
+
if (!siteConfig().token) throw new BrowseError("Not signed in to this site — run `canopy login` first.");
|
|
61
|
+
let link;
|
|
62
|
+
try {
|
|
63
|
+
link = await apiRequest("POST", `/projects/${slug}/browse-ticket`, {
|
|
64
|
+
channel: args.staging ? "staging" : "live",
|
|
65
|
+
...args.path !== void 0 && { path: args.path }
|
|
66
|
+
});
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof ApiError && err.status !== 401) throw new BrowseError(explainBrowseRefusal(err.code, slug, err.detail));
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
if (args.json) {
|
|
72
|
+
process.stdout.write(`${JSON.stringify({
|
|
73
|
+
url: link.url,
|
|
74
|
+
expires_at: link.expires_at
|
|
75
|
+
})}\n`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log(`\n ${link.url}\n`);
|
|
79
|
+
console.log(" Open it within a minute; it works once. The browser that opens it is signed in to");
|
|
80
|
+
console.log(` ${slug}${args.staging ? " (staging)" : ""} as you for an hour, and to nothing else on Canopy.\n`);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
process.stderr.write(`canopy browse: ${message}\n`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
//#endregion
|
|
89
|
+
export { browseCommand };
|
|
@@ -2,12 +2,13 @@ import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as sit
|
|
|
2
2
|
import { i as apiRequest, o as withContext, t as ApiError } from "./api-C4rMhE7i.mjs";
|
|
3
3
|
import { r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
|
|
4
4
|
import { r as readMigrationFiles } from "./migrations-Czlcddyz.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER } from "./src-CxzGOEhP.mjs";
|
|
6
6
|
import { n as writeDevServerRecord } from "./dev-server-file-Dk9pk4AR.mjs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { defineCommand } from "citty";
|
|
9
9
|
import { existsSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
11
12
|
import { pathToFileURL } from "node:url";
|
|
12
13
|
//#region src/dev.ts
|
|
13
14
|
/**
|
|
@@ -33,14 +34,14 @@ function workerDialect(flags) {
|
|
|
33
34
|
* gateway serves it in production (dev-worker.ts). Returns the plugin plus
|
|
34
35
|
* the notice lines to print, or undefined when the project has no worker.
|
|
35
36
|
*/
|
|
36
|
-
async function workerPlugin(flags, slug) {
|
|
37
|
+
async function workerPlugin(flags, slug, workerAttest) {
|
|
37
38
|
const entry = tryReadCanopyJson(flags.dir)?.worker;
|
|
38
39
|
if (entry === void 0) return void 0;
|
|
39
40
|
if (!existsSync(join(flags.dir, entry))) {
|
|
40
41
|
console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
|
|
41
42
|
return;
|
|
42
43
|
}
|
|
43
|
-
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-
|
|
44
|
+
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-CpSB_Okh.mjs");
|
|
44
45
|
const secrets = readDevVars(flags.dir);
|
|
45
46
|
const email = siteConfig().email;
|
|
46
47
|
const plugin = workerDevPlugin({
|
|
@@ -54,7 +55,8 @@ async function workerPlugin(flags, slug) {
|
|
|
54
55
|
email,
|
|
55
56
|
name: "Local Dev"
|
|
56
57
|
} : null,
|
|
57
|
-
dialect: workerDialect(flags)
|
|
58
|
+
dialect: workerDialect(flags),
|
|
59
|
+
workerAttest
|
|
58
60
|
});
|
|
59
61
|
const notes = [` /api/* is served by ${entry} (edit and refresh; no build step)`];
|
|
60
62
|
if (Object.keys(secrets).length > 0) notes.push(` worker secrets from .dev.vars: ${Object.keys(secrets).sort().join(", ")}`);
|
|
@@ -168,6 +170,8 @@ async function bridgeToSandbox(sandbox, req, res) {
|
|
|
168
170
|
const headers = new Headers();
|
|
169
171
|
if (req.headers["content-type"]) headers.set("content-type", req.headers["content-type"]);
|
|
170
172
|
if (req.headers["accept"]) headers.set("accept", req.headers["accept"]);
|
|
173
|
+
const attest = req.headers[WORKER_ATTEST_HEADER];
|
|
174
|
+
if (typeof attest === "string") headers.set(WORKER_ATTEST_HEADER, attest);
|
|
171
175
|
const response = await sandbox.handle(new Request(`http://canopy.local${req.url ?? ""}`, {
|
|
172
176
|
method: req.method,
|
|
173
177
|
headers,
|
|
@@ -225,7 +229,7 @@ async function devRemote(flags, slug) {
|
|
|
225
229
|
async function devLocal(flags, slug) {
|
|
226
230
|
let dump;
|
|
227
231
|
if (flags.pull) {
|
|
228
|
-
const { fetchProjectDump } = await import("./local-
|
|
232
|
+
const { fetchProjectDump } = await import("./local-Cc8H3NUw.mjs");
|
|
229
233
|
console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
|
|
230
234
|
dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
|
|
231
235
|
}
|
|
@@ -240,12 +244,14 @@ async function devLocal(flags, slug) {
|
|
|
240
244
|
const reason = err instanceof ApiError ? err.explain().why : String(err);
|
|
241
245
|
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
242
246
|
}
|
|
243
|
-
const
|
|
247
|
+
const workerAttest = randomBytes(24).toString("base64url");
|
|
248
|
+
const { createSandbox } = await import("./local-Cc8H3NUw.mjs");
|
|
244
249
|
const sandbox = await createSandbox({
|
|
245
250
|
slug,
|
|
246
251
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
247
252
|
email: siteConfig().email,
|
|
248
|
-
dialect: flags.dialect
|
|
253
|
+
dialect: flags.dialect,
|
|
254
|
+
workerAttest
|
|
249
255
|
});
|
|
250
256
|
if (dump !== void 0) {
|
|
251
257
|
await sandbox.applyDump(dump);
|
|
@@ -276,7 +282,7 @@ async function devLocal(flags, slug) {
|
|
|
276
282
|
headers: { authorization: `Bearer ${remote.token}` },
|
|
277
283
|
rewrite: rewriteApiPath
|
|
278
284
|
};
|
|
279
|
-
const worker = await workerPlugin(flags, slug);
|
|
285
|
+
const worker = await workerPlugin(flags, slug, workerAttest);
|
|
280
286
|
const server = await startVite(flags, { proxy: {
|
|
281
287
|
...remoteEntry && {
|
|
282
288
|
"/_api/data/v1/ai": remoteEntry,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as
|
|
1
|
+
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER, r as redactCanopyCredentials, t as CALLER_HEADER } from "./src-CxzGOEhP.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
4
|
//#region src/dev-worker.ts
|
|
@@ -97,7 +97,7 @@ function rewriteCanopyUrl(raw, base) {
|
|
|
97
97
|
* dev server's `/_api` mount so the running mode decides what answers.
|
|
98
98
|
* Credentials stay in the proxy; nothing here carries a token.
|
|
99
99
|
*/
|
|
100
|
-
function createCanopyFetch(apiBase, slug, channel, fetchImpl = fetch) {
|
|
100
|
+
function createCanopyFetch(apiBase, slug, channel, fetchImpl = fetch, workerAttest) {
|
|
101
101
|
return async (input, init) => {
|
|
102
102
|
const url = rewriteCanopyUrl(typeof input === "string" ? input : input instanceof URL ? input.href : input.url, apiBase());
|
|
103
103
|
const headers = stampWorkerHeaders(new Headers(input instanceof Request ? input.headers : init?.headers), {
|
|
@@ -106,6 +106,8 @@ function createCanopyFetch(apiBase, slug, channel, fetchImpl = fetch) {
|
|
|
106
106
|
});
|
|
107
107
|
headers.delete("authorization");
|
|
108
108
|
headers.delete("cookie");
|
|
109
|
+
if (workerAttest) headers.set(WORKER_ATTEST_HEADER, workerAttest);
|
|
110
|
+
else headers.delete(WORKER_ATTEST_HEADER);
|
|
109
111
|
const method = (input instanceof Request ? input.method : init?.method) ?? "GET";
|
|
110
112
|
const body = input instanceof Request ? input.body : init?.body;
|
|
111
113
|
const outbound = {
|
|
@@ -263,7 +265,7 @@ async function sendResponse(res, response) {
|
|
|
263
265
|
function workerDevPlugin(options) {
|
|
264
266
|
const moduleId = entryModuleId(options.dir, options.entry);
|
|
265
267
|
let apiBase = "";
|
|
266
|
-
const canopyFetch = createCanopyFetch(() => apiBase, options.slug, options.channel);
|
|
268
|
+
const canopyFetch = createCanopyFetch(() => apiBase, options.slug, options.channel, fetch, options.workerAttest);
|
|
267
269
|
return {
|
|
268
270
|
name: "canopy-project-worker",
|
|
269
271
|
configureServer(server) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Let your agent browse a project
|
|
3
|
+
description: Give an AI agent's browser a short sign-in to one project, so it can look at and click through your app as you.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Coding agents often need to see the app they're working on: check that a page renders, click through a flow, or read what a screen shows. Every Canopy project sits behind the Monks sign-in, which an agent can't get through on its own.
|
|
7
|
+
|
|
8
|
+
`canopy browse` gives the agent a way in that doesn't involve handing over your own browser session:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
canopy browse my-app --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
It prints a one-time link. Whatever browser opens it is signed in to **my-app**, as you, for **an hour**, and to nothing else on Canopy.
|
|
15
|
+
|
|
16
|
+
## What the agent gets
|
|
17
|
+
|
|
18
|
+
- **One project, one channel.** The live site, or staging with `--staging`. The browser is not signed in to your other projects, or to the dashboard.
|
|
19
|
+
- **You.** The app sees you as the visitor, exactly as when you open it yourself. Your backend's `env.USER` is you, and data you can see, the agent can see. On a collaborators-only project you have to be a collaborator.
|
|
20
|
+
- **An hour.** After that the agent meets the sign-in page and can run `canopy browse` again.
|
|
21
|
+
- **Marked as an agent.** Your backend can tell the request came from an agent: `mcpCaller(request, env).agent` from `@mediamonks/canopy/mcp` is `true`.
|
|
22
|
+
|
|
23
|
+
The link itself works **once**, within **a minute**. Opening it a second time shows a page saying to run `canopy browse` again.
|
|
24
|
+
|
|
25
|
+
## Using it from different tools
|
|
26
|
+
|
|
27
|
+
**Playwright, or a browser tool in your agent:** open the URL. The browser is redirected to the page and stays signed in.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
await page.goto(url); // lands on the project, signed in
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**curl:** keep the cookie in a jar and follow the redirect.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -c jar.txt -L "$(canopy browse my-app --json | jq -r .url)"
|
|
37
|
+
curl -b jar.txt https://my-app.canopy.website/api/notes
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**A specific page:** `canopy browse my-app --path /settings` lands there directly.
|
|
41
|
+
|
|
42
|
+
## Stopping it
|
|
43
|
+
|
|
44
|
+
The agent's session ends when the hour is up. It also ends early if you:
|
|
45
|
+
|
|
46
|
+
- run `canopy logout`,
|
|
47
|
+
- revoke that CLI sign-in on your Account page in the dashboard, or
|
|
48
|
+
- sign out everywhere.
|
|
49
|
+
|
|
50
|
+
If you lose access to a collaborators-only project, the agent loses it too.
|
|
51
|
+
|
|
52
|
+
## Good to know
|
|
53
|
+
|
|
54
|
+
- Your project's backend needs a redeploy after this feature shipped before an agent can call its `/api/*` routes. Until then those calls answer "redeploy required", while pages still load.
|
|
55
|
+
- The feedback widget doesn't appear for agents.
|
|
56
|
+
- Never give an agent your own browser session cookie. It opens every project and the dashboard, as you.
|
|
57
|
+
|
|
58
|
+
See also [canopy browse](https://docs.canopy.website/cli/browse), and [Agent tools](https://docs.canopy.website/guides/agent-tools) for letting agents call your backend directly.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: canopy browse
|
|
3
|
+
description: Print a one-time sign-in link that lets an agent's browser open a project as you.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`canopy browse` prints a link for an AI agent's browser: Playwright, a browser tool in Claude or Cursor, or plain `curl`. Opening the link signs that browser in to **one project**, as you, for **one hour**. It reaches nothing else on Canopy: no other projects, and not the dashboard.
|
|
7
|
+
|
|
8
|
+
Read [Let your agent browse a project](https://docs.canopy.website/guides/agent-browsing) for the full picture.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
canopy browse [slug] [--staging] [--path <page>] [--json]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Arguments & flags
|
|
17
|
+
|
|
18
|
+
- `[slug]` — the project. Defaults to the `canopy.json` in the current directory.
|
|
19
|
+
- `--staging` — link to the project's [staging channel](https://docs.canopy.website/guides/staging) instead of the live site.
|
|
20
|
+
- `--path <page>` — the page to land on after signing in, such as `/settings`. Defaults to `/`.
|
|
21
|
+
- `--json` — print only `{ "url", "expires_at" }`, for an agent to read.
|
|
22
|
+
|
|
23
|
+
The site comes from the active profile, as for every command (`--profile staging` links to the project on the staging site).
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
The command uses your `canopy login` sign-in to ask Canopy for a link to the project, under the same rules as opening it yourself. Collaborators-only projects only give links to collaborators.
|
|
28
|
+
|
|
29
|
+
- The link **works once** and **expires after a minute**. Run the command again for a new one.
|
|
30
|
+
- The browser that opens it is signed in to that project and channel for an hour. After that it sees the sign-in page again.
|
|
31
|
+
- Running `canopy logout`, revoking the CLI sign-in on your Account page, or signing out everywhere also signs the agent out.
|
|
32
|
+
- The command never opens a browser itself (that's [canopy open](https://docs.canopy.website/cli/open)), never prints a cookie, and saves nothing.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
canopy browse my-app --json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Prints `{"url":"https://my-app.…/__canopy/agent-login?ticket=…","expires_at":"…"}`. Hand the URL to your agent's browser.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
canopy browse --staging --path /admin
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A link to the staging channel of the project in this folder, landing on `/admin`.
|
|
47
|
+
|
|
48
|
+
See also [canopy mcp](https://docs.canopy.website/cli/mcp), which lets agents call your project's backend as tools instead of using its pages.
|
package/dist/docs/cli-dev.md
CHANGED
|
@@ -25,9 +25,9 @@ canopy dev --local --pull # sandbox hydrated from a fresh prod export
|
|
|
25
25
|
|
|
26
26
|
## How it works
|
|
27
27
|
|
|
28
|
-
**Remote-first (default).** The CLI mints a short-lived, project-scoped dev token from the platform, then starts Vite with a proxy: every `/_api/*` request is forwarded to the real platform API with that token attached. The token stays inside the proxy — browser code never holds a credential. When the token expires (the expiry is printed at startup), restart `canopy dev`. Because you're on live data, the startup banner suggests `canopy db snapshot` before risky changes, or switching to `--local --pull`.
|
|
28
|
+
**Remote-first (default).** The CLI mints a short-lived, project-scoped dev token from the platform, then starts Vite with a proxy: every `/_api/*` request is forwarded to the real platform API with that token attached. The token stays inside the proxy — browser code never holds a credential. When the token expires (the expiry is printed at startup), restart `canopy dev`. Because every call from your browser carries that token, the `private_` rule doesn't apply to your own pages in this mode; use `--local` to test it. Because you're on live data, the startup banner suggests `canopy db snapshot` before risky changes, or switching to `--local --pull`.
|
|
29
29
|
|
|
30
|
-
**Local sandbox (`--local`).** Database, file, and auth requests are answered entirely on your machine, from a sandbox stored under `.canopy/local/` in the project directory — no database to install, and writes never reach the platform. The sandbox behaves the same as the real API, so code that works locally works deployed. Two things still go remote: `ai` and `services` calls go through to the real platform (metered as usual) when you're logged in — fully offline, they return errors until you log in and restart.
|
|
30
|
+
**Local sandbox (`--local`).** Database, file, and auth requests are answered entirely on your machine, from a sandbox stored under `.canopy/local/` in the project directory — no database to install, and writes never reach the platform. The sandbox behaves the same as the real API, so code that works locally works deployed. That includes [`private_` collections and tables](https://docs.canopy.website/sdk/database): your pages get the same refusal they would get deployed, while your worker keeps full access. The one setting it doesn't copy is **Browser access** on the Data tab: locally, pages always get read and write. Two things still go remote: `ai` and `services` calls go through to the real platform (metered as usual) when you're logged in — fully offline, they return errors until you log in and restart.
|
|
31
31
|
|
|
32
32
|
**Matching your site's SQL dialect.** The document API (`canopy.db.collection(…)`) behaves identically everywhere, but raw `canopy.db.sql()` speaks your site's dialect — SQLite or Postgres (see [dialect()](https://docs.canopy.website/guides/data)). The sandbox defaults to Postgres. If your project uses raw SQL written for a SQLite site, lock the dialect in `canopy.json`:
|
|
33
33
|
|
package/dist/docs/index.json
CHANGED
|
@@ -77,6 +77,11 @@
|
|
|
77
77
|
"title": "Agent tools (MCP)",
|
|
78
78
|
"summary": "Let AI agents like Claude Code and Cursor use your project's backend as tools — as the person running them."
|
|
79
79
|
},
|
|
80
|
+
{
|
|
81
|
+
"slug": "agent-browsing",
|
|
82
|
+
"title": "Let your agent browse a project",
|
|
83
|
+
"summary": "Give an AI agent's browser a short sign-in to one project, so it can look at and click through your app as you."
|
|
84
|
+
},
|
|
80
85
|
{
|
|
81
86
|
"slug": "porting-nextjs",
|
|
82
87
|
"title": "Porting a Next.js app",
|
|
@@ -311,6 +316,11 @@
|
|
|
311
316
|
"title": "canopy open",
|
|
312
317
|
"summary": "Open the deployed project in your browser."
|
|
313
318
|
},
|
|
319
|
+
{
|
|
320
|
+
"slug": "cli-browse",
|
|
321
|
+
"title": "canopy browse",
|
|
322
|
+
"summary": "Print a one-time sign-in link that lets an agent's browser open a project as you."
|
|
323
|
+
},
|
|
314
324
|
{
|
|
315
325
|
"slug": "cli-docs",
|
|
316
326
|
"title": "canopy docs",
|
|
@@ -210,6 +210,14 @@ Disconnecting also tells Google the grant is over, so access stops immediately
|
|
|
210
210
|
rather than lingering. Visitors can drop their own connection with
|
|
211
211
|
`canopy.google.user().disconnect()`.
|
|
212
212
|
|
|
213
|
+
## Daily limit
|
|
214
|
+
|
|
215
|
+
Each project can make 10,000 Google calls a day. Calls Google turns down count
|
|
216
|
+
too. Once a project reaches its limit, further calls fail until the count starts
|
|
217
|
+
over at midnight UTC. The **Usage** tab shows today's count, the limit, and when
|
|
218
|
+
it resets. If your project needs more, ask the Canopy team; they can raise the
|
|
219
|
+
limit for your project.
|
|
220
|
+
|
|
213
221
|
## Local development
|
|
214
222
|
|
|
215
223
|
`canopy dev` uses the real connections, so Google works the same on your
|
|
@@ -141,6 +141,9 @@ the dashboard (or `canopy db access` from the terminal):
|
|
|
141
141
|
`collection("private_notes")` get a 403. Raw SQL cannot be checked table by
|
|
142
142
|
table, so while your database holds any `private_` name, `db.sql()` from a
|
|
143
143
|
page is refused too; use collections there, or run the SQL in the worker.
|
|
144
|
+
`canopy dev --local` applies the same rule, so you can test it on your
|
|
145
|
+
laptop. Plain `canopy dev` doesn't: your browser shares the dev server's
|
|
146
|
+
token there.
|
|
144
147
|
|
|
145
148
|
Everyone who opens the app gets the same rule — colleagues signed in with
|
|
146
149
|
OneLogin and, if you turned on [client access](https://docs.canopy.website/platform/client-access),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-BOXNJrWP.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
4
|
import { r as reportErrors } from "./ui-C_jSc1Kz.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-BOXNJrWP.mjs";
|
|
2
2
|
import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
3
3
|
import { a as apiUpload, i as apiRequest, r as apiDownload } from "./api-C4rMhE7i.mjs";
|
|
4
4
|
import { r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as version } from "./package-
|
|
2
|
+
import { t as version } from "./package-BOXNJrWP.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";
|
|
@@ -20,6 +20,7 @@ import { defineCommand, runMain } from "citty";
|
|
|
20
20
|
* ai models the site's AI models and auto/* aliases
|
|
21
21
|
* mcp add|serve a project's tools for AI agents (docs/49)
|
|
22
22
|
* open open the deployed project / dashboard [phase 1]
|
|
23
|
+
* browse one-time sign-in link for an agent's browser
|
|
23
24
|
*/
|
|
24
25
|
if (process.env["CANOPY_UPDATE_REFRESH"] === "1") {
|
|
25
26
|
await runRefresh();
|
|
@@ -69,7 +70,7 @@ const main = defineCommand({
|
|
|
69
70
|
init: () => import("./projects-DQAhCZm8.mjs").then((m) => m.initCommand),
|
|
70
71
|
projects: () => import("./projects-DQAhCZm8.mjs").then((m) => m.projectsCommand),
|
|
71
72
|
delete: () => import("./projects-DQAhCZm8.mjs").then((m) => m.deleteCommand),
|
|
72
|
-
dev: () => import("./dev-
|
|
73
|
+
dev: () => import("./dev-CID2LCES.mjs").then((m) => m.devCommand),
|
|
73
74
|
deploy: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.deployCommand),
|
|
74
75
|
promote: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.promoteCommand),
|
|
75
76
|
staging: () => import("./staging-BZVty57b.mjs").then((m) => m.stagingCommand),
|
|
@@ -79,16 +80,17 @@ const main = defineCommand({
|
|
|
79
80
|
egress: () => import("./egress-If_Gsx_M.mjs").then((m) => m.egressCommand),
|
|
80
81
|
secrets: () => import("./secrets-BSEjZnxg.mjs").then((m) => m.secretsCommand),
|
|
81
82
|
schedule: () => import("./schedule-C5lMf3ny.mjs").then((m) => m.scheduleCommand),
|
|
82
|
-
feedback: () => import("./feedback-
|
|
83
|
+
feedback: () => import("./feedback-CrmDfqmZ.mjs").then((m) => m.feedbackCommand),
|
|
83
84
|
db: () => import("./db-BB6O6085.mjs").then((m) => m.dbCommand),
|
|
84
85
|
usage: () => import("./usage-CUYrWxv0.mjs").then((m) => m.usageCommand),
|
|
85
86
|
ai: () => import("./ai-DHLnyqfq.mjs").then((m) => m.aiCommand),
|
|
86
87
|
open: () => import("./open-DcaQKi1v.mjs").then((m) => m.openCommand),
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
browse: () => import("./browse-Dycwj0V_.mjs").then((m) => m.browseCommand),
|
|
89
|
+
docs: () => import("./docs-CGrXLl7G.mjs").then((m) => m.docsCommand),
|
|
90
|
+
skill: () => import("./skill-GPJrYL61.mjs").then((m) => m.skillCommand),
|
|
91
|
+
upgrade: () => import("./upgrade-DSTg2cgu.mjs").then((m) => m.upgradeCommand),
|
|
90
92
|
logs: () => import("./logs-DQQOawOG.mjs").then((m) => m.logsCommand),
|
|
91
|
-
mcp: () => import("./mcp-
|
|
93
|
+
mcp: () => import("./mcp-DAaO4SQc.mjs").then((m) => m.mcpCommand),
|
|
92
94
|
repo: () => import("./repo-1RQ1vBMv.mjs").then((m) => m.repoCommand)
|
|
93
95
|
}
|
|
94
96
|
});
|
|
@@ -1,10 +1,54 @@
|
|
|
1
1
|
import { n as apiDataRequest } from "./api-C4rMhE7i.mjs";
|
|
2
|
-
import { a as
|
|
3
|
-
import { i as rewriteApiPath } from "./dev-
|
|
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-CxzGOEhP.mjs";
|
|
3
|
+
import { i as rewriteApiPath } from "./dev-oS4nYMSn.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";
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
+
//#region ../data-plane/src/db-access.ts
|
|
9
|
+
/** The worker-only marker: `private_` at the start of a collection or table name. */
|
|
10
|
+
const PRIVATE_PREFIX = "private_";
|
|
11
|
+
const isPrivateName = (name) => name.startsWith(PRIVATE_PREFIX);
|
|
12
|
+
const CHANGE_HINT = "Change it under Data in the dashboard, or move this call into the worker.";
|
|
13
|
+
/** The policy matrix. Only meaningful for callers where isBrowserOnProject() is true. */
|
|
14
|
+
function decideBrowserDbAccess(level, op) {
|
|
15
|
+
if (level === "off") return {
|
|
16
|
+
allow: false,
|
|
17
|
+
error: "browser_db_access",
|
|
18
|
+
detail: `Browser access to this project's database is off. ${CHANGE_HINT}`
|
|
19
|
+
};
|
|
20
|
+
if ((op.kind === "read" || op.kind === "write") && isPrivateName(op.collection)) return {
|
|
21
|
+
allow: false,
|
|
22
|
+
error: "private_collection",
|
|
23
|
+
detail: `"${op.collection}" is a private_ collection — only the project's worker can use it.`
|
|
24
|
+
};
|
|
25
|
+
if (op.kind === "sql") {
|
|
26
|
+
if (op.privateTables) return {
|
|
27
|
+
allow: false,
|
|
28
|
+
error: "private_tables_present",
|
|
29
|
+
detail: "This project has private_ tables or collections, so raw SQL is not available from the browser. Use collections here, or run the SQL in the worker."
|
|
30
|
+
};
|
|
31
|
+
if (level === "readonly") {
|
|
32
|
+
if (op.readonly === false) return {
|
|
33
|
+
allow: false,
|
|
34
|
+
error: "browser_db_access",
|
|
35
|
+
detail: `Browser access to this project's database is read-only. ${CHANGE_HINT}`
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
allow: true,
|
|
39
|
+
forceReadonly: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { allow: true };
|
|
43
|
+
}
|
|
44
|
+
if (op.kind === "write" && level === "readonly") return {
|
|
45
|
+
allow: false,
|
|
46
|
+
error: "browser_db_access",
|
|
47
|
+
detail: `Browser access to this project's database is read-only. ${CHANGE_HINT}`
|
|
48
|
+
};
|
|
49
|
+
return { allow: true };
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
8
52
|
//#region ../data-plane/src/dialect-postgres.ts
|
|
9
53
|
/**
|
|
10
54
|
* Postgres doc-store dialect (Cloud SQL, PGlite). Same table shape as SQLite
|
|
@@ -778,6 +822,7 @@ async function createPgliteDb(persistDir) {
|
|
|
778
822
|
const tables = (await db.query(`SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename`)).rows;
|
|
779
823
|
for (const { tablename } of tables) await db.exec(`DROP TABLE IF EXISTS "${tablename.replaceAll("\"", "\"\"")}" CASCADE`);
|
|
780
824
|
},
|
|
825
|
+
withoutForeignKeys: (fn) => fn(),
|
|
781
826
|
close: () => db.close()
|
|
782
827
|
};
|
|
783
828
|
}
|
|
@@ -811,6 +856,14 @@ async function createSqliteDb(persistDir) {
|
|
|
811
856
|
for (const name of listTables(`sql LIKE 'CREATE VIRTUAL TABLE%'`)) db.exec(`DROP TABLE IF EXISTS "${name.replaceAll("\"", "\"\"")}"`);
|
|
812
857
|
for (const name of listTables(`name NOT LIKE 'sqlite_%'`)) db.exec(`DROP TABLE IF EXISTS "${name.replaceAll("\"", "\"\"")}"`);
|
|
813
858
|
},
|
|
859
|
+
withoutForeignKeys: async (fn) => {
|
|
860
|
+
db.exec("PRAGMA foreign_keys = OFF");
|
|
861
|
+
try {
|
|
862
|
+
return await fn();
|
|
863
|
+
} finally {
|
|
864
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
865
|
+
}
|
|
866
|
+
},
|
|
814
867
|
close: async () => {
|
|
815
868
|
db.close();
|
|
816
869
|
}
|
|
@@ -827,10 +880,25 @@ async function createSandbox(options) {
|
|
|
827
880
|
const requested = c.req.header("x-canopy-project") ?? c.req.query("project");
|
|
828
881
|
return requested === void 0 || requested === options.slug ? null : c.json({ error: "project_not_found" }, 404);
|
|
829
882
|
};
|
|
830
|
-
app.route("/data/v1", dbRouter({
|
|
831
|
-
store
|
|
832
|
-
|
|
833
|
-
|
|
883
|
+
app.route("/data/v1", dbRouter({
|
|
884
|
+
store: async (c) => ownProject(c) ?? {
|
|
885
|
+
store,
|
|
886
|
+
dialect
|
|
887
|
+
},
|
|
888
|
+
guard: async (c, op, resolved) => {
|
|
889
|
+
const attest = c.req.header(WORKER_ATTEST_HEADER);
|
|
890
|
+
if (options.workerAttest && attest === options.workerAttest) return void 0;
|
|
891
|
+
const decision = decideBrowserDbAccess("full", op.kind === "sql" ? {
|
|
892
|
+
kind: "sql",
|
|
893
|
+
readonly: op.readonly,
|
|
894
|
+
privateTables: await hasPrivateNames(resolved.store, resolved.dialect.name, PRIVATE_PREFIX)
|
|
895
|
+
} : op);
|
|
896
|
+
return decision.allow ? void 0 : c.json({
|
|
897
|
+
error: decision.error,
|
|
898
|
+
detail: decision.detail
|
|
899
|
+
}, 403);
|
|
900
|
+
}
|
|
901
|
+
}));
|
|
834
902
|
app.route("/data/v1", blobRouter({
|
|
835
903
|
store: async (c) => ownProject(c) ?? {
|
|
836
904
|
slug: options.slug,
|
|
@@ -900,15 +968,17 @@ async function createSandbox(options) {
|
|
|
900
968
|
return app.fetch(new Request(`http://sandbox${pathname}${url.search}`, request));
|
|
901
969
|
}
|
|
902
970
|
async function applyDump(sql) {
|
|
903
|
-
await db.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
971
|
+
await db.withoutForeignKeys(async () => {
|
|
972
|
+
await db.reset();
|
|
973
|
+
for (const statement of splitSqlStatements(sql)) {
|
|
974
|
+
if (/^(BEGIN|COMMIT|ROLLBACK|SET\s+search_path|PRAGMA)\b/i.test(statement)) continue;
|
|
975
|
+
try {
|
|
976
|
+
await db.exec(statement);
|
|
977
|
+
} catch (err) {
|
|
978
|
+
throw new Error(`Dump statement failed: ${err instanceof Error ? err.message : String(err)}\n in: ${statement.slice(0, 120)}${statement.length > 120 ? "…" : ""}`);
|
|
979
|
+
}
|
|
910
980
|
}
|
|
911
|
-
}
|
|
981
|
+
});
|
|
912
982
|
await rebuildFtsIndex(store, dialect);
|
|
913
983
|
}
|
|
914
984
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-BOXNJrWP.mjs";
|
|
2
2
|
import { d as tryResolveProject, l as stagingUrl, n as siteConfig, s as projectUrl } from "./site-DM2J10CB.mjs";
|
|
3
3
|
import { i as apiRequest, t as ApiError } from "./api-C4rMhE7i.mjs";
|
|
4
4
|
import { t as liveDevServer } from "./dev-server-file-Dk9pk4AR.mjs";
|
|
@@ -97,6 +97,12 @@ and never go stale.
|
|
|
97
97
|
(own database cloned from live; `canopy staging status | sync | destroy`)
|
|
98
98
|
- Roll back: `canopy rollback`
|
|
99
99
|
- Open the live site: `canopy open`
|
|
100
|
+
- **Look at a deployed project in your own browser** (Playwright, a browser
|
|
101
|
+
tool, curl): `canopy browse <slug> --json` prints a one-time sign-in link
|
|
102
|
+
(`--staging`, `--path /page`). Open it within a minute; that browser is
|
|
103
|
+
then signed in to that one project, as the user, for an hour. On a sign-in
|
|
104
|
+
page later, run it again. Never ask the user for their session cookie
|
|
105
|
+
(`canopy docs agent-browsing`).
|
|
100
106
|
- Manage data: `canopy db pull | snapshot | snapshots | restore | migrate`
|
|
101
107
|
- Use AI (`canopy.ai`): run `canopy ai models` before naming a model in code.
|
|
102
108
|
Prefer the aliases `auto/low` (default) / `auto/mid` / `auto/high` over
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-BOXNJrWP.mjs";
|
|
2
2
|
import { r as reportErrors } from "./ui-C_jSc1Kz.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
@@ -292,6 +292,21 @@ async function introspectSchema(store, dialect) {
|
|
|
292
292
|
tables
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Does the database hold any table or doc collection whose name starts with
|
|
297
|
+
* `prefix`? Two cheap probes — the browser-SQL guard (canopy-api, docs/35)
|
|
298
|
+
* asks this on every page-originated raw-SQL call, so it must not walk the
|
|
299
|
+
* schema. `prefix` is interpolated as a LIKE pattern; callers pass a fixed
|
|
300
|
+
* identifier fragment (`private_`), never user input.
|
|
301
|
+
*/
|
|
302
|
+
async function hasPrivateNames(store, dialect, prefix) {
|
|
303
|
+
const pattern = lit(`${prefix.replaceAll("_", "\\_")}%`);
|
|
304
|
+
const [table, collection] = await Promise.all([store.query(dialect === "postgres" ? `SELECT 1 AS found FROM information_schema.tables
|
|
305
|
+
WHERE table_schema = current_schema() AND table_type = 'BASE TABLE'
|
|
306
|
+
AND table_name LIKE ${pattern} ESCAPE '\\' LIMIT 1` : `SELECT 1 AS found FROM sqlite_master
|
|
307
|
+
WHERE type = 'table' AND name LIKE ${pattern} ESCAPE '\\' LIMIT 1`).then((r) => r.results.length > 0), store.query(`SELECT 1 AS found FROM _canopy_docs WHERE collection LIKE ${pattern} ESCAPE '\\' LIMIT 1`).then((r) => r.results.length > 0).catch(() => false)]);
|
|
308
|
+
return table || collection;
|
|
309
|
+
}
|
|
295
310
|
//#endregion
|
|
296
311
|
//#region ../data-plane/src/db.ts
|
|
297
312
|
/**
|
|
@@ -844,6 +859,12 @@ const IDENTITY_STAMP_HEADERS = [
|
|
|
844
859
|
"x-canopy-user-name"
|
|
845
860
|
];
|
|
846
861
|
/**
|
|
862
|
+
* Header carrying the project's worker token as runtime-origin proof (docs/35).
|
|
863
|
+
* The data plane verifies it to exempt the worker from browser DB access even
|
|
864
|
+
* when the worker is acting as a visitor (forwarding their cookie).
|
|
865
|
+
*/
|
|
866
|
+
const WORKER_ATTEST_HEADER = "x-canopy-worker-attest";
|
|
867
|
+
/**
|
|
847
868
|
* Header the gateway attaches to a project tools call (`/__canopy/mcp`,
|
|
848
869
|
* docs/49): the caller's agent token. A credential like the session cookie —
|
|
849
870
|
* the host keeps it for `env.CANOPY.fetch` and redacts it from the bundle.
|
|
@@ -857,7 +878,11 @@ const AGENT_CREDENTIAL_HEADER = "x-canopy-agent-credential";
|
|
|
857
878
|
*/
|
|
858
879
|
const CALLER_HEADER = "x-canopy-caller";
|
|
859
880
|
/** Cookies that authenticate to Canopy itself — the monk session and the client-page session. */
|
|
860
|
-
const CANOPY_COOKIE_NAMES = /* @__PURE__ */ new Set([
|
|
881
|
+
const CANOPY_COOKIE_NAMES = /* @__PURE__ */ new Set([
|
|
882
|
+
"canopy_session",
|
|
883
|
+
"canopy_client",
|
|
884
|
+
"canopy_agent"
|
|
885
|
+
]);
|
|
861
886
|
/**
|
|
862
887
|
* Strip Canopy's own credentials from a request before untrusted project code
|
|
863
888
|
* sees it (R-01).
|
|
@@ -870,7 +895,7 @@ const CANOPY_COOKIE_NAMES = /* @__PURE__ */ new Set(["canopy_session", "canopy_c
|
|
|
870
895
|
*
|
|
871
896
|
* Redaction is surgical on purpose: an app's own cookies and its own
|
|
872
897
|
* Authorization scheme survive, because those belong to the app. Only
|
|
873
|
-
* `canopy_session` / `canopy_client` and a `cnpy_` bearer are removed.
|
|
898
|
+
* `canopy_session` / `canopy_client` / `canopy_agent` and a `cnpy_` bearer are removed.
|
|
874
899
|
*
|
|
875
900
|
* `env.CANOPY.fetch` still authenticates as the visitor — the host passes it
|
|
876
901
|
* the unredacted headers — so nothing about the platform-facing behaviour
|
|
@@ -903,4 +928,4 @@ function stampWorkerHeaders(headers, ctx) {
|
|
|
903
928
|
return headers;
|
|
904
929
|
}
|
|
905
930
|
//#endregion
|
|
906
|
-
export {
|
|
931
|
+
export { realtimeRouter as a, META_COLUMNS as c, stampWorkerHeaders as i, blobRouter as l, WORKER_ATTEST_HEADER as n, dbRouter as o, redactCanopyCredentials as r, hasPrivateNames as s, CALLER_HEADER as t };
|
|
@@ -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-GPJrYL61.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());
|
package/package.json
CHANGED
package/dist/dev-MoHHjQh1.mjs
DELETED