Repository navigation
feat: rewrite v2 - #206
feat: rewrite v2#206Exeloo wants to merge 2 commits into
Conversation
| import { type ApiFeature, EnvSchema } from './env.schema'; | ||
|
|
||
| export const loadEnv = ( | ||
| source: Record<string, string | undefined> = process.env, |
There was a problem hiding this comment.
Reading env at runtime cause the Editor to run in offline mode in prod (before we were building with env) : either change how we get env, or set the PUBLIC_MODE in run time
| pruneTimer.unref?.(); | ||
|
|
||
| /** Blocks cross-site requests: a local editor must not be driven by random web pages. */ | ||
| const originAllowed = (request: Request): boolean => { |
There was a problem hiding this comment.
Vulnerable to DNS rebinding : you only check Origin === Host, but never Host A page on evil.com that rebinds its DNS to 127.0.0.1 gets matching headers and the full RPC
| }; | ||
|
|
||
| const assertWritable = (path: string) => { | ||
| if (path === PACKAGES_DIR || path.startsWith(`${PACKAGES_DIR}/`)) { |
There was a problem hiding this comment.
Also unallow access to .git, writing inside it can cause RCE at clone, and leak tokens
| /** Build outputs of running games: `/runtime/<projectId>/<app>/<file>`. */ | ||
| const handleRuntime = async (request: Request, url: URL): Promise<Response> => { | ||
| const [projectId, appSegment, ...rest] = url.pathname.slice('/runtime/'.length).split('/'); | ||
| if (!projectId || !appSegment || !rest.length || env.mode !== 'OFFLINE') |
| } | ||
| if (dir === root || dirname(dir) === dir || relative(root, dir).startsWith('..')) break; | ||
| } | ||
| if (!packageDir || relative(root, packageDir).startsWith('..')) return []; |
There was a problem hiding this comment.
Vulnerable to already pushed symlinks in node_modules
|
|
||
| constructor(private readonly _env: EditorEnv) {} | ||
|
|
||
| resolve(cookieHeader: string | null | undefined): SessionResolution { |
There was a problem hiding this comment.
It seems like the token signature is never checked, just decoded and trusted, someone with just the user id could forge a token
| /** Returns an opened project the session may access, else throws NOT_FOUND. */ | ||
| get(session: Session, id: string): OpenProject { | ||
| const project = this._projects.get(id); | ||
| const allowed = session.mode === 'OFFLINE' || session.projects.has(id); |
There was a problem hiding this comment.
Open projects outlive the user who opened them, since session is holded by the browser
| } | ||
| let disposed = false; | ||
| editor.projects | ||
| .open(ref) |
There was a problem hiding this comment.
Opens any ?path= with no confirmation, and opening runs nanoforge.config.ts and the project's server plugins: any website can link localhost:3000/load?path=<cloned repo> and get RCE. Needs a trust prompt for paths not in recent.
| * Plugins that its packages suggest and that aren't installed are named in a notification, once | ||
| * per project. | ||
| */ | ||
| export const followProjectPlugins = (options: { |
There was a problem hiding this comment.
Project plugins' client code loads in ONLINE too (only the server entry is gated): anyone who can write .nanoforge/plugins in a shared project runs JS in other users' editor. Don't load them in ONLINE, or make it opt-in.
|
|
||
| const CONTENT_TYPES: Record<string, string> = { | ||
| '.css': 'text/css; charset=utf-8', | ||
| '.html': 'text/html; charset=utf-8', |
There was a problem hiding this comment.
.html / .svg from projects are served inline on the editor origin, no CSP, no nosniff: stored XSS in ONLINE. Send Content-Security-Policy: sandbox + nosniff, or serve them as attachments.
|
|
||
| /** Clones `url` into `path`, injecting a token for https remotes (GitHub style). */ | ||
| clone(url: string, path: string, token?: string): Promise<void> { | ||
| const remote = token ? withToken(url, token) : url; |
There was a problem hiding this comment.
Token put in the clone URL: visible in ps and saved in .git/config. Pass it with -c http.extraHeader or GIT_ASKPASS instead.
| clone(url: string, path: string, token?: string): Promise<void> { | ||
| const remote = token ? withToken(url, token) : url; | ||
| return this._mutex.run(path, async () => { | ||
| await this._git(this._env.fsRoot, ['clone', '--', remote, path]); |
There was a problem hiding this comment.
Clone and pull have no timeout (network is false): a remote that never answers holds the _opening lock forever.
| }); | ||
|
|
||
| /** Default merge of two commands with the same merge key: first undo, last redo. */ | ||
| export const mergeCommands = (previous: HistoryCommand, next: HistoryCommand): HistoryCommand => ({ |
There was a problem hiding this comment.
Merged undo only calls previous.undo(): mergeKey on document edits leaves next applied. No caller passes it yet, but the SDK exposes it.
| roots.found.push(root); | ||
| } | ||
| this.setFiles(files); | ||
| } catch {} |
There was a problem hiding this comment.
This empty catch {} hides failures to resolve engine types, which then show up as false errors in Problems.
| const text = await readFile(join(this.root, PACKAGES_FILE), 'utf8').catch(() => undefined); | ||
| if (text === undefined) return {}; | ||
| try { | ||
| return JSON.parse(text) as Record<string, unknown>; |
There was a problem hiding this comment.
A corrupt lock throws a raw SyntaxError instead of a RegistryError.
| * - `static`: the bundled Prettier with the closest JSON/YAML config, without plugins. | ||
| */ | ||
| export class Formatter { | ||
| private readonly _projectPrettier = new Map<string, Promise<Prettier | undefined>>(); |
There was a problem hiding this comment.
A missing Prettier is cached forever: installing it afterwards is ignored until restart.
| "dependencies": { | ||
| "chokidar": "catalog:server", | ||
| "prettier": "catalog:server", | ||
| "typescript": "catalog:build", |
There was a problem hiding this comment.
typescript is in both dependencies and devDependencies.
What does this PR do?
Rewrites the NanoForge editor from scratch (
rewrite/v2). It keeps what the previous editor did (edit a NanoForge game in the browser, hosted or throughnf editor) on a new base: everything visible is a plugin, the game's code is the only source of truth, and every change can be undone. The full list of user-facing changes is inCHANGELOG-v2.md; the plan, phase by phase, is indocs/rewrite-plan.md.Resolves #174
Resolves #156
Resolves #154
Resolves #128
Resolves #127
Resolves #126
Resolves #125
Resolves #123
Resolves #97
Architecture
apps/editor(SvelteKit static SPA + Bun server),packages/*(kernel, sdk, settings, history, layout, ui, code, runtime, protocol, rpc, project, server-core, meta, registry),plugins/*(12 built-in plugins),tooling/*(vite plugin for building plugins, example plugin).@nanoforge-dev/editor-sdk(enforced by ESLint). The SDK and@nanoforge-dev/editor-vite-pluginare the public API for third-party plugins (guide indocs/docs/plugins)..nanoforge/*.save.json. Entities, components and systems are read from and written to the app'smain.tsthrough a TypeScript code worker (ts-morph stays in the worker, out of the page bundle).nanoforge.manifest.json,@scope/name, packages installed read-only innf_modules, plugins per user or per project. Shared libraries between client and server (ADR 0004).docs/api/documents the registry and settings-sync contracts the API does not offer yet; contract tests run against stand-ins (API_FEATURESgates the calls).index.tsbarrels, kebab-case files named after their main export, kind suffixes (.type.ts,.enum.ts,.exception.ts,.const.ts), tests intest/**/*.spec.tsmirroringsrc/.Built-in plugins
code-editor, file-manager, viewport, history-panel, settings-ui, ecs, console, command-palette, inspectors, git, packages, scene.
Repository
.prettierignore, labels and issue-form entry; published workspaces (apps/editor,packages/sdk,tooling/vite-plugin) have their changelog and release config.e2eworkflow; the old editor's files are removed.Not in this PR
feat/core-editor-bridge(andfeat/scenefor scenes), loaderfeat/server-editor-ipc, CLIfeat/editor-open-project(nf editor,nf create plugin).docs/api/).How do you test this PR?