agentic-canvas 0.1.0 → 0.3.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 +57 -4
- package/SKILL.md +3 -0
- package/dist/chunk-AGODNBN7.js +166 -0
- package/dist/{chunk-NSZ2AC5O.js → chunk-GGQPE65T.js} +88 -297
- package/dist/chunk-NWQED42L.js +13 -0
- package/dist/{chunk-I3EJ5JB7.js → chunk-UKST5A57.js} +4 -13
- package/dist/chunk-V477KSZQ.js +2005 -0
- package/dist/chunk-W7NRP3EX.js +164 -0
- package/dist/cli.js +33 -7
- package/dist/client.d.ts +108 -0
- package/dist/client.js +78 -0
- package/dist/index.d.ts +10 -247
- package/dist/index.js +16 -11
- package/dist/live-worker.d.ts +22 -0
- package/dist/live-worker.js +25 -0
- package/dist/live.d.ts +102 -0
- package/dist/live.js +192 -0
- package/dist/read-BY6f-mmy.d.ts +167 -0
- package/dist/service-Cpl6wKeY.d.ts +109 -0
- package/dist/store-file.js +4 -2
- package/dist/worker.d.ts +55 -0
- package/dist/worker.js +15 -0
- package/llms.txt +3 -0
- package/package.json +23 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Board,
|
|
3
|
+
CanvasError,
|
|
4
|
+
drawFlow,
|
|
5
|
+
principalSchema,
|
|
6
|
+
readBoard,
|
|
7
|
+
toSvg
|
|
8
|
+
} from "./chunk-GGQPE65T.js";
|
|
9
|
+
import {
|
|
10
|
+
FileStoreError
|
|
11
|
+
} from "./chunk-NWQED42L.js";
|
|
12
|
+
|
|
13
|
+
// src/service.ts
|
|
14
|
+
import * as Y from "yjs";
|
|
15
|
+
import { assertId, StoreConflictError } from "agentic-identity";
|
|
16
|
+
var COLLECTION = "boards";
|
|
17
|
+
var MAX_LIVE_BYTES = 1572864;
|
|
18
|
+
var BOARD_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
19
|
+
function toBase64(bytes) {
|
|
20
|
+
let text = "";
|
|
21
|
+
for (let i = 0; i < bytes.length; i += 8192) text += String.fromCharCode(...bytes.subarray(i, i + 8192));
|
|
22
|
+
return btoa(text);
|
|
23
|
+
}
|
|
24
|
+
var fromBase64 = (text) => Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
|
|
25
|
+
function boardKey(id) {
|
|
26
|
+
try {
|
|
27
|
+
if (!BOARD_ID.test(id)) throw new Error();
|
|
28
|
+
assertId(id);
|
|
29
|
+
} catch {
|
|
30
|
+
throw new CanvasError("INVALID", "board id must be 1 to 64 of A-Z, a-z, 0-9, _ and -, and not like __x__");
|
|
31
|
+
}
|
|
32
|
+
return id;
|
|
33
|
+
}
|
|
34
|
+
function storeFailure(error) {
|
|
35
|
+
if (error instanceof StoreConflictError) throw new CanvasError("UNAVAILABLE", "the board changed at the same time. Try again");
|
|
36
|
+
if (error instanceof FileStoreError) throw new CanvasError(error.code === "LOCKED" ? "UNAVAILABLE" : "INVALID", error.message);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
var CanvasService = class {
|
|
40
|
+
store;
|
|
41
|
+
constructor(store) {
|
|
42
|
+
this.store = { ...store, transaction: (fn) => store.transaction(fn).catch(storeFailure) };
|
|
43
|
+
}
|
|
44
|
+
async create(actor, options = {}) {
|
|
45
|
+
const id = boardKey(options.id ?? crypto.randomUUID());
|
|
46
|
+
const board = new Board();
|
|
47
|
+
if (options.title !== void 0) board.setTitle(options.title, actor);
|
|
48
|
+
return this.store.transaction(async (tx) => {
|
|
49
|
+
if (await tx.get(COLLECTION, id)) throw new CanvasError("DUPLICATE_ID", `board "${id}" already exists`);
|
|
50
|
+
await tx.put(COLLECTION, id, { update: toBase64(board.encode()) });
|
|
51
|
+
return { id, title: board.title };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Throws NOT_FOUND unless the board exists. A live room opens only for a board that exists. */
|
|
55
|
+
async assertExists(id) {
|
|
56
|
+
boardKey(id);
|
|
57
|
+
if (!await this.store.transaction((tx) => tx.get(COLLECTION, id))) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
|
|
58
|
+
}
|
|
59
|
+
async read(id) {
|
|
60
|
+
boardKey(id);
|
|
61
|
+
return this.store.transaction(async (tx) => ({ id, ...readBoard(await this.load(tx.get(COLLECTION, id), id)) }));
|
|
62
|
+
}
|
|
63
|
+
/** Run `change` on the board and save it, all inside one transaction. */
|
|
64
|
+
async change(id, change) {
|
|
65
|
+
boardKey(id);
|
|
66
|
+
return this.store.transaction(async (tx) => {
|
|
67
|
+
const board = await this.load(tx.get(COLLECTION, id), id);
|
|
68
|
+
const result = change(board);
|
|
69
|
+
await tx.put(COLLECTION, id, { update: toBase64(board.encode()) });
|
|
70
|
+
return result;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
add(id, shape, actor) {
|
|
74
|
+
return this.change(id, (b) => b.addShape(shape, actor));
|
|
75
|
+
}
|
|
76
|
+
connect(id, connector, actor) {
|
|
77
|
+
return this.change(id, (b) => b.addConnector(connector, actor));
|
|
78
|
+
}
|
|
79
|
+
move(id, elementId, dx, dy, actor) {
|
|
80
|
+
return this.change(id, (b) => ({ moved: b.move(elementId, dx, dy, actor) }));
|
|
81
|
+
}
|
|
82
|
+
remove(id, elementId) {
|
|
83
|
+
return this.change(id, (b) => ({ removed: b.remove(elementId) }));
|
|
84
|
+
}
|
|
85
|
+
flow(id, text, actor, options) {
|
|
86
|
+
return this.change(id, (b) => drawFlow(b, text, actor, options));
|
|
87
|
+
}
|
|
88
|
+
async exportSvg(id) {
|
|
89
|
+
boardKey(id);
|
|
90
|
+
return this.store.transaction(async (tx) => toSvg(await this.load(tx.get(COLLECTION, id), id)));
|
|
91
|
+
}
|
|
92
|
+
/** The stored board as a Yjs update. A client applies it to a room to seed it: the room does not read it. */
|
|
93
|
+
async state(id) {
|
|
94
|
+
boardKey(id);
|
|
95
|
+
return this.store.transaction(async (tx) => (await this.load(tx.get(COLLECTION, id), id)).encode());
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Merge a live room's state into the stored board. `authors` are the principals the room saw send updates.
|
|
99
|
+
* An element stamped by anyone else is deleted, and a changed author is put back (see `Board.restamp`),
|
|
100
|
+
* so a client that writes raw Yjs cannot put another author on its work. Broken entries are repaired and
|
|
101
|
+
* a board past the element cap is refused. Returns the ids the checks fixed.
|
|
102
|
+
*
|
|
103
|
+
* Limits: the save corrects the stored board only. The room keeps what its clients wrote, so a client that sees
|
|
104
|
+
* 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 a client may stamp an element with the name of anyone who once sent an update in that room.
|
|
106
|
+
* A client that adds a root type of its own makes every later save of that room fail, because the room keeps it.
|
|
107
|
+
*/
|
|
108
|
+
async checkpoint(id, update, actor, authors = []) {
|
|
109
|
+
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);
|
|
113
|
+
return this.store.transaction(async (tx) => {
|
|
114
|
+
const prior = await this.load(tx.get(COLLECTION, id), id);
|
|
115
|
+
const next = prior.clone();
|
|
116
|
+
try {
|
|
117
|
+
Y.applyUpdate(next.doc, update);
|
|
118
|
+
} catch {
|
|
119
|
+
throw new CanvasError("INVALID", "the update is not a Yjs update");
|
|
120
|
+
}
|
|
121
|
+
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
|
+
const strange = [...next.doc.share.keys()].filter((key) => key !== "elements" && key !== "meta");
|
|
123
|
+
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()]);
|
|
125
|
+
const saved = next.encode();
|
|
126
|
+
if (saved.length > MAX_LIVE_BYTES) throw new CanvasError("LIMIT", `the board would hold over ${MAX_LIVE_BYTES} bytes`);
|
|
127
|
+
await tx.put(COLLECTION, id, { update: toBase64(saved) });
|
|
128
|
+
return { elements: next.list().length, fixed: [...fixed].sort() };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Owner only. Repair a board and delete the named elements. Only a human actor may call it. */
|
|
132
|
+
async repair(id, actor, drop = []) {
|
|
133
|
+
const by = principalSchema.safeParse(actor);
|
|
134
|
+
if (!by.success) throw new CanvasError("INVALID", "the actor must be a valid principal");
|
|
135
|
+
if (by.data.kind !== "human") throw new CanvasError("FORBIDDEN", "only a human owner can repair a board");
|
|
136
|
+
boardKey(id);
|
|
137
|
+
return this.store.transaction(async (tx) => {
|
|
138
|
+
const doc = await tx.get(COLLECTION, id);
|
|
139
|
+
if (!doc) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
|
|
140
|
+
const board = decodeStored(doc, id, Board.open);
|
|
141
|
+
const fixed = board.repair(drop);
|
|
142
|
+
await tx.put(COLLECTION, id, { update: toBase64(board.encode()) });
|
|
143
|
+
return { id, fixed, elements: board.list().length };
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
async load(read, id) {
|
|
147
|
+
const doc = await read;
|
|
148
|
+
if (!doc) throw new CanvasError("NOT_FOUND", `no board "${id}"`);
|
|
149
|
+
return decodeStored(doc, id, Board.decode);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
function decodeStored(doc, id, decode) {
|
|
153
|
+
try {
|
|
154
|
+
return decode(fromBase64(String(doc.update)));
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof CanvasError) throw error;
|
|
157
|
+
throw new CanvasError("INVALID", `stored board "${id}" is damaged`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export {
|
|
162
|
+
MAX_LIVE_BYTES,
|
|
163
|
+
CanvasService
|
|
164
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -2,14 +2,20 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ActorConfigError,
|
|
4
4
|
BRAND,
|
|
5
|
-
CanvasError,
|
|
6
|
-
CanvasService,
|
|
7
5
|
operatorActor,
|
|
8
|
-
runMcp
|
|
9
|
-
|
|
6
|
+
runMcp,
|
|
7
|
+
svgToPng
|
|
8
|
+
} from "./chunk-AGODNBN7.js";
|
|
10
9
|
import {
|
|
11
10
|
openLocalStore
|
|
12
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-UKST5A57.js";
|
|
12
|
+
import {
|
|
13
|
+
CanvasService
|
|
14
|
+
} from "./chunk-W7NRP3EX.js";
|
|
15
|
+
import {
|
|
16
|
+
CanvasError
|
|
17
|
+
} from "./chunk-GGQPE65T.js";
|
|
18
|
+
import "./chunk-NWQED42L.js";
|
|
13
19
|
|
|
14
20
|
// src/cli.ts
|
|
15
21
|
import { readFileSync, writeFileSync } from "fs";
|
|
@@ -33,6 +39,10 @@ Commands:
|
|
|
33
39
|
repair <board> [--drop <element>]... Owner only: fix a broken or over-cap board. Needs CANVAS_ACTOR=human:<id>.
|
|
34
40
|
mcp Serve these as MCP tools on stdio. Needs CANVAS_ACTOR.
|
|
35
41
|
|
|
42
|
+
Live mode: set CANVAS_SERVER=<worker url> and CANVAS_TOKEN=<bearer token>. Every command then runs in the live room of
|
|
43
|
+
the board on that server, so a person and an agent can edit one board at once. The token sets the actor, the kind,
|
|
44
|
+
and the role, so CANVAS_ACTOR is not needed. If it is set, it must match the token. --dir, CANVAS_DIR, and repair are local only.
|
|
45
|
+
|
|
36
46
|
Write commands need an actor in CANVAS_ACTOR as <kind>:<id>, such as "agent:helper".
|
|
37
47
|
The operator sets it. A caller cannot pick its own kind: there is no --actor flag.
|
|
38
48
|
Kinds: human, agent_of_human, agent_of_agent, agent, user_token.
|
|
@@ -78,7 +88,24 @@ async function run(argv) {
|
|
|
78
88
|
const need = (count, shape) => {
|
|
79
89
|
if (positionals.length !== count + 1) throw new UsageError(`usage: ${BRAND.command} ${command} ${shape}`);
|
|
80
90
|
};
|
|
91
|
+
const server = process.env.CANVAS_SERVER;
|
|
92
|
+
let live;
|
|
93
|
+
let service;
|
|
94
|
+
if (server) {
|
|
95
|
+
if (v.dir !== void 0 || process.env.CANVAS_DIR) throw new UsageError("--dir and CANVAS_DIR are local only: CANVAS_SERVER is set, so the board lives on the server");
|
|
96
|
+
const token = process.env.CANVAS_TOKEN;
|
|
97
|
+
if (!token) throw new UsageError("CANVAS_SERVER needs CANVAS_TOKEN");
|
|
98
|
+
if (command === "repair") throw new UsageError("repair runs on the local store only. Unset CANVAS_SERVER.");
|
|
99
|
+
const { LiveService, whoAmI } = await import("./live.js");
|
|
100
|
+
service = new LiveService({ server, token });
|
|
101
|
+
const claimed = process.env.CANVAS_ACTOR;
|
|
102
|
+
if (claimed || command !== "read" && command !== "export") live = (await whoAmI(server, token)).actor;
|
|
103
|
+
if (live && claimed && claimed !== `${live.kind}:${live.id}`) throw new UsageError(`CANVAS_ACTOR "${claimed}" does not match the token (${live.kind}:${live.id}). Unset CANVAS_ACTOR.`);
|
|
104
|
+
} else {
|
|
105
|
+
service = new CanvasService(openLocalStore(v.dir ?? process.env.CANVAS_DIR ?? ".agentic-canvas"));
|
|
106
|
+
}
|
|
81
107
|
const actor = () => {
|
|
108
|
+
if (live) return live;
|
|
82
109
|
try {
|
|
83
110
|
return operatorActor();
|
|
84
111
|
} catch (error) {
|
|
@@ -86,7 +113,6 @@ async function run(argv) {
|
|
|
86
113
|
throw error;
|
|
87
114
|
}
|
|
88
115
|
};
|
|
89
|
-
const service = new CanvasService(openLocalStore(v.dir ?? process.env.CANVAS_DIR ?? ".agentic-canvas"));
|
|
90
116
|
const print = (value) => console.log(JSON.stringify(value));
|
|
91
117
|
switch (command) {
|
|
92
118
|
case "new":
|
|
@@ -118,7 +144,7 @@ async function run(argv) {
|
|
|
118
144
|
const format = v.format ?? "svg";
|
|
119
145
|
if (format !== "svg" && format !== "png") throw new UsageError(`format must be svg or png, got "${format}"`);
|
|
120
146
|
if (format === "png" && !v.out) throw new UsageError("png is binary: pass --out <path>");
|
|
121
|
-
const data = format === "svg" ? await service.exportSvg(board) : await service.
|
|
147
|
+
const data = format === "svg" ? await service.exportSvg(board) : await svgToPng(await service.exportSvg(board));
|
|
122
148
|
if (!v.out) return void process.stdout.write(`${data}
|
|
123
149
|
`);
|
|
124
150
|
writeFileSync(v.out, data);
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { E as ErrorCode, B as BoardData, S as ShapeType, a as Shape, b as Endpoint, C as Connector } from './read-BY6f-mmy.js';
|
|
2
|
+
import { PrincipalKind } from 'agentic-identity';
|
|
3
|
+
import 'yjs';
|
|
4
|
+
import 'zod';
|
|
5
|
+
|
|
6
|
+
interface ClientOptions {
|
|
7
|
+
/** The API root, for example `https://canvas.example.com/api/v1/canvas`. */
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
/** A bearer token. The token sets the actor, the kind, and the role. */
|
|
10
|
+
token: string;
|
|
11
|
+
/** A custom `fetch`. Defaults to the global one. */
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
}
|
|
14
|
+
/** Options for every call. */
|
|
15
|
+
interface CallOptions {
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
18
|
+
interface ShapeInput {
|
|
19
|
+
id?: string;
|
|
20
|
+
type: ShapeType;
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
w?: number;
|
|
24
|
+
h?: number;
|
|
25
|
+
text?: string;
|
|
26
|
+
fill?: string;
|
|
27
|
+
stroke?: string;
|
|
28
|
+
/** A `data:` URL or a `blob:` ref, for an image. */
|
|
29
|
+
src?: string;
|
|
30
|
+
}
|
|
31
|
+
interface ConnectorInput {
|
|
32
|
+
id?: string;
|
|
33
|
+
from: Endpoint;
|
|
34
|
+
to: Endpoint;
|
|
35
|
+
text?: string;
|
|
36
|
+
startArrow?: boolean;
|
|
37
|
+
endArrow?: boolean;
|
|
38
|
+
stroke?: string;
|
|
39
|
+
}
|
|
40
|
+
interface ImageInfo {
|
|
41
|
+
id: string;
|
|
42
|
+
ref: string;
|
|
43
|
+
type: string;
|
|
44
|
+
size: number;
|
|
45
|
+
/** False when the same bytes were stored before. */
|
|
46
|
+
created: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A failed call. `code` is the API error code, such as `NOT_FOUND`, `UNAUTHORIZED`, or `INVALID`. `status` is the HTTP status.
|
|
50
|
+
* A reply that is not the API's error shape has code `HTTP_ERROR`. A call that never got a reply has code `NETWORK` and status 0.
|
|
51
|
+
*/
|
|
52
|
+
type ApiErrorCode = ErrorCode | "UNAUTHORIZED" | "NOT_CONFIGURED" | "TOO_LARGE" | "UNSUPPORTED_TYPE" | "UPGRADE_REQUIRED" | "INTERNAL" | "NETWORK" | "HTTP_ERROR" | (string & {});
|
|
53
|
+
declare class CanvasApiError extends Error {
|
|
54
|
+
readonly code: ApiErrorCode;
|
|
55
|
+
readonly status: number;
|
|
56
|
+
constructor(code: ApiErrorCode, message: string, status: number);
|
|
57
|
+
}
|
|
58
|
+
interface CanvasClient {
|
|
59
|
+
/** Who the token stands for. */
|
|
60
|
+
me(options?: CallOptions): Promise<{
|
|
61
|
+
actor: string;
|
|
62
|
+
kind: PrincipalKind;
|
|
63
|
+
role: "editor" | "viewer";
|
|
64
|
+
}>;
|
|
65
|
+
createBoard(input?: {
|
|
66
|
+
id?: string;
|
|
67
|
+
title?: string;
|
|
68
|
+
}, options?: CallOptions): Promise<{
|
|
69
|
+
id: string;
|
|
70
|
+
title: string;
|
|
71
|
+
}>;
|
|
72
|
+
readBoard(id: string, options?: CallOptions): Promise<BoardData & {
|
|
73
|
+
id: string;
|
|
74
|
+
}>;
|
|
75
|
+
/** The board as an SVG document. */
|
|
76
|
+
exportSvg(id: string, options?: CallOptions): Promise<string>;
|
|
77
|
+
addShape(id: string, shape: ShapeInput, options?: CallOptions): Promise<Shape>;
|
|
78
|
+
addConnector(id: string, connector: ConnectorInput, options?: CallOptions): Promise<Connector>;
|
|
79
|
+
/** Draw a flowchart from text such as `A -> B: label`. Returns the new ids. */
|
|
80
|
+
drawFlow(id: string, text: string, options?: CallOptions): Promise<{
|
|
81
|
+
nodes: string[];
|
|
82
|
+
edges: string[];
|
|
83
|
+
}>;
|
|
84
|
+
/** Move an element. Returns the ids that moved: a shape takes its connectors with it. */
|
|
85
|
+
move(id: string, element: string, dx: number, dy: number, options?: CallOptions): Promise<{
|
|
86
|
+
moved: string[];
|
|
87
|
+
}>;
|
|
88
|
+
/** Remove an element. Returns the ids removed. */
|
|
89
|
+
remove(id: string, element: string, options?: CallOptions): Promise<{
|
|
90
|
+
removed: string[];
|
|
91
|
+
}>;
|
|
92
|
+
uploadImage(bytes: Uint8Array | ArrayBuffer | Blob, options?: CallOptions): Promise<ImageInfo>;
|
|
93
|
+
/** The bytes of a stored image, and its content type. */
|
|
94
|
+
getImage(id: string, options?: CallOptions): Promise<{
|
|
95
|
+
bytes: Uint8Array;
|
|
96
|
+
type: string;
|
|
97
|
+
}>;
|
|
98
|
+
/** The stored board as a Yjs update. Only a server with live rooms serves it. */
|
|
99
|
+
state(id: string, options?: CallOptions): Promise<Uint8Array>;
|
|
100
|
+
/** Merge a live room's Yjs update into the stored board. Only a server with live rooms serves it. */
|
|
101
|
+
checkpoint(id: string, update: Uint8Array, options?: CallOptions): Promise<{
|
|
102
|
+
elements: number;
|
|
103
|
+
fixed: string[];
|
|
104
|
+
}>;
|
|
105
|
+
}
|
|
106
|
+
declare function createClient(options: ClientOptions): CanvasClient;
|
|
107
|
+
|
|
108
|
+
export { type ApiErrorCode, type CallOptions, CanvasApiError, type CanvasClient, type ClientOptions, type ConnectorInput, type ImageInfo, type ShapeInput, createClient };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var CanvasApiError = class extends Error {
|
|
3
|
+
constructor(code, message, status) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.name = "CanvasApiError";
|
|
8
|
+
}
|
|
9
|
+
code;
|
|
10
|
+
status;
|
|
11
|
+
};
|
|
12
|
+
function createClient(options) {
|
|
13
|
+
const { token } = options;
|
|
14
|
+
const base = options.baseUrl.replace(/\/+$/, "");
|
|
15
|
+
if (!token) throw new TypeError("token is required");
|
|
16
|
+
const doFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
17
|
+
const seg = encodeURIComponent;
|
|
18
|
+
async function send(method, path, init, call) {
|
|
19
|
+
const headers = { authorization: `Bearer ${token}` };
|
|
20
|
+
if (init.json !== void 0) headers["content-type"] = "application/json";
|
|
21
|
+
if (init.contentType) headers["content-type"] = init.contentType;
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await doFetch(`${base}${path}`, {
|
|
25
|
+
method,
|
|
26
|
+
headers,
|
|
27
|
+
body: init.json === void 0 ? init.body : JSON.stringify(init.json),
|
|
28
|
+
signal: call?.signal
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (call?.signal?.aborted) throw error;
|
|
32
|
+
throw new CanvasApiError("NETWORK", error instanceof Error ? error.message : "the request failed", 0);
|
|
33
|
+
}
|
|
34
|
+
if (response.ok) return response;
|
|
35
|
+
let code = "HTTP_ERROR";
|
|
36
|
+
let message = `the server answered ${response.status}`;
|
|
37
|
+
try {
|
|
38
|
+
const { error } = await response.json();
|
|
39
|
+
if (typeof error?.code === "string") code = error.code;
|
|
40
|
+
if (typeof error?.message === "string") message = error.message;
|
|
41
|
+
} catch {
|
|
42
|
+
if (call?.signal?.aborted) throw call.signal.reason;
|
|
43
|
+
}
|
|
44
|
+
throw new CanvasApiError(code, message, response.status);
|
|
45
|
+
}
|
|
46
|
+
const parse = async (response, call) => {
|
|
47
|
+
try {
|
|
48
|
+
return await response.json();
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (call?.signal?.aborted) throw error;
|
|
51
|
+
throw new CanvasApiError("HTTP_ERROR", "the server did not answer with JSON", response.status);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const json = async (method, path, body, call) => parse(await send(method, path, { json: body }, call), call);
|
|
55
|
+
const bytes = async (response) => new Uint8Array(await response.arrayBuffer());
|
|
56
|
+
return {
|
|
57
|
+
me: (o) => json("GET", "/me", void 0, o),
|
|
58
|
+
createBoard: (input = {}, o) => json("POST", "/boards", input, o),
|
|
59
|
+
readBoard: (id, o) => json("GET", `/boards/${seg(id)}`, void 0, o),
|
|
60
|
+
exportSvg: async (id, o) => (await send("GET", `/boards/${seg(id)}/svg`, {}, o)).text(),
|
|
61
|
+
addShape: (id, shape, o) => json("POST", `/boards/${seg(id)}/shapes`, shape, o),
|
|
62
|
+
addConnector: (id, connector, o) => json("POST", `/boards/${seg(id)}/connectors`, connector, o),
|
|
63
|
+
drawFlow: (id, text, o) => json("POST", `/boards/${seg(id)}/flow`, { text }, o),
|
|
64
|
+
move: (id, element, dx, dy, o) => json("POST", `/boards/${seg(id)}/move`, { element, dx, dy }, o),
|
|
65
|
+
remove: (id, element, o) => json("DELETE", `/boards/${seg(id)}/elements/${seg(element)}`, void 0, o),
|
|
66
|
+
uploadImage: async (data, o) => parse(await send("POST", "/images", { body: data, contentType: "application/octet-stream" }, o), o),
|
|
67
|
+
getImage: async (id, o) => {
|
|
68
|
+
const response = await send("GET", `/images/${seg(id)}`, {}, o);
|
|
69
|
+
return { bytes: await bytes(response), type: response.headers.get("content-type") ?? "application/octet-stream" };
|
|
70
|
+
},
|
|
71
|
+
state: async (id, o) => bytes(await send("GET", `/boards/${seg(id)}/state`, {}, o)),
|
|
72
|
+
checkpoint: async (id, update, o) => parse(await send("POST", `/boards/${seg(id)}/checkpoint`, { body: update, contentType: "application/octet-stream" }, o), o)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
CanvasApiError,
|
|
77
|
+
createClient
|
|
78
|
+
};
|