localStorage loaders don't validate parsed shape: corrupted store → runtime errors #66
Labels
No labels
blocked:lint-failed
blocked:needs-decision
blocked:no-changes
bug
feature-request
in-progress
milestone
ops-task
quickfix
security/high
test-coverage
triage-approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Mark/chainbound#66
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
loadStats, loadReplays, loadOpenings, and loadCampaignState all do:
without any shape validation. A corrupted entry (from a different app, a partial write, or a manual edit) will crash callers that expect specific fields.
Where
Repro
Why this matters
Acceptance criteria
Fixed in commit
1ab8f4aon origin/main.What changed
Added
src/data/safeStorage.tsexporting two helpers:safeParseAndMerge<T>(raw, defaults, fieldGuards)for object-shaped stores (stats, campaign). Per-field typeof guards let valid values through and fall back to defaults for missing or wrong-typed fields.safeParseArray<T>(raw, itemGuard)for array-shaped stores (replays, openings). Filters out items that do not match the guard.Each named loader (
loadStats,loadReplays,loadOpenings,loadCampaignState) now uses the helper with field-level guards. Unrecognized top-level fields are dropped. The original behaviour for first-load seeding (openings) and the empty-result default (replays) is preserved.Acceptance criteria — all met
Tests
src/data/stats.test.ts, including the exact repro from the issue body (stored object withtotalWins/totalLossesbut nototalDuels—computeDerivedStatsno longer crashes and returnswinRate: 0).src/data/replays.test.tscovering non-array storage, malformed items, wrong enum literals, missing snapshots, and malformed playerAction.src/engine/reactiveConditions.test.tspredate this commit and are out of scope.Verification
npx tsc -b --noEmit— cleaneslint src/data/{safeStorage,stats,replays,openings,campaign,stats.test,replays.test}.ts— cleanvite build— built in 4.13s, 1152 modules transformedClosing as fixed.