Skip to content

feat: Support SSR, SSG and HMR#10

Open
dawsontoth wants to merge 6 commits into
mainfrom
production
Open

feat: Support SSR, SSG and HMR#10
dawsontoth wants to merge 6 commits into
mainfrom
production

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

This adds support for this plugin to run in production, with or without SSR enabled too. This can let us run Vite based apps in Studio, in theory.

I created new templates in create-harper over at HarperFast/create-harper#98

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the Harper-Vite integration, introducing a hybrid production mode that statically serves production builds and supports automatic recompilation alongside the existing development mode. It also adds support for Server-Side Rendering (SSR), coordinates multi-threaded builds using a file-system lock, and introduces comprehensive tests. The review feedback identifies several critical issues: a race condition in the build lock release check, a severe memory leak in registerHttp due to unresolved promises on successful middleware responses, a downtime window during SSR rebuilds caused by hardcoded directory emptying, performance bottlenecks from synchronous file operations in HTTP handlers, and a potential double-close bug in registerShutdown.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/buildLock.ts Outdated
Comment thread src/http.ts Outdated
Comment thread src/production.ts
Comment thread src/production.ts Outdated
Comment thread src/http.ts Outdated

@Ethan-Arrowood Ethan-Arrowood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea, but the implementation needs some work. I assume an agent created this. Could you potentially have it review the Next.js plugin so it can see some better development patterns?

Comment thread scripts/setup-fixture.mjs Outdated
Comment thread src/buildLock.ts
Comment thread src/buildLock.ts Outdated
Comment thread src/index.ts Outdated
Comment thread src/wrappers.ts Outdated
Comment thread src/production.ts Outdated
Comment thread test-fixture/config.yaml
Comment thread src/index.ts Outdated
Comment thread src/http.ts Outdated
Comment thread src/http.ts Outdated
@dawsontoth dawsontoth marked this pull request as draft June 3, 2026 17:30
@dawsontoth

Copy link
Copy Markdown
Contributor Author

@Ethan-Arrowood this is a WIP that i've had going for a month or so, first hand crafted, and then gaining momentum today with Claude. I wanted to get it in front of you and Austin as it's one of the biggest hurdles to people building useful things inside Studio from their browser. Adapting the nextjs examples into create-harper would be an additional win that we could start doing there too, but I'm learning.

@Ethan-Arrowood

Copy link
Copy Markdown
Member

Yes, I totally agree this plugin is important

@dawsontoth dawsontoth changed the title Production feat: Support SSR, SSG and HMR Jun 4, 2026
@dawsontoth dawsontoth force-pushed the production branch 6 times, most recently from 67d5e1b to fb2b98e Compare June 9, 2026 19:54
@dawsontoth dawsontoth marked this pull request as ready for review June 9, 2026 20:31
@dawsontoth

Copy link
Copy Markdown
Contributor Author

@Ethan-Arrowood i think we're ready to look through this more! I need to take a few more passes through it too. But I did publish a beta (0.3.0-beta.6) to verify that it's working locally and in Studio in stage. There are a lot of permutations possible, but IMO, if we nail the main ones... that'll be a big win: letting people develop React and Vue apps via Vite in Studio.

Comment thread src/buildLock.ts Outdated
Comment thread src/index.test.ts
Comment thread src/integration.test.ts Outdated
Comment thread src/integration.test.ts Outdated
Comment thread src/log.ts
Comment thread src/options.ts
Comment thread src/production.ts Outdated
Comment thread src/wrappers.ts
Comment thread test-fixture/config.yaml
Comment thread package.json Outdated
@dawsontoth dawsontoth force-pushed the production branch 2 times, most recently from d55200f to 39b31c6 Compare June 10, 2026 18:16
@dawsontoth

Copy link
Copy Markdown
Contributor Author

@Ethan-Arrowood I believe I addressed everything, mind verifying? The integration tests will run in the GHA now too. Once agreed upon, I'll release this as 1.0.0, and I'll flag vite-plugin as deprecated in favor of this new hotness.

@Ethan-Arrowood Ethan-Arrowood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the only blocking one is removing rm RF from npm script. Otherwise ship it

Comment thread src/production.ts
Comment thread package.json Outdated
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Claude says:

I'll start by exploring the plugin's structure to understand what it does and where security-sensitive code lives.

This is a Harper plugin for running Vite apps. Let me read the core source files, focusing on the HTTP/middleware and dev server code where external exposure risks live.

Now the auth, options, and supporting files.

Let me look at config, schema, and the README to understand the deployment model and the HMR WebSocket exposure note.

This is a well-structured plugin. Let me run targeted greps for common vulnerability patterns and check the remaining files (tests, scripts, npmignore) to be thorough.

I've now reviewed the entire runtime surface. No need to read further — the picture is clear. Here's my audit.

Security audit: @harperfast/vite

Verdict: This plugin is well-secured against external attacks — notably better than most Vite integrations I see. I found no critical or high-severity vulnerabilities in the plugin's own code. The author has clearly threat-modeled the dangerous surfaces (dev server, SSR-before-auth, build coordination) and mitigated them. The findings below are residual risks and hardening items, not exploitable bugs.

The runtime code does the right things in the obvious places: no eval/child_process/shell in shipped code (the only execSync is in scripts/setup-fixture.mjs, a dev-only file not in the npm files allowlist); no path traversal (all file paths are fixed or config-derived, never request-derived); response sinks are controlled; req.url only ever flows to your own render() and to Vite (dev, behind auth).


The three external surfaces and how they're handled

1. Dev server HTTP surface (HMR mode) — ✅ strongly gated. Vite's dev server exposes arbitrary file reads (/@fs/) and on-the-fly module transforms. The plugin puts a super_user HTTP Basic-auth check (src/auth.ts) ahead of the entire Vite middleware chain via chain(authenticate, vite) in development.ts:34. This is exactly the right architecture: it does not rely on Vite's own server.fs.deny (which has had a steady stream of bypass CVEs through 2024–2025) — an unauthenticated attacker can't reach /@fs/ at all. Two details done correctly:

  • Uses authenticateUser (always validates the password) and explicitly avoids getUser, which skips validation for null passwords — auth.ts:56-62. That's a real footgun they sidestepped.
  • Strict === true super_user check (auth.ts:9), not a truthy check.

2. Production SSR render — ✅ safe in the plugin, by-design risk is on you. The handler is registered runFirst: true (production.ts:204) so it renders for anonymous visitors before authentication. The plugin itself is clean here: fixed template path, function-replacer to avoid $&/$$ replacement-pattern corruption (production.ts:177), HEAD short-circuit, no reflection of req.url by the plugin. The risk it creates is documented (README §"SSR renders before authentication") and is genuinely your responsibility — see finding M2.

3. Production static serving — ✅ not this plugin, and the SSR bundle is hidden. Serving is delegated to Harper's static plugin. The SSR server bundle is deliberately built to node_modules/.harper-vite-ssr (production.ts:29) so static (pointed at dist) never serves your server code. Good.


Findings (residual / hardening)

| Severity | Finding

-- | -- | --
M1 | Medium (dev/hmr only) | HMR WebSocket is not covered by the auth gate.
M2 | Medium (by design) | SSR render(url) runs unauthenticated with no user context.
L1 | Low | authorizeLocal + reverse proxy can silently bypass the dev gate.
L2 | Low | Unauthenticated SSR render is a DoS amplifier.
L3 | Info | Defense depends on keeping Vite patched.

M1 — HMR WebSocket bypasses the gate. Setting hmr: true in middleware mode (development.ts:20) makes Vite stand up a separate WebSocket server (default port 24678) that is not routed through Harper, so the super_user gate doesn't apply to it. The README correctly warns "keep it bound to localhost," but the plugin leaves the binding to Vite's defaults rather than pinning it. The WS is mostly a notification channel (the dangerous file-read endpoints are HTTP and are gated), so impact is limited — but it shouldn't depend on a default. Recommendation: explicitly constrain it (e.g. server: { hmr: { host: 'localhost' } }), and/or log a prominent startup warning when hmr is on but DEV_MODE is off (someone forced the dev server in a deployed context). handleApplication currently logs the mode neutrally (index.ts:19).

M2 — SSR render is pre-auth and context-free. Because the handler is runFirst, your render(url) receives the raw attacker-controllable URL with no authenticated user. Any tables.X.get() inside render runs without that user's authorization. This is documented, but it's the single most likely way your app leaks data: validate/encode url before routing or reflecting it, and enforce authz inside the render path. The plugin can't fix this for you — but it's the thing to double-check in your entry-server.

L1 — Proxy trust can bypass the dev gate. The gate passes loopback requests because Harper auto-authorizes them as super_user (authorizeLocal). If harper dev ever sits behind a reverse proxy that Harper trusts for client-IP, an external request appearing as loopback would sail straight past the gate. This is a Harper/deploy config concern, not plugin code — but it directly bears on "exposing to external attacks," so: don't run harper dev behind an IP-trusting proxy, and don't expose it to the internet at all (run hmr: false).

L2 — DoS via the public SSR endpoint. Any GET/HEAD with Accept: text/html triggers render() with no auth/rate-limit. Mitigated by the HEAD short-circuit and the README's caching guidance, but worth a deliberate cache/rate-limit in front of an expensive render.

L3 — Keep Vite current. Your real protection for the dev server is the auth gate, not Vite's fs.deny. Still, keep Vite patched (you're on peer >=7, dev on 8 — good) so the gated surface stays sound in depth.


Bottom line

You are not carelessly exposing servers. The dangerous dev endpoints are auth-gated with a correct, defense-in-depth design, and the production path delegates serving to static while keeping server code out of the served directory. The one code-level improvement I'd actually make is M1 (pin the HMR WebSocket to loopback + warn when the dev server runs outside DEV_MODE); everything else is deployment discipline (M2/L1/L2) that your README already documents well.

Want me to implement M1 — explicitly bind the HMR WebSocket to localhost and add a startup warning when hmr is enabled without DEV_MODE? It's a small, contained change to development.ts and index.ts.

I'll start by exploring the plugin's structure to understand what it does and where security-sensitive code lives.

This is a Harper plugin for running Vite apps. Let me read the core source files, focusing on the HTTP/middleware and dev server code where external exposure risks live.

Now the auth, options, and supporting files.

Let me look at config, schema, and the README to understand the deployment model and the HMR WebSocket exposure note.

This is a well-structured plugin. Let me run targeted greps for common vulnerability patterns and check the remaining files (tests, scripts, npmignore) to be thorough.

I've now reviewed the entire runtime surface. No need to read further — the picture is clear. Here's my audit.

Security audit: @harperfast/vite

Verdict: This plugin is well-secured against external attacks — notably better than most Vite integrations I see. I found no critical or high-severity vulnerabilities in the plugin's own code. The author has clearly threat-modeled the dangerous surfaces (dev server, SSR-before-auth, build coordination) and mitigated them. The findings below are residual risks and hardening items, not exploitable bugs.

The runtime code does the right things in the obvious places: no eval/child_process/shell in shipped code (the only execSync is in [scripts/setup-fixture.mjs](scripts/setup-fixture.mjs), a dev-only file not in the npm files allowlist); no path traversal (all file paths are fixed or config-derived, never request-derived); response sinks are controlled; req.url only ever flows to your own render() and to Vite (dev, behind auth).


The three external surfaces and how they're handled

1. Dev server HTTP surface (HMR mode) — ✅ strongly gated.
Vite's dev server exposes arbitrary file reads (/@fs/) and on-the-fly module transforms. The plugin puts a super_user HTTP Basic-auth check ([src/auth.ts](src/auth.ts)) ahead of the entire Vite middleware chain via chain(authenticate, vite) in [development.ts:34](src/development.ts:34). This is exactly the right architecture: it does not rely on Vite's own server.fs.deny (which has had a steady stream of bypass CVEs through 2024–2025) — an unauthenticated attacker can't reach /@fs/ at all. Two details done correctly:

  • Uses authenticateUser (always validates the password) and explicitly avoids getUser, which skips validation for null passwords — [auth.ts:56-62](src/auth.ts). That's a real footgun they sidestepped.
  • Strict === true super_user check ([auth.ts:9](src/auth.ts:9)), not a truthy check.

2. Production SSR render — ✅ safe in the plugin, by-design risk is on you.
The handler is registered runFirst: true ([production.ts:204](src/production.ts:204)) so it renders for anonymous visitors before authentication. The plugin itself is clean here: fixed template path, function-replacer to avoid $&/$$ replacement-pattern corruption ([production.ts:177](src/production.ts:177)), HEAD short-circuit, no reflection of req.url by the plugin. The risk it creates is documented (README §"SSR renders before authentication") and is genuinely your responsibility — see finding M2.

3. Production static serving — ✅ not this plugin, and the SSR bundle is hidden.
Serving is delegated to Harper's static plugin. The SSR server bundle is deliberately built to node_modules/.harper-vite-ssr ([production.ts:29](src/production.ts:29)) so static (pointed at dist) never serves your server code. Good.


Findings (residual / hardening)

# Severity Finding
M1 Medium (dev/hmr only) HMR WebSocket is not covered by the auth gate.
M2 Medium (by design) SSR render(url) runs unauthenticated with no user context.
L1 Low authorizeLocal + reverse proxy can silently bypass the dev gate.
L2 Low Unauthenticated SSR render is a DoS amplifier.
L3 Info Defense depends on keeping Vite patched.

M1 — HMR WebSocket bypasses the gate. Setting hmr: true in middleware mode ([development.ts:20](src/development.ts:20)) makes Vite stand up a separate WebSocket server (default port 24678) that is not routed through Harper, so the super_user gate doesn't apply to it. The README correctly warns "keep it bound to localhost," but the plugin leaves the binding to Vite's defaults rather than pinning it. The WS is mostly a notification channel (the dangerous file-read endpoints are HTTP and are gated), so impact is limited — but it shouldn't depend on a default. Recommendation: explicitly constrain it (e.g. server: { hmr: { host: 'localhost' } }), and/or log a prominent startup warning when hmr is on but DEV_MODE is off (someone forced the dev server in a deployed context). handleApplication currently logs the mode neutrally ([index.ts:19](src/index.ts:19)).

M2 — SSR render is pre-auth and context-free. Because the handler is runFirst, your render(url) receives the raw attacker-controllable URL with no authenticated user. Any tables.X.get() inside render runs without that user's authorization. This is documented, but it's the single most likely way your app leaks data: validate/encode url before routing or reflecting it, and enforce authz inside the render path. The plugin can't fix this for you — but it's the thing to double-check in your entry-server.

L1 — Proxy trust can bypass the dev gate. The gate passes loopback requests because Harper auto-authorizes them as super_user (authorizeLocal). If harper dev ever sits behind a reverse proxy that Harper trusts for client-IP, an external request appearing as loopback would sail straight past the gate. This is a Harper/deploy config concern, not plugin code — but it directly bears on "exposing to external attacks," so: don't run harper dev behind an IP-trusting proxy, and don't expose it to the internet at all (run hmr: false).

L2 — DoS via the public SSR endpoint. Any GET/HEAD with Accept: text/html triggers render() with no auth/rate-limit. Mitigated by the HEAD short-circuit and the README's caching guidance, but worth a deliberate cache/rate-limit in front of an expensive render.

L3 — Keep Vite current. Your real protection for the dev server is the auth gate, not Vite's fs.deny. Still, keep Vite patched (you're on peer >=7, dev on 8 — good) so the gated surface stays sound in depth.


Bottom line

You are not carelessly exposing servers. The dangerous dev endpoints are auth-gated with a correct, defense-in-depth design, and the production path delegates serving to static while keeping server code out of the served directory. The one code-level improvement I'd actually make is M1 (pin the HMR WebSocket to loopback + warn when the dev server runs outside DEV_MODE); everything else is deployment discipline (M2/L1/L2) that your README already documents well.

Want me to implement M1 — explicitly bind the HMR WebSocket to localhost and add a startup warning when hmr is enabled without DEV_MODE? It's a small, contained change to [development.ts](src/development.ts) and [index.ts](src/index.ts).

It isn’t as dangerous as the HTTP endpoints for HMR, but it bears protecting, too.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants