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.
@@ -34,6 +34,22 @@ function boardKey(id) {
34
34
  }
35
35
  return id;
36
36
  }
37
+ function isStoreId(id) {
38
+ try {
39
+ assertId(id);
40
+ return /^[A-Za-z0-9_-]{1,128}$/.test(id);
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ function isBoardId(id) {
46
+ try {
47
+ boardKey(id);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
37
53
  function storeFailure(error) {
38
54
  if (error instanceof StoreConflictError) throw new CanvasError("UNAVAILABLE", "the board changed at the same time. Try again");
39
55
  if (error instanceof FileStoreError) throw new CanvasError(error.code === "LOCKED" ? "UNAVAILABLE" : "INVALID", error.message);
@@ -57,12 +73,20 @@ var CanvasService = class {
57
73
  /** The first 100 boards (the store sorts by id), with titles. A board whose stored bytes are damaged shows an empty title: it stays in the list so a person can find and repair it. */
58
74
  async list() {
59
75
  const page = await this.store.query(COLLECTION, { limit: 100 }).catch(storeFailure);
60
- return page.items.map(({ id, data }) => {
61
- try {
62
- return { id, title: decodeStored(data, id, Board.decode).title };
63
- } catch {
64
- return { id, title: "" };
65
- }
76
+ return page.items.map(({ id, data }) => summary(id, data));
77
+ }
78
+ /** The listed boards that exist, with titles, in the order given. */
79
+ async summaries(ids) {
80
+ const found = await Promise.all(ids.map(async (id) => ({ id, data: await this.store.get(COLLECTION, id).catch(storeFailure) })));
81
+ return found.flatMap(({ id, data }) => data ? [summary(id, data)] : []);
82
+ }
83
+ /** Delete a board and everything on it. Throws NOT_FOUND unless the board exists. The Worker erases the live room too. */
84
+ async delete(id) {
85
+ boardKey(id);
86
+ return this.store.transaction(async (tx) => {
87
+ if (!await tx.get(COLLECTION, id)) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
88
+ await tx.delete(COLLECTION, id);
89
+ return { deleted: id };
66
90
  });
67
91
  }
68
92
  /** Throws NOT_FOUND unless the board exists. A live room opens only for a board that exists. */
@@ -182,6 +206,13 @@ var CanvasService = class {
182
206
  return decodeStored(doc, id, Board.decode);
183
207
  }
184
208
  };
209
+ function summary(id, data) {
210
+ try {
211
+ return { id, title: decodeStored(data, id, Board.decode).title };
212
+ } catch {
213
+ return { id, title: "" };
214
+ }
215
+ }
185
216
  function decodeStored(doc, id, decode) {
186
217
  try {
187
218
  return decode(fromBase64(String(doc.update)));
@@ -193,5 +224,7 @@ function decodeStored(doc, id, decode) {
193
224
 
194
225
  export {
195
226
  MAX_LIVE_BYTES,
227
+ isStoreId,
228
+ isBoardId,
196
229
  CanvasService
197
230
  };
@@ -0,0 +1,130 @@
1
+ import {
2
+ FileStoreError
3
+ } from "./chunk-NWQED42L.js";
4
+
5
+ // src/store-file.ts
6
+ import { randomUUID } from "crypto";
7
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "fs";
8
+ import { join } from "path";
9
+ import { createFileStore as createIdentityFileStore } from "agentic-identity/file";
10
+ import { StoreConflictError } from "agentic-identity";
11
+ var RETRY_FOR_MS = 1e4;
12
+ var LOCK_STALE_MS = 3e3;
13
+ var LOCK_WAIT_MS = 5e3;
14
+ var HELD = /locked by another process/;
15
+ var NOT_A_STORE_FILE = /is not an agentic-identity store file/;
16
+ async function retried(run) {
17
+ const deadline = Date.now() + RETRY_FOR_MS;
18
+ for (; ; ) {
19
+ try {
20
+ return await run();
21
+ } catch (error) {
22
+ const clash = error instanceof StoreConflictError && !HELD.test(error.message);
23
+ if (!clash || Date.now() > deadline) throw error;
24
+ await new Promise((resolve) => setTimeout(resolve, 5 + Math.random() * 20));
25
+ }
26
+ }
27
+ }
28
+ var isDoc = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29
+ var sleep = (ms) => void Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
30
+ function legacyCollections(path) {
31
+ try {
32
+ const data = JSON.parse(readFileSync(path, "utf8"));
33
+ const current = isDoc(data) && data.format === 1 && isDoc(data.collections);
34
+ const old = isDoc(data) && !current && Object.values(data).every((docs) => isDoc(docs) && Object.values(docs).every(isDoc));
35
+ return old ? data : void 0;
36
+ } catch {
37
+ return void 0;
38
+ }
39
+ }
40
+ function withLock(path, fn) {
41
+ const lock = `${path}.lock`;
42
+ const mine = `${process.pid}:${randomUUID()}`;
43
+ for (const start = Date.now(); ; ) {
44
+ try {
45
+ writeFileSync(lock, mine, { flag: "wx", mode: 384 });
46
+ break;
47
+ } catch (error) {
48
+ if (error.code !== "EEXIST") return false;
49
+ try {
50
+ if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) unlinkSync(lock);
51
+ } catch {
52
+ }
53
+ if (Date.now() - start > LOCK_WAIT_MS) return false;
54
+ sleep(10);
55
+ }
56
+ }
57
+ const owned = () => {
58
+ try {
59
+ return readFileSync(lock, "utf8") === mine;
60
+ } catch {
61
+ return false;
62
+ }
63
+ };
64
+ try {
65
+ fn(owned);
66
+ } finally {
67
+ if (owned()) rmSync(lock, { force: true });
68
+ }
69
+ return true;
70
+ }
71
+ function upgradeLegacyFile(path) {
72
+ if (legacyCollections(path) === void 0) return true;
73
+ try {
74
+ withLock(path, (owned) => {
75
+ const data = legacyCollections(path);
76
+ if (data === void 0) return;
77
+ const temp = `${path}.${process.pid}.upgrade`;
78
+ try {
79
+ const fd = openSync(temp, "w", 384);
80
+ try {
81
+ writeSync(fd, JSON.stringify({ format: 1, collections: data }));
82
+ fsyncSync(fd);
83
+ } finally {
84
+ closeSync(fd);
85
+ }
86
+ if (owned()) renameSync(temp, path);
87
+ } finally {
88
+ rmSync(temp, { force: true });
89
+ }
90
+ });
91
+ } catch {
92
+ }
93
+ return legacyCollections(path) === void 0;
94
+ }
95
+ function createFileStore(path, options = {}) {
96
+ upgradeLegacyFile(path);
97
+ const inner = createIdentityFileStore({ ...options, path });
98
+ const guard = async (run) => {
99
+ for (let upgraded = false; ; upgraded = true) {
100
+ try {
101
+ return await retried(run);
102
+ } catch (error) {
103
+ if (!(error instanceof Error)) throw error;
104
+ if (HELD.test(error.message)) throw new FileStoreError("LOCKED", "the database file is locked. If no agentic-canvas process runs, remove the .lock file next to it");
105
+ if (!NOT_A_STORE_FILE.test(error.message)) throw error;
106
+ if (legacyCollections(path) === void 0) throw new FileStoreError("DAMAGED", "the database file is not valid JSON. Repair or remove it");
107
+ if (upgraded || !upgradeLegacyFile(path)) throw new FileStoreError("DAMAGED", "the database file is in the older layout and could not be upgraded. Try again, and check that its folder is writable");
108
+ }
109
+ }
110
+ };
111
+ return {
112
+ region: inner.region,
113
+ get: (collection, id) => guard(() => inner.get(collection, id)),
114
+ query: (collection, query) => guard(() => inner.query(collection, query)),
115
+ put: (collection, id, doc) => guard(() => inner.put(collection, id, doc)),
116
+ delete: (collection, id) => guard(() => inner.delete(collection, id)),
117
+ transaction: (fn) => guard(() => inner.transaction(fn))
118
+ };
119
+ }
120
+ function openLocalStore(dir) {
121
+ mkdirSync(dir, { recursive: true });
122
+ return createFileStore(join(dir, STORE_FILE));
123
+ }
124
+ var STORE_FILE = "canvas.json";
125
+
126
+ export {
127
+ createFileStore,
128
+ openLocalStore,
129
+ STORE_FILE
130
+ };
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-7IOW6LML.js";
9
9
  import {
10
10
  openLocalStore
11
- } from "./chunk-UKST5A57.js";
11
+ } from "./chunk-NRLF6MGR.js";
12
12
  import {
13
13
  CanvasService
14
- } from "./chunk-66FFQMHI.js";
14
+ } from "./chunk-M74ENUWR.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
@@ -1,4 +1,4 @@
1
- import { E as ErrorCode, a as BoardData, c as ShapeType, S as Shape, d as Endpoint, C as Connector } from './read-CeWoSAAy.js';
1
+ import { E as ErrorCode, a as BoardData, c as ShapeType, S as Shape, d as Endpoint, C as Connector } from './read--Jd3kE13.js';
2
2
  import { PrincipalKind } from 'agentic-identity';
3
3
  import 'yjs';
4
4
  import 'zod';
@@ -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/embed.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { CanvasClient } from './client.js';
3
3
  import { PrincipalKind } from 'agentic-identity';
4
- import './read-CeWoSAAy.js';
4
+ import './read--Jd3kE13.js';
5
5
  import 'yjs';
6
6
  import 'zod';
7
7
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { B as Board, P as Principal } from './read-CeWoSAAy.js';
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';
1
+ import { B as Board, P as Principal } from './read--Jd3kE13.js';
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--Jd3kE13.js';
3
+ import { B as BoardAccess } from './service-OiTtNadu.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-OiTtNadu.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-7IOW6LML.js";
10
10
  import {
11
11
  createFileStore,
12
12
  openLocalStore
13
- } from "./chunk-UKST5A57.js";
13
+ } from "./chunk-NRLF6MGR.js";
14
14
  import {
15
15
  CanvasService
16
- } from "./chunk-66FFQMHI.js";
16
+ } from "./chunk-M74ENUWR.js";
17
17
  import "./chunk-NWQED42L.js";
18
18
  import {
19
19
  Board,
@@ -5,10 +5,10 @@ import 'agentic-identity';
5
5
  import 'agentic-realtime';
6
6
  import 'agentic-identity/cloudflare';
7
7
  import 'hono';
8
- import './read-CeWoSAAy.js';
8
+ import './read--Jd3kE13.js';
9
9
  import 'yjs';
10
10
  import 'zod';
11
- import './service-Ckz-3qhK.js';
11
+ import './service-OiTtNadu.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-JQCUVXXD.js";
4
+ import "./chunk-M74ENUWR.js";
5
5
  import "./chunk-NWQED42L.js";
6
6
  import {
7
7
  CanvasError
package/dist/live.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { B as Board, P as Principal, a as BoardData, S as Shape, C as Connector, b as CanvasElement } from './read-CeWoSAAy.js';
1
+ import { B as Board, P as Principal, a as BoardData, S as Shape, C as Connector, b as CanvasElement } from './read--Jd3kE13.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-OiTtNadu.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
  }
@@ -6,22 +6,16 @@ import { z } from 'zod';
6
6
  * The actor of a new write. The rule is the identity kernel's `parsePrincipal`: a known kind, and an id of 1 to 128
7
7
  * characters from A-Z, a-z, 0-9, ".", "_" and "-". The Worker, the CLI and the SDK all use it.
8
8
  */
9
- declare const principalSchema: z.ZodEffects<z.ZodObject<{
9
+ declare const principalSchema: z.ZodPipe<z.ZodObject<{
10
10
  kind: z.ZodString;
11
11
  id: z.ZodString;
12
- }, "strict", z.ZodTypeAny, {
13
- kind: string;
14
- id: string;
15
- }, {
16
- kind: string;
17
- id: string;
18
- }>, {
12
+ }, z.core.$strict>, z.ZodTransform<{
19
13
  kind: PrincipalKind;
20
14
  id: string;
21
15
  }, {
22
16
  kind: string;
23
17
  id: string;
24
- }>;
18
+ }>>;
25
19
  type Principal = {
26
20
  kind: PrincipalKind;
27
21
  id: string;
@@ -1,4 +1,4 @@
1
- import { c as ShapeType, B as Board, P as Principal, a as BoardData, S as Shape, C as Connector, b as CanvasElement } from './read-CeWoSAAy.js';
1
+ import { c as ShapeType, B as Board, P as Principal, a as BoardData, S as Shape, C as Connector, b as CanvasElement } from './read--Jd3kE13.js';
2
2
  import { Store } from 'agentic-identity';
3
3
 
4
4
  declare const MAX_FLOW_NODES = 500;
@@ -68,6 +68,15 @@ declare class CanvasService {
68
68
  id: string;
69
69
  title: string;
70
70
  }[]>;
71
+ /** The listed boards that exist, with titles, in the order given. */
72
+ summaries(ids: string[]): Promise<{
73
+ id: string;
74
+ title: string;
75
+ }[]>;
76
+ /** Delete a board and everything on it. Throws NOT_FOUND unless the board exists. The Worker erases the live room too. */
77
+ delete(id: string): Promise<{
78
+ deleted: string;
79
+ }>;
71
80
  /** Throws NOT_FOUND unless the board exists. A live room opens only for a board that exists. */
72
81
  assertExists(id: string): Promise<void>;
73
82
  read(id: string): Promise<BoardData & {
@@ -123,6 +132,6 @@ declare class CanvasService {
123
132
  private load;
124
133
  }
125
134
  /** 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">;
135
+ type BoardAccess = Pick<CanvasService, "create" | "list" | "read" | "delete" | "update" | "add" | "connect" | "move" | "remove" | "flow" | "exportSvg">;
127
136
 
128
137
  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 };
@@ -7,19 +7,8 @@ declare class FileStoreError extends Error {
7
7
  }
8
8
 
9
9
  interface FileStoreOptions extends AdapterOptions {
10
- /** How long a write waits for the lock before it fails. Defaults to 10000. */
11
- lockTimeoutMs?: number;
12
- /** A lock older than this is stale, if its owner is alive. Defaults to 30000. */
13
- staleAfterMs?: number;
14
10
  }
15
- /**
16
- * A Store kept in one JSON file. Node only. It is an adapter of the shared Store port and passes `runStoreContract`. Many processes may share the file, for example the CLI
17
- * and the MCP server. Each write takes a lock file, reads the file again, applies the change, and
18
- * replaces the file by an atomic rename. A read sees the last renamed file, so it takes no lock.
19
- * Each call reads the whole file and loads the docs it needs into the shared memory adapter, which runs the
20
- * queries and the transaction rules. Cost grows with the file. Use it for local work, not for scale.
21
- * The lock needs one machine: the process check and the clock do not work across hosts.
22
- */
11
+ /** A Store kept in one JSON file. Node only. It is the file adapter of agentic-identity, with a retry on a write clash. */
23
12
  declare function createFileStore(path: string, options?: FileStoreOptions): Store;
24
13
  /** The store the CLI and the MCP server share: one file in `dir`, which is made if it is missing. */
25
14
  declare function openLocalStore(dir: string): Store;
@@ -2,7 +2,7 @@ import {
2
2
  STORE_FILE,
3
3
  createFileStore,
4
4
  openLocalStore
5
- } from "./chunk-UKST5A57.js";
5
+ } from "./chunk-NRLF6MGR.js";
6
6
  import {
7
7
  FileStoreError
8
8
  } from "./chunk-NWQED42L.js";
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,20 +1,101 @@
1
- import { Blobs } from 'agentic-identity';
1
+ import { Store, PrincipalContext, Action, Grant, InviteSummary, 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
- import { P as Principal } from './read-CeWoSAAy.js';
6
- import { C as CanvasService } from './service-Ckz-3qhK.js';
5
+ import { P as Principal } from './read--Jd3kE13.js';
6
+ import { C as CanvasService } from './service-OiTtNadu.js';
7
7
  import 'yjs';
8
8
  import 'zod';
9
9
 
10
+ /**
11
+ * Who may do what on a board. A board is an identity resource of type `canvas`. The roles, the grants, the invites,
12
+ * and the links are those of agentic-identity: this class checks input and maps its failures to `CanvasError`.
13
+ * A caller who cannot read a board gets NOT_FOUND, so a board id tells a stranger nothing. A caller who can read
14
+ * it and lacks the role for the action gets FORBIDDEN.
15
+ */
16
+ declare class CanvasAccess {
17
+ private readonly store;
18
+ private readonly sharing;
19
+ private readonly now;
20
+ constructor(store: Store, options?: {
21
+ now?: () => number;
22
+ });
23
+ /** 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. */
24
+ private known;
25
+ /** Make the actor the owner of a new board. */
26
+ register(ctx: PrincipalContext, id: string): Promise<void>;
27
+ /** Remove the sharing records of a board. Needs an owner. */
28
+ unregister(ctx: PrincipalContext, id: string): Promise<void>;
29
+ /** True if the board has sharing records: it was made by an identity credential. */
30
+ registered(id: string): Promise<boolean>;
31
+ /**
32
+ * True if the caller may do `action` on the board. A link token adds the role of its link. It writes an audit entry.
33
+ * An id that belongs to a resource of another type, in a Store shared with another app, is no board: it is denied.
34
+ */
35
+ allowed(ctx: PrincipalContext | null, action: Action, id: string, linkToken?: string): Promise<boolean>;
36
+ /** Throw NOT_FOUND for a caller who cannot read the board, and FORBIDDEN for one who can read it but not do `action`. */
37
+ require(ctx: PrincipalContext | null, action: Action, id: string, linkToken?: string): Promise<void>;
38
+ /** The ids of the boards a person or agent holds a role on, the first 100. A credential without `canvas:read` gets none. */
39
+ readable(ctx: PrincipalContext): Promise<string[]>;
40
+ grant(ctx: PrincipalContext, id: string, input: {
41
+ subjectKind?: unknown;
42
+ subjectId?: unknown;
43
+ role?: unknown;
44
+ }): Promise<{
45
+ ok: true;
46
+ }>;
47
+ revoke(ctx: PrincipalContext, id: string, input: {
48
+ subjectKind?: unknown;
49
+ subjectId?: unknown;
50
+ }): Promise<{
51
+ ok: true;
52
+ }>;
53
+ /** The grants and the invites on a board. An invite never shows its email. Links are not listed: a token shows once. */
54
+ list(ctx: PrincipalContext, id: string): Promise<{
55
+ grants: Grant[];
56
+ invites: InviteSummary[];
57
+ }>;
58
+ invite(ctx: PrincipalContext, id: string, input: {
59
+ role?: unknown;
60
+ email?: unknown;
61
+ handle?: unknown;
62
+ expiresInMs?: unknown;
63
+ }): Promise<{
64
+ id: string;
65
+ }>;
66
+ /** The email comes from a verified sign-in and the handle from the agent's own record, never from the request. */
67
+ accept(ctx: PrincipalContext, inviteId: string): Promise<{
68
+ ok: true;
69
+ }>;
70
+ /** The token shows once. Only its hash is stored. */
71
+ createLink(ctx: PrincipalContext, id: string, input: {
72
+ role?: unknown;
73
+ signedInOnly?: unknown;
74
+ expiresInMs?: unknown;
75
+ }): Promise<{
76
+ id: string;
77
+ token: string;
78
+ }>;
79
+ /** Revoke a link of this board. A link of another board is NOT_FOUND here, even for an owner of both. */
80
+ revokeLink(ctx: PrincipalContext, id: string, linkId: string): Promise<{
81
+ ok: true;
82
+ }>;
83
+ }
84
+
10
85
  interface WorkerEnv {
11
86
  DB: D1Like;
12
87
  IMAGES: R2Like;
13
88
  /**
89
+ * Dev fallback. Set it to "1" to let `CANVAS_API_TOKENS` sign requests in. Without it, `CANVAS_API_TOKENS` is not read,
90
+ * and a request that sets it gets 503, so a deploy never trusts a static list by accident. Never set it in production.
91
+ */
92
+ CANVAS_UNSAFE_STATIC_TOKENS?: string;
93
+ /**
94
+ * Dev fallback, read only when `CANVAS_UNSAFE_STATIC_TOKENS` is "1". It is tried after the identity credentials.
14
95
  * JSON: `{ "<token>": { "actor": "ada", "kind": "human", "role": "viewer" } }`. A token has 32 characters or more.
15
96
  * A grant needs an actor id and a kind. A token with a missing or bad part gets 401.
16
97
  * 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.
98
+ * Every token reaches every board, and a token cannot be revoked without a new deploy.
18
99
  */
19
100
  CANVAS_API_TOKENS?: string;
20
101
  /**
@@ -27,13 +108,22 @@ interface WorkerEnv {
27
108
  CANVAS_MAX_IMAGE_BYTES?: string;
28
109
  }
29
110
  type Role = "editor" | "viewer";
111
+ /** The caller. `dev` is true for a token of the static list: it holds no board roles and reaches every board. */
112
+ interface Caller {
113
+ actor: Principal;
114
+ role: Role;
115
+ principal: PrincipalContext;
116
+ dev: boolean;
117
+ }
30
118
  type App = {
31
119
  Bindings: WorkerEnv;
32
120
  Variables: {
33
121
  service: CanvasService;
122
+ access: CanvasAccess;
34
123
  blobs: Blobs;
35
124
  actor: Principal;
36
125
  role: Role;
126
+ caller: Caller;
37
127
  };
38
128
  };
39
129
  interface AppOptions {
@@ -42,11 +132,18 @@ interface AppOptions {
42
132
  }
43
133
  /** A browser cannot set a header on a WebSocket. It offers the sub-protocols `agentic-canvas` and `agentic-canvas.bearer.<token in base64url>`. */
44
134
  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<{
135
+ /**
136
+ * The principal and role for a request, or null. A credential issued by agentic-identity (a bearer key or an access token in the
137
+ * store) signs in. Its scopes set the role: `canvas:write` is an editor, `canvas:read` a viewer, and none is denied.
138
+ * 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.
139
+ * Without `store` only the static list is read.
140
+ */
141
+ declare function authenticate(request: Request, env: WorkerEnv, store?: Store): Promise<{
47
142
  actor: Principal;
48
143
  role: Role;
49
144
  } | null>;
145
+ /** Like `authenticate`, with the whole principal context that board roles are checked against. */
146
+ declare function identify(request: Request, env: WorkerEnv, store?: Store): Promise<Caller | null>;
50
147
  /** The image types we accept, found by their first bytes. The client's claim of a type is not trusted. */
51
148
  declare function sniffImage(bytes: Uint8Array): string | null;
52
149
  declare function createApp(options?: AppOptions): Hono<App>;
@@ -54,4 +151,4 @@ declare const _default: {
54
151
  fetch: (request: Request, env: WorkerEnv, ctx: unknown) => Response | Promise<Response>;
55
152
  };
56
153
 
57
- export { type AppOptions, type Role, WS_PROTOCOL, type WorkerEnv, authenticate, createApp, _default as default, sniffImage };
154
+ export { type AppOptions, type Caller, type Role, WS_PROTOCOL, type WorkerEnv, authenticate, createApp, _default as default, identify, sniffImage };
package/dist/worker.js CHANGED
@@ -2,10 +2,11 @@ import {
2
2
  WS_PROTOCOL,
3
3
  authenticate,
4
4
  createApp,
5
+ identify,
5
6
  sniffImage,
6
7
  worker_default
7
- } from "./chunk-PH67TSYW.js";
8
- import "./chunk-66FFQMHI.js";
8
+ } from "./chunk-JQCUVXXD.js";
9
+ import "./chunk-M74ENUWR.js";
9
10
  import "./chunk-NWQED42L.js";
10
11
  import "./chunk-ZSPZMGHG.js";
11
12
  import "./chunk-JOBTIAFJ.js";
@@ -14,5 +15,6 @@ export {
14
15
  authenticate,
15
16
  createApp,
16
17
  worker_default as default,
18
+ identify,
17
19
  sniffImage
18
20
  };