overlord-cli 4.23.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 +77 -33
- package/bin/_cli/credentials.mjs +95 -56
- 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
|
|
|
@@ -556,10 +597,13 @@ async function printVerboseAuthStatus() {
|
|
|
556
597
|
console.log(` Error: ${status.error}`);
|
|
557
598
|
}
|
|
558
599
|
console.log(` Local secret: ${status.hasLocalSecret ? 'yes' : 'no'}`);
|
|
559
|
-
console.log(` credentials.json: ${status.credentialsFileExists ? 'present' : 'missing'}`);
|
|
560
|
-
|
|
561
|
-
`
|
|
562
|
-
|
|
600
|
+
console.log(` credentials.cli.json: ${status.credentialsFileExists ? 'present' : 'missing'}`);
|
|
601
|
+
if (status.legacyCredentialsFileExists) {
|
|
602
|
+
console.log(` credentials.json (legacy): present`);
|
|
603
|
+
}
|
|
604
|
+
if (status.electronCredentialsFileExists) {
|
|
605
|
+
console.log(` electron-credentials.json (legacy): present`);
|
|
606
|
+
}
|
|
563
607
|
}
|
|
564
608
|
|
|
565
609
|
export async function authStatus(args = []) {
|
package/bin/_cli/credentials.mjs
CHANGED
|
@@ -8,8 +8,10 @@ import path from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
|
|
10
10
|
const CREDENTIALS_DIR = path.join(os.homedir(), '.ovld');
|
|
11
|
-
const
|
|
12
|
-
const
|
|
11
|
+
const CLI_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.cli.json');
|
|
12
|
+
const LEGACY_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'credentials.json');
|
|
13
|
+
const LEGACY_ELECTRON_CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, 'electron-credentials.json');
|
|
14
|
+
const LEGACY_MIGRATION_MARKER = path.join(CREDENTIALS_DIR, '.cli-migrated');
|
|
13
15
|
const RUNTIME_FILE_PATTERN = /^runtime\..+\.json$/;
|
|
14
16
|
const HOSTED_OVERLORD_URL = 'https://www.ovld.ai';
|
|
15
17
|
const LOCAL_DEV_OVERLORD_URL = 'http://localhost:3000';
|
|
@@ -114,18 +116,39 @@ function normalizeCredentialsForSave(data) {
|
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
function migrateLegacyCredentials() {
|
|
120
|
+
if (fileExists(LEGACY_MIGRATION_MARKER)) return null;
|
|
121
|
+
|
|
122
|
+
const legacyShared = parseStoredCredentialsData(readJsonFile(LEGACY_CREDENTIALS_FILE), {
|
|
123
|
+
requireAuthData: true
|
|
124
|
+
});
|
|
125
|
+
const legacyElectron = parseStoredCredentialsData(readJsonFile(LEGACY_ELECTRON_CREDENTIALS_FILE), {
|
|
120
126
|
requireAuthData: true
|
|
121
127
|
});
|
|
122
|
-
|
|
128
|
+
|
|
129
|
+
const source = legacyShared ?? legacyElectron;
|
|
130
|
+
if (!source) return null;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
writeJsonFileAtomic(CLI_CREDENTIALS_FILE, { ...source, updated_at: new Date().toISOString() });
|
|
134
|
+
ensureCredentialsDir();
|
|
135
|
+
fs.writeFileSync(LEGACY_MIGRATION_MARKER, new Date().toISOString(), { mode: 0o600 });
|
|
136
|
+
} catch {
|
|
137
|
+
// Best-effort migration
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return source;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @returns {Credentials | null} */
|
|
144
|
+
export function loadCredentials() {
|
|
145
|
+
const cliCredentials = parseStoredCredentialsData(readJsonFile(CLI_CREDENTIALS_FILE), {
|
|
123
146
|
requireAuthData: true
|
|
124
147
|
});
|
|
125
148
|
|
|
126
149
|
if (cliCredentials?.refresh_token) return cliCredentials;
|
|
127
|
-
|
|
128
|
-
return
|
|
150
|
+
|
|
151
|
+
return migrateLegacyCredentials();
|
|
129
152
|
}
|
|
130
153
|
|
|
131
154
|
/** @param {Credentials} data */
|
|
@@ -135,45 +158,36 @@ export function saveCredentials(data) {
|
|
|
135
158
|
throw new Error('Cannot save empty Overlord credentials.');
|
|
136
159
|
}
|
|
137
160
|
|
|
138
|
-
|
|
139
|
-
writeJsonFileAtomic(CREDENTIALS_FILE, sharedCredentials);
|
|
140
|
-
|
|
141
|
-
const existingElectronCredentials = readJsonFile(ELECTRON_CREDENTIALS_FILE);
|
|
142
|
-
if (existingElectronCredentials && typeof existingElectronCredentials === 'object') {
|
|
143
|
-
const electronPayload = { ...existingElectronCredentials };
|
|
144
|
-
electronPayload.updated_at = sharedCredentials.updated_at;
|
|
145
|
-
if (credentials.platform_url) electronPayload.platform_url = credentials.platform_url;
|
|
146
|
-
if (credentials.access_token_expires_at) {
|
|
147
|
-
electronPayload.access_token_expires_at = credentials.access_token_expires_at;
|
|
148
|
-
}
|
|
149
|
-
if (credentials.organization_id) electronPayload.organization_id = credentials.organization_id;
|
|
150
|
-
if (credentials.user_email) electronPayload.user_email = credentials.user_email;
|
|
151
|
-
delete electronPayload.supabase_refresh_token;
|
|
152
|
-
writeJsonFileAtomic(ELECTRON_CREDENTIALS_FILE, electronPayload);
|
|
153
|
-
}
|
|
161
|
+
writeJsonFileAtomic(CLI_CREDENTIALS_FILE, { ...credentials, updated_at: new Date().toISOString() });
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
export function clearCredentials() {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// Already gone
|
|
162
|
-
}
|
|
165
|
+
try {
|
|
166
|
+
fs.unlinkSync(CLI_CREDENTIALS_FILE);
|
|
167
|
+
} catch {
|
|
168
|
+
// Already gone
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
function getCredentialFileSource() {
|
|
167
|
-
const cliCredentials = parseStoredCredentialsData(readJsonFile(
|
|
168
|
-
requireAuthData: true
|
|
169
|
-
});
|
|
170
|
-
const electronCredentials = parseStoredCredentialsData(readJsonFile(ELECTRON_CREDENTIALS_FILE), {
|
|
173
|
+
const cliCredentials = parseStoredCredentialsData(readJsonFile(CLI_CREDENTIALS_FILE), {
|
|
171
174
|
requireAuthData: true
|
|
172
175
|
});
|
|
173
|
-
if (cliCredentials?.refresh_token) return 'credentials.json';
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
176
|
+
if (cliCredentials?.refresh_token) return 'credentials.cli.json';
|
|
177
|
+
|
|
178
|
+
if (fileExists(LEGACY_CREDENTIALS_FILE)) {
|
|
179
|
+
const legacyShared = parseStoredCredentialsData(readJsonFile(LEGACY_CREDENTIALS_FILE), {
|
|
180
|
+
requireAuthData: true
|
|
181
|
+
});
|
|
182
|
+
if (legacyShared?.refresh_token) return 'credentials.json (legacy)';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (fileExists(LEGACY_ELECTRON_CREDENTIALS_FILE)) {
|
|
186
|
+
const legacyElectron = parseStoredCredentialsData(readJsonFile(LEGACY_ELECTRON_CREDENTIALS_FILE), {
|
|
187
|
+
requireAuthData: true
|
|
188
|
+
});
|
|
189
|
+
if (legacyElectron?.refresh_token) return 'electron-credentials.json (legacy)';
|
|
190
|
+
}
|
|
177
191
|
|
|
178
192
|
return 'none';
|
|
179
193
|
}
|
|
@@ -340,13 +354,29 @@ function isAccessTokenFresh(credentials) {
|
|
|
340
354
|
return expiresAt - Date.now() > 60_000;
|
|
341
355
|
}
|
|
342
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
|
+
|
|
343
368
|
const authConfigCache = new Map();
|
|
344
369
|
|
|
345
370
|
async function fetchAuthConfig(platformUrl, localSecret) {
|
|
346
371
|
if (authConfigCache.has(platformUrl)) return authConfigCache.get(platformUrl);
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
+
}
|
|
350
380
|
if (!res.ok) {
|
|
351
381
|
throw new Error(`Failed to fetch auth config (${res.status}).`);
|
|
352
382
|
}
|
|
@@ -364,15 +394,23 @@ async function refreshOAuthAccessToken(platformUrl, refreshToken, localSecret) {
|
|
|
364
394
|
throw new Error('OAuth is not configured for Overlord CLI auth.');
|
|
365
395
|
}
|
|
366
396
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
+
}
|
|
376
414
|
|
|
377
415
|
if (!res.ok) {
|
|
378
416
|
const text = await res.text().catch(() => '');
|
|
@@ -504,9 +542,9 @@ export async function resolveAuth() {
|
|
|
504
542
|
};
|
|
505
543
|
saveCredentials(nextCredentials);
|
|
506
544
|
} catch (refreshError) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
+
);
|
|
510
548
|
}
|
|
511
549
|
}
|
|
512
550
|
|
|
@@ -570,7 +608,7 @@ export async function getAuthStatus() {
|
|
|
570
608
|
}
|
|
571
609
|
|
|
572
610
|
return {
|
|
573
|
-
isLoggedIn: tokenSource !== 'fallback',
|
|
611
|
+
isLoggedIn: tokenSource !== 'fallback' && !error,
|
|
574
612
|
platformUrl: resolved.platformUrl,
|
|
575
613
|
platformUrlSource,
|
|
576
614
|
tokenPresent: tokenSource !== 'fallback',
|
|
@@ -579,8 +617,9 @@ export async function getAuthStatus() {
|
|
|
579
617
|
organizationId: resolved.organizationId ?? null,
|
|
580
618
|
authMode: resolved.authMode,
|
|
581
619
|
error,
|
|
582
|
-
credentialsFileExists: fileExists(
|
|
583
|
-
|
|
620
|
+
credentialsFileExists: fileExists(CLI_CREDENTIALS_FILE),
|
|
621
|
+
legacyCredentialsFileExists: fileExists(LEGACY_CREDENTIALS_FILE),
|
|
622
|
+
electronCredentialsFileExists: fileExists(LEGACY_ELECTRON_CREDENTIALS_FILE)
|
|
584
623
|
};
|
|
585
624
|
}
|
|
586
625
|
|