Skip to content

Inject the deployment secret into server builds (solidjs/solid#3239) - #343

Merged
ryansolid merged 2 commits into
nextfrom
flash-deployment-secret
Sep 8, 2026
Merged

Inject the deployment secret into server builds (solidjs/solid#3239)#343
ryansolid merged 2 commits into
nextfrom
flash-deployment-secret

Conversation

@ryansolid

@ryansolid ryansolid commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

Companion to the encrypted no-JS flash cookie landing in @solidjs/web (solidjs/solid#3239, commit solidjs/solid@fbe5bef4). The flash carries the submitted form input — whatever the user typed — so the runtime now AES-GCM encrypts it under a key derived (domain-separated) from the deployment secret, and withholds the flash entirely when no secret exists.

This PR provides that secret with zero configuration through the internal bundler contract:

globalThis.__SOLID_SECRET__ ??= "<random-per-build>";

emitted as the first statement of the generated server-function handler module (virtual:solid-server-function-handler).

Why this shape

  • Once per plugin instance (node:crypto randomBytes(32)): a production build bakes one value into the emitted server chunk, so every instance of that deployment shares it — the runtime requires this (a per-process value would silently lose flashes behind a load balancer). A dev session holds one value for its lifetime; a restart invalidates in-flight flashes, which are 60-second one-shot cookies — the next render just reads "no flash".
  • The handler module covers both halves: every dispatch surface (dev middleware, prod handler) loads it before serving a form POST (the encode), and the generated SSR handler imports it at module load, so the secret is in place for the render that follows the redirect (the decode).
  • Server-only by construction: the handler module already hard-errors when imported from a client graph, so the secret never rides client output.
  • ??= keeps explicit config authoritative: configureServerFunctionsServer({ secret }) outranks the global in the runtime's resolution order, and an outer harness that injects its own __SOLID_SECRET__ earlier wins over this snippet.
  • __SOLID_SECRET__, not a flash-specific name: the secret is the deployment-wide concept; the runtime derives per-purpose keys from it under domain strings (solid.flash.v1 today), so future features (and other tooling) share one contract without key-material overlap.

Verification

Built examples/start-ssr: dist/server/server.js leads with the ??= line carrying a 64-hex value; the client output contains no reference. pnpm build and the example suite pass.

Draft until @solidjs/web 2.0.0-rc.7 ships the encrypted flash runtime this pairs with.

Made with Cursor


Review against the published rc.7 runtime (@solidjs/web/server-functions/dist/server*.js): resolveSecret() is configuredSecret !== undefined ? configuredSecret : globalThis.__SOLID_SECRET__, read lazily per encode/decode and required to be a non-empty string; the key is SHA-256("solid.flash.v1\0" + secret). The ??= snippet matches that contract exactly — explicit configureServerFunctionsServer({ secret }) and an earlier-injected global both keep precedence. Built examples/start-ssr: the snippet opens the virtual:solid-server-function-handler region of dist/server/server.js with a 64-hex value; dist/client has no reference.

e2e (9e17922): the middleware suite now drives the no-JS convention end to end in dev and prod — form-navigation POST → 303 to the referrer carrying flash=1.… (Max-Age=60, SameSite=Lax, HttpOnly) → the following render decrypts it (src/setup.tsx via decodeFlashCookie), surfaces url/result/input, and clears the cookie. Red→green: with the plugin built from next (no secret) 8 of the 12 new assertions fail (303 goes out plain, no Set-Cookie, no flash on the render); with this branch 67/67. The POST uses node:http because undici forces Sec-Fetch-Mode: cors, which the runtime rightly refuses as a scripted call.

Full gate on the rebased branch: ssr 12/12 + 8/8, css-matrix 87/87 + 19/19, start-ssr 498/498 + http-bridge 10/10 + components-warning 9/9, start-client 45/45, start-env 47/47.

@changeset-bot

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9e17922

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@solidjs/vite-plugin Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@solidjs/vite-plugin@343

commit: 9e17922

ryansolid and others added 2 commits September 8, 2026 10:22
…3239)

The runtime's no-JS flash cookie is AES-GCM encrypted as of
@solidjs/web's #3239 fix — it carries the submitted form input, so
without a key the outcome is withheld entirely. This provides the key's
secret with zero configuration through the internal
globalThis.__SOLID_SECRET__ ??= contract: generated once per plugin
instance via node:crypto randomBytes, emitted as the first statement of
the generated server-function handler module, which every dispatch
surface loads and the generated SSR handler imports at module load — so
the secret is in place for both the flash's encode (the form POST) and
its decode (the render that follows the redirect).

Per plugin instance means a production build bakes one value into the
emitted server chunk — every instance of that deployment shares it,
which the runtime requires (a per-process value would silently lose
flashes behind a load balancer) — and a dev session holds one for its
lifetime; a restart invalidates in-flight flashes, which are 60-second
one-shot cookies. The handler module is hard-gated server-only, so the
secret never reaches the client graph, and ??= keeps an explicit
configureServerFunctionsServer({ secret }) — or an outer harness's
injected value — authoritative.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drives the no-JS server-function convention end to end in the middleware
suite (mw-dev and mw-prod): a form navigation POST to a server function's
bare address answers 303 back to the referrer with the encrypted flash
cookie (`flash=1.…`, Max-Age=60, SameSite=Lax, HttpOnly), the render that
follows decrypts it — src/setup.tsx plays the integration's half via
decodeFlashCookie and surfaces url/result/input as a marker — and clears
the one-shot cookie. Against @solidjs/web 2.0.0-rc.7 without the
deployment secret the redirect goes out plain (no Set-Cookie) and the
render sees no flash — 8 of the 12 new assertions fail; with the
`globalThis.__SOLID_SECRET__ ??=` snippet they pass.

The POST goes through node:http rather than fetch: undici treats the
Sec-Fetch-* names as forbidden request headers and sends its own
`Sec-Fetch-Mode: cors`, which the runtime rightly refuses (400) as a
scripted call to the bare address.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid force-pushed the flash-deployment-secret branch from 4479860 to 9e17922 Compare September 8, 2026 17:28
@ryansolid
ryansolid marked this pull request as ready for review September 8, 2026 17:31
@ryansolid
ryansolid merged commit 8cb24c0 into next Sep 8, 2026
6 checks passed
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.

1 participant