feat(weaverse): per-request projectId resolver + 24h cookie for Cloud Sandbox#383
Closed
paul-phan wants to merge 1 commit into
Closed
feat(weaverse): per-request projectId resolver + 24h cookie for Cloud Sandbox#383paul-phan wants to merge 1 commit into
paul-phan wants to merge 1 commit into
Conversation
Phase 1.5 Slice 1.5c of Weaverse/builder#2222 (Cloud Sandbox). A single sandbox machine serves multiple projects (M:N from Slice 1.5a). The Weaverse Hydrogen SDK already resolves `?weaverseProjectId=` from the URL query with highest priority \u2014 the missing piece is that once the user clicks an in-iframe link and the query string is gone, the SDK falls through to env.WEAVERSE_PROJECT_ID and the wrong theme renders. THIS PR adds a tiny cookie bridge: app/lib/weaverse/resolve-project.server.ts (NEW, ~75 LOC) readProjectIdFromCookie(request) -> string Validates against the same alphanumeric/hyphen/underscore pattern the SDK uses internally (rejects poisoned cookies that could try header injection or other shenanigans). Returns '' on miss so the SDK keeps falling through to env. maybeSetProjectIdCookie(request, response) -> void If URL has ?weaverseProjectId=<id> with a valid value, appends a Set-Cookie header to the response with: - Max-Age: 24h - SameSite=Lax (Strict would break in-iframe nav) - Secure + HttpOnly - Path=/ app/.server/context.ts Pass `projectId: () => readProjectIdFromCookie(request)` to WeaverseClient. The SDK's priority chain handles the rest: 1. ?weaverseProjectId= URL param (Studio iframe ALWAYS sets this) 2. our cookie function (in-iframe nav fallback) 3. env.WEAVERSE_PROJECT_ID (bare URL fallback) server.ts After handleRequest returns AND after session.commit(), call maybeSetProjectIdCookie(request, response). Order matters: session.commit() writes Set-Cookie via .set() (not .append()), which would clobber a cookie written earlier. By running our .append() afterward, both the session cookie and the project cookie ride along on the same response. Comment in code documents this so a future refactor doesn't reorder them. ZERO SDK CHANGES \u2014 verified @weaverse/hydrogen v5.13+ already accepts `projectId: string | (() => string) | (() => Promise<string>)` and runs the priority chain server-side. SECURITY NOTES - Cookie value is validated with /^[a-zA-Z0-9_-]{1,128}$/ on BOTH read and write paths. Poisoned cookies are silently ignored (return ''). - HttpOnly: not needed for our usage (we never read this cookie client-side) but defense-in-depth against XSS-induced exfiltration. - SameSite=Lax: chosen over Strict because the cookie is for the sandbox origin (<handle>.weaverse.dev), which IS the top-level when the SDK reads it. The Studio iframe lives on studio.weaverse.io so its cross-origin status is irrelevant \u2014 the cookie is set/read on its own domain. - Studio iframe ALWAYS sets the explicit query param when loading (per Phase 1.5 spec rule). The cookie is a fallback for in-iframe navigation only, never trusted cross-tab. VERIFICATION npm run typecheck 1 pre-existing error (bun cache duplicate @shopify/hydrogen versions, unrelated, present on bare main), 0 new npm run biome 3 useBlockStatements style warnings (biome exits 0), 0 errors NO UNIT TESTS Pilot uses Playwright e2e only; no unit-test runner is set up. The resolver is small (regex + cookie parse + Set-Cookie header build) and will be exercised by real traffic from Phase 1.5d onward. Adding a Vitest setup just for this module would be scope creep. Spec: .specs/2026-05-12--cloud-sandbox-phase-1/README.md in Weaverse/builder, section 'Phase 1.5 Slice 1.5c'. Stacks logically on Weaverse/builder PRs #2326 (schema) and #2327 (API). Independent at the git level \u2014 deploys to the weaverse-sandbox container and goes live as soon as merged + new theme image is built.
Member
Author
|
Closing \u2014 moving the implementation into Pilot is a starter template that customers fork and maintain on their own schedule; we can't ship cookie + resolver changes here without risking breakage in downstream forks. The proper place for multi-project preview support is the SDK package itself, where:
New PR coming on the Weaverse monorepo SDK. Will link back here. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1.5 Slice 1.5c of Weaverse/builder#2222 (Cloud Sandbox).
A single sandbox machine serves multiple projects (M:N preview model from builder#2326). The Weaverse Hydrogen SDK already resolves
?weaverseProjectId=<id>from the URL query with highest priority — the missing piece is that once the user clicks an in-iframe link and the query string disappears, the SDK falls through toenv.WEAVERSE_PROJECT_IDand the wrong theme renders.This PR adds a tiny cookie bridge: ~95 LOC, 3 files, zero SDK changes.
How it fits the SDK's existing priority chain
Slot 2 was unused. We fill it with
() => readProjectIdFromCookie(request). Cookie is set on the way out whenever slot 1 fires, so in-iframe navigation after the initial load (which loses the query string) still resolves the same project.Files
app/lib/weaverse/resolve-project.server.ts(new)readProjectIdFromCookie+maybeSetProjectIdCookie. ~75 LOC.app/.server/context.tsprojectId: () => readProjectIdFromCookie(request)toWeaverseClient.server.tsmaybeSetProjectIdCookie(request, response).Why the call is AFTER session.commit()
This is the subtle bit.
server.tsalready does:.set()(not.append()) clobbers any existingSet-Cookieheader. If I.append()my project cookie before that line, it gets nuked.So the order is:
handleRequest→ response builtsession.commit()→ session cookie.set()maybeSetProjectIdCookie→ project cookie.append()Both cookies ride along on the same response. There's an inline comment in
server.tsdocumenting this so a future refactor doesn't reorder them.Security
/^[a-zA-Z0-9_-]{1,128}$/on both read and write paths. Poisoned cookies silently return'', falling through to env.Max-Age=86400; Path=/; SameSite=Lax; Secure; HttpOnlyLaxoverStrict: cookie is for the sandbox origin (<handle>.weaverse.dev) which IS top-level when the SDK reads it. Strict would break in-iframe nav.HttpOnly: not strictly needed (we never read client-side) but defense-in-depth.?weaverseProjectId=query param when loading (per Phase 1.5 spec). Cookie is a fallback for in-iframe nav only, never trusted cross-tab.Test plan
npm run typecheck— 1 pre-existing error (bun cache duplicate@shopify/hydrogenversions, unrelated, present on baremain), 0 new.npm run biome— 3useBlockStatementsstyle warnings, exit 0.<handle>.weaverse.dev/?weaverseProjectId=A, click an in-iframe link, confirm the response still uses project A (not env default). Then load<handle>.weaverse.dev/?weaverseProjectId=B, confirm cookie is updated and B renders.Why no unit tests
Pilot uses Playwright e2e only; no unit-test runner is configured. The module is small (regex + cookie parse +
Set-Cookieheader build). Real traffic from Phase 1.5d onward will exercise it within a few days of merge. Adding a Vitest setup just for this would be scope creep.If you want me to add a Vitest setup anyway, happy to in a follow-up — but for one 75 LOC module on a tight schedule, the cost-benefit doesn't pencil out.
Rollout
Deploys to the
weaverse-sandboxPilot theme image. Once merged, the nextbun run build-theme.shinweaverse-sandboxpicks it up and bakes it into a new theme image digest. Then the next sandbox machine boot uses it. Existing running sandboxes pick it up on next image refresh.No breaking changes for non-sandbox Pilot deployments — the cookie is only ever read when set, and only ever set when the query param is present.
Refs
Weaverse/builder#2222Phase 1.5 Slice 1.5c. Spec:Weaverse/builder/.specs/2026-05-12--cloud-sandbox-phase-1/README.md.Stacks logically on:
Independent at the git level — this can merge any time.