agentic-canvas 0.3.0 → 0.5.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 +206 -12
- package/dist/{chunk-W7NRP3EX.js → chunk-G4YTESPS.js} +44 -11
- package/dist/{chunk-GGQPE65T.js → chunk-JCYNPYYJ.js} +96 -40
- package/dist/chunk-SPP6PINW.js +10 -0
- package/dist/{chunk-V477KSZQ.js → chunk-V74PWJKY.js} +186 -9
- package/dist/{chunk-AGODNBN7.js → chunk-VHHJIBAB.js} +33 -18
- package/dist/cli.js +15 -4
- package/dist/client.d.ts +13 -2
- package/dist/client.js +2 -0
- package/dist/embed.d.ts +46 -0
- package/dist/embed.js +535 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +9 -7
- package/dist/live-worker.d.ts +2 -2
- package/dist/live-worker.js +5 -4
- package/dist/live.d.ts +7 -2
- package/dist/live.js +15 -12
- package/dist/{read-BY6f-mmy.d.ts → read-BupIJSPJ.d.ts} +13 -4
- package/dist/{service-Cpl6wKeY.d.ts → service-CR4-r1yx.d.ts} +24 -5
- package/dist/ui.css +38 -0
- package/dist/worker.d.ts +5 -3
- package/dist/worker.js +6 -3
- package/llms.txt +5 -2
- package/package.json +34 -8
package/README.md
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
A shared whiteboard for diagrams, drawn by people and agents.
|
|
9
9
|
|
|
10
|
-
> Status: 0.
|
|
10
|
+
> Status: 0.5.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
|
-
> and the SDK can join a live room. A remote client calls the Worker API.
|
|
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
|
+
> The package exports `agentic-canvas/ui`, a board view you can mount in your own app, new in 0.5.0.
|
|
13
14
|
|
|
14
15
|
## Install
|
|
15
16
|
|
|
@@ -37,7 +38,7 @@ npx agentic-canvas export demo --out demo.svg
|
|
|
37
38
|
npx agentic-canvas export demo --format png --out demo.png
|
|
38
39
|
```
|
|
39
40
|
|
|
40
|
-
Other commands: `add`, `connect`, `move`, `rm`. Run `npx agentic-canvas --help`.
|
|
41
|
+
Other commands: `list`, `add`, `connect`, `update`, `move`, `rm`. Run `npx agentic-canvas --help`.
|
|
41
42
|
`repair` is for the owner only. It needs `CANVAS_ACTOR=human:<id>`.
|
|
42
43
|
Boards live in `./.agentic-canvas` (or `--dir`, or `CANVAS_DIR`). Every write
|
|
43
44
|
needs an actor in `CANVAS_ACTOR`, as `<kind>:<id>`. The operator sets it. A write
|
|
@@ -51,7 +52,7 @@ CANVAS_ACTOR=agent:helper npx agentic-canvas --dir ./boards mcp
|
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
The server speaks MCP on stdio. It has one tool for each CLI command: `create_board`,
|
|
54
|
-
`read_board`, `add_shape`, `connect`, `move`, `remove`, `draw_flow`, `export_svg`, and
|
|
55
|
+
`list_boards`, `read_board`, `add_shape`, `connect`, `update_element`, `move`, `remove`, `draw_flow`, `export_svg`, and
|
|
55
56
|
`export_png`. `repair` is not a tool. Only a human operator runs it, from the CLI. The server will not start unless `CANVAS_ACTOR` is set to a valid
|
|
56
57
|
`<kind>:<id>`. That actor stamps every write. No tool takes an actor, author, or kind, so a
|
|
57
58
|
caller cannot choose who it writes as. An error comes back as a tool error with a code.
|
|
@@ -104,13 +105,122 @@ try {
|
|
|
104
105
|
}
|
|
105
106
|
```
|
|
106
107
|
|
|
107
|
-
There is one method per route: `me`, `createBoard`, `readBoard`, `exportSvg`, `addShape`, `addConnector`,
|
|
108
|
-
`drawFlow`, `move`, `remove`, `uploadImage`, `getImage`, `state`, and `checkpoint`. Each takes
|
|
108
|
+
There is one method per route: `me`, `listBoards`, `createBoard`, `readBoard`, `exportSvg`, `addShape`, `addConnector`,
|
|
109
|
+
`drawFlow`, `update`, `move`, `remove`, `uploadImage`, `getImage`, `state`, and `checkpoint`. Each takes
|
|
109
110
|
`{ signal }` last, so an `AbortSignal` can stop a call. The token sets the actor. A failed call throws
|
|
110
111
|
`CanvasApiError` with the API `code` and the HTTP `status`. A call that gets no reply has code `NETWORK`
|
|
111
112
|
and status 0. The client never retries. A 503 means "try again", and the caller decides, because a
|
|
112
113
|
repeated write can change a board twice. Pass `fetch` to use your own.
|
|
113
114
|
|
|
115
|
+
## Live demo
|
|
116
|
+
|
|
117
|
+
A demo Worker runs on workers.dev: <https://agentic-canvas-demo.hello-fd1.workers.dev>. `GET /health` answers 200
|
|
118
|
+
and the web UI is at the root. The API needs a token, so a request without one gets 401. The demo's D1
|
|
119
|
+
database and R2 bucket are in the EU, and its live rooms use the EU jurisdiction. The demo data can be
|
|
120
|
+
reset at any time.
|
|
121
|
+
|
|
122
|
+
To redeploy it, a maintainer with a Cloudflare token runs this from the repo root. The deploy is manual and
|
|
123
|
+
no CI secret is used:
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
npm ci && npm run build
|
|
127
|
+
WRANGLER_SEND_METRICS=false CLOUDFLARE_API_TOKEN=... npx wrangler deploy -c wrangler.demo.jsonc
|
|
128
|
+
npx wrangler secret put CANVAS_API_TOKENS -c wrangler.demo.jsonc # only to set or rotate tokens
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`wrangler.demo.jsonc` holds the config. The tokens are a Worker secret and are not in the repo. To run your
|
|
132
|
+
own copy, create a D1 database and an R2 bucket (both in the EU), put their names and id in a copy of that
|
|
133
|
+
file, and deploy it the same way.
|
|
134
|
+
|
|
135
|
+
## UI
|
|
136
|
+
|
|
137
|
+
The Worker serves a small web UI at its root. It uses the published `agentic-design` package for
|
|
138
|
+
tokens, controls, and overlays, and the typed `agentic-canvas/client` for every call. It is
|
|
139
|
+
bundled at build time, so a build needs `agentic-design` from npm and the browser loads nothing
|
|
140
|
+
from another site.
|
|
141
|
+
|
|
142
|
+
Run it on your machine:
|
|
143
|
+
|
|
144
|
+
```sh
|
|
145
|
+
npm ci
|
|
146
|
+
npm run build # writes the UI to ui-dist/
|
|
147
|
+
cat > .dev.vars <<'EOF2'
|
|
148
|
+
CANVAS_REGION=unknown
|
|
149
|
+
CANVAS_API_TOKENS={"ada-token-0123456789-0123456789-0123456789":{"actor":"ada","kind":"human"}}
|
|
150
|
+
EOF2
|
|
151
|
+
WRANGLER_SEND_METRICS=false npx wrangler dev
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Open the address that wrangler prints and paste the token. A token has 32 characters or more. Its
|
|
155
|
+
grant sets the name, the kind (a person or an agent), and the role. A `viewer` token reads and
|
|
156
|
+
cannot change a board. The token stays in the tab's session storage.
|
|
157
|
+
|
|
158
|
+
What it does:
|
|
159
|
+
|
|
160
|
+
- Lists boards, makes a board, and opens one. Empty, loading, and error states have a retry.
|
|
161
|
+
- Draws shapes, connectors, text, and sticky notes. Select, move, resize, connect, and delete an
|
|
162
|
+
item. Pan by dragging the background, zoom with the wheel or the toolbar.
|
|
163
|
+
- Marks an item an agent wrote with an "Agent" tag and in its accessible name. The side panel shows
|
|
164
|
+
who made the item and who last edited it.
|
|
165
|
+
- Shows who is in the board, from the live room (`agentic-realtime`). Edits go over the HTTP API.
|
|
166
|
+
After a save, the editor raises a counter in its room presence, and the other editors read the
|
|
167
|
+
board again. The page also reads the board again every 5 seconds, so a change made through the API, the CLI, or an agent shows without a reload, a few seconds late.
|
|
168
|
+
- Works with a keyboard. Tab moves between items. Arrow keys move the selected item, Shift with an
|
|
169
|
+
arrow resizes it, Delete removes it, Escape clears the selection. With nothing selected, arrow
|
|
170
|
+
keys pan, `+` and `-` zoom, and `0` resets the view. The side panel also sets text, position, and size,
|
|
171
|
+
and connects shapes, with fields, and the toolbar has pan buttons. A person who cannot drag
|
|
172
|
+
can do everything a drag does.
|
|
173
|
+
|
|
174
|
+
### Mount the board in your app
|
|
175
|
+
|
|
176
|
+
`agentic-canvas/ui` renders the same board UI into any element. It needs no framework in your app.
|
|
177
|
+
React and react-dom (version 19 or later) are peer dependencies. The Worker's own page uses this code.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import "agentic-design/styles.css"; // tokens and fonts
|
|
181
|
+
import "agentic-canvas/ui.css"; // the board styles
|
|
182
|
+
import { createClient } from "agentic-canvas/client";
|
|
183
|
+
import { mount } from "agentic-canvas/ui";
|
|
184
|
+
|
|
185
|
+
const baseUrl = "https://canvas.example.com/api/v1/canvas";
|
|
186
|
+
const token = "...";
|
|
187
|
+
const board = mount(document.getElementById("board")!, {
|
|
188
|
+
boardId: "b1",
|
|
189
|
+
client: createClient({ baseUrl, token }),
|
|
190
|
+
principal: { kind: "human", name: "Ada", role: "editor" },
|
|
191
|
+
readOnly: false,
|
|
192
|
+
room: { baseUrl, token }, // optional: shows who else is in the board
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
board.update({ /* same options, for another board */ });
|
|
196
|
+
board.unmount(); // leaves the room and stops the refresh timer
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The principal needs a known `kind` and a `name`. `mount` throws a `TypeError` without them. Only the
|
|
200
|
+
role `editor` can change the board, and `readOnly: true` opens it read only for an editor too. The
|
|
201
|
+
server still checks every write against the token. The host page owns the theme. `ui.css` uses CSS nesting under `.cv-root`, so it does not change the rest of your page. It needs a browser from 2023 or later.
|
|
202
|
+
For a React app, `BoardEditor` is the same component without a root. Wrap it in the providers of
|
|
203
|
+
`agentic-design/react`.
|
|
204
|
+
|
|
205
|
+
Develop and test the UI:
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
npm run build # UI and library
|
|
209
|
+
npx playwright-core install chromium
|
|
210
|
+
npm run test:e2e # Playwright against `wrangler dev`, with axe (WCAG 2.2 AA)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The E2E run creates a board, adds a shape and a note, moves them, reloads, and checks each view
|
|
214
|
+
with axe in the light and the dark theme. It saves screenshots to `test-results/screens/`. The committed set in `docs/ui/` shows the UI at this release. Run `UPDATE_SCREENSHOTS=1 npm run test:e2e` to refresh it.
|
|
215
|
+
|
|
216
|
+
A browser cannot set a header on a WebSocket, so the UI sends its token to the live room as a WebSocket sub-protocol (`Sec-WebSocket-Protocol`). A proxy that logs request headers can record it. The API itself takes the token only in an `Authorization` header, and the sub-protocol only on a WebSocket upgrade.
|
|
217
|
+
|
|
218
|
+
The UI saves each edit in order, and shows "Saving…" until the last one is stored. It asks the browser to confirm before you leave a page with unsaved edits. It reads the board again at most once a second.
|
|
219
|
+
|
|
220
|
+
UI limits: the UI draws with the theme colors and ignores the `fill` and `stroke` of an element.
|
|
221
|
+
It shows no image pixels. A text field of a shape is plain text. Two editors who move one item
|
|
222
|
+
at the same time keep the last write. The board list shows the first 100 boards.
|
|
223
|
+
|
|
114
224
|
## Features
|
|
115
225
|
|
|
116
226
|
- Shapes: rectangle, ellipse, diamond, text, sticky note, frame, image.
|
|
@@ -132,23 +242,107 @@ repeated write can change a board twice. Pass `fetch` to use your own.
|
|
|
132
242
|
(EU jurisdiction), through the shared `Store` and `Blobs` ports. Set `CANVAS_API_TOKENS`
|
|
133
243
|
(a JSON map of token to `{ actor, kind }`, tokens of 32 characters or more) and `CANVAS_REGION`.
|
|
134
244
|
Routes: `POST /boards`, `GET /boards/:id`, `GET /boards/:id/svg`, `POST /boards/:id/shapes`,
|
|
135
|
-
`/connectors`, `/flow`, `/move`, `DELETE /boards/:id/elements/:element`, `POST /images`,
|
|
245
|
+
`/connectors`, `/flow`, `/move`, `GET /boards`, `PATCH /boards/:id/elements/:element`, `DELETE /boards/:id/elements/:element`, `POST /images`,
|
|
136
246
|
`GET /images/:id`. An image is stored under the SHA-256 of its bytes, and the reply gives
|
|
137
247
|
the `blob:` ref for a shape. A busy database or bucket answers 503: try again.
|
|
138
248
|
- Live rooms (`agentic-canvas/live-worker`, on `agentic-realtime`). One Durable Object holds one
|
|
139
249
|
board. A token may carry `"role": "viewer"`: a viewer reads and cannot write. `GET /boards/:id/ws`
|
|
140
|
-
opens a room, `GET /me` says who a token is
|
|
141
|
-
|
|
142
|
-
|
|
250
|
+
opens a room, `GET /me` says who a token is. The Worker seeds a room from the stored board when a client
|
|
251
|
+
joins, and saves the room to the stored board before it serves any board read or write. A raw WebSocket
|
|
252
|
+
client needs no seed and no save call. A room that no request reaches stays unsaved in the Store until the
|
|
253
|
+
next request, and it is safe in the room's own storage. `GET /boards/:id/state` and
|
|
254
|
+
`POST /boards/:id/checkpoint` still work for a client that saves by hand. Every save checks author stamps,
|
|
255
|
+
and the Worker sets them from the room's log of who sent each update (the token's actor and kind): a client
|
|
256
|
+
cannot claim another principal, and an agent cannot pass as a person, even by naming one who is in the room.
|
|
257
|
+
A new item that the log lacks (it keeps a capped number of ranges, and a hand save may hold content the room never had) has no sender: the save deletes a new element and undoes a change. A hand save puts the part of its update that the room lacks into the room as the poster, so the room refuses a client id that it holds for someone else and its log names the poster. It drops the part the room holds, saves the room, and fails if the room cannot be saved. It is one message of 1,000,000 bytes at most. A stored board over 1,000,000 bytes cannot seed a room: a live join answers 422 for it.
|
|
143
258
|
- The SDK joins a room: `import { LiveBoard, LiveService } from "agentic-canvas/live"`. The CLI and the MCP
|
|
144
259
|
server join when `CANVAS_SERVER` and `CANVAS_TOKEN` are set. The token sets the actor, so
|
|
145
260
|
`CANVAS_ACTOR` is not needed. `--dir`, `CANVAS_DIR`, and `repair` stay local.
|
|
146
261
|
- Yjs underneath: two copies of a board merge without conflicts.
|
|
147
|
-
-
|
|
262
|
+
- The board model has no UI code. It is plain TypeScript on Yjs, with no outbound calls.
|
|
263
|
+
|
|
264
|
+
## Exports
|
|
265
|
+
|
|
266
|
+
Every name that each entry point exports is listed here. A name marked (type) exists only at compile time.
|
|
267
|
+
|
|
268
|
+
### `agentic-canvas`
|
|
269
|
+
|
|
270
|
+
| Name | What it is |
|
|
271
|
+
|---|---|
|
|
272
|
+
| `VERSION`, `BRAND` | The package version. `BRAND` holds the command name, the description and the version that the CLI and the MCP server show. |
|
|
273
|
+
| `Board`, `CanvasElement` (type), `Shape` (type), `Connector` (type), `Group` (type), `Endpoint` (type), `ShapeType` (type) | The board model. `Board` holds the elements. Each element records its `author`, which never changes, and its `editedBy`. |
|
|
274
|
+
| `SHAPE_TYPES` | The shape kinds: `rect`, `ellipse`, `diamond`, `text`, `sticky`, `frame` and `image`. |
|
|
275
|
+
| `MAX_ELEMENTS`, `MAX_TEXT` | The limits: 10,000 elements on a board, and 10,000 UTF-16 code units in one text. |
|
|
276
|
+
| `CanvasError`, `ErrorCode` (type) | The error that the board throws, with a `code` such as `INVALID`, `NOT_FOUND` or `LIMIT`. |
|
|
277
|
+
| `parseFlow(source)`, `layoutFlow(graph)`, `drawFlow(board, source, actor, options?)` | `parseFlow` reads the flow text. `layoutFlow` returns placements with the top-left at (0, 0). `drawFlow` draws the flow onto the board as the actor. |
|
|
278
|
+
| `MAX_FLOW_NODES`, `MAX_FLOW_EDGES` | The flow limits: 500 nodes and 2000 edges. |
|
|
279
|
+
| `FlowGraph` (type), `FlowNode` (type), `FlowEdge` (type), `Placement` (type), `DrawOptions` (type) | Types for a flow and its layout. |
|
|
280
|
+
| `readBoard`, `BoardData` (type), `NodeData` (type), `EdgeData` (type) | Read a board as plain data. |
|
|
281
|
+
| `toSvg(board)`, `svgToPng(svg)` | `toSvg` returns the board as SVG. `svgToPng` turns an SVG string into PNG through Chromium. |
|
|
282
|
+
| `PRINCIPAL_KINDS`, `PrincipalKind` (type), `Principal` (type), `principalSchema`, `isMachineMade` | The principal kinds, the schema of a principal, and the rule that marks machine-made kinds. |
|
|
283
|
+
| `operatorActor(env?)`, `ActorConfigError` | Read the actor from `CANVAS_ACTOR` as `<kind>:<id>`. It throws `ActorConfigError` when the value is missing or bad. |
|
|
284
|
+
| `CanvasService` | The board operations that the CLI, the MCP server and the Worker share. It takes a `Store`. |
|
|
285
|
+
| `createMcpServer(service, actor)`, `runMcp(service, actor)` | Make the MCP server, or run it on stdio. |
|
|
286
|
+
| `createFileStore`, `openLocalStore`, `FileStoreOptions` (type) | Stores on one local file. See `agentic-canvas/file-store`. |
|
|
287
|
+
|
|
288
|
+
### `agentic-canvas/client`
|
|
289
|
+
|
|
290
|
+
| Name | What it is |
|
|
291
|
+
|---|---|
|
|
292
|
+
| `createClient(options)`, `CanvasApiError` | The remote client. See Remote client above. |
|
|
293
|
+
| `ClientOptions` (type), `CallOptions` (type), `CanvasClient` (type), `ShapeInput` (type), `ConnectorInput` (type), `ShapePatch` (type), `ConnectorPatch` (type), `ImageInfo` (type), `ApiErrorCode` (type) | Types for the client. `ApiErrorCode` adds the client's own codes, such as `NETWORK`, to `ErrorCode`. |
|
|
294
|
+
|
|
295
|
+
### `agentic-canvas/file-store`
|
|
296
|
+
|
|
297
|
+
| Name | What it is |
|
|
298
|
+
|---|---|
|
|
299
|
+
| `createFileStore(path, options?)`, `openLocalStore(dir)` | A store on one JSON file, or on `canvas.json` in a directory. |
|
|
300
|
+
| `FileStoreError` | The error that the store throws. Its `code` is `LOCKED` or `DAMAGED`. |
|
|
301
|
+
| `FileStoreOptions` (type) | `lockTimeoutMs` (default 10000) and `staleAfterMs` (default 30000), plus the adapter options. |
|
|
302
|
+
| `STORE_FILE` | The file name that `openLocalStore` uses, `canvas.json`. |
|
|
303
|
+
|
|
304
|
+
### `agentic-canvas/live`
|
|
305
|
+
|
|
306
|
+
| Name | What it is |
|
|
307
|
+
|---|---|
|
|
308
|
+
| `LiveBoard.join(options)` | Join one board's live room. Read the board, change it, `save` to the stored board, then `leave`. |
|
|
309
|
+
| `LiveService` | The CLI and MCP commands, run in a live room. A change joins the room, runs as the token's actor, saves, and leaves. `read` and `exportSvg` join and leave without a save. `create` and `list` call the HTTP API. |
|
|
310
|
+
| `whoAmI(server, token)`, `Me` (type) | Ask the Worker who a token is. It returns the actor and the role. |
|
|
311
|
+
| `LiveOptions` (type) | The server, the board id, the token, and the optional name and reconnect settings. |
|
|
312
|
+
|
|
313
|
+
### `agentic-canvas/ui`
|
|
314
|
+
|
|
315
|
+
| Name | What it is |
|
|
316
|
+
|---|---|
|
|
317
|
+
| `mount(element, options)` | Render the board UI into an element. It returns `update(options)` and `unmount()`. See Mount the board in your app. |
|
|
318
|
+
| `BoardEditor(options)` | The same board UI as a React component, without a root. |
|
|
319
|
+
| `BoardEditorOptions` (type), `BoardPrincipal` (type), `MountedBoard` (type) | Types for the options, the principal, and the object that `mount` returns. |
|
|
320
|
+
| `CanvasClient` (type), `RoomOptions` (type) | The client type, as in `agentic-canvas/client`, and the live room's base URL and token for the `room` option. |
|
|
321
|
+
|
|
322
|
+
The stylesheet is a separate export, `agentic-canvas/ui.css`. Load `agentic-design/styles.css` too, for the tokens.
|
|
323
|
+
|
|
324
|
+
### `agentic-canvas/worker`
|
|
325
|
+
|
|
326
|
+
| Name | What it is |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `createApp(options?)` | Make the Hono app for the Worker. Pass `live` to turn on the live-room socket. |
|
|
329
|
+
| `default` | The Worker, without live rooms. |
|
|
330
|
+
| `authenticate(request, env)` | Read a bearer token from the `Authorization` header, or from a WebSocket sub-protocol. Return the actor and the role, or `null`. |
|
|
331
|
+
| `sniffImage(bytes)` | Find the image type from the first bytes, or return `null`. The client's claim of a type is not trusted. |
|
|
332
|
+
| `WS_PROTOCOL` | The sub-protocol name that carries a token on a WebSocket: `agentic-canvas.bearer`. |
|
|
333
|
+
| `WorkerEnv` (type), `AppOptions` (type), `Role` (type) | Types for the Worker bindings, the app options and the role, `editor` or `viewer`. |
|
|
334
|
+
|
|
335
|
+
### `agentic-canvas/live-worker`
|
|
336
|
+
|
|
337
|
+
| Name | What it is |
|
|
338
|
+
|---|---|
|
|
339
|
+
| `default` | The Worker with live rooms. Bind the `SyncRoom` class as `SYNC_ROOM`. |
|
|
340
|
+
| `SyncRoom` | The Durable Object class of the live rooms, re-exported from `agentic-realtime/worker`. |
|
|
341
|
+
| `LiveEnv` (type) | The Worker bindings, plus `SYNC_ROOM`. |
|
|
148
342
|
|
|
149
343
|
## Limits
|
|
150
344
|
|
|
151
|
-
- Canvas takes principal kinds, regions, and the Store and Blobs ports from `agentic-identity` 0.
|
|
345
|
+
- Canvas takes principal kinds, regions, and the Store and Blobs ports from `agentic-identity` 0.5.
|
|
152
346
|
It does not use the sharing API yet. Its editor and viewer tokens are a separate table in
|
|
153
347
|
`CANVAS_API_TOKENS`. It has no board delete or erase. When it adopts sharing, grants are keyed
|
|
154
348
|
on kind and id, and a delete calls `deleteResource`.
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FileStoreError
|
|
3
|
+
} from "./chunk-NWQED42L.js";
|
|
1
4
|
import {
|
|
2
5
|
Board,
|
|
3
6
|
CanvasError,
|
|
4
7
|
drawFlow,
|
|
5
|
-
principalSchema,
|
|
6
8
|
readBoard,
|
|
7
9
|
toSvg
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JCYNPYYJ.js";
|
|
9
11
|
import {
|
|
10
|
-
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
principalSchema
|
|
13
|
+
} from "./chunk-SPP6PINW.js";
|
|
12
14
|
|
|
13
15
|
// src/service.ts
|
|
14
16
|
import * as Y from "yjs";
|
|
@@ -21,6 +23,7 @@ function toBase64(bytes) {
|
|
|
21
23
|
for (let i = 0; i < bytes.length; i += 8192) text += String.fromCharCode(...bytes.subarray(i, i + 8192));
|
|
22
24
|
return btoa(text);
|
|
23
25
|
}
|
|
26
|
+
var sameBytes = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
|
|
24
27
|
var fromBase64 = (text) => Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
|
|
25
28
|
function boardKey(id) {
|
|
26
29
|
try {
|
|
@@ -51,6 +54,17 @@ var CanvasService = class {
|
|
|
51
54
|
return { id, title: board.title };
|
|
52
55
|
});
|
|
53
56
|
}
|
|
57
|
+
/** The first 100 boards (the store sorts by id), with titles. A board whose stored bytes are damaged shows an empty title: it stays in the list so a person can find and repair it. */
|
|
58
|
+
async list() {
|
|
59
|
+
const page = await this.store.query(COLLECTION, { limit: 100 }).catch(storeFailure);
|
|
60
|
+
return page.items.map(({ id, data }) => {
|
|
61
|
+
try {
|
|
62
|
+
return { id, title: decodeStored(data, id, Board.decode).title };
|
|
63
|
+
} catch {
|
|
64
|
+
return { id, title: "" };
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
54
68
|
/** Throws NOT_FOUND unless the board exists. A live room opens only for a board that exists. */
|
|
55
69
|
async assertExists(id) {
|
|
56
70
|
boardKey(id);
|
|
@@ -76,6 +90,10 @@ var CanvasService = class {
|
|
|
76
90
|
connect(id, connector, actor) {
|
|
77
91
|
return this.change(id, (b) => b.addConnector(connector, actor));
|
|
78
92
|
}
|
|
93
|
+
/** Change fields of one shape or connector: position, size, text, colors. */
|
|
94
|
+
update(id, elementId, patch, actor) {
|
|
95
|
+
return this.change(id, (b) => b.update(elementId, patch, actor));
|
|
96
|
+
}
|
|
79
97
|
move(id, elementId, dx, dy, actor) {
|
|
80
98
|
return this.change(id, (b) => ({ moved: b.move(elementId, dx, dy, actor) }));
|
|
81
99
|
}
|
|
@@ -94,22 +112,37 @@ var CanvasService = class {
|
|
|
94
112
|
boardKey(id);
|
|
95
113
|
return this.store.transaction(async (tx) => (await this.load(tx.get(COLLECTION, id), id)).encode());
|
|
96
114
|
}
|
|
115
|
+
/** What the stored board has that a room with state `room` lacks, as a Yjs update, or null when the room has all of it. */
|
|
116
|
+
async missingFrom(id, room) {
|
|
117
|
+
boardKey(id);
|
|
118
|
+
const known = new Y.Doc();
|
|
119
|
+
if (room) Y.applyUpdate(known, room);
|
|
120
|
+
const diff = await this.store.transaction(async (tx) => Y.encodeStateAsUpdate((await this.load(tx.get(COLLECTION, id), id)).doc, Y.encodeStateVector(known)));
|
|
121
|
+
const before = Y.encodeStateAsUpdate(known);
|
|
122
|
+
Y.applyUpdate(known, diff);
|
|
123
|
+
return Y.decodeUpdate(diff).structs.length === 0 && sameBytes(before, Y.encodeStateAsUpdate(known)) ? null : diff;
|
|
124
|
+
}
|
|
97
125
|
/**
|
|
98
|
-
*
|
|
126
|
+
* `writer` names who sent each Yjs item, from the room's log. It sets the stamps where it knows (see `Board.restamp`).
|
|
127
|
+
* Merge a live room's state into the stored board. `authors` are the principals the room saw send updates. A null
|
|
128
|
+
* `actor` trusts only them: the Worker saves a room that way, with nobody present to vouch.
|
|
99
129
|
* An element stamped by anyone else is deleted, and a changed author is put back (see `Board.restamp`),
|
|
100
130
|
* so a client that writes raw Yjs cannot put another author on its work. Broken entries are repaired and
|
|
101
131
|
* a board past the element cap is refused. Returns the ids the checks fixed.
|
|
102
132
|
*
|
|
103
133
|
* Limits: the save corrects the stored board only. The room keeps what its clients wrote, so a client that sees
|
|
104
134
|
* a non-empty `fixed` must join again, or its later edits are lost at the next save. The room remembers its
|
|
105
|
-
* authors for good, so
|
|
135
|
+
* authors for good, so an OLD item that the log does not name (a seeded one, or a range the log dropped) keeps the older rule: its
|
|
136
|
+
* stamp may name anyone who once sent an update in that room. A NEW item with no writer is deleted or undone.
|
|
137
|
+
* The log records who adds an item, not who deletes a key: a deleted optional key is credited to the writer of the editor
|
|
138
|
+
* stamp written in the same save, or undone if there is none.
|
|
106
139
|
* A client that adds a root type of its own makes every later save of that room fail, because the room keeps it.
|
|
107
140
|
*/
|
|
108
|
-
async checkpoint(id, update, actor, authors = []) {
|
|
141
|
+
async checkpoint(id, update, actor, authors = [], writer) {
|
|
109
142
|
boardKey(id);
|
|
110
|
-
const by = principalSchema.safeParse(actor);
|
|
111
|
-
if (!by.success) throw new CanvasError("INVALID", "the actor must be a valid principal");
|
|
112
|
-
const trusted = (who) => [by.data, ...authors].some((a) => a.kind === who.kind && a.id === who.id);
|
|
143
|
+
const by = actor === null ? null : principalSchema.safeParse(actor);
|
|
144
|
+
if (by && !by.success) throw new CanvasError("INVALID", "the actor must be a valid principal");
|
|
145
|
+
const trusted = (who) => [...by?.success ? [by.data] : [], ...authors].some((a) => a.kind === who.kind && a.id === who.id);
|
|
113
146
|
return this.store.transaction(async (tx) => {
|
|
114
147
|
const prior = await this.load(tx.get(COLLECTION, id), id);
|
|
115
148
|
const next = prior.clone();
|
|
@@ -121,7 +154,7 @@ var CanvasService = class {
|
|
|
121
154
|
if (next.doc.store.pendingStructs || next.doc.store.pendingDs) throw new CanvasError("INVALID", "the update is not complete: it needs changes that the board does not have");
|
|
122
155
|
const strange = [...next.doc.share.keys()].filter((key) => key !== "elements" && key !== "meta");
|
|
123
156
|
if (strange.length > 0) throw new CanvasError("INVALID", "the update holds data that is not part of a board");
|
|
124
|
-
const fixed = /* @__PURE__ */ new Set([...next.restamp(prior, trusted), ...next.repair()]);
|
|
157
|
+
const fixed = /* @__PURE__ */ new Set([...next.restamp(prior, trusted, writer), ...next.repair()]);
|
|
125
158
|
const saved = next.encode();
|
|
126
159
|
if (saved.length > MAX_LIVE_BYTES) throw new CanvasError("LIMIT", `the board would hold over ${MAX_LIVE_BYTES} bytes`);
|
|
127
160
|
await tx.put(COLLECTION, id, { update: toBase64(saved) });
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
isMachineMade,
|
|
3
|
+
principalSchema
|
|
4
|
+
} from "./chunk-SPP6PINW.js";
|
|
5
5
|
|
|
6
6
|
// src/core/board.ts
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import * as Y from "yjs";
|
|
9
|
-
import { z
|
|
9
|
+
import { z } from "zod";
|
|
10
10
|
var CanvasError = class extends Error {
|
|
11
11
|
constructor(code, message) {
|
|
12
12
|
super(message);
|
|
@@ -18,18 +18,18 @@ var CanvasError = class extends Error {
|
|
|
18
18
|
var SHAPE_TYPES = ["rect", "ellipse", "diamond", "text", "sticky", "frame", "image"];
|
|
19
19
|
var MAX_ELEMENTS = 1e4;
|
|
20
20
|
var MAX_TEXT = 1e4;
|
|
21
|
-
var id =
|
|
22
|
-
var coord =
|
|
23
|
-
var size =
|
|
24
|
-
var text =
|
|
21
|
+
var id = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/);
|
|
22
|
+
var coord = z.number().finite().min(-1e7).max(1e7);
|
|
23
|
+
var size = z.number().finite().gt(0).max(1e7);
|
|
24
|
+
var text = z.string().max(MAX_TEXT);
|
|
25
25
|
var COLOR_PATTERN = /^(#[0-9a-fA-F]{3,8}|[a-z]{3,20})$/;
|
|
26
26
|
var IMAGE_PATTERN = /^(data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+=*|blob:[A-Za-z0-9._-]{1,200})$/;
|
|
27
|
-
var color =
|
|
28
|
-
var imageSrc =
|
|
29
|
-
var endpoint =
|
|
30
|
-
var shapeInput =
|
|
27
|
+
var color = z.string().regex(COLOR_PATTERN);
|
|
28
|
+
var imageSrc = z.string().max(2e6).regex(IMAGE_PATTERN);
|
|
29
|
+
var endpoint = z.union([z.object({ id }).strict(), z.object({ x: coord, y: coord }).strict()]);
|
|
30
|
+
var shapeInput = z.object({
|
|
31
31
|
id: id.optional(),
|
|
32
|
-
type:
|
|
32
|
+
type: z.enum(SHAPE_TYPES),
|
|
33
33
|
x: coord,
|
|
34
34
|
y: coord,
|
|
35
35
|
w: size.default(120),
|
|
@@ -41,40 +41,40 @@ var shapeInput = z2.object({
|
|
|
41
41
|
}).strict().refine((s) => s.type === "image" === (s.src !== void 0), {
|
|
42
42
|
message: "src is required for image shapes and allowed on no other type"
|
|
43
43
|
});
|
|
44
|
-
var connectorInput =
|
|
44
|
+
var connectorInput = z.object({
|
|
45
45
|
id: id.optional(),
|
|
46
46
|
from: endpoint,
|
|
47
47
|
to: endpoint,
|
|
48
48
|
text: text.default(""),
|
|
49
|
-
startArrow:
|
|
50
|
-
endArrow:
|
|
49
|
+
startArrow: z.boolean().default(false),
|
|
50
|
+
endArrow: z.boolean().default(true),
|
|
51
51
|
stroke: color.optional()
|
|
52
52
|
}).strict();
|
|
53
|
-
var shapePatch =
|
|
54
|
-
var connectorPatch =
|
|
55
|
-
var delta =
|
|
53
|
+
var shapePatch = z.object({ x: coord, y: coord, w: size, h: size, text, fill: color, stroke: color, src: imageSrc }).partial().strict();
|
|
54
|
+
var connectorPatch = z.object({ from: endpoint, to: endpoint, text, startArrow: z.boolean(), endArrow: z.boolean(), stroke: color }).partial().strict();
|
|
55
|
+
var delta = z.number().finite().min(-2e7).max(2e7);
|
|
56
56
|
var MAX_Z = 1e9;
|
|
57
57
|
var stampFields = {
|
|
58
58
|
id,
|
|
59
|
-
z:
|
|
59
|
+
z: z.number().finite().min(-MAX_Z).max(MAX_Z),
|
|
60
60
|
groupId: id.optional(),
|
|
61
61
|
author: principalSchema,
|
|
62
62
|
editedBy: principalSchema
|
|
63
63
|
};
|
|
64
|
-
var storedShape =
|
|
65
|
-
var storedConnector =
|
|
64
|
+
var storedShape = z.object({ ...stampFields, type: z.enum(SHAPE_TYPES), x: coord, y: coord, w: size, h: size, text, fill: color.optional(), stroke: color.optional(), src: imageSrc.optional() }).refine((s) => s.type === "image" === (s.src !== void 0), { message: "src is required for image shapes and allowed on no other type" });
|
|
65
|
+
var storedConnector = z.object({
|
|
66
66
|
...stampFields,
|
|
67
|
-
type:
|
|
67
|
+
type: z.literal("connector"),
|
|
68
68
|
from: endpoint,
|
|
69
69
|
to: endpoint,
|
|
70
70
|
text,
|
|
71
|
-
startArrow:
|
|
72
|
-
endArrow:
|
|
71
|
+
startArrow: z.boolean(),
|
|
72
|
+
endArrow: z.boolean(),
|
|
73
73
|
stroke: color.optional()
|
|
74
74
|
});
|
|
75
|
-
var storedGroup =
|
|
76
|
-
var storedElement =
|
|
77
|
-
var titleSchema =
|
|
75
|
+
var storedGroup = z.object({ ...stampFields, type: z.literal("group") });
|
|
76
|
+
var storedElement = z.union([storedShape, storedConnector, storedGroup]);
|
|
77
|
+
var titleSchema = z.string().max(200);
|
|
78
78
|
function parse(schema, value) {
|
|
79
79
|
const result = schema.safeParse(value);
|
|
80
80
|
if (!result.success) {
|
|
@@ -133,7 +133,7 @@ var Board = class _Board {
|
|
|
133
133
|
return value.success ? value.data : void 0;
|
|
134
134
|
}
|
|
135
135
|
setTitle(title, actor) {
|
|
136
|
-
const t = parse(
|
|
136
|
+
const t = parse(z.string().max(200), title);
|
|
137
137
|
const by = parse(principalSchema, actor);
|
|
138
138
|
this.doc.transact(() => {
|
|
139
139
|
this.meta.set("title", t);
|
|
@@ -184,12 +184,58 @@ var Board = class _Board {
|
|
|
184
184
|
* author, and a changed one needs a trusted editor or it goes back to its prior state. An editor stamp changed alone
|
|
185
185
|
* to an untrusted principal is put back. Unknown keys, nested Yjs types, and bad or foreign title values are deleted
|
|
186
186
|
* or undone. Returns the ids it deleted or restored.
|
|
187
|
+
*
|
|
188
|
+
* `writer` says who sent the update that holds a Yjs item, or undefined when it does not know. Where it knows, it
|
|
189
|
+
* decides: a new element gets its creator as author and editor, and an old one gets the sender of its changes as editor.
|
|
190
|
+
* A client cannot claim another principal, and an agent cannot pass as a person. A new item that the log lacks has no
|
|
191
|
+
* sender: it is deleted or undone. Only an old item falls back to `trusted`. A hand save names its poster as the sender
|
|
192
|
+
* of every item that the room does not hold, in `writer`.
|
|
187
193
|
*/
|
|
188
|
-
restamp(prior, trusted) {
|
|
194
|
+
restamp(prior, trusted, writer) {
|
|
189
195
|
const before = new Map(prior.allStored().map((e) => [e.id, e]));
|
|
190
196
|
const body = ({ author: _a, editedBy: _e, ...rest }) => JSON.stringify(rest);
|
|
191
197
|
const fixed = [];
|
|
198
|
+
const isNew = (item) => item.clock >= Y.getState(prior.doc.store, item.client);
|
|
199
|
+
const who = (item) => writer?.(item);
|
|
200
|
+
const pick = (writers, stamped) => writers.find((w) => isMachineMade(w.kind)) ?? (stamped && (writers.length === 0 || writers.some((w) => samePrincipal(w, stamped))) ? stamped : writers[0]);
|
|
192
201
|
this.doc.transact(() => {
|
|
202
|
+
for (const el of writer ? this.allStored() : []) {
|
|
203
|
+
const entry = this.elements.get(el.id);
|
|
204
|
+
const was = prior.elements.get(el.id);
|
|
205
|
+
const by2 = (key) => {
|
|
206
|
+
const item = entry._map.get(key);
|
|
207
|
+
return item && !item.deleted ? who(item.id) : void 0;
|
|
208
|
+
};
|
|
209
|
+
if (!was) {
|
|
210
|
+
const creator = entry._item ? who(entry._item.id) : void 0;
|
|
211
|
+
const writers2 = [...entry.keys()].filter((k) => k !== "author" && k !== "editedBy").map(by2);
|
|
212
|
+
if (!creator || writers2.some((w) => !w)) this.elements.delete(el.id);
|
|
213
|
+
else {
|
|
214
|
+
const editor2 = pick(writers2, by2("editedBy")) ?? creator;
|
|
215
|
+
if (samePrincipal(el.author, creator) && samePrincipal(el.editedBy, editor2)) continue;
|
|
216
|
+
entry.set("author", creator);
|
|
217
|
+
entry.set("editedBy", editor2);
|
|
218
|
+
}
|
|
219
|
+
fixed.push(el.id);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
const raw = (map, key) => JSON.stringify(map.get(key));
|
|
223
|
+
const changed = [.../* @__PURE__ */ new Set([...entry.keys(), ...was.keys()])].filter((k) => k !== "author" && k !== "editedBy" && raw(entry, k) !== raw(was, k));
|
|
224
|
+
const found = entry._map.get("editedBy");
|
|
225
|
+
const stamp = found?.deleted ? void 0 : found;
|
|
226
|
+
const stamped = stamp && isNew(stamp.id) ? who(stamp.id) : void 0;
|
|
227
|
+
const writers = changed.map((k) => entry.has(k) ? by2(k) : stamped);
|
|
228
|
+
if (writers.some((w) => !w) || stamp && isNew(stamp.id) && !stamped) {
|
|
229
|
+
this.elements.set(el.id, new Y.Map([...was.entries()]));
|
|
230
|
+
fixed.push(el.id);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const editor = pick(writers, stamped);
|
|
234
|
+
if (editor && !samePrincipal(el.editedBy, editor)) {
|
|
235
|
+
entry.set("editedBy", editor);
|
|
236
|
+
fixed.push(el.id);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
193
239
|
for (const el of this.allStored()) {
|
|
194
240
|
const old = before.get(el.id);
|
|
195
241
|
if (!old) {
|
|
@@ -216,9 +262,22 @@ var Board = class _Board {
|
|
|
216
262
|
if (extra.length > 0 && !fixed.includes(el.id)) fixed.push(el.id);
|
|
217
263
|
}
|
|
218
264
|
const same = (key) => !(this.meta.get(key) instanceof Y.AbstractType) && JSON.stringify(this.meta.get(key)) === JSON.stringify(prior.meta.get(key));
|
|
265
|
+
let titleBroken = false;
|
|
266
|
+
if (writer) {
|
|
267
|
+
const found = this.meta._map.get("title");
|
|
268
|
+
const titleItem = found?.deleted ? void 0 : found;
|
|
269
|
+
const byItem = this.meta._map.get("titleBy");
|
|
270
|
+
const written = [titleItem, byItem?.deleted ? void 0 : byItem].filter((item) => item !== void 0 && isNew(item.id)).map((item) => who(item.id));
|
|
271
|
+
titleBroken = !titleItem && prior.meta.has("title") || written.some((w) => !w);
|
|
272
|
+
const titleWriter = titleBroken ? void 0 : pick(written);
|
|
273
|
+
if (titleWriter && this.titleBy && !samePrincipal(this.titleBy, titleWriter)) {
|
|
274
|
+
this.meta.set("titleBy", titleWriter);
|
|
275
|
+
fixed.push("title");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
219
278
|
const by = this.titleBy;
|
|
220
279
|
const title = this.meta.get("title");
|
|
221
|
-
const honest = by !== void 0 && trusted(by) && (title === void 0 || titleSchema.safeParse(title).success);
|
|
280
|
+
const honest = !titleBroken && by !== void 0 && trusted(by) && (title === void 0 || titleSchema.safeParse(title).success);
|
|
222
281
|
if (!honest && !(same("title") && same("titleBy"))) {
|
|
223
282
|
for (const key of ["title", "titleBy"]) {
|
|
224
283
|
const old = prior.meta.get(key);
|
|
@@ -265,7 +324,7 @@ var Board = class _Board {
|
|
|
265
324
|
/** Shift an element, or a whole group. Returns the ids that moved. Bound connector ends follow their shapes. */
|
|
266
325
|
move(elementId, dx, dy, actor) {
|
|
267
326
|
const by = parse(principalSchema, actor);
|
|
268
|
-
const [mx, my] = parse(
|
|
327
|
+
const [mx, my] = parse(z.tuple([delta, delta]), [dx, dy]);
|
|
269
328
|
this.require(elementId);
|
|
270
329
|
const free = (e) => hasId(e) ? e : { x: e.x + mx, y: e.y + my };
|
|
271
330
|
const plan = [];
|
|
@@ -294,7 +353,7 @@ var Board = class _Board {
|
|
|
294
353
|
}
|
|
295
354
|
group(ids, actor, groupId) {
|
|
296
355
|
const by = parse(principalSchema, actor);
|
|
297
|
-
const members = parse(
|
|
356
|
+
const members = parse(z.array(id).min(2).max(MAX_ELEMENTS), ids);
|
|
298
357
|
const gid = parse(id.optional(), groupId);
|
|
299
358
|
if (new Set(members).size !== members.length) throw new CanvasError("INVALID", "member ids repeat");
|
|
300
359
|
const parents = new Set(members.map((m) => this.require(m).groupId));
|
|
@@ -364,12 +423,12 @@ var Board = class _Board {
|
|
|
364
423
|
}
|
|
365
424
|
/** The z value just past the top or the bottom. Not clamped: the caller checks the range. */
|
|
366
425
|
edgeZ(direction) {
|
|
367
|
-
let
|
|
426
|
+
let z2 = 0;
|
|
368
427
|
for (const m of this.elements.values()) {
|
|
369
428
|
const value = m instanceof Y.Map ? m.get("z") : void 0;
|
|
370
|
-
if (isValidZ(value))
|
|
429
|
+
if (isValidZ(value)) z2 = direction === 1 ? Math.max(z2, value) : Math.min(z2, value);
|
|
371
430
|
}
|
|
372
|
-
return
|
|
431
|
+
return z2 + direction;
|
|
373
432
|
}
|
|
374
433
|
/** The element and, for a group, everything below it. Cycle-safe. */
|
|
375
434
|
subtree(elementId) {
|
|
@@ -705,9 +764,6 @@ function toSvg(board) {
|
|
|
705
764
|
}
|
|
706
765
|
|
|
707
766
|
export {
|
|
708
|
-
PRINCIPAL_KINDS,
|
|
709
|
-
isMachineMade,
|
|
710
|
-
principalSchema,
|
|
711
767
|
CanvasError,
|
|
712
768
|
SHAPE_TYPES,
|
|
713
769
|
MAX_ELEMENTS,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// src/core/principal.ts
|
|
2
|
+
import { PRINCIPAL_KINDS, isMachineMade } from "agentic-identity";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
var principalSchema = z.object({ kind: z.enum(PRINCIPAL_KINDS), id: z.string().min(1).max(200) }).strict();
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
PRINCIPAL_KINDS,
|
|
8
|
+
isMachineMade,
|
|
9
|
+
principalSchema
|
|
10
|
+
};
|