@mediamonks/canopy-cli 0.7.22 → 0.7.23
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/dev-BFPlhkzp.mjs +2 -0
- package/dist/{dev-CNqx_53I.mjs → dev-XULukupE.mjs} +3 -3
- package/dist/{dev-worker-Bij9OUMk.mjs → dev-worker-BDMjExps.mjs} +35 -1
- package/dist/docs/cli-login.md +2 -0
- package/dist/docs/cli-logout.md +9 -6
- package/dist/docs/index.json +7 -2
- package/dist/docs/platform-ai-usage.md +36 -10
- package/dist/docs/platform-client-access.md +5 -2
- package/dist/docs/platform-members-and-access.md +29 -6
- package/dist/docs/platform-security-headers.md +53 -0
- package/dist/docs/platform-slack.md +3 -1
- package/dist/docs/sdk-ai.md +102 -11
- package/dist/docs/sdk-files.md +6 -0
- package/dist/{docs-c0CdeBxh.mjs → docs-BDPL0TI3.mjs} +1 -1
- package/dist/{feedback-QhD8k6Ph.mjs → feedback-B3pnUPLf.mjs} +1 -1
- package/dist/index.mjs +9 -9
- package/dist/{local-CsSP4Vpt.mjs → local-BjsuI1IB.mjs} +37 -8
- package/dist/{login-BrEDKrb3.mjs → login-By4y5O5-.mjs} +55 -4
- package/dist/{package-BuwV_mje.mjs → package-C5rZwlw8.mjs} +1 -1
- package/dist/{skill-BvoYCIKk.mjs → skill-CgToW_Ac.mjs} +1 -1
- package/dist/{src-BKKAEkbH.mjs → src-BPKfY9IN.mjs} +53 -1
- package/dist/{upgrade-0lmdjvom.mjs → upgrade-Bl_NJ_m5.mjs} +1 -1
- package/package.json +3 -3
- package/dist/dev-D7v7WK9O.mjs +0 -2
|
@@ -37,7 +37,7 @@ async function workerPlugin(flags, slug) {
|
|
|
37
37
|
console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
|
|
38
38
|
return;
|
|
39
39
|
}
|
|
40
|
-
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-
|
|
40
|
+
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-BDMjExps.mjs");
|
|
41
41
|
const secrets = readDevVars(flags.dir);
|
|
42
42
|
const email = siteConfig().email;
|
|
43
43
|
const plugin = workerDevPlugin({
|
|
@@ -165,7 +165,7 @@ async function devRemote(flags, slug) {
|
|
|
165
165
|
async function devLocal(flags, slug) {
|
|
166
166
|
let dump;
|
|
167
167
|
if (flags.pull) {
|
|
168
|
-
const { fetchProjectDump } = await import("./local-
|
|
168
|
+
const { fetchProjectDump } = await import("./local-BjsuI1IB.mjs");
|
|
169
169
|
console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
|
|
170
170
|
dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
|
|
171
171
|
}
|
|
@@ -180,7 +180,7 @@ async function devLocal(flags, slug) {
|
|
|
180
180
|
const reason = err instanceof ApiError ? err.explain().why : String(err);
|
|
181
181
|
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
182
182
|
}
|
|
183
|
-
const { createSandbox } = await import("./local-
|
|
183
|
+
const { createSandbox } = await import("./local-BjsuI1IB.mjs");
|
|
184
184
|
const sandbox = await createSandbox({
|
|
185
185
|
slug,
|
|
186
186
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./src-
|
|
1
|
+
import "./src-BPKfY9IN.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
4
|
//#region ../data-plane/src/worker-request.ts
|
|
@@ -25,6 +25,39 @@ const IDENTITY_STAMP_HEADERS = [
|
|
|
25
25
|
"x-canopy-user-email",
|
|
26
26
|
"x-canopy-user-name"
|
|
27
27
|
];
|
|
28
|
+
/** Cookies that authenticate to Canopy itself — the monk session and the client-page session. */
|
|
29
|
+
const CANOPY_COOKIE_NAMES = /* @__PURE__ */ new Set(["canopy_session", "canopy_client"]);
|
|
30
|
+
/**
|
|
31
|
+
* Strip Canopy's own credentials from a request before untrusted project code
|
|
32
|
+
* sees it (R-01).
|
|
33
|
+
*
|
|
34
|
+
* The visitor's session cookie arrives with every request the gateway
|
|
35
|
+
* dispatches, and a bundle that can read it can replay it against the platform
|
|
36
|
+
* as that person — against the control plane, and against other projects' data
|
|
37
|
+
* on the api host. It is a credential, so the host keeps it in closure scope
|
|
38
|
+
* like the raw DB and blob handles, and hands the bundle a redacted request.
|
|
39
|
+
*
|
|
40
|
+
* Redaction is surgical on purpose: an app's own cookies and its own
|
|
41
|
+
* Authorization scheme survive, because those belong to the app. Only
|
|
42
|
+
* `canopy_session` / `canopy_client` and a `cnpy_` bearer are removed.
|
|
43
|
+
*
|
|
44
|
+
* `env.CANOPY.fetch` still authenticates as the visitor — the host passes it
|
|
45
|
+
* the unredacted headers — so nothing about the platform-facing behaviour
|
|
46
|
+
* changes; the credential simply stops being reachable from bundle code.
|
|
47
|
+
*/
|
|
48
|
+
function redactCanopyCredentials(headers) {
|
|
49
|
+
const cookie = headers.get("cookie");
|
|
50
|
+
if (cookie !== null) {
|
|
51
|
+
const kept = cookie.split(";").filter((part) => {
|
|
52
|
+
const name = part.split("=")[0]?.trim() ?? "";
|
|
53
|
+
return name !== "" && !CANOPY_COOKIE_NAMES.has(name);
|
|
54
|
+
}).join(";").trim();
|
|
55
|
+
if (kept === "") headers.delete("cookie");
|
|
56
|
+
else headers.set("cookie", kept);
|
|
57
|
+
}
|
|
58
|
+
if (headers.get("authorization")?.startsWith("Bearer cnpy_")) headers.delete("authorization");
|
|
59
|
+
return headers;
|
|
60
|
+
}
|
|
28
61
|
/**
|
|
29
62
|
* Strip what must never travel upstream and stamp who is calling. Common to
|
|
30
63
|
* every host; says nothing about credentials.
|
|
@@ -331,6 +364,7 @@ function workerDevPlugin(options) {
|
|
|
331
364
|
}));
|
|
332
365
|
return;
|
|
333
366
|
}
|
|
367
|
+
redactCanopyCredentials(request.headers);
|
|
334
368
|
await sendResponse(res, await worker.fetch(request, env));
|
|
335
369
|
})().catch((err) => {
|
|
336
370
|
if (err instanceof Error) server.ssrFixStacktrace(err);
|
package/dist/docs/cli-login.md
CHANGED
|
@@ -24,6 +24,8 @@ canopy login --json # machine-readable status for agents/CI
|
|
|
24
24
|
2. You finish signing in **in the browser** — your password never passes through the CLI. When OneLogin hands control back, the browser shows a small "You're signed in" page and you can return to the terminal. The sign-in waits up to 5 minutes; the hand-back uses local port `8976`, so if another program holds that port the login fails with a clear message.
|
|
25
25
|
3. The CLI stores a token for **every** site (`main`, `staging`, `gcp-pilot`), not just the one you picked, in `~/.canopy/config.json` — readable only by your user account.
|
|
26
26
|
|
|
27
|
+
Each token is labelled with your machine's hostname and expires after 30 days. The dashboard's **Account** page (your avatar → Account) lists every machine you have signed in from and lets you revoke any of them; `canopy logout` revokes this machine's tokens for you.
|
|
28
|
+
|
|
27
29
|
One login therefore covers all sites; `--profile` only decides which one commands target by default. If minting fails for a non-active site you get a warning but login still succeeds; if it fails for your **active** profile the command errors and asks you to re-run.
|
|
28
30
|
|
|
29
31
|
## Examples
|
package/dist/docs/cli-logout.md
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: canopy logout
|
|
3
|
-
description: Sign out of Canopy
|
|
3
|
+
description: Sign out of Canopy — revoke your CLI tokens on every site and forget them locally.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
`canopy logout` signs you out on this machine
|
|
6
|
+
`canopy logout` signs you out on this machine: it revokes the CLI token stored for every site on the platform, then forgets them in `~/.canopy/config.json`. Use it on a shared machine, or when you want a clean slate before logging in as someone else.
|
|
7
7
|
|
|
8
8
|
## Usage
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
canopy logout
|
|
12
|
+
canopy logout --local # forget the tokens here, but leave them valid on the platform
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
## Arguments & flags
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
- `--local` — only remove the tokens from this machine; nothing is called on the server. The tokens stay valid until they expire (30 days after login), and you can still revoke them from the dashboard's **Account** page.
|
|
18
|
+
|
|
19
|
+
Logout is deliberately all-or-nothing: it signs out of **every** site (`main`, `staging`, `gcp-pilot`) at once — there is no per-profile logout, so the global `--profile` flag has no effect here.
|
|
17
20
|
|
|
18
21
|
## Details
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
For each site with a stored token, the command asks the platform to revoke that token, so it stops working everywhere immediately — not just on this machine. Then the file is cleaned up. If a site cannot be reached, the local copy is still removed and the command tells you which tokens it could not revoke; those keep working until they expire, or until you revoke them from the dashboard's **Account** page (your avatar → Account), which lists every machine you have signed in from.
|
|
21
24
|
|
|
22
|
-
If no token was stored to begin with, it prints `Not logged in.` and exits successfully.
|
|
25
|
+
Your active-site choice stays in the file, so the next `canopy login` picks up where you left off. If no token was stored to begin with, it prints `Not logged in.` and exits successfully.
|
|
23
26
|
|
|
24
27
|
## Examples
|
|
25
28
|
|
|
@@ -27,6 +30,6 @@ If no token was stored to begin with, it prints `Not logged in.` and exits succe
|
|
|
27
30
|
canopy logout
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
Revokes all site tokens on the platform and removes them from `~/.canopy/config.json`; subsequent commands that need the API will ask you to run `canopy login`.
|
|
31
34
|
|
|
32
35
|
See also [canopy login](https://docs.canopy.website/cli/login) and [canopy whoami](https://docs.canopy.website/cli/whoami).
|
package/dist/docs/index.json
CHANGED
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
{
|
|
118
118
|
"slug": "platform-ai-usage",
|
|
119
119
|
"title": "AI access & usage",
|
|
120
|
-
"summary": "Projects talk to AI models through the platform — no API keys to manage,
|
|
120
|
+
"summary": "Projects talk to AI models through the platform — no API keys to manage, hard per-project budgets, and full visibility of spend."
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
"slug": "platform-google",
|
|
@@ -154,6 +154,11 @@
|
|
|
154
154
|
"title": "Client access",
|
|
155
155
|
"summary": "Share a project with clients who have no Monks account — a shared password unlocks that one project's URL and nothing else."
|
|
156
156
|
},
|
|
157
|
+
{
|
|
158
|
+
"slug": "platform-security-headers",
|
|
159
|
+
"title": "Browser security",
|
|
160
|
+
"summary": "The security headers the platform adds to your app's pages and files, what they block, and the two switches in Settings."
|
|
161
|
+
},
|
|
157
162
|
{
|
|
158
163
|
"slug": "platform-sites-and-profiles",
|
|
159
164
|
"title": "Sites & profiles",
|
|
@@ -184,7 +189,7 @@
|
|
|
184
189
|
{
|
|
185
190
|
"slug": "cli-logout",
|
|
186
191
|
"title": "canopy logout",
|
|
187
|
-
"summary": "Sign out of Canopy
|
|
192
|
+
"summary": "Sign out of Canopy — revoke your CLI tokens on every site and forget them locally."
|
|
188
193
|
},
|
|
189
194
|
{
|
|
190
195
|
"slug": "cli-create",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: AI access & usage
|
|
3
|
-
description: Projects talk to AI models through the platform — no API keys to manage,
|
|
3
|
+
description: Projects talk to AI models through the platform — no API keys to manage, hard per-project budgets, and full visibility of spend.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Canopy projects can call AI models without you ever handling an API key. The
|
|
7
7
|
platform routes every request through its own metered gateway, tracks what
|
|
8
|
-
each project spends, and enforces
|
|
8
|
+
each project spends, and enforces hard budgets — so a runaway loop in
|
|
9
9
|
a vibecoded app becomes a capped bill and a visible chart, not a surprise
|
|
10
10
|
invoice.
|
|
11
11
|
|
|
@@ -26,9 +26,26 @@ const reply = await canopy.ai.chat({
|
|
|
26
26
|
default — ask for a model outside the list and the API tells you what _is_
|
|
27
27
|
available.
|
|
28
28
|
- **Streaming** via `canopy.ai.chatStream()` for chat-style UIs.
|
|
29
|
+
- **Documents and images.** Messages can attach files — reference something
|
|
30
|
+
in the project's [blob storage](https://docs.canopy.website/sdk/blob) by key, or inline base64 — see
|
|
31
|
+
[the AI SDK page](https://docs.canopy.website/sdk/ai). Models that read PDFs and images get the real
|
|
32
|
+
document, not a text extraction.
|
|
29
33
|
- **An OpenAI-compatible endpoint.** `canopy.ai.endpoint()` returns a base
|
|
30
34
|
URL and model list you can plug into ecosystem tools (like the Vercel AI
|
|
31
35
|
SDK) — same key handling, same budget, same tracking.
|
|
36
|
+
- **Provider-native endpoints.** For provider-specific capabilities —
|
|
37
|
+
Anthropic prompt caching and PDF citations, OpenAI's Responses API — point
|
|
38
|
+
an official SDK at the native base URLs from `canopy.ai.endpoint()`. Same
|
|
39
|
+
budget and tracking as everything else; see [the AI SDK page](https://docs.canopy.website/sdk/ai).
|
|
40
|
+
|
|
41
|
+
## Request size
|
|
42
|
+
|
|
43
|
+
A single chat request — prompt, inline base64 attachments, and referenced
|
|
44
|
+
blobs together — can be up to **8 MB**. Base64 makes files about a third
|
|
45
|
+
larger than they are on disk, so that fits roughly a 6 MB document per
|
|
46
|
+
request. Bigger requests answer with a `chat_too_large` error (HTTP 413)
|
|
47
|
+
before anything is sent to a model. Providers also have their own per-file
|
|
48
|
+
limits, which are typically above this.
|
|
32
49
|
|
|
33
50
|
## Your data and the model providers
|
|
34
51
|
|
|
@@ -41,16 +58,25 @@ This applies to every way your project reaches AI — `canopy.ai.chat()`,
|
|
|
41
58
|
streaming, and the OpenAI-compatible endpoint all go through the same gateway
|
|
42
59
|
to the same accounts.
|
|
43
60
|
|
|
44
|
-
##
|
|
61
|
+
## Budgets: a daily limit and a monthly cap
|
|
62
|
+
|
|
63
|
+
Every project has two AI budgets in USD, enforced from the same spend ledger
|
|
64
|
+
the dashboard displays — so what you see and what's enforced can never
|
|
65
|
+
disagree:
|
|
66
|
+
|
|
67
|
+
- **A daily limit** (default $5) catches runaway loops. Cross it and AI
|
|
68
|
+
calls answer `ai_budget_exceeded` until midnight UTC; everything resets
|
|
69
|
+
the next day.
|
|
70
|
+
- **A monthly cap** (default $20) is the real spending ceiling. Cross it and
|
|
71
|
+
AI calls pause until the calendar month rolls over.
|
|
45
72
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
the
|
|
50
|
-
disagree.
|
|
73
|
+
The error names which one tripped. The Usage tab shows spend against both.
|
|
74
|
+
If a project genuinely needs more headroom, a Canopy admin can raise either
|
|
75
|
+
budget from that same tab — ask in the project's feedback or reach out to
|
|
76
|
+
the platform team.
|
|
51
77
|
|
|
52
|
-
> **Note:** Hitting
|
|
53
|
-
> calls pause until the
|
|
78
|
+
> **Note:** Hitting a budget doesn't break the rest of your app — pages, data, and files keep working. Only AI
|
|
79
|
+
> calls pause until the window rolls over.
|
|
54
80
|
|
|
55
81
|
## The Usage tab
|
|
56
82
|
|
|
@@ -36,8 +36,11 @@ it** card:
|
|
|
36
36
|
2. Send the client the project URL and the password.
|
|
37
37
|
|
|
38
38
|
To change the password, set a new one — previously handed-out passwords stop
|
|
39
|
-
working
|
|
40
|
-
|
|
39
|
+
working, and every browser that unlocked the project with the old one is
|
|
40
|
+
signed out on its next visit and asked for the new password. To turn client
|
|
41
|
+
access off entirely, remove the password from the same card, or switch the
|
|
42
|
+
mode back to Monks only; either way, visitors are back to Monks-only
|
|
43
|
+
immediately, unlocked browsers included.
|
|
41
44
|
|
|
42
45
|
> **Warning:** A shared password is deliberately lightweight protection — right for work-in-progress previews,
|
|
43
46
|
> wrong for anything sensitive. Anyone who has it can view the app and share it further, so rotate
|
|
@@ -61,11 +61,34 @@ Turning it off again is the same toggle.
|
|
|
61
61
|
> **Note:** Collaborators-only gates the **live app**. Management access (settings, data, deploys) is always
|
|
62
62
|
> members-only, regardless of this toggle.
|
|
63
63
|
|
|
64
|
-
## Platform admins and the
|
|
65
|
-
|
|
66
|
-
Platform admins get an extra **
|
|
67
|
-
on Canopy — name, email,
|
|
68
|
-
to grant or remove admin
|
|
69
|
-
|
|
64
|
+
## Platform admins and the Admin page
|
|
65
|
+
|
|
66
|
+
Platform admins get an extra **Admin** page in the dashboard. Its **Users**
|
|
67
|
+
tab lists everyone on Canopy — name, email, when they first and last signed
|
|
68
|
+
in — with a one-click way to grant or remove admin and to sign someone out
|
|
69
|
+
everywhere. Its **Sessions** tab lists every live CLI sign-in on the site:
|
|
70
|
+
who, from which machine, and when it was last used, each revocable on the
|
|
71
|
+
spot. Accounts are created automatically on first OneLogin sign-in; there is
|
|
72
|
+
nothing to provision.
|
|
73
|
+
|
|
74
|
+
## Your sign-ins, and signing out everywhere
|
|
75
|
+
|
|
76
|
+
A Canopy sign-in lives in two places: the browser session from OneLogin, and
|
|
77
|
+
the CLI tokens `canopy login` stores on each machine (one per site). Your
|
|
78
|
+
**Account** page — open the avatar menu in the dashboard header — lists the
|
|
79
|
+
machines you have signed in from, with when each token was last used, and lets
|
|
80
|
+
you revoke any of them. Revoking a token is what `canopy logout` does for the
|
|
81
|
+
machine it runs on.
|
|
82
|
+
|
|
83
|
+
**Sign out everywhere** on the same page ends every browser session and every
|
|
84
|
+
CLI sign-in of yours on that site at once, this one included. Reach for it
|
|
85
|
+
when a laptop goes missing or you signed in somewhere you shouldn't have; the
|
|
86
|
+
next request from any of those places meets the sign-in page. Deploy tokens
|
|
87
|
+
used by CI belong to their projects, not to you, and keep working — revoke
|
|
88
|
+
those from the project's Deploys tab if they were on the lost machine too.
|
|
89
|
+
|
|
90
|
+
Platform admins have the same button for any user on the **Admin** page, which
|
|
91
|
+
is how someone who leaves the company is signed out of Canopy before their
|
|
92
|
+
open sessions would have expired on their own.
|
|
70
93
|
|
|
71
94
|
- [Client access](https://docs.canopy.website/platform/client-access) — Share work-in-progress with clients who don't have a Monks account.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Browser security
|
|
3
|
+
description: The security headers the platform adds to your app's pages and files, what they block, and the two switches in Settings.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Every project on Canopy shares one sign-in, so a script that manages to run
|
|
7
|
+
inside one app is a script running next to every other app a visitor can open.
|
|
8
|
+
To keep that from mattering, the platform adds a small set of browser security
|
|
9
|
+
headers to the pages and files it serves for you. You don't configure anything
|
|
10
|
+
to get them, and most projects never notice they're there.
|
|
11
|
+
|
|
12
|
+
## What your pages get
|
|
13
|
+
|
|
14
|
+
- **A Content-Security-Policy.** Scripts may load from your own app and from
|
|
15
|
+
any `https://` address, so CDNs and hosted libraries keep working. Inline
|
|
16
|
+
`<script>` blocks that are part of your deployed HTML keep working too — the
|
|
17
|
+
platform reads them at serve time and allows exactly those. What's blocked:
|
|
18
|
+
inline event handlers such as `onclick="…"`, `javascript:` links, plugins
|
|
19
|
+
(`<object>`, `<embed>`), scripts loaded over plain `http://`, and inline
|
|
20
|
+
scripts that were _not_ in your deploy — which is what an injected script
|
|
21
|
+
usually is.
|
|
22
|
+
- **No embedding by other sites.** Your pages can only be shown in an iframe by
|
|
23
|
+
your own app.
|
|
24
|
+
- **A referrer policy and HTTPS pinning.** Other sites see your origin but not
|
|
25
|
+
your full URLs, and browsers remember to always use HTTPS.
|
|
26
|
+
|
|
27
|
+
## The two switches
|
|
28
|
+
|
|
29
|
+
Both live on the project's **Settings → Security** page, and both take effect
|
|
30
|
+
on the next page load — no redeploy.
|
|
31
|
+
|
|
32
|
+
- **Content-Security-Policy** — on by default. Turn it off if your app depends
|
|
33
|
+
on something the policy blocks and you can't change it: an inline `onclick`,
|
|
34
|
+
a `javascript:` link, a library that writes `<script>` tags into the page at
|
|
35
|
+
runtime, or a backend route that renders HTML with its own inline scripts.
|
|
36
|
+
Check the browser console first — a blocked script shows up there with the
|
|
37
|
+
directive that stopped it.
|
|
38
|
+
- **Allow embedding in other sites** — off by default. Turn it on when another
|
|
39
|
+
site needs to show this app in an iframe.
|
|
40
|
+
|
|
41
|
+
Backend-rendered pages (a worker route that returns HTML) receive the framing
|
|
42
|
+
and referrer headers but no script policy, because the platform only hashes
|
|
43
|
+
inline scripts it knows came from a deploy.
|
|
44
|
+
|
|
45
|
+
## Files you store
|
|
46
|
+
|
|
47
|
+
Files saved with `canopy.blob` are served from your app's address, with the
|
|
48
|
+
content type you gave them. Images, video, audio, fonts, plain text, CSV, JSON
|
|
49
|
+
and PDF display normally. SVGs display too, but sandboxed — fine in an
|
|
50
|
+
`<img>`, and harmless if someone opens the URL directly. Anything a browser
|
|
51
|
+
would treat as a page — HTML, XML, scripts, or files with an unknown type —
|
|
52
|
+
downloads instead of rendering. If you want to show user-uploaded HTML, render
|
|
53
|
+
it inside your own page rather than linking to the stored file.
|
|
@@ -56,7 +56,9 @@ own screen. Click **Install to workspace** and follow it through.
|
|
|
56
56
|
|
|
57
57
|
If your workspace requires admin approval for new apps, the first attempt
|
|
58
58
|
becomes an approval request instead. Once the admin approves (Slack DMs you
|
|
59
|
-
about it), come back and click install again
|
|
59
|
+
about it), either come back and click install again, or use the install
|
|
60
|
+
link Slack itself offers — both complete the same way. A Slack-side install
|
|
61
|
+
only works into the workspace the app already belongs to.
|
|
60
62
|
|
|
61
63
|
The same button reappears whenever you change features in a way that needs
|
|
62
64
|
new permissions — the card shows a clear warning when saved changes are
|
package/dist/docs/sdk-ai.md
CHANGED
|
@@ -4,8 +4,8 @@ description: Metered LLM access through the project's budget-capped gateway key
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
`canopy.ai` gives your project LLM access without any API keys in your code.
|
|
7
|
-
Every request authenticates as the project and is metered against
|
|
8
|
-
per-project
|
|
7
|
+
Every request authenticates as the project and is metered against hard
|
|
8
|
+
per-project budgets — spend, limits, and how prompts are handled by the
|
|
9
9
|
model providers are covered in [AI access & usage](https://docs.canopy.website/platform/ai-usage).
|
|
10
10
|
|
|
11
11
|
## ai.chat(options)
|
|
@@ -32,16 +32,65 @@ res.usage; // { inputTokens: number, outputTokens: number }
|
|
|
32
32
|
**`ChatOptions`:**
|
|
33
33
|
|
|
34
34
|
- `messages` — required, non-empty array of `{ role, content }`. `role` is
|
|
35
|
-
`"system" | "user" | "assistant"`; `content` is a non-empty string
|
|
35
|
+
`"system" | "user" | "assistant"`; `content` is a non-empty string, or an
|
|
36
|
+
array of content parts to attach documents and images (below).
|
|
36
37
|
- `model` — optional `"{provider}/{model}"` id. Omit it and the platform's
|
|
37
38
|
default model answers. Ask for a model whose provider isn't enabled and the
|
|
38
39
|
call fails with a message listing what _is_ available; `ai.endpoint()`
|
|
39
40
|
returns the full curated list.
|
|
40
|
-
- `temperature` — optional number between 0 and 2.
|
|
41
|
-
-
|
|
41
|
+
- `temperature` — optional number between 0 and 2. Reasoning models (the
|
|
42
|
+
GPT-5 family) only accept their default temperature, so the platform drops
|
|
43
|
+
this setting for them rather than failing the call — it has effect on
|
|
44
|
+
Claude models.
|
|
45
|
+
- `maxTokens` — optional integer, at least 1. On reasoning models the budget
|
|
46
|
+
also covers the model's internal reasoning, so a very small value can
|
|
47
|
+
return an empty answer — leave headroom.
|
|
48
|
+
- `effort` — optional `"low" | "medium" | "high"`: how hard a reasoning model
|
|
49
|
+
should think. Lowered onto each provider's own dial; models without an
|
|
50
|
+
effort dial (Claude Haiku, for one) simply ignore it. Lower effort is
|
|
51
|
+
faster and cheaper, higher is more thorough.
|
|
52
|
+
|
|
53
|
+
**`ChatResponse`:** `{ message, model, usage: { inputTokens, outputTokens } }`.
|
|
54
|
+
`message.role` is `"assistant"` and `message.content` is always a plain
|
|
55
|
+
string, whatever went in.
|
|
56
|
+
|
|
57
|
+
## Attaching documents and images
|
|
58
|
+
|
|
59
|
+
A message's `content` can be an array of parts instead of a string. The
|
|
60
|
+
easiest way to send a file is to upload it to [blob storage](https://docs.canopy.website/sdk/blob) once
|
|
61
|
+
and reference it by key — the platform fetches it server-side and picks the
|
|
62
|
+
best way to hand it to the model:
|
|
42
63
|
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
```ts
|
|
65
|
+
await canopy.blob.put("briefs/q3.pdf", file); // once
|
|
66
|
+
|
|
67
|
+
const res = await canopy.ai.chat({
|
|
68
|
+
messages: [
|
|
69
|
+
{
|
|
70
|
+
role: "user",
|
|
71
|
+
content: [
|
|
72
|
+
{ type: "text", text: "Summarize this briefing." },
|
|
73
|
+
{ type: "blob", key: "briefs/q3.pdf" },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The part types:
|
|
81
|
+
|
|
82
|
+
- `{ type: "text", text }` — plain text.
|
|
83
|
+
- `{ type: "blob", key, mediaType? }` — a file already in the project's blob
|
|
84
|
+
storage, by key. `mediaType` overrides the stored content type if needed.
|
|
85
|
+
**Prefer this** for anything you'd otherwise base64 by hand.
|
|
86
|
+
- `{ type: "file", data, mediaType, filename? }` — an inline file;
|
|
87
|
+
`data` is base64 (no `data:` prefix), `mediaType` e.g. `"application/pdf"`.
|
|
88
|
+
- `{ type: "image", data, mediaType }` — an inline image, same encoding.
|
|
89
|
+
|
|
90
|
+
Pick a model that can read what you attach — document and image understanding
|
|
91
|
+
varies per model. The whole request (inline base64 and referenced blobs
|
|
92
|
+
together) must stay under the 8 MB request limit — see
|
|
93
|
+
[AI access & usage](https://docs.canopy.website/platform/ai-usage).
|
|
45
94
|
|
|
46
95
|
## ai.chatStream(options)
|
|
47
96
|
|
|
@@ -70,9 +119,10 @@ let an ecosystem client handle streaming for you.
|
|
|
70
119
|
Returns the project's OpenAI-compatible endpoint info:
|
|
71
120
|
|
|
72
121
|
```ts
|
|
73
|
-
const { baseUrl, models } = await canopy.ai.endpoint();
|
|
122
|
+
const { baseUrl, models, native } = await canopy.ai.endpoint();
|
|
74
123
|
// baseUrl — an OpenAI-compatible base URL
|
|
75
124
|
// models — the curated model list, "{provider}/{model}" ids; models[0] is the default
|
|
125
|
+
// native — provider-native base URLs: { anthropic, openai }
|
|
76
126
|
```
|
|
77
127
|
|
|
78
128
|
Point any OpenAI-compatible client — the Vercel AI SDK, an OpenAI SDK, a
|
|
@@ -87,15 +137,56 @@ const openai = createOpenAI({ baseURL: baseUrl, apiKey: "unused" });
|
|
|
87
137
|
const model = openai(models[0]);
|
|
88
138
|
```
|
|
89
139
|
|
|
140
|
+
## Provider-native endpoints
|
|
141
|
+
|
|
142
|
+
The compat endpoint speaks OpenAI's chat-completions dialect to every model —
|
|
143
|
+
portable, but the lowest common denominator. When you want a provider's own
|
|
144
|
+
protocol — Anthropic prompt caching, PDFs with citations, or OpenAI's
|
|
145
|
+
Responses API — use the native base URLs from `endpoint()`. Requests pass
|
|
146
|
+
through to the provider unchanged; auth, the model allowlist, the daily
|
|
147
|
+
budget, and metering all still apply.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
151
|
+
|
|
152
|
+
const { native } = await canopy.ai.endpoint();
|
|
153
|
+
const anthropic = new Anthropic({ baseURL: native.anthropic, apiKey: "unused" });
|
|
154
|
+
const msg = await anthropic.messages.create({
|
|
155
|
+
model: "claude-haiku-4-5", // bare or "anthropic/…" — must be on the curated list
|
|
156
|
+
max_tokens: 1024,
|
|
157
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A base URL is just a prefix — the client library appends the endpoint path
|
|
162
|
+
itself, based on which method you call. Both values point at the platform,
|
|
163
|
+
which forwards each path to the provider:
|
|
164
|
+
|
|
165
|
+
- `native.anthropic` — hand it to `@anthropic-ai/sdk` or `@ai-sdk/anthropic`.
|
|
166
|
+
Calling `anthropic.messages.create(...)` makes the SDK request
|
|
167
|
+
`POST {native.anthropic}/v1/messages` — the Messages API.
|
|
168
|
+
- `native.openai` — hand it to the `openai` package or `@ai-sdk/openai`. It
|
|
169
|
+
already ends in `/v1`, matching OpenAI's own base URL convention; the
|
|
170
|
+
method you call picks the endpoint: `client.responses.create(...)` requests
|
|
171
|
+
`POST {native.openai}/responses` (the Responses API) and
|
|
172
|
+
`client.chat.completions.create(...)` requests
|
|
173
|
+
`POST {native.openai}/chat/completions`. Both are supported.
|
|
174
|
+
|
|
175
|
+
Pick the surface by what you need: `ai.chat()` for plain text (and simple
|
|
176
|
+
attachments), the compat endpoint for portable multi-model apps and ecosystem
|
|
177
|
+
tooling, a native endpoint for provider-specific capabilities. Moving between
|
|
178
|
+
them never changes auth, budgets, or what the Usage tab shows.
|
|
179
|
+
|
|
90
180
|
## Errors
|
|
91
181
|
|
|
92
182
|
Failed calls throw `CanopyError` with the HTTP `status` and the response body
|
|
93
183
|
as the `message`. Two worth handling:
|
|
94
184
|
|
|
95
185
|
- **Budget exhausted** — status `429`, body contains
|
|
96
|
-
`"ai_budget_exceeded"
|
|
97
|
-
the next day
|
|
98
|
-
your app keeps
|
|
186
|
+
`"ai_budget_exceeded"` and names which budget tripped: the daily limit
|
|
187
|
+
(calls resume the next day) or the monthly cap (calls resume next month —
|
|
188
|
+
or after a maintainer raises it). Everything else in your app keeps
|
|
189
|
+
working.
|
|
99
190
|
- **Invalid request** — status `400`, body contains `"invalid_chat"` and a
|
|
100
191
|
detail explaining which option was rejected.
|
|
101
192
|
|
package/dist/docs/sdk-files.md
CHANGED
|
@@ -62,6 +62,12 @@ lifetime that isn't a whole number of seconds above zero is rejected. Keep the
|
|
|
62
62
|
window as short as the job needs — anyone holding the URL can download the
|
|
63
63
|
file until it expires.
|
|
64
64
|
|
|
65
|
+
What a URL does in the browser depends on the file's content type. Images,
|
|
66
|
+
video, audio, fonts, plain text, CSV, JSON and PDF display; SVG displays in
|
|
67
|
+
an `<img>` but is sandboxed when opened directly; anything a browser would
|
|
68
|
+
treat as a page — HTML, XML, scripts, unknown types — downloads instead of
|
|
69
|
+
rendering. See [Browser security](https://docs.canopy.website/platform/security-headers).
|
|
70
|
+
|
|
65
71
|
## Listing and deleting
|
|
66
72
|
|
|
67
73
|
```ts
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-C5rZwlw8.mjs";
|
|
2
2
|
import { n as siteConfig } from "./site-BFaWfSVu.mjs";
|
|
3
3
|
import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
|
|
4
4
|
import { r as reportErrors } from "./ui--atbZ-TM.mjs";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-C5rZwlw8.mjs";
|
|
2
2
|
import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BFaWfSVu.mjs";
|
|
3
3
|
import { c as apiDownload, l as apiRequest, r as reportErrors, t as formatDateTime, u as apiUpload } from "./ui--atbZ-TM.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
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-C5rZwlw8.mjs";
|
|
3
3
|
import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BFaWfSVu.mjs";
|
|
4
4
|
import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
@@ -60,14 +60,14 @@ const main = defineCommand({
|
|
|
60
60
|
description: "canopy — create and deploy vibecoded projects"
|
|
61
61
|
},
|
|
62
62
|
subCommands: {
|
|
63
|
-
login: () => import("./login-
|
|
64
|
-
whoami: () => import("./login-
|
|
65
|
-
logout: () => import("./login-
|
|
63
|
+
login: () => import("./login-By4y5O5-.mjs").then((m) => m.loginCommand),
|
|
64
|
+
whoami: () => import("./login-By4y5O5-.mjs").then((m) => m.whoamiCommand),
|
|
65
|
+
logout: () => import("./login-By4y5O5-.mjs").then((m) => m.logoutCommand),
|
|
66
66
|
create: () => import("./projects-1r5387ui.mjs").then((m) => m.createCommand),
|
|
67
67
|
init: () => import("./projects-1r5387ui.mjs").then((m) => m.initCommand),
|
|
68
68
|
projects: () => import("./projects-1r5387ui.mjs").then((m) => m.projectsCommand),
|
|
69
69
|
delete: () => import("./projects-1r5387ui.mjs").then((m) => m.deleteCommand),
|
|
70
|
-
dev: () => import("./dev-
|
|
70
|
+
dev: () => import("./dev-BFPlhkzp.mjs").then((m) => m.devCommand),
|
|
71
71
|
deploy: () => import("./deploy-DCggsLD7.mjs").then((m) => m.deployCommand),
|
|
72
72
|
promote: () => import("./deploy-DCggsLD7.mjs").then((m) => m.promoteCommand),
|
|
73
73
|
staging: () => import("./staging-CdpV2YHB.mjs").then((m) => m.stagingCommand),
|
|
@@ -76,13 +76,13 @@ const main = defineCommand({
|
|
|
76
76
|
token: () => import("./token-C8Bmzwfl.mjs").then((m) => m.tokenCommand),
|
|
77
77
|
egress: () => import("./egress-BhbQfjjV.mjs").then((m) => m.egressCommand),
|
|
78
78
|
schedule: () => import("./schedule-DK_Cv6Xf.mjs").then((m) => m.scheduleCommand),
|
|
79
|
-
feedback: () => import("./feedback-
|
|
79
|
+
feedback: () => import("./feedback-B3pnUPLf.mjs").then((m) => m.feedbackCommand),
|
|
80
80
|
db: () => import("./db-TaSLpQZ5.mjs").then((m) => m.dbCommand),
|
|
81
81
|
usage: () => import("./usage-C1-cvS2-.mjs").then((m) => m.usageCommand),
|
|
82
82
|
open: () => import("./open-D7f1wNn3.mjs").then((m) => m.openCommand),
|
|
83
|
-
docs: () => import("./docs-
|
|
84
|
-
skill: () => import("./skill-
|
|
85
|
-
upgrade: () => import("./upgrade-
|
|
83
|
+
docs: () => import("./docs-BDPL0TI3.mjs").then((m) => m.docsCommand),
|
|
84
|
+
skill: () => import("./skill-CgToW_Ac.mjs").then((m) => m.skillCommand),
|
|
85
|
+
upgrade: () => import("./upgrade-Bl_NJ_m5.mjs").then((m) => m.upgradeCommand),
|
|
86
86
|
logs: () => import("./logs-Beqz--Df.mjs").then((m) => m.logsCommand)
|
|
87
87
|
}
|
|
88
88
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { s as apiDataRequest } from "./ui--atbZ-TM.mjs";
|
|
2
|
-
import { n as META_COLUMNS, r as blobRouter, t as dbRouter } from "./src-
|
|
3
|
-
import { n as rewriteApiPath } from "./dev-
|
|
2
|
+
import { n as META_COLUMNS, r as blobRouter, t as dbRouter } from "./src-BPKfY9IN.mjs";
|
|
3
|
+
import { n as rewriteApiPath } from "./dev-XULukupE.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";
|
|
@@ -15,19 +15,48 @@ import { Hono } from "hono";
|
|
|
15
15
|
* - `fts: false` — search() is an ILIKE substring fallback: different
|
|
16
16
|
* matches, no ranking (results order by created_at). Surfaced to clients
|
|
17
17
|
* as `capabilities.fts` rather than papered over.
|
|
18
|
-
* - patch()
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* - patch() has the SAME RFC 7386 semantics as SQLite's json_patch (nested
|
|
19
|
+
* objects merge, null removes a key), but Postgres has no built-in for it,
|
|
20
|
+
* so it goes through `public._canopy_merge_patch(jsonb, jsonb)` — a small
|
|
21
|
+
* recursive plpgsql function declared in the DDL below. It lives in
|
|
22
|
+
* `public` (not the per-project schema) so one CREATE OR REPLACE covers
|
|
23
|
+
* every project schema in the database, existing ones included.
|
|
21
24
|
*/
|
|
22
25
|
const DOC_COLUMNS$1 = "id, data, created_at, updated_at";
|
|
23
|
-
const DOC_STORE_DDL_PG = [
|
|
26
|
+
const DOC_STORE_DDL_PG = [
|
|
27
|
+
`CREATE TABLE IF NOT EXISTS _canopy_docs (
|
|
24
28
|
collection TEXT NOT NULL,
|
|
25
29
|
id TEXT NOT NULL,
|
|
26
30
|
data TEXT NOT NULL,
|
|
27
31
|
created_at TEXT NOT NULL,
|
|
28
32
|
updated_at TEXT NOT NULL,
|
|
29
33
|
PRIMARY KEY (collection, id)
|
|
30
|
-
)`,
|
|
34
|
+
)`,
|
|
35
|
+
`CREATE INDEX IF NOT EXISTS _canopy_docs_created ON _canopy_docs (collection, created_at)`,
|
|
36
|
+
`CREATE OR REPLACE FUNCTION public._canopy_merge_patch(target jsonb, patch jsonb)
|
|
37
|
+
RETURNS jsonb LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
38
|
+
BEGIN
|
|
39
|
+
IF jsonb_typeof(patch) IS DISTINCT FROM 'object' THEN
|
|
40
|
+
RETURN patch;
|
|
41
|
+
END IF;
|
|
42
|
+
IF jsonb_typeof(target) IS DISTINCT FROM 'object' THEN
|
|
43
|
+
target := '{}'::jsonb;
|
|
44
|
+
END IF;
|
|
45
|
+
RETURN COALESCE((
|
|
46
|
+
SELECT jsonb_object_agg(m.key, m.value) FROM (
|
|
47
|
+
SELECT t.key, t.value FROM jsonb_each(target) t WHERE NOT (patch ? t.key)
|
|
48
|
+
UNION ALL
|
|
49
|
+
SELECT p.key,
|
|
50
|
+
CASE WHEN jsonb_typeof(p.value) = 'object'
|
|
51
|
+
THEN public._canopy_merge_patch(target -> p.key, p.value)
|
|
52
|
+
ELSE p.value END
|
|
53
|
+
FROM jsonb_each(patch) p
|
|
54
|
+
WHERE jsonb_typeof(p.value) <> 'null'
|
|
55
|
+
) m
|
|
56
|
+
), '{}'::jsonb);
|
|
57
|
+
END
|
|
58
|
+
$fn$`
|
|
59
|
+
];
|
|
31
60
|
/** `data`'s JSON value at a `$.a.b` path param, as jsonb. */
|
|
32
61
|
const jsonAt = (column, pathParam) => `(${column}::jsonb #> string_to_array(trim(leading '$.' from ${pathParam}::text), '.'))`;
|
|
33
62
|
/**
|
|
@@ -124,7 +153,7 @@ const postgresDialect = {
|
|
|
124
153
|
},
|
|
125
154
|
patch(collection, id, patchJson, now) {
|
|
126
155
|
return {
|
|
127
|
-
sql: `UPDATE _canopy_docs SET data = (data::jsonb
|
|
156
|
+
sql: `UPDATE _canopy_docs SET data = public._canopy_merge_patch(data::jsonb, $1::jsonb)::text, updated_at = $2
|
|
128
157
|
WHERE collection = $3 AND id = $4
|
|
129
158
|
RETURNING ${DOC_COLUMNS$1}`,
|
|
130
159
|
params: [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { _ as writeConfigFile, f as clearAllProfileTokens, g as upsertProfileAuth, h as setActiveProfile, m as readConfigFile, n as siteConfig, p as listBakedProfiles, v as getBakedProfile, y as isProfileId } from "./site-BFaWfSVu.mjs";
|
|
2
2
|
import { n as openBrowser, r as reportErrors } from "./ui--atbZ-TM.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
|
+
import { hostname } from "node:os";
|
|
4
5
|
import * as p from "@clack/prompts";
|
|
5
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
7
|
import { createServer } from "node:http";
|
|
@@ -47,19 +48,66 @@ const whoamiCommand = defineCommand({
|
|
|
47
48
|
}
|
|
48
49
|
})
|
|
49
50
|
});
|
|
51
|
+
/**
|
|
52
|
+
* Revoke one site's CLI token on the server (DELETE /control/v1/account/token —
|
|
53
|
+
* the token authenticates and names itself). Best-effort: an unreachable or
|
|
54
|
+
* already-expired token is still forgotten locally, the caller just hears
|
|
55
|
+
* about it. A 401 means the platform no longer knows the token — nothing left
|
|
56
|
+
* to revoke, so that counts as done.
|
|
57
|
+
*/
|
|
58
|
+
async function revokeRemote(apiUrl, token) {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${apiUrl}/control/v1/account/token`, {
|
|
61
|
+
method: "DELETE",
|
|
62
|
+
headers: { authorization: `Bearer ${token}` }
|
|
63
|
+
});
|
|
64
|
+
if (res.ok || res.status === 401) return { ok: true };
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
error: `${res.status} ${await res.text().catch(() => "")}`.trim()
|
|
68
|
+
};
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: err instanceof Error ? err.message : String(err)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
50
76
|
const logoutCommand = defineCommand({
|
|
51
77
|
meta: {
|
|
52
78
|
name: "logout",
|
|
53
|
-
description: "Sign out: forget all
|
|
79
|
+
description: "Sign out: revoke and forget all CLI tokens"
|
|
54
80
|
},
|
|
55
|
-
|
|
81
|
+
args: { local: {
|
|
82
|
+
type: "boolean",
|
|
83
|
+
default: false,
|
|
84
|
+
description: "Only forget the tokens on this machine; do not revoke them on the platform"
|
|
85
|
+
} },
|
|
86
|
+
run: reportErrors("logout", async ({ args }) => {
|
|
56
87
|
const file = readConfigFile();
|
|
57
|
-
|
|
88
|
+
const held = Object.entries(file.profiles ?? {}).filter(([, p]) => p?.token);
|
|
89
|
+
if (held.length === 0) {
|
|
58
90
|
console.log("Not logged in.");
|
|
59
91
|
return;
|
|
60
92
|
}
|
|
93
|
+
const failed = [];
|
|
94
|
+
if (!args.local) for (const [id, prof] of held) {
|
|
95
|
+
const baked = isProfileId(id) ? getBakedProfile(id) : void 0;
|
|
96
|
+
if (!baked) continue;
|
|
97
|
+
const result = await revokeRemote(baked.apiUrl, prof.token);
|
|
98
|
+
if (!result.ok) failed.push(`${id} (${result.error})`);
|
|
99
|
+
}
|
|
61
100
|
clearAllProfileTokens();
|
|
101
|
+
if (args.local) {
|
|
102
|
+
console.log("Logged out locally — tokens removed from ~/.canopy/config.json but still valid on the platform until they expire. Revoke them from the dashboard's Account page.");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (failed.length === 0) {
|
|
106
|
+
console.log("Logged out — all site tokens revoked and removed from ~/.canopy/config.json.");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
62
109
|
console.log("Logged out — all site tokens removed from ~/.canopy/config.json.");
|
|
110
|
+
console.log(`Could not revoke on: ${failed.join(", ")}. Those tokens stay valid until they expire; revoke them from the dashboard's Account page.`);
|
|
63
111
|
})
|
|
64
112
|
});
|
|
65
113
|
/** Wait for OneLogin to redirect back to the local loopback server. */
|
|
@@ -125,7 +173,10 @@ async function mintForProfile(id, idToken) {
|
|
|
125
173
|
const cliTokenRes = await fetch(`${baked.apiUrl}/control/v1/auth/cli-token`, {
|
|
126
174
|
method: "POST",
|
|
127
175
|
headers: { "content-type": "application/json" },
|
|
128
|
-
body: JSON.stringify({
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
id_token: idToken,
|
|
178
|
+
name: hostname()
|
|
179
|
+
})
|
|
129
180
|
});
|
|
130
181
|
if (!cliTokenRes.ok) return {
|
|
131
182
|
ok: false,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-C5rZwlw8.mjs";
|
|
2
2
|
import { r as reportErrors } from "./ui--atbZ-TM.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
@@ -1,4 +1,54 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
//#region ../data-plane/src/blob-headers.ts
|
|
3
|
+
/**
|
|
4
|
+
* Blob (SDK file store) serving policy. Blobs carry a caller-chosen content
|
|
5
|
+
* type and are served on the project's own origin, so a stored `text/html` or
|
|
6
|
+
* `image/svg+xml` upload would be a stored-XSS page with the project's origin.
|
|
7
|
+
* Types browsers can render harmlessly stay inline; SVG stays inline but is
|
|
8
|
+
* sandboxed (an `<img>` never runs its scripts, and a direct navigation gets an
|
|
9
|
+
* opaque origin); everything else — HTML, XML, scripts, unknown binaries —
|
|
10
|
+
* downloads instead of rendering.
|
|
11
|
+
*/
|
|
12
|
+
const INLINE_SAFE_TYPES = /* @__PURE__ */ new Set([
|
|
13
|
+
"image/png",
|
|
14
|
+
"image/jpeg",
|
|
15
|
+
"image/gif",
|
|
16
|
+
"image/webp",
|
|
17
|
+
"image/avif",
|
|
18
|
+
"image/bmp",
|
|
19
|
+
"image/x-icon",
|
|
20
|
+
"image/vnd.microsoft.icon",
|
|
21
|
+
"text/plain",
|
|
22
|
+
"text/csv",
|
|
23
|
+
"application/json",
|
|
24
|
+
"application/pdf"
|
|
25
|
+
]);
|
|
26
|
+
const INLINE_SAFE_PREFIXES = [
|
|
27
|
+
"video/",
|
|
28
|
+
"audio/",
|
|
29
|
+
"font/"
|
|
30
|
+
];
|
|
31
|
+
const SANDBOX = "sandbox";
|
|
32
|
+
function blobServingHeaders(contentType, key) {
|
|
33
|
+
const type = contentType.split(";")[0].trim().toLowerCase();
|
|
34
|
+
const headers = {
|
|
35
|
+
"content-type": contentType,
|
|
36
|
+
"x-content-type-options": "nosniff"
|
|
37
|
+
};
|
|
38
|
+
if (type === "image/svg+xml") {
|
|
39
|
+
headers["content-security-policy"] = `default-src 'none'; style-src 'unsafe-inline'; ${SANDBOX}`;
|
|
40
|
+
return headers;
|
|
41
|
+
}
|
|
42
|
+
if (type === "application/pdf") {
|
|
43
|
+
headers["content-security-policy"] = SANDBOX;
|
|
44
|
+
return headers;
|
|
45
|
+
}
|
|
46
|
+
if (INLINE_SAFE_TYPES.has(type) || INLINE_SAFE_PREFIXES.some((p) => type.startsWith(p))) return headers;
|
|
47
|
+
headers["content-disposition"] = `attachment; filename="${(key.split("/").at(-1) ?? "download").replace(/["\r\n\\]/g, "_")}"`;
|
|
48
|
+
headers["content-security-policy"] = SANDBOX;
|
|
49
|
+
return headers;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
2
52
|
//#region ../data-plane/src/blob.ts
|
|
3
53
|
/**
|
|
4
54
|
* /blob — the SDK's file storage over an injectable BlobStore (a bucket
|
|
@@ -118,7 +168,7 @@ function blobRouter(config) {
|
|
|
118
168
|
const object = await store.bucket.get(store.prefix + key);
|
|
119
169
|
if (!object) return c.json({ error: "not_found" }, 404);
|
|
120
170
|
return new Response(object.body, { headers: {
|
|
121
|
-
|
|
171
|
+
...blobServingHeaders(object.httpMetadata?.contentType ?? "application/octet-stream", key),
|
|
122
172
|
"cache-control": "private, no-store"
|
|
123
173
|
} });
|
|
124
174
|
});
|
|
@@ -270,6 +320,8 @@ const MAX_SQL_ROWS = 1e3;
|
|
|
270
320
|
*/
|
|
271
321
|
function readOnlySqlViolation(query) {
|
|
272
322
|
const bare = query.replace(/--[^\n]*/g, " ").replace(/\/\*[\s\S]*?\*\//g, " ").replace(/'(?:[^']|'')*'/g, "''").replace(/"(?:[^"]|"")*"/g, "\"\"");
|
|
323
|
+
const semicolon = bare.indexOf(";");
|
|
324
|
+
if (semicolon !== -1 && bare.slice(semicolon + 1).trim() !== "") return "multiple statements are not allowed in read-only mode";
|
|
273
325
|
const first = /^\s*([A-Za-z]+)/.exec(bare)?.[1]?.toUpperCase();
|
|
274
326
|
if (first === void 0) return "empty statement";
|
|
275
327
|
if (![
|
|
@@ -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-CgToW_Ac.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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mediamonks/canopy-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.23",
|
|
4
4
|
"description": "Canopy CLI — create, develop and deploy Canopy projects.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"canopy": "./dist/index.mjs"
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"esbuild": "^0.28.1",
|
|
21
21
|
"hono": "^4.8.0",
|
|
22
22
|
"vite-server": "npm:vite@^7",
|
|
23
|
-
"@mediamonks/canopy
|
|
24
|
-
"@mediamonks/canopy": "0.0.
|
|
23
|
+
"@mediamonks/canopy": "0.0.7",
|
|
24
|
+
"@mediamonks/canopy-data-plane": "0.0.4"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.1",
|
package/dist/dev-D7v7WK9O.mjs
DELETED