Skip to content

fix(idenfy): recreate expired iDenfy session so users can resume verification#53

Merged
calebtuttle merged 2 commits into
devfrom
fix/idenfy-expired-session-recreation
Jun 15, 2026
Merged

fix(idenfy): recreate expired iDenfy session so users can resume verification#53
calebtuttle merged 2 commits into
devfrom
fix/idenfy-expired-session-recreation

Conversation

@calebtuttle

Copy link
Copy Markdown
Contributor

Summary

Fixes the iDenfy expired-session resume loop (internal-docs#1343). When an iDenfy verification token EXPIRES, the parent Session stays IN_PROGRESS, so the frontend keeps routing the user back to the same dead /idenfy/verify token — 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

  • getIdenfyStatusForSession now treats EXPIRED as recoverable (not terminal), from both the cached-webhook path and a fresh /api/v2/status poll. It routes into recreateExpiredIdenfySession, which re-mints in place (new authToken/scanRef, statusin_progress) reusing clientId = createdBySessionIdno new payment/redemption (verified against iDenfy docs: same clientId mints a fresh session).
  • Anti-flash contract: never surface EXPIRED while re-creation is viable (the frontend's expired flag is sticky). 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 — serializes concurrent pollers (host page + external tab poll independently). Plain status reads never take the lock.
  • recreationCount cap (10) per IdenfySession row bounds iDenfy verification-credit cost/abuse (iDenfy bills per session).
  • APPROVED/DENIED/SUSPECTED stay terminal (no auto-retry); only EXPIRED recovers.
  • Shared by gov-id and Clean Hands (AML) flows via the same resolver — no flow-specific fork.

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-existing credentials/utils.test.ts env failure is unrelated.)

Post-Deploy Monitoring & Validation

  • Logs (service idenfy-sessions): watch for Recreated 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/token errors).
  • Healthy signal: gov-id/clean-hands "Completed redemption" volume holds or rises; fewer manual support session-resets for iDenfy expiry.
  • Failure signal / rollback trigger: spike in 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 (recreationCount is an additive optional field).
  • Window/owner: watch the first 24–48h after deploy.

🤖 Generated with Claude Code

calebtuttle and others added 2 commits June 15, 2026 10:40
…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>
@calebtuttle calebtuttle merged commit 93ce6d3 into dev Jun 15, 2026
1 check passed
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