agent-canvas 0.1.44 → 0.2.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.
Files changed (115) hide show
  1. package/README.md +14 -0
  2. package/bin/lib/commands/daemon.ts +3 -6
  3. package/bin/lib/commands/push.ts +1 -1
  4. package/bin/lib/daemon-lifecycle.ts +65 -72
  5. package/bin/lib/helpers.test.ts +1 -0
  6. package/bin/lib/helpers.ts +30 -74
  7. package/daemon/build.ts +37 -16
  8. package/daemon/client/App.tsx +27 -61
  9. package/daemon/client/CompareView.tsx +4 -4
  10. package/daemon/client/FileBrowser.tsx +4 -4
  11. package/daemon/client/FileViewer.tsx +4 -4
  12. package/daemon/client/OverviewView.tsx +2 -2
  13. package/daemon/client/RevisionSelect.tsx +1 -1
  14. package/daemon/client/canvas-runtime.ts +1 -0
  15. package/daemon/client/clientApi.ts +18 -0
  16. package/daemon/client/components/index.ts +3 -16
  17. package/daemon/client/styles.css +4 -457
  18. package/daemon/dist/annotations-runtime.js +1 -0
  19. package/daemon/dist/client.css +1 -1
  20. package/daemon/dist/client.js +76 -76
  21. package/daemon/dist/components.js +59 -59
  22. package/daemon/dist/index.html +1 -0
  23. package/daemon/dist/preact-compat.js +1 -1
  24. package/daemon/dist/runtime.js +1 -1
  25. package/daemon/src/components.ts +13 -0
  26. package/daemon/src/handlers/api.ts +13 -9
  27. package/daemon/src/handlers/files.ts +2 -3
  28. package/daemon/src/handlers/static.ts +1 -1
  29. package/daemon/src/handlers/uploads.ts +2 -3
  30. package/daemon/src/paths.ts +10 -24
  31. package/daemon/src/remote-feedback.test.ts +1 -1
  32. package/daemon/src/remote-feedback.ts +1 -1
  33. package/daemon/src/secrets-api.test.ts +2 -2
  34. package/daemon/src/server.ts +11 -10
  35. package/daemon/src/share-e2e.test.ts +1 -1
  36. package/daemon/src/share.test.ts +1 -1
  37. package/daemon/src/share.ts +1 -1
  38. package/daemon/tsconfig.json +1 -3
  39. package/package.json +11 -4
  40. package/.github/dark.png +0 -0
  41. package/.github/light.png +0 -0
  42. package/.github/workflows/ci.yml +0 -48
  43. package/.github/workflows/deploy-worker-prod.yml +0 -19
  44. package/.github/workflows/deploy-worker-stage.yml +0 -21
  45. package/.github/workflows/deploy-worker.yml +0 -72
  46. package/.github/workflows/release.yml +0 -36
  47. package/CLAUDE.md +0 -86
  48. package/bun.lock +0 -377
  49. package/daemon/client/AnnotationEditor.tsx +0 -315
  50. package/daemon/client/AnnotationProvider.tsx +0 -195
  51. package/daemon/client/AnnotationSidebar.tsx +0 -627
  52. package/daemon/client/FileIcon.tsx +0 -76
  53. package/daemon/client/PlanRenderer.tsx +0 -614
  54. package/daemon/client/Popover.tsx +0 -350
  55. package/daemon/client/RenderErrorContext.ts +0 -11
  56. package/daemon/client/ResponsePreview.tsx +0 -186
  57. package/daemon/client/SessionContext.tsx +0 -3
  58. package/daemon/client/annotationContext.ts +0 -114
  59. package/daemon/client/annotationDom.ts +0 -104
  60. package/daemon/client/annotationDraft.test.ts +0 -188
  61. package/daemon/client/annotationDraft.ts +0 -130
  62. package/daemon/client/compat/preact-all.ts +0 -4
  63. package/daemon/client/components/Callout.tsx +0 -26
  64. package/daemon/client/components/Checklist.tsx +0 -37
  65. package/daemon/client/components/Choice.tsx +0 -176
  66. package/daemon/client/components/CodeBlock.tsx +0 -35
  67. package/daemon/client/components/Diff.tsx +0 -65
  68. package/daemon/client/components/FilePreview.tsx +0 -88
  69. package/daemon/client/components/Image.tsx +0 -45
  70. package/daemon/client/components/Markdown.tsx +0 -44
  71. package/daemon/client/components/Mermaid.tsx +0 -565
  72. package/daemon/client/components/Note.tsx +0 -14
  73. package/daemon/client/components/Priority.tsx +0 -19
  74. package/daemon/client/components/ResponseNote.tsx +0 -19
  75. package/daemon/client/components/Section.tsx +0 -33
  76. package/daemon/client/components/Table.tsx +0 -35
  77. package/daemon/client/components/Task.tsx +0 -76
  78. package/daemon/client/components/UserInput.tsx +0 -174
  79. package/daemon/client/components/renderMarkdownChildren.tsx +0 -97
  80. package/daemon/client/generateMarkdown.ts +0 -273
  81. package/daemon/client/highlightRange.ts +0 -224
  82. package/daemon/client/popoverPosition.ts +0 -31
  83. package/daemon/client/runtime.ts +0 -123
  84. package/daemon/client/theme.css +0 -66
  85. package/daemon/client/useTextAnnotation.tsx +0 -188
  86. package/daemon/client/utils.ts +0 -13
  87. package/daemon/src/compiler.test.ts +0 -57
  88. package/daemon/src/compiler.ts +0 -360
  89. package/daemon/src/handlers/utils.ts +0 -14
  90. package/daemon/src/router.ts +0 -16
  91. package/daemon/src/session-secrets.test.ts +0 -83
  92. package/daemon/src/session.ts +0 -612
  93. package/daemon/src/watcher.ts +0 -71
  94. package/daemon/src/websocket.test.ts +0 -99
  95. package/daemon/src/websocket.ts +0 -211
  96. package/tsconfig.json +0 -14
  97. package/workers/canvas-share/README.md +0 -159
  98. package/workers/canvas-share/oblaka.ts +0 -56
  99. package/workers/canvas-share/package.json +0 -25
  100. package/workers/canvas-share/public/assets/.gitkeep +0 -0
  101. package/workers/canvas-share/public/assets/client.css +0 -2
  102. package/workers/canvas-share/public/assets/client.js +0 -91
  103. package/workers/canvas-share/public/assets/components.js +0 -65
  104. package/workers/canvas-share/public/assets/jsx-runtime.js +0 -3
  105. package/workers/canvas-share/public/assets/preact-compat.js +0 -1
  106. package/workers/canvas-share/public/assets/runtime.js +0 -1
  107. package/workers/canvas-share/public/index.html +0 -385
  108. package/workers/canvas-share/public/screenshot-dark.png +0 -0
  109. package/workers/canvas-share/public/screenshot-light.png +0 -0
  110. package/workers/canvas-share/src/index.test.ts +0 -209
  111. package/workers/canvas-share/src/index.ts +0 -518
  112. package/workers/canvas-share/src/types.ts +0 -119
  113. package/workers/canvas-share/src/util.ts +0 -156
  114. package/workers/canvas-share/src/validation.ts +0 -160
  115. package/workers/canvas-share/tsconfig.json +0 -16
@@ -1,99 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync } from "fs";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
- import { SessionManager } from "./session";
6
- import { createWebSocketManager, type CanvasSocket, type WSData } from "./websocket";
7
-
8
- const testDirectories: string[] = [];
9
-
10
- afterEach(() => {
11
- for (const directory of testDirectories.splice(0)) {
12
- rmSync(directory, { recursive: true, force: true });
13
- }
14
- });
15
-
16
- class FakeSocket implements CanvasSocket {
17
- readonly sent: string[] = [];
18
- closed = false;
19
-
20
- constructor(readonly data: WSData) {}
21
-
22
- send(message: string) {
23
- this.sent.push(message);
24
- }
25
-
26
- close() {
27
- this.closed = true;
28
- }
29
-
30
- ping() {}
31
- }
32
-
33
- function createSessionManager(): SessionManager {
34
- const directory = mkdtempSync(join(tmpdir(), "agent-canvas-websocket-test-"));
35
- testDirectories.push(directory);
36
- const manager = new SessionManager(directory);
37
- manager.upsert(
38
- "session",
39
- new Map([["architecture.jsx", "<Section title=\"Architecture\" />"]]),
40
- directory,
41
- );
42
- return manager;
43
- }
44
-
45
- describe("canvas render errors", () => {
46
- test("delivers an error that arrived before the CLI watcher connected", () => {
47
- const sessionManager = createSessionManager();
48
- const websocket = createWebSocketManager(sessionManager);
49
- const browser = new FakeSocket({ type: "browser", sessionId: "session" });
50
- websocket.handlers.open(browser);
51
-
52
- websocket.handlers.message(browser, JSON.stringify({
53
- type: "render-error",
54
- error: {
55
- revision: 1,
56
- filename: "architecture.jsx",
57
- message: "Cannot read properties of undefined (reading 'map')",
58
- },
59
- }));
60
-
61
- const watcher = new FakeSocket({ type: "wait", sessionId: "session" });
62
- websocket.handlers.open(watcher);
63
-
64
- expect(watcher.sent).toHaveLength(1);
65
- expect(JSON.parse(watcher.sent[0])).toEqual({
66
- type: "render-error",
67
- error: {
68
- revision: 1,
69
- filename: "architecture.jsx",
70
- message: "Cannot read properties of undefined (reading 'map')",
71
- },
72
- });
73
- expect(watcher.closed).toBe(true);
74
- expect(sessionManager.get("session")?.revisions[0].hasFeedback).toBe(false);
75
- });
76
-
77
- test("drops a pending error when a new revision is pushed", () => {
78
- const sessionManager = createSessionManager();
79
- const websocket = createWebSocketManager(sessionManager);
80
- const browser = new FakeSocket({ type: "browser", sessionId: "session" });
81
- websocket.handlers.open(browser);
82
- websocket.handlers.message(browser, JSON.stringify({
83
- type: "render-error",
84
- error: { revision: 1, filename: "architecture.jsx", message: "Broken revision" },
85
- }));
86
-
87
- sessionManager.upsert(
88
- "session",
89
- new Map([["architecture.jsx", "<Section title=\"Fixed\" />"]]),
90
- "/project",
91
- );
92
- websocket.broadcastPlanUpdate("session");
93
-
94
- const watcher = new FakeSocket({ type: "wait", sessionId: "session" });
95
- websocket.handlers.open(watcher);
96
- expect(watcher.sent).toEqual([]);
97
- expect(watcher.closed).toBe(false);
98
- });
99
- });
@@ -1,211 +0,0 @@
1
- import type { SessionManager, RemoteFeedbackEntry } from "./session";
2
-
3
- export type WSData = { type: "browser" | "wait"; sessionId: string };
4
-
5
- export interface CanvasSocket {
6
- data: WSData;
7
- send(message: string): unknown;
8
- close(): void;
9
- ping(): unknown;
10
- }
11
-
12
- interface CanvasRenderError {
13
- revision: number;
14
- filename: string;
15
- message: string;
16
- stack?: string;
17
- componentStack?: string;
18
- }
19
-
20
- function isCanvasRenderError(value: unknown): value is CanvasRenderError {
21
- return typeof value === "object" && value !== null
22
- && "revision" in value && Number.isInteger(value.revision)
23
- && "filename" in value && typeof value.filename === "string"
24
- && "message" in value && typeof value.message === "string"
25
- && (!("stack" in value) || value.stack === undefined || typeof value.stack === "string")
26
- && (!("componentStack" in value) || value.componentStack === undefined || typeof value.componentStack === "string");
27
- }
28
-
29
- export function createWebSocketManager(sessionManager: SessionManager) {
30
- const browserSockets = new Map<string, Set<CanvasSocket>>();
31
- const waitSockets = new Map<string, Set<CanvasSocket>>();
32
- const pendingRenderErrors = new Map<string, CanvasRenderError>();
33
-
34
- function broadcastPlanUpdate(sessionId: string) {
35
- pendingRenderErrors.delete(sessionId);
36
- const sockets = browserSockets.get(sessionId);
37
- if (!sockets) return;
38
- const session = sessionManager.get(sessionId);
39
- if (!session) return;
40
- const payload = JSON.stringify({
41
- type: "plan-updated",
42
- currentRevision: session.currentRevision,
43
- revisions: session.revisions,
44
- });
45
- for (const ws of sockets) {
46
- ws.send(payload);
47
- }
48
- }
49
-
50
- function broadcastRevisionUpdate(sessionId: string) {
51
- const sockets = browserSockets.get(sessionId);
52
- if (!sockets) return;
53
- const session = sessionManager.get(sessionId);
54
- if (!session) return;
55
- const payload = JSON.stringify({
56
- type: "revision-updated",
57
- revisions: session.revisions,
58
- });
59
- for (const ws of sockets) {
60
- ws.send(payload);
61
- }
62
- }
63
-
64
- function broadcastRemoteFeedback(sessionId: string, revision: number, entries: RemoteFeedbackEntry[]) {
65
- const sockets = browserSockets.get(sessionId);
66
- if (!sockets || sockets.size === 0) return;
67
- const payload = JSON.stringify({
68
- type: "remote-feedback",
69
- revision,
70
- entries,
71
- });
72
- for (const ws of sockets) {
73
- ws.send(payload);
74
- }
75
- }
76
-
77
- function broadcastWatcherStatus(sessionId: string) {
78
- const sockets = browserSockets.get(sessionId);
79
- if (!sockets) return;
80
- const watching = (waitSockets.get(sessionId)?.size ?? 0) > 0;
81
- const payload = JSON.stringify({ type: "watcher-status", watching });
82
- for (const ws of sockets) {
83
- ws.send(payload);
84
- }
85
- }
86
-
87
- function deliverPendingRenderError(sessionId: string): boolean {
88
- const error = pendingRenderErrors.get(sessionId);
89
- const waiters = waitSockets.get(sessionId);
90
- if (!error || !waiters || waiters.size === 0) return false;
91
-
92
- pendingRenderErrors.delete(sessionId);
93
- waitSockets.delete(sessionId);
94
- const payload = JSON.stringify({ type: "render-error", error });
95
- for (const waiter of waiters) {
96
- waiter.send(payload);
97
- waiter.close();
98
- }
99
- broadcastWatcherStatus(sessionId);
100
- return true;
101
- }
102
-
103
- // Ping wait sockets periodically to detect dead connections.
104
- // We can't rely on ws.close() triggering the close handler for dead sockets,
105
- // so we manually remove dead sockets and broadcast status.
106
- const PING_INTERVAL = 5_000;
107
- const pongReceived = new WeakSet<CanvasSocket>();
108
-
109
- function removeWaitSocket(ws: CanvasSocket, sessionId: string) {
110
- const sockets = waitSockets.get(sessionId);
111
- if (sockets) {
112
- sockets.delete(ws);
113
- if (sockets.size === 0) waitSockets.delete(sessionId);
114
- }
115
- try { ws.close(); } catch {}
116
- broadcastWatcherStatus(sessionId);
117
- }
118
-
119
- setInterval(() => {
120
- for (const [sessionId, sockets] of waitSockets) {
121
- for (const ws of sockets) {
122
- if (!pongReceived.has(ws)) {
123
- // No pong since last ping — connection is dead
124
- removeWaitSocket(ws, sessionId);
125
- continue;
126
- }
127
- pongReceived.delete(ws);
128
- try { ws.ping(); } catch {
129
- removeWaitSocket(ws, sessionId);
130
- }
131
- }
132
- }
133
- }, PING_INTERVAL);
134
-
135
- const handlers = {
136
- open(ws: CanvasSocket) {
137
- const { type, sessionId } = ws.data;
138
- const map = type === "browser" ? browserSockets : waitSockets;
139
- if (!map.has(sessionId)) map.set(sessionId, new Set());
140
- map.get(sessionId)!.add(ws);
141
- if (type === "browser") {
142
- // Send current watcher status to newly connected browser
143
- const watching = (waitSockets.get(sessionId)?.size ?? 0) > 0;
144
- ws.send(JSON.stringify({ type: "watcher-status", watching }));
145
- } else {
146
- // CLI waiter connected — notify browsers
147
- pongReceived.add(ws); // Give it a free pass on first interval
148
- if (!deliverPendingRenderError(sessionId)) {
149
- broadcastWatcherStatus(sessionId);
150
- }
151
- }
152
- },
153
- message(ws: CanvasSocket, message: string | Buffer) {
154
- const { type, sessionId } = ws.data;
155
- if (type === "browser") {
156
- try {
157
- const data = JSON.parse(typeof message === "string" ? message : new TextDecoder().decode(message));
158
- if (data.type === "submit") {
159
- const feedback = data.feedback as string;
160
-
161
- const session = sessionManager.get(sessionId);
162
- if (session) {
163
- sessionManager.saveFeedback(sessionId, session.currentRevision, feedback);
164
- broadcastRevisionUpdate(sessionId);
165
-
166
- const waiters = waitSockets.get(sessionId);
167
- if (waiters && waiters.size > 0) {
168
- sessionManager.consumeFeedback(sessionId, session.currentRevision);
169
- broadcastRevisionUpdate(sessionId);
170
- const payload = JSON.stringify({ type: "submit", feedback });
171
- for (const waiter of waiters) {
172
- waiter.send(payload);
173
- waiter.close();
174
- }
175
- waitSockets.delete(sessionId);
176
- }
177
- }
178
- }
179
- if (data.type === "render-error" && isCanvasRenderError(data.error)) {
180
- const session = sessionManager.get(sessionId);
181
- if (
182
- session
183
- && data.error.revision === session.currentRevision
184
- && session.canvasFiles.includes(data.error.filename)
185
- ) {
186
- pendingRenderErrors.set(sessionId, data.error);
187
- deliverPendingRenderError(sessionId);
188
- }
189
- }
190
- } catch {}
191
- }
192
- },
193
- pong(ws: CanvasSocket) {
194
- if (ws.data.type === "wait") {
195
- pongReceived.add(ws);
196
- }
197
- },
198
- close(ws: CanvasSocket) {
199
- const { type, sessionId } = ws.data;
200
- const map = type === "browser" ? browserSockets : waitSockets;
201
- map.get(sessionId)?.delete(ws);
202
- if (map.get(sessionId)?.size === 0) map.delete(sessionId);
203
- if (type === "wait") {
204
- // CLI waiter disconnected — notify browsers
205
- broadcastWatcherStatus(sessionId);
206
- }
207
- },
208
- };
209
-
210
- return { handlers, broadcastPlanUpdate, broadcastRevisionUpdate, broadcastWatcherStatus, broadcastRemoteFeedback };
211
- }
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "allowImportingTsExtensions": true,
10
- "noEmit": true,
11
- "types": ["@types/bun"]
12
- },
13
- "include": ["bin/**/*"]
14
- }
@@ -1,159 +0,0 @@
1
- # canvas-share
2
-
3
- Cloudflare Worker that hosts shared agent-canvas snapshots.
4
-
5
- ## Architecture
6
-
7
- - **R2 (`BLOBS`)** stores share payloads, compiled canvas JS, JSX source, and reviewer-uploaded annotation images.
8
- - **KV (`FEEDBACK`)** stores individual feedback entries (sortable keys for `since` queries) and rate limit counters (TTL 120s).
9
- - **Static assets (`ASSETS`)** serve runtime bundles (`runtime.js`, `components.js`, `preact-compat.js`, `client.js`, `client.css`) — these are mirrored from `daemon/dist/` automatically by `bun run build`.
10
-
11
- Validation is enforced with [Zod](https://zod.dev). Limits are centralized in [`src/types.ts`](src/types.ts).
12
-
13
- ## Infrastructure as code
14
-
15
- The Cloudflare config is defined programmatically in [`oblaka.ts`](oblaka.ts)
16
- using [`oblaka-iac`](https://github.com/contember/oblaka). The committed
17
- [`oblaka.ts`](oblaka.ts) is the source of truth; `wrangler.jsonc` is
18
- auto-generated and **gitignored** — never edit it by hand.
19
-
20
- | Command | Generates | Used for |
21
- |---|---|---|
22
- | `bun run oblaka` | `wrangler.jsonc` (env `local`) | local dev via Miniflare |
23
- | `bun run oblaka:stage` | `wrangler.jsonc` (env `stage`) | staging deploy |
24
- | `bun run oblaka:prod` | `wrangler.jsonc` (env `prod`) | production deploy |
25
-
26
- `oblaka-iac` auto-prefixes R2 bucket and KV namespace names with the env
27
- (e.g. `canvas-share-blobs` becomes `stage-canvas-share-blobs`) so the same
28
- config produces isolated stage and prod resources.
29
-
30
- ## Local development
31
-
32
- ```bash
33
- cd workers/canvas-share
34
- bun install
35
-
36
- # Build daemon bundles AND auto-copy them into ./public/assets
37
- cd ../..
38
- bun run build
39
-
40
- # Start the worker locally via Miniflare (no Cloudflare account needed).
41
- # `bun run dev` runs `oblaka` first to regenerate wrangler.jsonc, then
42
- # starts wrangler dev --local.
43
- cd workers/canvas-share
44
- bun run dev
45
-
46
- # Point the daemon at it
47
- cd ../..
48
- CANVAS_SHARE_ENDPOINT=http://127.0.0.1:8787 bun bin/agent-canvas.ts daemon restart
49
- ```
50
-
51
- Then click **Share** on any revision in the canvas UI.
52
-
53
- ## Testing
54
-
55
- ```bash
56
- cd workers/canvas-share
57
- bun test # unit tests for validation, util
58
- bun run typecheck # typecheck src/
59
- ```
60
-
61
- ## Continuous deployment
62
-
63
- The repo ships GitHub Actions workflows that deploy the worker automatically:
64
-
65
- | Workflow | Trigger | Env |
66
- |---|---|---|
67
- | `deploy-worker-stage.yml` | Push to `main` touching `workers/canvas-share/**`, `daemon/client/**`, or `daemon/build.ts` | `stage` |
68
- | `deploy-worker-prod.yml` | Version tag push (`v*`) | `prod` |
69
-
70
- Both call the reusable `deploy-worker.yml` which:
71
-
72
- 1. Installs deps + builds the daemon runtime bundles (auto-mirrored into `public/assets/`)
73
- 2. Runs `bunx oblaka oblaka.ts --env <env> --remote` to create or update the R2 bucket and KV namespace on Cloudflare
74
- 3. Sets `SHARE_AUTH_TOKEN` via `wrangler secret bulk` (skipped if no token is configured for the env)
75
- 4. Runs `bunx wrangler deploy`
76
-
77
- ### Required GitHub secrets
78
-
79
- | Secret | Purpose |
80
- |---|---|
81
- | `CLOUDFLARE_API_TOKEN` | Token with Workers + R2 + KV permissions on the target account |
82
- | `CLOUDFLARE_ACCOUNT_ID` | Account id the worker deploys to |
83
- | `SHARE_AUTH_TOKEN_STAGE` | Bearer token daemons must use to POST to stage (optional) |
84
- | `SHARE_AUTH_TOKEN_PROD` | Bearer token daemons must use to POST to prod (optional) |
85
-
86
- Both stage and prod use the same Cloudflare account; oblaka auto-prefixes resource names so stage and prod get separate `stage-canvas-share-blobs` and `prod-canvas-share-blobs` buckets (same for KV).
87
-
88
- Use GitHub Environments (`Settings → Environments → stage / prod`) to gate prod deploys behind a manual approval if you want belt-and-suspenders safety.
89
-
90
- ## Manual production deploy
91
-
92
- If you'd rather deploy by hand instead of via CI:
93
-
94
- 1. (Recommended) Generate a shared bearer token to gate share creation:
95
- ```bash
96
- openssl rand -hex 32 | bunx wrangler secret put SHARE_AUTH_TOKEN --env prod
97
- ```
98
- Without this, anyone on the internet can pump shares into your R2 bucket.
99
- Set the same value as `CANVAS_SHARE_AUTH_TOKEN` on the daemon side.
100
-
101
- 2. (Optional) Override default 30-day share expiration:
102
- ```bash
103
- echo "604800" | bunx wrangler secret put SHARE_TTL_SECONDS --env prod # 7 days
104
- ```
105
-
106
- 3. Build daemon bundles + deploy. The deploy script regenerates
107
- `wrangler.jsonc` for the target env, then runs `wrangler deploy`.
108
- The R2 bucket and KV namespace are created automatically by oblaka
109
- on first deploy:
110
- ```bash
111
- cd ../..
112
- bun run build
113
- cd workers/canvas-share
114
- bun run deploy:prod # or deploy:stage
115
- ```
116
-
117
- 4. Configure the daemon:
118
- ```bash
119
- export CANVAS_SHARE_ENDPOINT=https://canvas-share.<subdomain>.workers.dev
120
- export CANVAS_SHARE_AUTH_TOKEN=<the token you generated>
121
- bunx agent-canvas daemon restart
122
- ```
123
-
124
- The Share button in the canvas UI lights up once the daemon sees `CANVAS_SHARE_ENDPOINT`.
125
-
126
- ## Endpoints
127
-
128
- | Method | Path | Purpose | Auth |
129
- |--------|------------------------------------|-------------------------------------------------|---------------------|
130
- | GET | `/health` | Health check | none |
131
- | POST | `/shares` | Create a new share | `SHARE_AUTH_TOKEN` |
132
- | POST | `/shares/:shareId/revoke` | Revoke / delete a share | `ownerToken` |
133
- | GET | `/shares/:shareId/feedback?since=` | List feedback entries (called by daemon poller) | none (capability) |
134
- | GET | `/s/:shareId` | HTML shell for reviewers | none (capability) |
135
- | GET | `/s/:shareId/meta` | Share metadata | none |
136
- | GET | `/s/:shareId/canvas/:filename` | Serve compiled canvas JS / JSX source | none |
137
- | POST | `/s/:shareId/feedback` | Submit feedback (called by reviewer browser) | none |
138
- | POST | `/s/:shareId/upload` | Upload annotation image | none |
139
- | GET | `/s/:shareId/uploads/:filename` | Serve uploaded image | none |
140
-
141
- ## Rate limits
142
-
143
- Per-IP, per-minute, sliding window (KV-backed). Tweak in [`src/types.ts`](src/types.ts).
144
-
145
- | Bucket | Limit |
146
- |-----------------|-------|
147
- | Share creation | 5/min |
148
- | Feedback POST | 20/min/share |
149
- | Upload | 30/min/share |
150
-
151
- ## Security model
152
-
153
- - **Capability URLs** — anyone with `/s/:shareId` (24-char hex, ~96 bits entropy) has full view+comment access. Brute-forcing is impractical.
154
- - **Owner token** — returned to the daemon at create time, required to revoke. Stored in the daemon's session metadata so the canvas author can revoke from the Share dialog.
155
- - **Optional shared bearer token** (`SHARE_AUTH_TOKEN`) — gates share creation so only authorized daemons can push to the worker.
156
- - **No reviewer auth** — reviewers self-report a name (stored locally in their browser), there is no signup. Suitable for "send to a colleague" flows, **not** public publishing.
157
- - **Per-share TTL** — defaults to 30 days. Expired shares 404 and are eventually purged by KV's TTL.
158
- - **Size limits** — share payload max 2 MB, feedback body max 256 KB, image upload max 5 MB.
159
- - **Validation** — all incoming bodies are validated by Zod schemas before any storage write.
@@ -1,56 +0,0 @@
1
- import { define, KVNamespace, R2Bucket, Worker } from 'oblaka-iac'
2
-
3
- /**
4
- * Per-environment overrides. `local` is what `bunx oblaka oblaka.ts` uses
5
- * when generating wrangler.jsonc for `wrangler dev --local`. `stage` and
6
- * `prod` are picked when running `bunx oblaka oblaka.ts --env stage|prod`
7
- * for actual Cloudflare deploys.
8
- *
9
- * Secrets (SHARE_AUTH_TOKEN, SHARE_TTL_SECONDS overrides, etc.) are NOT
10
- * declared here — set them with `wrangler secret put` after deploy.
11
- */
12
- const vars = {
13
- local: {
14
- // Empty share auth in local dev so smoke tests don't need a token.
15
- },
16
- stage: {},
17
- prod: {},
18
- }
19
-
20
- export default define(({ env }) => {
21
- if (!(env in vars)) {
22
- throw new Error(`Unknown environment ${env}`)
23
- }
24
-
25
- const blobs = new R2Bucket({
26
- name: 'canvas-share-blobs',
27
- })
28
-
29
- const feedback = new KVNamespace({
30
- name: 'canvas-share-feedback',
31
- })
32
-
33
- return new Worker({
34
- dir: '.',
35
- name: 'canvas-share',
36
- main: './src/index.ts',
37
- compatibility_flags: ['nodejs_compat'],
38
- compatibility_date: '2025-01-01',
39
- observability: { enabled: true },
40
- // Runtime bundles (preact-compat, runtime, components, client.js + css)
41
- // are mirrored here from daemon/dist/ by `bun run build` in the repo
42
- // root. The worker serves them via the ASSETS binding.
43
- assets: {
44
- directory: './public',
45
- binding: 'ASSETS',
46
- },
47
- bindings: {
48
- BLOBS: blobs,
49
- FEEDBACK: feedback,
50
- },
51
- vars: {
52
- ENVIRONMENT: env,
53
- ...vars[env as keyof typeof vars],
54
- },
55
- })
56
- })
@@ -1,25 +0,0 @@
1
- {
2
- "name": "canvas-share",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "oblaka": "bunx oblaka oblaka.ts",
8
- "oblaka:stage": "bunx oblaka oblaka.ts --env stage",
9
- "oblaka:prod": "bunx oblaka oblaka.ts --env prod",
10
- "dev": "bun run oblaka && wrangler dev --local",
11
- "deploy:stage": "bun run oblaka:stage && wrangler deploy --env stage",
12
- "deploy:prod": "bun run oblaka:prod && wrangler deploy --env prod",
13
- "typecheck": "tsc --noEmit",
14
- "test": "bun test"
15
- },
16
- "dependencies": {
17
- "zod": "^3.23.8"
18
- },
19
- "devDependencies": {
20
- "@cloudflare/workers-types": "^4.20250101.0",
21
- "oblaka-iac": "latest",
22
- "typescript": "^5.6.0",
23
- "wrangler": "^4.24.0"
24
- }
25
- }
File without changes