(
row: ConnectionRow,
provider: CredentialProvider,
trigger: RefreshTrigger,
)
| 2269 | |
| 2270 | // Perform the actual refresh-token grant and persist the rotated material. |
| 2271 | const performTokenRefresh = ( |
| 2272 | row: ConnectionRow, |
| 2273 | provider: CredentialProvider, |
| 2274 | trigger: RefreshTrigger, |
| 2275 | ): Effect.Effect<string | null, StorageFailure | CredentialResolutionError> => |
| 2276 | Effect.gen(function* () { |
| 2277 | const owner = row.owner as Owner; |
| 2278 | const reauth = (message: string): CredentialResolutionError => |
| 2279 | new CredentialResolutionError({ |
| 2280 | owner, |
| 2281 | integration: IntegrationSlug.make(row.integration), |
| 2282 | name: ConnectionName.make(row.name), |
| 2283 | message, |
| 2284 | reauthRequired: true, |
| 2285 | }); |
| 2286 | |
| 2287 | // A recorded invalid_grant is the AS's standing verdict on this grant: |
| 2288 | // re-sending it cannot succeed, so don't. Fail as reauth-required |
| 2289 | // without a token request — the reconnect mint rewrites |
| 2290 | // `provider_state` and thereby re-arms refresh. Without this gate a |
| 2291 | // dead connection re-sent its dead grant on every proactive cycle, |
| 2292 | // indefinitely (owner.com's Datadog connections: 100+ identical |
| 2293 | // rejections over two days, surfacing nothing). |
| 2294 | const reauthState = oauthReauthRequiredFromProviderState(row.provider_state); |
| 2295 | if (reauthState !== null) { |
| 2296 | yield* Effect.annotateCurrentSpan({ "executor.oauth.refresh.skipped_known_dead": true }); |
| 2297 | const recordedHealth = Option.getOrNull(decodeLastHealth(row.last_health)); |
| 2298 | const recordedDetail = |
| 2299 | reauthState.oauthReauthRequiredDetail ?? |
| 2300 | (recordedHealth?.status === "expired" ? recordedHealth.detail : undefined); |
| 2301 | const detail = |
| 2302 | recordedDetail === undefined |
| 2303 | ? "The authorization server rejected this connection's refresh token (invalid_grant). Reconnect to continue." |
| 2304 | : recordedDetail.endsWith("Reconnect to continue.") |
| 2305 | ? recordedDetail |
| 2306 | : `${recordedDetail} Reconnect to continue.`; |
| 2307 | return yield* reauth(detail); |
| 2308 | } |
| 2309 | |
| 2310 | // Load the backing app. A `first-party:` slug resolves from host config |
| 2311 | // (deployment-owned identity, in-memory secret); a stored slug loads by |
| 2312 | // the owner STORED on the connection (a Personal connection may be |
| 2313 | // backed by a shared Workspace app) — no derivation — with its secret |
| 2314 | // resolved out of the credential provider by item id. |
| 2315 | const clientSlug = String(row.oauth_client); |
| 2316 | const clientRow: RefreshClient | null = yield* Effect.gen(function* () { |
| 2317 | if (isFirstPartyOAuthClientSlug(clientSlug)) { |
| 2318 | const firstParty = firstPartyOAuthBySlug.get(clientSlug); |
| 2319 | if (!firstParty) return null; |
| 2320 | return { |
| 2321 | clientId: firstParty.clientId, |
| 2322 | clientSecret: firstParty.clientSecret, |
| 2323 | tokenUrl: firstParty.tokenUrl, |
| 2324 | grant: "authorization_code", |
| 2325 | // RFC 8707: the SAME resource the authorize/exchange path sent |
| 2326 | // (`loadedFirstPartyClient`), so the refreshed token keeps the |
| 2327 | // audience the original grant was bound to. Dropping it here |
| 2328 | // made refresh asymmetric with authorize for first-party apps. |
no test coverage detected