@@ -44,27 +44,18 @@ interface StoredApplicationToken { |
44 | 44 | secretFingerprint: string |
45 | 45 | } |
46 | 46 |
|
47 | | -type StoredFailureCode = 'invalid_credentials' | 'grant_not_permitted' | 'site_not_found' |
48 | | - |
49 | 47 | interface StoredFailure { |
50 | | - code: StoredFailureCode |
| 48 | + code: 'invalid_credentials' | 'grant_not_permitted' | 'site_not_found' | 'provider_unavailable' |
51 | 49 | status: number |
52 | 50 | } |
53 | 51 |
|
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([ |
59 | 53 | 'invalid_credentials', |
60 | 54 | 'grant_not_permitted', |
61 | 55 | 'site_not_found', |
| 56 | + 'provider_unavailable', |
62 | 57 | ]) |
63 | 58 |
|
64 | | -function isStoredFailureCode(code: string): code is StoredFailureCode { |
65 | | - return STORED_FAILURE_CODES.has(code) |
66 | | -} |
67 | | - |
68 | 59 | function digest(value: unknown): string { |
69 | 60 | return createHash('sha256').update(JSON.stringify(value)).digest('hex') |
70 | 61 | } |
@@ -141,11 +132,11 @@ function decodeFailure(value: string, step: string): StoredFailure { |
141 | 132 | if ( |
142 | 133 | !isRecordLike(parsed) || |
143 | 134 | typeof parsed.code !== 'string' || |
144 | | - !isStoredFailureCode(parsed.code) || |
| 135 | + !STORED_FAILURE_CODES.has(parsed.code) || |
145 | 136 | typeof parsed.status !== 'number' |
146 | 137 | ) |
147 | 138 | 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 } |
149 | 140 | } |
150 | 141 |
|
151 | 142 | function remainingSeconds(expiresAt: Date): number { |
@@ -163,10 +154,11 @@ function isServable(expiresAt: Date): boolean { |
163 | 154 | * to reuse one token rather than accumulate them. Minting happens only under a |
164 | 155 | * transaction-scoped advisory lock keyed by the application, so two workers never race two |
165 | 156 | * 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. |
170 | 162 | */ |
171 | 163 | export async function resolveCoordinatedClientToken( |
172 | 164 | input: CoordinatedClientTokenInput |
@@ -235,12 +227,13 @@ export async function resolveCoordinatedClientToken( |
235 | 227 | expiresInSeconds: remainingSeconds(exchanged.expiresAt), |
236 | 228 | } |
237 | 229 | } 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 | + } |
244 | 237 | const { encrypted } = await encryptSecret(JSON.stringify(failure)) |
245 | 238 | const expiresAt = new Date(Date.now() + FAILURE_TTL_MS) |
246 | 239 | await tx |
|