()
| 521 | |
| 522 | // ── 8. 401 recovery (OAuth refresh + rebuild) ─────────────────────────── |
| 523 | async function recoverFromAuthFailure(): Promise<void> { |
| 524 | // setOnClose already guards `!authRecoveryInFlight` but that check and |
| 525 | // this set must be atomic against onRefresh — claim synchronously before |
| 526 | // any await. Laptop wake fires both paths ~simultaneously. |
| 527 | if (authRecoveryInFlight) return |
| 528 | authRecoveryInFlight = true |
| 529 | onStateChange?.('reconnecting', 'JWT expired — refreshing') |
| 530 | logForDebugging('[remote-bridge] 401 on SSE — attempting JWT refresh') |
| 531 | try { |
| 532 | const oauthToken = await runtimeAuth.refreshAccessToken() |
| 533 | if (!oauthToken || tornDown) { |
| 534 | if (!tornDown) { |
| 535 | onStateChange?.('failed', 'JWT refresh failed: no OAuth token') |
| 536 | } |
| 537 | return |
| 538 | } |
| 539 | |
| 540 | const fresh = await withRetry( |
| 541 | () => |
| 542 | fetchRemoteCredentials( |
| 543 | sessionId, |
| 544 | baseUrl, |
| 545 | oauthToken, |
| 546 | cfg.http_timeout_ms, |
| 547 | ), |
| 548 | 'fetchRemoteCredentials (recovery)', |
| 549 | cfg, |
| 550 | ) |
| 551 | if (!fresh || tornDown) { |
| 552 | if (!tornDown) { |
| 553 | onStateChange?.('failed', 'JWT refresh failed after 401') |
| 554 | } |
| 555 | return |
| 556 | } |
| 557 | // If 401 interrupted the initial flush, writeBatch may have silently |
| 558 | // no-op'd on the closed uploader (ccr.close() ran in the SSE wrapper |
| 559 | // before our setOnClose callback). Reset so the new onConnect re-flushes. |
| 560 | // (v1 scopes initialFlushDone inside the per-transport closure at |
| 561 | // replBridge.ts:1027 so it resets naturally; v2 has it at outer scope.) |
| 562 | initialFlushDone = false |
| 563 | await rebuildTransport(fresh, 'auth_401_recovery') |
| 564 | logForDebugging('[remote-bridge] Transport rebuilt after 401') |
| 565 | } catch (err) { |
| 566 | logForDebugging( |
| 567 | `[remote-bridge] 401 recovery failed: ${errorMessage(err)}`, |
| 568 | { level: 'error' }, |
| 569 | ) |
| 570 | logForDiagnosticsNoPII('error', 'bridge_repl_v2_jwt_refresh_failed') |
| 571 | if (!tornDown) { |
| 572 | onStateChange?.('failed', `JWT refresh failed: ${errorMessage(err)}`) |
| 573 | } |
| 574 | } finally { |
| 575 | authRecoveryInFlight = false |
| 576 | } |
| 577 | } |
| 578 | |
| 579 | wireTransportCallbacks() |
| 580 |
no test coverage detected