feat: [JWT-4] identity model JWT, the JWT repo, and the public API - #1709
feat: [JWT-4] identity model JWT, the JWT repo, and the public API#1709nan-li wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Multimodal adversarial review (interrogate)
Skill: Cursor interrogate (pstack). Reviewers: claude-fable-5-thinking-xhigh, gpt-5.6-sol-xhigh, cursor-grok-4.5-high-fast, claude-opus-5-thinking-high. Lead judgment applied below — not a raw dump.
Intent
PR4 of the IV/JWT stack (on PR3). Add the Identity Verification surface an app talks to: supply a JWT for a user (
updateUserJwt/ login token), and get told when that token stopped being accepted (OSUserJwtRepoask-once + late-listener catch-up + public listener API). Persist JWT fields onOSIdentityModel/ repo with compare-and-swap invalidate. Remove unused beta surface (requiresUserAuth,onJwtExpired). Request-pipeline / queue release intentionally later.
Reviewers
- A: claude-fable-5-thinking-xhigh — 11 findings
- B: gpt-5.6-sol-xhigh — 6 findings
- C: cursor-grok-4.5-high-fast — 8 findings
- D: claude-opus-5-thinking-high — 14 findings
Act On
login(externalId:token:)bypassesstoreJwt/ ask re-arm (A/B/C/D) —storeJwtclaims every app-supplied token arrives there, but login/createNewUserstill assignjwtBearerTokendirectly. That never clearsaskedForToken, so answering an invalidation via login permanently silences later asks. Anonymous→identify also parks the token on the anonymous model while the new external-ID model stays tokenless, sovalidJwt(externalId:)is nil right after the primary IV login path. Same-user re-login early-returns and drops a refreshed token entirely.- Raw JWT logged in
updateUserJwt(A/B/C/D) — Author TODO says omit before shipping;OSLogListeners still receive every level regardless of_nsLogLevel. Strip now. - Bearer token persisted to shared UserDefaults; secure-storage TODO deleted (A/B/C/D) — Encode path newly archives the live credential via
OSModelStore→ app-group defaults. At minimum restore a tracked TODO / keep real tokens memory-only and persist only the invalid sentinel; Keychain if cross-launch persistence is required. - No
login(..., token:)coverage against the new JWT surface (A/C/D) — Repo/API tests seed models withexternalIdalready set; they cannot catch (1). Add anonymous→identify, create-user, and same-user refresh cases assertinguserJwtRepo.validJwt+ ask re-arm.
Consider
- Ask/token TOCTOU (A/B/C/D) —
invalidateJwt’s nil-check thenaskForTokenis not atomic withupdateJwt; concurrent replacement can leave a valid token + stuck ask. - Late-listener catch-up (A/B/C/D) — Direct strong capture + main async bypasses
OSObservableweak/remove semantics; concurrent ask can double-deliver. set(property: OS_JWT_BEARER_TOKEN)withoutpreventServerUpdate: true(A/D) — Today alias cast fails; prefer explicit opt-out likeOSSubscriptionModel.askedForTokensurvives logout (D) — Late listeners can be told to mint for a logged-out user.- Silent drop when no model matches
externalId(D) — Void API + ask uncleared is terminal for the session duringclearDatahydration windows.
Noted
OSUserJwtProviding/ unreadidentityVerificationServicelook like stacked-PR scaffolding (A/D).- In-band
OS_JWT_TOKEN_INVALIDstring checked at three layers (A/D) — fine for now; tighten when executors land. - Lazy observer init race (B) — low practical risk for UI registration.
Dismissed
- Full JWT-state enum rewrite as a blocker for this PR — design preference; not required to ship the public surface if login routing + logging/persistence are fixed.
- Sticky-event observable redesign for catch-up — nicer long-term, overkill if membership/weak capture is fixed.
- Wrapper changelog as a hard block — PR body already documents beta API migration; still worth a release note.
Agreement Map
All four models independently flagged login-vs-storeJwt, JWT-in-logs, and plaintext persistence. Catch-up + ask/token races were also consensus (lower severity). D pushed hardest on silent drops / logout ask lifecycle; B on log-listener fanout and lazy observer init. Highest-confidence blockers are the login ingress contract break and shipping secrets to logs/disk.
Sent by Cursor Automation: Untitled
The surface an app talks to under Identity Verification: it hands the SDK a token for a user, and the SDK tells it when that token stopped being accepted. OSUserJwtRepo holds the token per external ID and remembers who has already been asked for one, so an app is asked once per user rather than once per rejected request. A listener registered after start or login still hears about an ask that already fired, since the alternative is an app that never learns it owes a token for the user it just logged in. Removes the beta JWT surface this replaces: requiresUserAuth, which nothing ever read, along with onJwtExpired and its handler typealiases. Anyone on the beta JWT API moves to addUserJwtInvalidatedListener and updateUserJwt. storeJwt lands here in the form the public API needs; the later PR that introduces the queues extends it to release the work held for want of a token. Co-authored-by: Cursor <cursoragent@cursor.com>
03f2ebf to
2f3cd34
Compare
Use preventServerUpdate so the token never becomes an identity delta by accident of the alias cast failing. Co-authored-by: Cursor <cursoragent@cursor.com>


Description
One Line Summary
Add the Identity Verification public surface: JWT on the identity model, the JWT repo, and the app-facing API to supply a token and hear when it was invalidated.
Details
Motivation
Under Identity Verification, an app must hand the SDK a JWT for a user and learn when that token stopped being accepted. The SDK needs per–external-ID storage and ask-once semantics so the app is prompted once per user, not once per rejected request — including when a listener registers after start or login.
Scope
OSIdentityModel/OSIdentityModelRepo— JWT field and persistence the public API needsOSUserJwtRepo— token per external ID; ask-once; late-listener catch-upOSUserJwtInvalidatedEvent+ public API (updateUserJwt,addUserJwtInvalidatedListener, …)requiresUserAuth,onJwtExpired, and related handler typealiasesstoreJwt, IAM, or demo UI (later PRs)Stacked on #1707 (
nan/jwt-pr3-iv-gate).Testing
Unit testing
OSIdentityModelTestsOSIdentityModelRepoTestsOSUserJwtRepoTestsUserJwtApiTestsManual testing
Built in sequence as part of the local JWT stack against an iOS Simulator; this PR’s commit compiled on top of PR3.
Affected code checklist
Checklist
Overview
Testing
Final pass