@@ -44,27 +44,18 @@ interface StoredApplicationToken {
4444 secretFingerprint : string
4545}
4646
47- type StoredFailureCode = 'invalid_credentials' | 'grant_not_permitted' | 'site_not_found'
48-
4947interface StoredFailure {
50- code : StoredFailureCode
48+ code : 'invalid_credentials' | 'grant_not_permitted' | 'site_not_found' | 'provider_unavailable'
5149 status : number
5250}
5351
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 > ( [
52+ const STORED_FAILURE_CODES : ReadonlySet < string > = new Set ( [
5953 'invalid_credentials' ,
6054 'grant_not_permitted' ,
6155 'site_not_found' ,
56+ 'provider_unavailable' ,
6257] )
6358
64- function isStoredFailureCode ( code : string ) : code is StoredFailureCode {
65- return STORED_FAILURE_CODES . has ( code )
66- }
67-
6859function digest ( value : unknown ) : string {
6960 return createHash ( 'sha256' ) . update ( JSON . stringify ( value ) ) . digest ( 'hex' )
7061}
@@ -141,11 +132,11 @@ function decodeFailure(value: string, step: string): StoredFailure {
141132 if (
142133 ! isRecordLike ( parsed ) ||
143134 typeof parsed . code !== 'string' ||
144- ! isStoredFailureCode ( parsed . code ) ||
135+ ! STORED_FAILURE_CODES . has ( parsed . code ) ||
145136 typeof parsed . status !== 'number'
146137 )
147138 throw unavailable ( step , 'stored token failure is malformed' )
148- return { code : parsed . code , status : parsed . status }
139+ return { code : parsed . code as StoredFailure [ 'code' ] , status : parsed . status }
149140}
150141
151142function remainingSeconds ( expiresAt : Date ) : number {
@@ -163,10 +154,11 @@ function isServable(expiresAt: Date): boolean {
163154 * to reuse one token rather than accumulate them. Minting happens only under a
164155 * transaction-scoped advisory lock keyed by the application, so two workers never race two
165156 * tokens into existence; a token minted with a different secret is never served, and a token the
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.
157+ * provider rejects is expired through {@link invalidateCoordinatedClientToken}. An exchange that
158+ * fails validation, whether refused for the credentials or answered with an outage, is remembered
159+ * for 30 seconds, the same backoff the per-instance mint cache applies, so a hot workflow neither
160+ * hammers the token endpoint with a revoked secret nor floods a struggling one. Tokens are
161+ * encrypted at rest.
170162 */
171163export async function resolveCoordinatedClientToken (
172164 input : CoordinatedClientTokenInput
@@ -235,12 +227,13 @@ export async function resolveCoordinatedClientToken(
235227 expiresInSeconds : remainingSeconds ( exchanged . expiresAt ) ,
236228 }
237229 } catch ( error ) {
238- if (
239- ! ( error instanceof TokenServiceAccountValidationError ) ||
240- ! isStoredFailureCode ( error . code )
241- )
242- throw error
243- const failure : StoredFailure = { code : error . code , status : error . status }
230+ if ( ! ( error instanceof TokenServiceAccountValidationError ) ) throw error
231+ const failure : StoredFailure = {
232+ code : STORED_FAILURE_CODES . has ( error . code )
233+ ? ( error . code as StoredFailure [ 'code' ] )
234+ : 'provider_unavailable' ,
235+ status : error . status ,
236+ }
244237 const { encrypted } = await encryptSecret ( JSON . stringify ( failure ) )
245238 const expiresAt = new Date ( Date . now ( ) + FAILURE_TTL_MS )
246239 await tx
0 commit comments