Add optional self-hosted UUID mode (SQLite + server) reusing the viewer#17
Add optional self-hosted UUID mode (SQLite + server) reusing the viewer#17
Conversation
Deploying agent-render with
|
| Latest commit: |
d4ccaf7
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://1577d374.agent-render.pages.dev |
| Branch Preview URL: | https://codex-add-self-hosted-app-wi.agent-render.pages.dev |
📝 WalkthroughWalkthroughThe PR introduces a self-hosted UUID mode for agent-render, extending the platform from a static fragment-only artifact viewer to support optional server-backed persistence. The deployment now offers two modes: static fragment mode (default, zero-retention by design) and self-hosted UUID mode (Node.js + SQLite with 24-hour sliding TTL), reusing the same viewer shell for both. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Browser Client
participant Server as Node.js Server
participant SQLite as SQLite DB
participant Static as Static Assets
Client->>Server: POST /api/artifacts<br/>{payload: "..."}
Server->>SQLite: INSERT artifact<br/>(id, payload, expires_at)
SQLite-->>Server: Stored artifact record
Server-->>Client: 201 {id, url, expiresAt}
Client->>Server: GET /:uuid
Server->>SQLite: SELECT * WHERE id=:uuid
SQLite-->>Server: Artifact record (if not expired)
Server->>Static: Read index.html
Static-->>Server: HTML content
Server->>Server: Inject <script><br/>window.__AGENT_RENDER_STORED_PAYLOAD__
Server-->>Client: HTML with embedded payload
Client->>Server: GET /api/artifacts/:uuid
Server->>SQLite: UPDATE last_viewed_at,<br/>expires_at (TTL refresh)
SQLite-->>Server: Updated record
Server-->>Client: 200 {id, payload, expiresAt}
sequenceDiagram
participant User as User/Agent
participant Client as Browser Client
participant Store as SelfHostedArtifactStore
participant SQLite as SQLite DB
User->>Client: Create artifact payload
Client->>Store: create(payload: string)
Store->>SQLite: INSERT INTO artifacts<br/>(id, payload, created_at,<br/>expires_at = now + 24h)
SQLite-->>Store: Artifact ID (UUID v4)
Store-->>Client: StoredArtifactRecord
Note over Client,Store: Time passes (< 24h)
Client->>Store: getById(id,<br/>{refreshTtl: true})
Store->>SQLite: UPDATE expires_at,<br/>last_viewed_at
SQLite-->>Store: Refreshed record
Store-->>Client: StoredArtifactRecord
Note over Client,Store: Time passes (> 24h from last view)
Client->>Store: getById(id)
Store->>SQLite: SELECT * WHERE id=id
SQLite-->>Store: Expired record (expires_at < now)
Store->>SQLite: DELETE WHERE id=id
Store-->>Client: null (artifact unavailable)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d4ccaf75c4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const payloadString = storedBootstrap ? getDisplayPayloadString(storedBootstrap.payload) : hash; | ||
| const [parsed, setParsed] = useState<ReturnType<typeof decodeFragment>>(() => decodeFragment(payloadString)); |
There was a problem hiding this comment.
Skip fragment-size checks for bootstrapped UUID payloads
This now feeds storedBootstrap.payload back through decodeFragment()/decodeFragmentAsync(), and those helpers still reject anything over MAX_FRAGMENT_LENGTH in src/lib/payload/fragment.ts:266-268. The result is that the new UUID mode cannot render the very payloads it is advertised for: a stored payload longer than 8,000 characters can be created successfully, but opening /{uuid} will only land in the too-large error state because it is treated like a URL fragment again instead of a server-delivered payload.
Useful? React with 👍 / 👎.
| function injectStoredPayload(indexHtml: string, artifact: { id: string; payload: string; expiresAt: string }): string { | ||
| const bootstrap = JSON.stringify({ id: artifact.id, payload: artifact.payload, expiresAt: artifact.expiresAt }); | ||
| const script = `<script>window.__AGENT_RENDER_STORED_PAYLOAD__=${bootstrap};</script>`; | ||
| return indexHtml.replace("</head>", `${script}</head>`); |
There was a problem hiding this comment.
Escape stored payloads before injecting the bootstrap script
injectStoredPayload() writes artifact.payload directly into an inline <script>, but validateStoredPayloadString() only checks the outer prefix in src/lib/selfhosted/stored-payload.ts:40-41. A request body such as agent-render=v1.arx.0.</script><script>... is therefore accepted and then breaks out of this script block when someone opens the UUID link, executing attacker-controlled JavaScript. This matters anywhere the self-hosted create/update API is reachable by untrusted clients or shared automation.
Useful? React with 👍 / 👎.
Code Review SummaryStatus: 1 Suggestion Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Other Observations (not in diff)The implementation is solid overall. The self-hosted mode correctly:
The documentation is thorough and correctly explains the distinction between static fragment mode (zero-retention) and self-hosted mode (SQLite-backed). Files Reviewed (24 files)
Reviewed by minimax-m2.5-20260211 · 1,671,235 tokens |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
package.json (1)
75-77:⚠️ Potential issue | 🟠 MajorNode engine constraint doesn't cover self-hosted mode requirements.
The
engines.nodefield declares>=20.10.0, but the self-hosted mode's dependency onnode:sqlite(imported insrc/lib/selfhosted/store.ts) requires Node 22.5+ with--experimental-sqliteflag, or Node 23+ where it's available without flags.This mismatch could lead to confusing runtime errors for operators who attempt self-hosted deployment on Node 20 or 21.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 75 - 77, The engines.node constraint in package.json must be raised to match the requirements of the self-hosted SQLite import (node:sqlite used in src/lib/selfhosted/store.ts); update the "engines.node" field from ">=20.10.0" to a minimum that guarantees native sqlite support (e.g., ">=23.0.0") or to ">=22.5.0" if you prefer to require the experimental flag, and also update any deployment/docs referencing Node requirements to reflect this change so operators won't run unsupported Node versions.
🧹 Nitpick comments (7)
server/selfhosted.ts (1)
5-5: Consider validating PORT for non-numeric values.If
PORTis set to an invalid value (e.g.,"abc"),Number()returnsNaN, which will causeserver.listento fail with a potentially unclear error. A simple guard could improve operator experience:🛡️ Suggested validation
const port = Number(process.env.PORT ?? 3000); +if (Number.isNaN(port) || port < 1 || port > 65535) { + console.error(`Invalid PORT: ${process.env.PORT}. Must be a number between 1 and 65535.`); + process.exit(1); +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/selfhosted.ts` at line 5, The PORT environment value is not validated: the const port = Number(process.env.PORT ?? 3000) can become NaN for non-numeric strings and break server.listen; change the logic around the port variable to parse and validate (e.g., parseInt or Number), check Number.isInteger and !Number.isNaN and that the port is within valid range (1–65535), and if invalid either fall back to the default (3000) or log a clear error and exit; update references to the port variable (used in server.listen) so they use the validated numeric value.docs/dependency-notes.md (1)
37-37: Consider specifying the exact Node version requirement.The note mentions
node:sqliteis "experimental in current Node 22 runtimes" but doesn't clarify the minimum version (22.5+) or that Node 23+ includes it as stable. Operators may not know which "supported Node release" to use.Suggested clarification:
-- `node:sqlite` is an experimental Node built-in as of current Node 22 runtimes; self-hosted operators should use a supported Node release that includes it. +- `node:sqlite` requires Node 22.5+ (with `--experimental-sqlite` flag) or Node 23+; self-hosted operators should ensure their runtime meets this requirement.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/dependency-notes.md` at line 37, Update the note about node:sqlite to state the exact version requirements: indicate that node:sqlite is available experimentally starting in Node 22.5 and becomes stable in Node 23+, and recommend using at least Node 22.5 (or Node 23+) for self-hosted operators; edit the sentence that currently reads about "current Node 22 runtimes" to explicitly mention "Node 22.5+ (experimental) or Node 23+ (stable)" and add a brief recommendation to prefer a supported Node release meeting those minimums.tests/selfhosted/store.test.ts (1)
52-52: Strengthen the UUID assertion to validate UUID v4 shape.Line 52 currently accepts any 36-char hex/dash sequence. Tightening this assertion better protects the UUID v4 contract.
Proposed assertion update
- expect(created.id).toMatch(/[0-9a-f-]{36}/i); + expect(created.id).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/selfhosted/store.test.ts` at line 52, The test currently validates created.id with a loose regex; tighten it to assert a UUID v4 shape by replacing the current expect(created.id).toMatch(/[0-9a-f-]{36}/i) check with a stricter UUIDv4 pattern (matching the 8-4-4-4-12 hex groups and the version/variant bits) so the test only accepts valid UUID v4 strings for created.id.src/lib/selfhosted/bootstrap.ts (1)
21-21: Add a runtime shape guard before returning the injected bootstrap object.Line 21 returns whatever is on
window.__AGENT_RENDER_STORED_PAYLOAD__; a malformed global could break downstream reads. Consider validating{ id, payload, expiresAt }are strings before returning.Proposed defensive guard
+function isStoredPayloadBootstrap(value: unknown): value is StoredPayloadBootstrap { + if (!value || typeof value !== "object") return false; + const candidate = value as Record<string, unknown>; + return ( + typeof candidate.id === "string" && + typeof candidate.payload === "string" && + typeof candidate.expiresAt === "string" + ); +} + /** * Reads the self-hosted bootstrap payload injected into the exported viewer page. */ export function getStoredPayloadBootstrap(): StoredPayloadBootstrap | null { if (typeof window === "undefined") { return null; } - return window.__AGENT_RENDER_STORED_PAYLOAD__ ?? null; + const value = window.__AGENT_RENDER_STORED_PAYLOAD__; + return isStoredPayloadBootstrap(value) ? value : null; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/selfhosted/bootstrap.ts` at line 21, The code currently returns window.__AGENT_RENDER_STORED_PAYLOAD__ directly which can be malformed; add a runtime shape guard in the exporter (the code that reads window.__AGENT_RENDER_STORED_PAYLOAD__) to verify the value is a non-null object with keys id, payload, expiresAt and that each is a string (or the expected primitive type) before returning it, otherwise return null; keep the check defensive (typeof checks and an object-null guard) and avoid throwing so downstream code still receives null for invalid shapes.tests/components/viewer-shell-selfhosted.test.tsx (1)
42-42: Make the explanatory-copy assertion less brittle.Line 42 matches a full sentence exactly, so minor copy edits will fail behavior-valid tests. Prefer a shorter regex/key phrase assertion.
Example adjustment
- expect(screen.getByText("Selecting an artifact updates the active viewer state locally while keeping the stored payload and UUID route unchanged.")).toBeInTheDocument(); + expect( + screen.getByText(/active viewer state locally/i), + ).toBeInTheDocument();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/components/viewer-shell-selfhosted.test.tsx` at line 42, The test's assertion using screen.getByText with the full sentence is brittle; replace the exact string match with a more resilient partial or regex match (e.g., use screen.getByText(/Selecting an artifact/i) or another concise key phrase) so the test still verifies the explanatory copy without depending on the entire sentence; update the assertion around getByText("Selecting an artifact updates the active viewer state locally while keeping the stored payload and UUID route unchanged.") accordingly in tests/components/viewer-shell-selfhosted.test.tsx.skills/selfhosted-agent-render/SKILL.md (1)
81-81: Use one route placeholder style consistently (:idvs{uuid}).The doc mixes
:idand{uuid}for the same endpoint family. Standardizing one style improves copy/paste clarity.Example normalization
-DELETE /api/artifacts/{uuid} +DELETE /api/artifacts/:idAlso applies to: 124-125
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@skills/selfhosted-agent-render/SKILL.md` at line 81, The API docs in SKILL.md use two different path placeholder styles (`:id` and `{uuid}`) for the same endpoints (e.g., `DELETE /api/artifacts/:id` and occurrences at lines 124-125); pick a single placeholder convention and apply it consistently across the file (replace all `:id` with `{uuid}` or vice versa), updating every endpoint string and any related examples or references (search for `:id`, `{uuid}`, and similar placeholders) so all routes use the chosen style uniformly.src/components/viewer-shell.tsx (1)
683-683: Minor formatting inconsistency.The indentation on this line appears misaligned compared to surrounding code. This doesn't affect functionality but may cause linter warnings.
🔧 Suggested fix
- {sampleCards.map((sample) => { + {sampleCards.map((sample) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/viewer-shell.tsx` at line 683, The line containing the JSX iterator "{sampleCards.map((sample) => {" in the ViewerShell component is mis-indented; adjust its indentation to match surrounding JSX blocks (align it with sibling JSX elements inside the same parent) so the code formatting is consistent with the rest of src/components/viewer-shell.tsx; locate the usage of sampleCards.map in the render/return of the ViewerShell (or the component function where sampleCards is mapped) and fix the whitespace/tabs to match the surrounding lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Around line 22-24: Update the Node engine constraint in package.json or add a
runtime guard in server/selfhosted.ts: either change the package.json
"engines.node" value to ">=22.5.0" so installs require a Node version that
supports node:sqlite, or add a startup check in server/selfhosted.ts (before
SelfHostedArtifactStore or any import/use of node:sqlite) that inspects
process.version and throws/logs a clear error instructing users to run Node
>=22.5.0; reference package.json::engines.node and server/selfhosted.ts /
SelfHostedArtifactStore / node:sqlite when making the change.
In `@server/selfhosted-app.ts`:
- Around line 108-133: The renderMissingArtifactPage function currently
interpolates the reason string directly into HTML, risking XSS if caller input
becomes untrusted; update renderMissingArtifactPage to HTML-escape the reason
before insertion (e.g., replace &, <, >, ", ' and / with safe entities) and use
that escaped value in the template; ensure the escaping utility is a small
helper (escapeHtml or similar) referenced and used inside
renderMissingArtifactPage so all callers (current and future) are protected.
In `@tests/selfhosted/server.test.ts`:
- Around line 1-9: The CI fails because src/lib/selfhosted/store.ts imports
DatabaseSync from the built-in node:sqlite (requires Node 22.5+); update the
implementation to use a Node 20-compatible SQLite library instead (or make the
import conditional). Replace the direct import of DatabaseSync from
"node:sqlite" in SelfHostedArtifactStore with a wrapper that uses better-sqlite3
(or sqlite3) — e.g., swap the import for the chosen package, adapt any
DatabaseSync-typed usages in the SelfHostedArtifactStore class and related
functions to the API of the new library (connection/open,
prepare/run/get/all/close), and ensure tests and exports that reference
createSelfHostedServer continue to work with the new store implementation;
alternatively, update CI to Node 22.5+ if you prefer to keep node:sqlite.
In `@tests/selfhosted/store.test.ts`:
- Around line 1-9: Update the Node.js requirement to meet node:sqlite's minimum
(v22.5.0): change the package.json engines.node entry (the "engines.node" field)
to require >=22.5.0 (or a recommended LTS range like ">=22.x" or ">=24.x"), and
update the CI configuration that sets the runner version (the workflow
"node-version" key in your GitHub Actions workflow) from 20 to a supported
version (22 or 24) so tests run with Node 22.5.0+.
---
Outside diff comments:
In `@package.json`:
- Around line 75-77: The engines.node constraint in package.json must be raised
to match the requirements of the self-hosted SQLite import (node:sqlite used in
src/lib/selfhosted/store.ts); update the "engines.node" field from ">=20.10.0"
to a minimum that guarantees native sqlite support (e.g., ">=23.0.0") or to
">=22.5.0" if you prefer to require the experimental flag, and also update any
deployment/docs referencing Node requirements to reflect this change so
operators won't run unsupported Node versions.
---
Nitpick comments:
In `@docs/dependency-notes.md`:
- Line 37: Update the note about node:sqlite to state the exact version
requirements: indicate that node:sqlite is available experimentally starting in
Node 22.5 and becomes stable in Node 23+, and recommend using at least Node 22.5
(or Node 23+) for self-hosted operators; edit the sentence that currently reads
about "current Node 22 runtimes" to explicitly mention "Node 22.5+
(experimental) or Node 23+ (stable)" and add a brief recommendation to prefer a
supported Node release meeting those minimums.
In `@server/selfhosted.ts`:
- Line 5: The PORT environment value is not validated: the const port =
Number(process.env.PORT ?? 3000) can become NaN for non-numeric strings and
break server.listen; change the logic around the port variable to parse and
validate (e.g., parseInt or Number), check Number.isInteger and !Number.isNaN
and that the port is within valid range (1–65535), and if invalid either fall
back to the default (3000) or log a clear error and exit; update references to
the port variable (used in server.listen) so they use the validated numeric
value.
In `@skills/selfhosted-agent-render/SKILL.md`:
- Line 81: The API docs in SKILL.md use two different path placeholder styles
(`:id` and `{uuid}`) for the same endpoints (e.g., `DELETE /api/artifacts/:id`
and occurrences at lines 124-125); pick a single placeholder convention and
apply it consistently across the file (replace all `:id` with `{uuid}` or vice
versa), updating every endpoint string and any related examples or references
(search for `:id`, `{uuid}`, and similar placeholders) so all routes use the
chosen style uniformly.
In `@src/components/viewer-shell.tsx`:
- Line 683: The line containing the JSX iterator "{sampleCards.map((sample) =>
{" in the ViewerShell component is mis-indented; adjust its indentation to match
surrounding JSX blocks (align it with sibling JSX elements inside the same
parent) so the code formatting is consistent with the rest of
src/components/viewer-shell.tsx; locate the usage of sampleCards.map in the
render/return of the ViewerShell (or the component function where sampleCards is
mapped) and fix the whitespace/tabs to match the surrounding lines.
In `@src/lib/selfhosted/bootstrap.ts`:
- Line 21: The code currently returns window.__AGENT_RENDER_STORED_PAYLOAD__
directly which can be malformed; add a runtime shape guard in the exporter (the
code that reads window.__AGENT_RENDER_STORED_PAYLOAD__) to verify the value is a
non-null object with keys id, payload, expiresAt and that each is a string (or
the expected primitive type) before returning it, otherwise return null; keep
the check defensive (typeof checks and an object-null guard) and avoid throwing
so downstream code still receives null for invalid shapes.
In `@tests/components/viewer-shell-selfhosted.test.tsx`:
- Line 42: The test's assertion using screen.getByText with the full sentence is
brittle; replace the exact string match with a more resilient partial or regex
match (e.g., use screen.getByText(/Selecting an artifact/i) or another concise
key phrase) so the test still verifies the explanatory copy without depending on
the entire sentence; update the assertion around getByText("Selecting an
artifact updates the active viewer state locally while keeping the stored
payload and UUID route unchanged.") accordingly in
tests/components/viewer-shell-selfhosted.test.tsx.
In `@tests/selfhosted/store.test.ts`:
- Line 52: The test currently validates created.id with a loose regex; tighten
it to assert a UUID v4 shape by replacing the current
expect(created.id).toMatch(/[0-9a-f-]{36}/i) check with a stricter UUIDv4
pattern (matching the 8-4-4-4-12 hex groups and the version/variant bits) so the
test only accepts valid UUID v4 strings for created.id.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e58bea96-3ac5-4db3-bd7c-94e3b405f4a8
📒 Files selected for processing (24)
AGENTS.mdREADME.mddocs/architecture.mddocs/dependency-notes.mddocs/deployment.mddocs/payload-format.mddocs/testing.mdpackage.jsonserver/cleanup-selfhosted.tsserver/selfhosted-app.tsserver/selfhosted.tsskills/agent-render-linking/SKILL.mdskills/selfhosted-agent-render/SKILL.mdsrc/components/viewer-shell.tsxsrc/lib/selfhosted/bootstrap.tssrc/lib/selfhosted/constants.tssrc/lib/selfhosted/store.tssrc/lib/selfhosted/stored-payload.tstests/components/viewer-shell-selfhosted.test.tsxtests/selfhosted/server.test.tstests/selfhosted/store.test.tstests/setup.tsxtsconfig.jsonvitest.config.ts
| "start:selfhosted": "node --experimental-strip-types server/selfhosted.ts", | ||
| "cleanup:selfhosted": "node --experimental-strip-types server/cleanup-selfhosted.ts", | ||
| "build:selfhosted": "npm run build" |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What Node.js version is required for node:sqlite built-in module?
💡 Result:
The node:sqlite built-in module requires Node.js version 22.5.0 or later. It was added in v22.5.0 as an experimental feature (initially requiring the --experimental-sqlite flag). In later versions, the flag requirement was removed (v23.4.0, v22.13.0), and it progressed to release candidate status (v25.7.0).
Citations:
- 1: https://nodejs.org/api/sqlite.html
- 2: https://nodejs.org/docs/latest/api/sqlite.html
- 3: https://nodejs.org/download/release/v22.12.0/docs/api/sqlite.html
- 4: https://nodejs.org/en/blog/release/v22.5.0
- 5: lib,src,test,doc: add node:sqlite module nodejs/node#53752
🏁 Script executed:
# Check package.json for engines field and the specific lines mentioned
head -80 package.json | tail -10Repository: baanish/agent-render
Length of output: 255
🏁 Script executed:
# Search for node:sqlite usage in the codebase
rg "node:sqlite|SelfHostedArtifactStore" --type json --type ts --type js -A 2Repository: baanish/agent-render
Length of output: 2372
🏁 Script executed:
# Check if server/selfhosted.ts exists and what it imports
fd "selfhosted.ts" -x cat -n {}Repository: baanish/agent-render
Length of output: 1600
Update engines.node to match self-hosted Node.js requirement.
The self-hosted scripts depend on node:sqlite (used by SelfHostedArtifactStore), which requires Node.js 22.5.0 or later. However, package.json specifies engines.node: ">=20.10.0", allowing Node 20/21 where node:sqlite will fail at runtime. Either update engines.node to ">=22.5.0", or add a runtime check in server/selfhosted.ts to provide a helpful error message.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` around lines 22 - 24, Update the Node engine constraint in
package.json or add a runtime guard in server/selfhosted.ts: either change the
package.json "engines.node" value to ">=22.5.0" so installs require a Node
version that supports node:sqlite, or add a startup check in
server/selfhosted.ts (before SelfHostedArtifactStore or any import/use of
node:sqlite) that inspects process.version and throws/logs a clear error
instructing users to run Node >=22.5.0; reference package.json::engines.node and
server/selfhosted.ts / SelfHostedArtifactStore / node:sqlite when making the
change.
| function renderMissingArtifactPage(reason: string): string { | ||
| return `<!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <title>Artifact unavailable · agent-render</title> | ||
| <style> | ||
| body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#0b1020;color:#f5f7fb;margin:0;display:grid;min-height:100vh;place-items:center;padding:24px} | ||
| main{max-width:640px;background:#111a30;border:1px solid rgba(255,255,255,.12);border-radius:20px;padding:28px;box-shadow:0 20px 60px rgba(0,0,0,.35)} | ||
| h1{margin:0 0 12px;font-size:2rem} | ||
| p{line-height:1.7;color:#d7dcef} | ||
| code{background:rgba(255,255,255,.08);padding:.15rem .4rem;border-radius:.45rem} | ||
| a{color:#8dd4ff} | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <main> | ||
| <h1>Artifact unavailable</h1> | ||
| <p>${reason}</p> | ||
| <p>Stored artifacts in self-hosted mode use a 24-hour sliding TTL. A successful read extends the expiry window; expired rows are removed on access or cleanup.</p> | ||
| <p><a href="/">Open the fragment-based homepage</a></p> | ||
| </main> | ||
| </body> | ||
| </html>`; | ||
| } |
There was a problem hiding this comment.
Potential XSS in error page rendering.
The reason parameter is interpolated directly into HTML at Line 127. While the current callers pass hardcoded strings, this could become a vulnerability if future code passes user-controlled input.
🛡️ Suggested fix to escape the reason string
+function escapeHtml(str: string): string {
+ return str
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """);
+}
+
function renderMissingArtifactPage(reason: string): string {
return `<!doctype html>
...
- <p>${reason}</p>
+ <p>${escapeHtml(reason)}</p>
...
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/selfhosted-app.ts` around lines 108 - 133, The
renderMissingArtifactPage function currently interpolates the reason string
directly into HTML, risking XSS if caller input becomes untrusted; update
renderMissingArtifactPage to HTML-escape the reason before insertion (e.g.,
replace &, <, >, ", ' and / with safe entities) and use that escaped value in
the template; ensure the escaping utility is a small helper (escapeHtml or
similar) referenced and used inside renderMissingArtifactPage so all callers
(current and future) are protected.
| // @vitest-environment node | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; | ||
| import { afterEach, describe, expect, it } from "vitest"; | ||
| import { encodeEnvelope } from "@/lib/payload/fragment"; | ||
| import type { PayloadEnvelope } from "@/lib/payload/schema"; | ||
| import { createSelfHostedServer } from "../../server/selfhosted-app"; | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the Node.js version requirement in package.json and CI workflow files
echo "=== package.json engines field ==="
cat package.json | jq '.engines'
echo -e "\n=== GitHub Actions workflow Node version ==="
fd -e yml -e yaml . .github/workflows --exec grep -A2 -E 'node-version|setup-node' {} \;Repository: baanish/agent-render
Length of output: 269
🏁 Script executed:
#!/bin/bash
# Verify store.ts imports from node:sqlite
echo "=== Checking src/lib/selfhosted/store.ts for node:sqlite import ==="
fd -n "store.ts" src/ --exec cat {} \;
echo -e "\n=== Checking for SQLite-related dependencies in package.json ==="
cat package.json | jq '.dependencies, .devDependencies' | grep -i sqlite
echo -e "\n=== Checking full import chain in test file ==="
head -20 tests/selfhosted/server.test.tsRepository: baanish/agent-render
Length of output: 1140
🏁 Script executed:
#!/bin/bash
# Find store.ts file properly
echo "=== Finding store.ts files ==="
find . -name "store.ts" -type f 2>/dev/null
echo -e "\n=== Searching for node:sqlite import anywhere in codebase ==="
rg "node:sqlite" --type ts --type js
echo -e "\n=== Searching for SelfHostedArtifactStore ==="
rg "SelfHostedArtifactStore" --type ts --type js -A 2 -B 2
echo -e "\n=== Checking what createSelfHostedServer imports/creates ==="
fd "selfhosted-app" . --exec cat {} \;Repository: baanish/agent-render
Length of output: 11988
Pipeline failure: node:sqlite requires Node.js 22.5+
The CI error "No such built-in module: node:sqlite" indicates the test environment is running Node.js 20 (configured in your GitHub Actions workflow), but node:sqlite was introduced in Node.js 22.5.0. The SelfHostedArtifactStore in src/lib/selfhosted/store.ts imports DatabaseSync from node:sqlite.
Consider either:
- Updating the CI Node.js version to 22.5+ in your workflow configuration
- Using an alternative SQLite library like
better-sqlite3that works with older Node versions
🧰 Tools
🪛 GitHub Actions: Test
[error] 1-1: Vitest failed. Error: No such built-in module: node:sqlite
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/selfhosted/server.test.ts` around lines 1 - 9, The CI fails because
src/lib/selfhosted/store.ts imports DatabaseSync from the built-in node:sqlite
(requires Node 22.5+); update the implementation to use a Node 20-compatible
SQLite library instead (or make the import conditional). Replace the direct
import of DatabaseSync from "node:sqlite" in SelfHostedArtifactStore with a
wrapper that uses better-sqlite3 (or sqlite3) — e.g., swap the import for the
chosen package, adapt any DatabaseSync-typed usages in the
SelfHostedArtifactStore class and related functions to the API of the new
library (connection/open, prepare/run/get/all/close), and ensure tests and
exports that reference createSelfHostedServer continue to work with the new
store implementation; alternatively, update CI to Node 22.5+ if you prefer to
keep node:sqlite.
| // @vitest-environment node | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import { mkdtempSync } from "node:fs"; | ||
| import { afterEach, describe, expect, it } from "vitest"; | ||
| import { encodeEnvelope } from "@/lib/payload/fragment"; | ||
| import type { PayloadEnvelope } from "@/lib/payload/schema"; | ||
| import { SelfHostedArtifactStore } from "@/lib/selfhosted/store"; | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package.json engines =="
if [ -f package.json ]; then
jq -r '.engines.node // "MISSING: engines.node"' package.json
else
echo "MISSING: package.json"
fi
echo
echo "== workflow node-version configuration =="
if [ -d .github/workflows ]; then
rg -n "actions/setup-node|node-version" .github/workflows -g "*.yml" -g "*.yaml" || true
else
echo "MISSING: .github/workflows"
fi
echo
echo "== node:sqlite imports =="
rg -n "node:sqlite" || trueRepository: baanish/agent-render
Length of output: 612
🌐 Web query:
node:sqlite Node.js version requirement minimum support
💡 Result:
The built-in node:sqlite module requires Node.js v22.5.0 or later, as it was added in v22.5.0. It is currently at stability 1.2 (release candidate) in Node.js v25.x and still experimental in earlier versions like v22 and v23. Popular third-party SQLite libraries have these minimum Node.js requirements based on their documentation: - sqlite3 (now deprecated): Prebuilt binaries supported on Node v20.17.0+ (v6.x); earlier v5.x supported Node v10+. - better-sqlite3: Requires a currently supported Node.js version (as of 2026, Node 20 LTS+, 22 LTS+, 24 LTS+). Prebuilt binaries for LTS versions; an older fork mentioned v14.21.1+.
Citations:
- 1: https://nodejs.org/api/sqlite.html
- 2: https://nodejs.org/docs/latest/api/sqlite.html
- 3: https://nodejs.org/download/release/v22.12.0/docs/api/sqlite.html
- 4: https://www.npmjs.com/package/sqlite3
- 5: https://www.npmjs.com/package/sqlite3?activeTab=versions
- 6: https://github.com/TryGhost/node-sqlite3
- 7: https://www.npmjs.com/package/better-sqlite3?activeTab=versions
- 8: https://github.com/WiseLibs/better-sqlite3
- 9: https://github.com/redacto/better-sqlite3
Update Node.js version requirements to support node:sqlite (v22.5.0+).
The store code imports node:sqlite which requires Node.js v22.5.0 or later. Currently, engines.node in package.json is pinned to >=20.10.0 and GitHub Actions test workflow uses node-version: 20, both of which are below the minimum requirement. Update both to Node 20 LTS or later (recommended: 22.x LTS or 24.x LTS) to resolve CI failures.
🧰 Tools
🪛 GitHub Actions: Test
[error] 1-1: Vitest failed. Error: No such built-in module: node:sqlite
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/selfhosted/store.test.ts` around lines 1 - 9, Update the Node.js
requirement to meet node:sqlite's minimum (v22.5.0): change the package.json
engines.node entry (the "engines.node" field) to require >=22.5.0 (or a
recommended LTS range like ">=22.x" or ">=24.x"), and update the CI
configuration that sets the runner version (the workflow "node-version" key in
your GitHub Actions workflow) from 20 to a supported version (22 or 24) so tests
run with Node 22.5.0+.
Motivation
agent-renderpayload strings under UUID v4 ids so operators/agents can share shorthttps://host/{uuid}links when fragments are impractical.Description
server/selfhosted-app.tsandserver/selfhosted.ts, plus a manual cleanup commandserver/cleanup-selfhosted.ts, exposingPOST /api/artifacts,GET/PUT/DELETE /api/artifacts/:idandGET /:idwhich injects the stored payload into the exported viewer HTML. Sliding 24-hour TTL and lazy/manual cleanup are implemented.src/lib/selfhosted/(store.ts,constants.ts,stored-payload.ts) which persists the canonical payload string, enforces UUID v4 ids, and performs TTL refresh on read.src/lib/selfhosted/bootstrap.tsand updates tosrc/components/viewer-shell.tsxso the viewer accepts an injected stored payload, preserves renderer behavior (copy/download/print, diff modes, artifact switching) and keeps artifact switching local in UUID mode.tests/selfhosted/*andtests/components/viewer-shell-selfhosted.test.tsx, update test/setup/config (tests/setup.tsx,vitest.config.ts,tsconfig.json) and add a new skill and docs (skills/selfhosted-agent-render/SKILL.md, updates toREADME.md,docs/*, andskills/agent-render-linking/SKILL.md) explaining differences between static fragment mode and the optional self-hosted mode.Testing
npm run typecheck— ✅ passed.npm run lint— ✅ passed (includes public-export docs check).npm test— ✅ passed (Vitest suite including new self-hosted store/server/viewer tests ran; all tests passed).PORT=4010 AGENT_RENDER_DB_PATH=.data/test-selfhosted.sqlite node --experimental-strip-types server/selfhosted.ts— ✅ server started and accepted test flows.npm run build—Automated test coverage added/verified: CRUD flows, UUID lookup, expired-item behavior, TTL refresh-on-read, HTML bootstrap injection for
/{uuid}, and rendering a stored payload through the shared viewer path.Codex Task
Summary by CodeRabbit
New Features
Documentation