@mediamonks/canopy-cli 0.7.11 → 0.7.13
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/{db-8Etjmzxh.mjs → db-DKnJQPm5.mjs} +2 -2
- package/dist/{deploy-BCtEY1is.mjs → deploy-CnxJ4TE6.mjs} +10 -3
- package/dist/dev-BqLvttKc.mjs +2 -0
- package/dist/{dev-DtoZrexp.mjs → dev-CyJT6EfQ.mjs} +7 -6
- package/dist/{dev-worker-BZgtEM9V.mjs → dev-worker-IWnHvQ1f.mjs} +42 -7
- package/dist/docs/index.json +5 -0
- package/dist/docs/platform-google.md +213 -0
- package/dist/docs/static-serving.md +14 -9
- package/dist/{docs-BV9qhLJx.mjs → docs-BBMVxVta.mjs} +2 -2
- package/dist/{egress-CN_axsAN.mjs → egress-BFSwa8Uu.mjs} +1 -1
- package/dist/{feedback-Cb8ccQ_R.mjs → feedback-BMX_d4KZ.mjs} +2 -2
- package/dist/index.mjs +25 -24
- package/dist/{local-C5AhA4GM.mjs → local-BQxsp-ZQ.mjs} +7 -470
- package/dist/{login-D7dI8D9N.mjs → login-BDTO3nVw.mjs} +1 -1
- package/dist/{logs-DNiZquki.mjs → logs-C53y2eAS.mjs} +1 -1
- package/dist/{migrations-C-5x6SYf.mjs → migrations-C6EcKD6a.mjs} +1 -1
- package/dist/{open-DRPp4QJg.mjs → open-C5x3tX6I.mjs} +1 -1
- package/dist/{package-BBpw-n7k.mjs → package-vJXq4RNN.mjs} +1 -1
- package/dist/{projects-C6JQjVqg.mjs → projects-MMbv7MmK.mjs} +1 -1
- package/dist/schedule-Cq9K0rnE.mjs +106 -0
- package/dist/{skill-DJHlTEhS.mjs → skill-Zq9aIPKf.mjs} +2 -2
- package/dist/src-Bu4Ze32I.mjs +470 -0
- package/dist/{staging-Bw1D_J8M.mjs → staging-D8G4amjO.mjs} +1 -1
- package/dist/{token-D7zrIF8G.mjs → token-BUxgRJVA.mjs} +1 -1
- package/dist/{ui-2mrJcjYC.mjs → ui-B1Px5y7q.mjs} +17 -0
- package/dist/{upgrade-B86Ao6T5.mjs → upgrade-ES8u8_Pn.mjs} +2 -2
- package/dist/{usage-CX1Izj_l.mjs → usage-DjNngyvy.mjs} +1 -1
- package/package.json +3 -3
- package/dist/dev-BYxSHUH6.mjs +0 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { i as unwrap, n as reportErrors, o as apiDataRequest, r as requireTty, u as withContext } from "./ui-
|
|
3
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
2
|
+
import { i as unwrap, n as reportErrors, o as apiDataRequest, r as requireTty, u as withContext } from "./ui-B1Px5y7q.mjs";
|
|
3
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C6EcKD6a.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import * as p from "@clack/prompts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, l as stagingUrl, n as siteConfig, o as projectFlagArgs, s as projectUrl, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { c as apiRequest, i as unwrap, n as reportErrors, o as apiDataRequest, s as apiDownload, u as withContext } from "./ui-
|
|
3
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
2
|
+
import { c as apiRequest, i as unwrap, n as reportErrors, o as apiDataRequest, s as apiDownload, u as withContext } from "./ui-B1Px5y7q.mjs";
|
|
3
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-C6EcKD6a.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
@@ -113,6 +113,12 @@ const deployCommand = defineCommand({
|
|
|
113
113
|
type: "boolean",
|
|
114
114
|
default: false,
|
|
115
115
|
description: "Deploy to the project's staging channel (created on first use, database cloned from live)"
|
|
116
|
+
},
|
|
117
|
+
worker: {
|
|
118
|
+
type: "boolean",
|
|
119
|
+
default: true,
|
|
120
|
+
description: "Include the backend worker from canopy.json",
|
|
121
|
+
negativeDescription: "Deploy the frontend only — for sites whose target hosts no per-project backends"
|
|
116
122
|
}
|
|
117
123
|
},
|
|
118
124
|
run: reportErrors("deploy", async ({ args }) => {
|
|
@@ -145,7 +151,8 @@ const deployCommand = defineCommand({
|
|
|
145
151
|
});
|
|
146
152
|
}
|
|
147
153
|
let workerBytes = null;
|
|
148
|
-
if (config.worker) {
|
|
154
|
+
if (config.worker && !args.worker) p.log.warn(`Skipping the backend (${config.worker}) — deploying the frontend only.`);
|
|
155
|
+
if (config.worker && args.worker) {
|
|
149
156
|
const spinWorker = p.spinner();
|
|
150
157
|
spinWorker.start(`Bundling worker ${config.worker}`);
|
|
151
158
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { a as ApiError, c as apiRequest, n as reportErrors, u as withContext } from "./ui-
|
|
3
|
-
import { r as readMigrationFiles } from "./migrations-
|
|
2
|
+
import { a as ApiError, c as apiRequest, n as reportErrors, u as withContext } from "./ui-B1Px5y7q.mjs";
|
|
3
|
+
import { r as readMigrationFiles } from "./migrations-C6EcKD6a.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
@@ -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-IWnHvQ1f.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-BQxsp-ZQ.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-BQxsp-ZQ.mjs");
|
|
184
184
|
const sandbox = await createSandbox({
|
|
185
185
|
slug,
|
|
186
186
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
@@ -233,7 +233,8 @@ async function devLocal(flags, slug) {
|
|
|
233
233
|
const server = await startVite(flags, { proxy: {
|
|
234
234
|
...remoteEntry && {
|
|
235
235
|
"/_api/data/v1/ai": remoteEntry,
|
|
236
|
-
"/_api/data/v1/services": remoteEntry
|
|
236
|
+
"/_api/data/v1/services": remoteEntry,
|
|
237
|
+
"/_api/data/v1/google": remoteEntry
|
|
237
238
|
},
|
|
238
239
|
"/_api": {
|
|
239
240
|
target: `http://127.0.0.1:${apiAddress.port}`,
|
|
@@ -1,5 +1,43 @@
|
|
|
1
|
+
import "./src-Bu4Ze32I.mjs";
|
|
1
2
|
import { readFileSync } from "node:fs";
|
|
2
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
|
+
//#region ../data-plane/src/worker-request.ts
|
|
5
|
+
/**
|
|
6
|
+
* How a project's backend addresses the platform — the one definition.
|
|
7
|
+
*
|
|
8
|
+
* Three hosts run project workers and each builds this request itself: the
|
|
9
|
+
* Workers-for-Platforms shim (apps/api/src/platform/wfp-shim.ts), the Node
|
|
10
|
+
* runtime (apps/project-runtime/src/canopy-fetch.ts), and `canopy dev`
|
|
11
|
+
* (packages/cli/src/dev-worker.ts). They had three copies of the header
|
|
12
|
+
* shaping, and the two that hold a worker token had two copies of the
|
|
13
|
+
* credential rule — which is exactly how the Google project grant came to
|
|
14
|
+
* work on one target and not the other.
|
|
15
|
+
*
|
|
16
|
+
* The shim cannot import this: it ships as a standalone module uploaded
|
|
17
|
+
* beside the user bundle, so it may only import `./bundle.mjs`. It keeps a
|
|
18
|
+
* transcription, and `wfp-shim.test.ts` runs it against the table in
|
|
19
|
+
* `worker-request.cases.ts` alongside this implementation, so the two cannot
|
|
20
|
+
* drift silently.
|
|
21
|
+
*/
|
|
22
|
+
/** Gateway-stamped identity, for the worker's own `env.USER` — never relayed. */
|
|
23
|
+
const IDENTITY_STAMP_HEADERS = [
|
|
24
|
+
"x-canopy-user-sub",
|
|
25
|
+
"x-canopy-user-email",
|
|
26
|
+
"x-canopy-user-name"
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Strip what must never travel upstream and stamp who is calling. Common to
|
|
30
|
+
* every host; says nothing about credentials.
|
|
31
|
+
*/
|
|
32
|
+
function stampWorkerHeaders(headers, ctx) {
|
|
33
|
+
headers.delete("host");
|
|
34
|
+
for (const stamp of IDENTITY_STAMP_HEADERS) headers.delete(stamp);
|
|
35
|
+
headers.set("x-canopy-project", ctx.projectSlug);
|
|
36
|
+
if (ctx.channel === "staging") headers.set("x-canopy-channel", "staging");
|
|
37
|
+
else headers.delete("x-canopy-channel");
|
|
38
|
+
return headers;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
3
41
|
//#region src/dev-worker.ts
|
|
4
42
|
/**
|
|
5
43
|
* Paths that belong to the worker rather than the SPA — the gateway's rule
|
|
@@ -79,15 +117,12 @@ function rewriteCanopyUrl(raw, base) {
|
|
|
79
117
|
function createCanopyFetch(apiBase, slug, channel, fetchImpl = fetch) {
|
|
80
118
|
return async (input, init) => {
|
|
81
119
|
const url = rewriteCanopyUrl(typeof input === "string" ? input : input instanceof URL ? input.href : input.url, apiBase());
|
|
82
|
-
const headers = new Headers(input instanceof Request ? input.headers : init?.headers)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
headers.delete("x-canopy-user-name");
|
|
120
|
+
const headers = stampWorkerHeaders(new Headers(input instanceof Request ? input.headers : init?.headers), {
|
|
121
|
+
projectSlug: slug,
|
|
122
|
+
channel
|
|
123
|
+
});
|
|
87
124
|
headers.delete("authorization");
|
|
88
125
|
headers.delete("cookie");
|
|
89
|
-
headers.set("x-canopy-project", slug);
|
|
90
|
-
if (channel === "staging") headers.set("x-canopy-channel", "staging");
|
|
91
126
|
const method = (input instanceof Request ? input.method : init?.method) ?? "GET";
|
|
92
127
|
const body = input instanceof Request ? input.body : init?.body;
|
|
93
128
|
const outbound = {
|
package/dist/docs/index.json
CHANGED
|
@@ -124,6 +124,11 @@
|
|
|
124
124
|
"title": "AI access & usage",
|
|
125
125
|
"summary": "Projects talk to AI models through the platform — no API keys to manage, a hard daily budget per project, and full visibility of spend."
|
|
126
126
|
},
|
|
127
|
+
{
|
|
128
|
+
"slug": "platform-google",
|
|
129
|
+
"title": "Google",
|
|
130
|
+
"summary": "Let your project work with Google data — Sheets, Docs, Calendar, Drive, Gmail — without handling any OAuth yourself."
|
|
131
|
+
},
|
|
127
132
|
{
|
|
128
133
|
"slug": "platform-members-and-access",
|
|
129
134
|
"title": "Members & access",
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Google
|
|
3
|
+
description: Let your project work with Google data — Sheets, Docs, Calendar, Drive, Gmail — without handling any OAuth yourself.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Your project can work with Google data in two different ways, and the
|
|
7
|
+
difference matters: as **the project's own account**, or as **each visitor's
|
|
8
|
+
own account**.
|
|
9
|
+
|
|
10
|
+
Either way there is no OAuth setup to do. No Cloud project, no consent screen,
|
|
11
|
+
no client ID or secret, no redirect URLs, no tokens to store or refresh. Canopy
|
|
12
|
+
holds all of that.
|
|
13
|
+
|
|
14
|
+
## The project's own account
|
|
15
|
+
|
|
16
|
+
The owner connects one Google account on the project's **Services** tab and
|
|
17
|
+
picks what it may do. Everything the project sends to Google then happens as
|
|
18
|
+
that account — including scheduled jobs that run when nobody is signed in. A
|
|
19
|
+
shared team account is usually a better choice than a personal one.
|
|
20
|
+
|
|
21
|
+
This account is **only usable from your backend code**:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// in your worker
|
|
25
|
+
const doc = await canopy.google.project().fetch("https://docs.googleapis.com/v1/documents/<id>");
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Pages cannot use it at all. That is deliberate: it is one person's real
|
|
29
|
+
account, and anything a page can call, any visitor can call from their browser
|
|
30
|
+
console. When your pages need something from it, put a route in your backend
|
|
31
|
+
that returns just that — you decide what to expose.
|
|
32
|
+
|
|
33
|
+
> **Note:** Only the project owner can connect or disconnect this account.
|
|
34
|
+
|
|
35
|
+
## Each visitor's own account
|
|
36
|
+
|
|
37
|
+
Visitors can connect their own Google account to your project, and your pages
|
|
38
|
+
can then work with _their_ data — their calendar, their documents. They consent
|
|
39
|
+
themselves, and it only ever acts as them.
|
|
40
|
+
|
|
41
|
+
First, on the **Services** tab, tick what your app is allowed to ask visitors
|
|
42
|
+
for. Your app can never request more than that.
|
|
43
|
+
|
|
44
|
+
Then, from your pages:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const google = canopy.google.user();
|
|
48
|
+
|
|
49
|
+
// On load: has this visitor connected, and what came back from a connect?
|
|
50
|
+
const result = google.consumeConnectResult(); // toast this, if not null
|
|
51
|
+
const status = await google.status();
|
|
52
|
+
|
|
53
|
+
if (!status.connected) {
|
|
54
|
+
// A full-page trip to Google, then straight back to this page.
|
|
55
|
+
google.connect(["calendar"]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const events = await google.fetch(
|
|
59
|
+
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`connect()` leaves the page, so put the button somewhere without unsaved work.
|
|
64
|
+
When the visitor comes back, `status()` is the reliable answer to "are they
|
|
65
|
+
connected" — `consumeConnectResult()` just tells you how the trip that _just_
|
|
66
|
+
happened went, so you can show a message.
|
|
67
|
+
|
|
68
|
+
Google asks the visitor for exactly the permissions you passed. Nothing your
|
|
69
|
+
project holds elsewhere carries over into what they see.
|
|
70
|
+
|
|
71
|
+
Your backend can use a visitor's connection too, while handling that visitor's
|
|
72
|
+
request — same `canopy.google.user()`, same account. It cannot in a scheduled
|
|
73
|
+
job, because then nobody is signed in.
|
|
74
|
+
|
|
75
|
+
> **Note:** Visitors need a Monks account to connect. People visiting with a project password cannot connect
|
|
76
|
+
> their own Google account.
|
|
77
|
+
|
|
78
|
+
## What a connection can reach
|
|
79
|
+
|
|
80
|
+
Permissions are grouped by product — Sheets, Docs, Slides, Calendar, Drive,
|
|
81
|
+
Gmail, Directory, Tasks, Forms, Meet — and most come in a reading and a
|
|
82
|
+
read-and-write form. Pick the narrowest that does the job: a page that shows a
|
|
83
|
+
spreadsheet wants **read spreadsheets**, not read and write.
|
|
84
|
+
|
|
85
|
+
A few are narrower than they first look, and are usually the right answer —
|
|
86
|
+
the full list is below:
|
|
87
|
+
|
|
88
|
+
| Instead of | Consider |
|
|
89
|
+
| ------------------------ | ------------------------------------------------------------------------------------------ |
|
|
90
|
+
| Read and write events | **Availability only** — find free slots without seeing what any meeting is |
|
|
91
|
+
| Read and write events | **Its own calendar** — a separate calendar your project owns, never touching the account's |
|
|
92
|
+
| Files this project makes | **Its own config folder** — a hidden folder nothing else can see |
|
|
93
|
+
|
|
94
|
+
A call to anything that wasn't ticked is refused before it reaches Google, and
|
|
95
|
+
so is a write using a reading-only permission.
|
|
96
|
+
|
|
97
|
+
Drive is deliberately limited to files your project created or that someone
|
|
98
|
+
explicitly opened with it — connecting Drive does not expose a whole Drive.
|
|
99
|
+
|
|
100
|
+
### Wider permissions
|
|
101
|
+
|
|
102
|
+
A few permissions read broadly across an account — a whole mailbox, meeting
|
|
103
|
+
recordings. Those are switched off until a Canopy admin unlocks them for your
|
|
104
|
+
project, and the picker shows them greyed out with the reason. Ask if you need
|
|
105
|
+
one. Others are listed as _under consideration_: they exist, we haven't opened
|
|
106
|
+
them yet.
|
|
107
|
+
|
|
108
|
+
## Every permission
|
|
109
|
+
|
|
110
|
+
Grouped the way the picker groups them. Pick the narrowest row that does
|
|
111
|
+
the job — most products offer a reading-only version, and a write using a
|
|
112
|
+
reading-only permission is refused before it reaches Google.
|
|
113
|
+
|
|
114
|
+
**Sheets**
|
|
115
|
+
|
|
116
|
+
| Permission | What it allows |
|
|
117
|
+
| --------------------------- | ----------------------------------------------------------- |
|
|
118
|
+
| Read spreadsheets | Read spreadsheets the account can open. Cannot change them. |
|
|
119
|
+
| Read and write spreadsheets | Read and change spreadsheets the account can open. |
|
|
120
|
+
|
|
121
|
+
**Docs**
|
|
122
|
+
|
|
123
|
+
| Permission | What it allows |
|
|
124
|
+
| ------------------------ | -------------------------------------------------------- |
|
|
125
|
+
| Read documents | Read documents the account can open. Cannot change them. |
|
|
126
|
+
| Read and write documents | Read and change documents the account can open. |
|
|
127
|
+
|
|
128
|
+
**Slides**
|
|
129
|
+
|
|
130
|
+
| Permission | What it allows |
|
|
131
|
+
| ---------------------------- | ------------------------------------------------------------ |
|
|
132
|
+
| Read presentations | Read presentations the account can open. Cannot change them. |
|
|
133
|
+
| Read and write presentations | Build and change presentations the account can open. |
|
|
134
|
+
|
|
135
|
+
**Calendar**
|
|
136
|
+
|
|
137
|
+
| Permission | What it allows |
|
|
138
|
+
| -------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
139
|
+
| Availability only | See when the account is free or busy, never what the meetings are. |
|
|
140
|
+
| Read events | Read events on the account's calendars. Cannot change them. |
|
|
141
|
+
| Read and write events | Read and manage events on the account's calendars. |
|
|
142
|
+
| Its own calendar | A separate calendar this project creates and owns. Never touches the account's own. |
|
|
143
|
+
| Manage all calendars _(under consideration)_ | Full control of every calendar, including creating and deleting them. |
|
|
144
|
+
|
|
145
|
+
**Drive**
|
|
146
|
+
|
|
147
|
+
| Permission | What it allows |
|
|
148
|
+
| --------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
149
|
+
| Files this project makes | Create files, and reopen the ones this project made or someone picked for it. |
|
|
150
|
+
| Its own config folder | A hidden folder only this project can see. Invisible in the account's Drive. |
|
|
151
|
+
| Recordings and transcripts **(wider access)** | Read the recordings and transcripts Meet saved to Drive. |
|
|
152
|
+
| Read the whole Drive _(under consideration)_ | Read and download every file the account can see. |
|
|
153
|
+
| Read all file names _(under consideration)_ | Names and metadata of every file the account can see, without contents. |
|
|
154
|
+
| Full Drive access _(under consideration)_ | Read, change and delete every file the account can see. |
|
|
155
|
+
|
|
156
|
+
**Gmail**
|
|
157
|
+
|
|
158
|
+
| Permission | What it allows |
|
|
159
|
+
| ---------------------------------------------- | ---------------------------------------------------------------- |
|
|
160
|
+
| Send mail | Send mail as the account. Cannot read the mailbox. |
|
|
161
|
+
| Read mail **(wider access)** | Read the account's mail. Cannot send, change or delete anything. |
|
|
162
|
+
| Read and organise mail _(under consideration)_ | Read, label and archive mail. Cannot permanently delete. |
|
|
163
|
+
|
|
164
|
+
**Directory**
|
|
165
|
+
|
|
166
|
+
| Permission | What it allows |
|
|
167
|
+
| ------------------------------------------------- | -------------------------------------------------------------------- |
|
|
168
|
+
| Look people up | Search the organisation's directory for colleagues. |
|
|
169
|
+
| Read personal contacts | Read the account's own contacts. |
|
|
170
|
+
| Read org users and groups _(under consideration)_ | Admin view of every user and group. Only works for Workspace admins. |
|
|
171
|
+
|
|
172
|
+
**Tasks**
|
|
173
|
+
|
|
174
|
+
| Permission | What it allows |
|
|
175
|
+
| -------------------- | ---------------------------------- |
|
|
176
|
+
| Read tasks | Read the account's task lists. |
|
|
177
|
+
| Read and write tasks | Create, change and complete tasks. |
|
|
178
|
+
|
|
179
|
+
**Forms**
|
|
180
|
+
|
|
181
|
+
| Permission | What it allows |
|
|
182
|
+
| ------------------- | -------------------------------------------------------- |
|
|
183
|
+
| Read form structure | Read a form's questions and settings, not its responses. |
|
|
184
|
+
| Build forms | Create and change forms. |
|
|
185
|
+
| Read responses | Read what people submitted. Often personal information. |
|
|
186
|
+
|
|
187
|
+
**Meet**
|
|
188
|
+
|
|
189
|
+
| Permission | What it allows |
|
|
190
|
+
| --------------------------- | ---------------------------------------------------------- |
|
|
191
|
+
| Meetings this project makes | Create meeting links and read details of the ones it made. |
|
|
192
|
+
|
|
193
|
+
## When a connection stops working
|
|
194
|
+
|
|
195
|
+
Google grants end on their own sometimes: someone changes their password,
|
|
196
|
+
removes Canopy from their account permissions, or the connection sits unused
|
|
197
|
+
for six months. Calls then fail asking for a reconnect. For the project's
|
|
198
|
+
account, the owner reconnects on the Services tab; for a visitor's, show the
|
|
199
|
+
connect button again. Nothing else needs changing.
|
|
200
|
+
|
|
201
|
+
Disconnecting also tells Google the grant is over, so access stops immediately
|
|
202
|
+
rather than lingering. Visitors can drop their own connection with
|
|
203
|
+
`canopy.google.user().disconnect()`.
|
|
204
|
+
|
|
205
|
+
## Local development
|
|
206
|
+
|
|
207
|
+
`canopy dev` uses the real connections, so Google works the same on your
|
|
208
|
+
machine as in production — including with `--local`. There is nothing to set up
|
|
209
|
+
locally, and no localhost URLs to register.
|
|
210
|
+
|
|
211
|
+
Because it is real, calls in development touch real data. Sending mail really
|
|
212
|
+
sends it. If that matters, connect a test account on your project's staging
|
|
213
|
+
channel and develop against that.
|
|
@@ -34,7 +34,7 @@ canopy-gateway (apps/gateway/src/index.ts)
|
|
|
34
34
|
▼ env.API.fetch(...) (service binding, no public hop)
|
|
35
35
|
canopy-api (apps/api/src/routes/assets.ts)
|
|
36
36
|
│ 4. Resolve the project from the stamp + which version is live
|
|
37
|
-
│ 5. Sanitize the asset path, SPA
|
|
37
|
+
│ 5. Sanitize the asset path; directory index, else SPA fallback
|
|
38
38
|
│ 6. R2 GET deploys/<project-id>/<version>/<path>
|
|
39
39
|
▼
|
|
40
40
|
R2 bucket "canopy-assets"
|
|
@@ -102,14 +102,19 @@ The serving handler is `GET /data/v1/assets/*` in
|
|
|
102
102
|
project's deploy history, else 404 — it never silently falls back to live);
|
|
103
103
|
otherwise the snapshot's `liveAssetVersion` is served, which falls back to
|
|
104
104
|
the max deployed version for legacy rows with a NULL pointer.
|
|
105
|
-
- **Path → R2 key.** The tail after `/assets/` is sanitized (see Security)
|
|
106
|
-
|
|
107
|
-
`
|
|
108
|
-
- **
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
- **Path → R2 key.** The tail after `/assets/` is sanitized (see Security)
|
|
106
|
+
and mapped to the R2 key `deploys/<project-id>/<version>/<path>`; the root
|
|
107
|
+
path serves `index.html`.
|
|
108
|
+
- **Directory index + SPA fallback**: the exact asset is read first. On a
|
|
109
|
+
miss, an extensionless path is probed for its own directory index — `/feeds/`
|
|
110
|
+
serves `feeds/index.html` (multi-page static exports), and the slash-less
|
|
111
|
+
`/feeds` 301-redirects to `/feeds/` so the page's relative URLs resolve.
|
|
112
|
+
Only when no directory index exists does the request fall back to the root
|
|
113
|
+
`index.html`, so client-side routes (`/settings`, `/projects/42`) still all
|
|
114
|
+
resolve to the SPA entry point. Both the fallback `index.html` and the
|
|
115
|
+
per-path probe results are served from per-version caches (see
|
|
116
|
+
[Performance](#performance-review)), so a deep-linked route doesn't pay a
|
|
117
|
+
second R2 read on every request.
|
|
113
118
|
|
|
114
119
|
### How assets are stored & versioned
|
|
115
120
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-vJXq4RNN.mjs";
|
|
2
2
|
import { n as siteConfig } from "./site-BUlQR117.mjs";
|
|
3
3
|
import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
|
|
4
|
-
import { n as reportErrors } from "./ui-
|
|
4
|
+
import { n as reportErrors } from "./ui-B1Px5y7q.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject } from "./site-BUlQR117.mjs";
|
|
2
|
-
import { c as apiRequest, n as reportErrors } from "./ui-
|
|
2
|
+
import { c as apiRequest, n as reportErrors } from "./ui-B1Px5y7q.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/egress.ts
|
|
5
5
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-vJXq4RNN.mjs";
|
|
2
2
|
import { a as projectDirArgs, c as resolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BUlQR117.mjs";
|
|
3
|
-
import { c as apiRequest, l as apiUpload, n as reportErrors, s as apiDownload } from "./ui-
|
|
3
|
+
import { c as apiRequest, l as apiUpload, n as reportErrors, s as apiDownload } from "./ui-B1Px5y7q.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { basename, extname, join } from "node:path";
|
|
6
6
|
import { readFile, writeFile } from "node:fs/promises";
|
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-vJXq4RNN.mjs";
|
|
3
3
|
import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BUlQR117.mjs";
|
|
4
4
|
import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
@@ -60,29 +60,30 @@ 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-
|
|
66
|
-
create: () => import("./projects-
|
|
67
|
-
init: () => import("./projects-
|
|
68
|
-
projects: () => import("./projects-
|
|
69
|
-
delete: () => import("./projects-
|
|
70
|
-
dev: () => import("./dev-
|
|
71
|
-
deploy: () => import("./deploy-
|
|
72
|
-
promote: () => import("./deploy-
|
|
73
|
-
staging: () => import("./staging-
|
|
74
|
-
rollback: () => import("./deploy-
|
|
75
|
-
download: () => import("./deploy-
|
|
76
|
-
token: () => import("./token-
|
|
77
|
-
egress: () => import("./egress-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
63
|
+
login: () => import("./login-BDTO3nVw.mjs").then((m) => m.loginCommand),
|
|
64
|
+
whoami: () => import("./login-BDTO3nVw.mjs").then((m) => m.whoamiCommand),
|
|
65
|
+
logout: () => import("./login-BDTO3nVw.mjs").then((m) => m.logoutCommand),
|
|
66
|
+
create: () => import("./projects-MMbv7MmK.mjs").then((m) => m.createCommand),
|
|
67
|
+
init: () => import("./projects-MMbv7MmK.mjs").then((m) => m.initCommand),
|
|
68
|
+
projects: () => import("./projects-MMbv7MmK.mjs").then((m) => m.projectsCommand),
|
|
69
|
+
delete: () => import("./projects-MMbv7MmK.mjs").then((m) => m.deleteCommand),
|
|
70
|
+
dev: () => import("./dev-BqLvttKc.mjs").then((m) => m.devCommand),
|
|
71
|
+
deploy: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.deployCommand),
|
|
72
|
+
promote: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.promoteCommand),
|
|
73
|
+
staging: () => import("./staging-D8G4amjO.mjs").then((m) => m.stagingCommand),
|
|
74
|
+
rollback: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.rollbackCommand),
|
|
75
|
+
download: () => import("./deploy-CnxJ4TE6.mjs").then((m) => m.downloadCommand),
|
|
76
|
+
token: () => import("./token-BUxgRJVA.mjs").then((m) => m.tokenCommand),
|
|
77
|
+
egress: () => import("./egress-BFSwa8Uu.mjs").then((m) => m.egressCommand),
|
|
78
|
+
schedule: () => import("./schedule-Cq9K0rnE.mjs").then((m) => m.scheduleCommand),
|
|
79
|
+
feedback: () => import("./feedback-BMX_d4KZ.mjs").then((m) => m.feedbackCommand),
|
|
80
|
+
db: () => import("./db-DKnJQPm5.mjs").then((m) => m.dbCommand),
|
|
81
|
+
usage: () => import("./usage-DjNngyvy.mjs").then((m) => m.usageCommand),
|
|
82
|
+
open: () => import("./open-C5x3tX6I.mjs").then((m) => m.openCommand),
|
|
83
|
+
docs: () => import("./docs-BBMVxVta.mjs").then((m) => m.docsCommand),
|
|
84
|
+
skill: () => import("./skill-Zq9aIPKf.mjs").then((m) => m.skillCommand),
|
|
85
|
+
upgrade: () => import("./upgrade-ES8u8_Pn.mjs").then((m) => m.upgradeCommand),
|
|
86
|
+
logs: () => import("./logs-C53y2eAS.mjs").then((m) => m.logsCommand)
|
|
86
87
|
}
|
|
87
88
|
});
|
|
88
89
|
if (process.argv[2] !== "upgrade") notifyOfUpdate(version);
|