agentic-canvas 0.12.1 → 0.14.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.
@@ -0,0 +1,188 @@
1
+ import {
2
+ isBoardId,
3
+ isStoreId
4
+ } from "./chunk-GLNF43N4.js";
5
+ import {
6
+ CanvasError
7
+ } from "./chunk-ZP6BLCB7.js";
8
+
9
+ // src/access.ts
10
+ import { createSharing, EraseRefusedError, hasScope, ROLES, SUBJECT_KINDS } from "agentic-identity";
11
+ var RESOURCE_TYPE = "canvas";
12
+ var SHARE_ROLES = ROLES.filter((r) => r !== "owner");
13
+ 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 .*)$/;
14
+ function refuse(error) {
15
+ if (error instanceof EraseRefusedError) throw new CanvasError("INVALID", error.message);
16
+ if (error instanceof Error && error.message === "forbidden") throw new CanvasError("FORBIDDEN", "you may not do this on this board");
17
+ if (error instanceof Error && BAD_INPUT.test(error.message)) throw new CanvasError("INVALID", error.message);
18
+ throw error;
19
+ }
20
+ var oneOf = (value, list, name) => {
21
+ if (typeof value !== "string" || !list.includes(value)) throw new CanvasError("INVALID", `${name} must be one of: ${list.join(", ")}`);
22
+ return value;
23
+ };
24
+ var storeId = (value, name) => {
25
+ 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__`);
26
+ return value;
27
+ };
28
+ var word = (value, name) => {
29
+ if (typeof value !== "string" || value === "") throw new CanvasError("INVALID", `${name} must be a non-empty string`);
30
+ return value;
31
+ };
32
+ var lifetime = (value, name) => {
33
+ if (value === void 0) return void 0;
34
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new CanvasError("INVALID", `${name} must be a whole number of milliseconds above 0`);
35
+ return value;
36
+ };
37
+ var CanvasAccess = class {
38
+ constructor(store, options = {}) {
39
+ this.store = store;
40
+ this.sharing = createSharing(store, options);
41
+ this.now = options.now ?? Date.now;
42
+ }
43
+ store;
44
+ sharing;
45
+ now;
46
+ /** 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. */
47
+ known(id) {
48
+ if (!isBoardId(id)) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
49
+ }
50
+ /** Make the actor the owner of a new board. */
51
+ async register(ctx, id) {
52
+ await this.sharing.createResource(ctx, { id, type: RESOURCE_TYPE }).catch((error) => {
53
+ if (error instanceof Error && error.message === "resource exists") throw new CanvasError("DUPLICATE_ID", `board "${id}" already exists`);
54
+ 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");
55
+ return refuse(error);
56
+ });
57
+ }
58
+ /** Remove the sharing records of a board. Needs an owner. */
59
+ async unregister(ctx, id) {
60
+ await this.sharing.deleteResource(ctx, id).catch(refuse);
61
+ }
62
+ /** True if the board has sharing records: it was made by an identity credential. */
63
+ async registered(id) {
64
+ if (!isBoardId(id)) return false;
65
+ return (await this.store.get("resources", id))?.type === RESOURCE_TYPE;
66
+ }
67
+ /**
68
+ * True if the caller may do `action` on the board. A link token adds the role of its link. It writes an audit entry.
69
+ * An id that belongs to a resource of another type, in a Store shared with another app, is no board: it is denied.
70
+ */
71
+ async allowed(ctx, action, id, linkToken) {
72
+ if (!await this.registered(id)) return false;
73
+ return (await this.sharing.can(ctx, action, id, linkToken === void 0 ? {} : { linkToken })).allowed;
74
+ }
75
+ /** Throw NOT_FOUND for a caller who cannot read the board, and FORBIDDEN for one who can read it but not do `action`. */
76
+ async require(ctx, action, id, linkToken) {
77
+ if (await this.allowed(ctx, action, id, linkToken)) return;
78
+ if (action === "read" || !await this.allowed(ctx, "read", id, linkToken)) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
79
+ throw new CanvasError("FORBIDDEN", `this needs the ${action === "admin" ? "owner" : "editor"} role on the board`);
80
+ }
81
+ /** The ids of the boards a person or agent holds a role on, the first 100. A credential without `canvas:read` gets none. */
82
+ async readable(ctx) {
83
+ const root = ctx.chain[0];
84
+ if (!root || !SUBJECT_KINDS.includes(root.kind) || !hasScope(ctx.scopes, `${RESOURCE_TYPE}:read`)) return [];
85
+ const ids = [];
86
+ let cursor;
87
+ for (let pages = 0; pages < 10 && ids.length < 100; pages++) {
88
+ const page = await this.store.query("grants", { where: { subjectKind: root.kind, subjectId: root.id }, limit: 100, ...cursor === void 0 ? {} : { cursor } });
89
+ for (const { data } of page.items) {
90
+ if (ids.length >= 100 || typeof data.resourceId !== "string" || !ROLES.includes(data.role)) continue;
91
+ if (await this.registered(data.resourceId)) ids.push(data.resourceId);
92
+ }
93
+ if ((cursor = page.nextCursor) === void 0) break;
94
+ }
95
+ return ids;
96
+ }
97
+ async grant(ctx, id, input) {
98
+ this.known(id);
99
+ const subject = { kind: oneOf(input.subjectKind, SUBJECT_KINDS, "subjectKind"), id: storeId(input.subjectId, "subjectId") };
100
+ await this.sharing.grant(ctx, id, subject, oneOf(input.role, ROLES, "role")).catch(refuse);
101
+ return { ok: true };
102
+ }
103
+ async revoke(ctx, id, input) {
104
+ this.known(id);
105
+ const subject = { kind: oneOf(input.subjectKind, SUBJECT_KINDS, "subjectKind"), id: storeId(input.subjectId, "subjectId") };
106
+ await this.sharing.revokeGrant(ctx, id, subject).catch(refuse);
107
+ return { ok: true };
108
+ }
109
+ /** The grants and the invites on a board. An invite never shows its email. `listLinks` lists the links, and never a token. */
110
+ async list(ctx, id) {
111
+ this.known(id);
112
+ const [grants, invites] = await Promise.all([this.sharing.listGrants(ctx, id, { limit: 200 }), this.sharing.listInvites(ctx, id, { limit: 200 })]).catch(refuse);
113
+ return { grants: grants.grants, invites: invites.invites };
114
+ }
115
+ async invite(ctx, id, input) {
116
+ this.known(id);
117
+ const role = oneOf(input.role, SHARE_ROLES, "role");
118
+ const expiresInMs = lifetime(input.expiresInMs, "expiresInMs");
119
+ const who = input.email !== void 0 ? { email: word(input.email, "email") } : input.handle !== void 0 ? { handle: word(input.handle, "handle") } : {};
120
+ if (input.email !== void 0 && input.handle !== void 0) throw new CanvasError("INVALID", "give an email or a handle, not both");
121
+ return this.sharing.createInvite(ctx, id, { role, ...who, ...expiresInMs === void 0 ? {} : { expiresInMs } }).catch(refuse);
122
+ }
123
+ /** The email comes from a verified sign-in and the handle from the agent's own record, never from the request. */
124
+ async accept(ctx, inviteId) {
125
+ if (!isStoreId(inviteId)) throw new CanvasError("FORBIDDEN", "you may not do this on this board");
126
+ const user = await this.store.get("users", ctx.principalId);
127
+ const agent = await this.store.get("principals", ctx.principalId);
128
+ const identity = {
129
+ ...user?.emailVerified === true && typeof user.email === "string" ? { email: user.email } : {},
130
+ ...typeof agent?.handle === "string" ? { handle: agent.handle } : {}
131
+ };
132
+ await this.sharing.acceptInvite(ctx, word(inviteId, "id"), identity).catch(refuse);
133
+ return { ok: true };
134
+ }
135
+ /** The token shows once. Only its hash is stored. */
136
+ async createLink(ctx, id, input) {
137
+ this.known(id);
138
+ const role = oneOf(input.role, SHARE_ROLES, "role");
139
+ const expiresInMs = lifetime(input.expiresInMs, "expiresInMs");
140
+ if (input.signedInOnly !== void 0 && typeof input.signedInOnly !== "boolean") throw new CanvasError("INVALID", "signedInOnly must be true or false");
141
+ return this.sharing.createLink(ctx, id, { role, signedInOnly: input.signedInOnly === true, ...expiresInMs === void 0 ? {} : { expiresAt: this.now() + expiresInMs } }).catch(refuse);
142
+ }
143
+ /** The links of a board. A summary never holds the token or its hash. Needs an owner. */
144
+ async listLinks(ctx, id) {
145
+ this.known(id);
146
+ await this.require(ctx, "admin", id);
147
+ const all = [];
148
+ let cursor;
149
+ let skipped = 0;
150
+ for (let pages = 0; pages < 10; pages++) {
151
+ const page = await this.store.query("links", { where: { resourceId: id }, limit: 100, ...cursor === void 0 ? {} : { cursor } });
152
+ for (const { id: key, data } of page.items) {
153
+ const expiresOk = data.expiresAt == null || typeof data.expiresAt === "number" && Number.isFinite(data.expiresAt);
154
+ if (data.id !== key || data.resourceId !== id || typeof data.tokenHash !== "string" || typeof data.id !== "string" || !SHARE_ROLES.includes(data.role) || !expiresOk) {
155
+ skipped++;
156
+ continue;
157
+ }
158
+ all.push({
159
+ id: data.id,
160
+ role: data.role,
161
+ signedInOnly: data.signedInOnly === true,
162
+ ...typeof data.expiresAt === "number" ? { expiresAt: data.expiresAt } : {},
163
+ // Any value but null means revoked, as in agentic-identity. A value that is not a time shows as 0.
164
+ ...data.revokedAt == null ? {} : { revokedAt: typeof data.revokedAt === "number" ? data.revokedAt : 0 }
165
+ });
166
+ }
167
+ if ((cursor = page.nextCursor) === void 0) break;
168
+ }
169
+ const now = this.now();
170
+ const live = (l) => l.revokedAt === void 0 && (l.expiresAt === void 0 || l.expiresAt > now);
171
+ all.sort((x, y) => Number(live(y)) - Number(live(x)) || (y.expiresAt ?? Infinity) - (x.expiresAt ?? Infinity) || x.id.localeCompare(y.id));
172
+ const links = all.slice(0, 200);
173
+ return { links, ...cursor !== void 0 || all.length > 200 ? { truncated: true } : {}, ...skipped > 0 ? { skipped } : {} };
174
+ }
175
+ /** Revoke a link of this board. A link of another board is NOT_FOUND here, even for an owner of both. */
176
+ async revokeLink(ctx, id, linkId) {
177
+ this.known(id);
178
+ const link = isStoreId(linkId) ? await this.store.get("links", linkId) : null;
179
+ if (!link || link.resourceId !== id) throw new CanvasError("NOT_FOUND", "no such link on this board");
180
+ await this.sharing.revokeLink(ctx, linkId).catch(refuse);
181
+ return { ok: true };
182
+ }
183
+ };
184
+
185
+ export {
186
+ RESOURCE_TYPE,
187
+ CanvasAccess
188
+ };
@@ -4,7 +4,7 @@ import {
4
4
  IMAGE_PATTERN,
5
5
  MAX_ELEMENTS,
6
6
  SHAPE_TYPES
7
- } from "./chunk-MVWUIBGV.js";
7
+ } from "./chunk-ZP6BLCB7.js";
8
8
 
9
9
  // src/core/flow.ts
10
10
  import dagre from "@dagrejs/dagre";
@@ -4,13 +4,13 @@ import {
4
4
  import {
5
5
  drawFlow,
6
6
  toSvg
7
- } from "./chunk-EDQNA2SU.js";
7
+ } from "./chunk-BKG3FXX3.js";
8
8
  import {
9
9
  Board,
10
10
  CanvasError,
11
11
  principalSchema,
12
12
  readBoard
13
- } from "./chunk-MVWUIBGV.js";
13
+ } from "./chunk-ZP6BLCB7.js";
14
14
 
15
15
  // src/service.ts
16
16
  import * as Y from "yjs";
@@ -224,6 +224,8 @@ function decodeStored(doc, id, decode) {
224
224
 
225
225
  export {
226
226
  MAX_LIVE_BYTES,
227
+ toBase64,
228
+ fromBase64,
227
229
  isStoreId,
228
230
  isBoardId,
229
231
  CanvasService
@@ -2,11 +2,11 @@ import {
2
2
  CanvasError,
3
3
  PRINCIPAL_KINDS,
4
4
  SHAPE_TYPES
5
- } from "./chunk-MVWUIBGV.js";
5
+ } from "./chunk-ZP6BLCB7.js";
6
6
 
7
7
  // src/version.ts
8
8
  import { createRequire } from "module";
9
- var VERSION = true ? "0.12.1" : readPackageVersion();
9
+ var VERSION = true ? "0.14.0" : readPackageVersion();
10
10
 
11
11
  // src/brand.ts
12
12
  var BRAND = {
@@ -198,6 +198,24 @@ function createMcpServer(service, actor, sharing) {
198
198
  },
199
199
  tool(async ({ board: b, ...input }) => json(await shared().invite(b, input)))
200
200
  );
201
+ server.registerTool(
202
+ "list_links",
203
+ { description: "List the share links of a board, without their tokens. Needs the owner role.", inputSchema: { board }, annotations: read },
204
+ tool(async (a) => json(await shared().links(a.board)))
205
+ );
206
+ server.registerTool(
207
+ "create_link",
208
+ {
209
+ description: "Make a share link. Anyone with the token can read the board, and only read: the role does not widen a link visit. With signedInOnly, only a signed-in visitor may use it. The token shows once: keep it. The role is not owner. Needs the owner role.",
210
+ inputSchema: { board, role: z.enum(["viewer", "commenter", "editor"]), signedInOnly: z.boolean().optional(), expiresInMs: z.number().int().positive().optional() }
211
+ },
212
+ tool(async ({ board: b, ...input }) => json(await shared().createLink(b, input)))
213
+ );
214
+ server.registerTool(
215
+ "revoke_link",
216
+ { description: "Revoke a share link of a board. Needs the owner role.", inputSchema: { board, link: z.string() }, annotations: { destructiveHint: true } },
217
+ tool(async (a) => json(await shared().revokeLink(a.board, a.link)))
218
+ );
201
219
  return server;
202
220
  }
203
221
  async function runMcp(service, actor, sharing) {
@@ -94,6 +94,7 @@ var isBindable = (type) => typeof type === "string" && type !== "connector" && t
94
94
  var isValidZ = (value) => typeof value === "number" && Number.isFinite(value) && Math.abs(value) <= MAX_Z;
95
95
  var clampZ = (value) => Math.min(MAX_Z, Math.max(-MAX_Z, value));
96
96
  var samePrincipal = (a, b) => a.kind === b.kind && a.id === b.id;
97
+ var isStamp = (value, who) => typeof value === "object" && value !== null && value.kind === who.kind && value.id === who.id;
97
98
  var hasId = (e) => typeof e === "object" && e !== null && typeof e.id === "string";
98
99
  var Board = class _Board {
99
100
  doc;
@@ -348,6 +349,62 @@ var Board = class _Board {
348
349
  this.write(plan, by);
349
350
  return plan.map(([memberId]) => memberId);
350
351
  }
352
+ /** Every stored entry as it is, valid or not, with its key as `id`. */
353
+ rawElements() {
354
+ return [...this.elements].map(([key, entry]) => ({ ...entry instanceof Y.Map ? entry.toJSON() : {}, id: key }));
355
+ }
356
+ /**
357
+ * The raw stored elements that `who` made (`author`) and the ids of those that `who` last edited but did not make.
358
+ * It reads entries as they are, with no element checks, so a damaged entry still shows. Account export uses it.
359
+ */
360
+ stampedBy(who) {
361
+ const authored = [];
362
+ const edited = [];
363
+ for (const [key, entry] of this.elements) {
364
+ if (!(entry instanceof Y.Map)) continue;
365
+ if (isStamp(entry.get("author"), who)) authored.push({ ...entry.toJSON(), id: key });
366
+ else if (isStamp(entry.get("editedBy"), who)) edited.push(key);
367
+ }
368
+ return { authored, edited, titleBy: isStamp(this.meta.get("titleBy"), who) };
369
+ }
370
+ /**
371
+ * Erase `who` from the board. An element they made is deleted alone: its members go to its parent group, and a
372
+ * connector of someone else that was bound to it goes too, because it would be hidden. An element they only edited
373
+ * keeps its content, and its editor becomes `(erased)`. A title they set is cleared. Idempotent. Returns the counts.
374
+ */
375
+ eraseStamps(who, erased) {
376
+ const mine = /* @__PURE__ */ new Set();
377
+ for (const [key, entry] of this.elements) if (entry instanceof Y.Map && isStamp(entry.get("author"), who)) mine.add(key);
378
+ const counts = { authored: mine.size, edited: 0, dangling: 0, title: isStamp(this.meta.get("titleBy"), who) };
379
+ this.doc.transact(() => {
380
+ for (const key of mine) {
381
+ const parent = this.elements.get(key).get("groupId");
382
+ for (const [, other] of this.elements) {
383
+ if (other instanceof Y.Map && other.get("groupId") === key) {
384
+ if (typeof parent === "string") other.set("groupId", parent);
385
+ else other.delete("groupId");
386
+ }
387
+ }
388
+ }
389
+ mine.forEach((key) => this.elements.delete(key));
390
+ for (const [key, entry] of [...this.elements]) {
391
+ if (!(entry instanceof Y.Map)) continue;
392
+ const ends = [entry.get("from"), entry.get("to")];
393
+ if (entry.get("type") === "connector" && ends.some((e) => hasId(e) && mine.has(e.id))) {
394
+ this.elements.delete(key);
395
+ counts.dangling += 1;
396
+ } else if (isStamp(entry.get("editedBy"), who)) {
397
+ entry.set("editedBy", erased);
398
+ counts.edited += 1;
399
+ }
400
+ }
401
+ if (counts.title) {
402
+ this.meta.delete("title");
403
+ this.meta.set("titleBy", erased);
404
+ }
405
+ });
406
+ return counts;
407
+ }
351
408
  /** Delete an element. A shape takes its connectors and a group takes its members. Returns the ids removed. */
352
409
  remove(elementId) {
353
410
  this.require(elementId);
package/dist/cli.js CHANGED
@@ -5,18 +5,18 @@ import {
5
5
  operatorActor,
6
6
  runMcp,
7
7
  svgToPng
8
- } from "./chunk-7EWHZCKY.js";
8
+ } from "./chunk-YKUHPOQV.js";
9
9
  import {
10
10
  openLocalStore
11
11
  } from "./chunk-NRLF6MGR.js";
12
12
  import {
13
13
  CanvasService
14
- } from "./chunk-Z6KLGAFP.js";
14
+ } from "./chunk-GLNF43N4.js";
15
15
  import "./chunk-NWQED42L.js";
16
- import "./chunk-EDQNA2SU.js";
16
+ import "./chunk-BKG3FXX3.js";
17
17
  import {
18
18
  CanvasError
19
- } from "./chunk-MVWUIBGV.js";
19
+ } from "./chunk-ZP6BLCB7.js";
20
20
 
21
21
  // src/cli.ts
22
22
  import { readFileSync, writeFileSync } from "fs";
@@ -100,6 +100,9 @@ function createClient(options) {
100
100
  grant: (id, input, o) => json("POST", `/boards/${seg(id)}/grants`, input, o),
101
101
  revoke: (id, { subjectKind, subjectId }, o) => json("DELETE", `/boards/${seg(id)}/grants?subjectKind=${seg(subjectKind)}&subjectId=${seg(subjectId)}`, void 0, o),
102
102
  invite: (id, input, o) => json("POST", `/boards/${seg(id)}/invites`, input, o),
103
+ links: (id, o) => json("GET", `/boards/${seg(id)}/links`, void 0, o),
104
+ createLink: (id, input, o) => json("POST", `/boards/${seg(id)}/links`, input, o),
105
+ revokeLink: (id, linkId, o) => json("DELETE", `/boards/${seg(id)}/links/${seg(linkId)}`, void 0, o),
103
106
  uploadImage: async (data, o) => parse(await send("POST", "/images", { body: data, contentType: "application/octet-stream" }, o), o),
104
107
  getImage: async (id, o) => {
105
108
  const response = await send("GET", `/images/${seg(id)}`, {}, o);
@@ -135,6 +138,9 @@ Commands:
135
138
  grant <board> <human|agent>:<id> --role <viewer|commenter|editor|owner> Server only. Give a role. Owner.
136
139
  revoke <board> <human|agent>:<id> Server only. Take a role back. Owner.
137
140
  invite <board> --role <viewer|commenter|editor> --email <e> | --handle <h> [--expires-in-ms <n>] Server only. Owner.
141
+ links <board> Server only. List the share links, without tokens. Owner.
142
+ link <board> --role <viewer|commenter|editor> [--signed-in-only] [--expires-in-ms <n>] Server only. Make a share link. Prints the token once. Owner.
143
+ revoke-link <board> <link-id> Server only. Revoke a share link. Owner.
138
144
  mcp Serve these as MCP tools on stdio. Needs CANVAS_ACTOR.
139
145
 
140
146
  Live mode: set CANVAS_SERVER=<worker url> and CANVAS_TOKEN=<bearer token>. Every command then runs in the live room of
@@ -168,6 +174,7 @@ var flags = {
168
174
  email: { type: "string" },
169
175
  handle: { type: "string" },
170
176
  "expires-in-ms": { type: "string" },
177
+ "signed-in-only": { type: "boolean" },
171
178
  out: { type: "string" },
172
179
  drop: { type: "string", multiple: true },
173
180
  help: { type: "boolean" },
@@ -293,6 +300,17 @@ async function run(argv) {
293
300
  const expiresInMs = num(v["expires-in-ms"]);
294
301
  return print(await sharing().invite(board, { role: v.role, email: v.email, handle: v.handle, ...expiresInMs === void 0 ? {} : { expiresInMs } }));
295
302
  }
303
+ case "links":
304
+ need(1, "<board>");
305
+ return print(await sharing().links(board));
306
+ case "link": {
307
+ need(1, "<board> --role <role> [--signed-in-only] [--expires-in-ms <n>]");
308
+ const expiresInMs = num(v["expires-in-ms"]);
309
+ return print(await sharing().createLink(board, { role: v.role, ...v["signed-in-only"] ? { signedInOnly: true } : {}, ...expiresInMs === void 0 ? {} : { expiresInMs } }));
310
+ }
311
+ case "revoke-link":
312
+ need(2, "<board> <link-id>");
313
+ return print(await sharing().revokeLink(board, rest[0]));
296
314
  case "mcp":
297
315
  need(0, "");
298
316
  return runMcp(service, actor(), server ? sharing() : void 0);
@@ -1,4 +1,4 @@
1
- import { a as BoardData, c as ShapeType, S as Shape, E as Endpoint, C as Connector } from './read-UIJlrkwx.js';
1
+ import { a as BoardData, c as ShapeType, S as Shape, E as Endpoint, C as Connector } from './read-DeS2Zaz8.js';
2
2
  import { PrincipalKind } from 'agentic-identity';
3
3
 
4
4
  /** Options for every call. */
@@ -43,6 +43,23 @@ interface BoardGrant {
43
43
  role: BoardRole;
44
44
  [field: string]: unknown;
45
45
  }
46
+ /** A share link of a board. It never shows the token: the token shows once, when the link is made. */
47
+ interface BoardLink {
48
+ id: string;
49
+ role: Exclude<BoardRole, "owner">;
50
+ signedInOnly: boolean;
51
+ expiresAt?: number;
52
+ /** Set once the link is revoked. Test it with `!== undefined`: a damaged record shows as 0, and 0 is falsy. */
53
+ revokedAt?: number;
54
+ }
55
+ interface LinkInput {
56
+ /** Not `owner`: only a grant makes an owner. */
57
+ role: Exclude<BoardRole, "owner">;
58
+ /** Only a signed-in visitor may use the link. */
59
+ signedInOnly?: boolean;
60
+ /** How long the link lives, in milliseconds. */
61
+ expiresInMs?: number;
62
+ }
46
63
  /** An open invite. It never shows the email. */
47
64
  interface BoardInvite {
48
65
  id: string;
@@ -133,6 +150,21 @@ interface CanvasClient {
133
150
  invite(id: string, input: InviteInput, options?: CallOptions): Promise<{
134
151
  id: string;
135
152
  }>;
153
+ /** The share links of a board, without their tokens, up to 200. `truncated` is set if there are more. `skipped` counts damaged records left out. Needs the owner role. */
154
+ links(id: string, options?: CallOptions): Promise<{
155
+ links: BoardLink[];
156
+ truncated?: true;
157
+ skipped?: number;
158
+ }>;
159
+ /** Make a share link. The token comes back once and is never stored. Needs the owner role. */
160
+ createLink(id: string, input: LinkInput, options?: CallOptions): Promise<{
161
+ id: string;
162
+ token: string;
163
+ }>;
164
+ /** Revoke a share link. Needs the owner role. */
165
+ revokeLink(id: string, linkId: string, options?: CallOptions): Promise<{
166
+ ok: true;
167
+ }>;
136
168
  uploadImage(bytes: Uint8Array | ArrayBuffer | Blob, options?: CallOptions): Promise<ImageInfo>;
137
169
  /** The bytes of a stored image, and its content type. */
138
170
  getImage(id: string, options?: CallOptions): Promise<{
@@ -148,6 +180,6 @@ interface CanvasClient {
148
180
  }>;
149
181
  }
150
182
  /** What the CLI and the MCP server need to share a board. Sharing lives on the server, so it needs a client. */
151
- type BoardSharing = Pick<CanvasClient, "access" | "grant" | "revoke" | "invite">;
183
+ type BoardSharing = Pick<CanvasClient, "access" | "grant" | "revoke" | "invite" | "links" | "createLink" | "revokeLink">;
152
184
 
153
185
  export type { BoardSharing as B, CanvasClient as C };
package/dist/client.d.ts CHANGED
@@ -114,6 +114,23 @@ interface BoardGrant {
114
114
  role: BoardRole;
115
115
  [field: string]: unknown;
116
116
  }
117
+ /** A share link of a board. It never shows the token: the token shows once, when the link is made. */
118
+ interface BoardLink {
119
+ id: string;
120
+ role: Exclude<BoardRole, "owner">;
121
+ signedInOnly: boolean;
122
+ expiresAt?: number;
123
+ /** Set once the link is revoked. Test it with `!== undefined`: a damaged record shows as 0, and 0 is falsy. */
124
+ revokedAt?: number;
125
+ }
126
+ interface LinkInput {
127
+ /** Not `owner`: only a grant makes an owner. */
128
+ role: Exclude<BoardRole, "owner">;
129
+ /** Only a signed-in visitor may use the link. */
130
+ signedInOnly?: boolean;
131
+ /** How long the link lives, in milliseconds. */
132
+ expiresInMs?: number;
133
+ }
117
134
  /** An open invite. It never shows the email. */
118
135
  interface BoardInvite {
119
136
  id: string;
@@ -214,6 +231,21 @@ interface CanvasClient {
214
231
  invite(id: string, input: InviteInput, options?: CallOptions): Promise<{
215
232
  id: string;
216
233
  }>;
234
+ /** The share links of a board, without their tokens, up to 200. `truncated` is set if there are more. `skipped` counts damaged records left out. Needs the owner role. */
235
+ links(id: string, options?: CallOptions): Promise<{
236
+ links: BoardLink[];
237
+ truncated?: true;
238
+ skipped?: number;
239
+ }>;
240
+ /** Make a share link. The token comes back once and is never stored. Needs the owner role. */
241
+ createLink(id: string, input: LinkInput, options?: CallOptions): Promise<{
242
+ id: string;
243
+ token: string;
244
+ }>;
245
+ /** Revoke a share link. Needs the owner role. */
246
+ revokeLink(id: string, linkId: string, options?: CallOptions): Promise<{
247
+ ok: true;
248
+ }>;
217
249
  uploadImage(bytes: Uint8Array | ArrayBuffer | Blob, options?: CallOptions): Promise<ImageInfo>;
218
250
  /** The bytes of a stored image, and its content type. */
219
251
  getImage(id: string, options?: CallOptions): Promise<{
@@ -229,9 +261,9 @@ interface CanvasClient {
229
261
  }>;
230
262
  }
231
263
  /** What the CLI and the MCP server need to share a board. Sharing lives on the server, so it needs a client. */
232
- type BoardSharing = Pick<CanvasClient, "access" | "grant" | "revoke" | "invite">;
264
+ type BoardSharing = Pick<CanvasClient, "access" | "grant" | "revoke" | "invite" | "links" | "createLink" | "revokeLink">;
233
265
  /** `kind:id` of a person or an agent, as the CLI and the tools write it. */
234
266
  declare function parseSubject(text: string): Subject;
235
267
  declare function createClient(options: ClientOptions): CanvasClient;
236
268
 
237
- export { type ApiErrorCode, type BoardGrant, type BoardInvite, type BoardRole, type BoardSharing, type CallOptions, CanvasApiError, type CanvasClient, type ClientOptions, type ConnectorInput, type ConnectorPatch, type ImageInfo, type InviteInput, type ShapeInput, type ShapePatch, type Subject, type SubjectKind, createClient, parseSubject };
269
+ export { type ApiErrorCode, type BoardGrant, type BoardInvite, type BoardLink, type BoardRole, type BoardSharing, type CallOptions, CanvasApiError, type CanvasClient, type ClientOptions, type ConnectorInput, type ConnectorPatch, type ImageInfo, type InviteInput, type LinkInput, type ShapeInput, type ShapePatch, type Subject, type SubjectKind, createClient, parseSubject };
package/dist/client.js CHANGED
@@ -76,6 +76,9 @@ function createClient(options) {
76
76
  grant: (id, input, o) => json("POST", `/boards/${seg(id)}/grants`, input, o),
77
77
  revoke: (id, { subjectKind, subjectId }, o) => json("DELETE", `/boards/${seg(id)}/grants?subjectKind=${seg(subjectKind)}&subjectId=${seg(subjectId)}`, void 0, o),
78
78
  invite: (id, input, o) => json("POST", `/boards/${seg(id)}/invites`, input, o),
79
+ links: (id, o) => json("GET", `/boards/${seg(id)}/links`, void 0, o),
80
+ createLink: (id, input, o) => json("POST", `/boards/${seg(id)}/links`, input, o),
81
+ revokeLink: (id, linkId, o) => json("DELETE", `/boards/${seg(id)}/links/${seg(linkId)}`, void 0, o),
79
82
  uploadImage: async (data, o) => parse(await send("POST", "/images", { body: data, contentType: "application/octet-stream" }, o), o),
80
83
  getImage: async (id, o) => {
81
84
  const response = await send("GET", `/images/${seg(id)}`, {}, o);
package/dist/embed.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import * as react from 'react';
2
- import { C as CanvasClient } from './client-Dyd30StF.js';
2
+ import { C as CanvasClient } from './client-BKTZ1pM5.js';
3
3
  import { PrincipalKind } from 'agentic-identity';
4
4
  import { SyncClient, Status } from 'agentic-realtime/client';
5
5
  import * as Y from 'yjs';
6
- import './read-UIJlrkwx.js';
6
+ import './read-DeS2Zaz8.js';
7
+ import './principal-blhsFbdS.js';
7
8
  import 'zod';
8
9
 
9
10
  /** The presence of a room: who is here. A `SyncClient` of agentic-realtime has these. */
package/dist/embed.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  PRINCIPAL_KINDS,
4
4
  principalSchema,
5
5
  readBoard
6
- } from "./chunk-MVWUIBGV.js";
6
+ } from "./chunk-ZP6BLCB7.js";
7
7
 
8
8
  // ui/embed.tsx
9
9
  import { ToastProvider, TooltipProvider } from "agentic-design/react";
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { B as Board, P as Principal } from './read-UIJlrkwx.js';
2
- export { a as BoardData, b as CanvasElement, d as CanvasError, C as Connector, e as EdgeData, E as Endpoint, f 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-UIJlrkwx.js';
3
- import { B as BoardAccess } from './service-ojpxVNVl.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-ojpxVNVl.js';
1
+ import { B as Board } from './read-DeS2Zaz8.js';
2
+ export { a as BoardData, b as CanvasElement, d as CanvasError, C as Connector, e as EdgeData, E as Endpoint, f 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, r as readBoard } from './read-DeS2Zaz8.js';
3
+ import { P as Principal } from './principal-blhsFbdS.js';
4
+ export { p as principalSchema } from './principal-blhsFbdS.js';
5
+ import { B as BoardAccess } from './service-BNEH2Z12.js';
6
+ 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-BNEH2Z12.js';
5
7
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
- import { B as BoardSharing } from './client-Dyd30StF.js';
8
+ import { B as BoardSharing } from './client-BKTZ1pM5.js';
7
9
  export { FileStoreOptions, createFileStore, openLocalStore } from './store-file.js';
8
10
  export { PRINCIPAL_KINDS, PrincipalKind, isMachineMade } from 'agentic-identity';
9
11
  import 'yjs';
package/dist/index.js CHANGED
@@ -6,14 +6,14 @@ import {
6
6
  operatorActor,
7
7
  runMcp,
8
8
  svgToPng
9
- } from "./chunk-7EWHZCKY.js";
9
+ } from "./chunk-YKUHPOQV.js";
10
10
  import {
11
11
  createFileStore,
12
12
  openLocalStore
13
13
  } from "./chunk-NRLF6MGR.js";
14
14
  import {
15
15
  CanvasService
16
- } from "./chunk-Z6KLGAFP.js";
16
+ } from "./chunk-GLNF43N4.js";
17
17
  import "./chunk-NWQED42L.js";
18
18
  import {
19
19
  MAX_FLOW_EDGES,
@@ -22,7 +22,7 @@ import {
22
22
  layoutFlow,
23
23
  parseFlow,
24
24
  toSvg
25
- } from "./chunk-EDQNA2SU.js";
25
+ } from "./chunk-BKG3FXX3.js";
26
26
  import {
27
27
  Board,
28
28
  CanvasError,
@@ -33,7 +33,7 @@ import {
33
33
  isMachineMade,
34
34
  principalSchema,
35
35
  readBoard
36
- } from "./chunk-MVWUIBGV.js";
36
+ } from "./chunk-ZP6BLCB7.js";
37
37
  export {
38
38
  ActorConfigError,
39
39
  BRAND,
@@ -5,10 +5,11 @@ import 'agentic-identity';
5
5
  import 'agentic-realtime';
6
6
  import 'agentic-identity/cloudflare';
7
7
  import 'hono';
8
- import './read-UIJlrkwx.js';
9
- import 'yjs';
8
+ import './principal-blhsFbdS.js';
10
9
  import 'zod';
11
- import './service-ojpxVNVl.js';
10
+ import './service-BNEH2Z12.js';
11
+ import './read-DeS2Zaz8.js';
12
+ import 'yjs';
12
13
 
13
14
  type Namespace = Parameters<typeof createDurableObjectRealtime>[0];
14
15
  interface LiveEnv extends WorkerEnv {
@@ -1,12 +1,13 @@
1
1
  import {
2
2
  createApp
3
- } from "./chunk-5II2WZIL.js";
4
- import "./chunk-Z6KLGAFP.js";
3
+ } from "./chunk-5QUL3URJ.js";
4
+ import "./chunk-A2F73EQJ.js";
5
+ import "./chunk-GLNF43N4.js";
5
6
  import "./chunk-NWQED42L.js";
6
- import "./chunk-EDQNA2SU.js";
7
+ import "./chunk-BKG3FXX3.js";
7
8
  import {
8
9
  CanvasError
9
- } from "./chunk-MVWUIBGV.js";
10
+ } from "./chunk-ZP6BLCB7.js";
10
11
 
11
12
  // src/live-worker.ts
12
13
  import { parseRegion } from "agentic-identity";
package/dist/live.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { B as Board, P as Principal, a as BoardData, S as Shape, C as Connector, b as CanvasElement } from './read-UIJlrkwx.js';
1
+ import { B as Board, a as BoardData, S as Shape, C as Connector, b as CanvasElement } from './read-DeS2Zaz8.js';
2
2
  import { Peer } from 'agentic-realtime/client';
3
- import { B as BoardAccess, D as DrawOptions } from './service-ojpxVNVl.js';
3
+ import { B as BoardAccess, D as DrawOptions } from './service-BNEH2Z12.js';
4
+ import { P as Principal } from './principal-blhsFbdS.js';
4
5
  import 'yjs';
5
6
  import 'agentic-identity';
6
7
  import 'zod';