bloby-bot 0.30.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "releaseNotes": [
5
5
  "1. # voice note (PTT bubble)",
6
6
  "2. # audio file + caption",
@@ -44,7 +44,7 @@ const REFRESH_LEEWAY_MS = 5 * 60 * 1000;
44
44
 
45
45
  let codeVerifier: string | null = null;
46
46
  let oauthState: string | null = null;
47
- let callbackServer: http.Server | null = null;
47
+ let callbackServers: http.Server[] = [];
48
48
 
49
49
  interface AuthDotJson {
50
50
  OPENAI_API_KEY: string | null;
@@ -236,7 +236,7 @@ export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string;
236
236
  const codeChallenge = crypto.createHash('sha256').update(codeVerifier).digest('base64url');
237
237
  oauthState = crypto.randomUUID();
238
238
 
239
- callbackServer = http.createServer((req, res) => {
239
+ const handleCallback = (req: http.IncomingMessage, res: http.ServerResponse) => {
240
240
  if (!req.url?.startsWith('/auth/callback')) {
241
241
  res.writeHead(404);
242
242
  res.end();
@@ -247,6 +247,7 @@ export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string;
247
247
  const code = url.searchParams.get('code');
248
248
  const returnedState = url.searchParams.get('state');
249
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'})`);
250
251
 
251
252
  res.writeHead(200, { 'Content-Type': 'text/html' });
252
253
  res.end(`<html><body style="font-family:sans-serif;text-align:center;padding:60px;background:#0a0a0a;color:#fff">
@@ -258,11 +259,24 @@ export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string;
258
259
 
259
260
  if (error || !code || returnedState !== oauthState) return;
260
261
  exchangeCode(code);
261
- });
262
-
263
- callbackServer.listen(OAUTH_CONFIG.PORT, '127.0.0.1', () => {
264
- log.ok(`Codex OAuth callback server on port ${OAUTH_CONFIG.PORT}`);
262
+ };
265
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
+ ];
271
+
272
+ let resolved = false;
273
+ let pendingBinds = bindHosts.length;
274
+ let bindFailures = 0;
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'})`);
266
280
  const params = new URLSearchParams({
267
281
  response_type: 'code',
268
282
  client_id: OAUTH_CONFIG.CLIENT_ID,
@@ -274,16 +288,41 @@ export function startCodexOAuth(): Promise<{ success: boolean; authUrl?: string;
274
288
  id_token_add_organizations: 'true',
275
289
  codex_cli_simplified_flow: 'true',
276
290
  });
277
-
278
291
  resolve({ success: true, authUrl: `${OAUTH_CONFIG.AUTHORIZE_URL}?${params.toString()}` });
279
- });
292
+ };
280
293
 
281
- callbackServer.on('error', (err: any) => {
282
- const error = err.code === 'EADDRINUSE'
294
+ const finishWithError = (err: any) => {
295
+ if (resolved) return;
296
+ resolved = true;
297
+ stopCallbackServer();
298
+ const error = err?.code === 'EADDRINUSE'
283
299
  ? `Port ${OAUTH_CONFIG.PORT} is busy. Close other Codex instances.`
284
- : err.message;
300
+ : (err?.message || String(err));
285
301
  resolve({ success: false, error });
286
- });
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
+ }
287
326
  });
288
327
  }
289
328
 
@@ -356,8 +395,8 @@ export function readCodexAccessToken(): string | null {
356
395
  /* ── Helpers ── */
357
396
 
358
397
  function stopCallbackServer(): void {
359
- if (callbackServer) {
360
- try { callbackServer.close(); } catch {}
361
- callbackServer = null;
398
+ for (const server of callbackServers) {
399
+ try { server.close(); } catch {}
362
400
  }
401
+ callbackServers = [];
363
402
  }