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,385 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Agent Canvas &mdash; Visual canvas for Claude Code</title>
7
- <meta name="description" content="Give Claude Code a browser. Author JSX canvases, annotate, collect feedback, and share with your team." />
8
-
9
- <!-- Moderní font, který eliminuje "generický system font" look -->
10
- <link rel="preconnect" href="https://fonts.googleapis.com">
11
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
13
-
14
- <style>
15
- :root {
16
- --bg-base: #050505;
17
- --bg-surface: #0e0e11;
18
- --border-color: rgba(255, 255, 255, 0.08);
19
- --text-main: #f3f3f6;
20
- --text-muted: #a1a1aa;
21
- --accent: #5e6ad2;
22
- --accent-glow: #8b96ff;
23
- }
24
-
25
- /* Reset & Base */
26
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
27
-
28
- body {
29
- font-family: 'Inter', sans-serif;
30
- background-color: var(--bg-base);
31
- color: var(--text-main);
32
- line-height: 1.6;
33
- -webkit-font-smoothing: antialiased;
34
- -moz-osx-font-smoothing: grayscale;
35
- overflow-x: hidden;
36
- position: relative;
37
- }
38
-
39
- /* Developer-style grid background */
40
- body::before {
41
- content: '';
42
- position: absolute;
43
- top: 0; left: 0; width: 100%; height: 100vh;
44
- background-image:
45
- linear-gradient(to right, rgba(255,255,255,0.03) 1px, transparent 1px),
46
- linear-gradient(to bottom, rgba(255,255,255,0.03) 1px, transparent 1px);
47
- background-size: 40px 40px;
48
- mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
49
- -webkit-mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
50
- pointer-events: none;
51
- z-index: -2;
52
- }
53
-
54
- /* Subtle ambient glow */
55
- .glow {
56
- position: absolute;
57
- top: -10vw;
58
- left: 50%;
59
- transform: translateX(-50%);
60
- width: 60vw;
61
- height: 30vw;
62
- background: radial-gradient(ellipse at center, rgba(94, 106, 210, 0.15) 0%, rgba(0,0,0,0) 70%);
63
- filter: blur(60px);
64
- pointer-events: none;
65
- z-index: -1;
66
- }
67
-
68
- .container {
69
- max-width: 1000px;
70
- margin: 0 auto;
71
- padding: 0 1.5rem;
72
- }
73
-
74
- /* --- Hero Section --- */
75
- .hero {
76
- padding: 8rem 0 5rem;
77
- text-align: center;
78
- display: flex;
79
- flex-direction: column;
80
- align-items: center;
81
- }
82
-
83
- .hero-badge {
84
- display: inline-block;
85
- padding: 0.4rem 0.8rem;
86
- background: rgba(255, 255, 255, 0.05);
87
- border: 1px solid var(--border-color);
88
- border-radius: 999px;
89
- font-size: 0.8rem;
90
- font-weight: 500;
91
- color: var(--text-muted);
92
- margin-bottom: 2rem;
93
- backdrop-filter: blur(10px);
94
- }
95
-
96
- .hero h1 {
97
- font-size: clamp(3rem, 6vw, 4.5rem);
98
- font-weight: 800;
99
- letter-spacing: -0.04em;
100
- line-height: 1.05;
101
- margin-bottom: 1.5rem;
102
- text-wrap: balance;
103
- }
104
-
105
- .hero h1 em {
106
- font-style: normal;
107
- background: linear-gradient(135deg, #fff, var(--accent-glow));
108
- -webkit-background-clip: text;
109
- -webkit-text-fill-color: transparent;
110
- padding-right: 0.1em;
111
- }
112
-
113
- .hero .lead {
114
- font-size: 1.15rem;
115
- color: var(--text-muted);
116
- max-width: 580px;
117
- margin: 0 auto 2.5rem;
118
- text-wrap: balance;
119
- }
120
-
121
- /* Buttons */
122
- .actions {
123
- display: flex;
124
- gap: 1rem;
125
- justify-content: center;
126
- }
127
-
128
- .btn {
129
- display: inline-flex;
130
- align-items: center;
131
- gap: 0.5rem;
132
- padding: 0.75rem 1.5rem;
133
- border-radius: 8px;
134
- font-size: 0.95rem;
135
- font-weight: 500;
136
- text-decoration: none;
137
- transition: all 0.2s ease;
138
- cursor: pointer;
139
- }
140
-
141
- .btn-primary {
142
- background: var(--text-main);
143
- color: var(--bg-base);
144
- border: 1px solid transparent;
145
- box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.2);
146
- }
147
- .btn-primary:hover {
148
- transform: translateY(-1px);
149
- box-shadow: 0 8px 20px -5px rgba(255, 255, 255, 0.2);
150
- }
151
-
152
- .btn-ghost {
153
- background: rgba(255, 255, 255, 0.03);
154
- color: var(--text-main);
155
- border: 1px solid var(--border-color);
156
- backdrop-filter: blur(10px);
157
- }
158
- .btn-ghost:hover {
159
- background: rgba(255, 255, 255, 0.08);
160
- }
161
-
162
- /* --- Screenshot / Browser Mockup --- */
163
- .screenshot {
164
- max-width: 1100px;
165
- margin: 0 auto 6rem;
166
- padding: 0 1.5rem;
167
- perspective: 1000px;
168
- }
169
- .screenshot img {
170
- display: block;
171
- width: 100%;
172
- height: auto;
173
- border-radius: 12px;
174
- border: 1px solid var(--border-color);
175
- box-shadow: 0 24px 80px rgba(0, 0, 0, 0.5);
176
- }
177
-
178
-
179
- /* --- Bento Box: How it works --- */
180
- .how {
181
- margin-bottom: 6rem;
182
- }
183
-
184
- .section-title {
185
- font-size: 2rem;
186
- font-weight: 700;
187
- text-align: center;
188
- margin-bottom: 3rem;
189
- letter-spacing: -0.02em;
190
- }
191
-
192
- .bento-grid {
193
- display: grid;
194
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
195
- gap: 1.5rem;
196
- }
197
-
198
- .bento-card {
199
- background: rgba(255, 255, 255, 0.02);
200
- border: 1px solid var(--border-color);
201
- padding: 2rem;
202
- border-radius: 16px;
203
- transition: background 0.2s, border-color 0.2s;
204
- }
205
- .bento-card:hover {
206
- background: rgba(255, 255, 255, 0.04);
207
- border-color: rgba(255, 255, 255, 0.12);
208
- }
209
-
210
- .step-num {
211
- width: 36px; height: 36px;
212
- background: rgba(94, 106, 210, 0.15);
213
- color: var(--accent-glow);
214
- display: flex; align-items: center; justify-content: center;
215
- border-radius: 10px;
216
- font-weight: 600;
217
- font-size: 0.9rem;
218
- margin-bottom: 1.5rem;
219
- }
220
-
221
- .bento-card h3 {
222
- font-size: 1.1rem;
223
- font-weight: 600;
224
- margin-bottom: 0.5rem;
225
- color: var(--text-main);
226
- }
227
-
228
- .bento-card p {
229
- font-size: 0.95rem;
230
- color: var(--text-muted);
231
- line-height: 1.5;
232
- }
233
-
234
- code {
235
- font-family: 'JetBrains Mono', monospace;
236
- font-size: 0.85em;
237
- padding: 0.2em 0.4em;
238
- border-radius: 6px;
239
- background: rgba(255,255,255,0.08);
240
- color: var(--text-main);
241
- }
242
-
243
- /* --- Terminal Install --- */
244
- .install-section {
245
- text-align: center;
246
- margin-bottom: 6rem;
247
- }
248
-
249
- .terminal-wrapper {
250
- display: inline-block;
251
- min-width: 400px;
252
- background: #000;
253
- border: 1px solid var(--border-color);
254
- border-radius: 12px;
255
- overflow: hidden;
256
- box-shadow: 0 20px 40px rgba(0,0,0,0.4);
257
- text-align: left;
258
- }
259
-
260
- .terminal-header {
261
- background: #111;
262
- padding: 0.6rem 1rem;
263
- border-bottom: 1px solid var(--border-color);
264
- display: flex;
265
- gap: 6px;
266
- }
267
-
268
- .terminal-header span {
269
- width: 10px; height: 10px; border-radius: 50%; background: #333;
270
- }
271
-
272
- .terminal-body {
273
- padding: 1.5rem;
274
- font-family: 'JetBrains Mono', monospace;
275
- font-size: 0.95rem;
276
- color: var(--text-muted);
277
- }
278
-
279
- .terminal-body span { color: var(--accent-glow); }
280
- .terminal-body b { color: var(--text-main); font-weight: normal; }
281
-
282
- /* --- Footer --- */
283
- footer {
284
- padding: 3rem 0;
285
- border-top: 1px solid var(--border-color);
286
- text-align: center;
287
- font-size: 0.85rem;
288
- color: var(--text-muted);
289
- }
290
-
291
- footer a {
292
- color: var(--text-main);
293
- text-decoration: none;
294
- transition: color 0.15s;
295
- }
296
-
297
- footer a:hover { color: var(--accent-glow); }
298
-
299
- @media (max-width: 768px) {
300
- .hero { padding: 5rem 0 3rem; }
301
- .terminal-wrapper { min-width: 100%; }
302
- }
303
- </style>
304
- </head>
305
- <body>
306
-
307
- <div class="glow"></div>
308
-
309
- <div class="container">
310
- <section class="hero">
311
- <div class="hero-badge">Claude Code Integration</div>
312
- <h1>Give your AI agent<br />a <em>canvas</em></h1>
313
- <p class="lead">
314
- Agent Canvas lets Claude Code render JSX documents in the browser.
315
- Annotate, review with your team, and send structured feedback
316
- back to the agent in one continuous loop.
317
- </p>
318
-
319
- <div class="actions">
320
- <a class="btn btn-primary" href="https://github.com/contember/agent-canvas">
321
- <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.387.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.756-1.333-1.756-1.09-.745.083-.729.083-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.418-1.305.762-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.605-.015 2.896-.015 3.286 0 .315.21.694.825.576C20.565 21.795 24 17.295 24 12c0-6.63-5.37-12-12-12"/></svg>
322
- View on GitHub
323
- </a>
324
- <a class="btn btn-ghost" href="https://www.npmjs.com/package/agent-canvas">npm package</a>
325
- </div>
326
- </section>
327
-
328
- <section class="install-section">
329
- <div class="terminal-wrapper">
330
- <div class="terminal-header">
331
- <span></span><span></span><span></span>
332
- </div>
333
- <div class="terminal-body">
334
- <b>bunx</b> agent-canvas install
335
- </div>
336
- </div>
337
- </section>
338
- </div>
339
-
340
- <div class="screenshot">
341
- <img src="/screenshot-dark.png" alt="Agent Canvas displaying annotated project UI" />
342
- </div>
343
-
344
- <div class="container">
345
- <section class="how">
346
- <h2 class="section-title">Workflow built for speed</h2>
347
- <div class="bento-grid">
348
-
349
- <div class="bento-card">
350
- <div class="step-num">1</div>
351
- <h3>Install the skill</h3>
352
- <p>Run <code>bunx agent-canvas install</code> once to globally register the <code>/canvas</code> command in your Claude Code CLI.</p>
353
- </div>
354
-
355
- <div class="bento-card">
356
- <div class="step-num">2</div>
357
- <h3>Generate a canvas</h3>
358
- <p>Ask Claude for a plan, component, or report. It writes standard JSX, auto-compiles it, and launches your browser instantly.</p>
359
- </div>
360
-
361
- <div class="bento-card">
362
- <div class="step-num">3</div>
363
- <h3>Click & annotate</h3>
364
- <p>Select any rendered element to leave feedback. Write notes, vote on tasks, or general comments. Feedback returns right to Claude.</p>
365
- </div>
366
-
367
- <div class="bento-card">
368
- <div class="step-num">4</div>
369
- <h3>Share the link</h3>
370
- <p>Create a shortlink for any revision. Your teammates can review without cloning code. Everything syncs back to your terminal securely.</p>
371
- </div>
372
-
373
- </div>
374
- </section>
375
-
376
- <footer>
377
- <p>
378
- <a href="https://github.com/contember/agent-canvas">contember/agent-canvas</a>
379
- &nbsp;&middot;&nbsp; Open Source &nbsp;&middot;&nbsp; MIT Licensed
380
- </p>
381
- </footer>
382
- </div>
383
-
384
- </body>
385
- </html>
@@ -1,209 +0,0 @@
1
- /**
2
- * Worker unit tests. Validates pure functions (validation, util) without
3
- * spinning up Miniflare. End-to-end tests against a real Worker run via
4
- * `wrangler dev` are documented in README.md.
5
- *
6
- * Run with `bun test` from workers/canvas-share/.
7
- */
8
-
9
- import { describe, expect, test } from "bun:test";
10
- import { validateSharePayload, validateFeedbackBody, validateShareId } from "./validation";
11
- import { LIMITS } from "./types";
12
- import { randomHexId, sha256Hex, timingSafeEqual } from "./util";
13
-
14
- describe("validateSharePayload", () => {
15
- const valid = {
16
- version: 1,
17
- origin: { sessionId: "s1", revision: 1, createdAt: "2026-04-09T00:00:00.000Z" },
18
- canvasFiles: [{ filename: "plan.jsx", compiledJs: "export default ()=>null" }],
19
- runtime: { componentsVersion: "0.1.0" },
20
- };
21
-
22
- test("accepts a minimal valid payload", () => {
23
- expect(typeof validateSharePayload(valid)).toBe("object");
24
- });
25
-
26
- test("rejects wrong version", () => {
27
- expect(typeof validateSharePayload({ ...valid, version: 2 })).toBe("string");
28
- });
29
-
30
- test("rejects missing canvasFiles", () => {
31
- expect(typeof validateSharePayload({ ...valid, canvasFiles: [] })).toBe("string");
32
- });
33
-
34
- test("rejects path traversal in filename", () => {
35
- expect(typeof validateSharePayload({
36
- ...valid,
37
- canvasFiles: [{ filename: "../etc/passwd.jsx", compiledJs: "x" }],
38
- })).toBe("string");
39
- });
40
-
41
- test("rejects non-jsx filenames", () => {
42
- expect(typeof validateSharePayload({
43
- ...valid,
44
- canvasFiles: [{ filename: "plan.js", compiledJs: "x" }],
45
- })).toBe("string");
46
- });
47
-
48
- test("rejects empty compiledJs", () => {
49
- expect(typeof validateSharePayload({
50
- ...valid,
51
- canvasFiles: [{ filename: "plan.jsx", compiledJs: "" }],
52
- })).toBe("string");
53
- });
54
-
55
- test("rejects too many canvas files", () => {
56
- expect(typeof validateSharePayload({
57
- ...valid,
58
- canvasFiles: Array.from({ length: 51 }, (_, i) => ({ filename: `f${i}.jsx`, compiledJs: "x" })),
59
- })).toBe("string");
60
- });
61
-
62
- test("rejects missing runtime version", () => {
63
- expect(typeof validateSharePayload({ ...valid, runtime: {} })).toBe("string");
64
- });
65
- });
66
-
67
- describe("validateFeedbackBody", () => {
68
- const valid = {
69
- author: { name: "Alice" },
70
- revision: 1,
71
- annotations: [{ id: "a1", snippet: "x", note: "y", createdAt: "2026-04-09T00:00:00.000Z" }],
72
- };
73
-
74
- test("accepts a minimal valid body", () => {
75
- expect(typeof validateFeedbackBody(valid)).toBe("object");
76
- });
77
-
78
- test("rejects missing author name", () => {
79
- expect(typeof validateFeedbackBody({ ...valid, author: {} })).toBe("string");
80
- });
81
-
82
- test("rejects empty author name", () => {
83
- expect(typeof validateFeedbackBody({ ...valid, author: { name: " " } })).toBe("string");
84
- });
85
-
86
- test("rejects too-long author name", () => {
87
- expect(typeof validateFeedbackBody({
88
- ...valid,
89
- author: { name: "x".repeat(LIMITS.AUTHOR_NAME_LENGTH + 1) },
90
- })).toBe("string");
91
- });
92
-
93
- test("rejects non-integer revision", () => {
94
- expect(typeof validateFeedbackBody({ ...valid, revision: 1.5 })).toBe("string");
95
- });
96
-
97
- test("rejects too-long generalNote", () => {
98
- expect(typeof validateFeedbackBody({
99
- ...valid,
100
- generalNote: "x".repeat(LIMITS.GENERAL_NOTE_LENGTH + 1),
101
- })).toBe("string");
102
- });
103
-
104
- test("rejects too-many annotations", () => {
105
- expect(typeof validateFeedbackBody({
106
- ...valid,
107
- annotations: Array.from({ length: LIMITS.MAX_ANNOTATIONS_PER_FEEDBACK + 1 }, (_, i) => ({
108
- id: `a${i}`, snippet: "x", note: "y", createdAt: "2026-04-09T00:00:00.000Z",
109
- })),
110
- })).toBe("string");
111
- });
112
-
113
- test("rejects malformed annotation", () => {
114
- expect(typeof validateFeedbackBody({
115
- ...valid,
116
- annotations: [{ id: "a1", snippet: 123, note: "y", createdAt: "2026-04-09T00:00:00.000Z" }],
117
- })).toBe("string");
118
- });
119
-
120
- test("accepts encrypted feedback with oversized ciphertext fields", () => {
121
- // In encrypted mode plaintext caps don't apply: ciphertext is opaque
122
- // base64 that can legitimately exceed AUTHOR_NAME_LENGTH /
123
- // GENERAL_NOTE_LENGTH while staying well under the wire-size caps.
124
- const result = validateFeedbackBody({
125
- ...valid,
126
- author: { name: "x".repeat(LIMITS.AUTHOR_NAME_LENGTH + 200) },
127
- generalNote: "x".repeat(LIMITS.GENERAL_NOTE_LENGTH + 5000),
128
- encryption: { alg: "AES-GCM", v: 1 },
129
- });
130
- expect(typeof result).toBe("object");
131
- });
132
-
133
- test("still rejects unknown encryption algorithm", () => {
134
- expect(typeof validateFeedbackBody({
135
- ...valid,
136
- encryption: { alg: "RSA", v: 1 },
137
- })).toBe("string");
138
- });
139
- });
140
-
141
- describe("validateSharePayload encryption", () => {
142
- test("accepts the encryption marker", () => {
143
- const result = validateSharePayload({
144
- version: 1,
145
- encryption: { alg: "AES-GCM", v: 1 },
146
- origin: { sessionId: "ciphertext", revision: 1, createdAt: "2026-04-09T00:00:00.000Z" },
147
- canvasFiles: [{ filename: "plan.jsx", compiledJs: "ciphertext-blob" }],
148
- runtime: { componentsVersion: "0.1.0" },
149
- });
150
- expect(typeof result).toBe("object");
151
- });
152
-
153
- test("rejects unknown version of encryption", () => {
154
- const result = validateSharePayload({
155
- version: 1,
156
- encryption: { alg: "AES-GCM", v: 99 },
157
- origin: { sessionId: "x", revision: 1, createdAt: "2026-04-09T00:00:00.000Z" },
158
- canvasFiles: [{ filename: "plan.jsx", compiledJs: "x" }],
159
- runtime: { componentsVersion: "0.1.0" },
160
- });
161
- expect(typeof result).toBe("string");
162
- });
163
- });
164
-
165
- describe("validateShareId", () => {
166
- test("accepts hex tokens", () => {
167
- expect(validateShareId("abc123def456")).toBe(true);
168
- });
169
-
170
- test("rejects path traversal", () => {
171
- expect(validateShareId("../etc")).toBe(false);
172
- });
173
-
174
- test("rejects too short", () => {
175
- expect(validateShareId("abc")).toBe(false);
176
- });
177
-
178
- test("rejects uppercase", () => {
179
- expect(validateShareId("ABC123DEF456")).toBe(false);
180
- });
181
-
182
- test("rejects non-hex chars", () => {
183
- expect(validateShareId("abcg12def456")).toBe(false);
184
- });
185
- });
186
-
187
- describe("util", () => {
188
- test("randomHexId returns hex of expected length", () => {
189
- const id = randomHexId(12);
190
- expect(id).toMatch(/^[a-f0-9]{24}$/);
191
- });
192
-
193
- test("randomHexId is unique-ish", () => {
194
- const a = randomHexId(12);
195
- const b = randomHexId(12);
196
- expect(a).not.toBe(b);
197
- });
198
-
199
- test("sha256Hex matches known value", async () => {
200
- const h = await sha256Hex("hello");
201
- expect(h).toBe("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
202
- });
203
-
204
- test("timingSafeEqual works", () => {
205
- expect(timingSafeEqual("hello", "hello")).toBe(true);
206
- expect(timingSafeEqual("hello", "world")).toBe(false);
207
- expect(timingSafeEqual("hello", "hi")).toBe(false);
208
- });
209
- });