Skip to content

feat: add online handling - #220

Merged
Exeloo merged 5 commits into
fix/improve-plugin-ecosystemefrom
feat/handle-online
Nov 5, 2026
Merged

Exeloo merged 5 commits into
fix/improve-plugin-ecosystemefrom
feat/handle-online

Conversation

@Exeloo

@Exeloo Exeloo commented Oct 30, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Handle online mode.

How do you test this PR?

@Exeloo
Exeloo requested a review from Tchips46 as a code owner October 30, 2026 14:16
@github-actions github-actions Bot added documentation Improvements or additions to documentation apps:editor Related to Editor app packages:rpc Related to RPC package packages:server-core Related to Server core package labels Oct 30, 2026
@Exeloo
Exeloo added this pull request to stack #212 October 30, 2026 16:17
@Exeloo
Exeloo force-pushed the feat/handle-online branch from de822a7 to 4df7213 Compare November 1, 2026 13:21
@github-actions github-actions Bot added plugins:ecs Related to ECS plugin plugins:git Related to Git plugin labels Nov 1, 2026
@Exeloo
Exeloo force-pushed the feat/handle-online branch from 4df7213 to 4f1bc83 Compare November 1, 2026 14:27
@Exeloo
Exeloo force-pushed the feat/handle-online branch from 4f1bc83 to 13508c1 Compare November 1, 2026 14:52
@Exeloo
Exeloo force-pushed the feat/handle-online branch 2 times, most recently from 85b8094 to edb24ff Compare November 5, 2026 11:41
@Exeloo
Exeloo force-pushed the feat/handle-online branch from edb24ff to ab91c78 Compare November 5, 2026 11:55
@github-actions github-actions Bot added packages:ui Related to UI package and removed plugins:ecs Related to ECS plugin plugins:git Related to Git plugin labels Nov 5, 2026

@Tchips46 Tchips46 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.

Review: online handling (4fa9f45)

Line numbers are at this PR's head. Checked and fine: the launch token (192-bit randomBytes, timingSafeEqual, stripped by the 303 before the page loads, checked on every route after the host and origin guards including the WS upgrade, HttpOnly + SameSite=Strict cookie), readBody capping while streaming, frame-ancestors 'self' kept, inline scripts allowed by hash, the rpc-session finish ordering, bounded session growth, and the archive skipping symlinks inside the jail.

Must fix

1. An API outage signs everyone out for 60 s (packages/server-core/src/session/session-store.ts:142-161, with api/api-client.ts:210-228 and project/project-registry.ts:188-189)

  • currentUser re-throws anything that isn't a 401, so the new 15 s timeout (UNAVAILABLE) and any 5xx land in .catch(() => null). That clears session.projects and caches rejectedUntil for those same cookies.
  • Example: the API is slow for 20 s while users reload. Each of them is user: null for a full minute after the API is back, every project call returns NOT_FOUND (new session.user !== null gate), retrying does nothing because the cookies are the same, and they must reopen each project.
  • Fix: cache the rejection only when verify resolved null (a real 401). On a thrown error, leave the user, projects and verification as they were.

2. The page policy cuts local games off from anything but loopback (packages/server-core/src/http/shell-csp.ts:16, 27)

  • Games are import()ed into the editor page (packages/runtime/src/service/runtime-service.ts:402), so they run under its policy, and connect-src only allows localhost / 127.0.0.1.
  • Example: a game that fetches https://… (a leaderboard, assets from a CDN), connects to a wss:// or LAN server, or uses [::1] works with nf start and breaks in the editor. The code is already trusted and given 'unsafe-eval', so the restriction buys little.
  • Fix: with games, use connect-src * (or add https: wss: ws:). Without it, drop the local list, since hosted editors run no games. The plugin guide doesn't mention the new policy either.

Should fix

3. Zipping a folder blocks the event loop (packages/server-core/src/fs/archive.ts:53-55)

  • The download moved from fflate's worker-based zip() to ZipDeflate.push(wholeFile, true), which deflates synchronously on the main thread. A 500 MiB asset stalls every WebSocket and RPC call for seconds, for every user of a hosted editor.
  • A file deleted between list and readFile (a build writing dist/) throws ENOENT after the 200 has gone out, so the user gets a truncated zip.
  • Fix: AsyncZipDeflate (same API, in fflate 0.8.3), and skip entries that throw ENOENT.

4. The token cookie isn't scoped to a port (packages/server-core/src/server/launch-token.ts:5, 37)

  • Cookies ignore ports. With two editors on random (pnpm example on 4800 and nf editor on 3000), opening the second link overwrites the first editor's cookie, and the first tab's next RPC call or plugin chunk gets a 401.
  • The token is also sent to every localhost:* server the browser opens, so another user of a shared machine (the stated threat) serving localhost:8000 reads it.
  • Fix: name the cookie per port (nf_editor_token_<port>), and document that it still reaches other local ports.

5. nf editor with EDITOR_TOKEN=random doesn't work, but the docs recommend it (docs/docs/getting-started/create-your-first-project.mdx:32, apps/editor/server/main.ts:50-54)

  • The token is generated inside the server, and the CLI (editor.action.ts:22-28) opens http://localhost:3000/load?path=… without one: that tab gets the 401 page. The server prints only the root link, so the /load?path= target is lost.
  • Fix: print the token link for the /load?path= URL, or have the CLI generate the token and pass it through the environment. Until then, drop the docs line.

6. Anonymous callers can make the server buffer 100 MiB per request (packages/server-core/src/server/create-editor-server.ts:126)

  • readBody runs before the session is resolved. In a hosted editor, N parallel cookieless 99 MiB POSTs to /rpc/x are all held in memory (chunks, a copy, then a string, about 3× each).
  • Fix: resolve the session first, and cap at about 1 MiB unless session.user is set (or the call is a file write).

Nice to have

  • EDITOR_TOKEN length (packages/server-core/src/env/env.schema.ts, load-env.ts:43-48): a fixed EDITOR_TOKEN=abc can be brute-forced over loopback in milliseconds (no rate limit). Fix: refuse anything under 16 characters other than random.
  • Stray edits in breakout (examples/breakout/apps/client/src/scenes/levels.ts:17, 124, components/position.ts:11): an unused Ball import, brick 6 moved from 793,130 to 795,132, and a @color tag. They look like writes from a manual editor run. Fix: revert both files.
  • Deleted CHANGELOG-v2.md is still cited at CLAUDE.md:302 (phase 13 row) and docs/rewrite-plan.md:1082 (13.4). Fix: reword both.
  • shell-csp.ts:3-7 says nothing else may receive the page's data, but img-src https: is an open channel out. Fix: correct the comment, or narrow img-src in hosted mode.

Not checked: nothing was run. I couldn't check whether Monaco and the plugins work under the hosted policy, which has no 'unsafe-eval' (e2e only covers local mode).

Exeloo and others added 5 commits November 5, 2026 15:24
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- session store: tokens are checked on a copy and kept only once the API accepts them;
  a refusal (401, other 4xx, an answer that is not a user) signs the session out and is
  cached; an outage keeps the user only for the tokens the session already holds, and
  the next request asks again
- shell CSP: a local editor's page connects over http, https, ws and wss; a hosted one
  only to its own origin
- folder zips: files of 160 kB and more are compressed in a worker, one file at a time,
  and files deleted since the listing are left out
- launch token: the cookie name carries the port; EDITOR_TOKEN takes at least 16
  characters (or random)
- docs: EDITOR_TOKEN only for an editor started directly, the CSP in the plugin guide,
  the removed v2 changelog
- breakout: revert stray edits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Exeloo
Exeloo force-pushed the feat/handle-online branch from 4fa9f45 to 0c4fb10 Compare November 5, 2026 14:47
@github-actions github-actions Bot added the plugins:git Related to Git plugin label Nov 5, 2026
@Exeloo

Exeloo commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review. Addressed in 6acab04 (fix: address the online handling review).

Fixed

  • 1. Only a refusal signs a session out and is cached for a minute: verify resolving null, an RpcError other than UNAVAILABLE, or an answer that isn't a user (currentUser now returns null instead of throwing on a parse error).
    • On an outage (UNAVAILABLE, timeout, 5xx, network error), the session keeps its user and projects only if it presented the same tokens it already holds, and nothing is cached.
    • New tokens are checked on a copy: session.tokens changes only once the API accepts them, so a token change during an outage signs the session out (fail closed).
    • Specs cover each case.
  • 2. With games: connect-src 'self' http: https: ws: wss:. Hosted: 'self' only. The header comment no longer overstates the policy (it mentions img-src https:), and the plugin guide (plugins/overview.mdx) describes it.
  • 3. Files of 160 KB or more go through AsyncZipDeflate, smaller ones through ZipDeflate (the split zip() used). pull waits for a file's last chunk before reading the next, and files gone by ENOENT are skipped. Specs cover the worker path and the skip.
  • 4. The cookie is nf_editor_token_<port>. The docs say other local ports still receive it.
  • 5. The getting-started page documents EDITOR_TOKEN=random only for an editor started directly, and says not to set it with nf editor. Making nf editor generate the token and add it to the link is a CLI change for later.
  • Token length: values under 16 characters other than random are refused.
  • Breakout: the stray edits are reverted.
  • Changelog references: reworded in CLAUDE.md and docs/rewrite-plan.md.

Not changed

  • 6. The body cap before the session is a fair hardening point, but not a regression: the base read bodies uncapped, and Bun's default maxRequestBodySize (128 MB) already bounds them. Left for the hosted work (phase 14), along with narrowing img-src.

Checked at the top of the stack: lint, typecheck and unit tests pass, and e2e passed 109/109. Not covered, as you noted: Monaco and the plugins under the hosted policy.

@Exeloo
Exeloo merged commit 53d10e4 into main Nov 5, 2026
6 checks passed
@Exeloo
Exeloo deleted the feat/handle-online branch November 5, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apps:editor Related to Editor app documentation Improvements or additions to documentation packages:rpc Related to RPC package packages:server-core Related to Server core package packages:ui Related to UI package plugins:git Related to Git plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants