bloby-bot 0.29.2 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/tempo-balance.ts +10 -10
- package/worker/codex-auth.ts +261 -58
- package/worker/index.ts +7 -7
- package/workspace/client/src/App.tsx +2 -2
package/package.json
CHANGED
package/scripts/tempo-balance.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
// Read on-chain USDC balances for one or more Tempo addresses.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// cp scripts/tempo-balance.ts /tmp/tempo-transfer/ && \
|
|
5
|
-
// (cd /tmp/tempo-transfer && ./node_modules/.bin/tsx tempo-balance.ts)
|
|
6
|
-
//
|
|
7
|
-
// (The script must be executed from inside the isolated /tmp/tempo-transfer/
|
|
8
|
-
// install — the repo's own node_modules has a @noble/hashes version conflict
|
|
9
|
-
// that breaks viem's ESM import resolution. To rebuild the isolated dir if
|
|
10
|
-
// missing: mkdir -p /tmp/tempo-transfer && cd /tmp/tempo-transfer \
|
|
11
|
-
// && npm init -y && npm install viem@2.47.6 tsx)
|
|
3
|
+
// RUN (one-liner, from repo root): cp scripts/tempo-balance.ts /tmp/tempo-transfer/ && (cd /tmp/tempo-transfer && ./node_modules/.bin/tsx tempo-balance.ts)
|
|
12
4
|
//
|
|
5
|
+
// Edit the ACCOUNTS list in the CONFIG block below to add/remove wallets.
|
|
13
6
|
// Each entry prints: USDC balance, nonce (0 = wallet has never sent a tx),
|
|
14
7
|
// and whether the address is an EOA or a contract.
|
|
8
|
+
//
|
|
9
|
+
// Why the copy-into-/tmp dance: the repo's own node_modules has a
|
|
10
|
+
// @noble/hashes version conflict that breaks viem's ESM import resolution,
|
|
11
|
+
// so the script must run from the isolated /tmp/tempo-transfer/ install.
|
|
12
|
+
// To rebuild that dir if it's missing:
|
|
13
|
+
// mkdir -p /tmp/tempo-transfer && cd /tmp/tempo-transfer && npm init -y && npm install viem@2.47.6 tsx
|
|
15
14
|
|
|
16
15
|
// ─── CONFIG ──────────────────────────────────────────────────────────────────
|
|
17
16
|
|
|
18
17
|
const ACCOUNTS = [
|
|
19
|
-
{ label: '
|
|
18
|
+
{ label: 'Bloby', wallet: '0xc65d8a0dB80f637337B87e42b8BEb5D86D46f942' },
|
|
20
19
|
{ label: 'Treasury', wallet: '0x084A80e395FaE8Aaf58F591f47F63DA1EeF06bbC' },
|
|
20
|
+
{ label: 'Serge', wallet: '0x21AC79F74da90c9F441699Ee6215dA32c0CE9F5a' },
|
|
21
21
|
];
|
|
22
22
|
|
|
23
23
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/worker/codex-auth.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Codex OAuth PKCE flow for ChatGPT Plus/Pro subscription authentication.
|
|
3
|
-
* Adapted from CodeDeck's CodexOAuthService for server-side Node.js.
|
|
4
3
|
*
|
|
5
4
|
* Spins up a temporary HTTP server on port 1455 to capture the OAuth callback.
|
|
6
|
-
* Credentials are stored in ~/.codex/
|
|
5
|
+
* Credentials are stored in ~/.codex/auth.json in the same shape Codex CLI
|
|
6
|
+
* itself writes, so a spawned `codex app-server` process can use them directly.
|
|
7
|
+
*
|
|
8
|
+
* Shape on disk (chatgpt mode):
|
|
9
|
+
* {
|
|
10
|
+
* "OPENAI_API_KEY": null,
|
|
11
|
+
* "auth_mode": "chatgpt",
|
|
12
|
+
* "tokens": {
|
|
13
|
+
* "id_token": "<raw jwt>",
|
|
14
|
+
* "access_token": "<jwt>",
|
|
15
|
+
* "refresh_token": "...",
|
|
16
|
+
* "account_id": "<chatgpt_account_id from id_token claims>"
|
|
17
|
+
* },
|
|
18
|
+
* "last_refresh": "2026-05-03T12:34:56.789Z"
|
|
19
|
+
* }
|
|
7
20
|
*/
|
|
8
21
|
|
|
9
22
|
import crypto from 'crypto';
|
|
@@ -23,50 +36,124 @@ const OAUTH_CONFIG = {
|
|
|
23
36
|
};
|
|
24
37
|
|
|
25
38
|
const AUTH_DIR = path.join(os.homedir(), '.codex');
|
|
26
|
-
const AUTH_FILE = path.join(AUTH_DIR, '
|
|
39
|
+
const AUTH_FILE = path.join(AUTH_DIR, 'auth.json');
|
|
40
|
+
const LEGACY_AUTH_FILE = path.join(AUTH_DIR, 'codedeck-auth.json');
|
|
41
|
+
|
|
42
|
+
/** Refresh access tokens this many ms before they actually expire. */
|
|
43
|
+
const REFRESH_LEEWAY_MS = 5 * 60 * 1000;
|
|
27
44
|
|
|
28
45
|
let codeVerifier: string | null = null;
|
|
29
46
|
let oauthState: string | null = null;
|
|
30
|
-
let
|
|
47
|
+
let callbackServers: http.Server[] = [];
|
|
31
48
|
|
|
32
|
-
|
|
49
|
+
interface AuthDotJson {
|
|
50
|
+
OPENAI_API_KEY: string | null;
|
|
51
|
+
auth_mode?: 'apikey' | 'chatgpt' | 'chatgptAuthTokens' | 'agentIdentity';
|
|
52
|
+
tokens?: {
|
|
53
|
+
id_token: string;
|
|
54
|
+
access_token: string;
|
|
55
|
+
refresh_token: string;
|
|
56
|
+
account_id?: string | null;
|
|
57
|
+
};
|
|
58
|
+
last_refresh?: string;
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
}
|
|
33
61
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
/* ── File I/O ── */
|
|
63
|
+
|
|
64
|
+
function readAuthFile(): AuthDotJson | null {
|
|
65
|
+
try {
|
|
66
|
+
if (!fs.existsSync(AUTH_FILE)) return null;
|
|
67
|
+
return JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8'));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
38
70
|
}
|
|
39
71
|
}
|
|
40
72
|
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
function writeAuthFile(auth: AuthDotJson): void {
|
|
74
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
75
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2), 'utf-8');
|
|
76
|
+
try { fs.chmodSync(AUTH_FILE, 0o600); } catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Decode a JWT and return its parsed payload, or null on failure. */
|
|
80
|
+
function decodeJwt(token: string): Record<string, any> | null {
|
|
81
|
+
try {
|
|
82
|
+
const parts = token.split('.');
|
|
83
|
+
if (parts.length < 2) return null;
|
|
84
|
+
return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8'));
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
44
87
|
}
|
|
88
|
+
}
|
|
45
89
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
90
|
+
/** Read the JWT `exp` claim as a Unix epoch (seconds). Null if missing/invalid. */
|
|
91
|
+
function jwtExpiryMs(token: string): number | null {
|
|
92
|
+
const payload = decodeJwt(token);
|
|
93
|
+
if (!payload || typeof payload.exp !== 'number') return null;
|
|
94
|
+
return payload.exp * 1000;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Pull `chatgpt_account_id` out of the id_token JWT claims, if present. */
|
|
98
|
+
function extractAccountId(idToken: string): string | undefined {
|
|
99
|
+
const payload = decodeJwt(idToken);
|
|
100
|
+
const authClaims = payload?.['https://api.openai.com/auth'] || {};
|
|
101
|
+
return authClaims.chatgpt_account_id || undefined;
|
|
102
|
+
}
|
|
50
103
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
104
|
+
/** One-shot migration from the old `codedeck-auth.json` layout (pre-codex-native). */
|
|
105
|
+
function migrateLegacyFile(): void {
|
|
106
|
+
if (fs.existsSync(AUTH_FILE)) {
|
|
107
|
+
// If auth.json already has chatgpt tokens, nothing to do.
|
|
108
|
+
const existing = readAuthFile();
|
|
109
|
+
if (existing?.tokens?.refresh_token) return;
|
|
55
110
|
}
|
|
111
|
+
if (!fs.existsSync(LEGACY_AUTH_FILE)) return;
|
|
56
112
|
|
|
57
|
-
// Decode JWT claims for account info
|
|
58
113
|
try {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
114
|
+
const legacy = JSON.parse(fs.readFileSync(LEGACY_AUTH_FILE, 'utf-8'));
|
|
115
|
+
if (!legacy.access_token || !legacy.refresh_token) return;
|
|
116
|
+
|
|
117
|
+
const existing = readAuthFile() || { OPENAI_API_KEY: null };
|
|
118
|
+
const next: AuthDotJson = {
|
|
119
|
+
...existing,
|
|
120
|
+
auth_mode: 'chatgpt',
|
|
121
|
+
tokens: {
|
|
122
|
+
id_token: legacy.id_token || '',
|
|
123
|
+
access_token: legacy.access_token,
|
|
124
|
+
refresh_token: legacy.refresh_token,
|
|
125
|
+
account_id: legacy.chatgpt_account_id || extractAccountId(legacy.access_token),
|
|
126
|
+
},
|
|
127
|
+
last_refresh: new Date().toISOString(),
|
|
128
|
+
};
|
|
129
|
+
writeAuthFile(next);
|
|
130
|
+
try { fs.unlinkSync(LEGACY_AUTH_FILE); } catch {}
|
|
131
|
+
log.ok('Codex: migrated legacy codedeck-auth.json → auth.json');
|
|
132
|
+
} catch (err: any) {
|
|
133
|
+
log.warn(`Codex: legacy migration failed — ${err.message}`);
|
|
65
134
|
}
|
|
135
|
+
}
|
|
66
136
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
137
|
+
/* ── Token exchange & refresh ── */
|
|
138
|
+
|
|
139
|
+
function storeTokens(tokens: { access_token: string; refresh_token?: string; id_token?: string }): void {
|
|
140
|
+
const existing = readAuthFile() || { OPENAI_API_KEY: null };
|
|
141
|
+
const prev = existing.tokens;
|
|
142
|
+
const idToken = tokens.id_token ?? prev?.id_token ?? '';
|
|
143
|
+
|
|
144
|
+
const next: AuthDotJson = {
|
|
145
|
+
...existing,
|
|
146
|
+
OPENAI_API_KEY: existing.OPENAI_API_KEY ?? null,
|
|
147
|
+
auth_mode: 'chatgpt',
|
|
148
|
+
tokens: {
|
|
149
|
+
id_token: idToken,
|
|
150
|
+
access_token: tokens.access_token,
|
|
151
|
+
refresh_token: tokens.refresh_token ?? prev?.refresh_token ?? '',
|
|
152
|
+
account_id: idToken ? extractAccountId(idToken) ?? prev?.account_id : prev?.account_id,
|
|
153
|
+
},
|
|
154
|
+
last_refresh: new Date().toISOString(),
|
|
155
|
+
};
|
|
156
|
+
writeAuthFile(next);
|
|
70
157
|
}
|
|
71
158
|
|
|
72
159
|
async function exchangeCode(code: string): Promise<{ success: boolean; error?: string }> {
|
|
@@ -94,28 +181,62 @@ async function exchangeCode(code: string): Promise<{ success: boolean; error?: s
|
|
|
94
181
|
}
|
|
95
182
|
|
|
96
183
|
const tokens = await response.json();
|
|
97
|
-
|
|
184
|
+
if (!tokens.access_token) {
|
|
185
|
+
return { success: false, error: 'OAuth response missing access_token.' };
|
|
186
|
+
}
|
|
187
|
+
storeTokens(tokens);
|
|
98
188
|
codeVerifier = null;
|
|
99
189
|
oauthState = null;
|
|
190
|
+
log.ok('Codex credentials stored');
|
|
191
|
+
// Clean up the legacy file on first successful new auth.
|
|
192
|
+
try { fs.unlinkSync(LEGACY_AUTH_FILE); } catch {}
|
|
100
193
|
return { success: true };
|
|
101
194
|
} catch (err: any) {
|
|
102
195
|
return { success: false, error: err.message };
|
|
103
196
|
}
|
|
104
197
|
}
|
|
105
198
|
|
|
199
|
+
async function refreshTokens(refreshToken: string): Promise<boolean> {
|
|
200
|
+
try {
|
|
201
|
+
log.ok('Codex: refreshing access token...');
|
|
202
|
+
const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { 'Content-Type': 'application/json' },
|
|
205
|
+
body: JSON.stringify({
|
|
206
|
+
client_id: OAUTH_CONFIG.CLIENT_ID,
|
|
207
|
+
grant_type: 'refresh_token',
|
|
208
|
+
refresh_token: refreshToken,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
log.warn(`Codex: refresh failed (${response.status})`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
const tokens = await response.json();
|
|
216
|
+
if (!tokens.access_token) {
|
|
217
|
+
log.warn('Codex: refresh response missing access_token');
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
storeTokens(tokens);
|
|
221
|
+
log.ok('Codex: token refreshed');
|
|
222
|
+
return true;
|
|
223
|
+
} catch (err: any) {
|
|
224
|
+
log.warn(`Codex: refresh error — ${err.message}`);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
106
229
|
/* ── Public API ── */
|
|
107
230
|
|
|
108
231
|
export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string; error?: string }> {
|
|
109
232
|
return new Promise((resolve) => {
|
|
110
233
|
stopCallbackServer();
|
|
111
234
|
|
|
112
|
-
// Generate PKCE
|
|
113
235
|
codeVerifier = crypto.randomBytes(32).toString('base64url');
|
|
114
236
|
const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
|
|
115
237
|
oauthState = crypto.randomUUID();
|
|
116
238
|
|
|
117
|
-
|
|
118
|
-
callbackServer = http.createServer((req, res) => {
|
|
239
|
+
const handleCallback = (req: http.IncomingMessage, res: http.ServerResponse) => {
|
|
119
240
|
if (!req.url?.startsWith('/auth/callback')) {
|
|
120
241
|
res.writeHead(404);
|
|
121
242
|
res.end();
|
|
@@ -126,6 +247,7 @@ export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string;
|
|
|
126
247
|
const code = url.searchParams.get('code');
|
|
127
248
|
const returnedState = url.searchParams.get('state');
|
|
128
249
|
const error = url.searchParams.get('error');
|
|
250
|
+
log.ok(`Codex OAuth callback hit (code=${code ? 'yes' : 'no'}, state=${returnedState === oauthState ? 'ok' : 'mismatch'}, error=${error || 'none'})`);
|
|
129
251
|
|
|
130
252
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
131
253
|
res.end(`<html><body style="font-family:sans-serif;text-align:center;padding:60px;background:#0a0a0a;color:#fff">
|
|
@@ -137,11 +259,24 @@ export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string;
|
|
|
137
259
|
|
|
138
260
|
if (error || !code || returnedState !== oauthState) return;
|
|
139
261
|
exchangeCode(code);
|
|
140
|
-
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Bind BOTH IPv4 and IPv6 loopback. On dual-stack systems where
|
|
265
|
+
// `localhost` resolves to ::1 first, an IPv4-only bind silently fails
|
|
266
|
+
// when the browser hits the callback URL.
|
|
267
|
+
const bindHosts: Array<{ host: string; required: boolean }> = [
|
|
268
|
+
{ host: '127.0.0.1', required: true },
|
|
269
|
+
{ host: '::1', required: false }, // tolerate machines without IPv6
|
|
270
|
+
];
|
|
141
271
|
|
|
142
|
-
|
|
143
|
-
|
|
272
|
+
let resolved = false;
|
|
273
|
+
let pendingBinds = bindHosts.length;
|
|
274
|
+
let bindFailures = 0;
|
|
144
275
|
|
|
276
|
+
const finishWithSuccess = () => {
|
|
277
|
+
if (resolved) return;
|
|
278
|
+
resolved = true;
|
|
279
|
+
log.ok(`Codex OAuth callback servers listening on port ${OAUTH_CONFIG.PORT} (${callbackServers.length} bind${callbackServers.length === 1 ? '' : 's'})`);
|
|
145
280
|
const params = new URLSearchParams({
|
|
146
281
|
response_type: 'code',
|
|
147
282
|
client_id: OAUTH_CONFIG.CLIENT_ID,
|
|
@@ -153,16 +288,41 @@ export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string;
|
|
|
153
288
|
id_token_add_organizations: 'true',
|
|
154
289
|
codex_cli_simplified_flow: 'true',
|
|
155
290
|
});
|
|
156
|
-
|
|
157
291
|
resolve({ success: true, authUrl: `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}` });
|
|
158
|
-
}
|
|
292
|
+
};
|
|
159
293
|
|
|
160
|
-
|
|
161
|
-
|
|
294
|
+
const finishWithError = (err: any) => {
|
|
295
|
+
if (resolved) return;
|
|
296
|
+
resolved = true;
|
|
297
|
+
stopCallbackServer();
|
|
298
|
+
const error = err?.code === 'EADDRINUSE'
|
|
162
299
|
? `Port ${OAUTH_CONFIG.PORT} is busy. Close other Codex instances.`
|
|
163
|
-
: err
|
|
300
|
+
: (err?.message || String(err));
|
|
164
301
|
resolve({ success: false, error });
|
|
165
|
-
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
for (const { host, required } of bindHosts) {
|
|
305
|
+
const server = http.createServer(handleCallback);
|
|
306
|
+
|
|
307
|
+
server.once('error', (err: any) => {
|
|
308
|
+
log.warn(`Codex OAuth bind ${host}:${OAUTH_CONFIG.PORT} failed — ${err.code || err.message}`);
|
|
309
|
+
if (required) {
|
|
310
|
+
finishWithError(err);
|
|
311
|
+
} else {
|
|
312
|
+
bindFailures++;
|
|
313
|
+
pendingBinds--;
|
|
314
|
+
// If only the optional bind failed but we already have at least one
|
|
315
|
+
// server up, that's still a success.
|
|
316
|
+
if (pendingBinds === 0 && callbackServers.length > 0) finishWithSuccess();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
server.listen(OAUTH_CONFIG.PORT, host, () => {
|
|
321
|
+
callbackServers.push(server);
|
|
322
|
+
pendingBinds--;
|
|
323
|
+
if (pendingBinds === 0) finishWithSuccess();
|
|
324
|
+
});
|
|
325
|
+
}
|
|
166
326
|
});
|
|
167
327
|
}
|
|
168
328
|
|
|
@@ -172,28 +332,71 @@ export function cancelCodexOAuth(): void {
|
|
|
172
332
|
oauthState = null;
|
|
173
333
|
}
|
|
174
334
|
|
|
175
|
-
export function getCodexAuthStatus(): {
|
|
335
|
+
export async function getCodexAuthStatus(): Promise<{
|
|
336
|
+
authenticated: boolean;
|
|
337
|
+
plan?: string;
|
|
338
|
+
error?: string;
|
|
339
|
+
}> {
|
|
176
340
|
try {
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
341
|
+
migrateLegacyFile();
|
|
342
|
+
const auth = readAuthFile();
|
|
343
|
+
const tokens = auth?.tokens;
|
|
344
|
+
if (!tokens?.access_token) return { authenticated: false };
|
|
345
|
+
|
|
346
|
+
const expMs = jwtExpiryMs(tokens.access_token);
|
|
347
|
+
const valid = expMs ? Date.now() + REFRESH_LEEWAY_MS < expMs : true;
|
|
348
|
+
|
|
349
|
+
if (!valid && tokens.refresh_token) {
|
|
350
|
+
const ok = await refreshTokens(tokens.refresh_token);
|
|
351
|
+
if (!ok) return { authenticated: false, error: 'Token expired and refresh failed' };
|
|
182
352
|
}
|
|
183
|
-
|
|
353
|
+
|
|
354
|
+
const fresh = readAuthFile();
|
|
355
|
+
const idClaims = fresh?.tokens?.id_token ? decodeJwt(fresh.tokens.id_token) : null;
|
|
356
|
+
const plan = idClaims?.['https://api.openai.com/auth']?.chatgpt_plan_type || 'plus';
|
|
357
|
+
return { authenticated: true, plan };
|
|
184
358
|
} catch (err: any) {
|
|
185
359
|
return { authenticated: false, error: err.message };
|
|
186
360
|
}
|
|
187
361
|
}
|
|
188
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Read a valid Codex access token, refreshing if expired. Returns null if unavailable.
|
|
365
|
+
* This is what the Codex harness should call before each app-server invocation.
|
|
366
|
+
*/
|
|
367
|
+
export async function getCodexAccessToken(): Promise<string | null> {
|
|
368
|
+
migrateLegacyFile();
|
|
369
|
+
const auth = readAuthFile();
|
|
370
|
+
const tokens = auth?.tokens;
|
|
371
|
+
if (!tokens?.access_token) return null;
|
|
372
|
+
|
|
373
|
+
const expMs = jwtExpiryMs(tokens.access_token);
|
|
374
|
+
if (!expMs || Date.now() + REFRESH_LEEWAY_MS < expMs) {
|
|
375
|
+
return tokens.access_token;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!tokens.refresh_token) return null;
|
|
379
|
+
const ok = await refreshTokens(tokens.refresh_token);
|
|
380
|
+
if (!ok) return null;
|
|
381
|
+
|
|
382
|
+
return readAuthFile()?.tokens?.access_token ?? null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Synchronous accessor — returns the stored access token without refreshing. */
|
|
189
386
|
export function readCodexAccessToken(): string | null {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
387
|
+
const auth = readAuthFile();
|
|
388
|
+
const token = auth?.tokens?.access_token;
|
|
389
|
+
if (!token) return null;
|
|
390
|
+
const expMs = jwtExpiryMs(token);
|
|
391
|
+
if (expMs && Date.now() >= expMs) return null;
|
|
392
|
+
return token;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* ── Helpers ── */
|
|
396
|
+
|
|
397
|
+
function stopCallbackServer(): void {
|
|
398
|
+
for (const server of callbackServers) {
|
|
399
|
+
try { server.close(); } catch {}
|
|
198
400
|
}
|
|
401
|
+
callbackServers = [];
|
|
199
402
|
}
|
package/worker/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { initDb, closeDb, listConversations, createConversation, deleteConversat
|
|
|
9
9
|
import webpush from 'web-push';
|
|
10
10
|
import { TOTP } from 'otpauth';
|
|
11
11
|
import QRCode from 'qrcode';
|
|
12
|
-
import { startCodexOAuth, cancelCodexOAuth, getCodexAuthStatus
|
|
12
|
+
import { startCodexOAuth, cancelCodexOAuth, getCodexAuthStatus } from './codex-auth.js';
|
|
13
13
|
import { startClaudeOAuth, exchangeClaudeCode, getClaudeAuthStatus, readClaudeAccessToken } from './claude-auth.js';
|
|
14
14
|
import { checkAvailability, registerHandle, claimReservedHandle, releaseHandle, updateTunnelUrl, startHeartbeat, stopHeartbeat } from '../shared/relay.js';
|
|
15
15
|
import { ensureFileDirs } from '../supervisor/file-saver.js';
|
|
@@ -181,8 +181,8 @@ app.post('/api/auth/codex/cancel', (_req, res) => {
|
|
|
181
181
|
res.json({ ok: true });
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
app.get('/api/auth/codex/status', (_req, res) => {
|
|
185
|
-
res.json(getCodexAuthStatus());
|
|
184
|
+
app.get('/api/auth/codex/status', async (_req, res) => {
|
|
185
|
+
res.json(await getCodexAuthStatus());
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
// ── Claude OAuth routes ──
|
|
@@ -729,10 +729,10 @@ app.post('/api/onboard', (req, res) => {
|
|
|
729
729
|
currentCfg.ai.model = model || '';
|
|
730
730
|
currentCfg.ai.baseUrl = baseUrl || undefined;
|
|
731
731
|
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
732
|
+
// OAuth providers store their tokens in their own credentials files
|
|
733
|
+
// (~/.codex/auth.json, ~/.claude/.credentials.json) and refresh them as
|
|
734
|
+
// needed — config.ai.apiKey only holds raw API keys.
|
|
735
|
+
if (!apiKey && provider === 'anthropic') {
|
|
736
736
|
currentCfg.ai.apiKey = readClaudeAccessToken() || '';
|
|
737
737
|
} else {
|
|
738
738
|
currentCfg.ai.apiKey = apiKey || '';
|
|
@@ -16,9 +16,9 @@ function DashboardError() {
|
|
|
16
16
|
playsInline
|
|
17
17
|
style={{ height: 120, width: 120, borderRadius: '50%', objectFit: 'cover', marginBottom: 32 }}
|
|
18
18
|
/>
|
|
19
|
-
<h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>
|
|
19
|
+
<h1 style={{ fontSize: 20, fontWeight: 600, marginBottom: 8 }}>Oopss.. Something wrong is not right</h1>
|
|
20
20
|
<p style={{ fontSize: 14, color: 'rgba(255,255,255,0.5)', maxWidth: 320, lineHeight: 1.5 }}>
|
|
21
|
-
|
|
21
|
+
If your agent is working, this is normal. If not, go poke them
|
|
22
22
|
</p>
|
|
23
23
|
</div>
|
|
24
24
|
);
|