feat: [JWT-6] Identity Verification for the request pipeline - #1711
feat: [JWT-6] Identity Verification for the request pipeline#1711nan-li wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
Multi-model adversarial review (interrogate)
Intent
Gate user-scoped request addressing and signing on Identity Verification. For a given user,
OSRequestAuth/OSAliasPairchoose the path alias and JWT bearer together so they never come from different users; percent-encode IV path segments viaOSUrlPath; requests carry their owning identity model;OSOperationRepoholds while the requirement is unknown, flushes on hydrate, and drops anonymous work when IV is on; Update Subscription is exempt; logout under IV disables push; test-onlyholdWhilePresentfor purge determinism.
Reviewers
- A: claude-fable-5-thinking-xhigh — 9 findings
- B: gpt-5.6-sol-xhigh — 6 findings
- C: cursor-grok-4.5-high-fast — 6 findings
- D: claude-opus-5-thinking-high — 15 findings
Act On
- Cold-start Identify User is dropped under IV instead of reshaped to Create User (C; related A) —
uncacheUserRequestsbranch 3 treatsprepare == falseas permanent discard, but Identify's prepare intentionally fails under IV so reshape can promote it. WhenidentityModelToUpdateis still the current user, the login is stranded with noonesignal_id/ no Create User. The existing test locks in drop-without-Create and misses the “ToUpdate still current” path. addsNewRecordsdecodesfalsefor pre-upgrade Create User caches (A, C, D) —decodeBoolon a missing key skipsnewRecordsState, weakening RYW and risking the 404 → logout path the cool-down exists to prevent.- Bearer JWTs land in VERBOSE request logs (B) —
setBearerwritesAuthorizationintoadditionalHeaders;OneSignalClientlogs those headers in full. awaitsTokenfalse-positive stalls non-auth prepare failures (A) — cool-down / other prepare failures are treated as “waiting for JWT,” so the wait-only pass schedules no delayed retry and never asks for a token.OneSignalUserManagerImpl.swiftcrosses 1k lines (987 → 1065) (A, B, C, D) — IV logout silencing, hydrate restore, andstoreJwtwake belong inOneSignalUserManagerImpl+Jwt.swift(or a focused helper).
Consider
ownerExternalId != nil || prepareon uncache is not gated on IV, so IV-off apps can retain forever-unsendable owned requests (D).- Update Subscription now archives a full
OSIdentityModel(including bearer) and can revive it into the token pool (D). - Cached-requirement hydrate clears
_isDisabledInternallybefore subscription listeners start, so IV-off restore can skip the server update (B). authorization(onesignalId:externalId:)/OSUserRequestAuthorizationare unused in production; the doc claims IAM already consumes them (A, B, D) — demote or land with the caller; IAM wiring itself can stay stacked.- Transient
clearUserDataanonymity can drop in-flight identified work under IV (A). - Legacy
FetchIdentityBySubscriptionmigration is hard-dropped under IV (A, C). - Unbounded 401 ↔ ask-for-token if the app keeps returning the same rejected JWT (D).
Noted
- Anonymous purge duplicated across executors; purge-on-every-flush overhead;
OSUrlPathdoc mentions Live Activities it does not encode; unlabeledOSAliasPairinit.
Dismissed
- Must wire IAM fetches in this PR (B critical) — out of scope for the queued user-request pipeline; the unused public API / false “currently IAM” claim is still Act/Consider above.
- Logout silencing while requirement is
.unknown(D) — intentional and covered byUserJwtLifecycleTests. - Trusting a persisted non-unknown requirement across sessions (B) — established JWT-config cache contract from earlier stack PRs; not introduced uniquely here.
Agreement Map
Strong consensus on the 1k-line crossing and addsNewRecords upgrade decode. High-confidence solo/pair hits after lead verification: Identify cold-start drop (C), JWT logging (B), awaitsToken stall (A). Largest disagreement: B treated missing IAM wiring as a ship-blocker; lead scopes that to a later consumer PR while still rejecting the dead public surface claim.
Sent by Cursor Automation: Untitled
96a17a3 to
f0dc805
Compare
8dabb1a to
1d08c49
Compare
f0dc805 to
2c267bf
Compare
1d08c49 to
b0aec18
Compare
Everything that sends a user-scoped call now decides how to address and sign it in one place. OSRequestAuth answers, for a given user, which alias names them in the path and which token signs it, read together so the alias and the token can never come from different users. Under Identity Verification a user is addressed by external_id, which the app chooses, so those path segments are percent-encoded through OSUrlPath. Requests carry the identity model that owns them rather than reading whoever is current at send time. The operation repo holds queued work while the requirement is still unknown, since sending unsigned would be rejected and sending signed too early is not possible. Once the answer arrives the queue flushes. When Identity Verification is on, work belonging to no external ID is dropped rather than sent: an anonymous user is never created on the server, so that work has no user to belong to. Update Subscription is deliberately exempt from all of this. A push subscription belongs to the device, not the signed-in user, so it always goes out whether or not anyone is logged in and whether or not a token is valid; nothing about it is gated on auth. Logging out while Identity Verification is on internally disables the push subscription, since the replacement anonymous user is never created and the subscription would otherwise keep reporting under the logged-out user. Logging back in clears that, as does the requirement hydrating to off. Co-authored-by: Cursor <cursoragent@cursor.com>
Under TEST, OP_REPO_POST_CREATE_DELAY_SECONDS is 0, so canAccess released an ID the instant it was added and the Requests left the executor queues before removeOperationsWithoutExternalId could see them. MockNewRecordsState.holdWhilePresent keeps an ID inaccessible for as long as it is present. Purge tests opt in; every other consumer keeps the production timer behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
No behavior change — move code into extensions / a top-level OSPushSubscriptionImpl so file_length and type_body_length stay under error thresholds. Co-authored-by: Cursor <cursoragent@cursor.com>
b0aec18 to
a642d1c
Compare
prepareForExecution is false when Identity Verification is on, which uncache treated as a drop. A cold-start Identify whose ToUpdate is still current never reached reshape, so the login had no Create User. Keep it when IV is on and let reshape convert or drop. Co-authored-by: Cursor <cursoragent@cursor.com>
decodeBool is false when the key is absent, so pre-upgrade Create User caches skipped newRecordsState and follow-ups could 404. A missing key cools down; recovery creates write the field explicitly. Co-authored-by: Cursor <cursoragent@cursor.com>
…ion is on Without it, an owned Request whose identity model is gone can never become sendable, so uncache must drop it instead of keeping it forever. Co-authored-by: Cursor <cursoragent@cursor.com>


Description
One Line Summary
Gate user-scoped request addressing and signing on Identity Verification — path alias + JWT from one user, hold the queue while the requirement is unknown, and drop anonymous work when IV is on — with Update Subscription intentionally exempt.
Details
Motivation
Everything that sends a user-scoped call needs one place to decide how to address and sign it. Under Identity Verification a user is named by
external_id(app-chosen, so path segments are percent-encoded), and the alias and token must never come from different users. Sending unsigned while the requirement is still unknown would be rejected; sending signed too early is not possible — so queued work must wait, then flush when the answer arrives.Scope
OSRequestAuth/OSAliasPair— for a given user, which alias names them in the path and which token signs it, read togetherOSUrlPath— percent-encode Identity Verification path segmentsOSOperationRepo— hold while JWT requirement is unknown; flush when hydrated; drop work with no external ID when IV is on (anonymous users are never created on the server)MockNewRecordsState.holdWhilePresentso purge tests stay deterministic under zeroOP_REPO_POST_CREATE_DELAY_SECONDSStacked on #1710 (
nan/jwt-pr5-delta-ownership).Testing
Unit testing
OSRequestAuthTests,RequestPathEncodingTestsOSOperationRepoIdentityVerificationTests, expanded flush tests / shared supportExecutorAnonymousPurgeTests, expandedUserExecutorTests/DeltaOwnershipTestsUserJwtLifecycleTestsManual testing
Built in sequence as part of the local JWT stack against an iOS Simulator; this PR’s commits compiled on top of PR5.
Affected code checklist
Checklist
Overview
Testing
Final pass
Made with Cursor