From a86f555cbbd94ea287b2df22452f2b59abb32088 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:13:40 +0530 Subject: [PATCH] refresh rowboat access token on every gateway request Wire a custom fetch into the OpenRouter gateway provider so each outbound request resolves a fresh access token, instead of baking one token into the provider at turn start. Add a 60s expiry margin and serialize concurrent refreshes behind a single in-flight promise. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/x/packages/core/src/auth/oauth-client.ts | 7 ++-- apps/x/packages/core/src/auth/tokens.ts | 36 +++++++++++++------ apps/x/packages/core/src/models/gateway.ts | 11 ++++-- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/apps/x/packages/core/src/auth/oauth-client.ts b/apps/x/packages/core/src/auth/oauth-client.ts index ccabab19..045ab920 100644 --- a/apps/x/packages/core/src/auth/oauth-client.ts +++ b/apps/x/packages/core/src/auth/oauth-client.ts @@ -216,12 +216,15 @@ export async function refreshTokens( return tokens; } +const EXPIRY_MARGIN_SECONDS = 60; + /** - * Check if tokens are expired + * Check if tokens are expired. Treats tokens as expired EXPIRY_MARGIN_SECONDS + * before the real expiry to absorb clock skew and in-flight request latency. */ export function isTokenExpired(tokens: OAuthTokens): boolean { const now = Math.floor(Date.now() / 1000); - return tokens.expires_at <= now; + return tokens.expires_at <= now + EXPIRY_MARGIN_SECONDS; } /** diff --git a/apps/x/packages/core/src/auth/tokens.ts b/apps/x/packages/core/src/auth/tokens.ts index 8a30bf9f..fe3afe0f 100644 --- a/apps/x/packages/core/src/auth/tokens.ts +++ b/apps/x/packages/core/src/auth/tokens.ts @@ -3,18 +3,12 @@ import { IOAuthRepo } from './repo.js'; import { IClientRegistrationRepo } from './client-repo.js'; import { getProviderConfig } from './providers.js'; import * as oauthClient from './oauth-client.js'; +import { OAuthTokens } from './types.js'; -export async function getAccessToken(): Promise { - const oauthRepo = container.resolve('oauthRepo'); - const { tokens } = await oauthRepo.read('rowboat'); - if (!tokens) { - throw new Error('Not signed into Rowboat'); - } - - if (!oauthClient.isTokenExpired(tokens)) { - return tokens.access_token; - } +let refreshInFlight: Promise | null = null; +async function performRefresh(tokens: OAuthTokens): Promise { + console.log("Refreshing rowboat access token"); if (!tokens.refresh_token) { throw new Error('Rowboat token expired and no refresh token available. Please sign in again.'); } @@ -40,7 +34,29 @@ export async function getAccessToken(): Promise { tokens.refresh_token, tokens.scopes, ); + + const oauthRepo = container.resolve('oauthRepo'); await oauthRepo.upsert('rowboat', { tokens: refreshed }); + return refreshed; +} + +export async function getAccessToken(): Promise { + const oauthRepo = container.resolve('oauthRepo'); + const { tokens } = await oauthRepo.read('rowboat'); + if (!tokens) { + throw new Error('Not signed into Rowboat'); + } + + if (!oauthClient.isTokenExpired(tokens)) { + return tokens.access_token; + } + + if (!refreshInFlight) { + refreshInFlight = performRefresh(tokens).finally(() => { + refreshInFlight = null; + }); + } + const refreshed = await refreshInFlight; return refreshed.access_token; } diff --git a/apps/x/packages/core/src/models/gateway.ts b/apps/x/packages/core/src/models/gateway.ts index a18a37f5..df9b413c 100644 --- a/apps/x/packages/core/src/models/gateway.ts +++ b/apps/x/packages/core/src/models/gateway.ts @@ -3,11 +3,18 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider'; import { getAccessToken } from '../auth/tokens.js'; import { API_URL } from '../config/env.js'; +const authedFetch: typeof fetch = async (input, init) => { + const token = await getAccessToken(); + const headers = new Headers(init?.headers); + headers.set('Authorization', `Bearer ${token}`); + return fetch(input, { ...init, headers }); +}; + export async function getGatewayProvider(): Promise { - const accessToken = await getAccessToken(); return createOpenRouter({ baseURL: `${API_URL}/v1/llm`, - apiKey: accessToken, + apiKey: 'managed-by-rowboat', + fetch: authedFetch, }); }