@@ -122,9 +122,13 @@ const parseTokenResponse = (json: unknown): CachedToken => {
122122/**
123123 * Run one attestation handshake and return the fresh token, or `undefined` when
124124 * there is nothing to do (no native module / unsupported platform). Never
125- * caches directly; the caller commits the result.
125+ * caches directly; the caller commits the result. `generation` identifies this
126+ * attempt so a stale (watchdog-released) handshake never mutates shared token
127+ * state that a newer handshake already owns.
126128 */
127- const performHandshake = async ( ) : Promise < CachedToken | undefined > => {
129+ const performHandshake = async (
130+ generation : number
131+ ) : Promise < CachedToken | undefined > => {
128132 // No native module (e.g. unsupported platform / dev environment).
129133 if ( EdgeAttestation == null ) return undefined
130134
@@ -162,8 +166,10 @@ const performHandshake = async (): Promise<CachedToken | undefined> => {
162166 // Server rejected the assertion: the enrolled key is no longer trusted,
163167 // so any previously-minted token is suspect too. Drop it now so gated
164168 // callers do not keep sending a token the server already rejects while
165- // re-enrollment is in progress; discard the key and re-attest.
166- cachedToken = undefined
169+ // re-enrollment is in progress; discard the key and re-attest. Only when
170+ // this is still the current handshake, so a stale (watchdog-released)
171+ // attempt cannot wipe a token a newer handshake already cached.
172+ if ( generation === handshakeGeneration ) cachedToken = undefined
167173 console . warn (
168174 `[attestation] assertion rejected (${ assertResponse . status } ); re-attesting`
169175 )
@@ -196,8 +202,9 @@ const performHandshake = async (): Promise<CachedToken | undefined> => {
196202 return parseTokenResponse ( await assertResponse . json ( ) )
197203 }
198204 // Server rejected the assertion: drop the now-suspect cached token (see
199- // the iOS branch above) before discarding the key and re-attesting.
200- cachedToken = undefined
205+ // the iOS branch above), guarded by the current generation, before
206+ // discarding the key and re-attesting.
207+ if ( generation === handshakeGeneration ) cachedToken = undefined
201208 console . warn (
202209 `[attestation] assertion rejected (${ assertResponse . status } ); re-attesting`
203210 )
@@ -265,7 +272,7 @@ const runHandshake = (): void => {
265272 // lock released so a newer handshake can start. Tag each attempt so a stale
266273 // one that finally resolves cannot clobber the newer handshake's token.
267274 const generation = ++ handshakeGeneration
268- const handshake : Promise < void > = performHandshake ( )
275+ const handshake : Promise < void > = performHandshake ( generation )
269276 . then ( freshToken => {
270277 if ( generation !== handshakeGeneration ) return
271278 lastFailureAt = 0
0 commit comments