agentic-canvas 0.6.1 → 0.8.0
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/README.md +85 -25
- package/dist/{chunk-VFJJGXTK.js → chunk-7IOW6LML.js} +6 -1
- package/dist/{chunk-PH67TSYW.js → chunk-JQCUVXXD.js} +302 -18
- package/dist/{chunk-66FFQMHI.js → chunk-M74ENUWR.js} +39 -6
- package/dist/chunk-NRLF6MGR.js +130 -0
- package/dist/cli.js +8 -3
- package/dist/client.d.ts +5 -1
- package/dist/client.js +1 -0
- package/dist/embed.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/live-worker.d.ts +2 -2
- package/dist/live-worker.js +2 -2
- package/dist/live.d.ts +5 -2
- package/dist/live.js +7 -0
- package/dist/{read-CeWoSAAy.d.ts → read--Jd3kE13.d.ts} +3 -9
- package/dist/{service-Ckz-3qhK.d.ts → service-OiTtNadu.d.ts} +11 -2
- package/dist/store-file.d.ts +1 -12
- package/dist/store-file.js +1 -1
- package/dist/ui.css +2 -2
- package/dist/worker.d.ts +104 -7
- package/dist/worker.js +4 -2
- package/llms.txt +3 -3
- package/package.json +9 -5
- package/dist/chunk-UKST5A57.js +0 -228
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
A shared whiteboard for diagrams, drawn by people and agents.
|
|
9
9
|
|
|
10
|
-
> Status: 0.
|
|
10
|
+
> Status: 0.8.0. The board model, the agent operations, the exports, the CLI, the
|
|
11
11
|
> MCP server, a small Worker API, and live co-editing work. The CLI, the MCP server,
|
|
12
12
|
> and the SDK can join a live room. A remote client calls the Worker API. A small web UI, served by the Worker, is new in 0.4.0.
|
|
13
13
|
> The package exports `agentic-canvas/ui`, a board view you can mount in your own app, new in 0.5.0.
|
|
@@ -38,7 +38,7 @@ npx agentic-canvas export demo --out demo.svg
|
|
|
38
38
|
npx agentic-canvas export demo --format png --out demo.png
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Other commands: `list`, `add`, `connect`, `update`, `move`, `rm
|
|
41
|
+
Other commands: `list`, `add`, `connect`, `update`, `move`, `rm`, `delete` (a whole board). Run `npx agentic-canvas --help`.
|
|
42
42
|
`repair` is for the owner only. It needs `CANVAS_ACTOR=human:<id>`.
|
|
43
43
|
Boards live in `./.agentic-canvas` (or `--dir`, or `CANVAS_DIR`). Every write
|
|
44
44
|
needs an actor in `CANVAS_ACTOR`, as `<kind>:<id>`. The operator sets it. A write
|
|
@@ -52,7 +52,7 @@ CANVAS_ACTOR=agent:helper npx agentic-canvas --dir ./boards mcp
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
The server speaks MCP on stdio. It has one tool for each CLI command: `create_board`,
|
|
55
|
-
`list_boards`, `read_board`, `add_shape`, `connect`, `update_element`, `move`, `remove`, `draw_flow`, `export_svg`, and
|
|
55
|
+
`delete_board`, `list_boards`, `read_board`, `add_shape`, `connect`, `update_element`, `move`, `remove`, `draw_flow`, `export_svg`, and
|
|
56
56
|
`export_png`. `repair` is not a tool. Only a human operator runs it, from the CLI. The server will not start unless `CANVAS_ACTOR` is set to a valid
|
|
57
57
|
`<kind>:<id>`. That actor stamps every write. No tool takes an actor, author, or kind, so a
|
|
58
58
|
caller cannot choose who it writes as. An error comes back as a tool error with a code.
|
|
@@ -105,7 +105,7 @@ try {
|
|
|
105
105
|
}
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
There is one method per route: `me`, `listBoards`, `createBoard`, `readBoard`, `exportSvg`, `addShape`, `addConnector`,
|
|
108
|
+
There is one method per route: `me`, `listBoards`, `createBoard`, `readBoard`, `deleteBoard`, `exportSvg`, `addShape`, `addConnector`,
|
|
109
109
|
`drawFlow`, `update`, `move`, `remove`, `uploadImage`, `getImage`, `state`, and `checkpoint`. Each takes
|
|
110
110
|
`{ signal }` last, so an `AbortSignal` can stop a call. The token sets the actor. A failed call throws
|
|
111
111
|
`CanvasApiError` with the API `code` and the HTTP `status`. A call that gets no reply has code `NETWORK`
|
|
@@ -125,12 +125,66 @@ no CI secret is used:
|
|
|
125
125
|
```sh
|
|
126
126
|
npm ci && npm run build
|
|
127
127
|
WRANGLER_SEND_METRICS=false CLOUDFLARE_API_TOKEN=... npx wrangler deploy -c wrangler.demo.jsonc
|
|
128
|
-
npx wrangler secret put CANVAS_API_TOKENS -c wrangler.demo.jsonc # only to set or rotate tokens
|
|
128
|
+
npx wrangler secret put CANVAS_API_TOKENS -c wrangler.demo.jsonc # only to set or rotate the demo tokens
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
`wrangler.demo.jsonc` holds the config. The
|
|
131
|
+
`wrangler.demo.jsonc` holds the config. The demo is a throwaway: it turns on the dev fallback (see "Auth") and
|
|
132
|
+
signs in with a static token list. The tokens are a Worker secret and are not in the repo. To run your
|
|
132
133
|
own copy, create a D1 database and an R2 bucket (both in the EU), put their names and id in a copy of that
|
|
133
|
-
file, and deploy it the same way.
|
|
134
|
+
file, and deploy it the same way. Remove `CANVAS_UNSAFE_STATIC_TOKENS` from your copy unless it is a throwaway.
|
|
135
|
+
|
|
136
|
+
## Auth
|
|
137
|
+
|
|
138
|
+
The Worker signs requests in with `agentic-identity` (`agentic-identity/http`). A bearer key (an `api_key` or
|
|
139
|
+
`pat` credential) is a credential in the same Store as the boards (D1). A `session` credential works as a
|
|
140
|
+
bearer key too, and as the `session` cookie for reads. A write or a socket that rides on the cookie gets 401,
|
|
141
|
+
because the Worker lists no allowed origins. A credential names a principal, and the principal's scopes set the role:
|
|
142
|
+
|
|
143
|
+
- Any scope that covers `canvas:write`, such as `canvas:write`, `canvas:admin`, `*:write`, or `*:admin`, is an editor.
|
|
144
|
+
- Any scope that covers `canvas:read`, such as `canvas:read` or `*:read`, is a viewer. A viewer reads and cannot change a board, over HTTP or in a live room.
|
|
145
|
+
- A principal with neither scope, an unknown, expired, or revoked credential, or a revoked parent in the
|
|
146
|
+
delegation chain, gets 401.
|
|
147
|
+
|
|
148
|
+
Make principals and credentials in the database of this Worker with `agentic-identity` (its Worker, or
|
|
149
|
+
`issueCredential` from its SDK). The two must share one D1 database and its schema (`D1_SCHEMA`). A credential
|
|
150
|
+
that lives in another database gets 401 here. The author kind of every write is the kind of the principal.
|
|
151
|
+
A browser sends its credential as the WebSocket sub-protocol `agentic-canvas.bearer.<token in base64url>`.
|
|
152
|
+
|
|
153
|
+
**Dev fallback.** `CANVAS_API_TOKENS` is a static list of tokens, as JSON: `{ "<token>": { "actor": "ada", "kind": "human", "role": "viewer" } }`.
|
|
154
|
+
It is read only when `CANVAS_UNSAFE_STATIC_TOKENS=1` is set too. Then it adds to the identity credentials: a
|
|
155
|
+
token that is not a credential is looked up in the list. A static token reaches every board and cannot be
|
|
156
|
+
revoked without a new deploy. It ignores revocation in the Store: a static grant named `ada` still writes as
|
|
157
|
+
`ada` after the identity credential of `ada` is revoked. Use it for local work and throwaway demos only. If `CANVAS_API_TOKENS` is set
|
|
158
|
+
and the switch is not, every API request gets 503 `NOT_CONFIGURED`, so a deploy never trusts the list by accident.
|
|
159
|
+
|
|
160
|
+
## Sharing
|
|
161
|
+
|
|
162
|
+
A board is an `agentic-identity` resource of type `canvas`. The person or agent that makes a board owns it.
|
|
163
|
+
Roles, from least to most: `viewer`, `commenter`, `editor`, `owner`.
|
|
164
|
+
|
|
165
|
+
| Role | May do |
|
|
166
|
+
|---|---|
|
|
167
|
+
| viewer, commenter | Read the board, its SVG, and join its live room to read. A board has no comments yet, so a commenter reads as a viewer. |
|
|
168
|
+
| editor | Also draw: add, update, move, and remove elements. |
|
|
169
|
+
| owner | Also delete the board, grant and revoke roles, list access, invite, and make or revoke share links. |
|
|
170
|
+
|
|
171
|
+
A role never exceeds the scopes of the credential. An agent of an owner holds the owner's role only inside its own
|
|
172
|
+
scopes: it needs `canvas:admin` to share, and `canvas:write` to draw. A caller with no role on a board gets 404, so a read or a write tells a stranger nothing. (A create with an id that is taken answers 409.) A caller with a role that is too low gets 403. `GET /boards` lists the boards you hold a role on.
|
|
173
|
+
|
|
174
|
+
HTTP routes, under `/api/v1/canvas`. Each needs a bearer token, except the link read:
|
|
175
|
+
|
|
176
|
+
| Route | Does |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `GET /boards/:id/access` | Owner. The grants and the invites. An invite shows no email. |
|
|
179
|
+
| `POST /boards/:id/grants` | Owner. Body `{ subjectKind: "human" or "agent", subjectId, role }`. |
|
|
180
|
+
| `DELETE /boards/:id/grants?subjectKind=&subjectId=` | Owner. Revoke. The last owner cannot be removed. |
|
|
181
|
+
| `POST /boards/:id/invites` | Owner. Body `{ role, email }` or `{ role, handle }`, and optional `expiresInMs`. The role is not `owner`. Returns `{ id }`. |
|
|
182
|
+
| `POST /invites/accept` | Body `{ id }`. The caller must be the person (verified email) or the agent (handle) the invite names. |
|
|
183
|
+
| `POST /boards/:id/links` | Owner. Body `{ role, signedInOnly?, expiresInMs? }`. Returns `{ id, token }`. The token shows once. |
|
|
184
|
+
| `DELETE /boards/:id/links/:link` | Owner. Revoke a link of this board. |
|
|
185
|
+
| `GET /shared/:id` and `GET /shared/:id/svg` | No bearer token needed. Send the link token in `x-link-token`. Read only. |
|
|
186
|
+
|
|
187
|
+
The static dev fallback holds no board roles. Its tokens reach every board and cannot share.
|
|
134
188
|
|
|
135
189
|
## UI
|
|
136
190
|
|
|
@@ -146,12 +200,13 @@ npm ci
|
|
|
146
200
|
npm run build # writes the UI to ui-dist/
|
|
147
201
|
cat > .dev.vars <<'EOF2'
|
|
148
202
|
CANVAS_REGION=unknown
|
|
203
|
+
CANVAS_UNSAFE_STATIC_TOKENS=1
|
|
149
204
|
CANVAS_API_TOKENS={"ada-token-0123456789-0123456789-0123456789":{"actor":"ada","kind":"human"}}
|
|
150
205
|
EOF2
|
|
151
206
|
WRANGLER_SEND_METRICS=false npx wrangler dev
|
|
152
207
|
```
|
|
153
208
|
|
|
154
|
-
Open the address that wrangler prints and paste the token. A token has 32 characters or more. Its
|
|
209
|
+
`CANVAS_UNSAFE_STATIC_TOKENS=1` turns on the dev fallback: see "Auth". Open the address that wrangler prints and paste the token. A static token has 32 characters or more. Its
|
|
155
210
|
grant sets the name, the kind (a person or an agent), and the role. A `viewer` token reads and
|
|
156
211
|
cannot change a board. The token stays in the tab's session storage.
|
|
157
212
|
|
|
@@ -239,11 +294,11 @@ at the same time keep the last write. The board list shows the first 100 boards.
|
|
|
239
294
|
- A CLI over a local file store. Several processes can write at once.
|
|
240
295
|
- An MCP server (stdio) over the same service as the CLI.
|
|
241
296
|
- A Worker (`agentic-canvas/worker`) under `/api/v1/canvas`. Boards live in D1 and images in R2
|
|
242
|
-
(EU jurisdiction), through the shared `Store` and `Blobs` ports. Set `
|
|
243
|
-
|
|
244
|
-
Routes: `POST /boards`, `GET /boards/:id`, `GET /boards/:id/svg`, `POST /boards/:id/shapes`,
|
|
297
|
+
(EU jurisdiction), through the shared `Store` and `Blobs` ports. Set `CANVAS_REGION`. Callers sign in with
|
|
298
|
+
`agentic-identity` credentials (see "Auth" below). `CANVAS_API_TOKENS` is a dev fallback only.
|
|
299
|
+
Routes: `POST /boards`, `GET /boards/:id`, `DELETE /boards/:id`, `GET /boards/:id/svg`, `POST /boards/:id/shapes`,
|
|
245
300
|
`/connectors`, `/flow`, `/move`, `GET /boards`, `PATCH /boards/:id/elements/:element`, `DELETE /boards/:id/elements/:element`, `POST /images`,
|
|
246
|
-
`GET /images/:id`. An image is stored under the SHA-256 of its bytes, and the reply gives
|
|
301
|
+
`GET /images/:id`. An editor may delete a board. The Worker also erases its live room. An image is stored under the SHA-256 of its bytes, and the reply gives
|
|
247
302
|
the `blob:` ref for a shape. A busy database or bucket answers 503: try again.
|
|
248
303
|
- Live rooms (`agentic-canvas/live-worker`, on `agentic-realtime`). One Durable Object holds one
|
|
249
304
|
board. A token may carry `"role": "viewer"`: a viewer reads and cannot write. `GET /boards/:id/ws`
|
|
@@ -304,9 +359,9 @@ old author stays on its elements. Only new writes follow the strict rule.
|
|
|
304
359
|
|
|
305
360
|
| Name | What it is |
|
|
306
361
|
|---|---|
|
|
307
|
-
| `createFileStore(path, options?)`, `openLocalStore(dir)` | A store on one JSON file, or on `canvas.json` in a directory. |
|
|
308
|
-
| `FileStoreError` | The error that
|
|
309
|
-
| `FileStoreOptions` (type) | `lockTimeoutMs`
|
|
362
|
+
| `createFileStore(path, options?)`, `openLocalStore(dir)` | A store on one JSON file, or on `canvas.json` in a directory. It is the file store of agentic-identity. |
|
|
363
|
+
| `FileStoreError` | The error for a file that is not a store file, or a lock that stays held. Its `code` is `DAMAGED` or `LOCKED`. |
|
|
364
|
+
| `FileStoreOptions` (type) | The adapter options. The lock options `lockTimeoutMs` and `staleAfterMs` are gone. |
|
|
310
365
|
| `STORE_FILE` | The file name that `openLocalStore` uses, `canvas.json`. |
|
|
311
366
|
|
|
312
367
|
### `agentic-canvas/live`
|
|
@@ -350,10 +405,13 @@ The stylesheet is a separate export, `agentic-canvas/ui.css`. Load `agentic-desi
|
|
|
350
405
|
|
|
351
406
|
## Limits
|
|
352
407
|
|
|
353
|
-
- Canvas takes principal kinds, regions, and the Store and Blobs ports from `agentic-identity` 0.
|
|
354
|
-
|
|
355
|
-
`
|
|
356
|
-
|
|
408
|
+
- Canvas takes principal kinds, regions, and the Store and Blobs ports from `agentic-identity` 0.6.
|
|
409
|
+
- A board made before sharing has no owner. Identity credentials get 404 on it. Only the dev fallback reaches it. Make it again, or add its
|
|
410
|
+
`canvas` resource and an owner grant with `agentic-identity`.
|
|
411
|
+
- An image is stored under the hash of its bytes, not under a board. Any credential with `canvas:read` can fetch an image if it knows the hash.
|
|
412
|
+
- A link visitor reads the stored board, which a live room saves at its next save. It cannot load `blob:` images.
|
|
413
|
+
- A revoke, or a grant of viewer or commenter, closes the live sockets of the person or agent whose role changed. An agent that acts for that person keeps its socket until it joins again.
|
|
414
|
+
- A link visitor reads. A link never writes. Links are not listed: the token shows once, at creation.
|
|
357
415
|
- A board holds at most 10,000 elements. A flow holds at most 500 nodes and 2,000
|
|
358
416
|
edges.
|
|
359
417
|
- Flow text has no chains (`A -> B -> C`). Write one edge per line.
|
|
@@ -374,13 +432,15 @@ The stylesheet is a separate export, `agentic-canvas/ui.css`. Load `agentic-desi
|
|
|
374
432
|
- `move` writes absolute positions. Two peers that move one shape at the same time do not add up:
|
|
375
433
|
the last writer wins.
|
|
376
434
|
- Frames are plain shapes. They do not own the shapes inside them yet.
|
|
377
|
-
- The store is one file, `canvas.json`, in the store folder.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
435
|
+
- The store is one file, `canvas.json`, in the store folder. It is the file store of
|
|
436
|
+
agentic-identity: a write takes `canvas.json.lock`, and a lock older than 3 seconds is taken
|
|
437
|
+
over, so a writer that is paused for longer can lose its lock and the write then fails with
|
|
438
|
+
`UNAVAILABLE`. A write that clashes with another process is run again, for up to 10 seconds.
|
|
439
|
+
Remove the lock by hand if no process runs. A file written by an older agentic-canvas is
|
|
440
|
+
upgraded in place the first time a store opens it, and an older agentic-canvas cannot read
|
|
441
|
+
the upgraded file. Stop older agentic-canvas processes before you upgrade.
|
|
381
442
|
- Node 22 or newer is needed. `agentic-canvas/file-store` exports `createFileStore(path)` and
|
|
382
|
-
`openLocalStore(dir)`.
|
|
383
|
-
and then still write.
|
|
443
|
+
`openLocalStore(dir)`.
|
|
384
444
|
- A room remembers its authors for good, so a client can stamp an element with the name of anyone who
|
|
385
445
|
once sent an update in that room. A save corrects the stored board and not the room: after a save that
|
|
386
446
|
reports `fixed`, join again. A client that adds a root Yjs type of its own blocks later saves of that room.
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
|
|
9
9
|
// src/version.ts
|
|
10
10
|
import { createRequire } from "module";
|
|
11
|
-
var VERSION = true ? "0.
|
|
11
|
+
var VERSION = true ? "0.8.0" : readPackageVersion();
|
|
12
12
|
|
|
13
13
|
// src/brand.ts
|
|
14
14
|
var BRAND = {
|
|
@@ -104,6 +104,11 @@ function createMcpServer(service, actor) {
|
|
|
104
104
|
{ description: "Read a board as data: nodes, edges, groups, text, and who made what.", inputSchema: { board }, annotations: read },
|
|
105
105
|
tool(async (a) => json(await service.read(a.board)))
|
|
106
106
|
);
|
|
107
|
+
server.registerTool(
|
|
108
|
+
"delete_board",
|
|
109
|
+
{ description: "Delete a whole board for good. It cannot be undone.", inputSchema: { board }, annotations: { destructiveHint: true } },
|
|
110
|
+
tool(async (a) => json(await service.delete(a.board)))
|
|
111
|
+
);
|
|
107
112
|
server.registerTool(
|
|
108
113
|
"add_shape",
|
|
109
114
|
{
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CanvasService,
|
|
3
|
-
MAX_LIVE_BYTES
|
|
4
|
-
|
|
3
|
+
MAX_LIVE_BYTES,
|
|
4
|
+
isBoardId,
|
|
5
|
+
isStoreId
|
|
6
|
+
} from "./chunk-M74ENUWR.js";
|
|
5
7
|
import {
|
|
6
8
|
CanvasError
|
|
7
9
|
} from "./chunk-ZSPZMGHG.js";
|
|
8
10
|
|
|
9
11
|
// src/worker.ts
|
|
10
|
-
import { hasScope, parsePrincipal, parseRegion } from "agentic-identity";
|
|
12
|
+
import { createStoreAuth, hasScope as hasScope2, parsePrincipal, parseRegion } from "agentic-identity";
|
|
11
13
|
import { createD1Store, createR2Blobs, D1_SCHEMA } from "agentic-identity/cloudflare";
|
|
12
14
|
import { acceptWebSocketProtocol, principalFromWebSocketRequest } from "agentic-identity/http";
|
|
13
15
|
|
|
@@ -1788,6 +1790,150 @@ var Hono3 = class extends Hono {
|
|
|
1788
1790
|
}
|
|
1789
1791
|
};
|
|
1790
1792
|
|
|
1793
|
+
// src/access.ts
|
|
1794
|
+
import { createSharing, EraseRefusedError, hasScope, ROLES, SUBJECT_KINDS } from "agentic-identity";
|
|
1795
|
+
var RESOURCE_TYPE = "canvas";
|
|
1796
|
+
var SHARE_ROLES = ROLES.filter((r) => r !== "owner");
|
|
1797
|
+
var BAD_INPUT = /^(give an email or a handle|invalid handle|invalid email|invalid subject( kind "[^"]*")?|the last owner cannot be removed|expiresInMs must be .*|expiresAt must be .*|invalid role .*)$/;
|
|
1798
|
+
function refuse(error) {
|
|
1799
|
+
if (error instanceof EraseRefusedError) throw new CanvasError("INVALID", error.message);
|
|
1800
|
+
if (error instanceof Error && error.message === "forbidden") throw new CanvasError("FORBIDDEN", "you may not do this on this board");
|
|
1801
|
+
if (error instanceof Error && BAD_INPUT.test(error.message)) throw new CanvasError("INVALID", error.message);
|
|
1802
|
+
throw error;
|
|
1803
|
+
}
|
|
1804
|
+
var oneOf = (value, list, name) => {
|
|
1805
|
+
if (typeof value !== "string" || !list.includes(value)) throw new CanvasError("INVALID", `${name} must be one of: ${list.join(", ")}`);
|
|
1806
|
+
return value;
|
|
1807
|
+
};
|
|
1808
|
+
var storeId = (value, name) => {
|
|
1809
|
+
if (typeof value !== "string" || !isStoreId(value)) throw new CanvasError("INVALID", `${name} must be 1 to 128 of A-Z, a-z, 0-9, _ and -, and not like __x__`);
|
|
1810
|
+
return value;
|
|
1811
|
+
};
|
|
1812
|
+
var word = (value, name) => {
|
|
1813
|
+
if (typeof value !== "string" || value === "") throw new CanvasError("INVALID", `${name} must be a non-empty string`);
|
|
1814
|
+
return value;
|
|
1815
|
+
};
|
|
1816
|
+
var lifetime = (value, name) => {
|
|
1817
|
+
if (value === void 0) return void 0;
|
|
1818
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new CanvasError("INVALID", `${name} must be a whole number of milliseconds above 0`);
|
|
1819
|
+
return value;
|
|
1820
|
+
};
|
|
1821
|
+
var CanvasAccess = class {
|
|
1822
|
+
constructor(store, options = {}) {
|
|
1823
|
+
this.store = store;
|
|
1824
|
+
this.sharing = createSharing(store, options);
|
|
1825
|
+
this.now = options.now ?? Date.now;
|
|
1826
|
+
}
|
|
1827
|
+
store;
|
|
1828
|
+
sharing;
|
|
1829
|
+
now;
|
|
1830
|
+
/** A board id that the Store refuses names no board. The gate checks this for a role holder: a token of the dev list skips the gate. */
|
|
1831
|
+
known(id) {
|
|
1832
|
+
if (!isBoardId(id)) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
|
|
1833
|
+
}
|
|
1834
|
+
/** Make the actor the owner of a new board. */
|
|
1835
|
+
async register(ctx, id) {
|
|
1836
|
+
await this.sharing.createResource(ctx, { id, type: RESOURCE_TYPE }).catch((error) => {
|
|
1837
|
+
if (error instanceof Error && error.message === "resource exists") throw new CanvasError("DUPLICATE_ID", `board "${id}" already exists`);
|
|
1838
|
+
if (error instanceof Error && error.message === "resource id has old audit entries") throw new CanvasError("UNAVAILABLE", "this board id was used a moment ago. Try again, or use another id");
|
|
1839
|
+
return refuse(error);
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
/** Remove the sharing records of a board. Needs an owner. */
|
|
1843
|
+
async unregister(ctx, id) {
|
|
1844
|
+
await this.sharing.deleteResource(ctx, id).catch(refuse);
|
|
1845
|
+
}
|
|
1846
|
+
/** True if the board has sharing records: it was made by an identity credential. */
|
|
1847
|
+
async registered(id) {
|
|
1848
|
+
if (!isBoardId(id)) return false;
|
|
1849
|
+
return (await this.store.get("resources", id))?.type === RESOURCE_TYPE;
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* True if the caller may do `action` on the board. A link token adds the role of its link. It writes an audit entry.
|
|
1853
|
+
* An id that belongs to a resource of another type, in a Store shared with another app, is no board: it is denied.
|
|
1854
|
+
*/
|
|
1855
|
+
async allowed(ctx, action, id, linkToken) {
|
|
1856
|
+
if (!await this.registered(id)) return false;
|
|
1857
|
+
return (await this.sharing.can(ctx, action, id, linkToken === void 0 ? {} : { linkToken })).allowed;
|
|
1858
|
+
}
|
|
1859
|
+
/** Throw NOT_FOUND for a caller who cannot read the board, and FORBIDDEN for one who can read it but not do `action`. */
|
|
1860
|
+
async require(ctx, action, id, linkToken) {
|
|
1861
|
+
if (await this.allowed(ctx, action, id, linkToken)) return;
|
|
1862
|
+
if (action === "read" || !await this.allowed(ctx, "read", id, linkToken)) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
|
|
1863
|
+
throw new CanvasError("FORBIDDEN", `this needs the ${action === "admin" ? "owner" : "editor"} role on the board`);
|
|
1864
|
+
}
|
|
1865
|
+
/** The ids of the boards a person or agent holds a role on, the first 100. A credential without `canvas:read` gets none. */
|
|
1866
|
+
async readable(ctx) {
|
|
1867
|
+
const root = ctx.chain[0];
|
|
1868
|
+
if (!root || !SUBJECT_KINDS.includes(root.kind) || !hasScope(ctx.scopes, `${RESOURCE_TYPE}:read`)) return [];
|
|
1869
|
+
const ids = [];
|
|
1870
|
+
let cursor;
|
|
1871
|
+
for (let pages = 0; pages < 10 && ids.length < 100; pages++) {
|
|
1872
|
+
const page = await this.store.query("grants", { where: { subjectKind: root.kind, subjectId: root.id }, limit: 100, ...cursor === void 0 ? {} : { cursor } });
|
|
1873
|
+
for (const { data } of page.items) {
|
|
1874
|
+
if (ids.length >= 100 || typeof data.resourceId !== "string" || !ROLES.includes(data.role)) continue;
|
|
1875
|
+
if (await this.registered(data.resourceId)) ids.push(data.resourceId);
|
|
1876
|
+
}
|
|
1877
|
+
if ((cursor = page.nextCursor) === void 0) break;
|
|
1878
|
+
}
|
|
1879
|
+
return ids;
|
|
1880
|
+
}
|
|
1881
|
+
async grant(ctx, id, input) {
|
|
1882
|
+
this.known(id);
|
|
1883
|
+
const subject = { kind: oneOf(input.subjectKind, SUBJECT_KINDS, "subjectKind"), id: storeId(input.subjectId, "subjectId") };
|
|
1884
|
+
await this.sharing.grant(ctx, id, subject, oneOf(input.role, ROLES, "role")).catch(refuse);
|
|
1885
|
+
return { ok: true };
|
|
1886
|
+
}
|
|
1887
|
+
async revoke(ctx, id, input) {
|
|
1888
|
+
this.known(id);
|
|
1889
|
+
const subject = { kind: oneOf(input.subjectKind, SUBJECT_KINDS, "subjectKind"), id: storeId(input.subjectId, "subjectId") };
|
|
1890
|
+
await this.sharing.revokeGrant(ctx, id, subject).catch(refuse);
|
|
1891
|
+
return { ok: true };
|
|
1892
|
+
}
|
|
1893
|
+
/** The grants and the invites on a board. An invite never shows its email. Links are not listed: a token shows once. */
|
|
1894
|
+
async list(ctx, id) {
|
|
1895
|
+
this.known(id);
|
|
1896
|
+
const [grants, invites] = await Promise.all([this.sharing.listGrants(ctx, id, { limit: 200 }), this.sharing.listInvites(ctx, id, { limit: 200 })]).catch(refuse);
|
|
1897
|
+
return { grants: grants.grants, invites: invites.invites };
|
|
1898
|
+
}
|
|
1899
|
+
async invite(ctx, id, input) {
|
|
1900
|
+
this.known(id);
|
|
1901
|
+
const role = oneOf(input.role, SHARE_ROLES, "role");
|
|
1902
|
+
const expiresInMs = lifetime(input.expiresInMs, "expiresInMs");
|
|
1903
|
+
const who = input.email !== void 0 ? { email: word(input.email, "email") } : input.handle !== void 0 ? { handle: word(input.handle, "handle") } : {};
|
|
1904
|
+
if (input.email !== void 0 && input.handle !== void 0) throw new CanvasError("INVALID", "give an email or a handle, not both");
|
|
1905
|
+
return this.sharing.createInvite(ctx, id, { role, ...who, ...expiresInMs === void 0 ? {} : { expiresInMs } }).catch(refuse);
|
|
1906
|
+
}
|
|
1907
|
+
/** The email comes from a verified sign-in and the handle from the agent's own record, never from the request. */
|
|
1908
|
+
async accept(ctx, inviteId) {
|
|
1909
|
+
if (!isStoreId(inviteId)) throw new CanvasError("FORBIDDEN", "you may not do this on this board");
|
|
1910
|
+
const user = await this.store.get("users", ctx.principalId);
|
|
1911
|
+
const agent = await this.store.get("principals", ctx.principalId);
|
|
1912
|
+
const identity = {
|
|
1913
|
+
...user?.emailVerified === true && typeof user.email === "string" ? { email: user.email } : {},
|
|
1914
|
+
...typeof agent?.handle === "string" ? { handle: agent.handle } : {}
|
|
1915
|
+
};
|
|
1916
|
+
await this.sharing.acceptInvite(ctx, word(inviteId, "id"), identity).catch(refuse);
|
|
1917
|
+
return { ok: true };
|
|
1918
|
+
}
|
|
1919
|
+
/** The token shows once. Only its hash is stored. */
|
|
1920
|
+
async createLink(ctx, id, input) {
|
|
1921
|
+
this.known(id);
|
|
1922
|
+
const role = oneOf(input.role, SHARE_ROLES, "role");
|
|
1923
|
+
const expiresInMs = lifetime(input.expiresInMs, "expiresInMs");
|
|
1924
|
+
if (input.signedInOnly !== void 0 && typeof input.signedInOnly !== "boolean") throw new CanvasError("INVALID", "signedInOnly must be true or false");
|
|
1925
|
+
return this.sharing.createLink(ctx, id, { role, signedInOnly: input.signedInOnly === true, ...expiresInMs === void 0 ? {} : { expiresAt: this.now() + expiresInMs } }).catch(refuse);
|
|
1926
|
+
}
|
|
1927
|
+
/** Revoke a link of this board. A link of another board is NOT_FOUND here, even for an owner of both. */
|
|
1928
|
+
async revokeLink(ctx, id, linkId) {
|
|
1929
|
+
this.known(id);
|
|
1930
|
+
const link = isStoreId(linkId) ? await this.store.get("links", linkId) : null;
|
|
1931
|
+
if (!link || link.resourceId !== id) throw new CanvasError("NOT_FOUND", "no such link on this board");
|
|
1932
|
+
await this.sharing.revokeLink(ctx, linkId).catch(refuse);
|
|
1933
|
+
return { ok: true };
|
|
1934
|
+
}
|
|
1935
|
+
};
|
|
1936
|
+
|
|
1791
1937
|
// src/room-sync.ts
|
|
1792
1938
|
import * as Y from "yjs";
|
|
1793
1939
|
var SEEDER = { kind: "agent", id: "canvas-room" };
|
|
@@ -1902,6 +2048,9 @@ function remember(id, mark) {
|
|
|
1902
2048
|
if (saved.size > 500) saved.clear();
|
|
1903
2049
|
saved.set(id, mark);
|
|
1904
2050
|
}
|
|
2051
|
+
function forget(id) {
|
|
2052
|
+
saved.delete(id);
|
|
2053
|
+
}
|
|
1905
2054
|
async function pull(live, service, id, force = false) {
|
|
1906
2055
|
const { updates, compactions } = await live.meta(id);
|
|
1907
2056
|
const mark = `${updates}:${compactions}`;
|
|
@@ -1979,11 +2128,52 @@ function tokenAuth(env) {
|
|
|
1979
2128
|
}
|
|
1980
2129
|
};
|
|
1981
2130
|
}
|
|
1982
|
-
|
|
1983
|
-
|
|
2131
|
+
var staticTokensOn = (env) => env.CANVAS_UNSAFE_STATIC_TOKENS === "1";
|
|
2132
|
+
function workerAuth(env, store) {
|
|
2133
|
+
const auths = [];
|
|
2134
|
+
if (store) auths.push(createStoreAuth(store));
|
|
2135
|
+
const devAuth = staticTokensOn(env) ? tokenAuth(env) : void 0;
|
|
2136
|
+
if (devAuth) auths.push(devAuth);
|
|
2137
|
+
let dev = false;
|
|
2138
|
+
let failure;
|
|
2139
|
+
const auth = {
|
|
2140
|
+
async authenticate(request) {
|
|
2141
|
+
for (const auth2 of auths) {
|
|
2142
|
+
let found;
|
|
2143
|
+
try {
|
|
2144
|
+
found = await auth2.authenticate(request);
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
failure = error;
|
|
2147
|
+
return null;
|
|
2148
|
+
}
|
|
2149
|
+
if (!found) continue;
|
|
2150
|
+
dev = auth2 === devAuth;
|
|
2151
|
+
let step;
|
|
2152
|
+
try {
|
|
2153
|
+
step = parsePrincipal({ kind: found.kind, id: found.principalId });
|
|
2154
|
+
} catch {
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
if (step.kind === SEEDER.kind && step.id === SEEDER.id) return null;
|
|
2158
|
+
return found;
|
|
2159
|
+
}
|
|
2160
|
+
return null;
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
return { auth, failure: () => failure, dev: () => dev };
|
|
2164
|
+
}
|
|
2165
|
+
async function authenticate(request, env, store) {
|
|
2166
|
+
const found = await identify(request, env, store);
|
|
2167
|
+
return found && { actor: found.actor, role: found.role };
|
|
2168
|
+
}
|
|
2169
|
+
async function identify(request, env, store) {
|
|
2170
|
+
const { auth, failure, dev } = workerAuth(env, store);
|
|
2171
|
+
const found = await principalFromWebSocketRequest(request, auth, { protocol: WS_PROTOCOL, scope: "canvas:read" });
|
|
2172
|
+
if (failure() !== void 0) throw failure();
|
|
1984
2173
|
if (found instanceof Response) return null;
|
|
1985
|
-
return { actor: { kind: found.kind, id: found.principalId }, role:
|
|
2174
|
+
return { actor: { kind: found.kind, id: found.principalId }, role: hasScope2(found.scopes, "canvas:write") ? "editor" : "viewer", principal: found, dev: dev() };
|
|
1986
2175
|
}
|
|
2176
|
+
var svgResponse = (svg) => new Response(svg, { headers: { "content-type": "image/svg+xml", "x-content-type-options": "nosniff", "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; img-src data:" } });
|
|
1987
2177
|
function sniffImage(bytes) {
|
|
1988
2178
|
const at = (offset, ...sig) => sig.every((b, i) => bytes[offset + i] === b);
|
|
1989
2179
|
if (at(0, 137, 80, 78, 71, 13, 10, 26, 10)) return "image/png";
|
|
@@ -2052,8 +2242,7 @@ var loggable = (error) => error instanceof Error ? LOGGABLE.exec(error.message)?
|
|
|
2052
2242
|
function createApp(options = {}) {
|
|
2053
2243
|
const app2 = new Hono3();
|
|
2054
2244
|
app2.get("/health", (c) => c.json({ ok: true }));
|
|
2055
|
-
const
|
|
2056
|
-
api.use("*", async (c, next) => {
|
|
2245
|
+
const open = async (c) => {
|
|
2057
2246
|
let region;
|
|
2058
2247
|
try {
|
|
2059
2248
|
region = parseRegion(c.env.CANVAS_REGION ?? "");
|
|
@@ -2061,14 +2250,36 @@ function createApp(options = {}) {
|
|
|
2061
2250
|
return fail("NOT_CONFIGURED", 'set CANVAS_REGION to "eu", "us", or "unknown"', 503);
|
|
2062
2251
|
}
|
|
2063
2252
|
if (!c.env.DB || !c.env.IMAGES) return fail("NOT_CONFIGURED", "bind DB (D1) and IMAGES (R2)", 503);
|
|
2064
|
-
|
|
2253
|
+
if (c.env.CANVAS_API_TOKENS && !staticTokensOn(c.env)) return fail("NOT_CONFIGURED", "CANVAS_API_TOKENS is a dev fallback. Set CANVAS_UNSAFE_STATIC_TOKENS=1 to use it, or remove it and issue agentic-identity credentials", 503);
|
|
2254
|
+
await ready(c.env.DB);
|
|
2255
|
+
return createD1Store(c.env.DB, { region });
|
|
2256
|
+
};
|
|
2257
|
+
const shared = new Hono3();
|
|
2258
|
+
const visit = async (c, route) => {
|
|
2259
|
+
const store = await open(c);
|
|
2260
|
+
if (store instanceof Response) return store;
|
|
2261
|
+
const who = await identify(c.req.raw, c.env, store);
|
|
2262
|
+
if (!who && c.req.header("authorization")) return fail("UNAUTHORIZED", "send a valid bearer token", 401);
|
|
2263
|
+
const id = c.req.param("id");
|
|
2264
|
+
await new CanvasAccess(store).require(who?.principal ?? null, "read", id, c.req.header("x-link-token") ?? "");
|
|
2265
|
+
return route(new CanvasService(store));
|
|
2266
|
+
};
|
|
2267
|
+
shared.get("/:id", (c) => visit(c, async (service) => c.json(await service.read(c.req.param("id")))));
|
|
2268
|
+
shared.get("/:id/svg", (c) => visit(c, async (service) => svgResponse(await service.exportSvg(c.req.param("id")))));
|
|
2269
|
+
app2.route("/api/v1/canvas/shared", shared);
|
|
2270
|
+
const api = new Hono3();
|
|
2271
|
+
api.use("*", async (c, next) => {
|
|
2272
|
+
const store = await open(c);
|
|
2273
|
+
if (store instanceof Response) return store;
|
|
2274
|
+
const who = await identify(c.req.raw, c.env, store);
|
|
2065
2275
|
if (!who) return fail("UNAUTHORIZED", "send a valid bearer token", 401);
|
|
2066
|
-
if (who.role === "viewer" && c.req.method !== "GET") return fail("FORBIDDEN", "this token is a viewer; it cannot change a board", 403);
|
|
2276
|
+
if (who.role === "viewer" && c.req.method !== "GET" && !(c.req.method === "POST" && c.req.path === "/api/v1/canvas/invites/accept")) return fail("FORBIDDEN", "this token is a viewer; it cannot change a board", 403);
|
|
2067
2277
|
c.set("actor", who.actor);
|
|
2068
2278
|
c.set("role", who.role);
|
|
2069
|
-
|
|
2070
|
-
c.set("service", new CanvasService(
|
|
2071
|
-
c.set("
|
|
2279
|
+
c.set("caller", who);
|
|
2280
|
+
c.set("service", new CanvasService(store));
|
|
2281
|
+
c.set("access", new CanvasAccess(store));
|
|
2282
|
+
c.set("blobs", createR2Blobs(c.env.IMAGES, { region: parseRegion(c.env.CANVAS_REGION ?? ""), urlBase: new URL(c.req.url).origin, urlSecret: `${crypto.randomUUID()}${crypto.randomUUID()}` }));
|
|
2072
2283
|
await next();
|
|
2073
2284
|
});
|
|
2074
2285
|
api.get("/me", (c) => c.json({ actor: c.var.actor.id, kind: c.var.actor.kind, role: c.var.role }));
|
|
@@ -2086,16 +2297,39 @@ function createApp(options = {}) {
|
|
|
2086
2297
|
}
|
|
2087
2298
|
};
|
|
2088
2299
|
const sync = async (c, next) => {
|
|
2089
|
-
if (!/\/(ws|checkpoint)$/.test(new URL(c.req.url).pathname))
|
|
2300
|
+
if (!/\/(ws|checkpoint)$/.test(new URL(c.req.url).pathname)) {
|
|
2301
|
+
await saveRoom(c, false).catch((error) => {
|
|
2302
|
+
if (!(c.req.method === "DELETE" && error instanceof CanvasError && error.code === "NOT_FOUND")) throw error;
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2090
2305
|
await next();
|
|
2091
2306
|
};
|
|
2307
|
+
const ADMIN = /* @__PURE__ */ new Set(["access", "grants", "invites", "links"]);
|
|
2308
|
+
const WRITE = /* @__PURE__ */ new Set(["shapes", "connectors", "flow", "move", "elements", "checkpoint"]);
|
|
2309
|
+
const needs = (method, path) => {
|
|
2310
|
+
let decoded;
|
|
2311
|
+
try {
|
|
2312
|
+
decoded = decodeURI(path);
|
|
2313
|
+
} catch {
|
|
2314
|
+
return "admin";
|
|
2315
|
+
}
|
|
2316
|
+
const part = decoded.split("/boards/")[1].split("/");
|
|
2317
|
+
if (part.length === 1) return method === "DELETE" ? "admin" : method === "GET" ? "read" : "write";
|
|
2318
|
+
if (ADMIN.has(part[1])) return "admin";
|
|
2319
|
+
return WRITE.has(part[1]) || method !== "GET" ? "write" : "read";
|
|
2320
|
+
};
|
|
2321
|
+
api.use("/boards/:id/*", async (c, next) => {
|
|
2322
|
+
if (!c.var.caller.dev) await c.var.access.require(c.var.caller.principal, needs(c.req.method, new URL(c.req.url).pathname), c.req.param("id"));
|
|
2323
|
+
await next();
|
|
2324
|
+
});
|
|
2092
2325
|
api.use("/boards/:id/*", sync);
|
|
2093
2326
|
api.get("/boards/:id/ws", async (c) => {
|
|
2094
2327
|
if (!options.live) return fail("NOT_FOUND", "this server hosts no live rooms", 404);
|
|
2095
2328
|
if (c.req.header("upgrade") !== "websocket") return fail("UPGRADE_REQUIRED", "open this route as a WebSocket", 426);
|
|
2096
2329
|
const id = c.req.param("id");
|
|
2097
2330
|
await saveRoom(c, true);
|
|
2098
|
-
const { actor,
|
|
2331
|
+
const { actor, caller } = c.var;
|
|
2332
|
+
const role = caller.dev ? c.var.role : await c.var.access.allowed(caller.principal, "write", id) ? "editor" : "viewer";
|
|
2099
2333
|
const opened = await options.live(c.env).connect(id, c.req.raw, { principal: { id: actor.id, kind: actor.kind }, role, name: actor.id });
|
|
2100
2334
|
return acceptWebSocketProtocol(c.req.raw, opened, WS_PROTOCOL);
|
|
2101
2335
|
});
|
|
@@ -2108,15 +2342,64 @@ function createApp(options = {}) {
|
|
|
2108
2342
|
if (!update) throw new CanvasError("INVALID", `the update must be at most ${MAX_LIVE_BYTES} bytes`);
|
|
2109
2343
|
return c.json(await handSave(options.live(c.env), c.var.service, id, update, c.var.actor));
|
|
2110
2344
|
});
|
|
2111
|
-
api.get("/boards", async (c) => c.json({ boards: await c.var.service.list() }));
|
|
2345
|
+
api.get("/boards", async (c) => c.json({ boards: c.var.caller.dev ? await c.var.service.list() : await c.var.service.summaries(await c.var.access.readable(c.var.caller.principal)) }));
|
|
2112
2346
|
api.post("/boards", async (c) => {
|
|
2113
2347
|
const b = await jsonBody(c);
|
|
2114
2348
|
const id = b.id === void 0 ? void 0 : text(b.id, "id");
|
|
2115
2349
|
const title = b.title === void 0 ? void 0 : text(b.title, "title");
|
|
2116
|
-
|
|
2350
|
+
if (c.var.caller.dev && id !== void 0 && await c.var.access.registered(id)) throw new CanvasError("DUPLICATE_ID", `board "${id}" already exists`);
|
|
2351
|
+
const created = await c.var.service.create(c.var.actor, { id, title });
|
|
2352
|
+
if (!c.var.caller.dev) {
|
|
2353
|
+
await c.var.access.register(c.var.caller.principal, created.id).catch(async (error) => {
|
|
2354
|
+
await c.var.service.delete(created.id).catch((failed) => console.error(JSON.stringify({ level: "error", op: "undo-create", message: loggable(failed) })));
|
|
2355
|
+
throw error;
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
return c.json(created, 201);
|
|
2117
2359
|
});
|
|
2118
2360
|
api.get("/boards/:id", async (c) => c.json(await c.var.service.read(c.req.param("id"))));
|
|
2119
|
-
api.
|
|
2361
|
+
api.delete("/boards/:id", async (c) => {
|
|
2362
|
+
const id = c.req.param("id");
|
|
2363
|
+
const { dev, principal } = c.var.caller;
|
|
2364
|
+
if (dev && await c.var.access.registered(id)) throw new CanvasError("FORBIDDEN", "this board has owners: only an owner deletes it");
|
|
2365
|
+
const present = await c.var.service.assertExists(id).then(() => true, (error) => {
|
|
2366
|
+
if (error instanceof CanvasError && error.code === "NOT_FOUND" && !dev) return false;
|
|
2367
|
+
throw error;
|
|
2368
|
+
});
|
|
2369
|
+
const live = options.live?.(c.env);
|
|
2370
|
+
if (live) await live.erase(id);
|
|
2371
|
+
const deleted = present ? await c.var.service.delete(id) : { deleted: id };
|
|
2372
|
+
if (!dev) await c.var.access.unregister(principal, id);
|
|
2373
|
+
if (live) {
|
|
2374
|
+
await live.erase(id);
|
|
2375
|
+
forget(id);
|
|
2376
|
+
}
|
|
2377
|
+
console.log(JSON.stringify({ level: "info", op: "delete-board", actorKind: c.var.actor.kind }));
|
|
2378
|
+
return c.json(deleted);
|
|
2379
|
+
});
|
|
2380
|
+
api.get("/boards/:id/svg", async (c) => svgResponse(await c.var.service.exportSvg(c.req.param("id"))));
|
|
2381
|
+
api.get("/boards/:id/access", async (c) => c.json(await c.var.access.list(c.var.caller.principal, c.req.param("id"))));
|
|
2382
|
+
api.post("/boards/:id/grants", async (c) => {
|
|
2383
|
+
const id = c.req.param("id");
|
|
2384
|
+
const input = await jsonBody(c);
|
|
2385
|
+
const done = await c.var.access.grant(c.var.caller.principal, id, input);
|
|
2386
|
+
if (options.live && (input.role === "viewer" || input.role === "commenter") && typeof input.subjectId === "string") {
|
|
2387
|
+
await options.live(c.env).revoke(id, input.subjectId).catch((error) => console.error(JSON.stringify({ level: "error", op: "revoke-room", message: loggable(error) })));
|
|
2388
|
+
}
|
|
2389
|
+
return c.json(done);
|
|
2390
|
+
});
|
|
2391
|
+
api.delete("/boards/:id/grants", async (c) => {
|
|
2392
|
+
const id = c.req.param("id");
|
|
2393
|
+
const subjectKind = c.req.query("subjectKind");
|
|
2394
|
+
const subjectId = c.req.query("subjectId");
|
|
2395
|
+
const done = await c.var.access.revoke(c.var.caller.principal, id, { subjectKind, subjectId });
|
|
2396
|
+
if (options.live && subjectId) await options.live(c.env).revoke(id, subjectId).catch((error) => console.error(JSON.stringify({ level: "error", op: "revoke-room", message: loggable(error) })));
|
|
2397
|
+
return c.json(done);
|
|
2398
|
+
});
|
|
2399
|
+
api.post("/boards/:id/invites", async (c) => c.json(await c.var.access.invite(c.var.caller.principal, c.req.param("id"), await jsonBody(c)), 201));
|
|
2400
|
+
api.post("/boards/:id/links", async (c) => c.json(await c.var.access.createLink(c.var.caller.principal, c.req.param("id"), await jsonBody(c)), 201));
|
|
2401
|
+
api.delete("/boards/:id/links/:link", async (c) => c.json(await c.var.access.revokeLink(c.var.caller.principal, c.req.param("id"), c.req.param("link"))));
|
|
2402
|
+
api.post("/invites/accept", async (c) => c.json(await c.var.access.accept(c.var.caller.principal, text((await jsonBody(c)).id, "id"))));
|
|
2120
2403
|
api.post("/boards/:id/shapes", async (c) => c.json(await c.var.service.add(c.req.param("id"), await jsonBody(c), c.var.actor), 201));
|
|
2121
2404
|
api.post("/boards/:id/connectors", async (c) => c.json(await c.var.service.connect(c.req.param("id"), await jsonBody(c), c.var.actor), 201));
|
|
2122
2405
|
api.post("/boards/:id/flow", async (c) => c.json(await c.var.service.flow(c.req.param("id"), text((await jsonBody(c)).text, "text"), c.var.actor)));
|
|
@@ -2176,6 +2459,7 @@ var worker_default = { fetch: (request, env, ctx) => app.fetch(request, env, ctx
|
|
|
2176
2459
|
export {
|
|
2177
2460
|
WS_PROTOCOL,
|
|
2178
2461
|
authenticate,
|
|
2462
|
+
identify,
|
|
2179
2463
|
sniffImage,
|
|
2180
2464
|
createApp,
|
|
2181
2465
|
worker_default
|