overlord-cli 5.0.0 → 5.1.0
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 +8 -0
- package/bin/_cli/auth.mjs +70 -29
- package/bin/_cli/credentials.mjs +40 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,14 @@ overlord help
|
|
|
25
25
|
The CLI exposes the same command set under both names.
|
|
26
26
|
`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.
|
|
27
27
|
|
|
28
|
+
Desktop-installed wrappers default `OVERLORD_URL` to `https://www.ovld.ai` unless you override it explicitly.
|
|
29
|
+
For local dev against the web app on port 3000, export the override before running auth or protocol commands:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export OVERLORD_URL=http://localhost:3000
|
|
33
|
+
ovld auth login
|
|
34
|
+
```
|
|
35
|
+
|
|
28
36
|
Common commands:
|
|
29
37
|
|
|
30
38
|
```bash
|
package/bin/_cli/auth.mjs
CHANGED
|
@@ -144,6 +144,17 @@ function snippet(value, max = 180) {
|
|
|
144
144
|
return normalized.length <= max ? normalized : `${normalized.slice(0, max)}...`;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function describeNetworkError(error, context) {
|
|
148
|
+
const cause = error?.cause;
|
|
149
|
+
const details = [cause?.code, cause?.message].filter(Boolean).join(': ');
|
|
150
|
+
if (details) {
|
|
151
|
+
return new Error(`${context}: ${details}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
return new Error(`${context}: ${message}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
async function readJsonOrThrow(res, context, baseUrl) {
|
|
148
159
|
const contentType = res.headers.get('content-type') ?? '';
|
|
149
160
|
const bodyText = await res.text();
|
|
@@ -165,9 +176,14 @@ async function readJsonOrThrow(res, context, baseUrl) {
|
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
async function fetchAuthConfig(platformUrl, localSecret) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
179
|
+
let res;
|
|
180
|
+
try {
|
|
181
|
+
res = await fetch(`${platformUrl}/api/auth/config`, {
|
|
182
|
+
headers: buildAuthHeaders('', localSecret)
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw describeNetworkError(error, `Failed to fetch auth config from ${platformUrl}`);
|
|
186
|
+
}
|
|
171
187
|
if (!res.ok) {
|
|
172
188
|
throw new Error(
|
|
173
189
|
`Failed to fetch auth config (${res.status}). Check that Overlord is running at ${platformUrl}.`
|
|
@@ -181,10 +197,18 @@ async function fetchAuthConfig(platformUrl, localSecret) {
|
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
async function requestDeviceAuthorization(platformUrl, localSecret) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
200
|
+
let res;
|
|
201
|
+
try {
|
|
202
|
+
res = await fetch(`${platformUrl}/api/auth/device/request`, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: buildAuthHeaders('', localSecret)
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw describeNetworkError(
|
|
208
|
+
error,
|
|
209
|
+
`Device authorization request failed for ${platformUrl}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
188
212
|
|
|
189
213
|
if (!res.ok) {
|
|
190
214
|
const text = await res.text();
|
|
@@ -195,14 +219,22 @@ async function requestDeviceAuthorization(platformUrl, localSecret) {
|
|
|
195
219
|
}
|
|
196
220
|
|
|
197
221
|
async function pollDeviceAuthorization(platformUrl, deviceCode, localSecret) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
let res;
|
|
223
|
+
try {
|
|
224
|
+
res = await fetch(`${platformUrl}/api/auth/device/poll`, {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: {
|
|
227
|
+
...buildAuthHeaders('', localSecret),
|
|
228
|
+
'Content-Type': 'application/json'
|
|
229
|
+
},
|
|
230
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
231
|
+
});
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw describeNetworkError(
|
|
234
|
+
error,
|
|
235
|
+
`Device authorization poll failed for ${platformUrl}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
206
238
|
|
|
207
239
|
const body = await readJsonOrThrow(res, 'Device authorization poll', platformUrl);
|
|
208
240
|
|
|
@@ -220,17 +252,25 @@ function sleep(ms) {
|
|
|
220
252
|
}
|
|
221
253
|
|
|
222
254
|
async function exchangeCodeForSupabaseTokens(supabaseUrl, clientId, code, codeVerifier, redirectUri) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
255
|
+
let res;
|
|
256
|
+
try {
|
|
257
|
+
res = await fetch(`${supabaseUrl}/auth/v1/oauth/token`, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
260
|
+
body: new URLSearchParams({
|
|
261
|
+
grant_type: 'authorization_code',
|
|
262
|
+
code,
|
|
263
|
+
client_id: clientId,
|
|
264
|
+
redirect_uri: redirectUri,
|
|
265
|
+
code_verifier: codeVerifier
|
|
266
|
+
})
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw describeNetworkError(
|
|
270
|
+
error,
|
|
271
|
+
`Token exchange failed for ${new URL(supabaseUrl).host}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
234
274
|
|
|
235
275
|
if (!res.ok) {
|
|
236
276
|
const text = await res.text();
|
|
@@ -474,8 +514,8 @@ async function promptForOrganization(organizations, preselectedId = null) {
|
|
|
474
514
|
// Public auth commands
|
|
475
515
|
// ---------------------------------------------------------------------------
|
|
476
516
|
|
|
477
|
-
export function resolveLoginPlatformUrl(runtime = null) {
|
|
478
|
-
return process.env.OVERLORD_URL ?? runtime?.platform_url ?? getDefaultOverlordUrl();
|
|
517
|
+
export function resolveLoginPlatformUrl(runtime = null, storedPlatformUrl = null) {
|
|
518
|
+
return process.env.OVERLORD_URL ?? storedPlatformUrl ?? runtime?.platform_url ?? getDefaultOverlordUrl();
|
|
479
519
|
}
|
|
480
520
|
|
|
481
521
|
function parseOrganizationFlag(args) {
|
|
@@ -491,7 +531,8 @@ function parseOrganizationFlag(args) {
|
|
|
491
531
|
|
|
492
532
|
export async function authLogin(args = []) {
|
|
493
533
|
const preselectedOrganizationId = parseOrganizationFlag(args);
|
|
494
|
-
const
|
|
534
|
+
const storedCredentials = loadCredentials();
|
|
535
|
+
const platformUrl = resolveLoginPlatformUrl(null, storedCredentials?.platform_url ?? null);
|
|
495
536
|
const runtime = loadRuntime(platformUrl);
|
|
496
537
|
const localSecret = runtime?.local_secret ?? process.env.OVERLORD_LOCAL_SECRET ?? '';
|
|
497
538
|
|
package/bin/_cli/credentials.mjs
CHANGED
|
@@ -354,13 +354,29 @@ function isAccessTokenFresh(credentials) {
|
|
|
354
354
|
return expiresAt - Date.now() > 60_000;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
+
function describeNetworkError(error, context) {
|
|
358
|
+
const cause = error?.cause;
|
|
359
|
+
const details = [cause?.code, cause?.message].filter(Boolean).join(': ');
|
|
360
|
+
if (details) {
|
|
361
|
+
return new Error(`${context}: ${details}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
+
return new Error(`${context}: ${message}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
357
368
|
const authConfigCache = new Map();
|
|
358
369
|
|
|
359
370
|
async function fetchAuthConfig(platformUrl, localSecret) {
|
|
360
371
|
if (authConfigCache.has(platformUrl)) return authConfigCache.get(platformUrl);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
372
|
+
let res;
|
|
373
|
+
try {
|
|
374
|
+
res = await fetch(`${platformUrl}/api/auth/config`, {
|
|
375
|
+
headers: buildAuthHeaders('', localSecret)
|
|
376
|
+
});
|
|
377
|
+
} catch (error) {
|
|
378
|
+
throw describeNetworkError(error, `Failed to fetch auth config from ${platformUrl}`);
|
|
379
|
+
}
|
|
364
380
|
if (!res.ok) {
|
|
365
381
|
throw new Error(`Failed to fetch auth config (${res.status}).`);
|
|
366
382
|
}
|
|
@@ -378,15 +394,23 @@ async function refreshOAuthAccessToken(platformUrl, refreshToken, localSecret) {
|
|
|
378
394
|
throw new Error('OAuth is not configured for Overlord CLI auth.');
|
|
379
395
|
}
|
|
380
396
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
397
|
+
let res;
|
|
398
|
+
try {
|
|
399
|
+
res = await fetch(`${supabaseUrl}/auth/v1/oauth/token`, {
|
|
400
|
+
method: 'POST',
|
|
401
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
402
|
+
body: new URLSearchParams({
|
|
403
|
+
grant_type: 'refresh_token',
|
|
404
|
+
refresh_token: refreshToken,
|
|
405
|
+
client_id: clientId
|
|
406
|
+
})
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
throw describeNetworkError(
|
|
410
|
+
error,
|
|
411
|
+
`OAuth token refresh failed for ${new URL(supabaseUrl).host}`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
390
414
|
|
|
391
415
|
if (!res.ok) {
|
|
392
416
|
const text = await res.text().catch(() => '');
|
|
@@ -518,9 +542,9 @@ export async function resolveAuth() {
|
|
|
518
542
|
};
|
|
519
543
|
saveCredentials(nextCredentials);
|
|
520
544
|
} catch (refreshError) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
545
|
+
throw new Error(
|
|
546
|
+
`Stored Overlord session expired and refresh failed. ${refreshError instanceof Error ? refreshError.message : String(refreshError)} Run \`ovld auth login\` again.`
|
|
547
|
+
);
|
|
524
548
|
}
|
|
525
549
|
}
|
|
526
550
|
|
|
@@ -584,7 +608,7 @@ export async function getAuthStatus() {
|
|
|
584
608
|
}
|
|
585
609
|
|
|
586
610
|
return {
|
|
587
|
-
isLoggedIn: tokenSource !== 'fallback',
|
|
611
|
+
isLoggedIn: tokenSource !== 'fallback' && !error,
|
|
588
612
|
platformUrl: resolved.platformUrl,
|
|
589
613
|
platformUrlSource,
|
|
590
614
|
tokenPresent: tokenSource !== 'fallback',
|