feat(den-gateway): serve the app and route each user to their own instance - #3191
Merged
Conversation
…tance First slice of ui.openworklabs.com. den-gateway serves the OpenWork web build and proxies each signed-in user to their own Cloud instance, injecting that instance's credentials server-side. The browser never receives an instance token or a preview URL, and never learns which compute vendor is behind it. The security boundary is the point: the gateway holds no Daytona key, never imports the SDK, and has no database access. den-api remains the only thing that talks to Daytona. The gateway also cannot assert identity on its own - it acts strictly on behalf of the caller's Den session, so den-api's per-process internal-principal secret stays untouched. den-api gains GET /v1/cloud/gateway/resolve: the caller's own session plus a shared gateway secret (timing-safe, and the route 404s when the secret env is unset, so it does not exist on deployments without a gateway). It returns the instance URL and collaborator token only when ready, never the host token, and reuses the existing per-user state machine rather than forking it. Routing is the subtle part. Every allowlisted entry matches as a namespace, because /files/sessions/*, /workspaces/local, /workspaces/:id/activate, /env/keys and /approvals/:id would otherwise fall through and answer an API caller with HTML. And /workspace/ is genuinely ambiguous - openwork-server owns API routes there while the BrowserRouter SPA owns reloadable session/settings deep links - so document navigations serve index.html while everything else proxies. /opencode/ and /w/ are always API, since openwork-server claims all of /w/. Streaming correctness is deliberate: the SPA holds two permanently-open SSE streams, so bodies are piped rather than buffered, content-encoding and transfer-encoding are stripped (undici already decoded), no idle timeout is imposed, and upstream caching passes through. Static serving mirrors openwork-server: SPA history fallback, immutable /assets caching, and a hard 404 for a missing hashed asset so a stale index surfaces as a 404 instead of HTML parsed as JavaScript. Gateway health lives at /__gw/health because /health belongs to the instance. Lands the approved voice-over and scaffolded flow for the den-gateway demo. Slice 1 only: apps/app is not wired to it yet and no deployment config is included. Verified: den-gateway builds, 10 gateway tests pass (static, SPA fallback, asset 404, traversal, injection, resolve cache, non-ready, SSE non-buffering, gzip, and every routing case above); 24 den-api cloud tests pass including the new resolve gating.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Slice 1 of
ui.openworklabs.com.ee/apps/den-gatewayserves the OpenWork web build and proxies each signed-in user to their own Cloud instance, injecting that instance's credentials server-side.Spec is the approved voice-over at
evals/voiceovers/den-gateway.md— the demo line being "I never learn what a sandbox is."The boundary is the feature
randomBytes(32)per process specifically to prevent cross-service impersonation, and this PR does not touch it.den-api:
GET /v1/cloud/gateway/resolveCaller's own session bearer plus a shared gateway secret (
timingSafeEqual); the route 404s when the secret env is unset, so it doesn't exist on deployments without a gateway. Returns URL + collaborator token only whenready, never the host token, and reuses the existing per-user state machine instead of forking it.Routing — the part worth reviewing
Two bugs were caught in review before this landed:
Namespace matching. The allowlist originally matched exactly, so
/files/sessions/*,/workspaces/local,/workspaces/:id/activate,/env/keysand/approvals/:idfell through and would have answered API callers with HTML — breaking file sessions, workspace create/activate, env management, and approving a write (frame 5 of the script). Every entry now matches as a namespace so this can't recur as the API grows./workspace/is genuinely ambiguous. openwork-server owns API routes there; theBrowserRouterSPA owns reloadable deep links (/workspace/:id/session/:sessionId,/workspace/:id/settings/*). A blanket prefix would proxy a user's reload to the instance — breaking frame 7 ("the same address opens my workspace"). Document navigations now serveindex.html, everything else proxies, via one named function with a comment so it doesn't get "simplified" back into a bug./opencode/and/w/are always API, since openwork-server claims all of/w/.Streaming correctness
The SPA holds two permanently-open SSE streams, so: bodies piped not buffered,
content-encoding/transfer-encodingstripped (undici already decoded — same fix openwork-server'ssanitizeProxyResponsemakes), no idle timeout, upstream caching passed through. These are exactly the four thingsden-worker-proxygets wrong, which is why this is written fresh rather than forked.Static serving mirrors
apps/server/src/static-ui.ts: SPA history fallback, immutable/assetscaching, traversal rejection, and a hard 404 for a missing hashed asset so a stale index surfaces as a real 404 instead of HTML parsed as JavaScript. Gateway health is/__gw/healthbecause/healthbelongs to the instance.Tests
/workspace/…/session/…reload.any/as.Scope
Slice 1 only —
apps/appis not wired to the gateway yet (slice 2: the host adapter + sending the Den bearer instead of an instance token) and no deployment config (slice 3: GHCR image + Render service + serving the SPA build). Nothing in production changes by merging this.