Skip to content

Fix periodic OAuth token validation throwing on Harper v5 frozen sessions - #223

Merged
heskew merged 2 commits into
mainfrom
fix/periodic-validation-tracked-session-oauth
Sep 1, 2026
Merged

Fix periodic OAuth token validation throwing on Harper v5 frozen sessions#223
heskew merged 2 commits into
mainfrom
fix/periodic-validation-tracked-session-oauth

Conversation

@Devin-Holland

Copy link
Copy Markdown
Member

Summary

On Harper v5, session.oauth is 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:

oauthMetadata.lastValidated = now;   // src/lib/sessionValidator.ts

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) — but lastValidated never advances, the tokenValidationInterval throttle never engages, and every request re-runs provider.validateToken (a live call to the provider) and re-logs Token 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.oauth explicitly 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's CLAUDE.md), so it would silently drop accessToken/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

  • decision — rebuild inline vs. a helper. The metadata field list is now spelled out in two places: the refresh path (existing) and this validation path. A rebuildOAuthMetadata(oauth, overrides) helper would make adding an OAuthSessionMetadata field 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

  • New regression unit test simulates a read-only, non-enumerable tracked session.oauth (matching production GenericTrackedObject semantics). 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.
  • Full unit suite: 1156 pass / 0 fail (npm test).
  • End-to-end route: unit is the verification route — no integration fixture exercises the periodic GitHub-validation timer; the unit test pins the tracked-object contract the production path depends on.

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 (agy not 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

Devin-Holland and others added 2 commits September 1, 2026 11:20
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>
@Devin-Holland
Devin-Holland requested a review from heskew September 1, 2026 15:34

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lib/sessionValidator.ts
@claude

claude Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

@heskew heskew left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/lib/sessionValidator.ts
Comment thread test/lib/sessionValidator.test.js

@heskew heskew left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@heskew
heskew merged commit 74566ea into main Sep 1, 2026
30 checks passed
@heskew
heskew deleted the fix/periodic-validation-tracked-session-oauth branch September 1, 2026 16:05
@heskew heskew mentioned this pull request Sep 1, 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.

sessionValidator mutates the frozen session record on Harper v5 — "Cannot assign to read only property 'lastValidated'"

2 participants