Skip to content

fix(persist): stop lenient Date.parse reviver corrupting ordinary strings (TODO 9) - #98

Closed
lopugit wants to merge 3 commits into
mainfrom
claude/persist-reviver-strict-date
Closed

fix(persist): stop lenient Date.parse reviver corrupting ordinary strings (TODO 9)#98
lopugit wants to merge 3 commits into
mainfrom
claude/persist-reviver-strict-date

Conversation

@lopugit

@lopugit lopugit commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Fixes TODO/TODO.md item 9 (data corruption): the localforage persist reviver in ThingtimeProvider revived any string passing V8's lenient Date.parse into a Date — so ordinary values like "Post 1", "1", "2024", "March 2024", "5 April" became Date objects on reload, and the replacer then rewrote them as ISO strings, permanently corrupting user data after one save/reload cycle.

Fix

  • Replacer now persists Dates as tagged {ttype: 'date', iso} objects, mirroring the existing ttype: 'function' scheme. It reads the raw value off the holder (this[key]) because flatted — like JSON.stringify — applies Date.prototype.toJSON before the replacer runs, so the previous value instanceof Date branch was dead code and Dates leaked through as bare ISO strings.
  • Reviver only revives:
    • tagged {ttype: 'date', iso} values, and
    • bare strings matching the exact toISOString() shape (YYYY-MM-DDTHH:mm:ss.sssZ) for backward compatibility with previously persisted state.
  • Everyday strings that merely pass lenient Date.parse are left untouched.

Old persisted payloads (bare ISO strings) still revive to Dates; after one save cycle they migrate to the tagged form.

Verification

  • Node round-trip harness against the real flatted package: buggy behaviour reproduced ("Post 1" → Date(2000-12-31…)), new code round-trips strings/Dates/functions correctly across generations, legacy payloads included.
  • Live in the running dev app (worktree dev stack on 127.0.0.1:12380): round-trips through the shipped window.flatted (the same parse/stringify used by the autosave and load paths); a marker ("Post 1", "2024", "March 2024", one real Date) planted in the IndexedDB thingtime payload survived a load → state change → autosave → re-read cycle with strings intact, Date preserved, and the new payload in tagged form. No console errors.
  • The two other flatted call sites (useThingtimeMachine undo/redo snapshots, UserSettingsModal export) use raw flatted without the reviver/replacer and are unaffected.

🤖 Generated with Claude Code

…ings

Persist Dates as tagged {ttype:'date', iso} objects (read off the replacer
holder, since flatted applies Date.prototype.toJSON before the replacer) and
only revive tagged values or exact toISOString()-shaped strings. Strings like
'Post 1', '2024', 'March 2024' no longer become Dates after a save/reload
cycle. TODO/TODO.md item 9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thingtime Ready Ready Preview, Comment Jul 26, 2026 12:50pm

Request Review

… in autosave)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lopugit

lopugit commented Jul 21, 2026

Copy link
Copy Markdown
Owner Author

Triage note (session 3/10): this duplicates TODO 9 with #94 and #96#94 was the first claim and carries a 26-check flatted suite + live browser load→autosave verification. Owner should pick one and close the others.

lopugit added a commit that referenced this pull request Jul 30, 2026
…98/#106)

Upgrade the persist codec from strict-ISO string revival to the tagged
scheme the duplicate TODO-9 PRs used, closing the remaining false-positive:

- Real Dates persist as {ttype:'date', iso}. The replacer reads the
  original off the holder (this[key]) because Date.toJSON converts Dates
  to strings before the replacer sees the value.
- A USER string that merely looks like a full ISO timestamp is escaped as
  {ttype:'iso-string', s} so the legacy fallback can never capture it —
  previously such a string became a Date on the next load.
- The legacy bare-ISO fallback remains only to migrate pre-tagging
  persists.
- Both replacer and reviver skip their own wrappers' inner keys (without
  the guard the escape rule re-wraps its own output unboundedly, and the
  legacy fallback hands the wrapper branches a Date instead of the
  original string — caught by the new tests).

Tests: 9/9 in test:persist, including Date + identical-looking string
coexisting, two-cycle string stability, and hostile-payload drops.
TESTING.md gains the register rate-limit checklist (from #106) and a
persisted-codec checklist. Live-verified: app hydrates under the CSP,
persists and reloads cleanly, zero console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lopugit

lopugit commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Consolidating the TODO 8/9/10 security PRs: #99 is the single winner and now carries this PR's unique value (see the consolidation note on #99 for exactly what was folded in and why). Closing as a duplicate.

@lopugit lopugit closed this Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant