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) <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-04-21 10:13:40 +05:30
parent a80ef4d320
commit a86f555cbb
3 changed files with 40 additions and 14 deletions

View file

@ -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;
}
/**

View file

@ -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<string> {
const oauthRepo = container.resolve<IOAuthRepo>('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<OAuthTokens> | null = null;
async function performRefresh(tokens: OAuthTokens): Promise<OAuthTokens> {
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<string> {
tokens.refresh_token,
tokens.scopes,
);
const oauthRepo = container.resolve<IOAuthRepo>('oauthRepo');
await oauthRepo.upsert('rowboat', { tokens: refreshed });
return refreshed;
}
export async function getAccessToken(): Promise<string> {
const oauthRepo = container.resolve<IOAuthRepo>('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;
}

View file

@ -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<ProviderV2> {
const accessToken = await getAccessToken();
return createOpenRouter({
baseURL: `${API_URL}/v1/llm`,
apiKey: accessToken,
apiKey: 'managed-by-rowboat',
fetch: authedFetch,
});
}