@@ -44,18 +44,27 @@ interface StoredApplicationToken {
4444 secretFingerprint : string
4545}
4646
47+ type StoredFailureCode = 'invalid_credentials' | 'grant_not_permitted' | 'site_not_found'
48+
4749interface StoredFailure {
48- code : 'invalid_credentials' | 'grant_not_permitted' | 'site_not_found' | 'provider_unavailable'
50+ code : StoredFailureCode
4951 status : number
5052}
5153
52- const STORED_FAILURE_CODES : ReadonlySet < string > = new Set ( [
54+ /**
55+ * Refusals that say the credentials themselves are wrong, so replaying them is safe. A transient
56+ * failure (`provider_unavailable`: a timeout, 5xx, 429, or malformed reply) is never remembered.
57+ */
58+ const STORED_FAILURE_CODES : ReadonlySet < string > = new Set < StoredFailureCode > ( [
5359 'invalid_credentials' ,
5460 'grant_not_permitted' ,
5561 'site_not_found' ,
56- 'provider_unavailable' ,
5762] )
5863
64+ function isStoredFailureCode ( code : string ) : code is StoredFailureCode {
65+ return STORED_FAILURE_CODES . has ( code )
66+ }
67+
5968function digest ( value : unknown ) : string {
6069 return createHash ( 'sha256' ) . update ( JSON . stringify ( value ) ) . digest ( 'hex' )
6170}
@@ -132,11 +141,11 @@ function decodeFailure(value: string, step: string): StoredFailure {
132141 if (
133142 ! isRecordLike ( parsed ) ||
134143 typeof parsed . code !== 'string' ||
135- ! STORED_FAILURE_CODES . has ( parsed . code ) ||
144+ ! isStoredFailureCode ( parsed . code ) ||
136145 typeof parsed . status !== 'number'
137146 )
138147 throw unavailable ( step , 'stored token failure is malformed' )
139- return { code : parsed . code as StoredFailure [ 'code' ] , status : parsed . status }
148+ return { code : parsed . code , status : parsed . status }
140149}
141150
142151function remainingSeconds ( expiresAt : Date ) : number {
@@ -154,9 +163,10 @@ function isServable(expiresAt: Date): boolean {
154163 * to reuse one token rather than accumulate them. Minting happens only under a
155164 * transaction-scoped advisory lock keyed by the application, so two workers never race two
156165 * tokens into existence; a token minted with a different secret is never served, and a token the
157- * provider rejects is expired through {@link invalidateCoordinatedClientToken}. A rejected
158- * exchange is remembered for 30 seconds so a hot workflow on a revoked secret does not hammer the
159- * token endpoint. Tokens are encrypted at rest.
166+ * provider rejects is expired through {@link invalidateCoordinatedClientToken}. An exchange the
167+ * provider refuses for the credentials themselves is remembered for 30 seconds so a hot workflow on
168+ * a revoked secret does not hammer the token endpoint; a transient failure is not remembered.
169+ * Tokens are encrypted at rest.
160170 */
161171export async function resolveCoordinatedClientToken (
162172 input : CoordinatedClientTokenInput
@@ -225,13 +235,12 @@ export async function resolveCoordinatedClientToken(
225235 expiresInSeconds : remainingSeconds ( exchanged . expiresAt ) ,
226236 }
227237 } catch ( error ) {
228- if ( ! ( error instanceof TokenServiceAccountValidationError ) ) throw error
229- const failure : StoredFailure = {
230- code : STORED_FAILURE_CODES . has ( error . code )
231- ? ( error . code as StoredFailure [ 'code' ] )
232- : 'provider_unavailable' ,
233- status : error . status ,
234- }
238+ if (
239+ ! ( error instanceof TokenServiceAccountValidationError ) ||
240+ ! isStoredFailureCode ( error . code )
241+ )
242+ throw error
243+ const failure : StoredFailure = { code : error . code , status : error . status }
235244 const { encrypted } = await encryptSecret ( JSON . stringify ( failure ) )
236245 const expiresAt = new Date ( Date . now ( ) + FAILURE_TTL_MS )
237246 await tx
0 commit comments