(
row: ConnectionRow,
provider: CredentialProvider,
trigger: RefreshTrigger,
)
| 1802 | |
| 1803 | // Perform the actual refresh-token grant and persist the rotated material. |
| 1804 | const performTokenRefresh = ( |
| 1805 | row: ConnectionRow, |
| 1806 | provider: CredentialProvider, |
| 1807 | trigger: RefreshTrigger, |
| 1808 | ): Effect.Effect<string | null, StorageFailure | CredentialResolutionError> => |
| 1809 | Effect.gen(function* () { |
| 1810 | const owner = row.owner as Owner; |
| 1811 | const reauth = (message: string): CredentialResolutionError => |
| 1812 | new CredentialResolutionError({ |
| 1813 | owner, |
| 1814 | integration: IntegrationSlug.make(row.integration), |
| 1815 | name: ConnectionName.make(row.name), |
| 1816 | message, |
| 1817 | reauthRequired: true, |
| 1818 | }); |
| 1819 | |
| 1820 | // A recorded invalid_grant is the AS's standing verdict on this grant: |
| 1821 | // re-sending it cannot succeed, so don't. Fail as reauth-required |
| 1822 | // without a token request — the reconnect mint rewrites |
| 1823 | // `provider_state` and thereby re-arms refresh. Without this gate a |
| 1824 | // dead connection re-sent its dead grant on every proactive cycle, |
| 1825 | // indefinitely (owner.com's Datadog connections: 100+ identical |
| 1826 | // rejections over two days, surfacing nothing). |
| 1827 | const reauthState = oauthReauthRequiredFromProviderState(row.provider_state); |
| 1828 | if (reauthState !== null) { |
| 1829 | yield* Effect.annotateCurrentSpan({ "executor.oauth.refresh.skipped_known_dead": true }); |
| 1830 | const recordedHealth = Option.getOrNull(decodeLastHealth(row.last_health)); |
| 1831 | const recordedDetail = |
| 1832 | reauthState.oauthReauthRequiredDetail ?? |
| 1833 | (recordedHealth?.status === "expired" ? recordedHealth.detail : undefined); |
| 1834 | const detail = |
| 1835 | recordedDetail === undefined |
| 1836 | ? "The authorization server rejected this connection's refresh token (invalid_grant). Reconnect to continue." |
| 1837 | : recordedDetail.endsWith("Reconnect to continue.") |
| 1838 | ? recordedDetail |
| 1839 | : `${recordedDetail} Reconnect to continue.`; |
| 1840 | return yield* reauth(detail); |
| 1841 | } |
| 1842 | |
| 1843 | // Load the backing app by the owner STORED on the connection (a Personal |
| 1844 | // connection may be backed by a shared Workspace app) — no derivation. |
| 1845 | const clientOwner = (row.oauth_client_owner ?? row.owner) as Owner; |
| 1846 | const clientRow = yield* loadOAuthClientRow(clientOwner, String(row.oauth_client)); |
| 1847 | if (!clientRow) { |
| 1848 | return yield* reauth(`OAuth client "${row.oauth_client}" is no longer registered.`); |
| 1849 | } |
| 1850 | |
| 1851 | // The secret is stored in the provider (a vault item id), not inline. |
| 1852 | const clientSecret = clientRow.client_secret_item_id |
| 1853 | ? ((yield* provider.get(ProviderItemId.make(String(clientRow.client_secret_item_id)))) ?? |
| 1854 | "") |
| 1855 | : ""; |
| 1856 | // Re-request the scopes this connection was GRANTED (RFC 6749 §6: a |
| 1857 | // refresh must not exceed the originally-granted scope). Empty → omit |
| 1858 | // the param, which the AS treats as "same scopes as granted". |
| 1859 | const grantedScopes = row.oauth_scope |
| 1860 | ? String(row.oauth_scope).split(/\s+/).filter(Boolean) |
| 1861 | : []; |
no test coverage detected