@agent-native/core 0.40.2 → 0.41.1

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 (148) hide show
  1. package/README.md +11 -1
  2. package/dist/cli/create.d.ts.map +1 -1
  3. package/dist/cli/create.js +57 -0
  4. package/dist/cli/create.js.map +1 -1
  5. package/dist/cli/index.js +16 -0
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/cli/pr-visual-recap-workflow.d.ts +11 -0
  8. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -0
  9. package/dist/cli/pr-visual-recap-workflow.js +11 -0
  10. package/dist/cli/pr-visual-recap-workflow.js.map +1 -0
  11. package/dist/cli/recap.d.ts +52 -0
  12. package/dist/cli/recap.d.ts.map +1 -0
  13. package/dist/cli/recap.js +581 -0
  14. package/dist/cli/recap.js.map +1 -0
  15. package/dist/cli/skills.d.ts +17 -4
  16. package/dist/cli/skills.d.ts.map +1 -1
  17. package/dist/cli/skills.js +60 -16
  18. package/dist/cli/skills.js.map +1 -1
  19. package/dist/cli/templates-meta.js +1 -1
  20. package/dist/cli/templates-meta.js.map +1 -1
  21. package/dist/cli/workspacify.d.ts.map +1 -1
  22. package/dist/cli/workspacify.js +19 -4
  23. package/dist/cli/workspacify.js.map +1 -1
  24. package/dist/client/blocks/index.d.ts +3 -0
  25. package/dist/client/blocks/index.d.ts.map +1 -1
  26. package/dist/client/blocks/index.js +3 -0
  27. package/dist/client/blocks/index.js.map +1 -1
  28. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts +6 -0
  29. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -0
  30. package/dist/client/blocks/library/AnnotatedCodeBlock.js +134 -0
  31. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -0
  32. package/dist/client/blocks/library/HighlightedCode.d.ts +21 -1
  33. package/dist/client/blocks/library/HighlightedCode.d.ts.map +1 -1
  34. package/dist/client/blocks/library/HighlightedCode.js +86 -4
  35. package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
  36. package/dist/client/blocks/library/annotated-code.config.d.ts +58 -0
  37. package/dist/client/blocks/library/annotated-code.config.d.ts.map +1 -0
  38. package/dist/client/blocks/library/annotated-code.config.js +53 -0
  39. package/dist/client/blocks/library/annotated-code.config.js.map +1 -0
  40. package/dist/client/blocks/library/checklist.js +2 -2
  41. package/dist/client/blocks/library/checklist.js.map +1 -1
  42. package/dist/client/blocks/library/code-highlight.d.ts +16 -0
  43. package/dist/client/blocks/library/code-highlight.d.ts.map +1 -0
  44. package/dist/client/blocks/library/code-highlight.js +160 -0
  45. package/dist/client/blocks/library/code-highlight.js.map +1 -0
  46. package/dist/client/blocks/library/code-tabs.config.d.ts +6 -0
  47. package/dist/client/blocks/library/code-tabs.config.d.ts.map +1 -1
  48. package/dist/client/blocks/library/code-tabs.config.js +1 -0
  49. package/dist/client/blocks/library/code-tabs.config.js.map +1 -1
  50. package/dist/client/blocks/library/code-tabs.d.ts.map +1 -1
  51. package/dist/client/blocks/library/code-tabs.js +35 -5
  52. package/dist/client/blocks/library/code-tabs.js.map +1 -1
  53. package/dist/client/blocks/library/code.config.d.ts +43 -0
  54. package/dist/client/blocks/library/code.config.d.ts.map +1 -0
  55. package/dist/client/blocks/library/code.config.js +34 -0
  56. package/dist/client/blocks/library/code.config.js.map +1 -0
  57. package/dist/client/blocks/library/code.d.ts +3 -0
  58. package/dist/client/blocks/library/code.d.ts.map +1 -0
  59. package/dist/client/blocks/library/code.js +95 -0
  60. package/dist/client/blocks/library/code.js.map +1 -0
  61. package/dist/client/blocks/library/dev-doc-ui.d.ts +2 -1
  62. package/dist/client/blocks/library/dev-doc-ui.d.ts.map +1 -1
  63. package/dist/client/blocks/library/dev-doc-ui.js +2 -1
  64. package/dist/client/blocks/library/dev-doc-ui.js.map +1 -1
  65. package/dist/client/blocks/library/server-specs.d.ts.map +1 -1
  66. package/dist/client/blocks/library/server-specs.js +21 -0
  67. package/dist/client/blocks/library/server-specs.js.map +1 -1
  68. package/dist/client/blocks/library/specs.d.ts +1 -1
  69. package/dist/client/blocks/library/specs.d.ts.map +1 -1
  70. package/dist/client/blocks/library/specs.js +30 -2
  71. package/dist/client/blocks/library/specs.js.map +1 -1
  72. package/dist/client/blocks/server.d.ts +1 -0
  73. package/dist/client/blocks/server.d.ts.map +1 -1
  74. package/dist/client/blocks/server.js +1 -0
  75. package/dist/client/blocks/server.js.map +1 -1
  76. package/dist/client/blocks/types.d.ts +1 -1
  77. package/dist/client/blocks/types.js.map +1 -1
  78. package/dist/client/extensions/ExtensionsListPage.d.ts.map +1 -1
  79. package/dist/client/extensions/ExtensionsListPage.js +28 -13
  80. package/dist/client/extensions/ExtensionsListPage.js.map +1 -1
  81. package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
  82. package/dist/client/extensions/ExtensionsSidebarSection.js +31 -9
  83. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  84. package/dist/client/rich-markdown-editor/CodeBlockNode.d.ts +49 -0
  85. package/dist/client/rich-markdown-editor/CodeBlockNode.d.ts.map +1 -0
  86. package/dist/client/rich-markdown-editor/CodeBlockNode.js +126 -0
  87. package/dist/client/rich-markdown-editor/CodeBlockNode.js.map +1 -0
  88. package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts.map +1 -1
  89. package/dist/client/rich-markdown-editor/RegistryBlockNode.js +26 -3
  90. package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
  91. package/dist/client/rich-markdown-editor/RichMarkdownEditor.d.ts +1 -1
  92. package/dist/client/rich-markdown-editor/extensions.d.ts.map +1 -1
  93. package/dist/client/rich-markdown-editor/extensions.js +8 -8
  94. package/dist/client/rich-markdown-editor/extensions.js.map +1 -1
  95. package/dist/client/rich-markdown-editor/index.d.ts +1 -0
  96. package/dist/client/rich-markdown-editor/index.d.ts.map +1 -1
  97. package/dist/client/rich-markdown-editor/index.js +1 -0
  98. package/dist/client/rich-markdown-editor/index.js.map +1 -1
  99. package/dist/client/rich-markdown-editor/registrySlashCommands.d.ts.map +1 -1
  100. package/dist/client/rich-markdown-editor/registrySlashCommands.js +1 -0
  101. package/dist/client/rich-markdown-editor/registrySlashCommands.js.map +1 -1
  102. package/dist/extensions/actions.d.ts.map +1 -1
  103. package/dist/extensions/actions.js +63 -2
  104. package/dist/extensions/actions.js.map +1 -1
  105. package/dist/extensions/routes.d.ts.map +1 -1
  106. package/dist/extensions/routes.js +24 -3
  107. package/dist/extensions/routes.js.map +1 -1
  108. package/dist/extensions/schema.d.ts +43 -2
  109. package/dist/extensions/schema.d.ts.map +1 -1
  110. package/dist/extensions/schema.js +12 -0
  111. package/dist/extensions/schema.js.map +1 -1
  112. package/dist/extensions/store.d.ts +20 -0
  113. package/dist/extensions/store.d.ts.map +1 -1
  114. package/dist/extensions/store.js +82 -3
  115. package/dist/extensions/store.js.map +1 -1
  116. package/dist/server/auth.d.ts.map +1 -1
  117. package/dist/server/auth.js +13 -0
  118. package/dist/server/auth.js.map +1 -1
  119. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  120. package/dist/server/core-routes-plugin.js +11 -0
  121. package/dist/server/core-routes-plugin.js.map +1 -1
  122. package/dist/server/recap-image-route.d.ts +8 -0
  123. package/dist/server/recap-image-route.d.ts.map +1 -0
  124. package/dist/server/recap-image-route.js +200 -0
  125. package/dist/server/recap-image-route.js.map +1 -0
  126. package/dist/server/recap-image-store.d.ts +41 -0
  127. package/dist/server/recap-image-store.d.ts.map +1 -0
  128. package/dist/server/recap-image-store.js +138 -0
  129. package/dist/server/recap-image-store.js.map +1 -0
  130. package/dist/styles/rich-markdown-editor.css +66 -17
  131. package/dist/templates/default/pnpm-workspace.yaml +7 -0
  132. package/dist/templates/workspace-root/package.json +0 -5
  133. package/dist/templates/workspace-root/pnpm-workspace.yaml +14 -0
  134. package/docs/content/cloneable-saas.md +10 -0
  135. package/docs/content/external-agents.md +4 -7
  136. package/docs/content/faq.md +10 -0
  137. package/docs/content/getting-started.md +11 -0
  138. package/docs/content/pr-visual-recap.md +103 -0
  139. package/docs/content/skills-guide.md +1 -3
  140. package/docs/content/template-assets.md +1 -4
  141. package/docs/content/template-design.md +0 -57
  142. package/docs/content/template-plan.md +22 -18
  143. package/docs/content/visual-plans.md +10 -7
  144. package/docs/content/what-is-agent-native.md +2 -0
  145. package/package.json +5 -1
  146. package/src/templates/default/pnpm-workspace.yaml +7 -0
  147. package/src/templates/workspace-root/package.json +0 -5
  148. package/src/templates/workspace-root/pnpm-workspace.yaml +14 -0
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Combined handler for the recap-image routes. Mount as a PREFIX handler at
3
+ * `/_agent-native/recap-image`; the framework strips the mount prefix, so:
4
+ * - `event.url.pathname === "/"` → POST upload (authenticated)
5
+ * - `event.url.pathname === "/<token>.png"` → GET/HEAD serve (anonymous)
6
+ */
7
+ export declare function createRecapImageHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<unknown>>;
8
+ //# sourceMappingURL=recap-image-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recap-image-route.d.ts","sourceRoot":"","sources":["../../src/server/recap-image-route.ts"],"names":[],"mappings":"AA+MA;;;;;GAKG;AACH,wBAAgB,uBAAuB,2FAoBtC"}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Routes for signed, content-only recap PNG images.
3
+ *
4
+ * POST /_agent-native/recap-image
5
+ * Auth: `Authorization: Bearer <token>` — accepts the SAME tokens the MCP /
6
+ * action surface accepts: a legacy `sessions` bearer (desktop/native) OR a
7
+ * connect-minted MCP OAuth access token (the `agent-native connect` token,
8
+ * audience-bound to this app's `{origin}/_agent-native/mcp` resource). A
9
+ * normal browser session cookie is also accepted. Rejects unauthenticated
10
+ * callers with 401.
11
+ * Body: raw `image/png` bytes, or JSON `{ "pngBase64": "..." }`. Capped at
12
+ * ~5 MB. Stores the PNG and returns `{ imageUrl: "<origin>/_agent-native/
13
+ * recap-image/<token>.png" }`.
14
+ *
15
+ * GET /_agent-native/recap-image/<token>.png
16
+ * ANONYMOUS (no auth) so GitHub's camo image proxy can fetch it into a
17
+ * private-repo PR comment. Returns the stored PNG with a strict
18
+ * `Content-Type: image/png` and a long immutable cache header. 404 on an
19
+ * unknown/malformed token. Only ever serves opaque image bytes — no plan
20
+ * data leaks through this route.
21
+ */
22
+ import { defineEventHandler, getHeader, getMethod, readRawBody, setResponseHeader, setResponseStatus, } from "h3";
23
+ import { getSession } from "./auth.js";
24
+ import { getAppUrl } from "./google-oauth.js";
25
+ import { RECAP_IMAGE_CONTENT_TYPE, RECAP_IMAGE_MAX_BYTES, getRecapImage, isValidRecapImageToken, saveRecapImage, } from "./recap-image-store.js";
26
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
27
+ /** Long immutable cache — the bytes for a given token never change. */
28
+ const RECAP_IMAGE_CACHE_CONTROL = "public, max-age=31536000, immutable, stale-while-revalidate=604800, stale-if-error=86400";
29
+ function isPngBuffer(buf) {
30
+ return (buf.byteLength >= PNG_MAGIC.byteLength &&
31
+ buf.subarray(0, 8).equals(PNG_MAGIC));
32
+ }
33
+ /**
34
+ * Resolve a session for the upload route. Reuses the SAME acceptance the MCP /
35
+ * action surface uses:
36
+ * 1. `getSession(event)` — browser cookie, ACCESS_TOKEN, and legacy bearer
37
+ * (`sessions` table) tokens.
38
+ * 2. A connect-minted MCP OAuth access token, verified through the MCP
39
+ * surface's canonical `verifyAuth` with this app's MCP resource as the
40
+ * expected audience and `allowDevOpen: false`. `getSession` only honors
41
+ * this token on the `/_agent-native/actions/*` surface, so we mirror that
42
+ * verification here for the recap-image upload route.
43
+ */
44
+ async function resolveUploadSession(event) {
45
+ const session = await getSession(event).catch(() => null);
46
+ if (session?.email)
47
+ return session;
48
+ // Trim once and reuse the trimmed value everywhere. Match verifyAuth's check
49
+ // exactly — a literal, case-sensitive `Bearer ` prefix (not `/i`, not `\s+`) —
50
+ // so this pre-check never accepts a header that verifyAuth would then reject
51
+ // (e.g. lowercase `bearer` or a tab separator).
52
+ const authHeader = getHeader(event, "authorization")?.trim();
53
+ if (!authHeader || !/^Bearer \S/.test(authHeader))
54
+ return null;
55
+ try {
56
+ const [{ getMcpOAuthResource }, { verifyAuth, resolveOrgIdFromDomain }] = await Promise.all([
57
+ import("../mcp/oauth-route.js"),
58
+ import("../mcp/build-server.js"),
59
+ ]);
60
+ const result = await verifyAuth(authHeader, undefined, {
61
+ resourceUrl: getMcpOAuthResource(event),
62
+ allowDevOpen: false,
63
+ });
64
+ const identity = result.authed ? result.identity : undefined;
65
+ if (!identity?.userEmail)
66
+ return null;
67
+ const orgId = identity.orgId ?? (await resolveOrgIdFromDomain(identity.orgDomain));
68
+ return {
69
+ email: identity.userEmail,
70
+ token: authHeader.replace(/^Bearer /, "").trim(),
71
+ ...(orgId ? { orgId } : {}),
72
+ };
73
+ }
74
+ catch (error) {
75
+ console.error("[recap-image] bearer verification error:", error);
76
+ return null;
77
+ }
78
+ }
79
+ /**
80
+ * Extract PNG bytes from the request. Supports raw `image/png` bytes and JSON
81
+ * `{ pngBase64 }`. Returns `null` on a malformed/oversized/non-PNG payload.
82
+ */
83
+ async function readPngFromRequest(event) {
84
+ const raw = await readRawBody(event, false).catch(() => undefined);
85
+ if (!raw || raw.byteLength === 0)
86
+ return null;
87
+ if (raw.byteLength > RECAP_IMAGE_MAX_BYTES)
88
+ return null;
89
+ const contentType = (getHeader(event, "content-type") || "").toLowerCase();
90
+ if (contentType.includes("application/json")) {
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(raw.toString("utf8"));
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ const base64 = parsed?.pngBase64;
99
+ if (typeof base64 !== "string" || !base64)
100
+ return null;
101
+ let bytes;
102
+ try {
103
+ bytes = Buffer.from(base64, "base64");
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ if (bytes.byteLength === 0 || bytes.byteLength > RECAP_IMAGE_MAX_BYTES) {
109
+ return null;
110
+ }
111
+ return isPngBuffer(bytes) ? bytes : null;
112
+ }
113
+ // Default: treat the raw body as PNG bytes (image/png or unspecified).
114
+ return isPngBuffer(raw) ? raw : null;
115
+ }
116
+ /** POST /_agent-native/recap-image — authenticated upload. */
117
+ async function handleUpload(event) {
118
+ const session = await resolveUploadSession(event);
119
+ if (!session?.email) {
120
+ setResponseStatus(event, 401);
121
+ return { error: "Authentication required" };
122
+ }
123
+ const png = await readPngFromRequest(event);
124
+ if (!png) {
125
+ setResponseStatus(event, 400);
126
+ return {
127
+ error: "Expected a PNG image (Content-Type: image/png raw bytes, or JSON { pngBase64 }), at most 5 MB.",
128
+ };
129
+ }
130
+ try {
131
+ const { token } = await saveRecapImage(png, { ownerEmail: session.email });
132
+ const imageUrl = getAppUrl(event, `/_agent-native/recap-image/${token}.png`);
133
+ setResponseStatus(event, 201);
134
+ return { imageUrl };
135
+ }
136
+ catch (error) {
137
+ console.error("[recap-image] failed to store image:", error);
138
+ setResponseStatus(event, 500);
139
+ return { error: "Failed to store recap image" };
140
+ }
141
+ }
142
+ /** GET/HEAD /_agent-native/recap-image/<token>.png — anonymous, content-only. */
143
+ async function handleServe(event, segment) {
144
+ // Require the strict `<hex>.png` shape — no directory traversal, no
145
+ // alternate extensions, no extra path segments.
146
+ const match = /^([0-9a-f]+)\.png$/i.exec(segment);
147
+ const token = match?.[1]?.toLowerCase() ?? "";
148
+ if (!isValidRecapImageToken(token)) {
149
+ setResponseStatus(event, 404);
150
+ return { error: "Not found" };
151
+ }
152
+ const stored = await getRecapImage(token).catch(() => null);
153
+ if (!stored) {
154
+ setResponseStatus(event, 404);
155
+ return { error: "Not found" };
156
+ }
157
+ // Strict image/png on read regardless of what was stored, plus a long
158
+ // immutable cache and a cross-origin policy so the camo proxy can fetch it.
159
+ const headers = {
160
+ "Content-Type": RECAP_IMAGE_CONTENT_TYPE,
161
+ "Cache-Control": RECAP_IMAGE_CACHE_CONTROL,
162
+ "CDN-Cache-Control": RECAP_IMAGE_CACHE_CONTROL,
163
+ "Cross-Origin-Resource-Policy": "cross-origin",
164
+ "Content-Length": String(stored.bytes.byteLength),
165
+ };
166
+ for (const [name, value] of Object.entries(headers)) {
167
+ setResponseHeader(event, name, value);
168
+ }
169
+ if (getMethod(event) === "HEAD")
170
+ return "";
171
+ const body = new ArrayBuffer(stored.bytes.byteLength);
172
+ new Uint8Array(body).set(stored.bytes);
173
+ return new Response(body, { headers });
174
+ }
175
+ /**
176
+ * Combined handler for the recap-image routes. Mount as a PREFIX handler at
177
+ * `/_agent-native/recap-image`; the framework strips the mount prefix, so:
178
+ * - `event.url.pathname === "/"` → POST upload (authenticated)
179
+ * - `event.url.pathname === "/<token>.png"` → GET/HEAD serve (anonymous)
180
+ */
181
+ export function createRecapImageHandler() {
182
+ return defineEventHandler(async (event) => {
183
+ const segment = (event.url?.pathname || "").replace(/^\/+/, "").split("/")[0] || "";
184
+ const method = getMethod(event);
185
+ if (!segment) {
186
+ if (method === "POST")
187
+ return handleUpload(event);
188
+ setResponseStatus(event, 405);
189
+ setResponseHeader(event, "Allow", "POST");
190
+ return { error: "Method not allowed" };
191
+ }
192
+ if (method === "GET" || method === "HEAD") {
193
+ return handleServe(event, segment);
194
+ }
195
+ setResponseStatus(event, 405);
196
+ setResponseHeader(event, "Allow", "GET, HEAD");
197
+ return { error: "Method not allowed" };
198
+ });
199
+ }
200
+ //# sourceMappingURL=recap-image-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recap-image-route.js","sourceRoot":"","sources":["../../src/server/recap-image-route.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Routes for signed, content-only recap PNG images.\n *\n * POST /_agent-native/recap-image\n * Auth: `Authorization: Bearer <token>` — accepts the SAME tokens the MCP /\n * action surface accepts: a legacy `sessions` bearer (desktop/native) OR a\n * connect-minted MCP OAuth access token (the `agent-native connect` token,\n * audience-bound to this app's `{origin}/_agent-native/mcp` resource). A\n * normal browser session cookie is also accepted. Rejects unauthenticated\n * callers with 401.\n * Body: raw `image/png` bytes, or JSON `{ \"pngBase64\": \"...\" }`. Capped at\n * ~5 MB. Stores the PNG and returns `{ imageUrl: \"<origin>/_agent-native/\n * recap-image/<token>.png\" }`.\n *\n * GET /_agent-native/recap-image/<token>.png\n * ANONYMOUS (no auth) so GitHub's camo image proxy can fetch it into a\n * private-repo PR comment. Returns the stored PNG with a strict\n * `Content-Type: image/png` and a long immutable cache header. 404 on an\n * unknown/malformed token. Only ever serves opaque image bytes — no plan\n * data leaks through this route.\n */\nimport {\n defineEventHandler,\n getHeader,\n getMethod,\n readRawBody,\n setResponseHeader,\n setResponseStatus,\n type H3Event,\n} from \"h3\";\nimport { getSession, type AuthSession } from \"./auth.js\";\nimport { getAppUrl } from \"./google-oauth.js\";\nimport {\n RECAP_IMAGE_CONTENT_TYPE,\n RECAP_IMAGE_MAX_BYTES,\n getRecapImage,\n isValidRecapImageToken,\n saveRecapImage,\n} from \"./recap-image-store.js\";\n\nconst PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);\n\n/** Long immutable cache — the bytes for a given token never change. */\nconst RECAP_IMAGE_CACHE_CONTROL =\n \"public, max-age=31536000, immutable, stale-while-revalidate=604800, stale-if-error=86400\";\n\nfunction isPngBuffer(buf: Buffer): boolean {\n return (\n buf.byteLength >= PNG_MAGIC.byteLength &&\n buf.subarray(0, 8).equals(PNG_MAGIC)\n );\n}\n\n/**\n * Resolve a session for the upload route. Reuses the SAME acceptance the MCP /\n * action surface uses:\n * 1. `getSession(event)` — browser cookie, ACCESS_TOKEN, and legacy bearer\n * (`sessions` table) tokens.\n * 2. A connect-minted MCP OAuth access token, verified through the MCP\n * surface's canonical `verifyAuth` with this app's MCP resource as the\n * expected audience and `allowDevOpen: false`. `getSession` only honors\n * this token on the `/_agent-native/actions/*` surface, so we mirror that\n * verification here for the recap-image upload route.\n */\nasync function resolveUploadSession(\n event: H3Event,\n): Promise<AuthSession | null> {\n const session = await getSession(event).catch(() => null);\n if (session?.email) return session;\n\n // Trim once and reuse the trimmed value everywhere. Match verifyAuth's check\n // exactly — a literal, case-sensitive `Bearer ` prefix (not `/i`, not `\\s+`) —\n // so this pre-check never accepts a header that verifyAuth would then reject\n // (e.g. lowercase `bearer` or a tab separator).\n const authHeader = getHeader(event, \"authorization\")?.trim();\n if (!authHeader || !/^Bearer \\S/.test(authHeader)) return null;\n\n try {\n const [{ getMcpOAuthResource }, { verifyAuth, resolveOrgIdFromDomain }] =\n await Promise.all([\n import(\"../mcp/oauth-route.js\"),\n import(\"../mcp/build-server.js\"),\n ]);\n const result = await verifyAuth(authHeader, undefined, {\n resourceUrl: getMcpOAuthResource(event),\n allowDevOpen: false,\n });\n const identity = result.authed ? result.identity : undefined;\n if (!identity?.userEmail) return null;\n const orgId =\n identity.orgId ?? (await resolveOrgIdFromDomain(identity.orgDomain));\n return {\n email: identity.userEmail,\n token: authHeader.replace(/^Bearer /, \"\").trim(),\n ...(orgId ? { orgId } : {}),\n };\n } catch (error) {\n console.error(\"[recap-image] bearer verification error:\", error);\n return null;\n }\n}\n\n/**\n * Extract PNG bytes from the request. Supports raw `image/png` bytes and JSON\n * `{ pngBase64 }`. Returns `null` on a malformed/oversized/non-PNG payload.\n */\nasync function readPngFromRequest(event: H3Event): Promise<Buffer | null> {\n const raw = await readRawBody(event, false).catch(() => undefined);\n if (!raw || raw.byteLength === 0) return null;\n if (raw.byteLength > RECAP_IMAGE_MAX_BYTES) return null;\n\n const contentType = (getHeader(event, \"content-type\") || \"\").toLowerCase();\n\n if (contentType.includes(\"application/json\")) {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw.toString(\"utf8\"));\n } catch {\n return null;\n }\n const base64 = (parsed as { pngBase64?: unknown })?.pngBase64;\n if (typeof base64 !== \"string\" || !base64) return null;\n let bytes: Buffer;\n try {\n bytes = Buffer.from(base64, \"base64\");\n } catch {\n return null;\n }\n if (bytes.byteLength === 0 || bytes.byteLength > RECAP_IMAGE_MAX_BYTES) {\n return null;\n }\n return isPngBuffer(bytes) ? bytes : null;\n }\n\n // Default: treat the raw body as PNG bytes (image/png or unspecified).\n return isPngBuffer(raw) ? raw : null;\n}\n\n/** POST /_agent-native/recap-image — authenticated upload. */\nasync function handleUpload(event: H3Event): Promise<unknown> {\n const session = await resolveUploadSession(event);\n if (!session?.email) {\n setResponseStatus(event, 401);\n return { error: \"Authentication required\" };\n }\n\n const png = await readPngFromRequest(event);\n if (!png) {\n setResponseStatus(event, 400);\n return {\n error:\n \"Expected a PNG image (Content-Type: image/png raw bytes, or JSON { pngBase64 }), at most 5 MB.\",\n };\n }\n\n try {\n const { token } = await saveRecapImage(png, { ownerEmail: session.email });\n const imageUrl = getAppUrl(\n event,\n `/_agent-native/recap-image/${token}.png`,\n );\n setResponseStatus(event, 201);\n return { imageUrl };\n } catch (error) {\n console.error(\"[recap-image] failed to store image:\", error);\n setResponseStatus(event, 500);\n return { error: \"Failed to store recap image\" };\n }\n}\n\n/** GET/HEAD /_agent-native/recap-image/<token>.png — anonymous, content-only. */\nasync function handleServe(event: H3Event, segment: string): Promise<unknown> {\n // Require the strict `<hex>.png` shape — no directory traversal, no\n // alternate extensions, no extra path segments.\n const match = /^([0-9a-f]+)\\.png$/i.exec(segment);\n const token = match?.[1]?.toLowerCase() ?? \"\";\n if (!isValidRecapImageToken(token)) {\n setResponseStatus(event, 404);\n return { error: \"Not found\" };\n }\n\n const stored = await getRecapImage(token).catch(() => null);\n if (!stored) {\n setResponseStatus(event, 404);\n return { error: \"Not found\" };\n }\n\n // Strict image/png on read regardless of what was stored, plus a long\n // immutable cache and a cross-origin policy so the camo proxy can fetch it.\n const headers: Record<string, string> = {\n \"Content-Type\": RECAP_IMAGE_CONTENT_TYPE,\n \"Cache-Control\": RECAP_IMAGE_CACHE_CONTROL,\n \"CDN-Cache-Control\": RECAP_IMAGE_CACHE_CONTROL,\n \"Cross-Origin-Resource-Policy\": \"cross-origin\",\n \"Content-Length\": String(stored.bytes.byteLength),\n };\n for (const [name, value] of Object.entries(headers)) {\n setResponseHeader(event, name, value);\n }\n\n if (getMethod(event) === \"HEAD\") return \"\";\n\n const body = new ArrayBuffer(stored.bytes.byteLength);\n new Uint8Array(body).set(stored.bytes);\n return new Response(body, { headers });\n}\n\n/**\n * Combined handler for the recap-image routes. Mount as a PREFIX handler at\n * `/_agent-native/recap-image`; the framework strips the mount prefix, so:\n * - `event.url.pathname === \"/\"` → POST upload (authenticated)\n * - `event.url.pathname === \"/<token>.png\"` → GET/HEAD serve (anonymous)\n */\nexport function createRecapImageHandler() {\n return defineEventHandler(async (event: H3Event) => {\n const segment =\n (event.url?.pathname || \"\").replace(/^\\/+/, \"\").split(\"/\")[0] || \"\";\n const method = getMethod(event);\n\n if (!segment) {\n if (method === \"POST\") return handleUpload(event);\n setResponseStatus(event, 405);\n setResponseHeader(event, \"Allow\", \"POST\");\n return { error: \"Method not allowed\" };\n }\n\n if (method === \"GET\" || method === \"HEAD\") {\n return handleServe(event, segment);\n }\n setResponseStatus(event, 405);\n setResponseHeader(event, \"Allow\", \"GET, HEAD\");\n return { error: \"Method not allowed\" };\n });\n}\n"]}
@@ -0,0 +1,41 @@
1
+ /** Maximum stored image size (~5 MB of raw PNG bytes). */
2
+ export declare const RECAP_IMAGE_MAX_BYTES: number;
3
+ /** Only `image/png` is ever stored or served. */
4
+ export declare const RECAP_IMAGE_CONTENT_TYPE = "image/png";
5
+ /**
6
+ * Stored recap images older than this are pruned on the next write (30 days).
7
+ * Each PR push uploads a fresh screenshot under a new token; without expiry the
8
+ * table — and the set of anonymously-fetchable image URLs — would grow without
9
+ * bound. 30 days comfortably outlives any PR's review window.
10
+ */
11
+ export declare const RECAP_IMAGE_TTL_MS: number;
12
+ export declare function ensureRecapImageTable(): Promise<void>;
13
+ /**
14
+ * Delete recap images older than {@link RECAP_IMAGE_TTL_MS}. Called best-effort
15
+ * after each write so the table stays bounded. Returns the number of rows
16
+ * removed. Dialect-agnostic (a plain `DELETE ... WHERE created_at < ?`).
17
+ */
18
+ export declare function pruneExpiredRecapImages(now?: number): Promise<number>;
19
+ /** Generate a long, unguessable lowercase-hex token (default 32 bytes → 64 hex chars). */
20
+ export declare function generateRecapImageToken(byteLength?: number): string;
21
+ /** True when `token` matches the strict hex token format (no traversal characters). */
22
+ export declare function isValidRecapImageToken(token: string | undefined | null): boolean;
23
+ export interface StoredRecapImage {
24
+ bytes: Buffer;
25
+ contentType: string;
26
+ }
27
+ /**
28
+ * Store PNG bytes and return the freshly minted token. Caller is responsible
29
+ * for enforcing the size cap before calling (we re-check defensively here too).
30
+ */
31
+ export declare function saveRecapImage(png: Buffer, options?: {
32
+ ownerEmail?: string | null;
33
+ }): Promise<{
34
+ token: string;
35
+ }>;
36
+ /**
37
+ * Load a stored image by token. Returns `null` for an unknown or malformed
38
+ * token. Never returns anything but the opaque image bytes — no plan data.
39
+ */
40
+ export declare function getRecapImage(token: string): Promise<StoredRecapImage | null>;
41
+ //# sourceMappingURL=recap-image-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recap-image-store.d.ts","sourceRoot":"","sources":["../../src/server/recap-image-store.ts"],"names":[],"mappings":"AAsBA,0DAA0D;AAC1D,eAAO,MAAM,qBAAqB,QAAkB,CAAC;AAErD,iDAAiD;AACjD,eAAO,MAAM,wBAAwB,cAAc,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAA2B,CAAC;AAW3D,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAuB3D;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,GAAE,MAAmB,GACvB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,0FAA0F;AAC1F,wBAAgB,uBAAuB,CAAC,UAAU,SAAK,GAAG,MAAM,CAE/D;AAED,uFAAuF;AACvF,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAC/B,OAAO,CAET;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAC3C,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAwB5B;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAqBlC"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * SQL persistence for signed, content-only recap PNG images.
3
+ *
4
+ * The PR visual-recap GitHub Action renders a recap plan to a PNG and uploads
5
+ * it through `POST /_agent-native/recap-image` (authenticated with the same
6
+ * `agent-native connect` bearer token the MCP / action surface accepts). The
7
+ * stored bytes are then served anonymously from
8
+ * `GET /_agent-native/recap-image/<token>.png` so GitHub's camo image proxy can
9
+ * fetch them into a (private-repo) PR comment without a login. The interactive
10
+ * plan itself stays login-gated; this store only ever holds opaque image bytes
11
+ * keyed by a long, unguessable token.
12
+ *
13
+ * Follows the same raw-SQL pattern as observability/store.ts and usage/store.ts
14
+ * — framework-owned tables use `getDbExec()` with dialect-agnostic
15
+ * `CREATE TABLE IF NOT EXISTS` DDL (additive only; never drops/renames/alters)
16
+ * rather than Drizzle ORM, which is reserved for template-level schemas. The
17
+ * PNG is stored as base64 TEXT so it is portable across SQLite, Neon/Postgres,
18
+ * libSQL/Turso, and D1 without per-dialect blob/bytea handling.
19
+ */
20
+ import { randomBytes } from "node:crypto";
21
+ import { getDbExec, intType, retryOnDdlRace } from "../db/client.js";
22
+ /** Maximum stored image size (~5 MB of raw PNG bytes). */
23
+ export const RECAP_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
24
+ /** Only `image/png` is ever stored or served. */
25
+ export const RECAP_IMAGE_CONTENT_TYPE = "image/png";
26
+ /**
27
+ * Stored recap images older than this are pruned on the next write (30 days).
28
+ * Each PR push uploads a fresh screenshot under a new token; without expiry the
29
+ * table — and the set of anonymously-fetchable image URLs — would grow without
30
+ * bound. 30 days comfortably outlives any PR's review window.
31
+ */
32
+ export const RECAP_IMAGE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
33
+ /**
34
+ * Token format for the public `<token>.png` path. Hex-only so it can never
35
+ * contain a path separator, `.`, or `..` — no directory traversal is possible
36
+ * via the token path param.
37
+ */
38
+ const TOKEN_PATTERN = /^[0-9a-f]{32,128}$/;
39
+ let _initPromise;
40
+ export async function ensureRecapImageTable() {
41
+ if (!_initPromise) {
42
+ _initPromise = (async () => {
43
+ const client = getDbExec();
44
+ await retryOnDdlRace(() => client.execute(`
45
+ CREATE TABLE IF NOT EXISTS recap_images (
46
+ token TEXT PRIMARY KEY,
47
+ png_base64 TEXT NOT NULL,
48
+ content_type TEXT NOT NULL DEFAULT '${RECAP_IMAGE_CONTENT_TYPE}',
49
+ byte_length ${intType()} NOT NULL DEFAULT 0,
50
+ owner_email TEXT,
51
+ created_at ${intType()} NOT NULL
52
+ )
53
+ `));
54
+ })().catch((error) => {
55
+ // Allow a later call to retry if the first init lost a DDL race.
56
+ _initPromise = undefined;
57
+ throw error;
58
+ });
59
+ }
60
+ return _initPromise;
61
+ }
62
+ /**
63
+ * Delete recap images older than {@link RECAP_IMAGE_TTL_MS}. Called best-effort
64
+ * after each write so the table stays bounded. Returns the number of rows
65
+ * removed. Dialect-agnostic (a plain `DELETE ... WHERE created_at < ?`).
66
+ */
67
+ export async function pruneExpiredRecapImages(now = Date.now()) {
68
+ await ensureRecapImageTable();
69
+ const client = getDbExec();
70
+ const { rowsAffected } = await client.execute({
71
+ sql: `DELETE FROM recap_images WHERE created_at < ?`,
72
+ args: [now - RECAP_IMAGE_TTL_MS],
73
+ });
74
+ return rowsAffected;
75
+ }
76
+ /** Generate a long, unguessable lowercase-hex token (default 32 bytes → 64 hex chars). */
77
+ export function generateRecapImageToken(byteLength = 32) {
78
+ return randomBytes(byteLength).toString("hex");
79
+ }
80
+ /** True when `token` matches the strict hex token format (no traversal characters). */
81
+ export function isValidRecapImageToken(token) {
82
+ return typeof token === "string" && TOKEN_PATTERN.test(token);
83
+ }
84
+ /**
85
+ * Store PNG bytes and return the freshly minted token. Caller is responsible
86
+ * for enforcing the size cap before calling (we re-check defensively here too).
87
+ */
88
+ export async function saveRecapImage(png, options = {}) {
89
+ if (png.byteLength > RECAP_IMAGE_MAX_BYTES) {
90
+ throw new Error("recap image exceeds maximum size");
91
+ }
92
+ await ensureRecapImageTable();
93
+ const client = getDbExec();
94
+ const token = generateRecapImageToken();
95
+ await client.execute({
96
+ sql: `INSERT INTO recap_images
97
+ (token, png_base64, content_type, byte_length, owner_email, created_at)
98
+ VALUES (?, ?, ?, ?, ?, ?)`,
99
+ args: [
100
+ token,
101
+ png.toString("base64"),
102
+ RECAP_IMAGE_CONTENT_TYPE,
103
+ png.byteLength,
104
+ options.ownerEmail ?? null,
105
+ Date.now(),
106
+ ],
107
+ });
108
+ // Best-effort retention: expire old images so the table and the set of public
109
+ // image URLs stay bounded. Never let a cleanup failure fail the upload.
110
+ await pruneExpiredRecapImages().catch(() => { });
111
+ return { token };
112
+ }
113
+ /**
114
+ * Load a stored image by token. Returns `null` for an unknown or malformed
115
+ * token. Never returns anything but the opaque image bytes — no plan data.
116
+ */
117
+ export async function getRecapImage(token) {
118
+ if (!isValidRecapImageToken(token))
119
+ return null;
120
+ await ensureRecapImageTable();
121
+ const client = getDbExec();
122
+ const { rows } = await client.execute({
123
+ sql: `SELECT png_base64, content_type FROM recap_images WHERE token = ? LIMIT 1`,
124
+ args: [token],
125
+ });
126
+ const row = rows[0];
127
+ if (!row || typeof row.png_base64 !== "string")
128
+ return null;
129
+ return {
130
+ bytes: Buffer.from(row.png_base64, "base64"),
131
+ // Stored content type is always image/png; never trust it for response
132
+ // headers — the route hard-codes image/png — but surface it for callers.
133
+ contentType: typeof row.content_type === "string"
134
+ ? row.content_type
135
+ : RECAP_IMAGE_CONTENT_TYPE,
136
+ };
137
+ }
138
+ //# sourceMappingURL=recap-image-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recap-image-store.js","sourceRoot":"","sources":["../../src/server/recap-image-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAErE,0DAA0D;AAC1D,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAErD,iDAAiD;AACjD,MAAM,CAAC,MAAM,wBAAwB,GAAG,WAAW,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3D;;;;GAIG;AACH,MAAM,aAAa,GAAG,oBAAoB,CAAC;AAE3C,IAAI,YAAuC,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,qBAAqB;IACzC,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,YAAY,GAAG,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;YAC3B,MAAM,cAAc,CAAC,GAAG,EAAE,CACxB,MAAM,CAAC,OAAO,CAAC;;;;gDAIyB,wBAAwB;wBAChD,OAAO,EAAE;;uBAEV,OAAO,EAAE;;OAEzB,CAAC,CACD,CAAC;QACJ,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACnB,iEAAiE;YACjE,YAAY,GAAG,SAAS,CAAC;YACzB,MAAM,KAAK,CAAC;QACd,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,MAAM,qBAAqB,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QAC5C,GAAG,EAAE,+CAA+C;QACpD,IAAI,EAAE,CAAC,GAAG,GAAG,kBAAkB,CAAC;KACjC,CAAC,CAAC;IACH,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,uBAAuB,CAAC,UAAU,GAAG,EAAE;IACrD,OAAO,WAAW,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACjD,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,sBAAsB,CACpC,KAAgC;IAEhC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAOD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAW,EACX,UAA0C,EAAE;IAE5C,IAAI,GAAG,CAAC,UAAU,GAAG,qBAAqB,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACtD,CAAC;IACD,MAAM,qBAAqB,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,uBAAuB,EAAE,CAAC;IACxC,MAAM,MAAM,CAAC,OAAO,CAAC;QACnB,GAAG,EAAE;;gCAEuB;QAC5B,IAAI,EAAE;YACJ,KAAK;YACL,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACtB,wBAAwB;YACxB,GAAG,CAAC,UAAU;YACd,OAAO,CAAC,UAAU,IAAI,IAAI;YAC1B,IAAI,CAAC,GAAG,EAAE;SACX;KACF,CAAC,CAAC;IACH,8EAA8E;IAC9E,wEAAwE;IACxE,MAAM,uBAAuB,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAChD,OAAO,EAAE,KAAK,EAAE,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAa;IAEb,IAAI,CAAC,sBAAsB,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAChD,MAAM,qBAAqB,EAAE,CAAC;IAC9B,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC;QACpC,GAAG,EAAE,2EAA2E;QAChF,IAAI,EAAE,CAAC,KAAK,CAAC;KACd,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAEL,CAAC;IACd,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC5D,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC;QAC5C,uEAAuE;QACvE,yEAAyE;QACzE,WAAW,EACT,OAAO,GAAG,CAAC,YAAY,KAAK,QAAQ;YAClC,CAAC,CAAC,GAAG,CAAC,YAAY;YAClB,CAAC,CAAC,wBAAwB;KAC/B,CAAC;AACJ,CAAC","sourcesContent":["/**\n * SQL persistence for signed, content-only recap PNG images.\n *\n * The PR visual-recap GitHub Action renders a recap plan to a PNG and uploads\n * it through `POST /_agent-native/recap-image` (authenticated with the same\n * `agent-native connect` bearer token the MCP / action surface accepts). The\n * stored bytes are then served anonymously from\n * `GET /_agent-native/recap-image/<token>.png` so GitHub's camo image proxy can\n * fetch them into a (private-repo) PR comment without a login. The interactive\n * plan itself stays login-gated; this store only ever holds opaque image bytes\n * keyed by a long, unguessable token.\n *\n * Follows the same raw-SQL pattern as observability/store.ts and usage/store.ts\n * — framework-owned tables use `getDbExec()` with dialect-agnostic\n * `CREATE TABLE IF NOT EXISTS` DDL (additive only; never drops/renames/alters)\n * rather than Drizzle ORM, which is reserved for template-level schemas. The\n * PNG is stored as base64 TEXT so it is portable across SQLite, Neon/Postgres,\n * libSQL/Turso, and D1 without per-dialect blob/bytea handling.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { getDbExec, intType, retryOnDdlRace } from \"../db/client.js\";\n\n/** Maximum stored image size (~5 MB of raw PNG bytes). */\nexport const RECAP_IMAGE_MAX_BYTES = 5 * 1024 * 1024;\n\n/** Only `image/png` is ever stored or served. */\nexport const RECAP_IMAGE_CONTENT_TYPE = \"image/png\";\n\n/**\n * Stored recap images older than this are pruned on the next write (30 days).\n * Each PR push uploads a fresh screenshot under a new token; without expiry the\n * table — and the set of anonymously-fetchable image URLs — would grow without\n * bound. 30 days comfortably outlives any PR's review window.\n */\nexport const RECAP_IMAGE_TTL_MS = 30 * 24 * 60 * 60 * 1000;\n\n/**\n * Token format for the public `<token>.png` path. Hex-only so it can never\n * contain a path separator, `.`, or `..` — no directory traversal is possible\n * via the token path param.\n */\nconst TOKEN_PATTERN = /^[0-9a-f]{32,128}$/;\n\nlet _initPromise: Promise<void> | undefined;\n\nexport async function ensureRecapImageTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n await retryOnDdlRace(() =>\n client.execute(`\n CREATE TABLE IF NOT EXISTS recap_images (\n token TEXT PRIMARY KEY,\n png_base64 TEXT NOT NULL,\n content_type TEXT NOT NULL DEFAULT '${RECAP_IMAGE_CONTENT_TYPE}',\n byte_length ${intType()} NOT NULL DEFAULT 0,\n owner_email TEXT,\n created_at ${intType()} NOT NULL\n )\n `),\n );\n })().catch((error) => {\n // Allow a later call to retry if the first init lost a DDL race.\n _initPromise = undefined;\n throw error;\n });\n }\n return _initPromise;\n}\n\n/**\n * Delete recap images older than {@link RECAP_IMAGE_TTL_MS}. Called best-effort\n * after each write so the table stays bounded. Returns the number of rows\n * removed. Dialect-agnostic (a plain `DELETE ... WHERE created_at < ?`).\n */\nexport async function pruneExpiredRecapImages(\n now: number = Date.now(),\n): Promise<number> {\n await ensureRecapImageTable();\n const client = getDbExec();\n const { rowsAffected } = await client.execute({\n sql: `DELETE FROM recap_images WHERE created_at < ?`,\n args: [now - RECAP_IMAGE_TTL_MS],\n });\n return rowsAffected;\n}\n\n/** Generate a long, unguessable lowercase-hex token (default 32 bytes → 64 hex chars). */\nexport function generateRecapImageToken(byteLength = 32): string {\n return randomBytes(byteLength).toString(\"hex\");\n}\n\n/** True when `token` matches the strict hex token format (no traversal characters). */\nexport function isValidRecapImageToken(\n token: string | undefined | null,\n): boolean {\n return typeof token === \"string\" && TOKEN_PATTERN.test(token);\n}\n\nexport interface StoredRecapImage {\n bytes: Buffer;\n contentType: string;\n}\n\n/**\n * Store PNG bytes and return the freshly minted token. Caller is responsible\n * for enforcing the size cap before calling (we re-check defensively here too).\n */\nexport async function saveRecapImage(\n png: Buffer,\n options: { ownerEmail?: string | null } = {},\n): Promise<{ token: string }> {\n if (png.byteLength > RECAP_IMAGE_MAX_BYTES) {\n throw new Error(\"recap image exceeds maximum size\");\n }\n await ensureRecapImageTable();\n const client = getDbExec();\n const token = generateRecapImageToken();\n await client.execute({\n sql: `INSERT INTO recap_images\n (token, png_base64, content_type, byte_length, owner_email, created_at)\n VALUES (?, ?, ?, ?, ?, ?)`,\n args: [\n token,\n png.toString(\"base64\"),\n RECAP_IMAGE_CONTENT_TYPE,\n png.byteLength,\n options.ownerEmail ?? null,\n Date.now(),\n ],\n });\n // Best-effort retention: expire old images so the table and the set of public\n // image URLs stay bounded. Never let a cleanup failure fail the upload.\n await pruneExpiredRecapImages().catch(() => {});\n return { token };\n}\n\n/**\n * Load a stored image by token. Returns `null` for an unknown or malformed\n * token. Never returns anything but the opaque image bytes — no plan data.\n */\nexport async function getRecapImage(\n token: string,\n): Promise<StoredRecapImage | null> {\n if (!isValidRecapImageToken(token)) return null;\n await ensureRecapImageTable();\n const client = getDbExec();\n const { rows } = await client.execute({\n sql: `SELECT png_base64, content_type FROM recap_images WHERE token = ? LIMIT 1`,\n args: [token],\n });\n const row = rows[0] as\n | { png_base64?: unknown; content_type?: unknown }\n | undefined;\n if (!row || typeof row.png_base64 !== \"string\") return null;\n return {\n bytes: Buffer.from(row.png_base64, \"base64\"),\n // Stored content type is always image/png; never trust it for response\n // headers — the route hard-codes image/png — but surface it for callers.\n contentType:\n typeof row.content_type === \"string\"\n ? row.content_type\n : RECAP_IMAGE_CONTENT_TYPE,\n };\n}\n"]}
@@ -128,10 +128,12 @@
128
128
  font-family:
129
129
  ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
130
130
  font-size: 0.86em;
131
- /* Fixed dark code surface (theme-independent) so the github-dark token palette
132
- below stays legible matches the read-side Shiki convention. */
133
- background: hsl(220 13% 11%);
134
- color: hsl(220 14% 90%);
131
+ /* Theme-aware code surface: follows the app's muted/foreground/border tokens
132
+ so block code respects light and dark mode (the token palette below is
133
+ tuned to read on a muted surface in both). */
134
+ background: hsl(var(--muted));
135
+ color: hsl(var(--foreground));
136
+ border: 1px solid hsl(var(--border));
135
137
  border-radius: 6px;
136
138
  padding: 0.8em 1em;
137
139
  margin: 0.55em 0;
@@ -147,53 +149,59 @@
147
149
  color: inherit;
148
150
  }
149
151
 
150
- /* highlight.js (lowlight) token palette — github-dark. Scoped to block code so
151
- inline `code` keeps its own background. No highlight.js base theme is imported;
152
- these rules are the entire theme, so the dark `pre` surface above is safe. */
152
+ /* highlight.js (lowlight) token palette. Scoped to block code so inline `code`
153
+ keeps its own background. No highlight.js base theme is imported; these rules
154
+ are the entire theme, tuned (mid-lightness HSL) to read on the muted surface
155
+ above in both light and dark mode. */
153
156
  .an-rich-md-prose pre code .hljs-comment,
154
157
  .an-rich-md-prose pre code .hljs-quote {
155
- color: #8b949e;
158
+ color: hsl(var(--muted-foreground));
156
159
  font-style: italic;
157
160
  }
158
161
  .an-rich-md-prose pre code .hljs-keyword,
159
162
  .an-rich-md-prose pre code .hljs-selector-tag,
160
163
  .an-rich-md-prose pre code .hljs-literal,
161
- .an-rich-md-prose pre code .hljs-doctag,
162
- .an-rich-md-prose pre code .hljs-deletion {
163
- color: #ff7b72;
164
+ .an-rich-md-prose pre code .hljs-doctag {
165
+ color: hsl(280 60% 58%);
164
166
  }
165
167
  .an-rich-md-prose pre code .hljs-string,
166
168
  .an-rich-md-prose pre code .hljs-regexp,
167
169
  .an-rich-md-prose pre code .hljs-addition {
168
- color: #a5d6ff;
170
+ color: hsl(140 52% 40%);
169
171
  }
170
172
  .an-rich-md-prose pre code .hljs-number,
171
173
  .an-rich-md-prose pre code .hljs-symbol,
174
+ .an-rich-md-prose pre code .hljs-literal {
175
+ color: hsl(28 78% 48%);
176
+ }
172
177
  .an-rich-md-prose pre code .hljs-meta {
173
- color: #79c0ff;
178
+ color: hsl(var(--muted-foreground));
174
179
  }
175
180
  .an-rich-md-prose pre code .hljs-title,
176
181
  .an-rich-md-prose pre code .hljs-title.function_,
177
182
  .an-rich-md-prose pre code .hljs-section {
178
- color: #d2a8ff;
183
+ color: hsl(210 72% 52%);
179
184
  }
180
185
  .an-rich-md-prose pre code .hljs-built_in,
181
186
  .an-rich-md-prose pre code .hljs-type,
182
187
  .an-rich-md-prose pre code .hljs-title.class_,
183
188
  .an-rich-md-prose pre code .hljs-variable.language_ {
184
- color: #ffa657;
189
+ color: hsl(190 64% 40%);
185
190
  }
186
191
  .an-rich-md-prose pre code .hljs-attr,
187
192
  .an-rich-md-prose pre code .hljs-attribute,
188
193
  .an-rich-md-prose pre code .hljs-property,
189
194
  .an-rich-md-prose pre code .hljs-params {
190
- color: #79c0ff;
195
+ color: hsl(35 68% 46%);
191
196
  }
192
197
  .an-rich-md-prose pre code .hljs-name,
193
198
  .an-rich-md-prose pre code .hljs-tag,
194
199
  .an-rich-md-prose pre code .hljs-selector-id,
195
200
  .an-rich-md-prose pre code .hljs-selector-class {
196
- color: #7ee787;
201
+ color: hsl(350 62% 52%);
202
+ }
203
+ .an-rich-md-prose pre code .hljs-deletion {
204
+ color: hsl(0 62% 52%);
197
205
  }
198
206
  .an-rich-md-prose pre code .hljs-emphasis {
199
207
  font-style: italic;
@@ -202,6 +210,47 @@
202
210
  font-weight: 600;
203
211
  }
204
212
 
213
+ /* In dark mode, lift the token lightness for contrast on the dark muted surface. */
214
+ .dark .an-rich-md-prose pre code .hljs-keyword,
215
+ .dark .an-rich-md-prose pre code .hljs-selector-tag,
216
+ .dark .an-rich-md-prose pre code .hljs-literal,
217
+ .dark .an-rich-md-prose pre code .hljs-doctag {
218
+ color: hsl(280 72% 72%);
219
+ }
220
+ .dark .an-rich-md-prose pre code .hljs-string,
221
+ .dark .an-rich-md-prose pre code .hljs-regexp,
222
+ .dark .an-rich-md-prose pre code .hljs-addition {
223
+ color: hsl(140 48% 62%);
224
+ }
225
+ .dark .an-rich-md-prose pre code .hljs-number,
226
+ .dark .an-rich-md-prose pre code .hljs-symbol,
227
+ .dark .an-rich-md-prose pre code .hljs-literal {
228
+ color: hsl(30 85% 65%);
229
+ }
230
+ .dark .an-rich-md-prose pre code .hljs-title,
231
+ .dark .an-rich-md-prose pre code .hljs-title.function_,
232
+ .dark .an-rich-md-prose pre code .hljs-section {
233
+ color: hsl(210 80% 70%);
234
+ }
235
+ .dark .an-rich-md-prose pre code .hljs-built_in,
236
+ .dark .an-rich-md-prose pre code .hljs-type,
237
+ .dark .an-rich-md-prose pre code .hljs-title.class_,
238
+ .dark .an-rich-md-prose pre code .hljs-variable.language_ {
239
+ color: hsl(190 70% 62%);
240
+ }
241
+ .dark .an-rich-md-prose pre code .hljs-attr,
242
+ .dark .an-rich-md-prose pre code .hljs-attribute,
243
+ .dark .an-rich-md-prose pre code .hljs-property,
244
+ .dark .an-rich-md-prose pre code .hljs-params {
245
+ color: hsl(40 78% 66%);
246
+ }
247
+ .dark .an-rich-md-prose pre code .hljs-name,
248
+ .dark .an-rich-md-prose pre code .hljs-tag,
249
+ .dark .an-rich-md-prose pre code .hljs-selector-id,
250
+ .dark .an-rich-md-prose pre code .hljs-selector-class {
251
+ color: hsl(350 72% 68%);
252
+ }
253
+
205
254
  .an-rich-md-prose hr {
206
255
  border: none;
207
256
  border-top: 1px solid hsl(var(--border));
@@ -0,0 +1,7 @@
1
+ overrides:
2
+ "@assistant-ui/store": ">=0.2.9 <0.2.14"
3
+ "@assistant-ui/tap": "^0.5.14"
4
+
5
+ allowBuilds:
6
+ better-sqlite3: true
7
+ esbuild: true
@@ -23,10 +23,5 @@
23
23
  "tsx": "catalog:",
24
24
  "typescript": "^6.0.3"
25
25
  },
26
- "pnpm": {
27
- "overrides": {
28
- "better-auth": "1.6.0"
29
- }
30
- },
31
26
  "packageManager": "pnpm@10.14.0"
32
27
  }
@@ -2,6 +2,11 @@ packages:
2
2
  - "packages/*"
3
3
  - "apps/*"
4
4
 
5
+ overrides:
6
+ "@assistant-ui/store": ">=0.2.9 <0.2.14"
7
+ "@assistant-ui/tap": "^0.5.14"
8
+ better-auth: "1.6.0"
9
+
5
10
  onlyBuiltDependencies:
6
11
  - "@swc/core"
7
12
  - better-sqlite3
@@ -10,3 +15,12 @@ onlyBuiltDependencies:
10
15
  - esbuild
11
16
  - lightningcss
12
17
  - node-pty
18
+
19
+ allowBuilds:
20
+ "@swc/core": true
21
+ better-sqlite3: true
22
+ canvas: true
23
+ electron: true
24
+ esbuild: true
25
+ lightningcss: true
26
+ node-pty: true
@@ -68,6 +68,16 @@ The result: Claude-Code-level flexibility for each user, with normal SaaS deploy
68
68
 
69
69
  You don't have to. Every template is also available as a hosted app on `agent-native.com` — `mail.agent-native.com`, `calendar.agent-native.com`, and so on. Use the hosted version for free or paid; fork only when you want to change something the hosted version doesn't expose.
70
70
 
71
+ ## Try it with a skill {#try-with-a-skill}
72
+
73
+ Don't want to scaffold a whole app yet? Add agent-native superpowers to a coding agent you already use — Claude Code, Codex, or Cursor — with a single command. Installing the **Plans** skill turns the plans your agent writes into structured, reviewable docs with diagrams, wireframes, and inline comments:
74
+
75
+ ```bash
76
+ npx @agent-native/core@latest skills add visual-plan
77
+ ```
78
+
79
+ That one command installs the skill instructions, registers the hosted MCP connector, and signs you in — no marketplace browsing, no manual OAuth. Then run `/visual-plan` in your agent. See the [Skills Guide](/docs/skills-guide#app-backed-skills) for more skills, local/offline installs, and how app-backed skills work.
80
+
71
81
  ## Building on this
72
82
 
73
83
  - [**Getting Started**](/docs/getting-started) — clone your first template and run it locally