overlord-cli 3.5.0 → 3.5.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/README.md CHANGED
@@ -20,6 +20,7 @@ overlord help
20
20
  ```
21
21
 
22
22
  The CLI exposes the same command set under both names.
23
+ `ovld auth login` opens a browser when possible and also prints a verification URL/code so login can be completed from another machine over SSH.
23
24
 
24
25
  Common commands:
25
26
 
package/bin/_cli/auth.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ /* global console, fetch, process, setTimeout, URL, URLSearchParams */
2
3
 
3
4
  import { execFileSync } from 'node:child_process';
4
5
  import crypto from 'node:crypto';
@@ -8,6 +9,7 @@ import { buildAuthHeaders, clearCredentials, loadCredentials, loadRuntime, saveC
8
9
 
9
10
  const DEFAULT_OVERLORD_URL = process.env.OVERLORD_URL ?? 'http://localhost:3000';
10
11
  const DEFAULT_CLI_REDIRECT_URI = 'http://127.0.0.1:45619/callback';
12
+ const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
11
13
 
12
14
  // ---------------------------------------------------------------------------
13
15
  // PKCE helpers
@@ -170,6 +172,45 @@ async function fetchAuthConfig(platformUrl, localSecret) {
170
172
  };
171
173
  }
172
174
 
175
+ async function requestDeviceAuthorization(platformUrl, localSecret) {
176
+ const res = await fetch(`${platformUrl}/api/auth/device/request`, {
177
+ method: 'POST',
178
+ headers: buildAuthHeaders('', localSecret)
179
+ });
180
+
181
+ if (!res.ok) {
182
+ const text = await res.text();
183
+ throw new Error(`Device authorization request failed (${res.status}): ${snippet(text)}`);
184
+ }
185
+
186
+ return readJsonOrThrow(res, 'Device authorization request', platformUrl);
187
+ }
188
+
189
+ async function pollDeviceAuthorization(platformUrl, deviceCode, localSecret) {
190
+ const res = await fetch(`${platformUrl}/api/auth/device/poll`, {
191
+ method: 'POST',
192
+ headers: {
193
+ ...buildAuthHeaders('', localSecret),
194
+ 'Content-Type': 'application/json'
195
+ },
196
+ body: JSON.stringify({ device_code: deviceCode })
197
+ });
198
+
199
+ const body = await readJsonOrThrow(res, 'Device authorization poll', platformUrl);
200
+
201
+ if (res.ok) return body;
202
+
203
+ if (res.status === 400 || res.status === 404 || res.status === 429) {
204
+ return body;
205
+ }
206
+
207
+ throw new Error(`Device authorization poll failed (${res.status}): ${snippet(JSON.stringify(body))}`);
208
+ }
209
+
210
+ function sleep(ms) {
211
+ return new Promise(resolve => setTimeout(resolve, ms));
212
+ }
213
+
173
214
  async function exchangeCodeForSupabaseTokens(supabaseUrl, clientId, code, codeVerifier, redirectUri) {
174
215
  const res = await fetch(`${supabaseUrl}/auth/v1/oauth/token`, {
175
216
  method: 'POST',
@@ -219,35 +260,98 @@ function openBrowser(url) {
219
260
  }
220
261
  }
221
262
 
222
- // ---------------------------------------------------------------------------
223
- // Public auth commands
224
- // ---------------------------------------------------------------------------
263
+ function printDeviceAuthorizationInstructions(verificationUri, userCode, logger = console) {
264
+ logger.log(' Verification URL:', verificationUri);
265
+ logger.log(' Authorization code:', userCode);
266
+ logger.log('\nOpen the verification URL in any browser to approve this CLI login.\n');
267
+ }
225
268
 
226
- export async function authLogin() {
227
- const runtime = loadRuntime();
228
- const platformUrl = process.env.OVERLORD_URL ?? runtime?.platform_url ?? DEFAULT_OVERLORD_URL;
229
- const localSecret = runtime?.local_secret ?? process.env.OVERLORD_LOCAL_SECRET ?? '';
269
+ export async function authLoginViaDeviceFlow(
270
+ platformUrl,
271
+ localSecret,
272
+ {
273
+ browserOpener = openBrowser,
274
+ logger = console,
275
+ sleepFn = sleep,
276
+ stdout = process.stdout
277
+ } = {}
278
+ ) {
279
+ const deviceAuth = await requestDeviceAuthorization(platformUrl, localSecret);
280
+ const verificationUri = String(deviceAuth.verification_uri ?? '').trim();
281
+ const userCode = String(deviceAuth.user_code ?? '').trim();
282
+ const deviceCode = String(deviceAuth.device_code ?? '').trim();
283
+
284
+ if (!verificationUri || !userCode || !deviceCode) {
285
+ throw new Error('Device authorization response was missing required fields.');
286
+ }
230
287
 
231
- console.log('Starting Overlord CLI authorization...\n');
288
+ const initialPollIntervalSeconds = Number(deviceAuth.interval);
289
+ let pollIntervalSeconds =
290
+ Number.isFinite(initialPollIntervalSeconds) && initialPollIntervalSeconds > 0
291
+ ? initialPollIntervalSeconds
292
+ : DEFAULT_DEVICE_POLL_INTERVAL_SECONDS;
293
+
294
+ printDeviceAuthorizationInstructions(verificationUri, userCode, logger);
295
+ logger.log('Opening browser...\n');
296
+ browserOpener(verificationUri);
297
+
298
+ stdout.write('Waiting for browser authorization');
232
299
 
300
+ for (;;) {
301
+ await sleepFn(pollIntervalSeconds * 1000);
302
+
303
+ const result = await pollDeviceAuthorization(platformUrl, deviceCode, localSecret);
304
+ const status = String(result?.status ?? '');
305
+
306
+ if (status === 'pending') {
307
+ stdout.write('.');
308
+ continue;
309
+ }
310
+
311
+ if (status === 'slow_down') {
312
+ stdout.write('.');
313
+ const nextInterval = Number(result?.interval);
314
+ if (Number.isFinite(nextInterval) && nextInterval > 0) {
315
+ pollIntervalSeconds = nextInterval;
316
+ } else {
317
+ pollIntervalSeconds += 1;
318
+ }
319
+ continue;
320
+ }
321
+
322
+ if (status === 'authorized') {
323
+ logger.log('\n');
324
+ return {
325
+ access_token: result.access_token,
326
+ platform_url: result.platform_url ?? platformUrl
327
+ };
328
+ }
329
+
330
+ if (status === 'expired') {
331
+ throw new Error('Authorization request expired. Please run `ovld auth login` again.');
332
+ }
333
+
334
+ if (result?.error) {
335
+ throw new Error(String(result.error));
336
+ }
337
+
338
+ throw new Error(`Unexpected device authorization status: ${status || 'unknown'}`);
339
+ }
340
+ }
341
+
342
+ export async function authLoginViaOAuthLoopback(platformUrl, localSecret) {
233
343
  // 1. Discover OAuth config from the platform
234
344
  let supabaseUrl, cliClientId, cliRedirectUri, resolvedPlatformUrl;
235
- try {
236
- const config = await fetchAuthConfig(platformUrl, localSecret);
237
- supabaseUrl = config.supabase_url;
238
- cliClientId = config.cli_client_id;
239
- cliRedirectUri = config.cli_redirect_uri;
240
- resolvedPlatformUrl = config.platform_url ?? platformUrl;
241
- } catch (err) {
242
- console.error(`\nError: ${err.message}`);
243
- process.exit(1);
244
- }
345
+ const config = await fetchAuthConfig(platformUrl, localSecret);
346
+ supabaseUrl = config.supabase_url;
347
+ cliClientId = config.cli_client_id;
348
+ cliRedirectUri = config.cli_redirect_uri;
349
+ resolvedPlatformUrl = config.platform_url ?? platformUrl;
245
350
 
246
351
  if (!supabaseUrl || !cliClientId) {
247
- console.error(
248
- '\nError: OAuth is not configured for CLI login. Set SUPABASE_OAUTH_CLI_CLIENT_ID on the Overlord server.'
352
+ throw new Error(
353
+ 'OAuth is not configured for CLI login. Set SUPABASE_OAUTH_CLI_CLIENT_ID on the Overlord server.'
249
354
  );
250
- process.exit(1);
251
355
  }
252
356
 
253
357
  // 2. PKCE parameters + state
@@ -256,14 +360,7 @@ export async function authLogin() {
256
360
  const state = generateState();
257
361
 
258
362
  // 3. Use exact loopback redirect URI (Supabase does not support wildcard callback URLs)
259
- let redirectTarget;
260
- try {
261
- redirectTarget = parseLoopbackRedirectUri(cliRedirectUri ?? DEFAULT_CLI_REDIRECT_URI);
262
- } catch (err) {
263
- console.error(`\nError: ${err.message}`);
264
- process.exit(1);
265
- }
266
-
363
+ const redirectTarget = parseLoopbackRedirectUri(cliRedirectUri ?? DEFAULT_CLI_REDIRECT_URI);
267
364
  const { host, port, callbackPath, redirectUri } = redirectTarget;
268
365
 
269
366
  // 4. Build the Supabase OAuth authorization URL
@@ -285,47 +382,69 @@ export async function authLogin() {
285
382
 
286
383
  // 6. Wait for the auth code
287
384
  let authCode;
288
- try {
289
- process.stdout.write('Waiting for browser authorization');
290
- authCode = await callbackPromise;
291
- console.log('\n');
292
- } catch (err) {
293
- console.error(`\n\nAuthorization failed: ${err.message}`);
294
- process.exit(1);
295
- }
385
+ process.stdout.write('Waiting for browser authorization');
386
+ authCode = await callbackPromise;
387
+ console.log('\n');
296
388
 
297
389
  // 7. Exchange auth code → Supabase tokens
298
- let supabaseTokens;
299
- try {
300
- supabaseTokens = await exchangeCodeForSupabaseTokens(
301
- supabaseUrl,
302
- cliClientId,
303
- authCode,
304
- codeVerifier,
305
- redirectUri
306
- );
307
- } catch (err) {
308
- console.error(`\nError exchanging code for tokens: ${err.message}`);
309
- process.exit(1);
310
- }
390
+ const supabaseTokens = await exchangeCodeForSupabaseTokens(
391
+ supabaseUrl,
392
+ cliClientId,
393
+ authCode,
394
+ codeVerifier,
395
+ redirectUri
396
+ );
311
397
 
312
398
  // 8. Exchange Supabase access token → Overlord agent_token
313
- let agentTokenData;
399
+ const agentTokenData = await exchangeForAgentToken(
400
+ resolvedPlatformUrl,
401
+ supabaseTokens.access_token,
402
+ localSecret
403
+ );
404
+
405
+ return {
406
+ access_token: agentTokenData.access_token,
407
+ platform_url: agentTokenData.platform_url ?? resolvedPlatformUrl
408
+ };
409
+ }
410
+
411
+ // ---------------------------------------------------------------------------
412
+ // Public auth commands
413
+ // ---------------------------------------------------------------------------
414
+
415
+ export async function authLogin() {
416
+ const runtime = loadRuntime();
417
+ const platformUrl = process.env.OVERLORD_URL ?? runtime?.platform_url ?? DEFAULT_OVERLORD_URL;
418
+ const localSecret = runtime?.local_secret ?? process.env.OVERLORD_LOCAL_SECRET ?? '';
419
+
420
+ console.log('Starting Overlord CLI authorization...\n');
421
+ let credentials;
314
422
  try {
315
- agentTokenData = await exchangeForAgentToken(
316
- resolvedPlatformUrl,
317
- supabaseTokens.access_token,
318
- localSecret
319
- );
423
+ credentials = await authLoginViaDeviceFlow(platformUrl, localSecret);
320
424
  } catch (err) {
321
- console.error(`\nError obtaining agent token: ${err.message}`);
322
- process.exit(1);
425
+ const message = err instanceof Error ? err.message : String(err);
426
+ const canFallbackToLoopback =
427
+ message.includes('Device authorization request failed (404)') ||
428
+ message.includes('Device authorization request failed (405)');
429
+
430
+ if (!canFallbackToLoopback) {
431
+ console.error(`\nAuthorization failed: ${message}`);
432
+ process.exit(1);
433
+ }
434
+
435
+ console.log('Device authorization is unavailable on this server. Falling back to loopback OAuth.\n');
436
+
437
+ try {
438
+ credentials = await authLoginViaOAuthLoopback(platformUrl, localSecret);
439
+ } catch (fallbackErr) {
440
+ console.error(`\nAuthorization failed: ${fallbackErr.message}`);
441
+ process.exit(1);
442
+ }
323
443
  }
324
444
 
325
- // 9. Persist credentials (same format — backward-compatible)
326
445
  saveCredentials({
327
- access_token: agentTokenData.access_token,
328
- platform_url: agentTokenData.platform_url ?? resolvedPlatformUrl
446
+ access_token: credentials.access_token,
447
+ platform_url: credentials.platform_url ?? platformUrl
329
448
  });
330
449
 
331
450
  console.log('Logged in successfully!');
@@ -354,7 +473,7 @@ export async function runAuthCommand(subcommand) {
354
473
  console.log(`ovld auth <subcommand>
355
474
 
356
475
  Subcommands:
357
- login Authorize the CLI via browser (OAuth PKCE flow)
476
+ login Authorize the CLI via browser (works locally or over SSH)
358
477
  status Show current login status
359
478
  logout Remove stored credentials
360
479
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "3.5.0",
3
+ "version": "3.5.1",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "type": "module",
6
6
  "bin": {