agentic-canvas 0.5.0 → 0.6.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.5.0. The board model, the agent operations, the exports, the CLI, the
10
+ > Status: 0.6.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.
@@ -327,9 +327,9 @@ The stylesheet is a separate export, `agentic-canvas/ui.css`. Load `agentic-desi
327
327
  |---|---|
328
328
  | `createApp(options?)` | Make the Hono app for the Worker. Pass `live` to turn on the live-room socket. |
329
329
  | `default` | The Worker, without live rooms. |
330
- | `authenticate(request, env)` | Read a bearer token from the `Authorization` header, or from a WebSocket sub-protocol. Return the actor and the role, or `null`. |
330
+ | `authenticate(request, env)` | Read a bearer token from the `Authorization` header, or from a WebSocket sub-protocol. Return a promise of the actor and the role, or `null`. |
331
331
  | `sniffImage(bytes)` | Find the image type from the first bytes, or return `null`. The client's claim of a type is not trusted. |
332
- | `WS_PROTOCOL` | The sub-protocol name that carries a token on a WebSocket: `agentic-canvas.bearer`. |
332
+ | `WS_PROTOCOL` | The sub-protocol name for a WebSocket: `agentic-canvas`. The token travels in the entry `agentic-canvas.bearer.<token in base64url>`. |
333
333
  | `WorkerEnv` (type), `AppOptions` (type), `Role` (type) | Types for the Worker bindings, the app options and the role, `editor` or `viewer`. |
334
334
 
335
335
  ### `agentic-canvas/live-worker`
@@ -5,13 +5,11 @@ import {
5
5
  import {
6
6
  CanvasError
7
7
  } from "./chunk-JCYNPYYJ.js";
8
- import {
9
- principalSchema
10
- } from "./chunk-SPP6PINW.js";
11
8
 
12
9
  // src/worker.ts
13
- import { parseRegion } from "agentic-identity";
10
+ import { hasScope, parsePrincipal, parseRegion } from "agentic-identity";
14
11
  import { createD1Store, createR2Blobs, D1_SCHEMA } from "agentic-identity/cloudflare";
12
+ import { acceptWebSocketProtocol, principalFromWebSocketRequest } from "agentic-identity/http";
15
13
 
16
14
  // node_modules/hono/dist/request/constants.js
17
15
  var GET_MATCH_RESULT = /* @__PURE__ */ Symbol();
@@ -1950,36 +1948,41 @@ function sameText(a, b) {
1950
1948
  for (let i = 0; i < Math.max(x.length, y.length); i++) diff |= (x[i] ?? 0) ^ (y[i] ?? 0);
1951
1949
  return diff === 0;
1952
1950
  }
1953
- var WS_PROTOCOL = "agentic-canvas.bearer";
1954
- function socketToken(request) {
1955
- if (request.headers.get("upgrade") !== "websocket") return void 0;
1956
- const [name, encoded, ...more] = (request.headers.get("sec-websocket-protocol") ?? "").split(",").map((p) => p.trim());
1957
- if (name !== WS_PROTOCOL || !encoded || more.length > 0 || !/^[A-Za-z0-9_-]+$/.test(encoded)) return void 0;
1958
- try {
1959
- return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(atob(encoded.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)));
1960
- } catch {
1961
- return void 0;
1962
- }
1951
+ var WS_PROTOCOL = "agentic-canvas";
1952
+ function tokenAuth(env) {
1953
+ return {
1954
+ async authenticate(request) {
1955
+ const token = /^Bearer (.+)$/.exec(request.headers.get("authorization") ?? "")?.[1];
1956
+ if (!token || !env.CANVAS_API_TOKENS) return null;
1957
+ let grants;
1958
+ try {
1959
+ grants = JSON.parse(env.CANVAS_API_TOKENS);
1960
+ } catch {
1961
+ return null;
1962
+ }
1963
+ if (typeof grants !== "object" || grants === null || Array.isArray(grants)) return null;
1964
+ let found;
1965
+ for (const [candidate, grant] of Object.entries(grants)) {
1966
+ if (sameText(candidate, token) && candidate.length >= MIN_TOKEN_LENGTH) found = grant;
1967
+ }
1968
+ if (typeof found !== "object" || found === null) return null;
1969
+ const { actor, kind, role = "editor" } = found;
1970
+ let step;
1971
+ try {
1972
+ step = parsePrincipal({ kind, id: actor });
1973
+ } catch {
1974
+ return null;
1975
+ }
1976
+ if (step.kind === SEEDER.kind && step.id === SEEDER.id) return null;
1977
+ if (role !== "editor" && role !== "viewer") return null;
1978
+ return { principalId: step.id, kind: step.kind, chain: [step], scopes: role === "viewer" ? ["canvas:read"] : ["canvas:read", "canvas:write"] };
1979
+ }
1980
+ };
1963
1981
  }
1964
- function authenticate(request, env) {
1965
- const token = /^Bearer (.+)$/.exec(request.headers.get("authorization") ?? "")?.[1] ?? socketToken(request);
1966
- if (!token || !env.CANVAS_API_TOKENS) return null;
1967
- let grants;
1968
- try {
1969
- grants = JSON.parse(env.CANVAS_API_TOKENS);
1970
- } catch {
1971
- return null;
1972
- }
1973
- if (typeof grants !== "object" || grants === null || Array.isArray(grants)) return null;
1974
- let found;
1975
- for (const [candidate, grant] of Object.entries(grants)) {
1976
- if (sameText(candidate, token) && candidate.length >= MIN_TOKEN_LENGTH) found = grant;
1977
- }
1978
- if (typeof found !== "object" || found === null) return null;
1979
- const { actor, kind, role = "editor" } = found;
1980
- const parsed = principalSchema.safeParse({ kind, id: actor });
1981
- const taken = parsed.success && parsed.data.kind === SEEDER.kind && parsed.data.id === SEEDER.id;
1982
- return parsed.success && !taken && (role === "editor" || role === "viewer") ? { actor: parsed.data, role } : null;
1982
+ async function authenticate(request, env) {
1983
+ const found = await principalFromWebSocketRequest(request, tokenAuth(env), { protocol: WS_PROTOCOL });
1984
+ if (found instanceof Response) return null;
1985
+ return { actor: { kind: found.kind, id: found.principalId }, role: hasScope(found.scopes, "canvas:write") ? "editor" : "viewer" };
1983
1986
  }
1984
1987
  function sniffImage(bytes) {
1985
1988
  const at = (offset, ...sig) => sig.every((b, i) => bytes[offset + i] === b);
@@ -2058,7 +2061,7 @@ function createApp(options = {}) {
2058
2061
  return fail("NOT_CONFIGURED", 'set CANVAS_REGION to "eu", "us", or "unknown"', 503);
2059
2062
  }
2060
2063
  if (!c.env.DB || !c.env.IMAGES) return fail("NOT_CONFIGURED", "bind DB (D1) and IMAGES (R2)", 503);
2061
- const who = authenticate(c.req.raw, c.env);
2064
+ const who = await authenticate(c.req.raw, c.env);
2062
2065
  if (!who) return fail("UNAUTHORIZED", "send a valid bearer token", 401);
2063
2066
  if (who.role === "viewer" && c.req.method !== "GET") return fail("FORBIDDEN", "this token is a viewer; it cannot change a board", 403);
2064
2067
  c.set("actor", who.actor);
@@ -2094,10 +2097,7 @@ function createApp(options = {}) {
2094
2097
  await saveRoom(c, true);
2095
2098
  const { actor, role } = c.var;
2096
2099
  const opened = await options.live(c.env).connect(id, c.req.raw, { principal: { id: actor.id, kind: actor.kind }, role, name: actor.id });
2097
- if (opened.status !== 101 || c.req.header("authorization") || !socketToken(c.req.raw)) return opened;
2098
- const headers = new Headers(opened.headers);
2099
- headers.set("sec-websocket-protocol", WS_PROTOCOL);
2100
- return new Response(null, { status: 101, headers, webSocket: opened.webSocket });
2100
+ return acceptWebSocketProtocol(c.req.raw, opened, WS_PROTOCOL);
2101
2101
  });
2102
2102
  api.get("/boards/:id/state", async (c) => !options.live ? fail("NOT_FOUND", "this server hosts no live rooms", 404) : c.body(new Uint8Array(await c.var.service.state(c.req.param("id"))), 200, { "content-type": "application/octet-stream" }));
2103
2103
  api.post("/boards/:id/checkpoint", async (c) => {
@@ -3,13 +3,12 @@ import {
3
3
  SHAPE_TYPES
4
4
  } from "./chunk-JCYNPYYJ.js";
5
5
  import {
6
- PRINCIPAL_KINDS,
7
- principalSchema
6
+ PRINCIPAL_KINDS
8
7
  } from "./chunk-SPP6PINW.js";
9
8
 
10
9
  // src/version.ts
11
10
  import { createRequire } from "module";
12
- var VERSION = true ? "0.5.0" : readPackageVersion();
11
+ var VERSION = true ? "0.6.0" : readPackageVersion();
13
12
 
14
13
  // src/brand.ts
15
14
  var BRAND = {
@@ -51,17 +50,19 @@ async function svgToPng(svg) {
51
50
  }
52
51
 
53
52
  // src/actor.ts
53
+ import { parsePrincipal } from "agentic-identity";
54
54
  var ActorConfigError = class extends Error {
55
55
  };
56
56
  function operatorActor(env = process.env) {
57
57
  const raw = env.CANVAS_ACTOR;
58
58
  if (!raw) throw new ActorConfigError("the operator must set CANVAS_ACTOR=<kind>:<id>");
59
59
  const at = raw.indexOf(":");
60
- const parsed = principalSchema.safeParse({ kind: raw.slice(0, at), id: raw.slice(at + 1) });
61
- if (at < 0 || !parsed.success) {
60
+ try {
61
+ if (at < 0) throw new Error("no kind");
62
+ return parsePrincipal({ kind: raw.slice(0, at), id: raw.slice(at + 1) });
63
+ } catch {
62
64
  throw new ActorConfigError(`bad CANVAS_ACTOR "${raw}": use <kind>:<id>, kind is one of ${PRINCIPAL_KINDS.join(", ")}`);
63
65
  }
64
- return parsed.data;
65
66
  }
66
67
 
67
68
  // src/mcp.ts
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  operatorActor,
6
6
  runMcp,
7
7
  svgToPng
8
- } from "./chunk-VHHJIBAB.js";
8
+ } from "./chunk-SIDWRC3P.js";
9
9
  import {
10
10
  openLocalStore
11
11
  } from "./chunk-UKST5A57.js";
package/dist/embed.js CHANGED
@@ -228,11 +228,11 @@ function Failure({ message, retry }) {
228
228
 
229
229
  // ui/presence.ts
230
230
  import { SyncClient } from "agentic-realtime/client";
231
- var WS_PROTOCOL = "agentic-canvas.bearer";
231
+ var WS_PROTOCOL = "agentic-canvas";
232
232
  var b64url = (text) => btoa(String.fromCharCode(...new TextEncoder().encode(text))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
233
233
  var bearerSocket = (token) => class extends WebSocket {
234
234
  constructor(url) {
235
- super(url, [WS_PROTOCOL, b64url(token)]);
235
+ super(url, [WS_PROTOCOL, `${WS_PROTOCOL}.bearer.${b64url(token)}`]);
236
236
  }
237
237
  };
238
238
  function roomUrl(baseUrl, board) {
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  operatorActor,
7
7
  runMcp,
8
8
  svgToPng
9
- } from "./chunk-VHHJIBAB.js";
9
+ } from "./chunk-SIDWRC3P.js";
10
10
  import {
11
11
  createFileStore,
12
12
  openLocalStore
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApp
3
- } from "./chunk-V74PWJKY.js";
3
+ } from "./chunk-BIIC2DPZ.js";
4
4
  import "./chunk-G4YTESPS.js";
5
5
  import "./chunk-NWQED42L.js";
6
6
  import {
package/dist/live.js CHANGED
@@ -5,11 +5,10 @@ import {
5
5
  readBoard,
6
6
  toSvg
7
7
  } from "./chunk-JCYNPYYJ.js";
8
- import {
9
- principalSchema
10
- } from "./chunk-SPP6PINW.js";
8
+ import "./chunk-SPP6PINW.js";
11
9
 
12
10
  // src/live.ts
11
+ import { parsePrincipal } from "agentic-identity";
13
12
  import { SyncClient } from "agentic-realtime/client";
14
13
  var CODES = ["INVALID", "DUPLICATE_ID", "NOT_FOUND", "BAD_REFERENCE", "LIMIT", "UNAVAILABLE", "FORBIDDEN"];
15
14
  var base = (server) => server.replace(/\/$/, "") + "/api/v1/canvas";
@@ -33,9 +32,13 @@ async function whoAmI(server, token) {
33
32
  const response = await call(server, token, "/me");
34
33
  if (!response.ok) throw await refusal(response, "the server refused the token");
35
34
  const { actor, kind, role } = await json(response);
36
- const principal = principalSchema.safeParse({ kind, id: actor });
37
- if (!principal.success || role !== "editor" && role !== "viewer") throw new CanvasError("INVALID", "the server sent an identity this client does not know");
38
- return { actor: principal.data, role };
35
+ let principal;
36
+ try {
37
+ principal = parsePrincipal({ kind, id: actor });
38
+ } catch {
39
+ }
40
+ if (!principal || role !== "editor" && role !== "viewer") throw new CanvasError("INVALID", "the server sent an identity this client does not know");
41
+ return { actor: principal, role };
39
42
  }
40
43
  var LiveBoard = class _LiveBoard {
41
44
  constructor(board, me, client, id, options, refusedAtStart) {
package/dist/worker.d.ts CHANGED
@@ -40,13 +40,13 @@ interface AppOptions {
40
40
  /** Makes the live-room server from the Worker bindings. Without it, the ws route answers 404. */
41
41
  live?: (env: WorkerEnv) => Realtime;
42
42
  }
43
- /** A browser cannot set a header on a WebSocket. It offers the token as the sub-protocols `agentic-canvas.bearer` and the token in base64url. */
44
- declare const WS_PROTOCOL = "agentic-canvas.bearer";
43
+ /** A browser cannot set a header on a WebSocket. It offers the sub-protocols `agentic-canvas` and `agentic-canvas.bearer.<token in base64url>`. */
44
+ declare const WS_PROTOCOL = "agentic-canvas";
45
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): {
46
+ declare function authenticate(request: Request, env: WorkerEnv): Promise<{
47
47
  actor: Principal;
48
48
  role: Role;
49
- } | null;
49
+ } | null>;
50
50
  /** The image types we accept, found by their first bytes. The client's claim of a type is not trusted. */
51
51
  declare function sniffImage(bytes: Uint8Array): string | null;
52
52
  declare function createApp(options?: AppOptions): Hono<App>;
package/dist/worker.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  createApp,
5
5
  sniffImage,
6
6
  worker_default
7
- } from "./chunk-V74PWJKY.js";
7
+ } from "./chunk-BIIC2DPZ.js";
8
8
  import "./chunk-G4YTESPS.js";
9
9
  import "./chunk-NWQED42L.js";
10
10
  import "./chunk-JCYNPYYJ.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-canvas",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "A shared whiteboard for diagrams, drawn by people and agents.",
5
5
  "keywords": [
6
6
  "whiteboard",
@@ -81,7 +81,7 @@
81
81
  "@dagrejs/dagre": "^3.1.3",
82
82
  "@modelcontextprotocol/sdk": "^1.12.0",
83
83
  "agentic-design": "^0.2.0",
84
- "agentic-identity": "^0.5.0",
84
+ "agentic-identity": "^0.6.0",
85
85
  "agentic-realtime": "^0.3.0",
86
86
  "yjs": "^13.6.0",
87
87
  "zod": "^3.25.0"