diff --git a/open-sse/utils/proxyFetch.js b/open-sse/utils/proxyFetch.js index 23dbdd1..38b3539 100644 --- a/open-sse/utils/proxyFetch.js +++ b/open-sse/utils/proxyFetch.js @@ -156,9 +156,11 @@ async function getDispatcher(proxyUrl) { * Create HTTPS request with manual socket connection (bypass DNS) */ async function createBypassRequest(parsedUrl, realIP, options) { - const https = await import("https"); - const net = await import("net"); - const { Readable } = await import("stream"); + const httpsModule = await import("https"); + const netModule = await import("net"); + // CJS modules expose exports via .default in ESM dynamic import context + const https = httpsModule.default ?? httpsModule; + const net = netModule.default ?? netModule; return new Promise((resolve, reject) => { const socket = new net.Socket(); diff --git a/package.json b/package.json index 15c8975..1aebeb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.52", + "version": "0.3.53", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/src/app/api/usage/[connectionId]/route.js b/src/app/api/usage/[connectionId]/route.js index adf6138..dd749f3 100644 --- a/src/app/api/usage/[connectionId]/route.js +++ b/src/app/api/usage/[connectionId]/route.js @@ -4,11 +4,21 @@ import "open-sse/index.js"; import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb"; import { getUsageForProvider } from "open-sse/services/usage.js"; import { getExecutor } from "open-sse/executors/index.js"; + +// Detect auth-expired messages returned by usage providers instead of throwing +const AUTH_EXPIRED_PATTERNS = ["expired", "authentication", "unauthorized", "401", "re-authorize"]; +function isAuthExpiredMessage(usage) { + if (!usage?.message) return false; + const msg = usage.message.toLowerCase(); + return AUTH_EXPIRED_PATTERNS.some((p) => msg.includes(p)); +} + /** * Refresh credentials using executor and update database + * @param {boolean} force - Skip needsRefresh check and always attempt refresh * @returns Promise<{ connection, refreshed: boolean }> */ -async function refreshAndUpdateCredentials(connection) { +async function refreshAndUpdateCredentials(connection, force = false) { const executor = getExecutor(connection.provider); // Build credentials object from connection @@ -22,8 +32,8 @@ async function refreshAndUpdateCredentials(connection) { copilotTokenExpiresAt: connection.providerSpecificData?.copilotTokenExpiresAt, }; - // Check if refresh is needed - const needsRefresh = executor.needsRefresh(credentials); + // Check if refresh is needed (skip when force=true) + const needsRefresh = force || executor.needsRefresh(credentials); if (!needsRefresh) { return { connection, refreshed: false }; @@ -95,6 +105,7 @@ export async function GET(request, { params }) { try { const { connectionId } = await params; + // Get connection from database connection = await getProviderConnectionById(connectionId); if (!connection) { @@ -118,10 +129,24 @@ export async function GET(request, { params }) { } // Fetch usage from provider API - const usage = await getUsageForProvider(connection); + let usage = await getUsageForProvider(connection); + + // If provider returned an auth-expired message instead of throwing, + // force-refresh token and retry once + if (isAuthExpiredMessage(usage) && connection.refreshToken) { + try { + const retryResult = await refreshAndUpdateCredentials(connection, true); + connection = retryResult.connection; + usage = await getUsageForProvider(connection); + } catch (retryError) { + console.warn(`[Usage] ${connection.provider}: force refresh failed: ${retryError.message}`); + } + } + return Response.json(usage); } catch (error) { - console.warn(`[Usage] ${connection?.provider}: ${error.message}`); + const provider = connection?.provider ?? "unknown"; + console.warn(`[Usage] ${provider}: ${error.message}`); return Response.json({ error: error.message }, { status: 500 }); } }