@oml/cli 0.14.1 → 0.14.3
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/out/auth/auth.d.ts +3 -3
- package/out/auth/auth.js +232 -97
- package/out/auth/auth.js.map +1 -1
- package/out/auth/platform.d.ts +1 -1
- package/out/auth/platform.js +14 -11
- package/out/auth/platform.js.map +1 -1
- package/out/cli.js +59 -78
- package/out/cli.js.map +1 -1
- package/out/commands/server/actions.d.ts +3 -3
- package/out/commands/server/actions.js +87 -22
- package/out/commands/server/actions.js.map +1 -1
- package/out/commands/server/require.js +1 -1
- package/out/commands/server/require.js.map +1 -1
- package/out/commands/server/rest.js +103 -31
- package/out/commands/server/rest.js.map +1 -1
- package/package.json +5 -4
- package/src/auth/auth.ts +261 -115
- package/src/auth/platform.ts +14 -13
- package/src/cli.ts +62 -84
- package/src/commands/server/actions.ts +107 -26
- package/src/commands/server/require.ts +1 -1
- package/src/commands/server/rest.ts +119 -32
package/src/auth/auth.ts
CHANGED
|
@@ -5,11 +5,13 @@ import chalk from 'chalk';
|
|
|
5
5
|
import * as fs from 'node:fs/promises';
|
|
6
6
|
import * as os from 'node:os';
|
|
7
7
|
import * as path from 'node:path';
|
|
8
|
+
import keytar from 'keytar';
|
|
8
9
|
import {
|
|
9
10
|
DEFAULT_API_BASE_URL,
|
|
10
11
|
DEFAULT_SUPABASE_ANON_KEY,
|
|
11
12
|
DEFAULT_SUPABASE_URL
|
|
12
13
|
} from './constants.js';
|
|
14
|
+
|
|
13
15
|
const GITHUB_DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
14
16
|
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
15
17
|
const GITHUB_USER_URL = 'https://api.github.com/user';
|
|
@@ -17,20 +19,32 @@ const DEFAULT_GITHUB_CLIENT_ID = 'Ov23liQkHYczdOAvHp5P';
|
|
|
17
19
|
const API_BASE_URL_ENV = 'OML_PLATFORM_API_URL';
|
|
18
20
|
const SUPABASE_URL_ENV = 'OML_SUPABASE_URL';
|
|
19
21
|
const SUPABASE_ANON_KEY_ENV = 'OML_SUPABASE_ANON_KEY';
|
|
22
|
+
const API_KEY_ENV = 'OML_PLATFORM_API_KEY';
|
|
23
|
+
const KEYCHAIN_SERVICE = 'oml-code';
|
|
24
|
+
const ACCESS_TOKEN_KEY = 'oml.cli.access_token';
|
|
25
|
+
const REFRESH_TOKEN_KEY = 'oml.cli.refresh_token';
|
|
26
|
+
const EXPIRES_AT_KEY = 'oml.cli.expires_at';
|
|
27
|
+
const PROFILE_PATH = path.join(os.homedir(), '.oml', 'cli-profile.json');
|
|
28
|
+
const LOCK_PATH = path.join(os.homedir(), '.oml', 'credentials.lock');
|
|
29
|
+
const SESSION_EXPIRATION_LEEWAY_MS = 20_000;
|
|
30
|
+
const LOCK_TIMEOUT_MS = 5_000;
|
|
31
|
+
const LOCK_POLL_INTERVAL_MS = 200;
|
|
20
32
|
|
|
21
33
|
type Provider = 'github';
|
|
22
34
|
|
|
23
|
-
type
|
|
35
|
+
type StoredProfile = {
|
|
24
36
|
provider: Provider;
|
|
25
37
|
userId: string;
|
|
26
38
|
userLabel?: string;
|
|
27
39
|
email: string | null;
|
|
28
40
|
tier?: string;
|
|
41
|
+
signedInAt: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type StoredCredential = {
|
|
29
45
|
accessToken: string;
|
|
30
46
|
refreshToken: string;
|
|
31
|
-
|
|
32
|
-
expiresIn: number;
|
|
33
|
-
signedInAt: string;
|
|
47
|
+
expiresAtMs: number;
|
|
34
48
|
};
|
|
35
49
|
|
|
36
50
|
type LoginOptions = {
|
|
@@ -43,134 +57,219 @@ export type OmlCliServerAuthSnapshot = {
|
|
|
43
57
|
};
|
|
44
58
|
|
|
45
59
|
export class OmlCliAuthService {
|
|
60
|
+
async login(_options: LoginOptions): Promise<void> {
|
|
61
|
+
const apiKey = process.env[API_KEY_ENV]?.trim();
|
|
62
|
+
if (apiKey) {
|
|
63
|
+
console.error(chalk.yellow(
|
|
64
|
+
'OML_PLATFORM_API_KEY is set and will take precedence over stored OAuth credentials. ' +
|
|
65
|
+
'Unset it to use interactive CLI login.'
|
|
66
|
+
));
|
|
67
|
+
console.error('Run `oml whoami` to inspect the currently effective authentication mode.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
const existing = await this.tryGetValidSnapshot();
|
|
72
|
+
if (existing) {
|
|
73
|
+
const profile = await readProfile();
|
|
74
|
+
console.error(chalk.green(
|
|
75
|
+
`Already signed in as ${profile?.userLabel ?? profile?.email ?? profile?.userId ?? 'current user'}.`
|
|
76
|
+
));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const session = await authenticateWithGitHub();
|
|
81
|
+
await writeCredential({
|
|
82
|
+
accessToken: session.accessToken,
|
|
83
|
+
refreshToken: session.refreshToken,
|
|
84
|
+
expiresAtMs: session.expiresAtMs,
|
|
85
|
+
});
|
|
86
|
+
await writeProfile({
|
|
87
|
+
provider: session.provider,
|
|
88
|
+
userId: session.userId,
|
|
89
|
+
userLabel: session.userLabel,
|
|
90
|
+
email: session.email,
|
|
91
|
+
tier: session.tier,
|
|
92
|
+
signedInAt: new Date().toISOString(),
|
|
93
|
+
});
|
|
50
94
|
const summary = session.userLabel ?? session.email ?? 'signed-in user';
|
|
51
95
|
console.error(chalk.green(`Signed in as ${summary} via ${session.provider}.`));
|
|
52
96
|
}
|
|
53
97
|
|
|
54
98
|
async logout(): Promise<void> {
|
|
55
|
-
await
|
|
56
|
-
|
|
99
|
+
const activeServers = await listActiveServers();
|
|
100
|
+
await deleteCredential();
|
|
101
|
+
await deleteProfile();
|
|
102
|
+
if (activeServers.length > 0) {
|
|
103
|
+
console.error(chalk.yellow('Stored OAuth credentials were cleared. Running servers were not stopped:'));
|
|
104
|
+
for (const server of activeServers) {
|
|
105
|
+
console.error(`- ${server.workspaceRoot ?? '(unknown workspace)'} on port ${server.port} (pid ${server.pid})`);
|
|
106
|
+
}
|
|
107
|
+
if (process.env[API_KEY_ENV]?.trim()) {
|
|
108
|
+
console.error(chalk.yellow('OML_PLATFORM_API_KEY is still set, so future starts may use API-key auth.'));
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.error(chalk.green('Stored OAuth credentials were cleared.'));
|
|
113
|
+
if (process.env[API_KEY_ENV]?.trim()) {
|
|
114
|
+
console.error(chalk.yellow('OML_PLATFORM_API_KEY is still set, so future starts may use API-key auth.'));
|
|
115
|
+
}
|
|
57
116
|
}
|
|
58
117
|
|
|
59
118
|
async whoami(): Promise<void> {
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
119
|
+
const apiKey = process.env[API_KEY_ENV]?.trim();
|
|
120
|
+
if (apiKey) {
|
|
121
|
+
console.error('Auth mode: api_key');
|
|
122
|
+
console.error('Account: resolved by OML Platform from OML_PLATFORM_API_KEY');
|
|
123
|
+
const profile = await readProfile();
|
|
124
|
+
if (profile) {
|
|
125
|
+
console.error(`Stored OAuth user: ${profile.userLabel ?? profile.email ?? profile.userId}`);
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const credential = await readCredential();
|
|
131
|
+
const profile = await readProfile();
|
|
132
|
+
if (!credential || !profile) {
|
|
62
133
|
console.error(chalk.yellow('Not signed in.'));
|
|
63
134
|
return;
|
|
64
135
|
}
|
|
65
|
-
console.error(
|
|
66
|
-
console.error(`
|
|
67
|
-
console.error(`User
|
|
68
|
-
console.error(`
|
|
69
|
-
console.error(`
|
|
70
|
-
console.error(`
|
|
136
|
+
console.error('Auth mode: oauth');
|
|
137
|
+
console.error(`Provider: ${profile.provider}`);
|
|
138
|
+
console.error(`User ID: ${profile.userId}`);
|
|
139
|
+
console.error(`User label: ${profile.userLabel ?? '(not set)'}`);
|
|
140
|
+
console.error(`Email: ${profile.email ?? '(not set)'}`);
|
|
141
|
+
console.error(`Tier: ${profile.tier ?? '(not set)'}`);
|
|
142
|
+
console.error(`Signed in at: ${profile.signedInAt}`);
|
|
143
|
+
console.error(`Access token expires at: ${new Date(credential.expiresAtMs).toISOString()}`);
|
|
71
144
|
}
|
|
72
145
|
|
|
73
146
|
async ensureAuthenticated(operationName: string): Promise<void> {
|
|
74
|
-
|
|
147
|
+
if (process.env[API_KEY_ENV]?.trim()) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const session = await readCredential();
|
|
75
151
|
if (!session) {
|
|
76
152
|
throw new Error(`${operationName} requires authentication. Run 'oml login' first.`);
|
|
77
153
|
}
|
|
78
154
|
}
|
|
79
155
|
|
|
80
156
|
async getAccessToken(): Promise<string> {
|
|
81
|
-
const session = await
|
|
82
|
-
if (!session?.accessToken) {
|
|
83
|
-
throw new Error('OML CLI authentication is required. Run \'oml login\' first.');
|
|
84
|
-
}
|
|
157
|
+
const session = await this.getServerAuthSnapshot();
|
|
85
158
|
return session.accessToken;
|
|
86
159
|
}
|
|
87
160
|
|
|
88
161
|
async refreshAccessToken(): Promise<string> {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
throw new Error('OML CLI authentication is required. Run \'oml login\' first.');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let refreshed;
|
|
95
|
-
try {
|
|
96
|
-
refreshed = await refreshSupabaseAccessToken(
|
|
97
|
-
resolveSupabaseUrl(),
|
|
98
|
-
resolveSupabaseAnonKey(),
|
|
99
|
-
session.refreshToken
|
|
100
|
-
);
|
|
101
|
-
} catch {
|
|
102
|
-
throw new Error('Authentication refresh failed. Check your network connection or sign in again with \'oml login\'.');
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const updatedSession: StoredSession = {
|
|
106
|
-
...session,
|
|
107
|
-
accessToken: refreshed.access_token,
|
|
108
|
-
refreshToken: refreshed.refresh_token,
|
|
109
|
-
tokenType: refreshed.token_type,
|
|
110
|
-
expiresIn: refreshed.expires_in,
|
|
111
|
-
email: refreshed.email ?? session.email,
|
|
112
|
-
signedInAt: new Date().toISOString(),
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
await writeSession(updatedSession);
|
|
116
|
-
return updatedSession.accessToken;
|
|
162
|
+
const refreshed = await this.refreshCredential();
|
|
163
|
+
return refreshed.accessToken;
|
|
117
164
|
}
|
|
118
165
|
|
|
119
166
|
async getServerAuthSnapshot(): Promise<OmlCliServerAuthSnapshot> {
|
|
120
|
-
const session = await
|
|
167
|
+
const session = await readCredential();
|
|
121
168
|
if (!session?.accessToken) {
|
|
122
169
|
throw new Error('OML CLI authentication is required. Run \'oml login\' first.');
|
|
123
170
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
171
|
+
if (Date.now() + SESSION_EXPIRATION_LEEWAY_MS >= session.expiresAtMs) {
|
|
172
|
+
const refreshed = await this.refreshCredential();
|
|
173
|
+
return {
|
|
174
|
+
accessToken: refreshed.accessToken,
|
|
175
|
+
refreshToken: refreshed.refreshToken,
|
|
176
|
+
expiresAtMs: refreshed.expiresAtMs,
|
|
177
|
+
};
|
|
131
178
|
}
|
|
132
179
|
return {
|
|
133
180
|
accessToken: session.accessToken,
|
|
134
181
|
refreshToken: session.refreshToken,
|
|
135
|
-
expiresAtMs,
|
|
182
|
+
expiresAtMs: session.expiresAtMs,
|
|
136
183
|
};
|
|
137
184
|
}
|
|
138
185
|
|
|
139
|
-
async
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
186
|
+
private async tryGetValidSnapshot(): Promise<OmlCliServerAuthSnapshot | null> {
|
|
187
|
+
try {
|
|
188
|
+
return await this.getServerAuthSnapshot();
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async refreshCredential(): Promise<StoredCredential> {
|
|
195
|
+
const session = await readCredential();
|
|
196
|
+
if (!session?.refreshToken) {
|
|
197
|
+
throw new Error('OML CLI authentication is required. Run \'oml login\' first.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const lock = await acquireCredentialLock();
|
|
201
|
+
try {
|
|
202
|
+
const latest = await readCredential();
|
|
203
|
+
if (latest && Date.now() + SESSION_EXPIRATION_LEEWAY_MS < latest.expiresAtMs) {
|
|
204
|
+
return latest;
|
|
205
|
+
}
|
|
206
|
+
const source = latest ?? session;
|
|
207
|
+
const refreshed = await refreshSupabaseAccessToken(
|
|
208
|
+
resolveSupabaseUrl(),
|
|
209
|
+
resolveSupabaseAnonKey(),
|
|
210
|
+
source.refreshToken
|
|
211
|
+
);
|
|
212
|
+
const updatedSession: StoredCredential = {
|
|
213
|
+
accessToken: refreshed.access_token,
|
|
214
|
+
refreshToken: refreshed.refresh_token,
|
|
215
|
+
expiresAtMs: Date.now() + (refreshed.expires_in * 1000),
|
|
216
|
+
};
|
|
217
|
+
await writeCredential(updatedSession);
|
|
218
|
+
const profile = await readProfile();
|
|
219
|
+
if (profile) {
|
|
220
|
+
await writeProfile({
|
|
221
|
+
...profile,
|
|
222
|
+
email: refreshed.email ?? profile.email,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return updatedSession;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (isUnauthorizedError(error)) {
|
|
228
|
+
await deleteCredential();
|
|
229
|
+
await deleteProfile();
|
|
230
|
+
throw new Error('Authentication refresh failed because the stored credential was revoked. Run \'oml login\' again.');
|
|
231
|
+
}
|
|
232
|
+
throw new Error('Authentication refresh failed. Check your network connection or sign in again with \'oml login\'.');
|
|
233
|
+
} finally {
|
|
234
|
+
await lock.release();
|
|
143
235
|
}
|
|
144
|
-
const expiresIn = Math.max(0, Math.round((expiresAtMs - Date.now()) / 1000));
|
|
145
|
-
const updatedSession: StoredSession = {
|
|
146
|
-
...session,
|
|
147
|
-
accessToken,
|
|
148
|
-
refreshToken,
|
|
149
|
-
expiresIn,
|
|
150
|
-
signedInAt: new Date().toISOString(),
|
|
151
|
-
};
|
|
152
|
-
await writeSession(updatedSession);
|
|
153
236
|
}
|
|
237
|
+
}
|
|
154
238
|
|
|
155
|
-
|
|
156
|
-
|
|
239
|
+
async function readCredential(): Promise<StoredCredential | undefined> {
|
|
240
|
+
const [accessToken, refreshToken, expiresAtRaw] = await Promise.all([
|
|
241
|
+
keytar.getPassword(KEYCHAIN_SERVICE, ACCESS_TOKEN_KEY),
|
|
242
|
+
keytar.getPassword(KEYCHAIN_SERVICE, REFRESH_TOKEN_KEY),
|
|
243
|
+
keytar.getPassword(KEYCHAIN_SERVICE, EXPIRES_AT_KEY),
|
|
244
|
+
]);
|
|
245
|
+
const expiresAtMs = Number(expiresAtRaw ?? NaN);
|
|
246
|
+
if (!accessToken || !refreshToken || !Number.isFinite(expiresAtMs)) {
|
|
247
|
+
return undefined;
|
|
157
248
|
}
|
|
249
|
+
return { accessToken, refreshToken, expiresAtMs };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function writeCredential(credential: StoredCredential): Promise<void> {
|
|
253
|
+
await Promise.all([
|
|
254
|
+
keytar.setPassword(KEYCHAIN_SERVICE, ACCESS_TOKEN_KEY, credential.accessToken),
|
|
255
|
+
keytar.setPassword(KEYCHAIN_SERVICE, REFRESH_TOKEN_KEY, credential.refreshToken),
|
|
256
|
+
keytar.setPassword(KEYCHAIN_SERVICE, EXPIRES_AT_KEY, String(credential.expiresAtMs)),
|
|
257
|
+
]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function deleteCredential(): Promise<void> {
|
|
261
|
+
await Promise.all([
|
|
262
|
+
keytar.deletePassword(KEYCHAIN_SERVICE, ACCESS_TOKEN_KEY),
|
|
263
|
+
keytar.deletePassword(KEYCHAIN_SERVICE, REFRESH_TOKEN_KEY),
|
|
264
|
+
keytar.deletePassword(KEYCHAIN_SERVICE, EXPIRES_AT_KEY),
|
|
265
|
+
]);
|
|
158
266
|
}
|
|
159
267
|
|
|
160
|
-
async function
|
|
268
|
+
async function readProfile(): Promise<StoredProfile | undefined> {
|
|
161
269
|
try {
|
|
162
|
-
const content = await fs.readFile(
|
|
163
|
-
const data = JSON.parse(content) as Partial<
|
|
164
|
-
if (!data.
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
if (!data.accessToken) {
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
if (!data.refreshToken || !data.tokenType || typeof data.expiresIn !== 'number') {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
if (data.provider !== 'github') {
|
|
270
|
+
const content = await fs.readFile(PROFILE_PATH, 'utf-8');
|
|
271
|
+
const data = JSON.parse(content) as Partial<StoredProfile>;
|
|
272
|
+
if (!data.provider || !data.userId || !data.signedInAt) {
|
|
174
273
|
return undefined;
|
|
175
274
|
}
|
|
176
275
|
return {
|
|
@@ -179,34 +278,21 @@ async function readSession(): Promise<StoredSession | undefined> {
|
|
|
179
278
|
userLabel: data.userLabel,
|
|
180
279
|
email: data.email ?? null,
|
|
181
280
|
tier: data.tier,
|
|
182
|
-
|
|
183
|
-
refreshToken: data.refreshToken,
|
|
184
|
-
tokenType: data.tokenType,
|
|
185
|
-
expiresIn: data.expiresIn,
|
|
186
|
-
signedInAt: data.signedInAt
|
|
281
|
+
signedInAt: data.signedInAt,
|
|
187
282
|
};
|
|
188
283
|
} catch {
|
|
189
284
|
return undefined;
|
|
190
285
|
}
|
|
191
286
|
}
|
|
192
287
|
|
|
193
|
-
async function
|
|
194
|
-
|
|
195
|
-
await fs.
|
|
196
|
-
await fs.writeFile(sessionPath, `${JSON.stringify(session, null, 2)}\n`, 'utf-8');
|
|
197
|
-
if (warnAboutPlaintext) {
|
|
198
|
-
process.stderr.write(
|
|
199
|
-
chalk.yellow(
|
|
200
|
-
`[oml] Warning: credentials stored in plaintext at ${sessionPath}. ` +
|
|
201
|
-
`A system keychain is not available in this environment.\n`
|
|
202
|
-
)
|
|
203
|
-
);
|
|
204
|
-
}
|
|
288
|
+
async function writeProfile(profile: StoredProfile): Promise<void> {
|
|
289
|
+
await fs.mkdir(path.dirname(PROFILE_PATH), { recursive: true });
|
|
290
|
+
await fs.writeFile(PROFILE_PATH, `${JSON.stringify(profile, null, 2)}\n`, 'utf-8');
|
|
205
291
|
}
|
|
206
292
|
|
|
207
|
-
async function
|
|
293
|
+
async function deleteProfile(): Promise<void> {
|
|
208
294
|
try {
|
|
209
|
-
await fs.unlink(
|
|
295
|
+
await fs.unlink(PROFILE_PATH);
|
|
210
296
|
} catch (error) {
|
|
211
297
|
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
212
298
|
throw error;
|
|
@@ -214,11 +300,16 @@ async function deleteSession(): Promise<void> {
|
|
|
214
300
|
}
|
|
215
301
|
}
|
|
216
302
|
|
|
217
|
-
function
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
303
|
+
async function authenticateWithGitHub(): Promise<{
|
|
304
|
+
provider: Provider;
|
|
305
|
+
userId: string;
|
|
306
|
+
userLabel?: string;
|
|
307
|
+
email: string | null;
|
|
308
|
+
tier?: string;
|
|
309
|
+
accessToken: string;
|
|
310
|
+
refreshToken: string;
|
|
311
|
+
expiresAtMs: number;
|
|
312
|
+
}> {
|
|
222
313
|
const clientId = resolveClientId();
|
|
223
314
|
const params = new URLSearchParams({
|
|
224
315
|
client_id: clientId,
|
|
@@ -266,9 +357,7 @@ async function authenticateWithGitHub(): Promise<StoredSession> {
|
|
|
266
357
|
tier: platformSession.tier,
|
|
267
358
|
accessToken: platformSession.access_token,
|
|
268
359
|
refreshToken: platformSession.refresh_token,
|
|
269
|
-
|
|
270
|
-
expiresIn: platformSession.expires_in,
|
|
271
|
-
signedInAt: new Date().toISOString()
|
|
360
|
+
expiresAtMs: Date.now() + platformSession.expires_in * 1000,
|
|
272
361
|
};
|
|
273
362
|
}
|
|
274
363
|
|
|
@@ -323,6 +412,59 @@ async function pollForGitHubAccessToken(clientId: string, device: GitHubDeviceCo
|
|
|
323
412
|
}
|
|
324
413
|
}
|
|
325
414
|
|
|
415
|
+
async function acquireCredentialLock(): Promise<{ release: () => Promise<void> }> {
|
|
416
|
+
await fs.mkdir(path.dirname(LOCK_PATH), { recursive: true });
|
|
417
|
+
const startedAt = Date.now();
|
|
418
|
+
while (Date.now() - startedAt < LOCK_TIMEOUT_MS) {
|
|
419
|
+
try {
|
|
420
|
+
const handle = await fs.open(LOCK_PATH, 'wx');
|
|
421
|
+
return {
|
|
422
|
+
release: async () => {
|
|
423
|
+
await handle.close();
|
|
424
|
+
await fs.rm(LOCK_PATH, { force: true });
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
} catch (error) {
|
|
428
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
await delay(LOCK_POLL_INTERVAL_MS);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
throw new Error('Timed out waiting for the CLI credential lock.');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function listActiveServers(): Promise<Array<{ pid: number; port: number; workspaceRoot?: string }>> {
|
|
438
|
+
const baseDir = path.join(os.homedir(), '.oml', 'workspaces');
|
|
439
|
+
try {
|
|
440
|
+
const workspaceDirs = await fs.readdir(baseDir);
|
|
441
|
+
const active: Array<{ pid: number; port: number; workspaceRoot?: string }> = [];
|
|
442
|
+
for (const workspaceDir of workspaceDirs) {
|
|
443
|
+
const lockFile = path.join(baseDir, workspaceDir, 'server.lock');
|
|
444
|
+
try {
|
|
445
|
+
const raw = await fs.readFile(lockFile, 'utf-8');
|
|
446
|
+
const parsed = JSON.parse(raw) as { pid?: unknown; port?: unknown; workspaceRoot?: unknown };
|
|
447
|
+
const pid = Number(parsed.pid);
|
|
448
|
+
const port = Number(parsed.port);
|
|
449
|
+
if (!Number.isFinite(pid) || !Number.isFinite(port)) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
process.kill(Math.trunc(pid), 0);
|
|
453
|
+
active.push({
|
|
454
|
+
pid: Math.trunc(pid),
|
|
455
|
+
port: Math.trunc(port),
|
|
456
|
+
workspaceRoot: typeof parsed.workspaceRoot === 'string' ? parsed.workspaceRoot : undefined,
|
|
457
|
+
});
|
|
458
|
+
} catch {
|
|
459
|
+
// ignore malformed or stale lock entries
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return active;
|
|
463
|
+
} catch {
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
326
468
|
function resolveClientId(): string {
|
|
327
469
|
const configured = process.env.OML_AUTH_GITHUB_CLIENT_ID?.trim() || DEFAULT_GITHUB_CLIENT_ID;
|
|
328
470
|
if (configured) {
|
|
@@ -349,6 +491,11 @@ function delay(ms: number): Promise<void> {
|
|
|
349
491
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
350
492
|
}
|
|
351
493
|
|
|
494
|
+
function isUnauthorizedError(error: unknown): boolean {
|
|
495
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
496
|
+
return /\b401\b/.test(message);
|
|
497
|
+
}
|
|
498
|
+
|
|
352
499
|
type GitHubDeviceCodeResponse = {
|
|
353
500
|
device_code?: string;
|
|
354
501
|
user_code?: string;
|
|
@@ -363,6 +510,5 @@ type GitHubTokenResponse = {
|
|
|
363
510
|
};
|
|
364
511
|
|
|
365
512
|
type GitHubUserResponse = {
|
|
366
|
-
id?: number;
|
|
367
513
|
login?: string;
|
|
368
514
|
};
|
package/src/auth/platform.ts
CHANGED
|
@@ -15,6 +15,7 @@ import chalk from 'chalk';
|
|
|
15
15
|
import { DEFAULT_API_BASE_URL } from './constants.js';
|
|
16
16
|
import { OmlCliAuthService } from './auth.js';
|
|
17
17
|
const API_BASE_URL_ENV = 'OML_PLATFORM_API_URL';
|
|
18
|
+
const API_KEY_ENV = 'OML_PLATFORM_API_KEY';
|
|
18
19
|
|
|
19
20
|
let client: OmlClient | null = null;
|
|
20
21
|
let shutdownHandle: NodeShutdownHandle | null = null;
|
|
@@ -34,22 +35,22 @@ export async function initializePlatform(
|
|
|
34
35
|
|
|
35
36
|
const resolvedApiBaseUrl = process.env[API_BASE_URL_ENV]?.trim() || apiBaseUrl;
|
|
36
37
|
|
|
38
|
+
const apiKey = process.env[API_KEY_ENV]?.trim();
|
|
37
39
|
const config: OmlClientConfig = {
|
|
38
40
|
apiBaseUrl: resolvedApiBaseUrl,
|
|
39
41
|
tool: 'oml-cli',
|
|
40
42
|
storage: new FileStorageAdapter(),
|
|
41
|
-
auth:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
},
|
|
43
|
+
auth: apiKey
|
|
44
|
+
? {
|
|
45
|
+
method: 'oauth',
|
|
46
|
+
getToken: async () => apiKey,
|
|
47
|
+
refreshToken: async () => apiKey,
|
|
48
|
+
}
|
|
49
|
+
: {
|
|
50
|
+
method: 'oauth',
|
|
51
|
+
getToken: () => authService.getAccessToken(),
|
|
52
|
+
refreshToken: () => authService.refreshAccessToken(),
|
|
53
|
+
},
|
|
53
54
|
onAuthError: (error) => {
|
|
54
55
|
console.error(chalk.red(
|
|
55
56
|
`OML Platform: authentication error — ${toGenericPlatformErrorMessage(error)}`
|
|
@@ -70,7 +71,7 @@ export async function initializePlatform(
|
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
73
|
* Dispose the platform client. Flushes any buffered telemetry
|
|
73
|
-
* and
|
|
74
|
+
* and flushes buffered telemetry. Call at the end of CLI execution.
|
|
74
75
|
*/
|
|
75
76
|
export async function disposePlatform(): Promise<void> {
|
|
76
77
|
if (shutdownHandle) {
|