Conversation
When a user re-authorizes an integration whose parent Integration record
is in a broken state (ERROR or DISABLED), Frigg refreshes the credential
correctly but leaves Integration.status untouched. The queue worker
short-circuits on DISABLED, so webhooks are silently discarded forever
even after the customer completes a new OAuth flow.
This fix walks from the re-authorized entity up to any parent integrations
and flips those in {ERROR, DISABLED} back to ENABLED, so customer-driven
reconnect actually recovers the integration.
Changes:
- Add findIntegrationsByEntityId to IntegrationRepositoryInterface,
implemented in Postgres (many-to-many via entities.some), Mongo
(entityIds.has scalar array), and DocumentDB (raw findMany).
- Inject integrationRepository into ProcessAuthorizationCallback; run
the restoration at the end of execute(), after credential + entity
are persisted.
- Status reset is best-effort (try/catch + console.error on failure)
so a DB hiccup never fails an otherwise-successful re-auth.
- Log each status flip for operator visibility.
- Extend TestIntegrationRepository double with matching in-memory method.
- 5 unit tests covering ERROR flip, DISABLED flip, ENABLED no-op, empty
list (first-time auth), and mixed statuses.
This is Gap C of 3 from an RCA on Attio dead-token loops. Gaps A
(unconditional refresh POST when refresh_token is null) and B (no
auto-flip to ERROR on DLGT_INVALID_AUTH) are separate upcoming PRs.
Verified end-to-end against a live Attio integration: status flipped
DISABLED → ENABLED on re-auth with log line
"[Frigg] Restoring integration 48 from DISABLED to ENABLED after
successful re-auth (entityId=60)".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The comment was referencing a Quo-internal RCA. Removed for upstream clarity without changing semantics — the rule itself and its rationale remain in the comment block. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contributor
Author
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
|
d-klotz
added a commit
that referenced
this pull request
Apr 15, 2026
…sible For OAuth providers that never issue refresh tokens (canonical case: Attio), Frigg's 401 retry path today makes a guaranteed HTTP 400 on every retry attempt because the token endpoint explicitly rejects grant_type=refresh_token. In production this produces the primary error-log volume on the Attio queue worker. The `isRefreshable` flag is already the gate for the 401 retry path at requester.js:74 — when false, the retry short-circuits straight to notify(DLGT_INVALID_AUTH) with no doomed POST. The bug is that the default is unconditionally `true`, ignoring whether a refresh_token was ever issued. This makes `isRefreshable` honest: `true` means "I can refresh," `false` means "I can't — go straight to invalidation." Derivation is consistent at every point where `refresh_token` state is set. Changes: - Constructor (oauth-2.js): narrow `isRefreshable` to false when the grant type is non-`client_credentials` and no `refresh_token` is present. Subclass opt-outs via `this.isRefreshable = false` after `super(params)` continue to work — the narrowing runs before the subclass body. - setTokens (oauth-2.js): re-derive `isRefreshable` from the current state after any token-response update. This correctly WIDENS from false → true on first-time auth for refresh-supporting providers (Pipedrive, Zoho) whose initial auth-code response carries a refresh_token. Idempotent on subsequent refreshes. - refreshAuth (oauth-2.js): defense-in-depth guard for direct callers. When `isRefreshable` is false, short-circuit to `notify(DLGT_INVALID_AUTH)` without invoking the token endpoint. Unit tests (oauth-2.test.js): - 5 tests for constructor narrowing across all grant types, including subclass-override-wins regression - 4 tests for setTokens re-derivation covering widen / preserve / narrow / client_credentials-passthrough - 1 test for refreshAuth guard - 1 integration test through _get: OAuth2Requester with no refresh_token receives 401, verifies refreshAuth/refreshAccessToken are never called, fetch invoked exactly once, DLGT_INVALID_AUTH fired once - Updated 1 pre-existing test to supply refresh_token explicitly, since "default isRefreshable=true" now requires a refresh_token This is Gap A of three framework-level fixes for the Attio dead-token RCA. Gap C (restore Integration.status on successful re-auth) landed in #574 — this PR reuses no Gap-C code but was stacked on that branch during local testing. Gap B (auto-flip Integration.status to DISABLED on DLGT_INVALID_AUTH) is a separate upcoming PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 tasks
d-klotz
added a commit
that referenced
this pull request
Apr 16, 2026
When OAuth2Requester.refreshAuth fails and Module.markCredentialsInvalid flips Credential.authIsValid to false, Integration.status was left as ENABLED. The queue worker only short-circuits on DISABLED, so every subsequent webhook kept getting processed, failed again, and re-entered the loop — producing 94k+ error lines/day for Attio integrations with dead tokens. This fix uses Frigg's existing Delegate pattern to propagate the failure upward: 1. Module fires a new CREDENTIAL_INVALIDATED event from markCredentialsInvalid (best-effort, try/catch so it never alters refreshAuth's documented `return false` contract). 2. IntegrationBase.receiveNotification catches the event and calls updateIntegrationStatus.execute(this.id, 'DISABLED'). 3. The delegate wire (module.delegate = this) is installed in IntegrationBase._appendModules, which covers all 7 code paths that construct Integration instances (HTTP reads, queue workers, create/update/delete flows). Recovery is handled by the Gap C fix (PR #574): ProcessAuthorizationCallback.restoreIntegrationsForEntity flips {ERROR, DISABLED} back to ENABLED on successful re-auth. This is Gap B of 3 from the Attio dead-token RCA. Gap A (refresh short-circuit when refresh_token is null) is PR #575. 6 unit tests covering: delegate propagation, null-delegate backward compat, status flip, unknown-delegate no-op, missing-id guard, and module-delegate wiring at construction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7 tasks
d-klotz
added a commit
that referenced
this pull request
Apr 16, 2026
…ker discard
Per Frigg team lead feedback: DISABLED is reserved for user-intent
("turn off integration, keep settings"). System-driven credential
failures should use ERROR to preserve the semantic distinction.
Changes:
- IntegrationBase.receiveNotification: DISABLED -> ERROR
- backend-utils.js queue worker: extend status discard check at
both lines 157 and 173 from `=== 'DISABLED'` to
`['DISABLED', 'ERROR'].includes(status)` so ERROR integrations
are also short-circuited (no wasted webhook processing)
- Updated log messages to show actual status dynamically
- Added test for ERROR discard in integration-defined-workers
Gap C (PR #574) already resets both {ERROR, DISABLED} on re-auth
via STATUSES_RESET_ON_REAUTH — no change needed there.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Summary
When a user re-authorizes an integration whose parent
Integrationrecord is in a broken state (ERRORorDISABLED), the framework refreshes the credential correctly but leavesIntegration.statusuntouched. The queue worker short-circuits onDISABLED(backend-utils.js:157,173), so webhooks are silently discarded forever even after the customer completes a new OAuth flow.This PR walks from the re-authorized entity up to any parent integrations and flips those in
{ERROR, DISABLED}back toENABLED, so customer-driven reconnect actually recovers the integration.This is Gap C of 3 from an RCA on Attio dead-token loops. Gap A (
OAuth2Requester.refreshAuthunconditionally POSTsgrant_type=refresh_tokeneven when the storedrefresh_tokenisnull) and Gap B (no auto-flip toERRORonDLGT_INVALID_AUTH) are separate upcoming PRs.Changes
findIntegrationsByEntityId(entityId)onIntegrationRepositoryInterface.entities.some.identityIds.hasfindManywith{ entityIds: objectId }ProcessAuthorizationCallback:integrationRepository(backward compatible — omitted injection is a silent no-op).execute(), calls newrestoreIntegrationsForEntity(entityId)helper which looks up integrations referencing the entity and flips any inSTATUSES_RESET_ON_REAUTH = ['ERROR', 'DISABLED']toENABLED.console.erroron failure.[Frigg] Restoring integration X from Y to ENABLED after successful re-auth (entityId=Z)for operator visibility.integration-router.jspasses the already-createdintegrationRepositoryto theProcessAuthorizationCallbackconstructor.TestIntegrationRepositoryextended with matching in-memoryfindIntegrationsByEntityId.Semantic decision
Both
ERRORandDISABLEDare reset on re-auth. Reasoning: the user clicking "Reconnect" is an implicit opt-in to re-enable — whether the integration went bad via a system error or a manual user pause, the action of re-authorizing expresses intent to use it again.PROCESSING(active-run state) andNEEDS_CONFIG(config-incomplete, auth alone doesn't fix) are intentionally excluded.Test plan
Unit tests (all 5 pass):
Manual end-to-end verification on a local install:
id=48set tostatus=DISABLED+Credential.authIsValid=falseGET /api/integrationsand direct DB query:Integration[48].status→ENABLED✓Credential[48].authIsValid→trueaccess_tokenadvanced[Frigg] Restoring integration 48 from DISABLED to ENABLED after successful re-auth (entityId=60)present🤖 Generated with Claude Code