fix(idenfy): recreate expired iDenfy session so users can resume verification#53
Merged
Merged
Conversation
…fication When an iDenfy verification token EXPIRES, lazily mint a fresh iDenfy session for the same already-paid parent session instead of dead-ending the user on the expired token. Fixes the resume loop where the parent Session stays IN_PROGRESS and the frontend keeps routing back to the same expired /idenfy/verify URL. - getIdenfyStatusForSession: treat EXPIRED as recoverable (not terminal) from both the cached-webhook path and a fresh /api/v2/status poll; route into recreateExpiredIdenfySession which re-mints in place (new authToken/scanRef, status reset to in_progress) reusing clientId=createdBySessionId (no re-pay). - Anti-flash contract: never surface EXPIRED while re-creation is viable; the lock-loser returns a pending status with the stale token withheld. - Valkey lock (idenfy:recreate-lock:<id>, SET NX EX 30s) held ONLY around the iDenfy token API call, serializing concurrent pollers (host + external tab). - recreationCount cap (10) bounds iDenfy verification-credit cost/abuse. - Shared by gov-id and Clean Hands (AML) flows via getIdenfyStatusForSession; no flow-specific fork. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses ce-code-review findings on PR #53: - Check updateOne result on re-mint: when matchedCount===0 (TTL-overrun duplicate mint already advanced the scanRef), return the persisted/peer state instead of the orphaned, never-recorded billed token whose webhook would 404. (P1) - Persist an EXPIRED sentinel (verification-status only, not status:"failed") on the freshly-fetched-EXPIRED path before recovery, so a lock-losing peer no longer hands back the stale expired token and a capped row stops re-polling /api/v2/status every cycle. (P1) - Introduce IdenfySessionView return type with nullable token fields; annotate getIdenfyStatusForSession/recreateExpiredIdenfySession and drop the `as unknown as` casts that hid null fields typed as string. (P1, type-only) - Add tests: 0-match re-mint returns peer state, fresh-fetch sentinel write, and SUSPECTED terminal regression guard. Skipped (accepted tradeoff): graceful degradation when Valkey is down — a cache outage 500s EXPIRED status polls rather than degrading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the iDenfy expired-session resume loop (internal-docs#1343). When an iDenfy verification token EXPIRES, the parent
SessionstaysIN_PROGRESS, so the frontend keeps routing the user back to the same dead/idenfy/verifytoken — with no self-serve recovery. This makes the backend lazily mint a fresh iDenfy session for the same already-paid parent session on the next status poll.Backend half. Frontend half + submodule bump is in the monorepo PR.
Changes
getIdenfyStatusForSessionnow treatsEXPIREDas recoverable (not terminal), from both the cached-webhook path and a fresh/api/v2/statuspoll. It routes intorecreateExpiredIdenfySession, which re-mints in place (newauthToken/scanRef,status→in_progress) reusingclientId = createdBySessionId— no new payment/redemption (verified against iDenfy docs: same clientId mints a fresh session).EXPIREDwhile re-creation is viable (the frontend's expired flag is sticky). Lock-loser returns a pending status with the stale token withheld.idenfy:recreate-lock:<id>,SET NX EX 30s) held only around the iDenfy token API call — serializes concurrent pollers (host page + external tab poll independently). Plain status reads never take the lock.recreationCountcap (10) perIdenfySessionrow bounds iDenfy verification-credit cost/abuse (iDenfy bills per session).APPROVED/DENIED/SUSPECTEDstay terminal (no auto-retry); onlyEXPIREDrecovers.Testing
bun test src/services/idenfy-sessions/functions.test.ts— 15 tests covering: cached & fresh-fetch EXPIRED recovery, cap (and overshoot self-heal), lock-miss pending (token withheld) + refreshed-row, iDenfy API error fallback, no-lock-on-plain-read, APPROVED/DENIED unchanged, and Clean Hands parity + cross-flow independence. (Pre-existingcredentials/utils.test.tsenv failure is unrelated.)Post-Deploy Monitoring & Validation
idenfy-sessions): watch forRecreated expired iDenfy session(healthy recovery),iDenfy recreate cap reached(users hitting the 10 cap — investigate if frequent),iDenfy session re-creation failed(iDenfy/api/v2/tokenerrors).re-creation failed, or duplicate-mint anomalies (recreationCount climbing unexpectedly). Rollback = revert this PR; behavior returns to the prior (looping) state, no data migration needed (recreationCountis an additive optional field).🤖 Generated with Claude Code