Skip to content

fix(stores): preserve exemption expiresAt across persistence - #59

Open
goeric wants to merge 1 commit into
dupe-com:mainfrom
goeric:fix/exemption-expires-at-persistence
Open

fix(stores): preserve exemption expiresAt across persistence#59
goeric wants to merge 1 commit into
dupe-com:mainfrom
goeric:fix/exemption-expires-at-persistence

Conversation

@goeric

@goeric goeric commented Jul 26, 2026

Copy link
Copy Markdown

What

parseExemptionRecord does not read expiresAt back off persisted state, so a session self-exemption loses its expiry whenever state round-trips through a store that serialises.

Why

0.3.3 added sessionExemptionTtlMs to bound how long a self-exemption lives — "Session exemptions were never pruned before, so a single self-click grant lasted the whole session; this bounds that window."

That bound is lost on reload:

  1. session.ts:486 sets record.expiresAt = grantedAt + ttlMs on grant.
  2. stores.ts:382 parseExemptionRecord destructures advertiserHost, policyIds, networkIds, grantedAtexpiresAt is dropped. parseSessionRecord, directly below, handles the same optional field correctly.
  3. session.ts:684 pruneExpiredState prunes an exemption only when expiresAt !== undefined, so the field-less record is never pruned.

The result is a permanent self-exemption for that host — the pre-0.3.3 behaviour, reintroduced by persistence. It affects SessionStorageStateStore and everything behind the persisted-envelope parser. MemoryStateStore is unaffected because it round-trips through cloneState, which preserves the field via spread — which is why the existing suite doesn't catch it.

Fix

Parse expiresAt back the same way parseSessionRecord already does: same optional-numeric validation clause, same conditional assignment. Read-side only — cloneState already preserved it.

Tests

Both fail on main and pass with the fix:

  • tests/stores.test.tsexpiresAt survives a ChromeLocalStateStore save/load.
  • tests/session.test.ts — the persistence-backed twin of the existing bounds a session self-exemption by the default TTL test. Grant, reload through a real store, advance past the TTL, and the lingering cookie stands down again. On main it returns self-exempted-session instead.

169 tests pass (167 + 2). The audit harness passes 30/30.

Checklist

  • npm run typecheck, npm run lint, npm test, and npm run build pass
  • New behavior has tests
  • No policy-pack changes, so policies-cite-check is untouched
  • No public API change; no docs update needed
  • No decision-path network call (I1) and no user-identity data in Signals (I2)

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

parseExemptionRecord dropped the optional expiresAt field, so a session
self-exemption reloaded from a persisting store had no expiry. pruneExpiredState
only prunes when expiresAt is set, making the exemption permanent and restoring
the unbounded behaviour that sessionExemptionTtlMs was added in 0.3.3 to bound.

Parse it back the same way parseSessionRecord already does. Read-side only:
cloneState already preserves the field, which is why MemoryStateStore-based
tests never caught it.
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