nothumanallowed 13.5.163 → 13.5.165

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": "nothumanallowed",
3
- "version": "13.5.163",
3
+ "version": "13.5.165",
4
4
  "description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,6 +60,9 @@ import {
60
60
 
61
61
  const DEFAULT_PORT = 3847;
62
62
 
63
+ // In-memory pending OAuth state (verifier + state for PKCE callback)
64
+ let _googleOAuthPending = null;
65
+
63
66
  /**
64
67
  * Extract text from PDF buffer — zero dependencies.
65
68
  * Handles text-based PDFs (not scanned images).
@@ -361,15 +364,18 @@ export async function cmdUI(args) {
361
364
  return;
362
365
  }
363
366
 
364
- // POST /api/google/auth — trigger Google OAuth flow from web UI
367
+ // POST /api/google/auth — return OAuth URL for the browser to open
365
368
  if (method === 'POST' && pathname === '/api/google/auth') {
366
369
  try {
367
- const { runAuthFlow } = await import('../services/google-oauth.mjs');
368
- // Run auth flow in background opens browser
369
- runAuthFlow(config).then(success => {
370
- if (success) config._googleConnected = true;
371
- }).catch(() => {});
372
- sendJSON(res, 200, { ok: true, message: 'OAuth flow started. Check the browser window that opened.' });
370
+ const { buildAuthUrl } = await import('../services/google-oauth.mjs');
371
+ // Derive the redirect URI from the request Host header so it works
372
+ // on any IP/port (localhost, LAN, VM, etc.)
373
+ const host = req.headers['host'] || `127.0.0.1:${PORT}`;
374
+ const redirectUri = `http://${host}/api/google/callback`;
375
+ const { url, verifier, state } = buildAuthUrl(config, redirectUri);
376
+ // Store verifier+state in memory for the callback
377
+ _googleOAuthPending = { verifier, state, redirectUri };
378
+ sendJSON(res, 200, { ok: true, url });
373
379
  } catch (e) {
374
380
  sendJSON(res, 500, { error: e.message });
375
381
  }
@@ -377,6 +383,32 @@ export async function cmdUI(args) {
377
383
  return;
378
384
  }
379
385
 
386
+ // GET /api/google/callback — receives OAuth code from Google redirect
387
+ if (method === 'GET' && pathname === '/api/google/callback') {
388
+ const params = new URL(req.url, `http://localhost`).searchParams;
389
+ const code = params.get('code');
390
+ const state = params.get('state');
391
+ if (!code || !_googleOAuthPending || _googleOAuthPending.state !== state) {
392
+ res.writeHead(400, { 'Content-Type': 'text/html' });
393
+ res.end('<html><body><h2>OAuth error: invalid state or missing code.</h2><p>Please try again from the NHA UI.</p></body></html>');
394
+ logRequest(method, pathname, 400, Date.now() - start);
395
+ return;
396
+ }
397
+ try {
398
+ const { exchangeCodeFromUI } = await import('../services/google-oauth.mjs');
399
+ const { email } = await exchangeCodeFromUI(config, code, _googleOAuthPending.verifier, _googleOAuthPending.redirectUri);
400
+ _googleOAuthPending = null;
401
+ res.writeHead(200, { 'Content-Type': 'text/html' });
402
+ res.end(`<html><body style="font-family:sans-serif;text-align:center;padding:60px;background:#0a0a0a;color:#fff"><h2 style="color:#22c55e">&#10003; Google Connected!</h2><p style="color:#aaa">Signed in as <strong>${email}</strong></p><p style="color:#aaa">You can close this tab and return to NHA.</p><script>setTimeout(function(){window.close()},3000)</script></body></html>`);
403
+ } catch (e) {
404
+ _googleOAuthPending = null;
405
+ res.writeHead(200, { 'Content-Type': 'text/html' });
406
+ res.end(`<html><body style="font-family:sans-serif;text-align:center;padding:60px;background:#0a0a0a;color:#fff"><h2 style="color:#ef4444">&#10007; Error</h2><p style="color:#aaa">${e.message}</p></body></html>`);
407
+ }
408
+ logRequest(method, pathname, 200, Date.now() - start);
409
+ return;
410
+ }
411
+
380
412
  // ── Collab (Alexandria proxy) ─────────────────────────────────────
381
413
  if (pathname.startsWith('/api/collab/')) {
382
414
  const collabAction = pathname.split('/').pop();
package/src/constants.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
 
8
- export const VERSION = '13.5.163';
8
+ export const VERSION = '13.5.165';
9
9
  export const BASE_URL = 'https://nothumanallowed.com/cli';
10
10
  export const API_BASE = 'https://nothumanallowed.com/api/v1';
11
11
 
@@ -305,6 +305,52 @@ export async function runAuthFlow(config, manual = false) {
305
305
  }
306
306
  }
307
307
 
308
+ /**
309
+ * Build an OAuth URL for the web UI flow.
310
+ * The redirect_uri points back to the NHA web UI server so the callback
311
+ * is received directly in the browser session (works across VMs/headless).
312
+ *
313
+ * @param {object} config
314
+ * @param {string} redirectUri — e.g. http://192.168.1.45:3847/api/google/callback
315
+ * @returns {{ url: string, verifier: string, state: string }}
316
+ */
317
+ export function buildAuthUrl(config, redirectUri) {
318
+ const clientId = config.google?.clientId || DEFAULT_CLIENT_ID;
319
+ if (!clientId) throw new Error('Google client ID not configured. Run: nha config set google-client-id YOUR_ID');
320
+ const { verifier, challenge } = generatePKCE();
321
+ const state = crypto.randomBytes(16).toString('hex');
322
+ const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
323
+ authUrl.searchParams.set('client_id', clientId);
324
+ authUrl.searchParams.set('redirect_uri', redirectUri);
325
+ authUrl.searchParams.set('response_type', 'code');
326
+ authUrl.searchParams.set('scope', SCOPES);
327
+ authUrl.searchParams.set('state', state);
328
+ authUrl.searchParams.set('code_challenge', challenge);
329
+ authUrl.searchParams.set('code_challenge_method', 'S256');
330
+ authUrl.searchParams.set('access_type', 'offline');
331
+ authUrl.searchParams.set('prompt', 'consent');
332
+ return { url: authUrl.toString(), verifier, state };
333
+ }
334
+
335
+ /**
336
+ * Exchange an auth code received by the web UI callback.
337
+ */
338
+ export async function exchangeCodeFromUI(config, code, verifier, redirectUri) {
339
+ const clientId = config.google?.clientId || DEFAULT_CLIENT_ID;
340
+ const clientSecret = config.google?.clientSecret || '';
341
+ const tokenData = await exchangeCode(code, verifier, clientId, clientSecret, redirectUri);
342
+ const email = await getUserEmail(tokenData.access_token);
343
+ const tokens = {
344
+ access_token: tokenData.access_token,
345
+ refresh_token: tokenData.refresh_token,
346
+ expires_at: Date.now() + (tokenData.expires_in * 1000),
347
+ scope: tokenData.scope,
348
+ email: email || 'unknown',
349
+ };
350
+ saveTokens(tokens);
351
+ return { email: email || 'unknown' };
352
+ }
353
+
308
354
  /**
309
355
  * Show connection status.
310
356
  */
@@ -1342,6 +1342,7 @@ function emailLoadGoogleMessages() {
1342
1342
  emailState.messages = (msgs || []).map(function(m) {
1343
1343
  return { id: m.id, subject: m.subject, from_name: m.from, from_address: m.from, internal_date: m.date, body_preview: m.snippet, is_read: !m.isUnread, is_starred: false, has_attachments: false, _google: true };
1344
1344
  });
1345
+ emailState.total = emailState.messages.length;
1345
1346
  emailRenderMessageList();
1346
1347
  });
1347
1348
  }).catch(function() {
@@ -1349,6 +1350,7 @@ function emailLoadGoogleMessages() {
1349
1350
  emailState.messages = (dash.emails || []).map(function(m) {
1350
1351
  return { id: m.id, subject: m.subject, from_name: m.from, from_address: m.from, internal_date: m.date, body_preview: m.snippet, is_read: !m.isUnread, is_starred: false, has_attachments: false, _google: true };
1351
1352
  });
1353
+ emailState.total = emailState.messages.length;
1352
1354
  emailRenderMessageList();
1353
1355
  });
1354
1356
  }
@@ -3405,10 +3407,15 @@ function imapSync(accountId) {
3405
3407
 
3406
3408
  function connectGoogle() {
3407
3409
  var s = document.getElementById('googleStatus');
3408
- if (s) s.textContent = 'Starting Google sign-in...';
3410
+ if (s) { s.textContent = 'Opening Google sign-in...'; s.style.color = 'var(--dim)'; }
3409
3411
  apiPost('/api/google/auth', {}).then(function(r) {
3410
- if (s) s.textContent = r.message || 'Check the browser window that opened.';
3411
- if (s) s.style.color = 'var(--green)';
3412
+ if (r.url) {
3413
+ // Open OAuth URL in current browser — works on VMs and LAN
3414
+ window.open(r.url, '_blank');
3415
+ if (s) { s.textContent = 'Sign-in page opened. Complete the login then reload NHA.'; s.style.color = 'var(--green)'; }
3416
+ } else if (r.error) {
3417
+ if (s) { s.textContent = 'Error: ' + r.error; s.style.color = 'var(--red)'; }
3418
+ }
3412
3419
  }).catch(function(e) {
3413
3420
  if (s) { s.textContent = 'Error: ' + e.message; s.style.color = 'var(--red)'; }
3414
3421
  });