fix: prevent false "OAuth token expired" push on startup
Start OAuthRefreshScheduler before the refresh-blind OAuthExpiryMonitor so a stale-but-refreshable access token is renewed before the monitor's first awaited check() reads `expires`. Previously the monitor fired a false "OAuth token expired" ntfy push on startup, moments before the refresher silently renewed the token. Ordering locked by an invocationCallOrder assertion in project-engine.test.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.changeset/oauth-expiry-refresh-order.md
Normal file
7
.changeset/oauth-expiry-refresh-order.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Stop the false "OAuth token expired" push notification on startup.
|
||||||
|
category: fix
|
||||||
|
dev: In ProjectEngine.start, OAuthRefreshScheduler.start() now runs before OAuthExpiryMonitor.start() so the proactive refresh renews a stale-but-refreshable access token before the refresh-blind monitor's first awaited check() reads `expires`. Ordering locked by an invocationCallOrder assertion in project-engine.test.ts.
|
||||||
@@ -408,6 +408,14 @@ describe("ProjectEngine notification ownership wiring", () => {
|
|||||||
expect(mocks.oauthExpiryMonitorStart).toHaveBeenCalledTimes(1);
|
expect(mocks.oauthExpiryMonitorStart).toHaveBeenCalledTimes(1);
|
||||||
expect(mocks.notifierStart).toHaveBeenCalledTimes(1);
|
expect(mocks.notifierStart).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// FNXC:ClaudeOAuth 2026-07-08-12:10: the proactive refresher must start BEFORE the
|
||||||
|
// refresh-blind expiry monitor's first (awaited) check, or a stale-but-refreshable
|
||||||
|
// access token fires a false "OAuth token expired" ntfy push on startup. Lock the order.
|
||||||
|
expect(mocks.oauthRefreshSchedulerStart).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.oauthRefreshSchedulerStart.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
mocks.oauthExpiryMonitorStart.mock.invocationCallOrder[0],
|
||||||
|
);
|
||||||
|
|
||||||
await engine.stop();
|
await engine.stop();
|
||||||
expect(mocks.oauthExpiryMonitorStop).toHaveBeenCalledTimes(1);
|
expect(mocks.oauthExpiryMonitorStop).toHaveBeenCalledTimes(1);
|
||||||
expect(mocks.notificationServiceStop).toHaveBeenCalledTimes(1);
|
expect(mocks.notificationServiceStop).toHaveBeenCalledTimes(1);
|
||||||
|
|||||||
@@ -716,22 +716,34 @@ export class ProjectEngine {
|
|||||||
const oauthAlertState = new OAuthAlertStateStore({
|
const oauthAlertState = new OAuthAlertStateStore({
|
||||||
statePath: getFusionOAuthAlertStatePath(),
|
statePath: getFusionOAuthAlertStatePath(),
|
||||||
});
|
});
|
||||||
|
/*
|
||||||
|
FNXC:ClaudeOAuth 2026-07-05-00:00:
|
||||||
|
FN-7574: proactively refresh OAuth access tokens ahead of expiry (widened window,
|
||||||
|
see OAUTH_REFRESH_BUFFER_MS in auth-storage.ts) so a healthy subscription session
|
||||||
|
never lapses waiting for something else to request a runtime API key. Reuses the
|
||||||
|
same authStorage instance as OAuthExpiryMonitor below so detection/notification and
|
||||||
|
proactive refresh observe a consistent, single credential source.
|
||||||
|
|
||||||
|
FNXC:ClaudeOAuth 2026-07-08-12:10:
|
||||||
|
Start the proactive refresher BEFORE OAuthExpiryMonitor. Both are awaited on startup
|
||||||
|
and share this authStorage; the monitor's OAuthExpiryMonitor.start() runs its first
|
||||||
|
check() synchronously, and that check is refresh-blind (it only reads the stored
|
||||||
|
`expires` timestamp, with no refresh token or getApiKey in its interface). If the
|
||||||
|
monitor ran first, a stale-but-refreshable access token (the normal state after the
|
||||||
|
app has been closed a while) fired a false "OAuth token expired" ntfy push even
|
||||||
|
though the connection was fine — the refresher would silently renew the token moments
|
||||||
|
later. Refreshing first means the scheduler's awaited initial tick() renews the token
|
||||||
|
and the monitor (which reload()s authStorage at the top of check()) sees the fresh
|
||||||
|
`expires`, so the alarm only fires when refresh genuinely fails.
|
||||||
|
*/
|
||||||
|
this.oauthRefreshScheduler = new OAuthRefreshScheduler({ authStorage });
|
||||||
|
await this.oauthRefreshScheduler.start();
|
||||||
this.oauthExpiryMonitor = new OAuthExpiryMonitor({
|
this.oauthExpiryMonitor = new OAuthExpiryMonitor({
|
||||||
authStorage,
|
authStorage,
|
||||||
notificationService: this.notificationService,
|
notificationService: this.notificationService,
|
||||||
alertState: oauthAlertState,
|
alertState: oauthAlertState,
|
||||||
});
|
});
|
||||||
await this.oauthExpiryMonitor.start();
|
await this.oauthExpiryMonitor.start();
|
||||||
/*
|
|
||||||
FNXC:ClaudeOAuth 2026-07-05-00:00:
|
|
||||||
FN-7574: proactively refresh OAuth access tokens ahead of expiry (widened window,
|
|
||||||
see OAUTH_REFRESH_BUFFER_MS in auth-storage.ts) so a healthy subscription session
|
|
||||||
never lapses waiting for something else to request a runtime API key. Reuses the
|
|
||||||
same authStorage instance as OAuthExpiryMonitor above so detection/notification and
|
|
||||||
proactive refresh observe a consistent, single credential source.
|
|
||||||
*/
|
|
||||||
this.oauthRefreshScheduler = new OAuthRefreshScheduler({ authStorage });
|
|
||||||
await this.oauthRefreshScheduler.start();
|
|
||||||
this.oauthValidityLogger = new OAuthValidityLogger({
|
this.oauthValidityLogger = new OAuthValidityLogger({
|
||||||
authStorage,
|
authStorage,
|
||||||
alertState: oauthAlertState,
|
alertState: oauthAlertState,
|
||||||
|
|||||||
Reference in New Issue
Block a user