@@ -13,6 +13,19 @@ struct CodexAccountScopedRefreshGuard: Equatable {
1313 let source : CodexActiveSource
1414 let identity : CodexIdentity
1515 let accountKey : String ?
16+ let authFingerprint : String ?
17+
18+ init (
19+ source: CodexActiveSource ,
20+ identity: CodexIdentity ,
21+ accountKey: String ? ,
22+ authFingerprint: String ? = nil )
23+ {
24+ self . source = source
25+ self . identity = identity
26+ self . accountKey = accountKey
27+ self . authFingerprint = CodexAuthFingerprint . normalize ( authFingerprint)
28+ }
1629}
1730
1831@MainActor
@@ -34,7 +47,7 @@ extension UsageStore {
3447 phaseDidChange ? ( . credits)
3548
3649 if self . settings. codexCookieSource. isEnabled {
37- let expectedGuard = self . currentCodexOpenAIWebRefreshGuard ( )
50+ let expectedGuard = self . freshCodexOpenAIWebRefreshGuard ( )
3851 await self . refreshOpenAIDashboardIfNeeded (
3952 force: true ,
4053 expectedGuard: expectedGuard,
@@ -55,13 +68,15 @@ extension UsageStore {
5568
5669 @discardableResult
5770 func prepareCodexAccountScopedRefreshIfNeeded( ) -> Bool {
58- let currentGuard = self . currentCodexAccountScopedRefreshGuard (
71+ let currentGuard = self . freshCodexAccountScopedRefreshGuard (
5972 preferCurrentSnapshot: false ,
6073 allowLastKnownLiveFallback: false )
6174 let previousGuard = self . lastCodexAccountScopedRefreshGuard
6275 self . lastCodexAccountScopedRefreshGuard = currentGuard
6376
64- guard previousGuard != nil , previousGuard != currentGuard else { return false }
77+ guard let previousGuard,
78+ !Self. codexScopedRefreshGuardsMatchAccount ( previousGuard, currentGuard)
79+ else { return false }
6580
6681 self . snapshots. removeValue ( forKey: . codex)
6782 self . errors [ . codex] = nil
@@ -106,7 +121,8 @@ extension UsageStore {
106121 self . lastCodexAccountScopedRefreshGuard = CodexAccountScopedRefreshGuard (
107122 source: resolvedSource,
108123 identity: resolvedIdentity,
109- accountKey: accountKey)
124+ accountKey: accountKey,
125+ authFingerprint: self . currentCodexAuthFingerprint ( source: resolvedSource) )
110126 }
111127
112128 func currentCodexAccountScopedRefreshGuard(
@@ -121,7 +137,8 @@ extension UsageStore {
121137 allowLastKnownLiveFallback: allowLastKnownLiveFallback) ,
122138 accountKey: self . codexAccountScopedRefreshKey (
123139 preferCurrentSnapshot: preferCurrentSnapshot,
124- allowLastKnownLiveFallback: allowLastKnownLiveFallback) )
140+ allowLastKnownLiveFallback: allowLastKnownLiveFallback) ,
141+ authFingerprint: self . currentCodexAuthFingerprint ( source: self . settings. codexResolvedActiveSource) )
125142 }
126143
127144 func currentCodexOpenAIWebRefreshGuard( ) -> CodexAccountScopedRefreshGuard {
@@ -137,36 +154,77 @@ extension UsageStore {
137154 return CodexAccountScopedRefreshGuard (
138155 source: source,
139156 identity: self . currentCodexOpenAIWebIdentity ( source: source) ,
140- accountKey: accountKey)
157+ accountKey: accountKey,
158+ authFingerprint: self . currentCodexAuthFingerprint ( source: source) )
159+ }
160+
161+ func freshCodexAccountScopedRefreshGuard(
162+ preferCurrentSnapshot: Bool = true ,
163+ allowLastKnownLiveFallback: Bool = true ) -> CodexAccountScopedRefreshGuard
164+ {
165+ self . settings. invalidateCodexAccountReconciliationSnapshotCache ( )
166+ return self . currentCodexAccountScopedRefreshGuard (
167+ preferCurrentSnapshot: preferCurrentSnapshot,
168+ allowLastKnownLiveFallback: allowLastKnownLiveFallback)
169+ }
170+
171+ func freshCodexOpenAIWebRefreshGuard( ) -> CodexAccountScopedRefreshGuard {
172+ self . settings. invalidateCodexAccountReconciliationSnapshotCache ( )
173+ return self . currentCodexOpenAIWebRefreshGuard ( )
141174 }
142175
143176 func shouldApplyCodexUsageResult(
144177 expectedGuard: CodexAccountScopedRefreshGuard ,
145178 usage: UsageSnapshot ) -> Bool
146179 {
147- let currentGuard = self . currentCodexAccountScopedRefreshGuard ( )
180+ let currentGuard = self . freshCodexAccountScopedRefreshGuard ( )
148181 guard currentGuard. source == expectedGuard. source else { return false }
182+ let fingerprintsAllowApply = Self . codexGuardAuthFingerprintAllowsUsageApply (
183+ currentGuard,
184+ expectedGuard)
185+ let expectedAuthFingerprint = CodexAuthFingerprint . normalize ( expectedGuard. authFingerprint)
186+ let currentAuthFingerprint = CodexAuthFingerprint . normalize ( currentGuard. authFingerprint)
187+ let canProveNilToCurrentAuth = expectedAuthFingerprint == nil && currentAuthFingerprint != nil
188+ let resultIdentity = CodexIdentityResolver . resolve ( accountId: nil , email: usage. accountEmail ( for: . codex) )
189+ let resultAccountKey = Self . normalizeCodexAccountScopedKey ( usage. accountEmail ( for: . codex) )
190+ let resultMatchesCurrentAccountKey = Self . codexUsageResultAccountKeyMatchesCurrentGuard (
191+ resultAccountKey,
192+ expectedGuard: expectedGuard,
193+ currentGuard: currentGuard)
149194
150195 if expectedGuard. identity != . unresolved {
151- return currentGuard. identity == expectedGuard. identity
196+ guard currentGuard. identity == expectedGuard. identity else { return false }
197+ if fingerprintsAllowApply {
198+ guard case . managedAccount = currentGuard. source else { return true }
199+ return resultMatchesCurrentAccountKey
200+ }
201+ guard canProveNilToCurrentAuth else { return false }
202+ guard resultMatchesCurrentAccountKey else { return false }
203+ return resultIdentity == currentGuard. identity ||
204+ ( resultAccountKey != nil && resultAccountKey == currentGuard. accountKey)
152205 }
153206
154- let resultIdentity = CodexIdentityResolver . resolve ( accountId: nil , email: usage. accountEmail ( for: . codex) )
155207 if currentGuard. identity != . unresolved {
156- return resultIdentity == currentGuard. identity
208+ guard resultIdentity == currentGuard. identity else { return false }
209+ return fingerprintsAllowApply || canProveNilToCurrentAuth
157210 }
158211
159212 switch currentGuard. source {
160213 case . liveSystem:
161- return resultIdentity != . unresolved
214+ guard resultIdentity != . unresolved else { return false }
215+ if fingerprintsAllowApply { return true }
216+ guard canProveNilToCurrentAuth else { return false }
217+ guard let currentAccountKey = currentGuard. accountKey else { return true }
218+ return resultAccountKey == currentAccountKey
162219 case . managedAccount:
163220 return false
164221 }
165222 }
166223
167224 func shouldApplyCodexScopedFailure( expectedGuard: CodexAccountScopedRefreshGuard ) -> Bool {
168- let currentGuard = self . currentCodexAccountScopedRefreshGuard ( )
225+ let currentGuard = self . freshCodexAccountScopedRefreshGuard ( )
169226 guard currentGuard. source == expectedGuard. source else { return false }
227+ guard Self . codexGuardAuthFingerprintMatches ( currentGuard, expectedGuard) else { return false }
170228
171229 if expectedGuard. identity != . unresolved {
172230 return currentGuard. identity == expectedGuard. identity
@@ -175,9 +233,25 @@ extension UsageStore {
175233 return currentGuard. identity == . unresolved
176234 }
177235
236+ func codexScopedNonUsageSuccessApplyGuard(
237+ expectedGuard: CodexAccountScopedRefreshGuard ) -> CodexAccountScopedRefreshGuard ?
238+ {
239+ let currentGuard = self . freshCodexAccountScopedRefreshGuard ( )
240+ guard currentGuard. source == expectedGuard. source else { return nil }
241+ guard Self . codexGuardAuthFingerprintAllowsUsageApply ( currentGuard, expectedGuard) else { return nil }
242+ guard expectedGuard. identity != . unresolved else { return nil }
243+ guard currentGuard. identity == expectedGuard. identity else { return nil }
244+ return currentGuard
245+ }
246+
178247 func shouldApplyCodexScopedNonUsageResult( expectedGuard: CodexAccountScopedRefreshGuard ) -> Bool {
179- let currentGuard = self . currentCodexAccountScopedRefreshGuard ( )
248+ self . codexScopedNonUsageSuccessApplyGuard ( expectedGuard: expectedGuard) != nil
249+ }
250+
251+ func shouldApplyCodexScopedNonUsageFailure( expectedGuard: CodexAccountScopedRefreshGuard ) -> Bool {
252+ let currentGuard = self . freshCodexAccountScopedRefreshGuard ( )
180253 guard currentGuard. source == expectedGuard. source else { return false }
254+ guard Self . codexGuardAuthFingerprintMatches ( currentGuard, expectedGuard) else { return false }
181255 guard expectedGuard. identity != . unresolved else { return false }
182256 return currentGuard. identity == expectedGuard. identity
183257 }
@@ -187,8 +261,9 @@ extension UsageStore {
187261 routingTargetEmail: String ? ) -> Bool
188262 {
189263 let normalizedRoutingTargetEmail = CodexIdentityResolver . normalizeEmail ( routingTargetEmail)
190- let currentGuard = self . currentCodexOpenAIWebRefreshGuard ( )
264+ let currentGuard = self . freshCodexOpenAIWebRefreshGuard ( )
191265 guard currentGuard. source == expectedGuard. source else { return false }
266+ guard Self . codexGuardAuthFingerprintAllowsUsageApply ( currentGuard, expectedGuard) else { return false }
192267
193268 if expectedGuard. identity != . unresolved {
194269 return currentGuard. identity == expectedGuard. identity
@@ -206,9 +281,44 @@ extension UsageStore {
206281 expectedGuard: CodexAccountScopedRefreshGuard ,
207282 routingTargetEmail: String ? ) -> Bool
208283 {
209- self . shouldApplyOpenAIDashboardRefreshGuard (
210- expectedGuard: expectedGuard,
211- routingTargetEmail: routingTargetEmail)
284+ let normalizedRoutingTargetEmail = CodexIdentityResolver . normalizeEmail ( routingTargetEmail)
285+ let currentGuard = self . freshCodexOpenAIWebRefreshGuard ( )
286+ guard currentGuard. source == expectedGuard. source else { return false }
287+ guard Self . codexGuardAuthFingerprintMatches ( currentGuard, expectedGuard) else { return false }
288+
289+ if expectedGuard. identity != . unresolved {
290+ return currentGuard. identity == expectedGuard. identity
291+ }
292+
293+ guard case . liveSystem = expectedGuard. source else { return false }
294+ guard currentGuard. identity == . unresolved else { return false }
295+ return CodexIdentityResolver . normalizeEmail (
296+ self . currentCodexOpenAIWebTargetEmail (
297+ allowCurrentSnapshotFallback: true ,
298+ allowLastKnownLiveFallback: false ) ) == normalizedRoutingTargetEmail
299+ }
300+
301+ func shouldApplyOpenAIDashboardPolicyResult(
302+ expectedGuard: CodexAccountScopedRefreshGuard ,
303+ routingTargetEmail: String ? ) -> Bool
304+ {
305+ let normalizedRoutingTargetEmail = CodexIdentityResolver . normalizeEmail ( routingTargetEmail)
306+ let currentGuard = self . freshCodexOpenAIWebRefreshGuard ( )
307+ guard currentGuard. source == expectedGuard. source else { return false }
308+
309+ if expectedGuard. identity != . unresolved {
310+ guard currentGuard. identity == expectedGuard. identity else { return false }
311+ return Self . codexGuardAuthFingerprintMatches ( currentGuard, expectedGuard) ||
312+ Self . codexGuardAuthFingerprintAllowsSameProviderAccount ( currentGuard, expectedGuard)
313+ }
314+
315+ guard case . liveSystem = expectedGuard. source else { return false }
316+ guard currentGuard. identity == . unresolved else { return false }
317+ guard Self . codexGuardAuthFingerprintMatches ( currentGuard, expectedGuard) else { return false }
318+ return CodexIdentityResolver . normalizeEmail (
319+ self . currentCodexOpenAIWebTargetEmail (
320+ allowCurrentSnapshotFallback: true ,
321+ allowLastKnownLiveFallback: false ) ) == normalizedRoutingTargetEmail
212322 }
213323
214324 func codexDashboardKnownOwnerCandidates( ) -> [ CodexDashboardKnownOwnerCandidate ] {
@@ -277,6 +387,83 @@ extension UsageStore {
277387 self . lastKnownLiveSystemCodexEmail = normalized
278388 }
279389
390+ nonisolated static func codexGuardAuthFingerprintMatches(
391+ _ lhs: CodexAccountScopedRefreshGuard ,
392+ _ rhs: CodexAccountScopedRefreshGuard ) -> Bool
393+ {
394+ let lhsFingerprint = CodexAuthFingerprint . normalize ( lhs. authFingerprint)
395+ let rhsFingerprint = CodexAuthFingerprint . normalize ( rhs. authFingerprint)
396+ if lhsFingerprint != nil || rhsFingerprint != nil {
397+ return lhsFingerprint == rhsFingerprint
398+ }
399+ return true
400+ }
401+
402+ nonisolated static func codexGuardAuthFingerprintAllowsUsageApply(
403+ _ lhs: CodexAccountScopedRefreshGuard ,
404+ _ rhs: CodexAccountScopedRefreshGuard ) -> Bool
405+ {
406+ if self . codexGuardAuthFingerprintMatches ( lhs, rhs) {
407+ return true
408+ }
409+ let lhsFingerprint = CodexAuthFingerprint . normalize ( lhs. authFingerprint)
410+ let rhsFingerprint = CodexAuthFingerprint . normalize ( rhs. authFingerprint)
411+ guard lhsFingerprint != nil , rhsFingerprint != nil else { return false }
412+ guard case . providerAccount = rhs. identity, lhs. identity == rhs. identity else { return false }
413+ guard case . liveSystem = lhs. source else { return true }
414+ return lhs. accountKey != nil && lhs. accountKey == rhs. accountKey
415+ }
416+
417+ private nonisolated static func codexGuardAuthFingerprintAllowsSameProviderAccount(
418+ _ lhs: CodexAccountScopedRefreshGuard ,
419+ _ rhs: CodexAccountScopedRefreshGuard ) -> Bool
420+ {
421+ let lhsFingerprint = CodexAuthFingerprint . normalize ( lhs. authFingerprint)
422+ let rhsFingerprint = CodexAuthFingerprint . normalize ( rhs. authFingerprint)
423+ guard lhsFingerprint != nil , rhsFingerprint != nil else { return false }
424+ guard case . providerAccount = rhs. identity else { return false }
425+ return lhs. identity == rhs. identity
426+ }
427+
428+ nonisolated static func codexScopedRefreshGuardsMatchAccount(
429+ _ lhs: CodexAccountScopedRefreshGuard ,
430+ _ rhs: CodexAccountScopedRefreshGuard ) -> Bool
431+ {
432+ guard lhs. source == rhs. source else { return false }
433+ if lhs == rhs { return true }
434+ guard lhs. identity != . unresolved,
435+ lhs. identity == rhs. identity,
436+ lhs. accountKey == rhs. accountKey
437+ else {
438+ return false
439+ }
440+ return self . codexGuardAuthFingerprintAllowsUsageApply ( lhs, rhs)
441+ }
442+
443+ private nonisolated static func codexUsageResultAccountKeyMatchesCurrentGuard(
444+ _ resultAccountKey: String ? ,
445+ expectedGuard: CodexAccountScopedRefreshGuard ,
446+ currentGuard: CodexAccountScopedRefreshGuard ) -> Bool
447+ {
448+ guard let currentAccountKey = currentGuard. accountKey else { return true }
449+ guard let resultAccountKey else {
450+ guard let expectedAccountKey = expectedGuard. accountKey else { return true }
451+ return expectedAccountKey == currentAccountKey
452+ }
453+ return resultAccountKey == currentAccountKey
454+ }
455+
456+ func currentCodexAuthFingerprint( source: CodexActiveSource ) -> String ? {
457+ let snapshot = self . settings. codexAccountReconciliationSnapshot
458+ switch source {
459+ case . liveSystem:
460+ return CodexAuthFingerprint . normalize ( snapshot. liveSystemAccount? . authFingerprint)
461+ case let . managedAccount( id) :
462+ guard let account = snapshot. storedAccounts. first ( where: { $0. id == id } ) else { return nil }
463+ return CodexAuthFingerprint . fingerprint ( homePath: account. managedHomePath)
464+ }
465+ }
466+
280467 func codexAccountScopedRefreshKey(
281468 preferCurrentSnapshot: Bool = true ,
282469 allowLastKnownLiveFallback: Bool = true ) -> String ?
0 commit comments