(
row: ConnectionRow,
provider: CredentialProvider,
trigger: RefreshTrigger,
)
| 1507 | |
| 1508 | // Perform the actual refresh-token grant and persist the rotated material. |
| 1509 | const performTokenRefresh = ( |
| 1510 | row: ConnectionRow, |
| 1511 | provider: CredentialProvider, |
| 1512 | trigger: RefreshTrigger, |
| 1513 | ): Effect.Effect<string | null, StorageFailure | CredentialResolutionError> => |
| 1514 | Effect.gen(function* () { |
| 1515 | const owner = row.owner as Owner; |
| 1516 | const reauth = (message: string): CredentialResolutionError => |
| 1517 | new CredentialResolutionError({ |
| 1518 | owner, |
| 1519 | integration: IntegrationSlug.make(row.integration), |
| 1520 | name: ConnectionName.make(row.name), |
| 1521 | message, |
| 1522 | reauthRequired: true, |
| 1523 | }); |
| 1524 | |
| 1525 | // Load the backing app by the owner STORED on the connection (a Personal |
| 1526 | // connection may be backed by a shared Workspace app) — no derivation. |
| 1527 | const clientOwner = (row.oauth_client_owner ?? row.owner) as Owner; |
| 1528 | const clientRow = yield* loadOAuthClientRow(clientOwner, String(row.oauth_client)); |
| 1529 | if (!clientRow) { |
| 1530 | return yield* reauth(`OAuth client "${row.oauth_client}" is no longer registered.`); |
| 1531 | } |
| 1532 | |
| 1533 | // The secret is stored in the provider (a vault item id), not inline. |
| 1534 | const clientSecret = clientRow.client_secret_item_id |
| 1535 | ? ((yield* provider.get(ProviderItemId.make(String(clientRow.client_secret_item_id)))) ?? |
| 1536 | "") |
| 1537 | : ""; |
| 1538 | // Re-request the scopes this connection was GRANTED (RFC 6749 §6: a |
| 1539 | // refresh must not exceed the originally-granted scope). Empty → omit |
| 1540 | // the param, which the AS treats as "same scopes as granted". |
| 1541 | const grantedScopes = row.oauth_scope |
| 1542 | ? String(row.oauth_scope).split(/\s+/).filter(Boolean) |
| 1543 | : []; |
| 1544 | |
| 1545 | // Refresh against the region the code was redeemed at when one was |
| 1546 | // recorded at connect time (multi-site providers like Datadog), else |
| 1547 | // the oauth_client's configured token endpoint. |
| 1548 | const tokenUrl = row.oauth_token_url |
| 1549 | ? String(row.oauth_token_url) |
| 1550 | : String(clientRow.token_url); |
| 1551 | |
| 1552 | // client_credentials (machine-to-machine) has NO refresh token — the |
| 1553 | // token is RE-MINTED from the client id/secret. The authorization_code |
| 1554 | // path below needs a stored refresh token. Branching on grant here is |
| 1555 | // what keeps a client_credentials connection (e.g. DealCloud) from |
| 1556 | // demanding a re-auth on a credential that has no human to re-auth. |
| 1557 | const token = |
| 1558 | String(clientRow.grant) === "client_credentials" |
| 1559 | ? yield* exchangeClientCredentials({ |
| 1560 | tokenUrl, |
| 1561 | clientId: String(clientRow.client_id), |
| 1562 | clientSecret, |
| 1563 | scopes: grantedScopes, |
| 1564 | resource: clientRow.resource ? String(clientRow.resource) : undefined, |
| 1565 | endpointUrlPolicy: config.oauthEndpointUrlPolicy, |
| 1566 | fetch: config.fetch, |
no test coverage detected