agentic-canvas 0.12.1 → 0.13.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.12.1. The board model, the agent operations, the exports, the CLI, the
10
+ > Status: 0.13.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.
@@ -230,6 +230,36 @@ A live MCP server with an admin token can share boards: an agent that holds it c
230
230
 
231
231
  The static dev fallback holds no board roles. Its tokens reach every board and cannot share.
232
232
 
233
+ ## Account export and erase
234
+
235
+ A host that runs a person's data export and erase imports `canvasAccount` from `agentic-canvas/account`. It never reads the storage layout.
236
+
237
+ ```ts
238
+ import { canvasAccount } from "agentic-canvas/account";
239
+ const data = await canvasAccount.exportAccount({ kind: "human", id: "ada" }, { store, saveRoom, eraseRoom });
240
+ const counts = await canvasAccount.eraseAccount({ kind: "human", id: "ada" }, { store, saveRoom, eraseRoom });
241
+ ```
242
+
243
+ The export is plain JSON in the collections `boards`, `grants`, `invites`, `authored_elements`, `edited_elements`, `titles`, and `audit`. It has no format or time. The erase returns a count for each name.
244
+
245
+ - A board that the subject alone owns is deleted with its grants, invites, links, and audit entries. The erase calls `eraseRoom(boardId)` first, as the Worker does.
246
+ - On any other board, the subject's grant and invites go. The elements it made are deleted. A connector of someone else that was bound to one of them goes too, and no count or export lists it. The members of a deleted group move to its parent. The editor of an element it only edited becomes `(erased)`, and a title it set is cleared.
247
+ - Its audit entries (it acted, it roots the chain, or a grant named it) stay with the id set to `(erased)`. The kinds stay, so an entry still says if a machine acted.
248
+ - A second erase counts zero. Stop writes first: a write between export and erase is erased and not exported.
249
+ - The erase reads every stored board to find the subject's elements, before it changes data. It stops with `INVALID` on a board that does not decode, on a damaged grant of the subject, on a damaged invite of the subject, on a board with child resources, on an owner grant that the sharing records do not accept, on a damaged co-owner grant, and on an audit entry that names the subject, as the actor, the root of the chain, or the target, with an unknown kind. The checks may add audit entries about the subject: the erase scrubs them.
250
+ - Live rooms: give `saveRoom(boardId)`, which saves the room to the Store as the Worker does before a read. Without it, edits that no request has saved yet are not seen, and the reset below drops them. Give `eraseRoom(boardId)` too: the erase calls it for every board that it changes or deletes, so the room opens again from the Store.
251
+ - `boards` holds each stored entry as it is, with full images and groups. It includes the elements that others made on the board.
252
+ - Run the canvas erase before the identity erase of the person: the identity erase removes the grants that this erase finds boards by.
253
+ - The subject is any principal kind, for example `agent_of_human`: call once for each principal. The id follows the strict identity rule: an old board that has an id from the older, looser rule cannot be named.
254
+ - Known gaps:
255
+ - Invites and audit entries name a principal by id with no kind, so `human:ada` and `agent:ada` share them. Do not give two principals one id.
256
+ - An audit entry where the subject is only a middle step of the chain is not found.
257
+ - An invite that someone else made for the subject's email or handle stays. The package cannot map a principal to an address.
258
+ - Image bytes in the image store have no uploader record, so they are not exported or erased. Only the `blob:` key in an element is.
259
+ - A deleted element leaves its id in the stored Yjs history. Its content is gone, and an id that the caller chose can stay in the stored bytes.
260
+ - A connector of someone else that the subject edited, and that goes with a deleted shape, is in `edited_elements` of the export and not in its erase count.
261
+ - The shape is provisional. It is the `AccountPackage` contract that a later `agentic-identity` release adds to `agentic-identity/account`. The `AccountApp` of 0.9.0 is a different contract, and this does not fit it.
262
+
233
263
  ## UI
234
264
 
235
265
  The Worker serves a small web UI at its root. It uses the published `agentic-design` package for
@@ -0,0 +1,33 @@
1
+ import { Store, Json } from 'agentic-identity';
2
+ import { P as Principal } from './principal-blhsFbdS.js';
3
+ import 'zod';
4
+
5
+ interface CanvasAccountDeps {
6
+ /** The Store that holds the boards and the sharing records. */
7
+ store: Store;
8
+ /** Saves the live room of a board to the Store, as the Worker does before it reads a board. Without it, edits that no request has saved yet are not seen, and an erase that resets a room drops them. The call runs for every stored board, so it must be cheap for a board with no room. A board with no room needs no work. Omit it when no room runs. */
9
+ saveRoom?: (boardId: string) => Promise<void>;
10
+ /** Erases the live room of a board, so it opens again from the Store. The erase calls it for every board that it changes or deletes. Omit it when no room runs. */
11
+ eraseRoom?: (boardId: string) => Promise<void>;
12
+ }
13
+ type CanvasCollection = "boards" | "grants" | "invites" | "authored_elements" | "edited_elements" | "titles" | "audit";
14
+ interface CanvasAccount {
15
+ id: "canvas";
16
+ /**
17
+ * Collections: `boards` (each board the subject is the only owner of, with its elements), `grants` (every role the subject
18
+ * holds on a board), `invites` (made by the subject, with no email), `authored_elements` (made by the subject on a board of someone else, with
19
+ * its `boardId`), `edited_elements` (made by someone else, last edited by the subject: `boardId` and `elementId`), `titles` (boards
20
+ * of someone else whose title the subject set), and `audit` (entries on a board where the subject acted as the principal, as the root of the chain, or was the target of a grant, as stored). It holds no format, version, or time.
21
+ */
22
+ exportAccount(subject: Principal, deps: CanvasAccountDeps): Promise<Record<CanvasCollection, Json[]>>;
23
+ /**
24
+ * Deletes the boards the subject is the only owner of, with their grants, invites, links, and audit entries. On every other
25
+ * board it removes the subject's grant and invites, deletes the elements the subject made, sets the editor of the
26
+ * elements it edited to `(erased)`, clears a title it set, and sets its audit entries to `(erased)`. Counts by the same collection
27
+ * names. A second run counts zero. Run it with writes stopped: a write after the export is erased and not exported.
28
+ */
29
+ eraseAccount(subject: Principal, deps: CanvasAccountDeps): Promise<Record<CanvasCollection, number>>;
30
+ }
31
+ declare const canvasAccount: CanvasAccount;
32
+
33
+ export { type CanvasAccount, type CanvasAccountDeps, type CanvasCollection, canvasAccount };
@@ -0,0 +1,213 @@
1
+ import {
2
+ RESOURCE_TYPE
3
+ } from "./chunk-LMSMUGBF.js";
4
+ import {
5
+ fromBase64,
6
+ isBoardId,
7
+ toBase64
8
+ } from "./chunk-GLNF43N4.js";
9
+ import "./chunk-NWQED42L.js";
10
+ import "./chunk-BKG3FXX3.js";
11
+ import {
12
+ Board,
13
+ CanvasError,
14
+ PRINCIPAL_KINDS,
15
+ principalSchema
16
+ } from "./chunk-ZP6BLCB7.js";
17
+
18
+ // src/account.ts
19
+ import { createSharing, ROLES, SUBJECT_KINDS } from "agentic-identity";
20
+ var ERASED = "(erased)";
21
+ var PAGE = 100;
22
+ async function* pages(store, collection, where) {
23
+ let cursor;
24
+ do {
25
+ const page = await store.query(collection, { where, limit: PAGE, ...cursor === void 0 ? {} : { cursor } });
26
+ yield* page.items;
27
+ cursor = page.nextCursor;
28
+ } while (cursor !== void 0);
29
+ }
30
+ var rows = async (store, collection, where) => {
31
+ const out = [];
32
+ for await (const row of pages(store, collection, where)) out.push(row);
33
+ return out;
34
+ };
35
+ var plain = (value) => JSON.parse(JSON.stringify(value));
36
+ function check(subject, deps) {
37
+ if (typeof deps?.store?.query !== "function") throw new CanvasError("INVALID", "canvasAccount needs deps.store");
38
+ for (const hook of [deps.saveRoom, deps.eraseRoom]) if (hook !== void 0 && typeof hook !== "function") throw new CanvasError("INVALID", "deps.saveRoom and deps.eraseRoom must be functions");
39
+ if (!principalSchema.safeParse(subject).success) throw new CanvasError("INVALID", "subject needs an id and a known kind");
40
+ }
41
+ async function held(store, subject) {
42
+ const all = [];
43
+ const sole = /* @__PURE__ */ new Set();
44
+ for await (const { id: grantId, data } of pages(store, "grants", { subjectKind: subject.kind, subjectId: subject.id })) {
45
+ const { resourceId: id, role } = data;
46
+ if (typeof id !== "string" || typeof role !== "string" || !ROLES.includes(role)) throw new CanvasError("INVALID", `grant "${grantId}" of the subject is damaged`);
47
+ const resource = await store.get("resources", id);
48
+ if (resource === null ? !isBoardId(id) : resource.type !== RESOURCE_TYPE) continue;
49
+ all.push({ grantId, resourceId: id, role });
50
+ if (resource === null || role !== "owner") continue;
51
+ const owners = await rows(store, "grants", { resourceId: id, role: "owner" });
52
+ if (owners.some((o) => typeof o.data.subjectId !== "string" || !SUBJECT_KINDS.includes(o.data.subjectKind))) throw new CanvasError("INVALID", `an owner grant on board "${id}" is damaged`);
53
+ if (owners.every((o) => o.data.subjectKind === subject.kind && o.data.subjectId === subject.id)) sole.add(id);
54
+ }
55
+ return { all, sole };
56
+ }
57
+ async function* boards(store, saveRoom) {
58
+ let cursor;
59
+ do {
60
+ const page = await store.query("boards", { limit: PAGE, ...cursor === void 0 ? {} : { cursor } });
61
+ for (const item of page.items) {
62
+ const { id } = item;
63
+ let data = item.data;
64
+ if (saveRoom) {
65
+ await saveRoom(id);
66
+ data = await store.get("boards", id) ?? data;
67
+ }
68
+ try {
69
+ yield { id, board: Board.open(fromBase64(String(data.update))) };
70
+ } catch {
71
+ throw new CanvasError("INVALID", `stored board "${id}" is damaged`);
72
+ }
73
+ }
74
+ cursor = page.nextCursor;
75
+ } while (cursor !== void 0);
76
+ }
77
+ var isSubject = (value, subject) => typeof value === "object" && value !== null && value.kind === subject.kind && value.id === subject.id;
78
+ async function auditOf(store, subject) {
79
+ const found = /* @__PURE__ */ new Map();
80
+ for (const field of ["principalId", "rootId", "targetId"]) for (const row of await rows(store, "audit", { [field]: subject.id })) found.set(row.id, row);
81
+ const out = [];
82
+ const known = (kind, extra = []) => PRINCIPAL_KINDS.includes(kind) || extra.includes(kind);
83
+ for (const row of found.values()) {
84
+ const { principalId, principalKind, chain, rootId, resourceId, targetKind, targetId } = row.data;
85
+ if (typeof resourceId !== "string" || !isBoardId(resourceId)) continue;
86
+ const resource = await store.get("resources", resourceId);
87
+ if (resource !== null && resource.type !== RESOURCE_TYPE) continue;
88
+ const first = Array.isArray(chain) ? chain[0] : null;
89
+ const damaged = principalId === subject.id && !known(principalKind, ["anonymous"]) || targetId === subject.id && !known(targetKind, ["invite", "link", "person", "app", "key"]) || (first?.id === subject.id || first === null && rootId === subject.id) && !known(first?.kind);
90
+ if (damaged) throw new CanvasError("INVALID", `audit entry "${row.id}" is damaged`);
91
+ if (isSubject({ kind: principalKind, id: principalId }, subject) || isSubject(first, subject) || isSubject({ kind: targetKind, id: targetId }, subject)) out.push(row);
92
+ }
93
+ return out;
94
+ }
95
+ async function invitesOf(store, subject) {
96
+ const out = [];
97
+ for (const row of await rows(store, "invites", { createdBy: subject.id })) {
98
+ const { resourceId } = row.data;
99
+ if (typeof resourceId !== "string") throw new CanvasError("INVALID", `invite "${row.id}" of the subject is damaged`);
100
+ if (!isBoardId(resourceId)) continue;
101
+ const resource = await store.get("resources", resourceId);
102
+ if (resource === null || resource.type === RESOURCE_TYPE) out.push(row);
103
+ }
104
+ return out;
105
+ }
106
+ function scrub(data, subject) {
107
+ const erase = (step) => isSubject(step, subject) ? { ...step, id: ERASED } : step;
108
+ return {
109
+ ...data,
110
+ ...isSubject({ kind: data.principalKind, id: data.principalId }, subject) ? { principalId: ERASED } : {},
111
+ ...data.rootId === subject.id ? { rootId: ERASED } : {},
112
+ ...isSubject({ kind: data.targetKind, id: data.targetId }, subject) ? { targetId: ERASED } : {},
113
+ ...Array.isArray(data.chain) ? { chain: data.chain.map(erase) } : {}
114
+ };
115
+ }
116
+ var canvasAccount = {
117
+ id: "canvas",
118
+ async exportAccount(subject, deps) {
119
+ check(subject, deps);
120
+ const { store } = deps;
121
+ const out = { boards: [], grants: [], invites: [], authored_elements: [], edited_elements: [], titles: [], audit: [] };
122
+ const mine = await held(store, subject);
123
+ const owned = /* @__PURE__ */ new Map();
124
+ for await (const { id, board } of boards(store, deps.saveRoom)) {
125
+ if (mine.sole.has(id)) {
126
+ owned.set(id, plain([{ id, title: board.title, elements: board.rawElements() }])[0]);
127
+ continue;
128
+ }
129
+ const found = board.stampedBy(subject);
130
+ out.authored_elements.push(...plain(found.authored.map((element) => ({ boardId: id, element }))));
131
+ out.edited_elements.push(...plain(found.edited.map((elementId) => ({ boardId: id, elementId }))));
132
+ if (found.titleBy) out.titles.push(...plain([{ boardId: id, title: board.title }]));
133
+ }
134
+ for (const g of mine.all) {
135
+ out.grants.push(...plain([{ resourceId: g.resourceId, subjectKind: subject.kind, subjectId: subject.id, role: g.role }]));
136
+ }
137
+ for (const id of mine.sole) if (owned.has(id)) out.boards.push(owned.get(id));
138
+ for (const { id, data } of await invitesOf(store, subject)) {
139
+ const { resourceId, role, expiresAt, acceptedAt, revokedAt } = data;
140
+ out.invites.push(...plain([{ id, resourceId, role, expiresAt, acceptedAt, revokedAt }]));
141
+ }
142
+ out.audit.push(...plain((await auditOf(store, subject)).map((r) => r.data)));
143
+ return out;
144
+ },
145
+ async eraseAccount(subject, deps) {
146
+ check(subject, deps);
147
+ const { store } = deps;
148
+ const sharing = createSharing(store);
149
+ const total = { boards: 0, grants: 0, invites: 0, authored_elements: 0, edited_elements: 0, titles: 0, audit: 0 };
150
+ const ctx = { principalId: subject.id, kind: subject.kind, chain: [{ id: subject.id, kind: subject.kind }], scopes: [`${RESOURCE_TYPE}:admin`] };
151
+ const mine = await held(store, subject);
152
+ const stamped = [];
153
+ for await (const { id, board } of boards(store, deps.saveRoom)) {
154
+ if (mine.sole.has(id)) continue;
155
+ const found = board.stampedBy(subject);
156
+ if (found.authored.length > 0 || found.edited.length > 0 || found.titleBy) stamped.push(id);
157
+ }
158
+ for (const id of mine.sole) {
159
+ if ((await rows(store, "resources", { parentId: id })).length > 0) throw new CanvasError("INVALID", `board "${id}" has child resources: remove them first`);
160
+ if (!(await sharing.can(ctx, "admin", id)).allowed) throw new CanvasError("INVALID", `the owner grant on board "${id}" is not valid: an operator repairs it first`);
161
+ }
162
+ for (const { id, data } of await auditOf(store, subject)) {
163
+ await store.put("audit", id, scrub(data, subject));
164
+ total.audit += 1;
165
+ }
166
+ for (const { id } of await invitesOf(store, subject)) {
167
+ await store.delete("invites", id);
168
+ total.invites += 1;
169
+ }
170
+ for (const id of mine.sole) {
171
+ await deps.eraseRoom?.(id);
172
+ await store.delete("boards", id);
173
+ await deps.eraseRoom?.(id);
174
+ await sharing.deleteResource(ctx, id).catch((e) => {
175
+ if (e instanceof Error && (e.name === "EraseRefusedError" || e.message === "forbidden")) throw new CanvasError("INVALID", e.message);
176
+ throw e;
177
+ });
178
+ total.boards += 1;
179
+ }
180
+ for (const id of stamped) {
181
+ const done = await store.transaction(async (tx) => {
182
+ const doc = await tx.get("boards", id);
183
+ if (!doc) return null;
184
+ let board;
185
+ try {
186
+ board = Board.open(fromBase64(String(doc.update)));
187
+ } catch {
188
+ throw new CanvasError("INVALID", `stored board "${id}" is damaged`);
189
+ }
190
+ const counts = board.eraseStamps(subject, { kind: subject.kind, id: ERASED });
191
+ await tx.put("boards", id, { update: toBase64(board.encode()) });
192
+ return counts;
193
+ });
194
+ await deps.eraseRoom?.(id);
195
+ if (!done) continue;
196
+ total.authored_elements += done.authored;
197
+ total.edited_elements += done.edited;
198
+ total.titles += done.title ? 1 : 0;
199
+ }
200
+ for (const g of mine.all) {
201
+ if (mine.sole.has(g.resourceId)) {
202
+ total.grants += 1;
203
+ continue;
204
+ }
205
+ await store.delete("grants", g.grantId);
206
+ total.grants += 1;
207
+ }
208
+ return total;
209
+ }
210
+ };
211
+ export {
212
+ canvasAccount
213
+ };
@@ -1,15 +1,16 @@
1
+ import {
2
+ CanvasAccess
3
+ } from "./chunk-LMSMUGBF.js";
1
4
  import {
2
5
  CanvasService,
3
- MAX_LIVE_BYTES,
4
- isBoardId,
5
- isStoreId
6
- } from "./chunk-Z6KLGAFP.js";
6
+ MAX_LIVE_BYTES
7
+ } from "./chunk-GLNF43N4.js";
7
8
  import {
8
9
  CanvasError
9
- } from "./chunk-MVWUIBGV.js";
10
+ } from "./chunk-ZP6BLCB7.js";
10
11
 
11
12
  // src/worker.ts
12
- import { createStoreAuth, hasScope as hasScope2, parsePrincipal, parseRegion } from "agentic-identity";
13
+ import { createStoreAuth, hasScope, parsePrincipal, parseRegion } from "agentic-identity";
13
14
  import { createD1Store, createR2Blobs, D1_SCHEMA } from "agentic-identity/cloudflare";
14
15
  import { acceptWebSocketProtocol, principalFromWebSocketRequest } from "agentic-identity/http";
15
16
 
@@ -1790,150 +1791,6 @@ var Hono3 = class extends Hono {
1790
1791
  }
1791
1792
  };
1792
1793
 
1793
- // src/access.ts
1794
- import { createSharing, EraseRefusedError, hasScope, ROLES, SUBJECT_KINDS } from "agentic-identity";
1795
- var RESOURCE_TYPE = "canvas";
1796
- var SHARE_ROLES = ROLES.filter((r) => r !== "owner");
1797
- var BAD_INPUT = /^(give an email or a handle|invalid handle|invalid email|invalid subject( kind "[^"]*")?|the last owner cannot be removed|expiresInMs must be .*|expiresAt must be .*|invalid role .*)$/;
1798
- function refuse(error) {
1799
- if (error instanceof EraseRefusedError) throw new CanvasError("INVALID", error.message);
1800
- if (error instanceof Error && error.message === "forbidden") throw new CanvasError("FORBIDDEN", "you may not do this on this board");
1801
- if (error instanceof Error && BAD_INPUT.test(error.message)) throw new CanvasError("INVALID", error.message);
1802
- throw error;
1803
- }
1804
- var oneOf = (value, list, name) => {
1805
- if (typeof value !== "string" || !list.includes(value)) throw new CanvasError("INVALID", `${name} must be one of: ${list.join(", ")}`);
1806
- return value;
1807
- };
1808
- var storeId = (value, name) => {
1809
- if (typeof value !== "string" || !isStoreId(value)) throw new CanvasError("INVALID", `${name} must be 1 to 128 of A-Z, a-z, 0-9, _ and -, and not like __x__`);
1810
- return value;
1811
- };
1812
- var word = (value, name) => {
1813
- if (typeof value !== "string" || value === "") throw new CanvasError("INVALID", `${name} must be a non-empty string`);
1814
- return value;
1815
- };
1816
- var lifetime = (value, name) => {
1817
- if (value === void 0) return void 0;
1818
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) throw new CanvasError("INVALID", `${name} must be a whole number of milliseconds above 0`);
1819
- return value;
1820
- };
1821
- var CanvasAccess = class {
1822
- constructor(store, options = {}) {
1823
- this.store = store;
1824
- this.sharing = createSharing(store, options);
1825
- this.now = options.now ?? Date.now;
1826
- }
1827
- store;
1828
- sharing;
1829
- now;
1830
- /** A board id that the Store refuses names no board. The gate checks this for a role holder: a token of the dev list skips the gate. */
1831
- known(id) {
1832
- if (!isBoardId(id)) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
1833
- }
1834
- /** Make the actor the owner of a new board. */
1835
- async register(ctx, id) {
1836
- await this.sharing.createResource(ctx, { id, type: RESOURCE_TYPE }).catch((error) => {
1837
- if (error instanceof Error && error.message === "resource exists") throw new CanvasError("DUPLICATE_ID", `board "${id}" already exists`);
1838
- if (error instanceof Error && error.message === "resource id has old audit entries") throw new CanvasError("UNAVAILABLE", "this board id was used a moment ago. Try again, or use another id");
1839
- return refuse(error);
1840
- });
1841
- }
1842
- /** Remove the sharing records of a board. Needs an owner. */
1843
- async unregister(ctx, id) {
1844
- await this.sharing.deleteResource(ctx, id).catch(refuse);
1845
- }
1846
- /** True if the board has sharing records: it was made by an identity credential. */
1847
- async registered(id) {
1848
- if (!isBoardId(id)) return false;
1849
- return (await this.store.get("resources", id))?.type === RESOURCE_TYPE;
1850
- }
1851
- /**
1852
- * True if the caller may do `action` on the board. A link token adds the role of its link. It writes an audit entry.
1853
- * An id that belongs to a resource of another type, in a Store shared with another app, is no board: it is denied.
1854
- */
1855
- async allowed(ctx, action, id, linkToken) {
1856
- if (!await this.registered(id)) return false;
1857
- return (await this.sharing.can(ctx, action, id, linkToken === void 0 ? {} : { linkToken })).allowed;
1858
- }
1859
- /** Throw NOT_FOUND for a caller who cannot read the board, and FORBIDDEN for one who can read it but not do `action`. */
1860
- async require(ctx, action, id, linkToken) {
1861
- if (await this.allowed(ctx, action, id, linkToken)) return;
1862
- if (action === "read" || !await this.allowed(ctx, "read", id, linkToken)) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
1863
- throw new CanvasError("FORBIDDEN", `this needs the ${action === "admin" ? "owner" : "editor"} role on the board`);
1864
- }
1865
- /** The ids of the boards a person or agent holds a role on, the first 100. A credential without `canvas:read` gets none. */
1866
- async readable(ctx) {
1867
- const root = ctx.chain[0];
1868
- if (!root || !SUBJECT_KINDS.includes(root.kind) || !hasScope(ctx.scopes, `${RESOURCE_TYPE}:read`)) return [];
1869
- const ids = [];
1870
- let cursor;
1871
- for (let pages = 0; pages < 10 && ids.length < 100; pages++) {
1872
- const page = await this.store.query("grants", { where: { subjectKind: root.kind, subjectId: root.id }, limit: 100, ...cursor === void 0 ? {} : { cursor } });
1873
- for (const { data } of page.items) {
1874
- if (ids.length >= 100 || typeof data.resourceId !== "string" || !ROLES.includes(data.role)) continue;
1875
- if (await this.registered(data.resourceId)) ids.push(data.resourceId);
1876
- }
1877
- if ((cursor = page.nextCursor) === void 0) break;
1878
- }
1879
- return ids;
1880
- }
1881
- async grant(ctx, id, input) {
1882
- this.known(id);
1883
- const subject = { kind: oneOf(input.subjectKind, SUBJECT_KINDS, "subjectKind"), id: storeId(input.subjectId, "subjectId") };
1884
- await this.sharing.grant(ctx, id, subject, oneOf(input.role, ROLES, "role")).catch(refuse);
1885
- return { ok: true };
1886
- }
1887
- async revoke(ctx, id, input) {
1888
- this.known(id);
1889
- const subject = { kind: oneOf(input.subjectKind, SUBJECT_KINDS, "subjectKind"), id: storeId(input.subjectId, "subjectId") };
1890
- await this.sharing.revokeGrant(ctx, id, subject).catch(refuse);
1891
- return { ok: true };
1892
- }
1893
- /** The grants and the invites on a board. An invite never shows its email. Links are not listed: a token shows once. */
1894
- async list(ctx, id) {
1895
- this.known(id);
1896
- const [grants, invites] = await Promise.all([this.sharing.listGrants(ctx, id, { limit: 200 }), this.sharing.listInvites(ctx, id, { limit: 200 })]).catch(refuse);
1897
- return { grants: grants.grants, invites: invites.invites };
1898
- }
1899
- async invite(ctx, id, input) {
1900
- this.known(id);
1901
- const role = oneOf(input.role, SHARE_ROLES, "role");
1902
- const expiresInMs = lifetime(input.expiresInMs, "expiresInMs");
1903
- const who = input.email !== void 0 ? { email: word(input.email, "email") } : input.handle !== void 0 ? { handle: word(input.handle, "handle") } : {};
1904
- if (input.email !== void 0 && input.handle !== void 0) throw new CanvasError("INVALID", "give an email or a handle, not both");
1905
- return this.sharing.createInvite(ctx, id, { role, ...who, ...expiresInMs === void 0 ? {} : { expiresInMs } }).catch(refuse);
1906
- }
1907
- /** The email comes from a verified sign-in and the handle from the agent's own record, never from the request. */
1908
- async accept(ctx, inviteId) {
1909
- if (!isStoreId(inviteId)) throw new CanvasError("FORBIDDEN", "you may not do this on this board");
1910
- const user = await this.store.get("users", ctx.principalId);
1911
- const agent = await this.store.get("principals", ctx.principalId);
1912
- const identity = {
1913
- ...user?.emailVerified === true && typeof user.email === "string" ? { email: user.email } : {},
1914
- ...typeof agent?.handle === "string" ? { handle: agent.handle } : {}
1915
- };
1916
- await this.sharing.acceptInvite(ctx, word(inviteId, "id"), identity).catch(refuse);
1917
- return { ok: true };
1918
- }
1919
- /** The token shows once. Only its hash is stored. */
1920
- async createLink(ctx, id, input) {
1921
- this.known(id);
1922
- const role = oneOf(input.role, SHARE_ROLES, "role");
1923
- const expiresInMs = lifetime(input.expiresInMs, "expiresInMs");
1924
- if (input.signedInOnly !== void 0 && typeof input.signedInOnly !== "boolean") throw new CanvasError("INVALID", "signedInOnly must be true or false");
1925
- return this.sharing.createLink(ctx, id, { role, signedInOnly: input.signedInOnly === true, ...expiresInMs === void 0 ? {} : { expiresAt: this.now() + expiresInMs } }).catch(refuse);
1926
- }
1927
- /** Revoke a link of this board. A link of another board is NOT_FOUND here, even for an owner of both. */
1928
- async revokeLink(ctx, id, linkId) {
1929
- this.known(id);
1930
- const link = isStoreId(linkId) ? await this.store.get("links", linkId) : null;
1931
- if (!link || link.resourceId !== id) throw new CanvasError("NOT_FOUND", "no such link on this board");
1932
- await this.sharing.revokeLink(ctx, linkId).catch(refuse);
1933
- return { ok: true };
1934
- }
1935
- };
1936
-
1937
1794
  // src/room-sync.ts
1938
1795
  import * as Y from "yjs";
1939
1796
  var SEEDER = { kind: "agent", id: "canvas-room" };
@@ -2171,7 +2028,7 @@ async function identify(request, env, store) {
2171
2028
  const found = await principalFromWebSocketRequest(request, auth, { protocol: WS_PROTOCOL, scope: "canvas:read" });
2172
2029
  if (failure() !== void 0) throw failure();
2173
2030
  if (found instanceof Response) return null;
2174
- return { actor: { kind: found.kind, id: found.principalId }, role: hasScope2(found.scopes, "canvas:write") ? "editor" : "viewer", principal: found, dev: dev() };
2031
+ return { actor: { kind: found.kind, id: found.principalId }, role: hasScope(found.scopes, "canvas:write") ? "editor" : "viewer", principal: found, dev: dev() };
2175
2032
  }
2176
2033
  var svgResponse = (svg) => new Response(svg, { headers: { "content-type": "image/svg+xml", "x-content-type-options": "nosniff", "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; img-src data:" } });
2177
2034
  function sniffImage(bytes) {
@@ -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.13.0" : readPackageVersion();
10
10
 
11
11
  // src/brand.ts
12
12
  var BRAND = {
@@ -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
@@ -0,0 +1,156 @@
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. Links are not listed: a token shows once. */
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
+ /** Revoke a link of this board. A link of another board is NOT_FOUND here, even for an owner of both. */
144
+ async revokeLink(ctx, id, linkId) {
145
+ this.known(id);
146
+ const link = isStoreId(linkId) ? await this.store.get("links", linkId) : null;
147
+ if (!link || link.resourceId !== id) throw new CanvasError("NOT_FOUND", "no such link on this board");
148
+ await this.sharing.revokeLink(ctx, linkId).catch(refuse);
149
+ return { ok: true };
150
+ }
151
+ };
152
+
153
+ export {
154
+ RESOURCE_TYPE,
155
+ CanvasAccess
156
+ };
@@ -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-AI5NWPEW.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";
@@ -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. */
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-DBb1jpxl.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-DBb1jpxl.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-AI5NWPEW.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-4PY5SN2N.js";
4
+ import "./chunk-LMSMUGBF.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';
package/dist/live.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  drawFlow,
3
3
  toSvg
4
- } from "./chunk-EDQNA2SU.js";
4
+ } from "./chunk-BKG3FXX3.js";
5
5
  import {
6
6
  Board,
7
7
  CanvasError,
8
8
  readBoard
9
- } from "./chunk-MVWUIBGV.js";
9
+ } from "./chunk-ZP6BLCB7.js";
10
10
 
11
11
  // src/live.ts
12
12
  import { parsePrincipal } from "agentic-identity";
@@ -0,0 +1,23 @@
1
+ import { PrincipalKind } from 'agentic-identity';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * The actor of a new write. The rule is the identity kernel's `parsePrincipal`: a known kind, and an id of 1 to 128
6
+ * characters from A-Z, a-z, 0-9, ".", "_" and "-". The Worker, the CLI and the SDK all use it.
7
+ */
8
+ declare const principalSchema: z.ZodPipe<z.ZodObject<{
9
+ kind: z.ZodString;
10
+ id: z.ZodString;
11
+ }, z.core.$strict>, z.ZodTransform<{
12
+ kind: PrincipalKind;
13
+ id: string;
14
+ }, {
15
+ kind: string;
16
+ id: string;
17
+ }>>;
18
+ type Principal = {
19
+ kind: PrincipalKind;
20
+ id: string;
21
+ };
22
+
23
+ export { type Principal as P, principalSchema as p };
@@ -1,25 +1,5 @@
1
1
  import * as Y from 'yjs';
2
- import { PrincipalKind } from 'agentic-identity';
3
- import { z } from 'zod';
4
-
5
- /**
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
- * characters from A-Z, a-z, 0-9, ".", "_" and "-". The Worker, the CLI and the SDK all use it.
8
- */
9
- declare const principalSchema: z.ZodPipe<z.ZodObject<{
10
- kind: z.ZodString;
11
- id: z.ZodString;
12
- }, z.core.$strict>, z.ZodTransform<{
13
- kind: PrincipalKind;
14
- id: string;
15
- }, {
16
- kind: string;
17
- id: string;
18
- }>>;
19
- type Principal = {
20
- kind: PrincipalKind;
21
- id: string;
22
- };
2
+ import { P as Principal } from './principal-blhsFbdS.js';
23
3
 
24
4
  type ErrorCode = "INVALID" | "DUPLICATE_ID" | "NOT_FOUND" | "BAD_REFERENCE" | "LIMIT" | "UNAVAILABLE" | "FORBIDDEN";
25
5
  declare class CanvasError extends Error {
@@ -132,6 +112,28 @@ declare class Board {
132
112
  update(elementId: string, patch: unknown, actor: Principal): CanvasElement;
133
113
  /** Shift an element, or a whole group. Returns the ids that moved. Bound connector ends follow their shapes. */
134
114
  move(elementId: string, dx: number, dy: number, actor: Principal): string[];
115
+ /** Every stored entry as it is, valid or not, with its key as `id`. */
116
+ rawElements(): Record<string, unknown>[];
117
+ /**
118
+ * The raw stored elements that `who` made (`author`) and the ids of those that `who` last edited but did not make.
119
+ * It reads entries as they are, with no element checks, so a damaged entry still shows. Account export uses it.
120
+ */
121
+ stampedBy(who: Principal): {
122
+ authored: Record<string, unknown>[];
123
+ edited: string[];
124
+ titleBy: boolean;
125
+ };
126
+ /**
127
+ * Erase `who` from the board. An element they made is deleted alone: its members go to its parent group, and a
128
+ * connector of someone else that was bound to it goes too, because it would be hidden. An element they only edited
129
+ * keeps its content, and its editor becomes `(erased)`. A title they set is cleared. Idempotent. Returns the counts.
130
+ */
131
+ eraseStamps(who: Principal, erased: Principal): {
132
+ authored: number;
133
+ edited: number;
134
+ dangling: number;
135
+ title: boolean;
136
+ };
135
137
  /** Delete an element. A shape takes its connectors and a group takes its members. Returns the ids removed. */
136
138
  remove(elementId: string): string[];
137
139
  group(ids: string[], actor: Principal, groupId?: string): Group;
@@ -181,4 +183,4 @@ interface BoardData {
181
183
  /** The board as plain data for an agent. Reads only. Embedded image bytes are left out. */
182
184
  declare function readBoard(board: Board): BoardData;
183
185
 
184
- export { Board as B, type Connector as C, type Endpoint as E, type Group as G, MAX_ELEMENTS as M, type NodeData as N, type Principal as P, type Shape as S, type BoardData as a, type CanvasElement as b, type ShapeType as c, CanvasError as d, type EdgeData as e, type ErrorCode as f, MAX_TEXT as g, SHAPE_TYPES as h, principalSchema as p, readBoard as r };
186
+ export { Board as B, type Connector as C, type Endpoint as E, type Group as G, MAX_ELEMENTS as M, type NodeData as N, type Shape as S, type BoardData as a, type CanvasElement as b, type ShapeType as c, CanvasError as d, type EdgeData as e, type ErrorCode as f, MAX_TEXT as g, SHAPE_TYPES as h, readBoard as r };
@@ -1,5 +1,6 @@
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-UIJlrkwx.js';
1
+ import { c as ShapeType, B as Board, a as BoardData, S as Shape, C as Connector, b as CanvasElement } from './read-DeS2Zaz8.js';
2
2
  import { Store } from 'agentic-identity';
3
+ import { P as Principal } from './principal-blhsFbdS.js';
3
4
 
4
5
  declare const MAX_FLOW_NODES = 500;
5
6
  declare const MAX_FLOW_EDGES = 2000;
package/dist/worker.d.ts CHANGED
@@ -2,10 +2,11 @@ import { Store, PrincipalContext, Action, Grant, InviteSummary, Blobs } from 'ag
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-UIJlrkwx.js';
6
- import { C as CanvasService } from './service-ojpxVNVl.js';
7
- import 'yjs';
5
+ import { P as Principal } from './principal-blhsFbdS.js';
6
+ import { C as CanvasService } from './service-BNEH2Z12.js';
8
7
  import 'zod';
8
+ import './read-DeS2Zaz8.js';
9
+ import 'yjs';
9
10
 
10
11
  /**
11
12
  * Who may do what on a board. A board is an identity resource of type `canvas`. The roles, the grants, the invites,
package/dist/worker.js CHANGED
@@ -5,11 +5,12 @@ import {
5
5
  identify,
6
6
  sniffImage,
7
7
  worker_default
8
- } from "./chunk-5II2WZIL.js";
9
- import "./chunk-Z6KLGAFP.js";
8
+ } from "./chunk-4PY5SN2N.js";
9
+ import "./chunk-LMSMUGBF.js";
10
+ import "./chunk-GLNF43N4.js";
10
11
  import "./chunk-NWQED42L.js";
11
- import "./chunk-EDQNA2SU.js";
12
- import "./chunk-MVWUIBGV.js";
12
+ import "./chunk-BKG3FXX3.js";
13
+ import "./chunk-ZP6BLCB7.js";
13
14
  export {
14
15
  WS_PROTOCOL,
15
16
  authenticate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-canvas",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "A shared whiteboard for diagrams, drawn by people and agents.",
5
5
  "keywords": [
6
6
  "whiteboard",
@@ -44,6 +44,10 @@
44
44
  "types": "./dist/worker.d.ts",
45
45
  "import": "./dist/worker.js"
46
46
  },
47
+ "./account": {
48
+ "types": "./dist/account.d.ts",
49
+ "import": "./dist/account.js"
50
+ },
47
51
  "./live": {
48
52
  "types": "./dist/live.d.ts",
49
53
  "import": "./dist/live.js"