@mediamonks/canopy-cli 0.7.10 → 0.7.12
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/{deploy-BKFICJT1.mjs → deploy-B1WrPq5W.mjs} +12 -2
- package/dist/{dev-DtoZrexp.mjs → dev-BnUYGQQm.mjs} +5 -4
- package/dist/dev-CySBxW7I.mjs +2 -0
- package/dist/{dev-worker-BZgtEM9V.mjs → dev-worker-IWnHvQ1f.mjs} +42 -7
- package/dist/{docs-Ck9xogEV.mjs → docs-DxB7NhZs.mjs} +1 -1
- package/dist/{feedback-BpiCvItH.mjs → feedback-B-GXUBPs.mjs} +1 -1
- package/dist/index.mjs +10 -10
- package/dist/{local-C5AhA4GM.mjs → local-BGRd_FMZ.mjs} +6 -469
- package/dist/{package-DRp7sI6i.mjs → package-CDLufecy.mjs} +1 -1
- package/dist/{skill-B2sDMAmv.mjs → skill-BOWfqwr2.mjs} +1 -1
- package/dist/src-Bu4Ze32I.mjs +470 -0
- package/dist/{upgrade-tOfSmAcc.mjs → upgrade-fhfrid_O.mjs} +1 -1
- package/package.json +3 -3
- package/dist/dev-BYxSHUH6.mjs +0 -2
- package/dist/docs/ci.md +0 -50
- package/dist/docs/cli-create.md +0 -46
- package/dist/docs/cli-db.md +0 -86
- package/dist/docs/cli-delete.md +0 -44
- package/dist/docs/cli-deploy.md +0 -58
- package/dist/docs/cli-dev.md +0 -82
- package/dist/docs/cli-docs.md +0 -47
- package/dist/docs/cli-download.md +0 -42
- package/dist/docs/cli-egress.md +0 -37
- package/dist/docs/cli-feedback.md +0 -75
- package/dist/docs/cli-index.md +0 -61
- package/dist/docs/cli-init.md +0 -46
- package/dist/docs/cli-login.md +0 -43
- package/dist/docs/cli-logout.md +0 -32
- package/dist/docs/cli-logs.md +0 -57
- package/dist/docs/cli-open.md +0 -41
- package/dist/docs/cli-projects.md +0 -43
- package/dist/docs/cli-promote.md +0 -44
- package/dist/docs/cli-rollback.md +0 -45
- package/dist/docs/cli-skill.md +0 -39
- package/dist/docs/cli-staging.md +0 -54
- package/dist/docs/cli-token.md +0 -47
- package/dist/docs/cli-upgrade.md +0 -43
- package/dist/docs/cli-usage.md +0 -43
- package/dist/docs/cli-whoami.md +0 -46
- package/dist/docs/cli.md +0 -86
- package/dist/docs/data.md +0 -85
- package/dist/docs/deploy.md +0 -81
- package/dist/docs/dev.md +0 -72
- package/dist/docs/developers-agents-and-ai.md +0 -95
- package/dist/docs/developers-architecture.md +0 -106
- package/dist/docs/getting-started.md +0 -108
- package/dist/docs/index.json +0 -331
- package/dist/docs/infrastructure.md +0 -406
- package/dist/docs/migrations.md +0 -183
- package/dist/docs/overview-faq.md +0 -105
- package/dist/docs/overview-how-it-works.md +0 -82
- package/dist/docs/overview-use-cases.md +0 -76
- package/dist/docs/overview-what-is-canopy.md +0 -75
- package/dist/docs/platform-ai-usage.md +0 -65
- package/dist/docs/platform-client-access.md +0 -54
- package/dist/docs/platform-deploys-and-rollbacks.md +0 -93
- package/dist/docs/platform-egress.md +0 -75
- package/dist/docs/platform-feedback.md +0 -114
- package/dist/docs/platform-members-and-access.md +0 -71
- package/dist/docs/platform-project-data.md +0 -76
- package/dist/docs/platform-schedules.md +0 -90
- package/dist/docs/platform-sites-and-profiles.md +0 -61
- package/dist/docs/porting-nextjs.md +0 -233
- package/dist/docs/sdk-ai.md +0 -103
- package/dist/docs/sdk-auth.md +0 -57
- package/dist/docs/sdk-database.md +0 -126
- package/dist/docs/sdk-files.md +0 -82
- package/dist/docs/sdk-services.md +0 -58
- package/dist/docs/sdk.md +0 -126
- package/dist/docs/staging.md +0 -106
- package/dist/docs/static-serving.md +0 -318
- package/dist/docs/troubleshooting.md +0 -102
- package/dist/skill/canopy/SKILL.md +0 -160
- package/dist/templates/spa/.github/workflows/canopy-deploy.yml +0 -34
- package/dist/templates/spa/AGENTS.md +0 -47
- package/dist/templates/spa/README.md +0 -14
- package/dist/templates/spa/_gitignore +0 -4
- package/dist/templates/spa/canopy.json +0 -5
- package/dist/templates/spa/index.html +0 -12
- package/dist/templates/spa/package.json +0 -21
- package/dist/templates/spa/src/app.tsx +0 -25
- package/dist/templates/spa/src/canopy.ts +0 -5
- package/dist/templates/spa/src/main.tsx +0 -10
- package/dist/templates/spa/src/style.css +0 -29
- package/dist/templates/spa/tsconfig.json +0 -16
- package/dist/templates/spa/vite.config.ts +0 -7
- package/dist/templates/static/AGENTS.md +0 -45
- package/dist/templates/static/README.md +0 -9
- package/dist/templates/static/_gitignore +0 -2
- package/dist/templates/static/canopy.json +0 -4
- package/dist/templates/static/index.html +0 -21
- package/dist/templates/static/main.js +0 -9
- package/dist/templates/static/style.css +0 -29
- package/dist/templates/worker/.dev.vars.example +0 -9
- package/dist/templates/worker/.github/workflows/canopy-deploy.yml +0 -34
- package/dist/templates/worker/AGENTS.md +0 -63
- package/dist/templates/worker/README.md +0 -17
- package/dist/templates/worker/_gitignore +0 -5
- package/dist/templates/worker/canopy.json +0 -6
- package/dist/templates/worker/index.html +0 -12
- package/dist/templates/worker/package.json +0 -22
- package/dist/templates/worker/src/app.tsx +0 -25
- package/dist/templates/worker/src/canopy.ts +0 -5
- package/dist/templates/worker/src/main.tsx +0 -10
- package/dist/templates/worker/src/style.css +0 -29
- package/dist/templates/worker/tsconfig.json +0 -16
- package/dist/templates/worker/vite.config.ts +0 -7
- package/dist/templates/worker/worker/index.ts +0 -52
package/dist/docs/sdk-ai.md
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: AI
|
|
3
|
-
description: Metered LLM access through the project's budget-capped gateway key — chat, streaming, and an OpenAI-compatible endpoint.
|
|
4
|
-
---
|
|
5
|
-
|
|
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 a hard
|
|
8
|
-
per-project daily budget — spend and limits are covered in
|
|
9
|
-
[AI access & usage](https://docs.canopy.website/platform/ai-usage).
|
|
10
|
-
|
|
11
|
-
## ai.chat(options)
|
|
12
|
-
|
|
13
|
-
One-shot chat completion. Takes a `ChatOptions` object, resolves to a
|
|
14
|
-
`ChatResponse`:
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
const res = await canopy.ai.chat({
|
|
18
|
-
messages: [
|
|
19
|
-
{ role: "system", content: "You are terse." },
|
|
20
|
-
{ role: "user", content: "Say hi in five words." },
|
|
21
|
-
],
|
|
22
|
-
model: "anthropic/claude-haiku-4-5", // optional
|
|
23
|
-
temperature: 0.7, // optional, 0–2
|
|
24
|
-
maxTokens: 256, // optional
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
res.message.content; // the assistant's reply
|
|
28
|
-
res.model; // the model that actually answered
|
|
29
|
-
res.usage; // { inputTokens: number, outputTokens: number }
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
**`ChatOptions`:**
|
|
33
|
-
|
|
34
|
-
- `messages` — required, non-empty array of `{ role, content }`. `role` is
|
|
35
|
-
`"system" | "user" | "assistant"`; `content` is a non-empty string.
|
|
36
|
-
- `model` — optional `"{provider}/{model}"` id. Omit it and the platform's
|
|
37
|
-
default model answers. Ask for a model whose provider isn't enabled and the
|
|
38
|
-
call fails with a message listing what _is_ available; `ai.endpoint()`
|
|
39
|
-
returns the full curated list.
|
|
40
|
-
- `temperature` — optional number between 0 and 2.
|
|
41
|
-
- `maxTokens` — optional integer, at least 1.
|
|
42
|
-
|
|
43
|
-
**`ChatResponse`:** `{ message: ChatMessage, model: string, usage: { inputTokens, outputTokens } }`.
|
|
44
|
-
`message.role` is `"assistant"`.
|
|
45
|
-
|
|
46
|
-
## ai.chatStream(options)
|
|
47
|
-
|
|
48
|
-
Same `ChatOptions`, but resolves to a `ReadableStream<Uint8Array>` of
|
|
49
|
-
server-sent events — the raw SSE bytes of the completion, for chat-style UIs
|
|
50
|
-
that render tokens as they arrive:
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
const stream = await canopy.ai.chatStream({
|
|
54
|
-
messages: [{ role: "user", content: "Count to five." }],
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const decoder = new TextDecoder();
|
|
58
|
-
let text = "";
|
|
59
|
-
for await (const chunk of stream) {
|
|
60
|
-
text += decoder.decode(chunk, { stream: true });
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Each SSE event is a `data: {...}` line carrying an OpenAI-style completion
|
|
65
|
-
chunk. If you'd rather not parse SSE yourself, use `ai.endpoint()` below and
|
|
66
|
-
let an ecosystem client handle streaming for you.
|
|
67
|
-
|
|
68
|
-
## ai.endpoint()
|
|
69
|
-
|
|
70
|
-
Returns the project's OpenAI-compatible endpoint info:
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
const { baseUrl, models } = await canopy.ai.endpoint();
|
|
74
|
-
// baseUrl — an OpenAI-compatible base URL
|
|
75
|
-
// models — the curated model list, "{provider}/{model}" ids; models[0] is the default
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Point any OpenAI-compatible client — the Vercel AI SDK, an OpenAI SDK, a
|
|
79
|
-
tool-calling loop — at `baseUrl`. It uses the same project key and counts
|
|
80
|
-
against the same budget as `ai.chat()`:
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
84
|
-
|
|
85
|
-
const { baseUrl, models } = await canopy.ai.endpoint();
|
|
86
|
-
const openai = createOpenAI({ baseURL: baseUrl, apiKey: "unused" });
|
|
87
|
-
const model = openai(models[0]);
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Errors
|
|
91
|
-
|
|
92
|
-
Failed calls throw `CanopyError` with the HTTP `status` and the response body
|
|
93
|
-
as the `message`. Two worth handling:
|
|
94
|
-
|
|
95
|
-
- **Budget exhausted** — status `429`, body contains
|
|
96
|
-
`"ai_budget_exceeded"`. The project hit its daily AI cap; AI calls resume
|
|
97
|
-
the next day (or after a maintainer raises the budget). Everything else in
|
|
98
|
-
your app keeps working.
|
|
99
|
-
- **Invalid request** — status `400`, body contains `"invalid_chat"` and a
|
|
100
|
-
detail explaining which option was rejected.
|
|
101
|
-
|
|
102
|
-
> **Note:** Budgets are enforced before every call, so a runaway loop becomes a capped bill — see [AI access
|
|
103
|
-
> & usage](https://docs.canopy.website/platform/ai-usage) for how spend is tracked and displayed.
|
package/dist/docs/sdk-auth.md
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Auth
|
|
3
|
-
description: Read who the platform signed in — you never build a login screen.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Authentication happens before your code runs: the platform gates every request
|
|
7
|
-
to your project, so by the time a page loads or a worker route fires, there is
|
|
8
|
-
always a signed-in user. You never build a login screen, store passwords, or
|
|
9
|
-
check tokens — see [Members and access](https://docs.canopy.website/platform/members-and-access) for
|
|
10
|
-
who gets in and how. `canopy.auth` only reports who the platform let in.
|
|
11
|
-
|
|
12
|
-
## auth.user()
|
|
13
|
-
|
|
14
|
-
Returns the authenticated user for the current session as a `CanopyUser`:
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
const user = await canopy.auth.user();
|
|
18
|
-
// { id: "…", email: "ada@monks.com", name: "Ada Lovelace", kind: "monk" }
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
| Field | Type | Meaning |
|
|
22
|
-
| ------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
-
| `id` | `string` | Stable subject from OneLogin — safe to key user data on. |
|
|
24
|
-
| `email` | `string` | The user's email address. |
|
|
25
|
-
| `name` | `string` | Display name. |
|
|
26
|
-
| `kind` | `"monk" \| "external"` | `"monk"` = signed in through OneLogin; `"external"` = a visitor who got in with the project's client password instead. |
|
|
27
|
-
|
|
28
|
-
`kind: "external"` sessions come from client-password access — clients viewing
|
|
29
|
-
a shared project without a Monks account. Branch on it when externals should
|
|
30
|
-
see a trimmed-down experience:
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
const user = await canopy.auth.user();
|
|
34
|
-
|
|
35
|
-
document.querySelector("h1")!.textContent = `Welcome, ${user.name}`;
|
|
36
|
-
|
|
37
|
-
if (user.kind === "external") {
|
|
38
|
-
// Client viewer: hide internal tooling, show the presentation view.
|
|
39
|
-
document.querySelector("#admin-panel")?.remove();
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
> **Warning:** Branching on `kind` in the browser is presentation, not security — anything your page can fetch, a
|
|
44
|
-
> visitor can fetch. The platform's access rules are what actually protect data.
|
|
45
|
-
|
|
46
|
-
## auth.logout()
|
|
47
|
-
|
|
48
|
-
Ends the platform session for the current visitor. Their next request hits the
|
|
49
|
-
login flow again:
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
await canopy.auth.logout();
|
|
53
|
-
location.reload(); // next load re-authenticates
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
This signs out the browser session only — it does not touch your `canopy login`
|
|
57
|
-
on the CLI.
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Database
|
|
3
|
-
description: Document-style collections on your project's private database, plus a raw SQL escape hatch.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Every project has its own private database. The paved road is `canopy.db` —
|
|
7
|
-
document-style collections with no tables to create and no migrations to run.
|
|
8
|
-
Insert a JSON document, get it back with an id and timestamps. For the rare
|
|
9
|
-
case where documents aren't enough, `db.sql()` runs raw SQL against the same
|
|
10
|
-
database.
|
|
11
|
-
|
|
12
|
-
## Collections
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
const todos = canopy.db.collection<Todo>("todos");
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
`db.collection<T>(name)` returns a typed `Collection<T>`. The name must be
|
|
19
|
-
lowercase: start with a letter, then letters, digits, and underscores only
|
|
20
|
-
(`^[a-z][a-z0-9_]*$`). Use `snake_case` for multi-word names — `blog_posts`,
|
|
21
|
-
not `blogPosts`. An invalid name throws immediately.
|
|
22
|
-
|
|
23
|
-
Pass an interface as `T` to type every call end to end:
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
interface Todo extends Record<string, unknown> {
|
|
27
|
-
title: string;
|
|
28
|
-
done: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const todos = canopy.db.collection<Todo>("todos");
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Every document you read back is a `Doc<T>` — your own fields plus three
|
|
35
|
-
platform-managed ones:
|
|
36
|
-
|
|
37
|
-
| Field | Type | Meaning |
|
|
38
|
-
| ------------ | -------- | ------------------------------- |
|
|
39
|
-
| `_id` | `string` | Stable document id |
|
|
40
|
-
| `_createdAt` | `string` | ISO timestamp, set on insert |
|
|
41
|
-
| `_updatedAt` | `string` | ISO timestamp, bumped on update |
|
|
42
|
-
|
|
43
|
-
## Writing documents
|
|
44
|
-
|
|
45
|
-
```ts
|
|
46
|
-
// insert(data: T) → Promise<Doc<T>>
|
|
47
|
-
const created = await todos.insert({ title: "try canopy", done: false });
|
|
48
|
-
created._id; // "…" — use this for get/update/delete
|
|
49
|
-
|
|
50
|
-
// update(id, patch: Partial<T>) → Promise<Doc<T>>
|
|
51
|
-
await todos.update(created._id, { done: true });
|
|
52
|
-
|
|
53
|
-
// delete(id) → Promise<void>
|
|
54
|
-
await todos.delete(created._id);
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
`update` is a merge-patch (RFC 7386): top-level and nested objects merge into
|
|
58
|
-
the existing document, and setting a key to `null` removes it. Fields you
|
|
59
|
-
leave out of the patch are untouched.
|
|
60
|
-
|
|
61
|
-
## Reading documents
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
// get(id) → Promise<Doc<T> | null> — null when the id doesn't exist
|
|
65
|
-
const one = await todos.get(created._id);
|
|
66
|
-
|
|
67
|
-
// find(options?) → Promise<Doc<T>[]>
|
|
68
|
-
const open = await todos.find({
|
|
69
|
-
where: { done: false },
|
|
70
|
-
orderBy: "_createdAt",
|
|
71
|
-
order: "desc",
|
|
72
|
-
limit: 5,
|
|
73
|
-
offset: 0,
|
|
74
|
-
});
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
`find` takes a `FindOptions` object; every field is optional (call
|
|
78
|
-
`find()` with no arguments to list everything):
|
|
79
|
-
|
|
80
|
-
| Option | Type | Meaning |
|
|
81
|
-
| --------- | ------------------------- | ---------------------------------------------------- |
|
|
82
|
-
| `where` | `Record<string, unknown>` | Filter on document fields, e.g. `{ status: "open" }` |
|
|
83
|
-
| `orderBy` | `string` | Field to sort on (your fields or `_createdAt` etc.) |
|
|
84
|
-
| `order` | `"asc" \| "desc"` | Sort direction |
|
|
85
|
-
| `limit` | `number` | Maximum documents to return |
|
|
86
|
-
| `offset` | `number` | Documents to skip (pagination) |
|
|
87
|
-
|
|
88
|
-
## Searching text
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
// search(text, options?) → Promise<Doc<T>[]>
|
|
92
|
-
const hits = await todos.search("canopy", { limit: 10 });
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
`search` matches `text` against the content of every document in the
|
|
96
|
-
collection. On SQLite sites it is real full-text search; on Postgres sites it
|
|
97
|
-
falls back to substring matching — same call, same results shape, slightly
|
|
98
|
-
different ranking. `options` accepts everything `FindOptions` does except
|
|
99
|
-
`where` (`orderBy`, `order`, `limit`, `offset`).
|
|
100
|
-
|
|
101
|
-
## Raw SQL
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
// dialect() → Promise<"sqlite" | "postgres">
|
|
105
|
-
const dialect = await canopy.db.dialect();
|
|
106
|
-
|
|
107
|
-
// sql<Row>(query, params?) → Promise<Row[]>
|
|
108
|
-
const rows =
|
|
109
|
-
dialect === "postgres"
|
|
110
|
-
? await canopy.db.sql("SELECT * FROM my_table WHERE id = $1", [42])
|
|
111
|
-
: await canopy.db.sql("SELECT * FROM my_table WHERE id = ?", [42]);
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
`db.sql()` runs against the same project database, but it is **not
|
|
115
|
-
portable**: the query passes through untranslated in the deployment's own
|
|
116
|
-
dialect. That means SQLite syntax on some sites and Postgres syntax on
|
|
117
|
-
others — including placeholders, `?` on SQLite versus `$1` on Postgres.
|
|
118
|
-
`db.dialect()` tells you which one you're talking to; the result is cached
|
|
119
|
-
per SDK instance, so calling it often is free.
|
|
120
|
-
|
|
121
|
-
> **Note:** Reach for collections first — they work identically everywhere your project can deploy. Use
|
|
122
|
-
> `db.sql()` only when you genuinely need SQL (aggregates, joins, bulk updates), and branch on
|
|
123
|
-
> `db.dialect()` whenever the syntax differs.
|
|
124
|
-
|
|
125
|
-
Where the data lives, snapshots, and how to inspect it from the dashboard are
|
|
126
|
-
covered in [Project data](https://docs.canopy.website/platform/project-data).
|
package/dist/docs/sdk-files.md
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Files
|
|
3
|
-
description: Upload, download, and list files on your project's private blob storage.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
`canopy.blob` stores files under your project's own isolated prefix. Nothing
|
|
7
|
-
is publicly accessible — every download goes through the platform with the
|
|
8
|
-
visitor's session, or through a short-lived signed URL you request explicitly.
|
|
9
|
-
|
|
10
|
-
## Uploading
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
// put(key, data, options?) → Promise<BlobObject>
|
|
14
|
-
await canopy.blob.put("notes.txt", "hello canopy", { contentType: "text/plain" });
|
|
15
|
-
await canopy.blob.put("avatars/arjan.png", file); // a File/Blob carries its own type
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
`data` can be a `Blob` (including a `File`), an `ArrayBuffer`, or a `string`.
|
|
19
|
-
The content type is resolved in order: the explicit `options.contentType`, then
|
|
20
|
-
the `Blob`'s own `type`, then `application/octet-stream`. Keys are plain
|
|
21
|
-
strings — use `/`-separated segments (`avatars/arjan.png`) so related files
|
|
22
|
-
share a prefix you can `list()` later. Putting to an existing key overwrites it.
|
|
23
|
-
|
|
24
|
-
Every upload (and every `list` entry) is described by a `BlobObject`:
|
|
25
|
-
|
|
26
|
-
| Field | Type | Meaning |
|
|
27
|
-
| ------------- | -------- | ---------------------------- |
|
|
28
|
-
| `key` | `string` | The key you stored it under |
|
|
29
|
-
| `size` | `number` | Size in bytes |
|
|
30
|
-
| `contentType` | `string` | MIME type it was stored with |
|
|
31
|
-
| `uploadedAt` | `string` | ISO timestamp of the upload |
|
|
32
|
-
|
|
33
|
-
## Downloading
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
// get(key) → Promise<Blob | null> — null when the key doesn't exist
|
|
37
|
-
const blob = await canopy.blob.get("notes.txt");
|
|
38
|
-
const text = await blob?.text();
|
|
39
|
-
|
|
40
|
-
// getUrl(key) → Promise<{ url: string; expiresAt: string }>
|
|
41
|
-
const { url, expiresAt } = await canopy.blob.getUrl("avatars/arjan.png");
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
`get` returns the content as a standard `Blob`, typed with the MIME type it
|
|
45
|
-
was uploaded with — use `.text()` for text, `.arrayBuffer()` or `.stream()`
|
|
46
|
-
for processing, or `URL.createObjectURL(blob)` for an `<img src>`.
|
|
47
|
-
|
|
48
|
-
`getUrl` returns a short-lived download URL served through the platform (the
|
|
49
|
-
signature alone grants access, so no cookies are needed). Prefer it when the
|
|
50
|
-
browser should fetch the bytes itself — image `src` attributes, download
|
|
51
|
-
links, media elements — and prefer `get` when your code needs the content.
|
|
52
|
-
Request a fresh URL when you render rather than storing one: it expires at
|
|
53
|
-
`expiresAt`.
|
|
54
|
-
|
|
55
|
-
## Listing and deleting
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
// list(prefix?) → Promise<BlobObject[]>
|
|
59
|
-
const everything = await canopy.blob.list();
|
|
60
|
-
const avatars = await canopy.blob.list("avatars/");
|
|
61
|
-
|
|
62
|
-
// delete(key) → Promise<void>
|
|
63
|
-
await canopy.blob.delete("notes.txt");
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Example: upload from a file input
|
|
67
|
-
|
|
68
|
-
```tsx
|
|
69
|
-
async function onFileChange(event: React.ChangeEvent<HTMLInputElement>) {
|
|
70
|
-
const file = event.target.files?.[0];
|
|
71
|
-
if (!file) return;
|
|
72
|
-
|
|
73
|
-
const stored = await canopy.blob.put(`uploads/${file.name}`, file);
|
|
74
|
-
const { url } = await canopy.blob.getUrl(stored.key);
|
|
75
|
-
setImageUrl(url); // <img src={imageUrl} />
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
<input type="file" accept="image/*" onChange={onFileChange} />;
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
Storage location and limits are covered in
|
|
82
|
-
[Project data](https://docs.canopy.website/platform/project-data).
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Services
|
|
3
|
-
description: Curated internal data services, invoked through the platform's connector — auth handled for you.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
`canopy.services` is the catalog of curated internal data services enabled
|
|
7
|
-
for your project. Calls go through the platform's connector, which handles
|
|
8
|
-
authentication — a service token or on-behalf-of-user credentials, depending
|
|
9
|
-
on the service — so there is nothing to configure in your code.
|
|
10
|
-
|
|
11
|
-
> **Warning:** Services aren't live yet: the SDK surface below is stable, but the platform currently answers
|
|
12
|
-
> every service call with "not implemented" and no services can be enabled. This page describes what
|
|
13
|
-
> to build against once the first services land.
|
|
14
|
-
|
|
15
|
-
## services.list()
|
|
16
|
-
|
|
17
|
-
The services enabled for this project:
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
const available = await canopy.services.list();
|
|
21
|
-
// [{ name: string, description: string }]
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
The catalog is curated per project — an empty array means no services are
|
|
25
|
-
enabled yet.
|
|
26
|
-
|
|
27
|
-
## services.get(name)
|
|
28
|
-
|
|
29
|
-
Returns a `ServiceRef` for a named service. This is a local handle — no
|
|
30
|
-
network call happens until you invoke it:
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
const service = canopy.services.get("example");
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## ref.fetch(path, init?)
|
|
37
|
-
|
|
38
|
-
Invoke a path on the service and get its JSON response back, typed with the
|
|
39
|
-
generic you provide:
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
const pong = await canopy.services.get("example").fetch("/ping");
|
|
43
|
-
|
|
44
|
-
const report = await canopy.services.get("example").fetch<Report>("/reports/weekly", {
|
|
45
|
-
method: "POST",
|
|
46
|
-
body: { week: 33 },
|
|
47
|
-
});
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
- `path` — the path to invoke on the service, e.g. `"/ping"`.
|
|
51
|
-
- `init.method` — optional HTTP method, defaults to `"GET"`.
|
|
52
|
-
- `init.body` — optional request body, sent as JSON.
|
|
53
|
-
- Returns `Promise<T>` (defaults to `unknown`) — the service's JSON response.
|
|
54
|
-
|
|
55
|
-
Failed invocations throw `CanopyError` with the HTTP `status`.
|
|
56
|
-
|
|
57
|
-
> **Note:** Services are for reading and invoking internal Monks data sources the platform has connected — not
|
|
58
|
-
> arbitrary outbound HTTP. For external APIs, use `fetch` directly.
|
package/dist/docs/sdk.md
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Using the SDK
|
|
3
|
-
description: One typed client for auth, data, files, AI, and services — no keys, no connection strings, the platform authenticates for you.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
`@mediamonks/canopy` is a thin, typed client over the platform's project API.
|
|
7
|
-
One `createCanopy()` call gives you five modules — `auth`, `db`, `blob`, `ai`,
|
|
8
|
-
and `services` — and that is the entire surface. There are no API keys,
|
|
9
|
-
connection strings, or environment variables to manage: every call rides the
|
|
10
|
-
session the platform already established before your code ran.
|
|
11
|
-
|
|
12
|
-
The same code works everywhere your project runs — in the browser during
|
|
13
|
-
`canopy dev`, in the deployed site, and in a project's backend worker.
|
|
14
|
-
|
|
15
|
-
## Getting the SDK
|
|
16
|
-
|
|
17
|
-
You usually don't install anything. The `spa` and `worker` templates ship with
|
|
18
|
-
`@mediamonks/canopy` in `package.json`; the `static` template imports the same
|
|
19
|
-
bundle straight from the platform, untyped but always version-matched to the
|
|
20
|
-
API:
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
// spa / worker templates (typed, from npm)
|
|
24
|
-
import { createCanopy } from "@mediamonks/canopy";
|
|
25
|
-
|
|
26
|
-
// static template (untyped, served by the platform)
|
|
27
|
-
import { createCanopy } from "/_api/data/v1/sdk.js";
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## createCanopy(config)
|
|
31
|
-
|
|
32
|
-
`createCanopy` takes an optional config object and returns the client:
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
import { createCanopy } from "@mediamonks/canopy";
|
|
36
|
-
|
|
37
|
-
const canopy = createCanopy();
|
|
38
|
-
// canopy.auth, canopy.db, canopy.blob, canopy.ai, canopy.services
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
| Option | Type | Default | What it does |
|
|
42
|
-
| --------- | -------------- | -------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
-
| `baseUrl` | `string` | `"/_api"` | Base URL of the platform data API. The default is same-origin, so it works untouched in the browser. |
|
|
44
|
-
| `token` | `string` | none | A project-scoped `cnpy_` token sent as a bearer on every request — for scripts and CI running outside Canopy hosting. |
|
|
45
|
-
| `fetch` | `typeof fetch` | global `fetch` | Override how requests are sent — a backend worker passes its platform service binding here. |
|
|
46
|
-
|
|
47
|
-
In the browser no config is needed. In a backend worker (the `worker`
|
|
48
|
-
template), the platform is reachable through the `CANOPY` service binding, so
|
|
49
|
-
create the client per request with that binding's fetch:
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
app.get("/api/hello", async (c) => {
|
|
53
|
-
const canopy = createCanopy({ fetch: c.env.CANOPY.fetch.bind(c.env.CANOPY) });
|
|
54
|
-
const user = await canopy.auth.user();
|
|
55
|
-
return c.json({ message: `Hello from the backend, ${user.name}!` });
|
|
56
|
-
});
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Outside Canopy hosting — a local Node script, a CI job — there is no session
|
|
60
|
-
and no service binding. Mint a dev token (`canopy dev` does this for you, or
|
|
61
|
-
`POST /control/v1/projects/:slug/dev-token`) and pass it with the platform's
|
|
62
|
-
API host:
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
const canopy = createCanopy({
|
|
66
|
-
baseUrl: "https://api.canopy.website",
|
|
67
|
-
token: process.env.CANOPY_TOKEN, // a project-scoped cnpy_ token
|
|
68
|
-
});
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## One instance, shared
|
|
72
|
-
|
|
73
|
-
In the browser, create the client once and import it everywhere. The scaffolds
|
|
74
|
-
do this in `src/canopy.ts`:
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
// src/canopy.ts
|
|
78
|
-
import { createCanopy } from "@mediamonks/canopy";
|
|
79
|
-
|
|
80
|
-
export const canopy = createCanopy();
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Error handling
|
|
84
|
-
|
|
85
|
-
Any call that fails throws a `CanopyError` (exported from the package). It
|
|
86
|
-
extends `Error` with the HTTP `status` of the failed request; `message` carries
|
|
87
|
-
the response body when the platform sent one:
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
import { CanopyError } from "@mediamonks/canopy";
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
await canopy.db.collection("todos").insert({ title: "hi", done: false });
|
|
94
|
-
} catch (err) {
|
|
95
|
-
if (err instanceof CanopyError && err.status === 403) {
|
|
96
|
-
// e.g. a viewer hitting a write — show a friendly message
|
|
97
|
-
}
|
|
98
|
-
throw err;
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Two read calls are deliberately soft instead: `collection.get(id)` and
|
|
103
|
-
`blob.get(key)` resolve to `null` for a missing document or object rather than
|
|
104
|
-
throwing.
|
|
105
|
-
|
|
106
|
-
## Where it runs
|
|
107
|
-
|
|
108
|
-
The client is plain `fetch` underneath, so behavior is identical across
|
|
109
|
-
environments:
|
|
110
|
-
|
|
111
|
-
- **Browser, deployed** — calls go to same-origin `/_api`; the platform
|
|
112
|
-
authenticated the visitor before your page loaded, and the session cookie
|
|
113
|
-
rides along automatically.
|
|
114
|
-
- **Browser, `canopy dev`** — the dev server proxies `/_api` to the platform
|
|
115
|
-
with your CLI login, so local code talks to your project's real data.
|
|
116
|
-
- **Backend worker** — pass the `CANOPY` service binding's fetch (shown
|
|
117
|
-
above); the incoming request's identity carries through, so `auth.user()`
|
|
118
|
-
and per-project budgets behave exactly as in the browser.
|
|
119
|
-
|
|
120
|
-
## Modules
|
|
121
|
-
|
|
122
|
-
- [Auth](https://docs.canopy.website/sdk/auth) — who is signed in, and ending sessions.
|
|
123
|
-
- [Database](https://docs.canopy.website/sdk/database) — document collections, plus a raw SQL escape hatch.
|
|
124
|
-
- [Files](https://docs.canopy.website/sdk/files) — private blob storage under the project's prefix.
|
|
125
|
-
- [AI](https://docs.canopy.website/sdk/ai) — metered, budget-capped LLM access.
|
|
126
|
-
- [Services](https://docs.canopy.website/sdk/services) — curated internal data services.
|
package/dist/docs/staging.md
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Staging & promoting
|
|
3
|
-
description: Deploy to a safe copy of your project first — own database cloned from live — then promote the exact same version.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
Every project can have a **staging channel**: a second, private copy of the
|
|
7
|
-
app with its own database (cloned from live), its own files, and its own
|
|
8
|
-
backend, served at
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
https://staging-dot-<slug>.canopy.website
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
behind the same sign-in as the live site. Deploy there first, click around
|
|
15
|
-
against real-shaped data, then **promote** — the exact version you tested
|
|
16
|
-
becomes live, nothing is rebuilt.
|
|
17
|
-
|
|
18
|
-
You don't set anything up. The first staged deploy creates the channel and
|
|
19
|
-
copies the live database into it.
|
|
20
|
-
|
|
21
|
-
## The workflow
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
canopy deploy --staging # build + deploy to staging (created on first use)
|
|
25
|
-
canopy open --staging # check it in the browser
|
|
26
|
-
canopy promote # happy? make that version live
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
`canopy deploy --staging` is the normal deploy in every way — same build, same
|
|
30
|
-
versioning — except the result serves on the staging URL and the live site is
|
|
31
|
-
untouched. Versions share one sequence: if live is at v7 and you stage v8,
|
|
32
|
-
promoting makes that same v8 live.
|
|
33
|
-
|
|
34
|
-
`canopy promote` is not a redeploy: the platform re-uses the exact files and
|
|
35
|
-
backend bundle you staged. If migrations were rehearsed on staging, they replay
|
|
36
|
-
against the live database first (with an automatic snapshot), then the pointer
|
|
37
|
-
flips.
|
|
38
|
-
|
|
39
|
-
## Testing schema migrations
|
|
40
|
-
|
|
41
|
-
This is the staging channel's superpower. Because the staging database starts
|
|
42
|
-
as a copy of live, a migration that works there will behave the same way on
|
|
43
|
-
the real thing:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
canopy deploy --staging # pending migrations apply to the STAGING database
|
|
47
|
-
# … verify the app against the migrated copy …
|
|
48
|
-
canopy promote # the same migrations replay against live, then v goes live
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
A migration that fails on staging costs you nothing — live never saw it. See
|
|
52
|
-
[Schema migrations](https://docs.canopy.website/guides/migrations) for the migration system itself.
|
|
53
|
-
|
|
54
|
-
## Staging an older version
|
|
55
|
-
|
|
56
|
-
Any version in the deploy history can be pointed at the staging channel —
|
|
57
|
-
from the dashboard's deploy list (the **Stage** action, next to Rollback) or
|
|
58
|
-
with `canopy staging use v3`. Like rollback, it's a pointer move: nothing is
|
|
59
|
-
rebuilt, and live is untouched. Handy for comparing an old build against the
|
|
60
|
-
staging data, or re-testing a version before promoting it.
|
|
61
|
-
|
|
62
|
-
## Keeping staging fresh
|
|
63
|
-
|
|
64
|
-
Staging data drifts as you test. Re-clone it from live anytime:
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
canopy staging sync # replace staging data with a fresh copy of live
|
|
68
|
-
canopy staging status # what's staged vs live
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Writes on staging never flow back to live — promote moves code and schema, not
|
|
72
|
-
data.
|
|
73
|
-
|
|
74
|
-
## What's shared, what's separate
|
|
75
|
-
|
|
76
|
-
Separate per channel: the database, uploaded files, and the backend worker.
|
|
77
|
-
Shared with live: members, secrets, the client password, schedules (they run
|
|
78
|
-
against live only), and the AI budget — staging traffic counts against the
|
|
79
|
-
same cap.
|
|
80
|
-
|
|
81
|
-
## Developing against staging data
|
|
82
|
-
|
|
83
|
-
Remote dev can point at the staging database instead of live — useful while a
|
|
84
|
-
schema is in flux:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
canopy dev --staging
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Cleaning up
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
canopy staging destroy # removes the staging database, files and backend
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Live is untouched, and the next `canopy deploy --staging` recreates the channel
|
|
97
|
-
from a fresh copy. The dashboard's Settings tab has the same controls, the
|
|
98
|
-
Overview tab shows what's staged with a Promote button, and a Live/Staging
|
|
99
|
-
toggle above the Data and Files tabs browses either channel's data.
|
|
100
|
-
|
|
101
|
-
## Staging vs version previews
|
|
102
|
-
|
|
103
|
-
Version preview URLs (`v12-dot-<slug>`) show an old **frontend** against the
|
|
104
|
-
**live** data — good for comparing builds. The staging channel is the full
|
|
105
|
-
package: candidate frontend, candidate backend, and its own database. Use
|
|
106
|
-
previews to look backwards, staging to look forwards.
|