@oml/cli 0.14.17 → 0.15.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/out/auth/auth.d.ts +10 -0
- package/out/auth/auth.js +225 -72
- package/out/auth/auth.js.map +1 -1
- package/out/cli.js +15 -18
- package/out/cli.js.map +1 -1
- package/out/commands/export.d.ts +0 -1
- package/out/commands/export.js +0 -1
- package/out/commands/export.js.map +1 -1
- package/out/commands/lint.js +2 -1
- package/out/commands/lint.js.map +1 -1
- package/out/commands/reason.d.ts +0 -1
- package/out/commands/reason.js +20 -7
- package/out/commands/reason.js.map +1 -1
- package/out/commands/render.d.ts +0 -1
- package/out/commands/render.js.map +1 -1
- package/out/commands/server/actions.d.ts +8 -0
- package/out/commands/server/actions.js +33 -9
- package/out/commands/server/actions.js.map +1 -1
- package/out/commands/validate.js +9 -6
- package/out/commands/validate.js.map +1 -1
- package/package.json +6 -4
- package/src/auth/auth.ts +238 -88
- package/src/cli.ts +22 -27
- package/src/commands/export.ts +0 -2
- package/src/commands/lint.ts +2 -1
- package/src/commands/reason.ts +21 -10
- package/src/commands/render.ts +0 -1
- package/src/commands/server/actions.ts +44 -9
- package/src/commands/validate.ts +8 -6
package/src/auth/auth.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import { exchangeGitHubToken, refreshSupabaseAccessToken } from '@oml/platform';
|
|
3
|
+
import { exchangeGitHubToken, refreshSupabaseAccessToken, verifyEntitlementsToken } from '@oml/platform';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
+
import * as crypto from 'node:crypto';
|
|
6
|
+
import type { JsonWebKey } from 'node:crypto';
|
|
5
7
|
import * as fs from 'node:fs/promises';
|
|
6
8
|
import * as os from 'node:os';
|
|
7
9
|
import * as path from 'node:path';
|
|
@@ -23,7 +25,10 @@ const KEYCHAIN_SERVICE = 'oml-code';
|
|
|
23
25
|
const ACCESS_TOKEN_KEY = 'oml.cli.access_token';
|
|
24
26
|
const REFRESH_TOKEN_KEY = 'oml.cli.refresh_token';
|
|
25
27
|
const EXPIRES_AT_KEY = 'oml.cli.expires_at';
|
|
26
|
-
const
|
|
28
|
+
const ENTITLEMENT_EXPIRY_KEY = 'oml.cli.entitlement_expiry';
|
|
29
|
+
const ENTITLEMENT_FEATURES_KEY = 'oml.cli.entitlement_features';
|
|
30
|
+
const ENTITLEMENT_TOKEN_KEY = 'oml.cli.entitlement_token';
|
|
31
|
+
const ENTITLEMENT_PUBKEY_KEY = 'oml.cli.entitlement_pubkey';
|
|
27
32
|
const LOCK_PATH = path.join(os.homedir(), '.oml', 'credentials.lock');
|
|
28
33
|
const SESSION_EXPIRATION_LEEWAY_MS = 20_000;
|
|
29
34
|
const LOCK_TIMEOUT_MS = 5_000;
|
|
@@ -37,17 +42,6 @@ type KeytarModule = {
|
|
|
37
42
|
|
|
38
43
|
let keytarModulePromise: Promise<KeytarModule> | undefined;
|
|
39
44
|
|
|
40
|
-
type Provider = 'github';
|
|
41
|
-
|
|
42
|
-
type StoredProfile = {
|
|
43
|
-
provider: Provider;
|
|
44
|
-
userId: string;
|
|
45
|
-
userLabel?: string;
|
|
46
|
-
email: string | null;
|
|
47
|
-
tier?: string;
|
|
48
|
-
signedInAt: string;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
45
|
type StoredCredential = {
|
|
52
46
|
accessToken: string;
|
|
53
47
|
refreshToken: string;
|
|
@@ -77,10 +71,9 @@ export class OmlCliAuthService {
|
|
|
77
71
|
|
|
78
72
|
const existing = await this.tryGetValidSnapshot();
|
|
79
73
|
if (existing) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
));
|
|
74
|
+
const claims = decodeJwtClaims(existing.accessToken);
|
|
75
|
+
const label = claims.userLabel ?? claims.email ?? 'current user';
|
|
76
|
+
console.error(chalk.green(`Already signed in as ${label}.`));
|
|
84
77
|
return;
|
|
85
78
|
}
|
|
86
79
|
|
|
@@ -90,22 +83,14 @@ export class OmlCliAuthService {
|
|
|
90
83
|
refreshToken: session.refreshToken,
|
|
91
84
|
expiresAtMs: session.expiresAtMs,
|
|
92
85
|
});
|
|
93
|
-
|
|
94
|
-
provider: session.provider,
|
|
95
|
-
userId: session.userId,
|
|
96
|
-
userLabel: session.userLabel,
|
|
97
|
-
email: session.email,
|
|
98
|
-
tier: session.tier,
|
|
99
|
-
signedInAt: new Date().toISOString(),
|
|
100
|
-
});
|
|
86
|
+
void fetchAndSaveEntitlementsPubkey(resolveApiBaseUrl(), this).catch(() => undefined);
|
|
101
87
|
const summary = session.userLabel ?? session.email ?? 'signed-in user';
|
|
102
|
-
console.error(chalk.green(`Signed in as ${summary} via
|
|
88
|
+
console.error(chalk.green(`Signed in as ${summary} via GitHub.`));
|
|
103
89
|
}
|
|
104
90
|
|
|
105
91
|
async logout(): Promise<void> {
|
|
106
92
|
const activeServers = await listActiveServers();
|
|
107
93
|
await deleteCredential();
|
|
108
|
-
await deleteProfile();
|
|
109
94
|
if (activeServers.length > 0) {
|
|
110
95
|
console.error(chalk.yellow('Stored OAuth credentials were cleared. Running servers were not stopped:'));
|
|
111
96
|
for (const server of activeServers) {
|
|
@@ -127,27 +112,85 @@ export class OmlCliAuthService {
|
|
|
127
112
|
if (apiKey) {
|
|
128
113
|
console.error('Auth mode: api_key');
|
|
129
114
|
console.error('Account: resolved by OML Platform from OML_PLATFORM_API_KEY');
|
|
130
|
-
const profile = await readProfile();
|
|
131
|
-
if (profile) {
|
|
132
|
-
console.error(`Stored OAuth user: ${profile.userLabel ?? profile.email ?? profile.userId}`);
|
|
133
|
-
}
|
|
134
115
|
return;
|
|
135
116
|
}
|
|
136
117
|
|
|
137
118
|
const credential = await readCredential();
|
|
138
|
-
|
|
139
|
-
if (!credential || !profile) {
|
|
119
|
+
if (!credential) {
|
|
140
120
|
console.error(chalk.yellow('Not signed in.'));
|
|
141
121
|
return;
|
|
142
122
|
}
|
|
123
|
+
const claims = decodeJwtClaims(credential.accessToken);
|
|
143
124
|
console.error('Auth mode: oauth');
|
|
144
|
-
console.error(`
|
|
145
|
-
console.error(`User
|
|
146
|
-
console.error(`
|
|
147
|
-
console.error(`
|
|
148
|
-
console.error(`
|
|
149
|
-
|
|
150
|
-
|
|
125
|
+
console.error(`User ID: ${claims.userId ?? '(unknown)'}`);
|
|
126
|
+
console.error(`User label: ${claims.userLabel ?? '(not set)'}`);
|
|
127
|
+
console.error(`Email: ${claims.email ?? '(not set)'}`);
|
|
128
|
+
console.error(`Tier: ${claims.tier ?? '(not set)'}`);
|
|
129
|
+
console.error(`Token expires at: ${new Date(credential.expiresAtMs).toISOString()}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getDeviceId(): Promise<string> {
|
|
133
|
+
return getOrCreateDeviceId();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async getEntitlementCache(): Promise<{ expiry: number; featureIds: string[]; token?: string } | null> {
|
|
137
|
+
try {
|
|
138
|
+
const keytar = await getKeytarModule();
|
|
139
|
+
const [expiryRaw, featuresRaw, tokenRaw, pubkeyRaw] = await Promise.all([
|
|
140
|
+
keytar.getPassword(KEYCHAIN_SERVICE, ENTITLEMENT_EXPIRY_KEY),
|
|
141
|
+
keytar.getPassword(KEYCHAIN_SERVICE, ENTITLEMENT_FEATURES_KEY),
|
|
142
|
+
keytar.getPassword(KEYCHAIN_SERVICE, ENTITLEMENT_TOKEN_KEY),
|
|
143
|
+
keytar.getPassword(KEYCHAIN_SERVICE, ENTITLEMENT_PUBKEY_KEY),
|
|
144
|
+
]);
|
|
145
|
+
const expiry = Number(expiryRaw ?? NaN);
|
|
146
|
+
if (!Number.isFinite(expiry) || expiry <= Date.now() || !featuresRaw) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const featureIds = JSON.parse(featuresRaw) as unknown;
|
|
150
|
+
if (!Array.isArray(featureIds) || !featureIds.every((x) => typeof x === 'string')) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
let token: string | undefined;
|
|
154
|
+
if (tokenRaw && pubkeyRaw) {
|
|
155
|
+
try {
|
|
156
|
+
const deviceId = await getOrCreateDeviceId();
|
|
157
|
+
const valid = await verifyEntitlementsToken(tokenRaw, deviceId, JSON.parse(pubkeyRaw) as JsonWebKey);
|
|
158
|
+
if (valid) {
|
|
159
|
+
token = tokenRaw;
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// Verification failed — proceed without token; gate will re-fetch.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { expiry, featureIds: featureIds as string[], token };
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async saveEntitlementCache(expiry: number, featureIds: string[], token?: string): Promise<void> {
|
|
172
|
+
try {
|
|
173
|
+
const keytar = await getKeytarModule();
|
|
174
|
+
const writes: Promise<void>[] = [
|
|
175
|
+
keytar.setPassword(KEYCHAIN_SERVICE, ENTITLEMENT_EXPIRY_KEY, String(expiry)),
|
|
176
|
+
keytar.setPassword(KEYCHAIN_SERVICE, ENTITLEMENT_FEATURES_KEY, JSON.stringify(featureIds)),
|
|
177
|
+
];
|
|
178
|
+
if (token) {
|
|
179
|
+
writes.push(keytar.setPassword(KEYCHAIN_SERVICE, ENTITLEMENT_TOKEN_KEY, token));
|
|
180
|
+
}
|
|
181
|
+
await Promise.all(writes);
|
|
182
|
+
} catch {
|
|
183
|
+
// Credential store unavailable — skip silently.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async saveEntitlementsPubkey(pubkeyJwk: JsonWebKey): Promise<void> {
|
|
188
|
+
try {
|
|
189
|
+
const keytar = await getKeytarModule();
|
|
190
|
+
await keytar.setPassword(KEYCHAIN_SERVICE, ENTITLEMENT_PUBKEY_KEY, JSON.stringify(pubkeyJwk));
|
|
191
|
+
} catch {
|
|
192
|
+
// Credential store unavailable — skip silently.
|
|
193
|
+
}
|
|
151
194
|
}
|
|
152
195
|
|
|
153
196
|
async ensureAuthenticated(operationName: string): Promise<void> {
|
|
@@ -198,6 +241,10 @@ export class OmlCliAuthService {
|
|
|
198
241
|
}
|
|
199
242
|
}
|
|
200
243
|
|
|
244
|
+
async hasStoredCredential(): Promise<boolean> {
|
|
245
|
+
return (await readCredential()) !== undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
201
248
|
private async refreshCredential(): Promise<StoredCredential> {
|
|
202
249
|
const session = await readCredential();
|
|
203
250
|
if (!session?.refreshToken) {
|
|
@@ -222,18 +269,10 @@ export class OmlCliAuthService {
|
|
|
222
269
|
expiresAtMs: Date.now() + (refreshed.expires_in * 1000),
|
|
223
270
|
};
|
|
224
271
|
await writeCredential(updatedSession);
|
|
225
|
-
const profile = await readProfile();
|
|
226
|
-
if (profile) {
|
|
227
|
-
await writeProfile({
|
|
228
|
-
...profile,
|
|
229
|
-
email: refreshed.email ?? profile.email,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
272
|
return updatedSession;
|
|
233
273
|
} catch (error) {
|
|
234
274
|
if (isUnauthorizedError(error)) {
|
|
235
275
|
await deleteCredential();
|
|
236
|
-
await deleteProfile();
|
|
237
276
|
throw new Error('Authentication refresh failed because the stored credential was revoked. Run \'oml login\' again.');
|
|
238
277
|
}
|
|
239
278
|
throw new Error('Authentication refresh failed. Check your network connection or sign in again with \'oml login\'.');
|
|
@@ -272,50 +311,40 @@ async function deleteCredential(): Promise<void> {
|
|
|
272
311
|
keytar.deletePassword(KEYCHAIN_SERVICE, ACCESS_TOKEN_KEY),
|
|
273
312
|
keytar.deletePassword(KEYCHAIN_SERVICE, REFRESH_TOKEN_KEY),
|
|
274
313
|
keytar.deletePassword(KEYCHAIN_SERVICE, EXPIRES_AT_KEY),
|
|
314
|
+
keytar.deletePassword(KEYCHAIN_SERVICE, ENTITLEMENT_EXPIRY_KEY),
|
|
315
|
+
keytar.deletePassword(KEYCHAIN_SERVICE, ENTITLEMENT_FEATURES_KEY),
|
|
316
|
+
keytar.deletePassword(KEYCHAIN_SERVICE, ENTITLEMENT_TOKEN_KEY),
|
|
317
|
+
keytar.deletePassword(KEYCHAIN_SERVICE, ENTITLEMENT_PUBKEY_KEY),
|
|
275
318
|
]);
|
|
276
319
|
}
|
|
277
320
|
|
|
278
|
-
async function
|
|
321
|
+
async function getOrCreateDeviceId(): Promise<string> {
|
|
279
322
|
try {
|
|
280
|
-
|
|
281
|
-
const data = JSON.parse(content) as Partial<StoredProfile>;
|
|
282
|
-
if (!data.provider || !data.userId || !data.signedInAt) {
|
|
283
|
-
return undefined;
|
|
284
|
-
}
|
|
285
|
-
return {
|
|
286
|
-
provider: data.provider,
|
|
287
|
-
userId: data.userId,
|
|
288
|
-
userLabel: data.userLabel,
|
|
289
|
-
email: data.email ?? null,
|
|
290
|
-
tier: data.tier,
|
|
291
|
-
signedInAt: data.signedInAt,
|
|
292
|
-
};
|
|
323
|
+
return (await fs.readFile('/etc/machine-id', 'utf-8')).trim();
|
|
293
324
|
} catch {
|
|
294
|
-
|
|
325
|
+
try {
|
|
326
|
+
return (await fs.readFile(LOCAL_MACHINE_ID_PATH, 'utf-8')).trim();
|
|
327
|
+
} catch {
|
|
328
|
+
const id = crypto.randomUUID();
|
|
329
|
+
await fs.mkdir(path.dirname(LOCAL_MACHINE_ID_PATH), { recursive: true });
|
|
330
|
+
await fs.writeFile(LOCAL_MACHINE_ID_PATH, id, { encoding: 'utf-8', mode: 0o600 });
|
|
331
|
+
return id;
|
|
332
|
+
}
|
|
295
333
|
}
|
|
296
334
|
}
|
|
297
335
|
|
|
298
|
-
async function
|
|
299
|
-
await
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
async function deleteProfile(): Promise<void> {
|
|
304
|
-
try {
|
|
305
|
-
await fs.unlink(PROFILE_PATH);
|
|
306
|
-
} catch (error) {
|
|
307
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
308
|
-
throw error;
|
|
309
|
-
}
|
|
336
|
+
async function fetchAndSaveEntitlementsPubkey(apiBaseUrl: string, authService: OmlCliAuthService): Promise<void> {
|
|
337
|
+
const response = await fetch(`${apiBaseUrl}/entitlements/pubkey`);
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
return;
|
|
310
340
|
}
|
|
341
|
+
const jwk = await response.json() as JsonWebKey;
|
|
342
|
+
await authService.saveEntitlementsPubkey(jwk);
|
|
311
343
|
}
|
|
312
344
|
|
|
313
345
|
async function authenticateWithGitHub(): Promise<{
|
|
314
|
-
provider: Provider;
|
|
315
|
-
userId: string;
|
|
316
346
|
userLabel?: string;
|
|
317
347
|
email: string | null;
|
|
318
|
-
tier?: string;
|
|
319
348
|
accessToken: string;
|
|
320
349
|
refreshToken: string;
|
|
321
350
|
expiresAtMs: number;
|
|
@@ -360,11 +389,8 @@ async function authenticateWithGitHub(): Promise<{
|
|
|
360
389
|
const platformSession = await exchangeGitHubToken(resolveApiBaseUrl(), token);
|
|
361
390
|
|
|
362
391
|
return {
|
|
363
|
-
provider: 'github',
|
|
364
|
-
userId: platformSession.user_id,
|
|
365
392
|
userLabel: user.login,
|
|
366
393
|
email: platformSession.email,
|
|
367
|
-
tier: platformSession.tier,
|
|
368
394
|
accessToken: platformSession.access_token,
|
|
369
395
|
refreshToken: platformSession.refresh_token,
|
|
370
396
|
expiresAtMs: Date.now() + platformSession.expires_in * 1000,
|
|
@@ -506,25 +532,149 @@ function isUnauthorizedError(error: unknown): boolean {
|
|
|
506
532
|
return /\b401\b/.test(message);
|
|
507
533
|
}
|
|
508
534
|
|
|
535
|
+
const CREDENTIALS_FILE_PATH = path.join(os.homedir(), '.oml', 'credentials.json');
|
|
536
|
+
const LOCAL_MACHINE_ID_PATH = path.join(os.homedir(), '.oml', 'machine-id');
|
|
537
|
+
|
|
538
|
+
// Derive a 256-bit encryption key bound to this machine and user.
|
|
539
|
+
// Uses /etc/machine-id (Linux) or a locally generated UUID as the key material,
|
|
540
|
+
// combined with the user's home directory so the key is user-specific too.
|
|
541
|
+
async function deriveMachineKey(): Promise<Buffer> {
|
|
542
|
+
const machineId = await getOrCreateDeviceId();
|
|
543
|
+
const ikm = Buffer.from(`${machineId}:${os.homedir()}`, 'utf-8');
|
|
544
|
+
return new Promise<Buffer>((resolve, reject) => {
|
|
545
|
+
crypto.hkdf('sha256', ikm, Buffer.alloc(0), 'oml-cli-credentials-v1', 32, (err, key) => {
|
|
546
|
+
if (err) { reject(err); } else { resolve(Buffer.from(key)); }
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function encryptValue(value: string, key: Buffer): string {
|
|
552
|
+
const iv = crypto.randomBytes(12);
|
|
553
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
554
|
+
const encrypted = Buffer.concat([cipher.update(value, 'utf-8'), cipher.final()]);
|
|
555
|
+
const tag = cipher.getAuthTag();
|
|
556
|
+
return JSON.stringify({
|
|
557
|
+
v: 1,
|
|
558
|
+
iv: iv.toString('base64'),
|
|
559
|
+
tag: tag.toString('base64'),
|
|
560
|
+
data: encrypted.toString('base64'),
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function decryptValue(stored: string, key: Buffer): string {
|
|
565
|
+
const parsed = JSON.parse(stored) as { v?: number; iv: string; tag: string; data: string };
|
|
566
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(parsed.iv, 'base64'));
|
|
567
|
+
decipher.setAuthTag(Buffer.from(parsed.tag, 'base64'));
|
|
568
|
+
return decipher.update(Buffer.from(parsed.data, 'base64'), undefined, 'utf-8') + decipher.final('utf-8');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
class FileKeytar implements KeytarModule {
|
|
572
|
+
private keyPromise: Promise<Buffer> | undefined;
|
|
573
|
+
|
|
574
|
+
private getKey(): Promise<Buffer> {
|
|
575
|
+
if (!this.keyPromise) {
|
|
576
|
+
this.keyPromise = deriveMachineKey();
|
|
577
|
+
}
|
|
578
|
+
return this.keyPromise;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private async read(): Promise<Record<string, string>> {
|
|
582
|
+
try {
|
|
583
|
+
const content = await fs.readFile(CREDENTIALS_FILE_PATH, 'utf-8');
|
|
584
|
+
return JSON.parse(content) as Record<string, string>;
|
|
585
|
+
} catch {
|
|
586
|
+
return {};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private async write(store: Record<string, string>): Promise<void> {
|
|
591
|
+
await fs.mkdir(path.dirname(CREDENTIALS_FILE_PATH), { recursive: true });
|
|
592
|
+
await fs.writeFile(CREDENTIALS_FILE_PATH, `${JSON.stringify(store, null, 2)}\n`, { encoding: 'utf-8', mode: 0o600 });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private storeKey(service: string, account: string): string {
|
|
596
|
+
return `${service}:${account}`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
async getPassword(service: string, account: string): Promise<string | null> {
|
|
600
|
+
const store = await this.read();
|
|
601
|
+
const raw = store[this.storeKey(service, account)];
|
|
602
|
+
if (raw === undefined || raw === null) {
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const key = await this.getKey();
|
|
607
|
+
return decryptValue(raw, key);
|
|
608
|
+
} catch {
|
|
609
|
+
// Unreadable entry (wrong machine, corrupt, or legacy plaintext) — treat as missing.
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async setPassword(service: string, account: string, password: string): Promise<void> {
|
|
615
|
+
const [store, key] = await Promise.all([this.read(), this.getKey()]);
|
|
616
|
+
store[this.storeKey(service, account)] = encryptValue(password, key);
|
|
617
|
+
await this.write(store);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async deletePassword(service: string, account: string): Promise<boolean> {
|
|
621
|
+
const store = await this.read();
|
|
622
|
+
const k = this.storeKey(service, account);
|
|
623
|
+
if (!(k in store)) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
delete store[k];
|
|
627
|
+
await this.write(store);
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
509
632
|
async function getKeytarModule(): Promise<KeytarModule> {
|
|
510
633
|
if (!keytarModulePromise) {
|
|
511
634
|
keytarModulePromise = import('keytar')
|
|
512
635
|
.then((loaded) => loaded.default as KeytarModule)
|
|
513
636
|
.catch((error: unknown) => {
|
|
637
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
638
|
+
if (/libsecret|dlopen|ERR_DLOPEN_FAILED/i.test(message)) {
|
|
639
|
+
console.error(chalk.yellow(
|
|
640
|
+
'System keychain unavailable; credentials will be stored encrypted at ' +
|
|
641
|
+
CREDENTIALS_FILE_PATH + ' using a machine-bound key. ' +
|
|
642
|
+
'Set OML_PLATFORM_API_KEY for non-interactive environments.'
|
|
643
|
+
));
|
|
644
|
+
return new FileKeytar();
|
|
645
|
+
}
|
|
514
646
|
keytarModulePromise = undefined;
|
|
515
|
-
throw new Error(
|
|
647
|
+
throw new Error(`Secure credential storage is unavailable: ${message}`);
|
|
516
648
|
});
|
|
517
649
|
}
|
|
518
650
|
return keytarModulePromise;
|
|
519
651
|
}
|
|
520
652
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
653
|
+
type JwtClaims = {
|
|
654
|
+
userId?: string;
|
|
655
|
+
userLabel?: string;
|
|
656
|
+
email?: string;
|
|
657
|
+
tier?: string;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
function decodeJwtClaims(token: string): JwtClaims {
|
|
661
|
+
try {
|
|
662
|
+
const payload = token.split('.')[1];
|
|
663
|
+
if (!payload) { return {}; }
|
|
664
|
+
const json = JSON.parse(Buffer.from(payload.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf-8')) as Record<string, unknown>;
|
|
665
|
+
const userMeta = json['user_metadata'];
|
|
666
|
+
const appMeta = json['app_metadata'];
|
|
667
|
+
return {
|
|
668
|
+
userId: typeof json['sub'] === 'string' ? json['sub'] : undefined,
|
|
669
|
+
email: typeof json['email'] === 'string' ? json['email'] : undefined,
|
|
670
|
+
userLabel: userMeta && typeof (userMeta as Record<string, unknown>)['user_name'] === 'string'
|
|
671
|
+
? (userMeta as Record<string, unknown>)['user_name'] as string : undefined,
|
|
672
|
+
tier: appMeta && typeof (appMeta as Record<string, unknown>)['tier'] === 'string'
|
|
673
|
+
? (appMeta as Record<string, unknown>)['tier'] as string : undefined,
|
|
674
|
+
};
|
|
675
|
+
} catch {
|
|
676
|
+
return {};
|
|
526
677
|
}
|
|
527
|
-
return `Secure credential storage is unavailable: ${message}`;
|
|
528
678
|
}
|
|
529
679
|
|
|
530
680
|
type GitHubDeviceCodeResponse = {
|
package/src/cli.ts
CHANGED
|
@@ -28,20 +28,20 @@ export interface CliCommandInfo {
|
|
|
28
28
|
export function getWorkspaceCommands(): CliCommandInfo[] {
|
|
29
29
|
return [
|
|
30
30
|
{ name: 'lint', description: 'lints OML files and prints any syntax or validation errors' },
|
|
31
|
-
{
|
|
32
|
-
name: 'render [options]',
|
|
31
|
+
{
|
|
32
|
+
name: 'render [options]',
|
|
33
33
|
description: 'lint the workspace, then render markdown files to static html',
|
|
34
|
-
usage: 'render -m <input-folder> -b <output-folder> [
|
|
34
|
+
usage: 'render -m <input-folder> -b <output-folder> [-c <ontology-iri>]'
|
|
35
35
|
},
|
|
36
|
-
{
|
|
37
|
-
name: 'export [options]',
|
|
36
|
+
{
|
|
37
|
+
name: 'export [options]',
|
|
38
38
|
description: 'export asserted OWL files (no reasoning)',
|
|
39
|
-
usage: 'export [-o <dir>] [-f <ext>] [--
|
|
39
|
+
usage: 'export [-o <dir>] [-f <ext>] [--pretty]'
|
|
40
40
|
},
|
|
41
|
-
{
|
|
42
|
-
name: 'reason [options]',
|
|
41
|
+
{
|
|
42
|
+
name: 'reason [options]',
|
|
43
43
|
description: 'run workspace consistency checks (or persist assertions/entailments with --owl)',
|
|
44
|
-
usage: 'reason [-o <dir>] [-f <ext>] [--
|
|
44
|
+
usage: 'reason [-o <dir>] [-f <ext>] [--pretty] [-e <true|false>]'
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
name: 'validate [options]',
|
|
@@ -94,14 +94,20 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
94
94
|
.option('-w, --workspace <workspace>', 'workspace root used by REST facade initialize (default: cwd)')
|
|
95
95
|
.description('start an OML server')
|
|
96
96
|
.action(async (port: string | undefined, options: { port?: string; workspace?: string }) => {
|
|
97
|
+
const [entitlementCache, deviceId] = await Promise.all([
|
|
98
|
+
authService.getEntitlementCache().then((c) => c ?? undefined),
|
|
99
|
+
authService.getDeviceId().catch(() => undefined),
|
|
100
|
+
]);
|
|
97
101
|
if (process.env.OML_PLATFORM_API_KEY?.trim()) {
|
|
98
|
-
await serverStartAction(port, { ...options, auth: await resolveServerStartAuth() });
|
|
102
|
+
await serverStartAction(port, { ...options, auth: await resolveServerStartAuth(), entitlementCache });
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
101
105
|
await serverRunAction(port, {
|
|
102
106
|
...options,
|
|
103
107
|
authService,
|
|
104
108
|
auth: await resolveServerRunAuth(authService),
|
|
109
|
+
entitlementCache,
|
|
110
|
+
deviceId,
|
|
105
111
|
});
|
|
106
112
|
});
|
|
107
113
|
|
|
@@ -148,7 +154,6 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
148
154
|
.command('render')
|
|
149
155
|
.requiredOption('-m, --md <input-folder>', 'folder containing markdown files to render')
|
|
150
156
|
.requiredOption('-b, --web <output-folder>', 'folder where rendered static site files are written')
|
|
151
|
-
.option('--clean', 'remove output folder before render')
|
|
152
157
|
.option('-c, --context <model-path>', 'workspace-relative .oml model path used as default navigation context for wikilinks')
|
|
153
158
|
.description('lint the workspace, then render markdown files to static html')
|
|
154
159
|
.action(async (...args: unknown[]) => {
|
|
@@ -170,7 +175,6 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
170
175
|
.command('export')
|
|
171
176
|
.option('-o, --owl <dir>', 'folder where RDF output files are written')
|
|
172
177
|
.option('-f, --format <ext>', 'RDF format extension: ttl, trig, nt, nq, or n3', 'ttl')
|
|
173
|
-
.option('--clean', 'remove output folder before export')
|
|
174
178
|
.option('--pretty', 'pretty-print Turtle/TriG output with blank lines between top-level blocks')
|
|
175
179
|
.description('export asserted OWL files (no reasoning)')
|
|
176
180
|
.action(async (...args: unknown[]) => {
|
|
@@ -192,7 +196,6 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
192
196
|
.command('reason')
|
|
193
197
|
.option('-o, --owl <dir>', 'persist assertions/entailments to folder (default: check-only with no persistence)')
|
|
194
198
|
.option('-f, --format <ext>', 'RDF format extension: ttl, trig, nt, nq, or n3', 'ttl')
|
|
195
|
-
.option('--clean', 'remove output folder before reason (only when --owl is provided)')
|
|
196
199
|
.option('--pretty', 'pretty-print Turtle/TriG output with blank lines between top-level blocks (only when --owl is provided)')
|
|
197
200
|
.option('-u, --unique-names-assumption [value]', 'enable or disable the unique names assumption', parseBooleanOption, true)
|
|
198
201
|
.option('-e, --explanation [value]', 'enable or disable inconsistency explanations', parseBooleanOption, false)
|
|
@@ -322,29 +325,21 @@ async function resolveServerStartAuth(): Promise<{ accessToken: string }> {
|
|
|
322
325
|
const apiKey = process.env.OML_PLATFORM_API_KEY?.trim();
|
|
323
326
|
if (!apiKey) {
|
|
324
327
|
throw new CliExitError(
|
|
325
|
-
'OML_PLATFORM_API_KEY is
|
|
326
|
-
'
|
|
328
|
+
'OML_PLATFORM_API_KEY is required for non-interactive server start. ' +
|
|
329
|
+
'Unset it and run \'oml start\' again to use interactive OAuth login instead.'
|
|
327
330
|
);
|
|
328
331
|
}
|
|
329
332
|
return { accessToken: apiKey };
|
|
330
333
|
}
|
|
331
334
|
|
|
332
|
-
async function resolveServerRunAuth(authService: OmlCliAuthService): Promise<{
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
let snapshot;
|
|
336
|
-
try {
|
|
337
|
-
snapshot = await authService.getServerAuthSnapshot();
|
|
338
|
-
} catch {
|
|
335
|
+
async function resolveServerRunAuth(authService: OmlCliAuthService): Promise<{ accessToken: string }> {
|
|
336
|
+
if (!(await authService.hasStoredCredential())) {
|
|
337
|
+
console.error(chalk.cyan('Authentication required to start the server. Signing in...'));
|
|
339
338
|
await authService.login({});
|
|
340
|
-
snapshot = await authService.getServerAuthSnapshot();
|
|
341
339
|
}
|
|
342
|
-
return {
|
|
343
|
-
accessToken: snapshot.accessToken,
|
|
344
|
-
};
|
|
340
|
+
return { accessToken: (await authService.getServerAuthSnapshot()).accessToken };
|
|
345
341
|
}
|
|
346
342
|
|
|
347
|
-
|
|
348
343
|
function formatDetailedError(error: unknown): string {
|
|
349
344
|
if (!(error instanceof Error)) {
|
|
350
345
|
return String(error);
|
package/src/commands/export.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { restPost } from './server/rest.js';
|
|
|
10
10
|
export type ExportOptions = {
|
|
11
11
|
owl?: string;
|
|
12
12
|
format?: 'ttl' | 'trig' | 'nt' | 'nq' | 'n3' | string;
|
|
13
|
-
clean?: boolean;
|
|
14
13
|
pretty?: boolean;
|
|
15
14
|
authToken?: string;
|
|
16
15
|
};
|
|
@@ -44,7 +43,6 @@ export const exportAction = async (opts: ExportOptions): Promise<void> => {
|
|
|
44
43
|
{
|
|
45
44
|
owl: opts.owl,
|
|
46
45
|
format,
|
|
47
|
-
clean: opts.clean,
|
|
48
46
|
pretty: opts.pretty === true,
|
|
49
47
|
} as Record<string, unknown>,
|
|
50
48
|
opts.authToken,
|
package/src/commands/lint.ts
CHANGED
|
@@ -71,7 +71,8 @@ export const lintAction = async (opts: LintOptions): Promise<void> => {
|
|
|
71
71
|
failCli(chalk.red(formatLintSummary(result, elapsedMs)));
|
|
72
72
|
}
|
|
73
73
|
if (result.warnings > 0) {
|
|
74
|
-
|
|
74
|
+
console.warn(chalk.yellow(formatLintSummary(result, elapsedMs)));
|
|
75
|
+
return;
|
|
75
76
|
}
|
|
76
77
|
console.log(chalk.green(formatLintSummary(result, elapsedMs)));
|
|
77
78
|
};
|
package/src/commands/reason.ts
CHANGED
|
@@ -16,7 +16,6 @@ type RdfFormat = 'ttl' | 'trig' | 'nt' | 'nq' | 'n3';
|
|
|
16
16
|
export type ReasonOptions = {
|
|
17
17
|
owl?: string;
|
|
18
18
|
format?: RdfFormat | string;
|
|
19
|
-
clean?: boolean;
|
|
20
19
|
pretty?: boolean;
|
|
21
20
|
explanation?: boolean;
|
|
22
21
|
uniqueNamesAssumption?: boolean;
|
|
@@ -36,7 +35,6 @@ type AssertionsPayload = {
|
|
|
36
35
|
files: Array<{
|
|
37
36
|
modelUri: string;
|
|
38
37
|
ontologyIri: string;
|
|
39
|
-
path: string;
|
|
40
38
|
content: string;
|
|
41
39
|
}>;
|
|
42
40
|
};
|
|
@@ -150,8 +148,21 @@ function resolveEntailmentsPath(uri: string): string {
|
|
|
150
148
|
return trimmed;
|
|
151
149
|
}
|
|
152
150
|
|
|
153
|
-
function
|
|
154
|
-
|
|
151
|
+
function ontologyIriToTempPath(ontologyIri: string, format: RdfFormat): string {
|
|
152
|
+
try {
|
|
153
|
+
const iri = new URL(ontologyIri);
|
|
154
|
+
const rawPath = iri.pathname.replace(/\/+$/, '');
|
|
155
|
+
const segments = rawPath.split('/').filter(Boolean);
|
|
156
|
+
const stem = segments.at(-1) || 'index';
|
|
157
|
+
const dirSegs = iri.host ? [iri.host, ...segments.slice(0, -1)] : segments.slice(0, -1);
|
|
158
|
+
return dirSegs.length > 0 ? path.join(...dirSegs, `${stem}.${format}`) : `${stem}.${format}`;
|
|
159
|
+
} catch {
|
|
160
|
+
return ontologyIri.replace(/[^a-zA-Z0-9_/-]/g, '_') + '.' + format;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function modelUriToRelativePath(modelUri: string | undefined): string {
|
|
165
|
+
const trimmed = (modelUri ?? '').trim();
|
|
155
166
|
if (trimmed.startsWith('file://')) {
|
|
156
167
|
const absolute = fileURLToPath(trimmed);
|
|
157
168
|
const relative = path.relative(process.cwd(), absolute);
|
|
@@ -190,9 +201,7 @@ export const reasonAction = async (opts: ReasonOptions): Promise<void> => {
|
|
|
190
201
|
: undefined;
|
|
191
202
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'oml-reason-cli-'));
|
|
192
203
|
if (outputDir) {
|
|
193
|
-
|
|
194
|
-
await fs.rm(outputDir, { recursive: true, force: true });
|
|
195
|
-
}
|
|
204
|
+
await fs.rm(outputDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
196
205
|
}
|
|
197
206
|
const orderedFiles = sortFilesLeafToRoot(assertions.files);
|
|
198
207
|
let firstInconsistent: { file: AssertionsPayload['files'][number]; result: Record<string, unknown> } | undefined;
|
|
@@ -200,12 +209,12 @@ export const reasonAction = async (opts: ReasonOptions): Promise<void> => {
|
|
|
200
209
|
let entailmentsProduced = 0;
|
|
201
210
|
try {
|
|
202
211
|
for (const file of orderedFiles) {
|
|
203
|
-
const target = path.join(tempRoot, file.
|
|
212
|
+
const target = path.join(tempRoot, ontologyIriToTempPath(file.ontologyIri, outputFormat));
|
|
204
213
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
205
214
|
await fs.writeFile(target, prettyPrintRdf(file.content, outputFormat), 'utf-8');
|
|
206
215
|
}
|
|
207
216
|
for (const file of orderedFiles) {
|
|
208
|
-
const target = path.join(tempRoot, file.
|
|
217
|
+
const target = path.join(tempRoot, ontologyIriToTempPath(file.ontologyIri, outputFormat));
|
|
209
218
|
try {
|
|
210
219
|
const result = await checkConsistency({
|
|
211
220
|
input: target,
|
|
@@ -257,7 +266,9 @@ export const reasonAction = async (opts: ReasonOptions): Promise<void> => {
|
|
|
257
266
|
failCli(chalk.red(`reason failed: ${modelUri}: ${firstFailure.error}`));
|
|
258
267
|
}
|
|
259
268
|
if (firstInconsistent) {
|
|
260
|
-
const where = modelUriToRelativePath(firstInconsistent.file.modelUri)
|
|
269
|
+
const where = modelUriToRelativePath(firstInconsistent.file.modelUri)
|
|
270
|
+
|| firstInconsistent.file.ontologyIri.trim()
|
|
271
|
+
|| '<unknown>';
|
|
261
272
|
if (opts.explanation === true) {
|
|
262
273
|
failCli(chalk.red(`Inconsistency found in: ${where}\n${JSON.stringify(firstInconsistent.result, null, 2)}`));
|
|
263
274
|
}
|