@@ -5,14 +5,26 @@ import os from "node:os";
55import path from "node:path" ;
66import { pathToFileURL } from "node:url" ;
77import { normalizeOptionalString } from "../src/shared/string-coerce.ts" ;
8- import { maskIdentifier , previewForDevToolLog , redactHomePath } from "./lib/dev-tooling-safety.ts" ;
8+ import {
9+ maskIdentifier ,
10+ parseStrictIntegerOption ,
11+ previewForDevToolLog ,
12+ redactHomePath ,
13+ } from "./lib/dev-tooling-safety.ts" ;
914
1015type Args = {
1116 agentId : string ;
1217 reveal : boolean ;
1318 sessionKey ?: string ;
1419} ;
1520
21+ type FetchOptions = {
22+ fetchImpl ?: typeof fetch ;
23+ timeoutMs ?: number ;
24+ } ;
25+
26+ const DEFAULT_FETCH_TIMEOUT_MS = 30_000 ;
27+
1628const mask = ( value : string ) => {
1729 return maskIdentifier (
1830 value ,
@@ -80,17 +92,68 @@ const pickAnthropicTokens = (store: {
8092 return found ;
8193} ;
8294
83- const fetchAnthropicOAuthUsage = async ( token : string ) => {
84- const res = await fetch ( "https://api.anthropic.com/api/oauth/usage" , {
85- headers : {
86- Authorization : `Bearer ${ token } ` ,
87- Accept : "application/json" ,
88- "anthropic-version" : "2023-06-01" ,
89- "anthropic-beta" : "oauth-2025-04-20" ,
90- "User-Agent" : "openclaw-debug" ,
91- } ,
95+ const resolveFetchTimeoutMs = ( raw = process . env . OPENCLAW_DEBUG_CLAUDE_USAGE_FETCH_TIMEOUT_MS ) => {
96+ return parseStrictIntegerOption ( {
97+ fallback : DEFAULT_FETCH_TIMEOUT_MS ,
98+ label : "OPENCLAW_DEBUG_CLAUDE_USAGE_FETCH_TIMEOUT_MS" ,
99+ min : 1 ,
100+ raw,
101+ } ) ;
102+ } ;
103+
104+ const withFetchTimeout = async < T > (
105+ label : string ,
106+ timeoutMs : number ,
107+ run : ( signal : AbortSignal ) => Promise < T > ,
108+ ) : Promise < T > => {
109+ const controller = new AbortController ( ) ;
110+ let timeout : ReturnType < typeof setTimeout > | undefined ;
111+ const timeoutPromise = new Promise < T > ( ( _resolve , reject ) => {
112+ timeout = setTimeout ( ( ) => {
113+ const error = new Error ( `${ label } exceeded timeout of ${ timeoutMs } ms` ) ;
114+ reject ( error ) ;
115+ controller . abort ( error ) ;
116+ } , timeoutMs ) ;
92117 } ) ;
93- const text = await res . text ( ) ;
118+ try {
119+ return await Promise . race ( [ run ( controller . signal ) , timeoutPromise ] ) ;
120+ } finally {
121+ if ( timeout ) {
122+ clearTimeout ( timeout ) ;
123+ }
124+ }
125+ } ;
126+
127+ const fetchText = async (
128+ label : string ,
129+ url : string ,
130+ init : RequestInit ,
131+ options : FetchOptions = { } ,
132+ ) => {
133+ const fetchImpl = options . fetchImpl ?? fetch ;
134+ const timeoutMs = options . timeoutMs ?? resolveFetchTimeoutMs ( ) ;
135+ return await withFetchTimeout ( label , timeoutMs , async ( signal ) => {
136+ const res = await fetchImpl ( url , { ...init , signal } ) ;
137+ const text = await res . text ( ) ;
138+ return { res, text } ;
139+ } ) ;
140+ } ;
141+
142+ const fetchAnthropicOAuthUsage = async ( token : string , options : FetchOptions = { } ) => {
143+ const { res, text } = await fetchText (
144+ "Anthropic OAuth usage request" ,
145+ "https://api.anthropic.com/api/oauth/usage" ,
146+ {
147+ headers : {
148+ Authorization : `Bearer ${ token } ` ,
149+ Accept : "application/json" ,
150+ "anthropic-version" : "2023-06-01" ,
151+ "anthropic-beta" : "oauth-2025-04-20" ,
152+ "User-Agent" : "openclaw-debug" ,
153+ } ,
154+ } ,
155+ options ,
156+ ) ;
94157 return { status : res . status , contentType : res . headers . get ( "content-type" ) , text } ;
95158} ;
96159
@@ -303,15 +366,19 @@ const findClaudeSessionKey = (): { sessionKey: string; source: string } | null =
303366 return null ;
304367} ;
305368
306- const fetchClaudeWebUsage = async ( sessionKey : string ) => {
369+ const fetchClaudeWebUsage = async ( sessionKey : string , options : FetchOptions = { } ) => {
307370 const headers = {
308371 Cookie : `sessionKey=${ sessionKey } ` ,
309372 Accept : "application/json" ,
310373 "User-Agent" :
311374 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15" ,
312375 } ;
313- const orgRes = await fetch ( "https://claude.ai/api/organizations" , { headers } ) ;
314- const orgText = await orgRes . text ( ) ;
376+ const { res : orgRes , text : orgText } = await fetchText (
377+ "Claude organizations request" ,
378+ "https://claude.ai/api/organizations" ,
379+ { headers } ,
380+ options ,
381+ ) ;
315382 if ( ! orgRes . ok ) {
316383 return { ok : false as const , step : "organizations" , status : orgRes . status , body : orgText } ;
317384 }
@@ -321,8 +388,12 @@ const fetchClaudeWebUsage = async (sessionKey: string) => {
321388 return { ok : false as const , step : "organizations" , status : 200 , body : orgText } ;
322389 }
323390
324- const usageRes = await fetch ( `https://claude.ai/api/organizations/${ orgId } /usage` , { headers } ) ;
325- const usageText = await usageRes . text ( ) ;
391+ const { res : usageRes , text : usageText } = await fetchText (
392+ "Claude usage request" ,
393+ `https://claude.ai/api/organizations/${ orgId } /usage` ,
394+ { headers } ,
395+ options ,
396+ ) ;
326397 return usageRes . ok
327398 ? { ok : true as const , orgId, body : usageText }
328399 : { ok : false as const , step : "usage" , status : usageRes . status , body : usageText } ;
@@ -397,7 +468,9 @@ export const testing = {
397468 CLAUDE_COOKIE_HOST_SQL ,
398469 CLAUDE_FIREFOX_COOKIE_HOST_SQL ,
399470 browserRootLabel,
471+ fetchAnthropicOAuthUsage,
400472 mask,
473+ resolveFetchTimeoutMs,
401474} ;
402475
403476if ( import . meta. url === pathToFileURL ( process . argv [ 1 ] ?? "" ) . href ) {
0 commit comments