agent-canvas 0.1.44 → 0.1.45

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 (110) hide show
  1. package/bin/lib/commands/daemon.ts +3 -6
  2. package/bin/lib/commands/push.ts +1 -1
  3. package/bin/lib/daemon-lifecycle.ts +65 -72
  4. package/bin/lib/helpers.test.ts +1 -0
  5. package/bin/lib/helpers.ts +30 -74
  6. package/daemon/build.ts +16 -13
  7. package/daemon/client/App.tsx +32 -61
  8. package/daemon/client/CompareView.tsx +4 -4
  9. package/daemon/client/FileBrowser.tsx +4 -4
  10. package/daemon/client/FileViewer.tsx +4 -4
  11. package/daemon/client/OverviewView.tsx +2 -2
  12. package/daemon/client/RevisionSelect.tsx +1 -1
  13. package/daemon/client/clientApi.ts +18 -0
  14. package/daemon/client/components/index.ts +3 -16
  15. package/daemon/client/styles.css +4 -457
  16. package/daemon/dist/client.css +1 -1
  17. package/daemon/dist/client.js +76 -76
  18. package/daemon/dist/components.js +29 -29
  19. package/daemon/dist/runtime.js +1 -1
  20. package/daemon/src/components.ts +13 -0
  21. package/daemon/src/handlers/api.ts +14 -9
  22. package/daemon/src/handlers/files.ts +1 -3
  23. package/daemon/src/handlers/static.ts +1 -1
  24. package/daemon/src/handlers/uploads.ts +1 -3
  25. package/daemon/src/paths.ts +10 -24
  26. package/daemon/src/remote-feedback.test.ts +1 -1
  27. package/daemon/src/remote-feedback.ts +1 -1
  28. package/daemon/src/secrets-api.test.ts +1 -2
  29. package/daemon/src/server.ts +13 -10
  30. package/daemon/src/share-e2e.test.ts +1 -1
  31. package/daemon/src/share.test.ts +1 -1
  32. package/daemon/src/share.ts +1 -1
  33. package/daemon/tsconfig.json +1 -3
  34. package/package.json +10 -2
  35. package/.github/dark.png +0 -0
  36. package/.github/light.png +0 -0
  37. package/.github/workflows/ci.yml +0 -48
  38. package/.github/workflows/deploy-worker-prod.yml +0 -19
  39. package/.github/workflows/deploy-worker-stage.yml +0 -21
  40. package/.github/workflows/deploy-worker.yml +0 -72
  41. package/.github/workflows/release.yml +0 -36
  42. package/CLAUDE.md +0 -86
  43. package/bun.lock +0 -377
  44. package/daemon/client/AnnotationEditor.tsx +0 -315
  45. package/daemon/client/AnnotationProvider.tsx +0 -195
  46. package/daemon/client/AnnotationSidebar.tsx +0 -627
  47. package/daemon/client/FileIcon.tsx +0 -76
  48. package/daemon/client/PlanRenderer.tsx +0 -614
  49. package/daemon/client/Popover.tsx +0 -350
  50. package/daemon/client/RenderErrorContext.ts +0 -11
  51. package/daemon/client/ResponsePreview.tsx +0 -186
  52. package/daemon/client/SessionContext.tsx +0 -3
  53. package/daemon/client/annotationContext.ts +0 -114
  54. package/daemon/client/annotationDom.ts +0 -104
  55. package/daemon/client/annotationDraft.test.ts +0 -188
  56. package/daemon/client/annotationDraft.ts +0 -130
  57. package/daemon/client/compat/preact-all.ts +0 -4
  58. package/daemon/client/components/Callout.tsx +0 -26
  59. package/daemon/client/components/Checklist.tsx +0 -37
  60. package/daemon/client/components/Choice.tsx +0 -176
  61. package/daemon/client/components/CodeBlock.tsx +0 -35
  62. package/daemon/client/components/Diff.tsx +0 -65
  63. package/daemon/client/components/FilePreview.tsx +0 -88
  64. package/daemon/client/components/Image.tsx +0 -45
  65. package/daemon/client/components/Markdown.tsx +0 -44
  66. package/daemon/client/components/Mermaid.tsx +0 -565
  67. package/daemon/client/components/Note.tsx +0 -14
  68. package/daemon/client/components/Priority.tsx +0 -19
  69. package/daemon/client/components/ResponseNote.tsx +0 -19
  70. package/daemon/client/components/Section.tsx +0 -33
  71. package/daemon/client/components/Table.tsx +0 -35
  72. package/daemon/client/components/Task.tsx +0 -76
  73. package/daemon/client/components/UserInput.tsx +0 -174
  74. package/daemon/client/components/renderMarkdownChildren.tsx +0 -97
  75. package/daemon/client/generateMarkdown.ts +0 -273
  76. package/daemon/client/highlightRange.ts +0 -224
  77. package/daemon/client/popoverPosition.ts +0 -31
  78. package/daemon/client/runtime.ts +0 -123
  79. package/daemon/client/theme.css +0 -66
  80. package/daemon/client/useTextAnnotation.tsx +0 -188
  81. package/daemon/client/utils.ts +0 -13
  82. package/daemon/src/compiler.test.ts +0 -57
  83. package/daemon/src/compiler.ts +0 -360
  84. package/daemon/src/handlers/utils.ts +0 -14
  85. package/daemon/src/router.ts +0 -16
  86. package/daemon/src/session-secrets.test.ts +0 -83
  87. package/daemon/src/session.ts +0 -612
  88. package/daemon/src/watcher.ts +0 -71
  89. package/daemon/src/websocket.test.ts +0 -99
  90. package/daemon/src/websocket.ts +0 -211
  91. package/tsconfig.json +0 -14
  92. package/workers/canvas-share/README.md +0 -159
  93. package/workers/canvas-share/oblaka.ts +0 -56
  94. package/workers/canvas-share/package.json +0 -25
  95. package/workers/canvas-share/public/assets/.gitkeep +0 -0
  96. package/workers/canvas-share/public/assets/client.css +0 -2
  97. package/workers/canvas-share/public/assets/client.js +0 -91
  98. package/workers/canvas-share/public/assets/components.js +0 -65
  99. package/workers/canvas-share/public/assets/jsx-runtime.js +0 -3
  100. package/workers/canvas-share/public/assets/preact-compat.js +0 -1
  101. package/workers/canvas-share/public/assets/runtime.js +0 -1
  102. package/workers/canvas-share/public/index.html +0 -385
  103. package/workers/canvas-share/public/screenshot-dark.png +0 -0
  104. package/workers/canvas-share/public/screenshot-light.png +0 -0
  105. package/workers/canvas-share/src/index.test.ts +0 -209
  106. package/workers/canvas-share/src/index.ts +0 -518
  107. package/workers/canvas-share/src/types.ts +0 -119
  108. package/workers/canvas-share/src/util.ts +0 -156
  109. package/workers/canvas-share/src/validation.ts +0 -160
  110. package/workers/canvas-share/tsconfig.json +0 -16
@@ -1,156 +0,0 @@
1
- import type { Env } from "./types";
2
- import { RATE_LIMITS } from "./types";
3
-
4
- /**
5
- * Liberal CORS — the worker is intentionally open. The shared canvas is
6
- * loaded from the worker's own origin so reviewer fetches are same-origin
7
- * anyway. The daemon polls server-to-server and is exempt from CORS. We
8
- * keep `*` for resilience against future deployments behind alternate
9
- * origins (e.g. custom domain in front of the worker).
10
- */
11
- export const CORS_HEADERS: Record<string, string> = {
12
- "Access-Control-Allow-Origin": "*",
13
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
14
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
15
- "Access-Control-Max-Age": "86400",
16
- "Vary": "Origin",
17
- };
18
-
19
- export function json(data: unknown, init: ResponseInit = {}): Response {
20
- return new Response(JSON.stringify(data), {
21
- ...init,
22
- headers: {
23
- "Content-Type": "application/json",
24
- ...CORS_HEADERS,
25
- ...(init.headers || {}),
26
- },
27
- });
28
- }
29
-
30
- /** Hex-encoded random id of `byteLen` bytes (always lowercase). */
31
- export function randomHexId(byteLen = 12): string {
32
- const bytes = new Uint8Array(byteLen);
33
- crypto.getRandomValues(bytes);
34
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
35
- }
36
-
37
- /** SHA-256(message) hex */
38
- export async function sha256Hex(message: string | Uint8Array): Promise<string> {
39
- const data = typeof message === "string" ? new TextEncoder().encode(message) : message;
40
- const buf = await crypto.subtle.digest("SHA-256", data);
41
- return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
42
- }
43
-
44
- /** Constant-time string comparison. */
45
- export function timingSafeEqual(a: string, b: string): boolean {
46
- if (a.length !== b.length) return false;
47
- let mismatch = 0;
48
- for (let i = 0; i < a.length; i++) {
49
- mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
50
- }
51
- return mismatch === 0;
52
- }
53
-
54
- /** Best-effort client IP from request headers (CF or X-Forwarded-For). */
55
- export function clientIp(req: Request): string {
56
- return req.headers.get("CF-Connecting-IP")
57
- || req.headers.get("X-Forwarded-For")?.split(",")[0]?.trim()
58
- || "unknown";
59
- }
60
-
61
- /**
62
- * Simple sliding-window rate limiter using KV with per-minute buckets.
63
- * Returns `null` if the request is allowed; returns a `Response` (HTTP 429)
64
- * if it's rate-limited so the caller can early-return.
65
- *
66
- * Note: KV is eventually consistent, so a determined attacker can race the
67
- * limit by ~1 bucket. For the threat model here (abuse prevention, not
68
- * cryptographic auth) that's acceptable.
69
- */
70
- export async function checkRateLimit(
71
- env: Env,
72
- bucket: string,
73
- identityKey: string,
74
- maxPerMinute: number,
75
- ): Promise<Response | null> {
76
- const minute = Math.floor(Date.now() / 60_000);
77
- const key = `rl:${bucket}:${identityKey}:${minute}`;
78
- const current = parseInt((await env.FEEDBACK.get(key)) || "0", 10);
79
- if (current >= maxPerMinute) {
80
- return json(
81
- { error: "Rate limit exceeded. Please try again later." },
82
- {
83
- status: 429,
84
- headers: { "Retry-After": "60" },
85
- },
86
- );
87
- }
88
- // Increment and persist with a 2-minute TTL so the bucket auto-expires.
89
- await env.FEEDBACK.put(key, String(current + 1), { expirationTtl: 120 });
90
- return null;
91
- }
92
-
93
- export async function rateLimitShareCreate(env: Env, req: Request): Promise<Response | null> {
94
- const ip = clientIp(req);
95
- return checkRateLimit(env, "share", ip, RATE_LIMITS.SHARE_CREATE_PER_MINUTE);
96
- }
97
-
98
- export async function rateLimitFeedback(env: Env, req: Request, shareId: string): Promise<Response | null> {
99
- const ip = clientIp(req);
100
- return checkRateLimit(env, "fb", `${ip}:${shareId}`, RATE_LIMITS.FEEDBACK_PER_MINUTE);
101
- }
102
-
103
- export async function rateLimitUpload(env: Env, req: Request, shareId: string): Promise<Response | null> {
104
- const ip = clientIp(req);
105
- return checkRateLimit(env, "up", `${ip}:${shareId}`, RATE_LIMITS.UPLOAD_PER_MINUTE);
106
- }
107
-
108
- /**
109
- * Verify the optional shared-secret bearer token used to gate share
110
- * creation. Returns null if auth is OK, otherwise an error Response.
111
- */
112
- export function verifyShareAuth(env: Env, req: Request): Response | null {
113
- if (!env.SHARE_AUTH_TOKEN) return null; // open mode (dev)
114
- const header = req.headers.get("Authorization") || "";
115
- const match = header.match(/^Bearer\s+(.+)$/);
116
- if (!match || !timingSafeEqual(match[1], env.SHARE_AUTH_TOKEN)) {
117
- return json({ error: "Unauthorized" }, { status: 401 });
118
- }
119
- return null;
120
- }
121
-
122
- /** Reads the request body as text but rejects if Content-Length exceeds the limit. */
123
- export async function readBodyWithLimit(req: Request, maxBytes: number): Promise<string | Response> {
124
- const cl = req.headers.get("Content-Length");
125
- if (cl && parseInt(cl, 10) > maxBytes) {
126
- return json(
127
- { error: `Body too large (${maxBytes} bytes max)` },
128
- { status: 413 },
129
- );
130
- }
131
- // Defensive: also enforce by counting bytes during read.
132
- const reader = req.body?.getReader();
133
- if (!reader) return "";
134
- let received = 0;
135
- const chunks: Uint8Array[] = [];
136
- while (true) {
137
- const { done, value } = await reader.read();
138
- if (done) break;
139
- received += value.length;
140
- if (received > maxBytes) {
141
- try { reader.cancel(); } catch {}
142
- return json(
143
- { error: `Body too large (${maxBytes} bytes max)` },
144
- { status: 413 },
145
- );
146
- }
147
- chunks.push(value);
148
- }
149
- // Join chunks into a single string. For our payloads (≤ 2 MB) this is fine.
150
- let total = 0;
151
- for (const c of chunks) total += c.length;
152
- const merged = new Uint8Array(total);
153
- let offset = 0;
154
- for (const c of chunks) { merged.set(c, offset); offset += c.length; }
155
- return new TextDecoder().decode(merged);
156
- }
@@ -1,160 +0,0 @@
1
- /**
2
- * Wire-format validators for incoming bodies. Backed by Zod so the schemas
3
- * double as TypeScript types and the error messages stay readable.
4
- *
5
- * Two helpers — `validateSharePayload` and `validateFeedbackBody` — wrap
6
- * the Zod schemas with a `unknown -> Result | string` shape so handlers
7
- * can early-return a `400` with the error string without needing
8
- * try/catch around every parse.
9
- */
10
-
11
- import { z } from "zod";
12
- import { LIMITS } from "./types";
13
-
14
- // --- Encryption marker ------------------------------------------------------
15
-
16
- /** Marker on a SharePayload / FeedbackPostBody that tells consumers the
17
- * associated string fields contain ciphertext rather than plaintext. The
18
- * worker itself never inspects encrypted contents — it just stores them
19
- * as opaque strings. The marker exists so the *daemon* (when polling
20
- * feedback) and the *browser* (when rendering a share) know whether to
21
- * decrypt. */
22
- const encryptionSchema = z.object({
23
- alg: z.literal("AES-GCM"),
24
- v: z.literal(1),
25
- });
26
-
27
- // --- Share creation payload -------------------------------------------------
28
-
29
- const canvasFileSchema = z.object({
30
- filename: z
31
- .string()
32
- .endsWith(".jsx", { message: "must end in .jsx" })
33
- .refine((s) => !s.includes("/") && !s.includes(".."), {
34
- message: "must not contain path separators",
35
- }),
36
- compiledJs: z.string().min(1, "must be a non-empty string"),
37
- sourceJsx: z.string().optional(),
38
- });
39
-
40
- const sharePayloadSchema = z.object({
41
- version: z.literal(1),
42
- encryption: encryptionSchema.optional(),
43
- origin: z.object({
44
- sessionId: z.string(),
45
- revision: z.number().int(),
46
- label: z.string().optional(),
47
- createdAt: z.string(),
48
- }),
49
- canvasFiles: z
50
- .array(canvasFileSchema)
51
- .min(1, "must be a non-empty array")
52
- .max(50, "Too many canvas files (max 50)"),
53
- runtime: z.object({
54
- componentsVersion: z.string(),
55
- }),
56
- });
57
-
58
- export type SharePayloadParsed = z.infer<typeof sharePayloadSchema>;
59
-
60
- // --- Feedback POST body -----------------------------------------------------
61
-
62
- const annotationSchema = z.object({
63
- id: z.string(),
64
- snippet: z.string(),
65
- note: z.string(),
66
- createdAt: z.string(),
67
- filePath: z.string().optional(),
68
- canvasFile: z.string().optional(),
69
- context: z.unknown().optional(),
70
- attachments: z
71
- .array(
72
- z.object({
73
- url: z.string(),
74
- mime: z.string().optional(),
75
- }),
76
- )
77
- .optional(),
78
- });
79
-
80
- // In encrypted mode `name` / `generalNote` are ciphertext blobs (base64)
81
- // and cannot be sensibly length-bound to plaintext limits. The base schema
82
- // uses the looser ciphertext caps; a post-parse refine then re-applies the
83
- // strict plaintext bounds when no `encryption` marker is present.
84
- const feedbackBodySchema = z
85
- .object({
86
- author: z.object({
87
- id: z.string().optional(),
88
- name: z
89
- .string()
90
- .min(1, "author.name is required")
91
- .max(LIMITS.AUTHOR_NAME_CIPHERTEXT_LENGTH, "author.name too long"),
92
- }),
93
- revision: z.number().int(),
94
- annotations: z
95
- .array(annotationSchema)
96
- .max(LIMITS.MAX_ANNOTATIONS_PER_FEEDBACK, `Too many annotations (max ${LIMITS.MAX_ANNOTATIONS_PER_FEEDBACK})`)
97
- .optional(),
98
- generalNote: z
99
- .string()
100
- .max(LIMITS.GENERAL_NOTE_CIPHERTEXT_LENGTH, `generalNote too long`)
101
- .optional(),
102
- encryption: encryptionSchema.optional(),
103
- })
104
- .superRefine((body, ctx) => {
105
- if (body.encryption) return;
106
- if (body.author.name.trim().length === 0) {
107
- ctx.addIssue({
108
- code: z.ZodIssueCode.custom,
109
- path: ["author", "name"],
110
- message: "author.name is required",
111
- });
112
- }
113
- if (body.author.name.length > LIMITS.AUTHOR_NAME_LENGTH) {
114
- ctx.addIssue({
115
- code: z.ZodIssueCode.custom,
116
- path: ["author", "name"],
117
- message: `author.name must be ≤ ${LIMITS.AUTHOR_NAME_LENGTH} chars`,
118
- });
119
- }
120
- if (body.generalNote && body.generalNote.length > LIMITS.GENERAL_NOTE_LENGTH) {
121
- ctx.addIssue({
122
- code: z.ZodIssueCode.custom,
123
- path: ["generalNote"],
124
- message: `generalNote too long (max ${LIMITS.GENERAL_NOTE_LENGTH} chars)`,
125
- });
126
- }
127
- });
128
-
129
- export type FeedbackBodyParsed = z.infer<typeof feedbackBodySchema>;
130
-
131
- // --- Wrappers ---------------------------------------------------------------
132
-
133
- /**
134
- * Format the first issue from a Zod error into a single-line message.
135
- * For our needs (one error at a time, returned as JSON.error) we don't
136
- * need to surface every issue.
137
- */
138
- function firstError(err: z.ZodError): string {
139
- const issue = err.issues[0];
140
- if (!issue) return "Invalid input";
141
- const path = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
142
- return `${path}${issue.message}`;
143
- }
144
-
145
- export function validateSharePayload(raw: unknown): SharePayloadParsed | string {
146
- const result = sharePayloadSchema.safeParse(raw);
147
- if (!result.success) return firstError(result.error);
148
- return result.data;
149
- }
150
-
151
- export function validateFeedbackBody(raw: unknown): FeedbackBodyParsed | string {
152
- const result = feedbackBodySchema.safeParse(raw);
153
- if (!result.success) return firstError(result.error);
154
- return result.data;
155
- }
156
-
157
- /** Validate a shareId path segment — must look like a hex token. */
158
- export function validateShareId(id: string): boolean {
159
- return /^[a-f0-9]{8,64}$/.test(id);
160
- }
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ES2022",
5
- "moduleResolution": "bundler",
6
- "lib": ["ES2022"],
7
- "types": ["@cloudflare/workers-types"],
8
- "strict": true,
9
- "noEmit": true,
10
- "esModuleInterop": true,
11
- "skipLibCheck": true,
12
- "isolatedModules": true
13
- },
14
- "include": ["src/**/*.ts"],
15
- "exclude": ["src/**/*.test.ts"]
16
- }