Skip to content

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). Migrated apps/web to @clerk/react@6.12.10: swapped the import path across all 61 consuming files, replaced the removed <SignedIn>/<SignedOut>/<RedirectToSignIn> trio in App.tsx with the new <Show when="signed-in" fallback={<RedirectToSignIn />}> component, moved UserButton's now -removed afterSignOutUrl prop up to <ClerkProvider afterSignOutUrl="/"> (Core 3 centralizes redirect config), and hardened the two most shared getToken() call sites (useAuthFetch, the configureDataTableClient wiring in App.tsx) against Core 3's new throw-on-offline behavior (previously resolved null). Live-verified sign-in, an authenticated campaign-list load, and the UserButton menu in a real browser session — no console errors. The ~26 other ad-hoc getToken() 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-BY NOTICE already names SRD 5.2.1. Done in vtt-modules 4ca627e (branch version/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-stable races/rules buckets, 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 at index.ts:945), and the index.ts:305-307 mounts. Move the D&D-shaped record types (Dnd5eFeatRecord et al.) out of packages/shared into the module. Done to the extent M-3 allows (commit c03abc42): −1,582 lines. Deleted the whole 2024 router, the 2014 router's classes/skills/spells/feats/ability-score endpoints, and the entire dnd5eApiStore preload stack (5 loaders + transforms + disk cache + the boot-time init). fetchDnd5eFeats / fetchDnd5eSpells were 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-Count wasn't in exposedHeaders, so every paginated list (system8's included) silently read totalCount: 0 cross-origin. CC-1 guard ratcheted dnd5eApiStore 1 → 0. Now fully done (commit a9d6376f): the initially-blocking piece was solved rather than deferred. InsertMonster needed 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-modules b12e880). Insert Monster reads /api/modules/dnd5e/monsters?edition=… through a new utils/srdMonster.ts adapter, and dnd5eApiStore.ts + routes/dnd5e2014Api.ts are 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 in packages/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's projectToken is 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 delete apps/web/src/utils/characterToToken.ts. Consumers rewired (commit 6cec4144): all six real call sites now go through the new utils/projectCharacterToken.ts seam, which uses the system's projectToken. (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-in custom pseudo-system (rules-free by design), and it is still reached by tokenFromGeneratedCharacter, i.e. System 8 monster generation, deferred to a later version. Deleting it needs custom covered and that generation path retired. The seam also degrades to it when a module lacks projectToken or 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.ts still hardcodes System 8's wiki hosts (ursa.bertball.com, wiki.bertball.com) even though the 3.3 allowedHosts contribution mechanism shipped and no module uses it yet — move them into the system8 module. Also remove the characterView.ts:468 isSystem8 branch. Hosts: done (platform d2cb176b, vtt-modules cf7e573) — the first real use of the R3.2 mechanism. System 8 now declares system8.purplecat.wiki plus both legacy bertball.com hosts (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. characterView branch: 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 verify seedSystemFixture runs for every system on a fresh DB and against prod, sqlite and pg. Getting it wrong silently drops default combat actions in production. Done (commit 24c4a962). 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 inside initializeDatabase (schema creation), before seedAllSystems, and both are insert-or-ignore — so on every fresh database it won and installed a stale Heal that wrote hp via setProperty (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 via DB_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, not initializeModules() (skip it and System 8 silently vanishes from seeding), and POSTGRES_URL outranks DATABASE_URL (overriding only the latter quietly ran against the real dev database). Also fixed System 8's fixture Heal the same way (vtt-modules f25483c) — 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 + systemRegistry at zero system literals; the current baseline in apps/web/src/systemLiterals.guard.test.ts still carries fixtures/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 (commit 60a78767). packages/server-core has reached zero real leakage: db.ts 12 → 0 (the enumerated seeder deleted in 3.5 was its only system knowledge), dnd5eApiStore.ts + both dnd5e*Api.ts routes deleted outright in 5.6, ai.ts cleared back in 3.4, and dbSchema.ts 2 → 0 (a stale comment naming ids that never existed here). Its residual 2 are a regex artifact — systemId !== systemId in systemTraits.ts compares two variables and names no system; left counted deliberately, since narrowing the pattern would also stop catching real systemId === '<literal>' branches. packages/shared is the remainder, and it splits cleanly: | Count | What | Status | |---|---|---| | 91 0 | the 5e fixtures + their prop-type/action tables | ✅ done — lifted into the dnd5e module | | 16 | old-sheet-coupled: dataTableApi 5e fetchers, feats.service switch(systemId), characterView isSystem8 | deferred by the Option-3 scope call | | 1 | systemId === 'custom' | platform's own pseudo-system; legitimate | The 91 are now closed (platform db95a66d, vtt-modules 131503f): the 5e fixtures live in the dnd5e module as systemFixtures, alongside system8's. packages/shared ships no system's seed data at all — SYSTEM_FIXTURES is an empty map and DEFAULT_SYSTEMS holds only custom. Also deleted rather than moved: DND5E_PROP_TYPES/_PROPERTY_DEFINITIONS/_BARS (dead code — zero references anywhere; the fixture always inlined its own) and SUPPORTED_SYSTEMS (a hardcoded roster of every hostable system, which the module registry exists to replace). shared is now down to 17: 16 old-sheet-coupled (deferred) + 1 legitimate custom check, plus BUNDLED_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 on ISystem instead. Closed for 0.44.0: server-core is at zero, and the ratchet is the release's deliverable. The 17 left in shared are 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 (createCharacterView is 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 (subtype skill, carrying ability/tier/modifier in metadata), so the platform groups and styles them like any other trait. - Trait richness passes through ProjectedTrait.metadata: wiki url/bodyHtml for the detail modal and derived power actions for 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. - TokenPlayModeSection was deleted. It was a 0.44.0 scaffold for a sheet that didn't render the token half; PlayModeView renders those same four LinkedToken* 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.evaluate times 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-modules 36ca617) — 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, platform 84502deb): 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: ModuleProjectionSheet renders 5 sections (header, actions, abilities, derived stats, traits-by-subtype) against PlayModeView'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 props ModuleProjectionSheet doesn'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/powers ARE 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