agentic-canvas 0.14.3 → 0.15.1
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 +16 -1
- package/dist/account.d.ts +20 -3
- package/dist/account.js +57 -26
- package/dist/{chunk-BZZKCECH.js → chunk-LHQ2H4BF.js} +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +8 -5
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.
|
|
10
|
+
> Status: 0.15.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.
|
|
@@ -247,6 +247,21 @@ const data = await canvasAccount.exportAccount({ kind: "human", id: "ada" }, { s
|
|
|
247
247
|
const counts = await canvasAccount.eraseAccount({ kind: "human", id: "ada" }, { store, saveRoom, eraseRoom });
|
|
248
248
|
```
|
|
249
249
|
|
|
250
|
+
Set `resourceType` (default `canvas`) and `collection` (default `boards`) to match the host. Board ids must still use 1 to 64 ASCII letters, digits, underscores, or hyphens, and must not start and end with two underscores. This rule also applies to custom resource types and external content sources. The output collection names stay the same. By default, rows without an `update` field are skipped. Give `isBoard(id, data)` to select rows by a host kind field. Rows of the configured resource type are always selected, so damaged personal data cannot be skipped. Selected rows that do not decode still fail with `INVALID`. Shared-board writes keep other row fields. Each scan reads the sharing resource for every stored row to check its type. An erase scans the collection, then reads stamped boards again inside transactions. Allow for these reads on large collections.
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
const deps = {
|
|
254
|
+
store,
|
|
255
|
+
resourceType: "drawing",
|
|
256
|
+
collection: "drawings",
|
|
257
|
+
isBoard: (_id: string, row: Readonly<Record<string, unknown>>) => row.kind === "drawing",
|
|
258
|
+
};
|
|
259
|
+
const data = await canvasAccount.exportAccount(subject, deps);
|
|
260
|
+
await canvasAccount.eraseAccount(subject, deps);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
For content held outside the Store, give `contentSource: { exportContent(id), eraseContent(id) }`. Both methods return promises. The export method returns JSON; each sole-owned board exports as `{ id, content }`. Content must use arrays and plain objects from the current realm (with `Object.prototype` or a null prototype). The export rejects `undefined` values, functions, symbols, class instances, and non-finite numbers, including nested values. A `toJSON()` method can return supported JSON, so dates export as strings. The erase method deletes its content before sharing records go. This mode ignores `collection` and `isBoard`, does not read or write the content collection, and does not call `saveRoom` or `eraseRoom`. It uses sharing records to find sole-owned boards. It still exports and erases grants, invites, and audit data for the configured resource type. It does not inspect or scrub stamps in shared-board content: the host must handle that data separately. For a room backend, the methods can export room history and delete the room. Methods must tolerate retries, including missing content. A rejected method rejects the account call; earlier changes can remain. Sharing records for a failed content delete stay so a retry can finish. Stop writes during the operation.
|
|
264
|
+
|
|
250
265
|
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.
|
|
251
266
|
|
|
252
267
|
- 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.
|
package/dist/account.d.ts
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Json, Store } from 'agentic-identity';
|
|
2
2
|
import { P as Principal } from './principal-blhsFbdS.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
|
|
5
|
+
/** Host-owned whole-board content. Methods must tolerate retries. */
|
|
6
|
+
interface CanvasContentSource {
|
|
7
|
+
readonly exportContent: (id: string) => Promise<Json>;
|
|
8
|
+
readonly eraseContent: (id: string) => Promise<void>;
|
|
9
|
+
}
|
|
5
10
|
interface CanvasAccountDeps {
|
|
11
|
+
/** Sharing resource type. Default: `canvas`. */
|
|
12
|
+
resourceType?: string;
|
|
13
|
+
/** Serialized board collection. Default: `boards`. */
|
|
14
|
+
collection?: string;
|
|
15
|
+
/** Select canvas rows before decoding or saving rooms. Default: rows with an `update` field. */
|
|
16
|
+
isBoard?: (id: string, data: Readonly<Record<string, unknown>>) => boolean;
|
|
17
|
+
/** Whole-board content for sole owners. Replaces collection access and room hooks.
|
|
18
|
+
* Shared-board stamps are outside this mode; the host must handle them separately.
|
|
19
|
+
* Both methods must tolerate retries, including a board already erased.
|
|
20
|
+
* Ignores collection, isBoard, saveRoom, and eraseRoom when supplied.
|
|
21
|
+
*/
|
|
22
|
+
contentSource?: CanvasContentSource;
|
|
6
23
|
/** The Store that holds the boards and the sharing records. */
|
|
7
24
|
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
|
|
25
|
+
/** 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 only for selected canvas rows, and never with contentSource. 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
26
|
saveRoom?: (boardId: string) => Promise<void>;
|
|
10
27
|
/** 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
28
|
eraseRoom?: (boardId: string) => Promise<void>;
|
|
@@ -30,4 +47,4 @@ interface CanvasAccount {
|
|
|
30
47
|
}
|
|
31
48
|
declare const canvasAccount: CanvasAccount;
|
|
32
49
|
|
|
33
|
-
export { type CanvasAccount, type CanvasAccountDeps, type CanvasCollection, canvasAccount };
|
|
50
|
+
export { type CanvasAccount, type CanvasAccountDeps, type CanvasCollection, type CanvasContentSource, canvasAccount };
|
package/dist/account.js
CHANGED
|
@@ -36,16 +36,19 @@ var plain = (value) => JSON.parse(JSON.stringify(value));
|
|
|
36
36
|
function check(subject, deps) {
|
|
37
37
|
if (typeof deps?.store?.query !== "function") throw new CanvasError("INVALID", "canvasAccount needs deps.store");
|
|
38
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
|
+
for (const value of [deps.resourceType, deps.collection]) if (value !== void 0 && (typeof value !== "string" || !value.trim())) throw new CanvasError("INVALID", "resourceType and collection must be nonempty strings");
|
|
40
|
+
if (deps.isBoard !== void 0 && typeof deps.isBoard !== "function") throw new CanvasError("INVALID", "isBoard must be a function");
|
|
41
|
+
if (deps.contentSource !== void 0 && (typeof deps.contentSource?.exportContent !== "function" || typeof deps.contentSource?.eraseContent !== "function")) throw new CanvasError("INVALID", "contentSource needs exportContent and eraseContent");
|
|
39
42
|
if (!principalSchema.safeParse(subject).success) throw new CanvasError("INVALID", "subject needs an id and a known kind");
|
|
40
43
|
}
|
|
41
|
-
async function held(store, subject) {
|
|
44
|
+
async function held(store, subject, resourceType) {
|
|
42
45
|
const all = [];
|
|
43
46
|
const sole = /* @__PURE__ */ new Set();
|
|
44
47
|
for await (const { id: grantId, data } of pages(store, "grants", { subjectKind: subject.kind, subjectId: subject.id })) {
|
|
45
48
|
const { resourceId: id, role } = data;
|
|
46
49
|
if (typeof id !== "string" || typeof role !== "string" || !ROLES.includes(role)) throw new CanvasError("INVALID", `grant "${grantId}" of the subject is damaged`);
|
|
47
50
|
const resource = await store.get("resources", id);
|
|
48
|
-
if (resource === null ? !isBoardId(id) : resource.type !==
|
|
51
|
+
if (resource === null ? !isBoardId(id) : resource.type !== resourceType) continue;
|
|
49
52
|
all.push({ grantId, resourceId: id, role });
|
|
50
53
|
if (resource === null || role !== "owner") continue;
|
|
51
54
|
const owners = await rows(store, "grants", { resourceId: id, role: "owner" });
|
|
@@ -54,16 +57,21 @@ async function held(store, subject) {
|
|
|
54
57
|
}
|
|
55
58
|
return { all, sole };
|
|
56
59
|
}
|
|
57
|
-
async function* boards(store,
|
|
60
|
+
async function* boards(store, deps) {
|
|
61
|
+
if (deps.contentSource) return;
|
|
62
|
+
const collection = deps.collection ?? "boards";
|
|
58
63
|
let cursor;
|
|
59
64
|
do {
|
|
60
|
-
const page = await store.query(
|
|
65
|
+
const page = await store.query(collection, { limit: PAGE, ...cursor === void 0 ? {} : { cursor } });
|
|
61
66
|
for (const item of page.items) {
|
|
62
67
|
const { id } = item;
|
|
63
68
|
let data = item.data;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
const resource = await store.get("resources", id);
|
|
70
|
+
if (resource !== null && resource.type !== (deps.resourceType ?? RESOURCE_TYPE)) continue;
|
|
71
|
+
if (resource === null && !(deps.isBoard ? deps.isBoard(id, data) : Object.hasOwn(data, "update"))) continue;
|
|
72
|
+
if (deps.saveRoom) {
|
|
73
|
+
await deps.saveRoom(id);
|
|
74
|
+
data = await store.get(collection, id) ?? data;
|
|
67
75
|
}
|
|
68
76
|
try {
|
|
69
77
|
yield { id, board: Board.open(fromBase64(String(data.update))) };
|
|
@@ -75,7 +83,7 @@ async function* boards(store, saveRoom) {
|
|
|
75
83
|
} while (cursor !== void 0);
|
|
76
84
|
}
|
|
77
85
|
var isSubject = (value, subject) => typeof value === "object" && value !== null && value.kind === subject.kind && value.id === subject.id;
|
|
78
|
-
async function auditOf(store, subject) {
|
|
86
|
+
async function auditOf(store, subject, resourceType) {
|
|
79
87
|
const found = /* @__PURE__ */ new Map();
|
|
80
88
|
for (const field of ["principalId", "rootId", "targetId"]) for (const row of await rows(store, "audit", { [field]: subject.id })) found.set(row.id, row);
|
|
81
89
|
const out = [];
|
|
@@ -84,7 +92,7 @@ async function auditOf(store, subject) {
|
|
|
84
92
|
const { principalId, principalKind, chain, rootId, resourceId, targetKind, targetId } = row.data;
|
|
85
93
|
if (typeof resourceId !== "string" || !isBoardId(resourceId)) continue;
|
|
86
94
|
const resource = await store.get("resources", resourceId);
|
|
87
|
-
if (resource !== null && resource.type !==
|
|
95
|
+
if (resource !== null && resource.type !== resourceType) continue;
|
|
88
96
|
const first = Array.isArray(chain) ? chain[0] : null;
|
|
89
97
|
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
98
|
if (damaged) throw new CanvasError("INVALID", `audit entry "${row.id}" is damaged`);
|
|
@@ -92,14 +100,14 @@ async function auditOf(store, subject) {
|
|
|
92
100
|
}
|
|
93
101
|
return out;
|
|
94
102
|
}
|
|
95
|
-
async function invitesOf(store, subject) {
|
|
103
|
+
async function invitesOf(store, subject, resourceType) {
|
|
96
104
|
const out = [];
|
|
97
105
|
for (const row of await rows(store, "invites", { createdBy: subject.id })) {
|
|
98
106
|
const { resourceId } = row.data;
|
|
99
107
|
if (typeof resourceId !== "string") throw new CanvasError("INVALID", `invite "${row.id}" of the subject is damaged`);
|
|
100
108
|
if (!isBoardId(resourceId)) continue;
|
|
101
109
|
const resource = await store.get("resources", resourceId);
|
|
102
|
-
if (resource === null || resource.type ===
|
|
110
|
+
if (resource === null || resource.type === resourceType) out.push(row);
|
|
103
111
|
}
|
|
104
112
|
return out;
|
|
105
113
|
}
|
|
@@ -118,10 +126,11 @@ var canvasAccount = {
|
|
|
118
126
|
async exportAccount(subject, deps) {
|
|
119
127
|
check(subject, deps);
|
|
120
128
|
const { store } = deps;
|
|
129
|
+
const resourceType = deps.resourceType ?? RESOURCE_TYPE;
|
|
121
130
|
const out = { boards: [], grants: [], invites: [], authored_elements: [], edited_elements: [], titles: [], audit: [] };
|
|
122
|
-
const mine = await held(store, subject);
|
|
131
|
+
const mine = await held(store, subject, resourceType);
|
|
123
132
|
const owned = /* @__PURE__ */ new Map();
|
|
124
|
-
for await (const { id, board } of boards(store, deps
|
|
133
|
+
for await (const { id, board } of boards(store, deps)) {
|
|
125
134
|
if (mine.sole.has(id)) {
|
|
126
135
|
owned.set(id, plain([{ id, title: board.title, elements: board.rawElements() }])[0]);
|
|
127
136
|
continue;
|
|
@@ -134,23 +143,41 @@ var canvasAccount = {
|
|
|
134
143
|
for (const g of mine.all) {
|
|
135
144
|
out.grants.push(...plain([{ resourceId: g.resourceId, subjectKind: subject.kind, subjectId: subject.id, role: g.role }]));
|
|
136
145
|
}
|
|
137
|
-
for (const id of mine.sole)
|
|
138
|
-
|
|
146
|
+
for (const id of mine.sole) {
|
|
147
|
+
if (deps.contentSource) {
|
|
148
|
+
const content = await deps.contentSource.exportContent(id);
|
|
149
|
+
let normalized;
|
|
150
|
+
try {
|
|
151
|
+
normalized = JSON.parse(JSON.stringify(content, (_key, value) => {
|
|
152
|
+
if (value === void 0 || typeof value === "function" || typeof value === "symbol" || typeof value === "number" && !Number.isFinite(value) || typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
153
|
+
throw new Error("not JSON content");
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}));
|
|
157
|
+
} catch {
|
|
158
|
+
throw new CanvasError("INVALID", `content source returned invalid JSON for board "${id}"`);
|
|
159
|
+
}
|
|
160
|
+
out.boards.push({ id, content: normalized });
|
|
161
|
+
} else if (owned.has(id)) out.boards.push(owned.get(id));
|
|
162
|
+
}
|
|
163
|
+
for (const { id, data } of await invitesOf(store, subject, resourceType)) {
|
|
139
164
|
const { resourceId, role, expiresAt, acceptedAt, revokedAt } = data;
|
|
140
165
|
out.invites.push(...plain([{ id, resourceId, role, expiresAt, acceptedAt, revokedAt }]));
|
|
141
166
|
}
|
|
142
|
-
out.audit.push(...plain((await auditOf(store, subject)).map((r) => r.data)));
|
|
167
|
+
out.audit.push(...plain((await auditOf(store, subject, resourceType)).map((r) => r.data)));
|
|
143
168
|
return out;
|
|
144
169
|
},
|
|
145
170
|
async eraseAccount(subject, deps) {
|
|
146
171
|
check(subject, deps);
|
|
147
172
|
const { store } = deps;
|
|
173
|
+
const resourceType = deps.resourceType ?? RESOURCE_TYPE;
|
|
174
|
+
const collection = deps.collection ?? "boards";
|
|
148
175
|
const sharing = createSharing(store);
|
|
149
176
|
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: [`${
|
|
151
|
-
const mine = await held(store, subject);
|
|
177
|
+
const ctx = { principalId: subject.id, kind: subject.kind, chain: [{ id: subject.id, kind: subject.kind }], scopes: [`${resourceType}:admin`] };
|
|
178
|
+
const mine = await held(store, subject, resourceType);
|
|
152
179
|
const stamped = [];
|
|
153
|
-
for await (const { id, board } of boards(store, deps
|
|
180
|
+
for await (const { id, board } of boards(store, deps)) {
|
|
154
181
|
if (mine.sole.has(id)) continue;
|
|
155
182
|
const found = board.stampedBy(subject);
|
|
156
183
|
if (found.authored.length > 0 || found.edited.length > 0 || found.titleBy) stamped.push(id);
|
|
@@ -159,18 +186,22 @@ var canvasAccount = {
|
|
|
159
186
|
if ((await rows(store, "resources", { parentId: id })).length > 0) throw new CanvasError("INVALID", `board "${id}" has child resources: remove them first`);
|
|
160
187
|
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
188
|
}
|
|
162
|
-
for (const { id, data } of await auditOf(store, subject)) {
|
|
189
|
+
for (const { id, data } of await auditOf(store, subject, resourceType)) {
|
|
163
190
|
await store.put("audit", id, scrub(data, subject));
|
|
164
191
|
total.audit += 1;
|
|
165
192
|
}
|
|
166
|
-
for (const { id } of await invitesOf(store, subject)) {
|
|
193
|
+
for (const { id } of await invitesOf(store, subject, resourceType)) {
|
|
167
194
|
await store.delete("invites", id);
|
|
168
195
|
total.invites += 1;
|
|
169
196
|
}
|
|
170
197
|
for (const id of mine.sole) {
|
|
171
|
-
await deps.
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
if (deps.contentSource) await deps.contentSource.eraseContent(id);
|
|
199
|
+
else {
|
|
200
|
+
await deps.eraseRoom?.(id);
|
|
201
|
+
const row = await store.get(collection, id);
|
|
202
|
+
if (row) await store.delete(collection, id);
|
|
203
|
+
await deps.eraseRoom?.(id);
|
|
204
|
+
}
|
|
174
205
|
await sharing.deleteResource(ctx, id).catch((e) => {
|
|
175
206
|
if (e instanceof Error && (e.name === "EraseRefusedError" || e.message === "forbidden")) throw new CanvasError("INVALID", e.message);
|
|
176
207
|
throw e;
|
|
@@ -179,7 +210,7 @@ var canvasAccount = {
|
|
|
179
210
|
}
|
|
180
211
|
for (const id of stamped) {
|
|
181
212
|
const done = await store.transaction(async (tx) => {
|
|
182
|
-
const doc = await tx.get(
|
|
213
|
+
const doc = await tx.get(collection, id);
|
|
183
214
|
if (!doc) return null;
|
|
184
215
|
let board;
|
|
185
216
|
try {
|
|
@@ -188,7 +219,7 @@ var canvasAccount = {
|
|
|
188
219
|
throw new CanvasError("INVALID", `stored board "${id}" is damaged`);
|
|
189
220
|
}
|
|
190
221
|
const counts = board.eraseStamps(subject, { kind: subject.kind, id: ERASED });
|
|
191
|
-
await tx.put(
|
|
222
|
+
await tx.put(collection, id, { ...doc, update: toBase64(board.encode()) });
|
|
192
223
|
return counts;
|
|
193
224
|
});
|
|
194
225
|
await deps.eraseRoom?.(id);
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-canvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "A shared whiteboard for diagrams, drawn by people and agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"whiteboard",
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
"dependencies": {
|
|
88
88
|
"@dagrejs/dagre": "^3.1.3",
|
|
89
89
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
90
|
-
"agentic-design": ">=0.8.0 <0.
|
|
91
|
-
"agentic-identity": ">=0.9.0 <0.
|
|
92
|
-
"agentic-realtime": ">=0.4.4 <0.
|
|
90
|
+
"agentic-design": ">=0.8.0 <0.11.0",
|
|
91
|
+
"agentic-identity": ">=0.9.0 <0.12.0",
|
|
92
|
+
"agentic-realtime": ">=0.4.4 <0.7.0",
|
|
93
93
|
"yjs": "^13.6.0",
|
|
94
94
|
"zod": "^4.6.7"
|
|
95
95
|
},
|
|
@@ -127,7 +127,10 @@
|
|
|
127
127
|
"typescript": "^5.6.0",
|
|
128
128
|
"vite": "^7.3.7",
|
|
129
129
|
"vitest": "^3.0.0",
|
|
130
|
-
"wrangler": "^4.155.0"
|
|
130
|
+
"wrangler": "^4.155.0",
|
|
131
|
+
"agentic-design-current": "npm:agentic-design@0.10.1",
|
|
132
|
+
"agentic-identity-current": "npm:agentic-identity@0.11.0",
|
|
133
|
+
"agentic-realtime-current": "npm:agentic-realtime@0.6.0"
|
|
131
134
|
},
|
|
132
135
|
"overrides": {
|
|
133
136
|
"zod": "$zod"
|