agentic-canvas 0.6.1 → 0.7.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 CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  A shared whiteboard for diagrams, drawn by people and agents.
9
9
 
10
- > Status: 0.6.0. The board model, the agent operations, the exports, the CLI, the
10
+ > Status: 0.7.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`. Run `npx agentic-canvas --help`.
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,37 @@ 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 tokens are a Worker secret and are not in the repo. To run your
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.
134
159
 
135
160
  ## UI
136
161
 
@@ -146,12 +171,13 @@ npm ci
146
171
  npm run build # writes the UI to ui-dist/
147
172
  cat > .dev.vars <<'EOF2'
148
173
  CANVAS_REGION=unknown
174
+ CANVAS_UNSAFE_STATIC_TOKENS=1
149
175
  CANVAS_API_TOKENS={"ada-token-0123456789-0123456789-0123456789":{"actor":"ada","kind":"human"}}
150
176
  EOF2
151
177
  WRANGLER_SEND_METRICS=false npx wrangler dev
152
178
  ```
153
179
 
154
- Open the address that wrangler prints and paste the token. A token has 32 characters or more. Its
180
+ `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
181
  grant sets the name, the kind (a person or an agent), and the role. A `viewer` token reads and
156
182
  cannot change a board. The token stays in the tab's session storage.
157
183
 
@@ -239,11 +265,11 @@ at the same time keep the last write. The board list shows the first 100 boards.
239
265
  - A CLI over a local file store. Several processes can write at once.
240
266
  - An MCP server (stdio) over the same service as the CLI.
241
267
  - 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 `CANVAS_API_TOKENS`
243
- (a JSON map of token to `{ actor, kind }`, tokens of 32 characters or more) and `CANVAS_REGION`.
244
- Routes: `POST /boards`, `GET /boards/:id`, `GET /boards/:id/svg`, `POST /boards/:id/shapes`,
268
+ (EU jurisdiction), through the shared `Store` and `Blobs` ports. Set `CANVAS_REGION`. Callers sign in with
269
+ `agentic-identity` credentials (see "Auth" below). `CANVAS_API_TOKENS` is a dev fallback only.
270
+ Routes: `POST /boards`, `GET /boards/:id`, `DELETE /boards/:id`, `GET /boards/:id/svg`, `POST /boards/:id/shapes`,
245
271
  `/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
272
+ `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
273
  the `blob:` ref for a shape. A busy database or bucket answers 503: try again.
248
274
  - Live rooms (`agentic-canvas/live-worker`, on `agentic-realtime`). One Durable Object holds one
249
275
  board. A token may carry `"role": "viewer"`: a viewer reads and cannot write. `GET /boards/:id/ws`
@@ -350,9 +376,8 @@ The stylesheet is a separate export, `agentic-canvas/ui.css`. Load `agentic-desi
350
376
 
351
377
  ## Limits
352
378
 
353
- - Canvas takes principal kinds, regions, and the Store and Blobs ports from `agentic-identity` 0.5.
354
- It does not use the sharing API yet. Its editor and viewer tokens are a separate table in
355
- `CANVAS_API_TOKENS`. It has no board delete or erase. When it adopts sharing, grants are keyed
379
+ - Canvas takes principal kinds, regions, and the Store and Blobs ports from `agentic-identity` 0.6.
380
+ It does not use the sharing API yet: every principal with `canvas:write` reaches every board. When it adopts sharing, grants are keyed
356
381
  on kind and id, and a delete calls `deleteResource`.
357
382
  - A board holds at most 10,000 elements. A flow holds at most 500 nodes and 2,000
358
383
  edges.
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  // src/version.ts
10
10
  import { createRequire } from "module";
11
- var VERSION = true ? "0.6.1" : readPackageVersion();
11
+ var VERSION = true ? "0.7.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,13 @@
1
1
  import {
2
2
  CanvasService,
3
3
  MAX_LIVE_BYTES
4
- } from "./chunk-66FFQMHI.js";
4
+ } from "./chunk-GPSOLW2Y.js";
5
5
  import {
6
6
  CanvasError
7
7
  } from "./chunk-ZSPZMGHG.js";
8
8
 
9
9
  // src/worker.ts
10
- import { hasScope, parsePrincipal, parseRegion } from "agentic-identity";
10
+ import { createStoreAuth, hasScope, parsePrincipal, parseRegion } from "agentic-identity";
11
11
  import { createD1Store, createR2Blobs, D1_SCHEMA } from "agentic-identity/cloudflare";
12
12
  import { acceptWebSocketProtocol, principalFromWebSocketRequest } from "agentic-identity/http";
13
13
 
@@ -1902,6 +1902,9 @@ function remember(id, mark) {
1902
1902
  if (saved.size > 500) saved.clear();
1903
1903
  saved.set(id, mark);
1904
1904
  }
1905
+ function forget(id) {
1906
+ saved.delete(id);
1907
+ }
1905
1908
  async function pull(live, service, id, force = false) {
1906
1909
  const { updates, compactions } = await live.meta(id);
1907
1910
  const mark = `${updates}:${compactions}`;
@@ -1979,8 +1982,41 @@ function tokenAuth(env) {
1979
1982
  }
1980
1983
  };
1981
1984
  }
1982
- async function authenticate(request, env) {
1983
- const found = await principalFromWebSocketRequest(request, tokenAuth(env), { protocol: WS_PROTOCOL });
1985
+ var staticTokensOn = (env) => env.CANVAS_UNSAFE_STATIC_TOKENS === "1";
1986
+ function workerAuth(env, store) {
1987
+ const auths = [];
1988
+ if (store) auths.push(createStoreAuth(store));
1989
+ if (staticTokensOn(env)) auths.push(tokenAuth(env));
1990
+ let failure;
1991
+ const auth = {
1992
+ async authenticate(request) {
1993
+ for (const auth2 of auths) {
1994
+ let found;
1995
+ try {
1996
+ found = await auth2.authenticate(request);
1997
+ } catch (error) {
1998
+ failure = error;
1999
+ return null;
2000
+ }
2001
+ if (!found) continue;
2002
+ let step;
2003
+ try {
2004
+ step = parsePrincipal({ kind: found.kind, id: found.principalId });
2005
+ } catch {
2006
+ return null;
2007
+ }
2008
+ if (step.kind === SEEDER.kind && step.id === SEEDER.id) return null;
2009
+ return found;
2010
+ }
2011
+ return null;
2012
+ }
2013
+ };
2014
+ return { auth, failure: () => failure };
2015
+ }
2016
+ async function authenticate(request, env, store) {
2017
+ const { auth, failure } = workerAuth(env, store);
2018
+ const found = await principalFromWebSocketRequest(request, auth, { protocol: WS_PROTOCOL, scope: "canvas:read" });
2019
+ if (failure() !== void 0) throw failure();
1984
2020
  if (found instanceof Response) return null;
1985
2021
  return { actor: { kind: found.kind, id: found.principalId }, role: hasScope(found.scopes, "canvas:write") ? "editor" : "viewer" };
1986
2022
  }
@@ -2061,13 +2097,15 @@ function createApp(options = {}) {
2061
2097
  return fail("NOT_CONFIGURED", 'set CANVAS_REGION to "eu", "us", or "unknown"', 503);
2062
2098
  }
2063
2099
  if (!c.env.DB || !c.env.IMAGES) return fail("NOT_CONFIGURED", "bind DB (D1) and IMAGES (R2)", 503);
2064
- const who = await authenticate(c.req.raw, c.env);
2100
+ 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);
2101
+ await ready(c.env.DB);
2102
+ const store = createD1Store(c.env.DB, { region });
2103
+ const who = await authenticate(c.req.raw, c.env, store);
2065
2104
  if (!who) return fail("UNAUTHORIZED", "send a valid bearer token", 401);
2066
2105
  if (who.role === "viewer" && c.req.method !== "GET") return fail("FORBIDDEN", "this token is a viewer; it cannot change a board", 403);
2067
2106
  c.set("actor", who.actor);
2068
2107
  c.set("role", who.role);
2069
- await ready(c.env.DB);
2070
- c.set("service", new CanvasService(createD1Store(c.env.DB, { region })));
2108
+ c.set("service", new CanvasService(store));
2071
2109
  c.set("blobs", createR2Blobs(c.env.IMAGES, { region, urlBase: new URL(c.req.url).origin, urlSecret: `${crypto.randomUUID()}${crypto.randomUUID()}` }));
2072
2110
  await next();
2073
2111
  });
@@ -2116,6 +2154,19 @@ function createApp(options = {}) {
2116
2154
  return c.json(await c.var.service.create(c.var.actor, { id, title }), 201);
2117
2155
  });
2118
2156
  api.get("/boards/:id", async (c) => c.json(await c.var.service.read(c.req.param("id"))));
2157
+ api.delete("/boards/:id", async (c) => {
2158
+ const id = c.req.param("id");
2159
+ await c.var.service.assertExists(id);
2160
+ const live = options.live?.(c.env);
2161
+ if (live) await live.erase(id);
2162
+ const deleted = await c.var.service.delete(id);
2163
+ if (live) {
2164
+ await live.erase(id);
2165
+ forget(id);
2166
+ }
2167
+ console.log(JSON.stringify({ level: "info", op: "delete-board", actorKind: c.var.actor.kind }));
2168
+ return c.json(deleted);
2169
+ });
2119
2170
  api.get("/boards/:id/svg", async (c) => new Response(await c.var.service.exportSvg(c.req.param("id")), { headers: { "content-type": "image/svg+xml", "x-content-type-options": "nosniff", "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; img-src data:" } }));
2120
2171
  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
2172
  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));
@@ -65,6 +65,15 @@ var CanvasService = class {
65
65
  }
66
66
  });
67
67
  }
68
+ /** Delete a board and everything on it. Throws NOT_FOUND unless the board exists. The Worker erases the live room too. */
69
+ async delete(id) {
70
+ boardKey(id);
71
+ return this.store.transaction(async (tx) => {
72
+ if (!await tx.get(COLLECTION, id)) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
73
+ await tx.delete(COLLECTION, id);
74
+ return { deleted: id };
75
+ });
76
+ }
68
77
  /** Throws NOT_FOUND unless the board exists. A live room opens only for a board that exists. */
69
78
  async assertExists(id) {
70
79
  boardKey(id);
package/dist/cli.js CHANGED
@@ -5,13 +5,13 @@ import {
5
5
  operatorActor,
6
6
  runMcp,
7
7
  svgToPng
8
- } from "./chunk-VFJJGXTK.js";
8
+ } from "./chunk-3QW4UF2R.js";
9
9
  import {
10
10
  openLocalStore
11
11
  } from "./chunk-UKST5A57.js";
12
12
  import {
13
13
  CanvasService
14
- } from "./chunk-66FFQMHI.js";
14
+ } from "./chunk-GPSOLW2Y.js";
15
15
  import "./chunk-NWQED42L.js";
16
16
  import {
17
17
  CanvasError
@@ -36,6 +36,7 @@ Commands:
36
36
  connect <board> <from> <to> [--text <t>] Join two shapes with an arrow.
37
37
  update <board> <element> [--x --y --w --h --text --fill --stroke] Change fields of an element. Resize with --w and --h.
38
38
  move <board> <element> --dx <n> --dy <n> Move an element or a group.
39
+ delete <board> Delete a whole board for good.
39
40
  rm <board> <element> Delete an element.
40
41
  flow <board> --text <t> | --file <p> Draw a flowchart from text ("A -> B: label"). Use --file - for stdin.
41
42
  export <board> [--format svg|png] [--out <path>] Print SVG, or write PNG to --out.
@@ -141,6 +142,10 @@ async function run(argv) {
141
142
  case "move":
142
143
  need(2, "<board> <element> --dx <n> --dy <n>");
143
144
  return print(await service.move(board, rest[0], num(v.dx) ?? Number.NaN, num(v.dy) ?? Number.NaN, actor()));
145
+ case "delete":
146
+ need(1, "<board>");
147
+ actor();
148
+ return print(await service.delete(board));
144
149
  case "rm":
145
150
  need(2, "<board> <element>");
146
151
  return print(await service.remove(board, rest[0]));
package/dist/client.d.ts CHANGED
@@ -81,6 +81,10 @@ interface CanvasClient {
81
81
  readBoard(id: string, options?: CallOptions): Promise<BoardData & {
82
82
  id: string;
83
83
  }>;
84
+ /** Delete a board for good, with its live room. Returns the id. */
85
+ deleteBoard(id: string, options?: CallOptions): Promise<{
86
+ deleted: string;
87
+ }>;
84
88
  /** The board as an SVG document. */
85
89
  exportSvg(id: string, options?: CallOptions): Promise<string>;
86
90
  addShape(id: string, shape: ShapeInput, options?: CallOptions): Promise<Shape>;
package/dist/client.js CHANGED
@@ -58,6 +58,7 @@ function createClient(options) {
58
58
  listBoards: (o) => json("GET", "/boards", void 0, o),
59
59
  createBoard: (input = {}, o) => json("POST", "/boards", input, o),
60
60
  readBoard: (id, o) => json("GET", `/boards/${seg(id)}`, void 0, o),
61
+ deleteBoard: (id, o) => json("DELETE", `/boards/${seg(id)}`, void 0, o),
61
62
  exportSvg: async (id, o) => (await send("GET", `/boards/${seg(id)}/svg`, {}, o)).text(),
62
63
  addShape: (id, shape, o) => json("POST", `/boards/${seg(id)}/shapes`, shape, o),
63
64
  addConnector: (id, connector, o) => json("POST", `/boards/${seg(id)}/connectors`, connector, o),
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { B as Board, P as Principal } from './read-CeWoSAAy.js';
2
2
  export { a as BoardData, b as CanvasElement, e as CanvasError, C as Connector, f as EdgeData, d as Endpoint, E as ErrorCode, G as Group, M as MAX_ELEMENTS, g as MAX_TEXT, N as NodeData, h as SHAPE_TYPES, S as Shape, c as ShapeType, p as principalSchema, r as readBoard } from './read-CeWoSAAy.js';
3
- import { B as BoardAccess } from './service-Ckz-3qhK.js';
4
- export { C as CanvasService, D as DrawOptions, F as FlowEdge, a as FlowGraph, b as FlowNode, M as MAX_FLOW_EDGES, c as MAX_FLOW_NODES, P as Placement, d as drawFlow, l as layoutFlow, p as parseFlow } from './service-Ckz-3qhK.js';
3
+ import { B as BoardAccess } from './service-D9WzMLH1.js';
4
+ export { C as CanvasService, D as DrawOptions, F as FlowEdge, a as FlowGraph, b as FlowNode, M as MAX_FLOW_EDGES, c as MAX_FLOW_NODES, P as Placement, d as drawFlow, l as layoutFlow, p as parseFlow } from './service-D9WzMLH1.js';
5
5
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
6
  export { FileStoreOptions, createFileStore, openLocalStore } from './store-file.js';
7
7
  export { PRINCIPAL_KINDS, PrincipalKind, isMachineMade } from 'agentic-identity';
package/dist/index.js CHANGED
@@ -6,14 +6,14 @@ import {
6
6
  operatorActor,
7
7
  runMcp,
8
8
  svgToPng
9
- } from "./chunk-VFJJGXTK.js";
9
+ } from "./chunk-3QW4UF2R.js";
10
10
  import {
11
11
  createFileStore,
12
12
  openLocalStore
13
13
  } from "./chunk-UKST5A57.js";
14
14
  import {
15
15
  CanvasService
16
- } from "./chunk-66FFQMHI.js";
16
+ } from "./chunk-GPSOLW2Y.js";
17
17
  import "./chunk-NWQED42L.js";
18
18
  import {
19
19
  Board,
@@ -8,7 +8,7 @@ import 'hono';
8
8
  import './read-CeWoSAAy.js';
9
9
  import 'yjs';
10
10
  import 'zod';
11
- import './service-Ckz-3qhK.js';
11
+ import './service-D9WzMLH1.js';
12
12
 
13
13
  type Namespace = Parameters<typeof createDurableObjectRealtime>[0];
14
14
  interface LiveEnv extends WorkerEnv {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createApp
3
- } from "./chunk-PH67TSYW.js";
4
- import "./chunk-66FFQMHI.js";
3
+ } from "./chunk-EH7WCMK6.js";
4
+ import "./chunk-GPSOLW2Y.js";
5
5
  import "./chunk-NWQED42L.js";
6
6
  import {
7
7
  CanvasError
package/dist/live.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { B as Board, P as Principal, a as BoardData, S as Shape, C as Connector, b as CanvasElement } from './read-CeWoSAAy.js';
2
2
  import { Peer } from 'agentic-realtime/client';
3
- import { B as BoardAccess, D as DrawOptions } from './service-Ckz-3qhK.js';
3
+ import { B as BoardAccess, D as DrawOptions } from './service-D9WzMLH1.js';
4
4
  import 'yjs';
5
5
  import 'agentic-identity';
6
6
  import 'zod';
@@ -83,6 +83,9 @@ declare class LiveService implements BoardAccess {
83
83
  id: string;
84
84
  title: string;
85
85
  }[]>;
86
+ delete(id: string): Promise<{
87
+ deleted: string;
88
+ }>;
86
89
  read(id: string): Promise<BoardData & {
87
90
  id: string;
88
91
  }>;
package/dist/live.js CHANGED
@@ -145,6 +145,13 @@ var LiveService = class {
145
145
  if (!Array.isArray(boards) || !boards.every((b) => typeof b?.id === "string" && typeof b?.title === "string")) throw new CanvasError("INVALID", "the server sent a board list this client does not know");
146
146
  return boards;
147
147
  }
148
+ async delete(id) {
149
+ const response = await call(this.options.server, this.options.token, `/boards/${encodeURIComponent(id)}`, { method: "DELETE" });
150
+ if (!response.ok) throw await refusal(response, "the server refused the request");
151
+ const { deleted } = await json(response);
152
+ if (deleted !== id) throw new CanvasError("INVALID", "the server sent a result this client does not know");
153
+ return { deleted };
154
+ }
148
155
  async read(id) {
149
156
  return this.use(id, async (live) => ({ id, ...readBoard(live.board) }));
150
157
  }
@@ -68,6 +68,10 @@ declare class CanvasService {
68
68
  id: string;
69
69
  title: string;
70
70
  }[]>;
71
+ /** Delete a board and everything on it. Throws NOT_FOUND unless the board exists. The Worker erases the live room too. */
72
+ delete(id: string): Promise<{
73
+ deleted: string;
74
+ }>;
71
75
  /** Throws NOT_FOUND unless the board exists. A live room opens only for a board that exists. */
72
76
  assertExists(id: string): Promise<void>;
73
77
  read(id: string): Promise<BoardData & {
@@ -123,6 +127,6 @@ declare class CanvasService {
123
127
  private load;
124
128
  }
125
129
  /** What the CLI and the MCP server need. `CanvasService` runs it on a store, and `LiveService` runs it in a live room. */
126
- type BoardAccess = Pick<CanvasService, "create" | "list" | "read" | "update" | "add" | "connect" | "move" | "remove" | "flow" | "exportSvg">;
130
+ type BoardAccess = Pick<CanvasService, "create" | "list" | "read" | "delete" | "update" | "add" | "connect" | "move" | "remove" | "flow" | "exportSvg">;
127
131
 
128
132
  export { type BoardAccess as B, CanvasService as C, type DrawOptions as D, type FlowEdge as F, MAX_FLOW_EDGES as M, type Placement as P, type FlowGraph as a, type FlowNode as b, MAX_FLOW_NODES as c, drawFlow as d, layoutFlow as l, parseFlow as p };
package/dist/ui.css CHANGED
@@ -15,9 +15,9 @@
15
15
  .shape { fill: var(--ad-color-surface-raised); stroke: var(--ad-color-border-strong); stroke-width: 1.5; }
16
16
  .shape.text { fill: transparent; stroke: none; }
17
17
  .shape.frame, .shape.image { fill: none; stroke-dasharray: 6 4; pointer-events: visiblePainted; }
18
- .shape.sticky { fill: #ffe08a; stroke: #7a5b00; }
18
+ .shape.sticky { fill: var(--ad-color-warning-subtle); stroke: var(--ad-color-warning); }
19
19
  .label { fill: var(--ad-color-text); pointer-events: none; }
20
- .sticky-label { fill: #1b1b1b; }
20
+ .sticky-label { fill: var(--ad-color-warning-text); }
21
21
  .edge { stroke: var(--ad-color-text-muted); stroke-width: 2; fill: none; pointer-events: none; }
22
22
  .hit { stroke: transparent; stroke-width: 14; }
23
23
  .arrowhead { fill: var(--ad-color-text-muted); }
package/dist/worker.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { Blobs } from 'agentic-identity';
1
+ import { Store, Blobs } from 'agentic-identity';
2
2
  import { Realtime } from 'agentic-realtime';
3
3
  import { D1Like, R2Like } from 'agentic-identity/cloudflare';
4
4
  import { Hono } from 'hono';
5
5
  import { P as Principal } from './read-CeWoSAAy.js';
6
- import { C as CanvasService } from './service-Ckz-3qhK.js';
6
+ import { C as CanvasService } from './service-D9WzMLH1.js';
7
7
  import 'yjs';
8
8
  import 'zod';
9
9
 
@@ -11,10 +11,16 @@ interface WorkerEnv {
11
11
  DB: D1Like;
12
12
  IMAGES: R2Like;
13
13
  /**
14
+ * Dev fallback. Set it to "1" to let `CANVAS_API_TOKENS` sign requests in. Without it, `CANVAS_API_TOKENS` is not read,
15
+ * and a request that sets it gets 503, so a deploy never trusts a static list by accident. Never set it in production.
16
+ */
17
+ CANVAS_UNSAFE_STATIC_TOKENS?: string;
18
+ /**
19
+ * Dev fallback, read only when `CANVAS_UNSAFE_STATIC_TOKENS` is "1". It is tried after the identity credentials.
14
20
  * JSON: `{ "<token>": { "actor": "ada", "kind": "human", "role": "viewer" } }`. A token has 32 characters or more.
15
21
  * A grant needs an actor id and a kind. A token with a missing or bad part gets 401.
16
22
  * The role is "editor" (the default) or "viewer". A viewer reads and cannot write, over HTTP or in a live room.
17
- * Without this setting, every request gets 401. Every token reaches every board.
23
+ * Every token reaches every board, and a token cannot be revoked without a new deploy.
18
24
  */
19
25
  CANVAS_API_TOKENS?: string;
20
26
  /**
@@ -42,8 +48,13 @@ interface AppOptions {
42
48
  }
43
49
  /** A browser cannot set a header on a WebSocket. It offers the sub-protocols `agentic-canvas` and `agentic-canvas.bearer.<token in base64url>`. */
44
50
  declare const WS_PROTOCOL = "agentic-canvas";
45
- /** The principal and role for a bearer token, or null. The kind is never defaulted: a grant without a valid kind gets null. */
46
- declare function authenticate(request: Request, env: WorkerEnv): Promise<{
51
+ /**
52
+ * The principal and role for a request, or null. A credential issued by agentic-identity (a bearer key or an access token in the
53
+ * store) signs in. Its scopes set the role: `canvas:write` is an editor, `canvas:read` a viewer, and none is denied.
54
+ * The static list signs in only with `CANVAS_UNSAFE_STATIC_TOKENS=1`. The kind is never defaulted: a principal without a valid kind gets null.
55
+ * Without `store` only the static list is read.
56
+ */
57
+ declare function authenticate(request: Request, env: WorkerEnv, store?: Store): Promise<{
47
58
  actor: Principal;
48
59
  role: Role;
49
60
  } | null>;
package/dist/worker.js CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  createApp,
5
5
  sniffImage,
6
6
  worker_default
7
- } from "./chunk-PH67TSYW.js";
8
- import "./chunk-66FFQMHI.js";
7
+ } from "./chunk-EH7WCMK6.js";
8
+ import "./chunk-GPSOLW2Y.js";
9
9
  import "./chunk-NWQED42L.js";
10
10
  import "./chunk-ZSPZMGHG.js";
11
11
  import "./chunk-JOBTIAFJ.js";
package/llms.txt CHANGED
@@ -6,11 +6,11 @@ Keywords: whiteboard,canvas,diagrams,drawing,collaboration,agents,mcp,cli.
6
6
 
7
7
  - SDK: import { Board, drawFlow, readBoard, toSvg } from "agentic-canvas".
8
8
  - Exports: the README "Exports" section lists every name of each entry point: agentic-canvas, /client, /file-store, /live, /worker and /live-worker.
9
- - CLI: agentic-canvas new|list|read|add|connect|update|move|rm|flow|export.
10
- - UI: the Worker serves a web UI at its root (run `npm run build`, then `wrangler dev`). A person signs in with a bearer token.
9
+ - CLI: agentic-canvas new|list|read|add|connect|update|move|rm|delete|flow|export.
10
+ - UI: the Worker serves a web UI at its root (run `npm run build`, then `wrangler dev`). A person signs in with a bearer token: an agentic-identity credential (scope canvas:write or canvas:read). A static CANVAS_API_TOKENS list works only with CANVAS_UNSAFE_STATIC_TOKENS=1 (dev).
11
11
  - Live: set CANVAS_SERVER and CANVAS_TOKEN, and the CLI and `agentic-canvas mcp` edit the board in its live room.
12
12
  - SDK: import { LiveBoard, LiveService } from "agentic-canvas/live".
13
- - SDK: import { createClient, CanvasApiError } from "agentic-canvas/client". It calls the Worker API with fetch. Methods: me, listBoards, createBoard, readBoard, exportSvg, addShape, addConnector, drawFlow, update, move, remove, uploadImage, getImage, state, checkpoint. A failure throws CanvasApiError with `code` and `status`. No retries.
13
+ - SDK: import { createClient, CanvasApiError } from "agentic-canvas/client". It calls the Worker API with fetch. Methods: me, listBoards, createBoard, readBoard, deleteBoard, exportSvg, addShape, addConnector, drawFlow, update, move, remove, uploadImage, getImage, state, checkpoint. A failure throws CanvasApiError with `code` and `status`. No retries.
14
14
  - UI: import { mount } from "agentic-canvas/ui"; import "agentic-canvas/ui.css" (with agentic-design/styles.css). mount(element, { boardId, client, principal: { kind, name, role }, readOnly?, room?: { baseUrl, token } }) renders the board and returns { update(options), unmount() }. BoardEditor is the same as a React component. React 19 is a peer dependency.
15
15
 
16
16
  ## Docs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-canvas",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "A shared whiteboard for diagrams, drawn by people and agents.",
5
5
  "keywords": [
6
6
  "whiteboard",
@@ -82,7 +82,7 @@
82
82
  "@modelcontextprotocol/sdk": "^1.12.0",
83
83
  "agentic-design": "^0.2.0",
84
84
  "agentic-identity": "^0.6.0",
85
- "agentic-realtime": "^0.3.0",
85
+ "agentic-realtime": "^0.4.2",
86
86
  "yjs": "^13.6.0",
87
87
  "zod": "^3.25.0"
88
88
  },