v0.44.0 - Finish System-Platform Separation
Node 26
- [x] Update app and github actions to node 26
Dependabot
- [x] Resolve critical vulnerabilities: GitHub found 144 vulnerabilities on purple-cat-games/platform's default branch (5 critical, 76 high, 53 moderate, 10 low). To find out more, visit: remote: https://github.com/purple-cat-games/platform/security/dependabot
All 5 critical alerts fixed: tar (via onnxruntime-node, DoS), protobufjs (via onnxruntime-web, RCE),
fast-xml-parser (via @aws-sdk/xml-builder, entity-encoding bypass) pinned to patched minimums via pnpm
overrides; vitest/@vitest/coverage-v8/@vitest/ui bumped 4.0.15 → 4.1.10 (arbitrary file read/execute
via UI server); @clerk/shared patched by bumping @clerk/clerk-react 5.57.0 → 5.61.3 (middleware
route-protection bypass — only the clerk-react chain was vulnerable; the older @clerk/clerk-sdk-node →
@clerk/shared@1.4.2 chain predates the vulnerable range). High/moderate/low alerts (139) not in scope here.
- [x] Follow-up:
@clerk/clerk-react(still on 5.x) is npm-deprecated in favor of@clerk/react(Clerk's "Core 3" rename). Migratedapps/webto@clerk/react@6.12.10: swapped the import path across all 61 consuming files, replaced the removed<SignedIn>/<SignedOut>/<RedirectToSignIn>trio inApp.tsxwith the new<Show when="signed-in" fallback={<RedirectToSignIn />}>component, movedUserButton's now -removedafterSignOutUrlprop up to<ClerkProvider afterSignOutUrl="/">(Core 3 centralizes redirect config), and hardened the two most sharedgetToken()call sites (useAuthFetch, theconfigureDataTableClientwiring inApp.tsx) against Core 3's new throw-on-offline behavior (previously resolvednull). Live-verified sign-in, an authenticated campaign-list load, and theUserButtonmenu in a real browser session — no console errors. The ~26 other ad-hocgetToken()call sites were left as-is (already throw-on-failure by design, same as before this migration).
System-Platform Separation
Everything left over from the 0.43.0 System-Platform Separation project, so 0.43.0 can ship as-is. Nothing here is a regression — 0.43.0 built every seam additively and left the legacy paths in place; this version deletes the legacy paths and flips the consumers onto the seams. Most of it is entangled with the old Character Sheet retirement, so it is a natural pairing with (or prerequisite of) the Character Sheet Overhaul.
Status keys below refer to the task ids in 4-system-platform-separation/todo.md, where each item's blocking issue is recorded in full.
Scope decision: separate everything except the old character sheet's vestiges
Separation completes in 0.44.0 for the server / shared / registry / data-store layers. The system-specific code still living in the old character sheet stays put and is deleted wholesale by the Character Sheet Overhaul in a following release. This is what the project spec already committed to — see the todo's "Scope boundaries (read first)": full zero-literal CC-1 acceptance is only reachable once the old sheet retires.
Nothing is left broken by this: every path works, the CC-1 guard simply doesn't reach zero in files
that already have a deletion date. The alternative — refactoring them first — would spend real effort and
real regression risk on ~4,100 lines of doomed code (AddFeat/AddPower/AddEquipment/EquipmentList
= 3,577 L for the Content API flips, plus ~564 L of shared for 3.6), against a total old-sheet surface of
~12,100 lines. For scale, 28% of the guard's 247 remaining literals sit in that doomed ui-components
sheet. Deferred items below each carry their specific blocker.
5e module — finish the extraction
-
[x] Bundle SRD 5.2.1 (2024 edition) data (5.2, partial). Only
data/2014/(SRD 5.1, 1078 entities) is bundled;data/2024/does not exist yet, so the 2024 edition has no bundled content behind its provider. Same pipeline (scripts/build-srd.mjs+ provenance manifest); the CC-BYNOTICEalready names SRD 5.2.1. Done in vtt-modules4ca627e(branchversion/dnd5e-0.2.0, module 0.1.1 → 0.2.0):data/2024/ships 1486 SRD 5.2.1 entities. The build script is now edition-driven,species+glossary(new in 5.2.1) map into the edition-stableraces/rulesbuckets, and edition is threaded through the data store, router (?edition=), and content provider — each 5e SystemModule now carries its own edition-bound provider instead of both sharing one that always read 2014. All defaults keep 2014 byte-identical. Verified end-to-end against the running platform. -
[x] Delete the platform-side 5e content plumbing (5.6, platform half). The module now serves its own content, but the old fetch-and-cache stack is still mounted:
packages/server-core/src/routes/dnd5e2014Api.ts,routes/dnd5e2024Api.ts,stores/dnd5eApiStore.ts+initializeDnd5eApiStore(called atindex.ts:945), and theindex.ts:305-307mounts. Move the D&D-shaped record types (Dnd5eFeatRecordet al.) out ofpackages/sharedinto the module. Done to the extent M-3 allows (commitc03abc42): −1,582 lines. Deleted the whole 2024 router, the 2014 router's classes/skills/spells/feats/ability-score endpoints, and the entirednd5eApiStorepreload stack (5 loaders + transforms + disk cache + the boot-time init).fetchDnd5eFeats/fetchDnd5eSpellswere repointed at/api/modules/dnd5e/…?edition=first (adapting ContentRecords back to the legacy response shape) so the old sheet didn't regress. Fixed a latent CORS bug en route —X-Total-Countwasn't inexposedHeaders, so every paginated list (system8's included) silently readtotalCount: 0cross-origin. CC-1 guard ratcheteddnd5eApiStore1 → 0. Now fully done (commita9d6376f): the initially-blocking piece was solved rather than deferred.InsertMonsterneeded dnd5eapi.co's structured stat blocks, which the bundled SRD only had as prose — so the module now parses traits/actions/reactions/legendary actions out of that prose at build time (attack bonus, damage dice + type, save DC, usage limits; vtt-modulesb12e880). Insert Monster reads/api/modules/dnd5e/monsters?edition=…through a newutils/srdMonster.tsadapter, anddnd5eApiStore.ts+routes/dnd5e2014Api.tsare deleted. The platform now holds no 5e content and makes no external request for it — the SRD ships in the module, so monster insertion works offline and no longer depends on a third-party API. Monsters follow the campaign's edition (2024 campaigns get the 5.2.1 bestiary; the System 8 stopgap still falls back to 2014). The D&D-shaped record types still stay inpackages/shared— their only consumers are the out-of-scope old character sheet, so they retire with it. -
[x] Retire
convertCharacterToToken(5.4 follow-up). The module'sprojectTokenis proven equal (CC-4) but nothing calls it for 5e yet. Rewire the ~11 remaining consumers (AddToMapModal,ImportPartyModal,LinkCharacterToTokenModal,useRefreshTokenFromCharacter,CurrentTokenBar,TokenContext,CharacterImportModal,PlayModeView,useCharacters, …) onto the module projection, then deleteapps/web/src/utils/characterToToken.ts. Consumers rewired (commit6cec4144): all six real call sites now go through the newutils/projectCharacterToken.tsseam, which uses the system'sprojectToken. (The "~11" counted comment mentions and re-exports; there were 7 actual call sites, one of them monster generation.) The file is NOT deleted, deliberately — it survives as the fallback for systems with no module, which today means the built-incustompseudo-system (rules-free by design), and it is still reached bytokenFromGeneratedCharacter, i.e. System 8 monster generation, deferred to a later version. Deleting it needscustomcovered and that generation path retired. The seam also degrades to it when a module lacksprojectTokenor throws, so a broken module can't stop a GM putting a token on the map. Worth a smoke test: these are core token-creation flows and the map UI couldn't be driven in this session (the map route bounced to the campaign chooser), so they're verified by tests + typecheck rather than by hand. -
[x] Final de-leak (5.8).
systemDataProxy.tsstill hardcodes System 8's wiki hosts (ursa.bertball.com,wiki.bertball.com) even though the 3.3allowedHostscontribution mechanism shipped and no module uses it yet — move them into the system8 module. Also remove thecharacterView.ts:468isSystem8branch. Hosts: done (platformd2cb176b, vtt-modulescf7e573) — the first real use of the R3.2 mechanism. System 8 now declaressystem8.purplecat.wikiplus both legacybertball.comhosts (kept because existing campaigns reference them for artwork); the platform's static proxy list is empty and the image list keeps only general hosts. Beyond tidiness, those hosts are now reachable only while System 8 is loaded. Verified live: an unlisted host 403s with a message enumerating exactly the module-contributed hosts.characterViewbranch: deferred (see below) — carried into the character-sheet work.`System8ManualFeatsEditor`, `System8ManualPowersEditor`) into the module client bundle as CS-7 sheet-section extensions. **Prereq: a React-capable module client build** — no module ships React components today. (NOTE: the todo describes these as sheet-section extensions, but they are the Monster Generator's own editors — they live under `components/UI/MonsterGenerator/` and nothing outside it imports them.)
Deferred with recorded blockers
- [x] 3.5 — Default-fixture registration. Removing the enumerated seeder (
db.ts:846-853, pg:2309-2315) needs a running DB: first add the inflict-damage/heal actions to the dnd5e2014/2024 fixtures (only the enumerated list carries them today), then verifyseedSystemFixtureruns for every system on a fresh DB and against prod, sqlite and pg. Getting it wrong silently drops default combat actions in production. Done (commit24c4a962). The premise was stale — all three fixtures already carried the actions with matching ids, so the enumerated list was pure duplication and both dialects' copies are deleted. It was also a live bug: the enumerated seeder ran insideinitializeDatabase(schema creation), beforeseedAllSystems, and both are insert-or-ignore — so on every fresh database it won and installed a staleHealthat wrote hp viasetProperty(in-memory only; the fixture's own comment calls this "a no-op"). Fresh installs have been shipping a broken Heal. Verified empirically on genuinely fresh DBs in both dialects (sqlite viaDB_PATH, postgres via a throwaway cluster), replicating the real boot order: before = 6 actions with all three Heals stale; after = 6 actions across all three systems with all three Heals persisting. Existing installs unaffected (rows already exist; confirmed against the dev DB). Two traps that each produced a false pass first time: module fixtures are registered by index.ts's bootstrap loop, notinitializeModules()(skip it and System 8 silently vanishes from seeding), andPOSTGRES_URLoutranksDATABASE_URL(overriding only the latter quietly ran against the real dev database). Also fixed System 8's fixture Heal the same way (vtt-modulesf25483c) — 5e's had been fixed, its hadn't. Existing installs keep the old Heal until the system8 fixture version is bumped or an admin force-reseed runs; only fresh installs pick it up. - [x] CC-1 guard → zero for server/shared. Project acceptance wants
packages/server-core+packages/shared+systemRegistryat zero system literals; the current baseline inapps/web/src/systemLiterals.guard.test.tsstill carriesfixtures/dnd5e2014.ts(53),fixtures/dnd5e2024.ts(12),feats.service.ts(10),db.ts(12),dataTableApi.ts(4),dbSchema.ts(2),systemTraits.ts(2),characterView.ts(2),dnd5eApiStore.ts(1). Most fall out of the items above; the fixtures move with 3.5. Ratcheted 247 → 232 (commit60a78767).packages/server-corehas reached zero real leakage:db.ts12 → 0 (the enumerated seeder deleted in 3.5 was its only system knowledge),dnd5eApiStore.ts+ bothdnd5e*Api.tsroutes deleted outright in 5.6,ai.tscleared back in 3.4, anddbSchema.ts2 → 0 (a stale comment naming ids that never existed here). Its residual 2 are a regex artifact —systemId !== systemIdinsystemTraits.tscompares two variables and names no system; left counted deliberately, since narrowing the pattern would also stop catching realsystemId === '<literal>'branches.packages/sharedis the remainder, and it splits cleanly: | Count | What | Status | |---|---|---| |910 | the 5e fixtures + their prop-type/action tables | ✅ done — lifted into the dnd5e module | | 16 | old-sheet-coupled:dataTableApi5e fetchers,feats.serviceswitch(systemId),characterViewisSystem8| deferred by the Option-3 scope call | | 1 |systemId === 'custom'| platform's own pseudo-system; legitimate | The 91 are now closed (platformdb95a66d, vtt-modules131503f): the 5e fixtures live in the dnd5e module assystemFixtures, alongside system8's.packages/sharedships no system's seed data at all —SYSTEM_FIXTURESis an empty map andDEFAULT_SYSTEMSholds onlycustom. Also deleted rather than moved:DND5E_PROP_TYPES/_PROPERTY_DEFINITIONS/_BARS(dead code — zero references anywhere; the fixture always inlined its own) andSUPPORTED_SYSTEMS(a hardcoded roster of every hostable system, which the module registry exists to replace).sharedis now down to 17: 16 old-sheet-coupled (deferred) + 1 legitimatecustomcheck, plusBUNDLED_SYSTEM_IDS(2) which stays deliberately — it drives CLIENT-side UI and the browser has no fixture registry to derive it from, so it needs a server-supplied flag onISysteminstead. Closed for 0.44.0:server-coreis at zero, and the ratchet is the release's deliverable. The 17 left insharedare the Option-3 carry-forward — they die with the old sheet, so the zero-literal acceptance moves to 0.45.0 with the consumers that hold them.
Play Mode → default
- [x] The module-projection sheet ships behind a "Classic | Projected (beta)" toggle with Classic as the
default (
CharacterContext.tsx:496). Promote Projected to the default once the cosmetic gaps close, then retire Classic with the old sheet. DONE (0.44.0). The toggle is gone; the module-projected sheet is the only Play-Mode sheet. The section gap tabulated below closed without building twelve new sections: rather than grow a second sheet, the projection is adapted onto the view model the existing markup already reads (projectionToCharacterView.ts→PlayModeView). Only the DATA SOURCE changed — the platform no longer computes 5e/System 8 rules (createCharacterViewis unused by Play Mode, and with it the hardcoded per-system proficiency branch) — so the sheet cannot look different from what it replaced. What that adaptation required: - Skills moved from derived stats to the trait channel (subtypeskill, carrying ability/tier/modifier inmetadata), so the platform groups and styles them like any other trait. - Trait richness passes throughProjectedTrait.metadata: wikiurl/bodyHtmlfor the detail modal and derived poweractionsfor the inline expand caret. Without this the sheet silently lost detail — the modal would have shown only the brief, reproducing the v0.24.0 bug from the other end. -TokenPlayModeSectionwas deleted. It was a 0.44.0 scaffold for a sheet that didn't render the token half;PlayModeViewrenders those same fourLinkedToken*components itself, so keeping both double-rendered conditions / vision-light / auras. The platform/module split the composition was meant to express already holds inside the sheet: those sections consult no module. Not verified interactively. The map view (which hosts Character Context) freezes the renderer, as it has all release;Runtime.evaluatetimes out. Parity is argued structurally (same markup, same props) and pinned by source-contract tests — it has not been seen on screen. Worth a smoke test. UPDATE (0.44.0): the projection now works for all three systems. Both 5e editions gained CS-1/CS-2 (vtt-modules36ca617) — they previously had neither, which is why a 5e character reported "this system's module doesn't provide a projection sheet yet" — and Play Mode is now composed as module-orchestrated character sheet + platform-rendered token section (TokenPlayModeSection, platform84502deb): conditions, auras, vision/light and HP bars are identical across systems, so the platform owns them outright and no module is consulted for them. That closes most of the section gap below; what remains unprojected is equipment (see Committed Scope) and the action add/edit/delete controls. The default is still Classic — flipping it belongs with the Character Sheet Overhaul. Original assessment — the remaining gap is functional, not cosmetic. Measured:ModuleProjectionSheetrenders 5 sections (header, actions, abilities, derived stats, traits-by-subtype) againstPlayModeView's 12. Making Projected the default today would remove, for every user: | Missing section | Why it matters | |---|---| |conditions| core in-play state (frightened/prone/…) | |equipment| carried gear | |auras| map-affecting emanations | |vision-light| darkvision / light sources | |skills| no skills rendering at all | |portrait| character art | Plus, from the propsModuleProjectionSheetdoesn't accept: no add / edit / delete action, no action filter-group-sort control (ActionsPrefsControl), and no linked-token integration (live HP bars, token-sourced HP/AC/Speed strip).feats/powersARE covered, via the module's traits-by-subtype channel, in simplified read-only form. The 4.4 smoke that "PASSED" validated the sections that exist (attributes / derived HP / traits / action execution) — it did not cover the absent ones, which is why "minor cosmetics" understates this. Closing that list is the Character Sheet Overhaul, i.e. exactly the work Option 3 defers: build the remaining sections onto the projection (platform components driven by module schema + data), then flip the default and retire Classic together. The toggle stays as-is — Projected remains opt-in beta, which costs nothing and keeps the seam exercised.
Misc
- [x] Fixed a bug where clicking certain Help links caused an infinite loop of re-routing