Fix periodic OAuth token validation throwing on Harper v5 frozen sessions - #223
Conversation
The periodic-validation path (GitHub-style non-expiring tokens) recorded
the check by mutating session.oauth in place: `oauthMetadata.lastValidated
= now`. On Harper v5, session.oauth is a tracked, frozen object, so that
assignment throws "Cannot assign to read only property 'lastValidated'".
The throw is caught and swallowed, so lastValidated never advances and the
tokenValidationInterval throttle never engages — every request re-runs
provider.validateToken (a live call to the provider) and re-logs the error.
Rebuild session.oauth explicitly with all fields, mirroring the token
refresh path. Spread cannot be used: Harper tracked objects copy nothing
on `{ ...obj }`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The spread-trap guard only checked provider/providerConfigId/providerType/ accessToken. Give scope/tokenType/lastRefreshed real values in the tracked fixture and assert they survive the rebuild, so dropping one from the rebuild in sessionValidator.ts fails the test — TypeScript won't catch a dropped optional OAuthSessionMetadata field. Trim the test comment to intent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request modifies sessionValidator.ts to rebuild the session.oauth object explicitly instead of mutating it in place, preventing runtime errors when dealing with read-only Harper tracked objects. A new unit test has been added to verify this behavior and ensure properties are correctly preserved. There are no review comments, and I have no additional feedback to provide.
|
Reviewed; no blockers found. |
|
Reviewed; no blockers found. |
heskew
left a comment
There was a problem hiding this comment.
I found one significant concurrency concern and one medium regression-test gap. The explicit rebuild itself copies every currently defined OAuthSessionMetadata field, and the local/full CI suites are green.
The concurrency issue is inherited from the working v4 path and the existing refresh path, but this PR makes that write reachable again for affected v5 sessions, so it deserves an explicit decision: either make the timestamp persistence conditional/atomic here, or track and sequence the required Harper session-update fix.
Adjacent and pre-existing (not a blocker on this diff): clearOAuthSession() assumes production sessions expose delete(), while current Harper request sessions expose only update(); its fallback mutates only the request-local copy and does not persist revocation/expiry logout. I found no existing OAuth issue for that, so it should be tracked separately.
heskew
left a comment
There was a problem hiding this comment.
Scoping update after checking the existing issue history: no blockers on #223. I found no new runtime correctness or security issue introduced by this patch. The session-write race is pre-existing and tracked separately in #212 (now reproduction-confirmed); durable logout is already #211. The remaining open note is a non-blocking improvement to make the new regression test prove persistence across requests.
Summary
On Harper v5,
session.oauthis a frozen, tracked object. The periodic token-validation path for non-expiring tokens (e.g. GitHub) recorded each successful check by mutating it in place:On v5 that throws
TypeError: Cannot assign to read only property 'lastValidated'. The throw is caught and swallowed, so validation still returns{ valid: true }(users are not logged out) — butlastValidatednever advances, thetokenValidationIntervalthrottle never engages, and every request re-runsprovider.validateToken(a live call to the provider) and re-logsToken validation error: Cannot assign to read only property 'lastValidated'. Observed live on a Harper v5 central-manager cluster using GitHub OAuth.The fix rebuilds
session.oauthexplicitly with all fields — mirroring the token-refresh path just below — instead of mutating in place. Spread ({ ...oauthMetadata }) can't be used: Harper tracked objects copy nothing on spread (documented in this repo'sCLAUDE.md), so it would silently dropaccessToken/provider/etc.This affects the
2.x(Harper v5) line; v4 (1.x) does not freeze records, so the same code did not throw there.For the human reviewer
rebuildOAuthMetadata(oauth, overrides)helper would make adding anOAuthSessionMetadatafield a one-site edit and remove the divergence risk (TypeScript won't flag a dropped optional field). I kept the inline rebuild to hold the fix to the broken path and avoid touching the working refresh path; the regression test now asserts every value-carrying field survives, so a dropped field fails a test rather than shipping silently. Happy to extract the helper if you'd prefer — it's a reversible refactor.Verification
session.oauth(matching productionGenericTrackedObjectsemantics). It fails on base (lastValidated should advance— the swallowed throw leaves it stale) and passes with the fix, and asserts every preserved field (provider,providerConfigId,providerType,accessToken,scope,tokenType,lastRefreshed) survives the rebuild.npm test).Cross-model review
Two rounds, independent (opposite-family graded leg): round 1 (codex + Harper-domain adjudicator) →
COMMENTS, both findings test-only; round 2 (codex, resumed delta) after addressing them → no findings,Adjudicated-Severity: none. Reduced coverage both rounds: the Gemini leg was down (agynot authenticated) and the Cursor lens failed a format check / was pruned on the delta — recorded, not silently dropped.Complexity: low
Review-Coverage: authored=claude; ran=codex; blocked=gemini(auth); declined=cursor-grok,cursor-composer,domain; rounds=2 @ d26818c
Human-Review-Need: 3 @ d26818c