@enactprotocol/cli 2.0.0 → 2.0.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.
Files changed (111) hide show
  1. package/dist/commands/auth/index.d.ts +12 -0
  2. package/dist/commands/auth/index.d.ts.map +1 -0
  3. package/dist/commands/auth/index.js +743 -0
  4. package/dist/commands/auth/index.js.map +1 -0
  5. package/dist/commands/cache/index.d.ts +11 -0
  6. package/dist/commands/cache/index.d.ts.map +1 -0
  7. package/dist/commands/cache/index.js +304 -0
  8. package/dist/commands/cache/index.js.map +1 -0
  9. package/dist/commands/config/index.d.ts +11 -0
  10. package/dist/commands/config/index.d.ts.map +1 -0
  11. package/dist/commands/config/index.js +138 -0
  12. package/dist/commands/config/index.js.map +1 -0
  13. package/dist/commands/env/index.d.ts +11 -0
  14. package/dist/commands/env/index.d.ts.map +1 -0
  15. package/dist/commands/env/index.js +303 -0
  16. package/dist/commands/env/index.js.map +1 -0
  17. package/dist/commands/exec/index.d.ts +12 -0
  18. package/dist/commands/exec/index.d.ts.map +1 -0
  19. package/dist/commands/exec/index.js +154 -0
  20. package/dist/commands/exec/index.js.map +1 -0
  21. package/dist/commands/get/index.d.ts +11 -0
  22. package/dist/commands/get/index.d.ts.map +1 -0
  23. package/dist/commands/get/index.js +151 -0
  24. package/dist/commands/get/index.js.map +1 -0
  25. package/dist/commands/index.d.ts +24 -0
  26. package/dist/commands/index.d.ts.map +1 -0
  27. package/dist/commands/index.js +27 -0
  28. package/dist/commands/index.js.map +1 -0
  29. package/dist/commands/inspect/index.d.ts +13 -0
  30. package/dist/commands/inspect/index.d.ts.map +1 -0
  31. package/dist/commands/inspect/index.js +199 -0
  32. package/dist/commands/inspect/index.js.map +1 -0
  33. package/dist/commands/install/index.d.ts +16 -0
  34. package/dist/commands/install/index.d.ts.map +1 -0
  35. package/dist/commands/install/index.js +520 -0
  36. package/dist/commands/install/index.js.map +1 -0
  37. package/dist/commands/list/index.d.ts +15 -0
  38. package/dist/commands/list/index.d.ts.map +1 -0
  39. package/dist/commands/list/index.js +103 -0
  40. package/dist/commands/list/index.js.map +1 -0
  41. package/dist/commands/publish/index.d.ts +11 -0
  42. package/dist/commands/publish/index.d.ts.map +1 -0
  43. package/dist/commands/publish/index.js +274 -0
  44. package/dist/commands/publish/index.js.map +1 -0
  45. package/dist/commands/report/index.d.ts +12 -0
  46. package/dist/commands/report/index.d.ts.map +1 -0
  47. package/dist/commands/report/index.js +279 -0
  48. package/dist/commands/report/index.js.map +1 -0
  49. package/dist/commands/run/index.d.ts +16 -0
  50. package/dist/commands/run/index.d.ts.map +1 -0
  51. package/dist/commands/run/index.js +525 -0
  52. package/dist/commands/run/index.js.map +1 -0
  53. package/dist/commands/search/index.d.ts +12 -0
  54. package/dist/commands/search/index.d.ts.map +1 -0
  55. package/dist/commands/search/index.js +275 -0
  56. package/dist/commands/search/index.js.map +1 -0
  57. package/dist/commands/setup/index.d.ts +11 -0
  58. package/dist/commands/setup/index.d.ts.map +1 -0
  59. package/dist/commands/setup/index.js +241 -0
  60. package/dist/commands/setup/index.js.map +1 -0
  61. package/dist/commands/sign/index.d.ts +17 -0
  62. package/dist/commands/sign/index.d.ts.map +1 -0
  63. package/dist/commands/sign/index.js +507 -0
  64. package/dist/commands/sign/index.js.map +1 -0
  65. package/dist/commands/trust/index.d.ts +13 -0
  66. package/dist/commands/trust/index.d.ts.map +1 -0
  67. package/dist/commands/trust/index.js +366 -0
  68. package/dist/commands/trust/index.js.map +1 -0
  69. package/dist/commands/unyank/index.d.ts +11 -0
  70. package/dist/commands/unyank/index.d.ts.map +1 -0
  71. package/dist/commands/unyank/index.js +87 -0
  72. package/dist/commands/unyank/index.js.map +1 -0
  73. package/dist/commands/yank/index.d.ts +13 -0
  74. package/dist/commands/yank/index.d.ts.map +1 -0
  75. package/dist/commands/yank/index.js +109 -0
  76. package/dist/commands/yank/index.js.map +1 -0
  77. package/dist/index.d.ts +10 -0
  78. package/dist/index.d.ts.map +1 -0
  79. package/dist/index.js +67 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/types.d.ts +69 -0
  82. package/dist/types.d.ts.map +1 -0
  83. package/dist/types.js +15 -0
  84. package/dist/types.js.map +1 -0
  85. package/dist/utils/errors.d.ts +159 -0
  86. package/dist/utils/errors.d.ts.map +1 -0
  87. package/dist/utils/errors.js +321 -0
  88. package/dist/utils/errors.js.map +1 -0
  89. package/dist/utils/exit-codes.d.ts +83 -0
  90. package/dist/utils/exit-codes.d.ts.map +1 -0
  91. package/dist/utils/exit-codes.js +126 -0
  92. package/dist/utils/exit-codes.js.map +1 -0
  93. package/dist/utils/ignore.d.ts +25 -0
  94. package/dist/utils/ignore.d.ts.map +1 -0
  95. package/dist/utils/ignore.js +123 -0
  96. package/dist/utils/ignore.js.map +1 -0
  97. package/dist/utils/index.d.ts +8 -0
  98. package/dist/utils/index.d.ts.map +1 -0
  99. package/dist/utils/index.js +12 -0
  100. package/dist/utils/index.js.map +1 -0
  101. package/dist/utils/output.d.ts +103 -0
  102. package/dist/utils/output.d.ts.map +1 -0
  103. package/dist/utils/output.js +201 -0
  104. package/dist/utils/output.js.map +1 -0
  105. package/dist/utils/spinner.d.ts +83 -0
  106. package/dist/utils/spinner.d.ts.map +1 -0
  107. package/dist/utils/spinner.js +162 -0
  108. package/dist/utils/spinner.js.map +1 -0
  109. package/package.json +5 -5
  110. package/src/index.ts +4 -0
  111. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,743 @@
1
+ /**
2
+ * enact auth command
3
+ *
4
+ * Manage authentication for the Enact registry.
5
+ * Implements OAuth 2.0 flow with GitHub, Google, or Microsoft.
6
+ */
7
+ import { createServer } from "node:http";
8
+ import { URL } from "node:url";
9
+ import { createApiClient, exchangeCodeForToken, getCurrentUser, initiateLogin, logout, refreshAccessToken, } from "@enactprotocol/api";
10
+ import { deleteSecret, getSecret, setSecret } from "@enactprotocol/secrets";
11
+ import { AuthError, dim, handleError, header, info, json, keyValue, newline, success, warning, } from "../../utils";
12
+ /** Namespace for storing auth tokens in keyring */
13
+ const AUTH_NAMESPACE = "enact:auth";
14
+ /** Token keys in keyring */
15
+ const ACCESS_TOKEN_KEY = "access_token";
16
+ const REFRESH_TOKEN_KEY = "refresh_token";
17
+ const TOKEN_EXPIRY_KEY = "token_expiry";
18
+ const AUTH_METHOD_KEY = "auth_method"; // 'supabase' or 'legacy'
19
+ /** Supabase configuration (matches web app - defaults to local Supabase) */
20
+ const SUPABASE_URL = process.env.SUPABASE_URL || "https://siikwkfgsmouioodghho.supabase.co";
21
+ const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY ??
22
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNpaWt3a2Znc21vdWlvb2RnaGhvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ2MTkzMzksImV4cCI6MjA4MDE5NTMzOX0.kxnx6-IPFhmGx6rzNx36vbyhFMFZKP_jFqaDbKnJ_E0";
23
+ /** Default callback port for OAuth */
24
+ const DEFAULT_CALLBACK_PORT = 9876;
25
+ /** Default port for web-based auth */
26
+ const WEB_AUTH_PORT = 8118;
27
+ /** Web app URL for authentication - imported from shared constants */
28
+ import { ENACT_WEB_URL } from "@enactprotocol/shared";
29
+ const WEB_APP_URL = ENACT_WEB_URL;
30
+ /**
31
+ * Get stored access token from keyring
32
+ */
33
+ async function getStoredToken() {
34
+ return await getSecret(AUTH_NAMESPACE, ACCESS_TOKEN_KEY);
35
+ }
36
+ /**
37
+ * Get stored refresh token from keyring
38
+ */
39
+ async function getStoredRefreshToken() {
40
+ return await getSecret(AUTH_NAMESPACE, REFRESH_TOKEN_KEY);
41
+ }
42
+ /**
43
+ * Store tokens in keyring
44
+ */
45
+ async function storeTokens(accessToken, refreshToken, expiresIn, authMethod = "supabase") {
46
+ await setSecret(AUTH_NAMESPACE, ACCESS_TOKEN_KEY, accessToken);
47
+ await setSecret(AUTH_NAMESPACE, REFRESH_TOKEN_KEY, refreshToken);
48
+ await setSecret(AUTH_NAMESPACE, AUTH_METHOD_KEY, authMethod);
49
+ // Store expiry as ISO timestamp
50
+ const expiryTime = new Date(Date.now() + expiresIn * 1000).toISOString();
51
+ await setSecret(AUTH_NAMESPACE, TOKEN_EXPIRY_KEY, expiryTime);
52
+ }
53
+ /**
54
+ * Clear stored tokens from keyring
55
+ */
56
+ async function clearStoredTokens() {
57
+ await deleteSecret(AUTH_NAMESPACE, ACCESS_TOKEN_KEY);
58
+ await deleteSecret(AUTH_NAMESPACE, REFRESH_TOKEN_KEY);
59
+ await deleteSecret(AUTH_NAMESPACE, TOKEN_EXPIRY_KEY);
60
+ await deleteSecret(AUTH_NAMESPACE, AUTH_METHOD_KEY);
61
+ }
62
+ /**
63
+ * Refresh Supabase token using the refresh token
64
+ */
65
+ async function refreshSupabaseToken(refreshToken) {
66
+ try {
67
+ const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ apikey: SUPABASE_ANON_KEY,
72
+ },
73
+ body: JSON.stringify({ refresh_token: refreshToken }),
74
+ });
75
+ if (!response.ok) {
76
+ return null;
77
+ }
78
+ const data = (await response.json());
79
+ return {
80
+ access_token: data.access_token,
81
+ refresh_token: data.refresh_token,
82
+ expires_in: data.expires_in,
83
+ };
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ /**
90
+ * Get a valid access token, refreshing if necessary
91
+ */
92
+ async function getValidToken() {
93
+ const accessToken = await getStoredToken();
94
+ if (!accessToken) {
95
+ return null;
96
+ }
97
+ // Check if token is expired
98
+ const expiryStr = await getSecret(AUTH_NAMESPACE, TOKEN_EXPIRY_KEY);
99
+ if (expiryStr) {
100
+ const expiry = new Date(expiryStr);
101
+ if (expiry.getTime() - Date.now() < 60000) {
102
+ // Less than 1 minute left, try to refresh
103
+ const refreshToken = await getStoredRefreshToken();
104
+ if (refreshToken) {
105
+ const authMethod = await getSecret(AUTH_NAMESPACE, AUTH_METHOD_KEY);
106
+ if (authMethod === "supabase") {
107
+ // Use Supabase refresh
108
+ const result = await refreshSupabaseToken(refreshToken);
109
+ if (result) {
110
+ await storeTokens(result.access_token, result.refresh_token, result.expires_in, "supabase");
111
+ return result.access_token;
112
+ }
113
+ }
114
+ else {
115
+ // Use legacy API refresh
116
+ try {
117
+ const client = createApiClient();
118
+ const result = await refreshAccessToken(client, refreshToken);
119
+ await storeTokens(result.access_token, refreshToken, result.expires_in, "legacy");
120
+ return result.access_token;
121
+ }
122
+ catch {
123
+ // Refresh failed
124
+ }
125
+ }
126
+ // Refresh failed, need to re-authenticate
127
+ await clearStoredTokens();
128
+ return null;
129
+ }
130
+ }
131
+ }
132
+ return accessToken;
133
+ }
134
+ /**
135
+ * Open a URL in the default browser
136
+ */
137
+ async function openBrowser(url) {
138
+ const { exec } = await import("node:child_process");
139
+ const { promisify } = await import("node:util");
140
+ const execAsync = promisify(exec);
141
+ const platform = process.platform;
142
+ try {
143
+ if (platform === "darwin") {
144
+ await execAsync(`open "${url}"`);
145
+ }
146
+ else if (platform === "win32") {
147
+ await execAsync(`start "" "${url}"`);
148
+ }
149
+ else {
150
+ // Linux
151
+ await execAsync(`xdg-open "${url}"`);
152
+ }
153
+ }
154
+ catch (err) {
155
+ throw new Error(`Failed to open browser: ${err instanceof Error ? err.message : "Unknown error"}`);
156
+ }
157
+ }
158
+ /**
159
+ * Start a local HTTP server to receive the OAuth callback
160
+ */
161
+ async function waitForCallback(port) {
162
+ return new Promise((resolve, reject) => {
163
+ const server = createServer((req, res) => {
164
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
165
+ if (url.pathname === "/callback") {
166
+ const code = url.searchParams.get("code");
167
+ const error = url.searchParams.get("error");
168
+ const errorDescription = url.searchParams.get("error_description");
169
+ const state = url.searchParams.get("state") ?? undefined;
170
+ // Send response to browser
171
+ res.writeHead(200, { "Content-Type": "text/html" });
172
+ if (error) {
173
+ res.end(`
174
+ <!DOCTYPE html>
175
+ <html>
176
+ <head><title>Authentication Failed</title></head>
177
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
178
+ <h1>❌ Authentication Failed</h1>
179
+ <p>${errorDescription ?? error}</p>
180
+ <p>You can close this window.</p>
181
+ </body>
182
+ </html>
183
+ `);
184
+ server.close();
185
+ reject(new Error(errorDescription ?? error));
186
+ return;
187
+ }
188
+ if (!code) {
189
+ res.end(`
190
+ <!DOCTYPE html>
191
+ <html>
192
+ <head><title>Authentication Failed</title></head>
193
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
194
+ <h1>❌ Authentication Failed</h1>
195
+ <p>No authorization code received.</p>
196
+ <p>You can close this window.</p>
197
+ </body>
198
+ </html>
199
+ `);
200
+ server.close();
201
+ reject(new Error("No authorization code received"));
202
+ return;
203
+ }
204
+ res.end(`
205
+ <!DOCTYPE html>
206
+ <html>
207
+ <head><title>Authentication Successful</title></head>
208
+ <body style="font-family: system-ui; padding: 40px; text-align: center;">
209
+ <h1>✅ Authentication Successful</h1>
210
+ <p>You are now logged in to the Enact registry.</p>
211
+ <p>You can close this window and return to your terminal.</p>
212
+ </body>
213
+ </html>
214
+ `);
215
+ server.close();
216
+ resolve({ code, state: state ?? undefined });
217
+ }
218
+ else {
219
+ res.writeHead(404);
220
+ res.end("Not found");
221
+ }
222
+ });
223
+ server.listen(port, "127.0.0.1", () => {
224
+ // Server is ready
225
+ });
226
+ // Timeout after 5 minutes
227
+ setTimeout(() => {
228
+ server.close();
229
+ reject(new Error("Authentication timed out. Please try again."));
230
+ }, 5 * 60 * 1000);
231
+ });
232
+ }
233
+ /**
234
+ * Wait for web-based auth callback (receives tokens from web app)
235
+ */
236
+ async function waitForWebCallback(port) {
237
+ return new Promise((resolve, reject) => {
238
+ const state = { timeoutId: undefined };
239
+ const server = createServer(async (req, res) => {
240
+ // Enable CORS for the web app
241
+ res.setHeader("Access-Control-Allow-Origin", "*");
242
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
243
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
244
+ // Disable keep-alive to allow server to close immediately
245
+ res.setHeader("Connection", "close");
246
+ if (req.method === "OPTIONS") {
247
+ res.writeHead(200);
248
+ res.end();
249
+ return;
250
+ }
251
+ if (req.method === "POST" && req.url === "/callback") {
252
+ let body = "";
253
+ req.on("data", (chunk) => {
254
+ body += chunk.toString();
255
+ });
256
+ req.on("end", () => {
257
+ try {
258
+ const data = JSON.parse(body);
259
+ if (!data.access_token || !data.refresh_token) {
260
+ res.writeHead(400, { "Content-Type": "application/json" });
261
+ res.end(JSON.stringify({ error: "Missing tokens" }));
262
+ return;
263
+ }
264
+ res.writeHead(200, { "Content-Type": "application/json" });
265
+ res.end(JSON.stringify({ success: true }));
266
+ // Clear timeout and close server
267
+ if (state.timeoutId)
268
+ clearTimeout(state.timeoutId);
269
+ server.close();
270
+ server.closeAllConnections?.();
271
+ resolve({
272
+ accessToken: data.access_token,
273
+ refreshToken: data.refresh_token,
274
+ user: data.user,
275
+ });
276
+ }
277
+ catch {
278
+ res.writeHead(400, { "Content-Type": "application/json" });
279
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
280
+ }
281
+ });
282
+ }
283
+ else {
284
+ res.writeHead(404);
285
+ res.end("Not found");
286
+ }
287
+ });
288
+ server.listen(port, "127.0.0.1", () => {
289
+ // Server is ready
290
+ });
291
+ // Timeout after 5 minutes
292
+ state.timeoutId = setTimeout(() => {
293
+ server.close();
294
+ server.closeAllConnections?.();
295
+ reject(new Error("Authentication timed out. Please try again."));
296
+ }, 5 * 60 * 1000);
297
+ });
298
+ }
299
+ /**
300
+ * Auth login handler (web-based flow via enact.dev)
301
+ */
302
+ async function webLoginHandler(options, _ctx) {
303
+ // Check for existing valid token
304
+ const existingToken = await getValidToken();
305
+ if (existingToken) {
306
+ const client = createApiClient();
307
+ client.setAuthToken(existingToken);
308
+ try {
309
+ const user = await getCurrentUser(client);
310
+ warning(`Already logged in as ${user.username}`);
311
+ info("Use 'enact auth logout' to sign out first");
312
+ return;
313
+ }
314
+ catch {
315
+ // Token invalid, continue with login
316
+ await clearStoredTokens();
317
+ }
318
+ }
319
+ const port = WEB_AUTH_PORT;
320
+ const authUrl = `${WEB_APP_URL}/auth/cli?port=${port}`;
321
+ info("Authenticating via web browser...");
322
+ newline();
323
+ try {
324
+ // 1. Start local callback server
325
+ const callbackPromise = waitForWebCallback(port);
326
+ // 2. Open browser for authentication
327
+ info("Opening browser for authentication...");
328
+ dim("If the browser doesn't open, visit:");
329
+ dim(authUrl);
330
+ newline();
331
+ await openBrowser(authUrl);
332
+ // 3. Wait for callback with tokens
333
+ info("Waiting for authentication...");
334
+ const { accessToken, refreshToken, user } = await callbackPromise;
335
+ // 4. Store tokens securely in keyring (default 1 hour expiry for Supabase tokens)
336
+ await storeTokens(accessToken, refreshToken, 3600, "supabase");
337
+ // Get username from user metadata
338
+ const username = user.user_metadata?.user_name ||
339
+ user.user_metadata?.full_name ||
340
+ user.email?.split("@")[0] ||
341
+ "user";
342
+ const email = user.email || "";
343
+ if (options.json) {
344
+ json({
345
+ success: true,
346
+ username,
347
+ email,
348
+ });
349
+ return;
350
+ }
351
+ newline();
352
+ success(`Logged in as ${username}`);
353
+ if (email) {
354
+ keyValue("Email", email);
355
+ }
356
+ }
357
+ catch (err) {
358
+ throw new AuthError(err instanceof Error ? err.message : "Authentication failed");
359
+ }
360
+ }
361
+ /**
362
+ * Auth login handler
363
+ */
364
+ async function loginHandler(options, _ctx) {
365
+ // Use web-based flow if --web flag is set (now the default)
366
+ if (options.web !== false) {
367
+ return webLoginHandler(options, _ctx);
368
+ }
369
+ // Legacy API-based OAuth flow
370
+ const client = createApiClient();
371
+ // Check for existing valid token
372
+ const existingToken = await getValidToken();
373
+ if (existingToken) {
374
+ client.setAuthToken(existingToken);
375
+ try {
376
+ const user = await getCurrentUser(client);
377
+ warning(`Already logged in as ${user.username}`);
378
+ info("Use 'enact auth logout' to sign out first");
379
+ return;
380
+ }
381
+ catch {
382
+ // Token invalid, continue with login
383
+ await clearStoredTokens();
384
+ }
385
+ }
386
+ const provider = options.provider ?? "github";
387
+ const port = DEFAULT_CALLBACK_PORT;
388
+ const redirectUri = `http://localhost:${port}/callback`;
389
+ info(`Authenticating with ${provider}...`);
390
+ newline();
391
+ try {
392
+ // 1. Initiate OAuth flow
393
+ const loginResponse = await initiateLogin(client, provider, redirectUri);
394
+ // 2. Start local callback server
395
+ const callbackPromise = waitForCallback(port);
396
+ // 3. Open browser for authentication
397
+ info("Opening browser for authentication...");
398
+ dim("If the browser doesn't open, visit:");
399
+ dim(loginResponse.auth_url);
400
+ newline();
401
+ await openBrowser(loginResponse.auth_url);
402
+ // 4. Wait for callback
403
+ info("Waiting for authentication...");
404
+ const { code } = await callbackPromise;
405
+ // 5. Exchange code for tokens
406
+ const tokenResponse = await exchangeCodeForToken(client, provider, code);
407
+ // 6. Store tokens securely in keyring
408
+ await storeTokens(tokenResponse.access_token, tokenResponse.refresh_token, tokenResponse.expires_in, "legacy");
409
+ // 7. Update client with new token
410
+ client.setAuthToken(tokenResponse.access_token);
411
+ if (options.json) {
412
+ json({
413
+ success: true,
414
+ username: tokenResponse.user.username,
415
+ email: tokenResponse.user.email,
416
+ });
417
+ return;
418
+ }
419
+ newline();
420
+ success(`Logged in as ${tokenResponse.user.username}`);
421
+ keyValue("Email", tokenResponse.user.email);
422
+ }
423
+ catch (err) {
424
+ throw new AuthError(err instanceof Error ? err.message : "Authentication failed");
425
+ }
426
+ }
427
+ /**
428
+ * Auth logout handler
429
+ */
430
+ async function logoutHandler(options, _ctx) {
431
+ const client = createApiClient();
432
+ // Check for stored token
433
+ const token = await getStoredToken();
434
+ if (!token) {
435
+ info("Not currently logged in");
436
+ return;
437
+ }
438
+ // Get username before clearing
439
+ let username;
440
+ try {
441
+ client.setAuthToken(token);
442
+ const user = await getCurrentUser(client);
443
+ username = user.username;
444
+ }
445
+ catch {
446
+ // Token might be invalid, but we still want to clear it
447
+ }
448
+ // Clear stored tokens
449
+ await clearStoredTokens();
450
+ logout(client);
451
+ if (options.json) {
452
+ json({ success: true, message: "Logged out" });
453
+ return;
454
+ }
455
+ success(`Logged out${username ? ` (was ${username})` : ""}`);
456
+ }
457
+ /**
458
+ * Get Supabase user from access token
459
+ */
460
+ async function getSupabaseUser(accessToken) {
461
+ try {
462
+ const response = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
463
+ headers: {
464
+ Authorization: `Bearer ${accessToken}`,
465
+ apikey: SUPABASE_ANON_KEY,
466
+ },
467
+ });
468
+ if (!response.ok) {
469
+ return null;
470
+ }
471
+ return (await response.json());
472
+ }
473
+ catch {
474
+ return null;
475
+ }
476
+ }
477
+ /**
478
+ * Get user profile from Supabase database
479
+ */
480
+ async function getSupabaseProfile(accessToken, userId) {
481
+ try {
482
+ const response = await fetch(`${SUPABASE_URL}/rest/v1/profiles?id=eq.${userId}&select=username,display_name,avatar_url`, {
483
+ headers: {
484
+ Authorization: `Bearer ${accessToken}`,
485
+ apikey: SUPABASE_ANON_KEY,
486
+ },
487
+ });
488
+ if (!response.ok) {
489
+ return null;
490
+ }
491
+ const data = (await response.json());
492
+ return data[0] || null;
493
+ }
494
+ catch {
495
+ return null;
496
+ }
497
+ }
498
+ /**
499
+ * Auth status handler
500
+ */
501
+ async function statusHandler(options, _ctx) {
502
+ // Try to get a valid token
503
+ const token = await getValidToken();
504
+ if (!token) {
505
+ if (options.json) {
506
+ json({ authenticated: false });
507
+ return;
508
+ }
509
+ header("Authentication Status");
510
+ newline();
511
+ warning("Not authenticated");
512
+ newline();
513
+ dim("Run 'enact auth login' to authenticate");
514
+ return;
515
+ }
516
+ const authMethod = await getSecret(AUTH_NAMESPACE, AUTH_METHOD_KEY);
517
+ const expiryStr = await getSecret(AUTH_NAMESPACE, TOKEN_EXPIRY_KEY);
518
+ if (authMethod === "supabase") {
519
+ // Get user info from Supabase
520
+ const user = await getSupabaseUser(token);
521
+ if (!user) {
522
+ await clearStoredTokens();
523
+ if (options.json) {
524
+ json({ authenticated: false });
525
+ return;
526
+ }
527
+ header("Authentication Status");
528
+ newline();
529
+ warning("Not authenticated (token invalid)");
530
+ newline();
531
+ dim("Run 'enact auth login' to authenticate");
532
+ return;
533
+ }
534
+ // Try to get profile username from database, fall back to user_metadata
535
+ const profile = await getSupabaseProfile(token, user.id);
536
+ const username = profile?.username ||
537
+ user.user_metadata?.username ||
538
+ user.user_metadata?.user_name ||
539
+ user.user_metadata?.full_name ||
540
+ user.email?.split("@")[0] ||
541
+ "user";
542
+ if (options.json) {
543
+ json({
544
+ authenticated: true,
545
+ user: {
546
+ username,
547
+ email: user.email,
548
+ },
549
+ expiresAt: expiryStr ?? undefined,
550
+ });
551
+ return;
552
+ }
553
+ header("Authentication Status");
554
+ newline();
555
+ success("Authenticated");
556
+ keyValue("Username", username);
557
+ if (user.email) {
558
+ keyValue("Email", user.email);
559
+ }
560
+ if (expiryStr) {
561
+ keyValue("Token Expires", new Date(expiryStr).toISOString());
562
+ }
563
+ return;
564
+ }
565
+ // Legacy API-based auth
566
+ const client = createApiClient();
567
+ client.setAuthToken(token);
568
+ try {
569
+ const user = await getCurrentUser(client);
570
+ if (options.json) {
571
+ json({
572
+ authenticated: true,
573
+ user: {
574
+ username: user.username,
575
+ email: user.email,
576
+ namespaces: user.namespaces,
577
+ },
578
+ expiresAt: expiryStr ?? undefined,
579
+ });
580
+ return;
581
+ }
582
+ header("Authentication Status");
583
+ newline();
584
+ success("Authenticated");
585
+ keyValue("Username", user.username);
586
+ keyValue("Email", user.email);
587
+ if (user.namespaces.length > 0) {
588
+ keyValue("Namespaces", user.namespaces.join(", "));
589
+ }
590
+ if (expiryStr) {
591
+ keyValue("Token Expires", new Date(expiryStr).toISOString());
592
+ }
593
+ }
594
+ catch {
595
+ // Token invalid
596
+ await clearStoredTokens();
597
+ if (options.json) {
598
+ json({ authenticated: false });
599
+ return;
600
+ }
601
+ header("Authentication Status");
602
+ newline();
603
+ warning("Not authenticated (token expired)");
604
+ newline();
605
+ dim("Run 'enact auth login' to authenticate");
606
+ }
607
+ }
608
+ /**
609
+ * Auth whoami handler (alias for status)
610
+ */
611
+ async function whoamiHandler(options, _ctx) {
612
+ const token = await getValidToken();
613
+ if (!token) {
614
+ throw new AuthError("Not logged in");
615
+ }
616
+ const authMethod = await getSecret(AUTH_NAMESPACE, AUTH_METHOD_KEY);
617
+ if (authMethod === "supabase") {
618
+ const user = await getSupabaseUser(token);
619
+ if (!user) {
620
+ await clearStoredTokens();
621
+ throw new AuthError("Not logged in (token expired)");
622
+ }
623
+ // Try to get profile username from database, fall back to user_metadata
624
+ const profile = await getSupabaseProfile(token, user.id);
625
+ const username = profile?.username ||
626
+ user.user_metadata?.username ||
627
+ user.user_metadata?.user_name ||
628
+ user.user_metadata?.full_name ||
629
+ user.email?.split("@")[0] ||
630
+ "user";
631
+ if (options.json) {
632
+ json({
633
+ username,
634
+ email: user.email,
635
+ });
636
+ return;
637
+ }
638
+ console.log(username);
639
+ return;
640
+ }
641
+ // Legacy API-based auth
642
+ const client = createApiClient();
643
+ client.setAuthToken(token);
644
+ try {
645
+ const user = await getCurrentUser(client);
646
+ if (options.json) {
647
+ json({
648
+ username: user.username,
649
+ email: user.email,
650
+ namespaces: user.namespaces,
651
+ });
652
+ return;
653
+ }
654
+ console.log(user.username);
655
+ }
656
+ catch {
657
+ await clearStoredTokens();
658
+ throw new AuthError("Not logged in (token expired)");
659
+ }
660
+ }
661
+ /**
662
+ * Configure the auth command
663
+ */
664
+ export function configureAuthCommand(program) {
665
+ const auth = program.command("auth").description("Manage registry authentication");
666
+ auth
667
+ .command("login")
668
+ .description("Authenticate with the Enact registry")
669
+ .option("-p, --provider <provider>", "OAuth provider for API mode (github, google, microsoft)", "github")
670
+ .option("--web", "Use web-based authentication (default)", true)
671
+ .option("--no-web", "Use direct API-based OAuth (legacy)")
672
+ .option("-v, --verbose", "Show detailed output")
673
+ .option("--json", "Output as JSON")
674
+ .action(async (options) => {
675
+ const ctx = {
676
+ cwd: process.cwd(),
677
+ options,
678
+ isCI: Boolean(process.env.CI),
679
+ isInteractive: process.stdout.isTTY ?? false,
680
+ };
681
+ try {
682
+ await loginHandler(options, ctx);
683
+ }
684
+ catch (err) {
685
+ handleError(err, options.verbose ? { verbose: true } : undefined);
686
+ }
687
+ });
688
+ auth
689
+ .command("logout")
690
+ .description("Sign out from the Enact registry")
691
+ .option("--json", "Output as JSON")
692
+ .action(async (options) => {
693
+ const ctx = {
694
+ cwd: process.cwd(),
695
+ options,
696
+ isCI: Boolean(process.env.CI),
697
+ isInteractive: process.stdout.isTTY ?? false,
698
+ };
699
+ try {
700
+ await logoutHandler(options, ctx);
701
+ }
702
+ catch (err) {
703
+ handleError(err);
704
+ }
705
+ });
706
+ auth
707
+ .command("status")
708
+ .description("Show current authentication status")
709
+ .option("--json", "Output as JSON")
710
+ .action(async (options) => {
711
+ const ctx = {
712
+ cwd: process.cwd(),
713
+ options,
714
+ isCI: Boolean(process.env.CI),
715
+ isInteractive: process.stdout.isTTY ?? false,
716
+ };
717
+ try {
718
+ await statusHandler(options, ctx);
719
+ }
720
+ catch (err) {
721
+ handleError(err);
722
+ }
723
+ });
724
+ auth
725
+ .command("whoami")
726
+ .description("Print the current username")
727
+ .option("--json", "Output as JSON")
728
+ .action(async (options) => {
729
+ const ctx = {
730
+ cwd: process.cwd(),
731
+ options,
732
+ isCI: Boolean(process.env.CI),
733
+ isInteractive: process.stdout.isTTY ?? false,
734
+ };
735
+ try {
736
+ await whoamiHandler(options, ctx);
737
+ }
738
+ catch (err) {
739
+ handleError(err);
740
+ }
741
+ });
742
+ }
743
+ //# sourceMappingURL=index.js.map