Main - #754
Conversation
Adds authentication so scenes belong to the user who creates them, using the database and infrastructure already in place — no new runtime dependency. Password hashing is node:crypto scrypt; sessions are opaque tokens stored hashed, delivered as an httpOnly SameSite=Lax cookie whose Secure flag follows x-forwarded-proto (the Hostinger proxy terminates TLS). Auth uses its own small mysql2 pool from the same PASCAL_MYSQL_URL and creates its two tables (users, user_sessions) at boot via instrumentation.ts, mirroring the scene store. - lib/auth: db (pool + migrate + authAvailable), password (hash/verify), session (token + cookie + getSessionUser), service (register/login/ session/logout + DIGITALTWIN_ADMIN_EMAIL admin seed), guard (per-scene mutation authorization). - API: POST /api/auth/register|login|logout, GET /api/auth/session — origin-guarded (no scene token), login rate-limited, force-dynamic. - Scenes: POST stamps ownerId from the session (401 when signed out); GET and /scenes list filter to the caller; PUT/PATCH/DELETE authorize against the scene's owner (403 across users, admin may edit any). Pre-existing null-owner scenes become unowned — absent from user lists, still openable by direct URL; a later admin panel manages them. - Client: SessionProvider + useSession, a dependency-free auth dialog, an AuthMenu on the editor and /scenes; create/save/save-as and the autosave path open the dialog when signed out and on a 401. - MySQL-only: with no database (SQLite dev) auth is disabled — endpoints report it, creation stays open and unowned, nothing regresses. - health reports auth: ok|disabled. Verified against MariaDB end to end: register sets a Secure cookie under forwarded https; session round-trips; a signed-in POST stamps owner_id; the list shows only the caller's scenes; a second user gets 403 deleting the first's scene; logout returns 204 and clears the session. Unit tests cover hashing and the cookie-secure logic; an env-gated integration test covers register/login/session/admin-seed. 306 mcp tests still pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
An admin-only /admin page (any other visitor gets a 404) that lists users and scenes and manages access, built on the role column and session already in place. - Users table: email, role, scene count, join date; promote/demote a user's role. An admin can't demote themselves out of the panel. - Scenes table: every scene with its owner; reassign a scene to any user or make it unowned; one-click adopt of all legacy null-owner scenes to the admin — the migration path promised when ownership landed. - API under /api/admin/* is guarded by role admin (403 otherwise); the page redirects non-admins to a 404. - AuthMenu shows an Admin link only to admins. Verified against MariaDB: a non-admin gets 404 on /admin and 403 on the APIs; DIGITALTWIN_ADMIN_EMAIL makes the first account admin; promote, self-demote guard, adopt-unowned, and scene reassignment (valid, invalid owner, make-unowned) all behave. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The variables typed into the hosting panel are the last place the old project name was still visible. Every configuration value is now read as DIGITALTWIN_<NAME>, falling back to PASCAL_<NAME> so a running deployment keeps working until its panel is updated. A shared readEnv() does the two-name lookup and treats a blank value as unset — control panels save an empty field rather than omitting it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The runner has no ssh key, so `bun install` fails to clone the two plugins pinned as git dependencies — every job dies before it reaches a test. Both plugin repositories are public, so rewriting the transport to https resolves the same commits with no credentials. This rewrites transport only: the lockfile still records the ssh URL, so --frozen-lockfile stays satisfied and the pinned commits do not move. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
This reverts commit 20761bd.
The ssh form cannot be cloned on a CI runner, which has no key, so every job died in `bun install` before reaching a test. The sibling plugin in the same file uses the `github:` shorthand and resolves fine in the same run. Same repository, same commit — only the transport changes. The lockfile still needs regenerating: `bun install` resolves `github:` specs through api.github.com, which this environment's egress policy blocks, so it could not be updated here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
A refused connection arrives as an AggregateError whose own message is empty, so the boot log printed a bare "AggregateError:" over a stack through minified chunks — naming neither the host, the port, nor the reason. A wrong port cost a live deploy and a log read to find. Unwrap the aggregate and name the misconfiguration each driver code points at, so the log line is the fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Some hosting panels drop their environment variables on redeploy, and because the database is required the app then refuses to start. Settings can now come from a file as well: `.env` beside the server, or `~/.digitaltwin.env`, which is the one that survives a release since the deployed directory is replaced wholesale. A real environment variable always wins, so a configured panel keeps precedence and nothing changes for a working deploy. The boot log names the files read and how many settings each supplied, never the values. The publish workflow now carries an existing `.env` across instead of force-pushing over it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Architects export from Revit or ArchiCAD as IFC, and until now the only way to get one into the editor was the separate converter app: convert there, download JSON, load it here. The conversion package already existed — this wires it into the editor as one button. Conversion runs in the browser. web-ifc is a WASM parser and a model is routinely tens of megabytes, so uploading the file first would buy nothing. Only the converted graph is posted, through the same endpoint as "Create new scene", so an import is owned by the signed-in user and stored like any other scene. The converter and its WASM load on first use, keeping ~1.5 MB out of the initial page for everyone who never imports a model. Only the two browser blobs are copied into public/; the node build would be dead weight in the deployed bundle. IFC support is upstream's own early alpha — exports vary wildly and elements can land wrong or not at all. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
An AI assistant can now edit scenes through the same operations the editor uses, over /api/mcp. The MCP tools have no notion of a user: left alone they write scenes with no owner, which appear in nobody's list yet can be opened and changed by anyone holding the link — a hole straight through the ownership rules. So access is per person. A bearer token, issued from the admin panel, resolves to a user, and a wrapper binds the scene store to them: writes are stamped with their id, lists are confined to what they own, and someone else's scene reads as missing rather than forbidden so an agent cannot probe for ids. Admins get no bypass here. They can already reach every scene through the panel; letting an agent inherit that would mean one leaked token edits the whole installation. Only the sha256 of a token is stored, as for sessions, and granting again replaces the previous token so access removed from a machine cannot be resurrected by an older copy. Sessions hold the agent's working scene between requests, dropped on idle and capped, since the host is one long-lived process. Not yet exercised against a live client — the endpoint compiles and the panel wiring is in place, but the round trip is untested. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
This reverts 02da3fb. The endpoint cannot work as written, and the reason is structural rather than a bug to chase. MCP's tools edit through the live scene store, and that store is a 'use client' module. Bundled into Next's server graph it is replaced by a client-reference stub that throws on every call — the built output literally contains `throw Error("... is on the client")`, which is the `getState is not a function` seen at runtime. Marking the packages external is the documented escape, but Next rejects it: the same packages must be transpiled for the editor UI, and `serverExternalPackages` and `transpilePackages` cannot both claim them. Carrying it meanwhile costs something real: a dead /api/mcp route and a migration creating an mcp_tokens table on a live database for a feature that does not exist. The work is preserved in 02da3fb and restores with `git revert` of this commit once the packaging question is settled — either vendoring the packages so Node loads them outside the bundle, or running MCP as its own process. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
plugin-warehouse was switched to private, so every job dies at `bun install` with a 404 on its tarball — GitHub's answer to an unauthenticated request for a private repository. Back to the git+ssh pin, and a key rather than a token. The `github:` shorthand fetches through api.github.com, and bun has no documented way to authenticate that request — the tracking issue is still open. bun does shell out to `git clone` for a git+ssh spec, so an ssh key works through git's own machinery. Its url-rewriting does not: a `url.insteadOf` rule has no effect on bun's clones, which I verified before abandoning that route. Needs a read-only deploy key on plugin-warehouse, with the private half stored as the PLUGIN_SSH_KEY secret. Without it the step says so and carries on, so the failure names its own cause. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Saving any scene containing a plugin node — every warehouse object, for one — failed with 400. The API validates each node against core's AnyNode, and AnyNode is a hand-maintained union of the HOST's kinds: by construction it cannot know a plugin's. The warehouse plugin's own source says exactly this and points at the answer — "the registry validates against def.schema at runtime." Do the same at the boundary: kinds claimed by a plugin manifest are validated against that plugin's schema, and everything else still faces AnyNode, so unknown kinds and malformed plugin nodes are refused as before. The plugin barrels already run server-side during SSR, so importing them in a route adds no new constraint. Proven against the built bundle on MySQL: a pallet built by the plugin's schema saves with 200, loads back, renders in the editor, and the editor's own autosave of that graph succeeds — the exact path that returned 400. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The console app's lib, components and migrations, copied under an isolated @panel alias so they cannot collide with the editor's @/*. Nothing imports them yet — routes, session bridge and the db-env shim land as separate steps.
The console app (ovurrsl/panel) now lives inside the editor: its screens mount under app/(panel) and its API routes under /api, all its code stays under the isolated @panel alias so a re-sync from its own repository cannot mix with editor code. `/` routes by session state — sign-in, 2FA, forced password change, console — and the editor moved to /editor with /scenes and /scene/[id] unchanged. Identity is the console's: argon2id passwords, TOTP, invitations, lockout, its own sessions table. The editor's account machinery (register/login routes, scrypt, the sign-in dialog) is gone; its getSessionUser() now validates the console session and folds the permission model down to the editor's two roles, so scene ownership, guards and the scenes admin keep working with console ULIDs as owner ids. A half-open session (2FA pending, password change due) counts as signed out. Portability shims, worth upstreaming to the panel repo: DIGITALTWIN_* database variables honoured before DATABASE_*; utf8mb4_0900_ai_ci and CAST(... AS JSON) and the functional index in 002 replaced with MariaDB-safe equivalents — shared hosting runs MariaDB, which has none of the three. Proven locally end to end against MariaDB: fresh migrate + seed, / → signin, Admin/Admin → forced password change (policy checks all pass) → console overview live, then /scenes under the same cookie, scene created with owner_id = the console admin's ULID. Not deployed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
Rack levels are now one table — each row a level with its clearance and its type. Fixes three measured bugs: the top level's clearance was never editable (rows counted fitted levels, not levels+1), a level that did not fit was hidden so its clearance could not be reduced to make it fit, and levelClears was never trimmed so overrides reappeared when the level count went down and back up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
plugin-warehouse went public, which changes the transport story: bun resolves any GitHub https/github: spec through the api.github.com tarball endpoint, which now needs no credential — so the ssh deploy-key steps come out of all four workflows and the pin becomes git+https. The lockfile cannot be regenerated from the sandbox: it records each GitHub tarball's sha512, and a locally-built tarball's hash would never match GitHub's bytes — verified by diffing what a local regeneration wrote. A one-shot Relock workflow runs `bun install` on a real runner and pushes the corrected bun.lock back to the branch instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
The dispatch endpoint only recognizes workflows present on the default branch; a path-filtered push trigger reaches the same one-shot effect.
A redeploy is now the entire upgrade. At boot, ensureConsoleSchema() recognizes where the database is in its history and brings it forward: the editor's old auth tables are renamed aside untouched, the console migrations run (tracked in schema_migrations), a settings row appears with 2FA optional, and every legacy account is carried over — Admin role preserved, scenes re-owned to the new ULID, and a temporary password printed once to the runtime log, since scrypt hashes cannot become argon2 ones and the log is where this deployment's operator reads. Re-runs are no-ops at every step. Turbopack requires an externalized native package under a build-specific hashed alias it never creates; setup-native.mjs, run as the bundle's build step right after npm install on the host, scans the chunks and symlinks each alias to the real @node-rs/argon2. Rehearsed against a replica of the live database: legacy tables set aside, three migrations applied, the account migrated, sign-in with the logged temporary password lands in firstSignIn with Admin permissions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EvTkeoX8srShi7YwB2kZzw
# Conflicts: # apps/editor/package.json # bun.lock
…ed console The console was written against its own looser Biome/tsconfig; this repo runs noUncheckedIndexedAccess and a stricter Biome profile, so the vendored sources needed a formatting pass plus guards on indexed access. Panel-only hook-dependency pinning is exempted via a biome.jsonc override instead of being rewritten.
`floorPlaced.collides` is the spatial grid's plan rectangle: XZ only, no height at all. That is right for furniture on a floor and wrong for anything whose usable volume is mostly air — to the grid, a conveyor threading the walkway under a racking run is indistinguishable from one driven through its uprights. Kinds like that must leave `collides` off. Turning it off turned off their move validation entirely. `boxDimensions` is what drives `recomputeValidity`, and it was gated on `collides` alone, so such a kind could be placed through a gate that checks it in three dimensions and then dragged straight into solid steel with nothing consulted again. Placement refused what the drag then allowed. `movable.canMoveTo` is the kind's own answer, run on every pointer move of a drag and composed with the grid's rather than replacing it. Declaring it also turns the green/red box on, because refusing a drop with no visible reason reads as the object simply refusing to move; Alt forces it, exactly as it does for a colliding kind. A kind that declares neither keeps the plain arrow cursor it has always had, which is what the extracted `showsValidityBox` is tested on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5bdAFduH4BkPCjvtJgFzn
…push-turbopack-fix-v0z56s # Conflicts: # bun.lock
Opening a zone's settings took the whole editor down with React 185 —
maximum update depth exceeded.
The contents selector ran through `useShallow`, which compares array
elements with `Object.is`, and it built fresh `{ label, count }` objects
on every call. Two calls over an identical scene were therefore never
equal, so the snapshot read as changed on every render and the loop never
settled. Every other `useShallow` in this codebase maps ids onto nodes
that already exist; not one constructs an object, and this is why.
The selector now returns strings — one label per node, compared by value —
and the grouping moved out into a `useMemo`. `collectZoneObjectLabels`
sits beside `collectZoneObjectIds` so the property that matters can be
tested without a renderer: two calls over an unchanged scene are
shallow-equal, and a node moving out of the zone still makes them
unequal, so the panel keeps updating. Both fail against the old shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T5bdAFduH4BkPCjvtJgFzn
Was f93b4f8.
Was 119716b.
…custom modifications
…SM, and preserve warehouse plugins/local features
…it-lock in keyboard shortcuts
|
|
||
| - name: Install dependencies | ||
| if: steps.token.outputs.available == 'true' && steps.pull.outputs.changed == 'true' | ||
| run: bun install --frozen-lockfile |
There was a problem hiding this comment.
Panel pull uses unpinned Bun
Medium Severity
pull-panel runs setup-bun with no bun-version, then bun install --frozen-lockfile. CI comments elsewhere state an unpinned Bun floats to latest and can fail the frozen lock without any repo change. A console pull that cannot install never type-checks or lands on integration.
Reviewed by Cursor Bugbot for commit 46948bb. Configure here.
| } from '@pascal-app/core' | ||
| import { editorHostPanelRegistry } from '@pascal-app/editor' | ||
| import { PLUGIN_CATALOG, getPluginDescriptor } from '../lib/plugins/catalog' | ||
| import { usePluginManager } from '../lib/plugins/use-plugin-manager' |
There was a problem hiding this comment.
Tests import missing plugin catalog
Medium Severity
Four new suites import lib/plugins/catalog and lib/plugins/use-plugin-manager, which are not in the tree, and they assert that bootstrap.ts has no static plugin imports. bootstrap.ts still statically loads warehouse, trees, bones, mint, and streetscape. The editor test script only runs lib/, so these suites never execute and describe an architecture that this change did not ship.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 46948bb. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f1e43e7. Configure here.
| "lint": "biome lint", | ||
| "check-types": "next typegen && tsc --noEmit", | ||
| "test": "bun test lib" | ||
| "test": "bun test lib --timeout 30000" |
There was a problem hiding this comment.
New editor tests never run
Medium Severity
Four new suites under apps/editor/__tests__/ are presented as bundle-isolation and dynamic-activation guards, but the editor test script is bun test lib. Those files sit outside lib/, so turbo run test and CI never execute them. The adversarial coverage they claim is not actually enforced.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit f1e43e7. Configure here.


What does this PR do?
How to test
Screenshots / screen recording
Checklist
bun devbun checkto verify)mainbranchNote
High Risk
Changes production deploy topology, required MySQL at boot, GitHub secrets/tokens, and force-pushed live artifacts—mistakes can block releases or ship broken bundles while CI branch assumptions shift from
maintointegration.Overview
This PR turns ovurrsl/editor into an operable fork:
integrationbecomes the working and CI default (replacingmainfor pushes/PRs),mainstays a pure upstream mirror, and a large set of docs (OTOMASYON.md,UPSTREAM.md,YAYINLAMA.md, fork block inAGENTS.md, handover notes) encodes branch rules and merge playbooks.Release path changes materially: the root
Dockerfileand.dockerignoreare removed; production is documented as Hostinger via a newdeploy-bundleworkflow that builds Next standalone, runs MySQL boot smoke tests (refuse start without DB), assembles.github/deploy/artifacts (package.json,setup-native.mjsfor argon2 aliases), and force-pushes toovurrsl/digitaltwin. Companion automation addsmirror-upstream,bump-plugin,pull-panel/ manualsync-panel,relock, andupstream-check; CI drops the macOScli-smokejob with an explained rationale.Product wiring: the DigitalTwin console is mounted under
apps/editor/app/(panel)/(auth, MFA,/console/[tab]with admin/permission guards) using@panelimports and panel styling isolated viadata-dt-theme.SETUP.mdstates Docker was removed on purpose (MySQL-only prod story).Quality gates: new editor tests assert lazy plugin catalog behavior and, when
.nextexists, no plugin code in initial chunks and dynamic split for seven plugins.Reviewed by Cursor Bugbot for commit f1e43e7. Bugbot is set up for automated code reviews on this repo. Configure here.