feat(iv): add externalId and requiresJwt to Operation base class (2/6)#2624
feat(iv): add externalId and requiresJwt to Operation base class (2/6)#2624nan-li wants to merge 2 commits intofeat/iv-foundation-01from
Conversation
0caf0f3 to
aec3996
Compare
|
@claude review |
9a3c903 to
5835891
Compare
…criptionId KDoc Two PR review nits on #2624: - Operation.externalId.set was implicitly public after the hoist from LoginUserOperation/TrackCustomEventOperation (both of which had `private set` before). Make it `internal set`, matching the precedent on LoginUserOperation.existingOnesignalId. Preserves the tripwire against accidental post-construction mutation that the new KDoc ("captured at construction time") promises. - LoginUserFromSubscriptionOperation.subscriptionId had a copy-pasted "optional external ID..." KDoc that became actively misleading once externalId was hoisted to the base class — readers now see an external-ID description above subscriptionId with the real externalId silently inherited. Replace with an accurate description. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
aec3996 to
3c2e5f9
Compare
…criptionId KDoc Two PR review nits on #2624: - Operation.externalId.set was implicitly public after the hoist from LoginUserOperation/TrackCustomEventOperation (both of which had `private set` before). Make it `internal set`, matching the precedent on LoginUserOperation.existingOnesignalId. Preserves the tripwire against accidental post-construction mutation that the new KDoc ("captured at construction time") promises. - LoginUserFromSubscriptionOperation.subscriptionId had a copy-pasted "optional external ID..." KDoc that became actively misleading once externalId was hoisted to the base class — readers now see an external-ID description above subscriptionId with the real externalId silently inherited. Replace with an accurate description.
3c2e5f9 to
e5a77f9
Compare
There was a problem hiding this comment.
Both prior nits addressed (tightened setter visibility and fixed the subscriptionId KDoc) — mechanical refactor looks good.
Extended reasoning...
Overview
This is PR 2/6 of the Identity Verification series. It hoists two properties to the Operation base class — externalId: String? and open val requiresJwt: Boolean = true — and threads externalId through the constructors of all 16 Operation subclasses plus their ~10 call sites. LoginUserOperation and TrackCustomEventOperation shed their local externalId declarations in favor of the inherited base-class property. Tests are updated mechanically to the new constructor signatures.
Security risks
None introduced by this PR. The change is a structural refactor; it does not touch HTTP/auth/crypto code paths or change what data is sent to the backend. The new externalId field on operations is sourced from the same IdentityModelStore that already populated it on LoginUserOperation/TrackCustomEventOperation. The IV gating logic that will consume these properties is explicitly deferred to PR 3/6.
Level of scrutiny
Low-to-moderate. The diff is wide (34 files) but each touched site is mechanical — adding an externalId parameter at a fixed position and forwarding _identityModelStore.model.externalId. The two non-mechanical bits (introducing the new base-class property, and adding IdentityModelStore as a dep on PropertiesModelStoreListener) are localized and straightforward. There is no runtime behavior change in this PR.
Other factors
- Both prior inline comments from my earlier review were resolved by recent commits (
internal seton the setter, correctedsubscriptionIdKDoc). - Existing test coverage was preserved — every call site is exercised through compile-time wiring.
- The PR is part of a 6-PR series, deliberately kept narrow so the base-class hoist has its own reviewable surface before downstream IV behavior lands.
fea5a86 to
0fb5092
Compare
PR 2 of 6 — prerequisite refactor for queue-level IV behavior. `externalId: String?` moves onto [Operation] as a protected property set at construction time. Captures the user the op was enqueued for so the op stays bound to that user even if the current user changes before execution. `requiresJwt: Boolean` (default `true`) declares whether an op's backend endpoint needs a JWT when Identity Verification is active; subclasses can override to `false` for endpoints that don't require auth. Every operation subclass constructor gains an `externalId: String?` parameter as the third arg (after appId + onesignalId). Every call site that creates an operation — model-store listeners, session listener, executors that build result ops, RebuildUserService, UserRefreshService, etc. — now reads `_identityModelStore.model.externalId` and passes it in. `LoginUserOperation` and `TrackCustomEventOperation` previously had their own local `externalId` property; those are removed in favor of the inherited base-class property. No runtime behavior change in this PR. Queue-level IV work (pre-HYDRATE deferral, FAIL_UNAUTHORIZED handler, IdentityVerificationService with anon op purge) lands in PR 2.5.
…criptionId KDoc Two PR review nits on #2624: - Operation.externalId.set was implicitly public after the hoist from LoginUserOperation/TrackCustomEventOperation (both of which had `private set` before). Make it `internal set`, matching the precedent on LoginUserOperation.existingOnesignalId. Preserves the tripwire against accidental post-construction mutation that the new KDoc ("captured at construction time") promises. - LoginUserFromSubscriptionOperation.subscriptionId had a copy-pasted "optional external ID..." KDoc that became actively misleading once externalId was hoisted to the base class — readers now see an external-ID description above subscriptionId with the real externalId silently inherited. Replace with an accurate description.
e5a77f9 to
8c5e762
Compare
There was a problem hiding this comment.
Prior concerns from my last review were addressed in 8c5e762 (internal set on Operation.externalId; corrected subscriptionId KDoc); rest is a mechanical externalId-threading refactor with no runtime behavior change.
Extended reasoning...
Overview
This PR (2 of 6 in the Identity Verification series) hoists two properties onto the Operation base class — externalId: String? and open val requiresJwt: Boolean — and threads externalId through every subclass constructor (16 subclasses) plus every call site (10 producer files: listeners, executors, services, UserSwitcher, RebuildUserService, SessionListener, TrackGooglePurchase, UserRefreshService). PropertiesModelStoreListener gains IdentityModelStore as a constructor dependency to read externalId. Existing test fixtures across 6 test files are updated to the new constructor signature with null substitutions for anonymous cases.
Security risks
None directly introduced by this PR. The new externalId and requiresJwt fields are scaffolding — no consumer reads them yet. The actual IV gating (hasValidJwtIfRequired, FAIL_UNAUTHORIZED handling, pre-HYDRATE deferral, anon-op purge) lands in PR 3/6, and the HTTP JWT attachment in PR 4/6. Setter visibility on the new externalId property is internal set (matching existingOnesignalId precedent in LoginUserOperation.kt:42), preserving the "captured at construction" invariant for same-module subclass constructors.
Level of scrutiny
Low-medium. The change is mechanical and the runtime delta is zero — every call site that constructs an Operation now also passes the externalId from the same identity model that was already accessible to it, and no consumer reads externalId polymorphically yet. The risk surface is essentially "did all 26 producers thread the right externalId value through?" — and the call-site reads are uniformly _identityModelStore.model.externalId (or identityModel.externalId from a captured copy in RebuildUserService), which is the only sensible source. Test fixtures pass null consistently for anonymous cases.
Other factors
- My two prior review comments (visibility setter, misplaced KDoc) were both addressed in commit 8c5e762 and are marked resolved.
- Bug hunting system found no issues on this revision.
- 34-file size is large but every touch is one of: add a parameter, pass through
externalId, or update a test fixture. No new control flow, no new branching, no new I/O. - This is part of a security-sensitive feature series (Identity Verification), but the security-relevant logic deliberately lives in subsequent PRs, keeping this one a clean reviewable surface for the base-class change.
Description
One Line Summary
PR 2 of 6 against the Identity Verification integration branch — mechanical refactor that adds
externalId: String?andrequiresJwt: Booleanto theOperationbase class so the next PR can implement queue-level IV gating. No runtime behavior change.Details
Motivation
The next PR in the series (PR 3/6, "queue-level IV") needs to read
op.externalIdpolymorphically fromOperationRepo. TodayexternalIdis declared onLoginUserOperationandTrackCustomEventOperationonly —OperationRepocan't read it from a genericOperationreference without casting to each of the 16 subclass types. This PR hoists the property (and a companionrequiresJwtflag) to the base class so downstream consumers can read them uniformly.Kept deliberately narrow so the base-class refactor has its own reviewable surface; the behavior changes (pre-HYDRATE deferral, FAIL_UNAUTHORIZED handling,
IdentityVerificationServicewith anon-op purge) come in the next PR.Scope
Operationbase class:var externalId: String?— captures the externalId of the user the op was enqueued for. Nullable because anonymous users have no externalId; base-class declaration keeps the typeString?uniformly across all subclasses.open val requiresJwt: Boolean get() = true— declares whether an op's backend endpoint needs a JWT when IV is active. Subclasses may override tofalsefor endpoints that don't require auth (none do yet — defaulttruecovers every current op).Operation subclasses (16):
externalId: String?as the 3rd parameter (afterappIdandonesignalId).LoginUserOperationandTrackCustomEventOperationshed their localexternalIdproperty in favor of the inherited base-class property (previously defined on each subclass independently).TransferSubscriptionOperationis the one exception in parameter position — its constructor is(appId, subscriptionId, onesignalId, externalId)to match the existing layout whereonesignalIdcomes aftersubscriptionId.Call sites updated to pass
externalIdat construction time (10 files):IdentityModelStoreListener,PropertiesModelStoreListener,SubscriptionModelStoreListener— read_identityModelStore.model.externalId.SessionListener(session start + end).TrackGooglePurchase(purchase tracking).UserSwitcher(legacy player ID migration).RebuildUserService(reconstructs queued ops from cached models).UserRefreshService(foreground user refresh).LoginUserOperationExecutor,LoginUserFromSubscriptionOperationExecutor,SubscriptionOperationExecutor(ops they emit as side effects, e.g. post-loginRefreshUserOperation, post-subscription-recreateCreateSubscriptionOperation).PropertiesModelStoreListenergainsIdentityModelStoreas a constructor dependency — the properties model doesn't carry externalId, so the listener has to read it from the identity model.Placement decision: base class vs per-subclass
onesignalIdis declared on each subclass individually, not on the base. Considered matching that convention forexternalId/requiresJwt, but chose base-class placement:externalIdis naturally nullable;String?works uniformly across every subclass without weakening any type.hasValidJwtIfRequired,removeOperationsWithoutExternalId) need to readop.externalIdpolymorphically — that requires at least an abstract declaration on the base. Per-subclass with abstract-on-base would mean ~80 lines of boilerplate (override + getter/setter × 16 subclasses) vs the 5-line base-class declaration.requiresJwthas natural override semantics (open valwith default), which is idiomatic on a base class.nameonOperationis precedent for a non-nullable universal property on the base, so there's no technical constraint keepingonesignalIdoff the base — that placement is a historical style preference that this PR does not touch.What is NOT in this PR
IdentityVerificationService— PR 3/6.OperationRepoIV gating (hasValidJwtIfRequireddispatch, FAIL_UNAUTHORIZED handler, pre-HYDRATE deferral ingetNextOps) — PR 3/6.login(externalId, jwt),updateUserJwt, listeners) — PR 5/6.Testing
Unit testing
externalId(see compile errors list narrowed from 50+ to 0).nullas 3rd arg for anon/un-scoped tests; add the relevant externalId where the test exercises it).SDKInitTestsfailures on the integration branch (unrelated: one is about an error-message assertion, the other about init not blocking). Same two failures exist onfeat/iv-foundation-01without this PR.Manual testing
Not applicable — this PR adds a property + parameter with no runtime behavior change. Every call site still passes identically-shaped data; the only difference is that
externalIdis now available for polymorphic reads in the next PR.Affected code checklist
Checklist
Overview
Operationand updates every subclass + call siteTesting
SDKInitTestsfailures are unrelated — see Manual testing)Final pass