@enactprotocol/cli 2.0.0 → 2.0.2
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/dist/commands/auth/index.d.ts +12 -0
- package/dist/commands/auth/index.d.ts.map +1 -0
- package/dist/commands/auth/index.js +743 -0
- package/dist/commands/auth/index.js.map +1 -0
- package/dist/commands/cache/index.d.ts +11 -0
- package/dist/commands/cache/index.d.ts.map +1 -0
- package/dist/commands/cache/index.js +304 -0
- package/dist/commands/cache/index.js.map +1 -0
- package/dist/commands/config/index.d.ts +11 -0
- package/dist/commands/config/index.d.ts.map +1 -0
- package/dist/commands/config/index.js +138 -0
- package/dist/commands/config/index.js.map +1 -0
- package/dist/commands/env/index.d.ts +11 -0
- package/dist/commands/env/index.d.ts.map +1 -0
- package/dist/commands/env/index.js +303 -0
- package/dist/commands/env/index.js.map +1 -0
- package/dist/commands/exec/index.d.ts +12 -0
- package/dist/commands/exec/index.d.ts.map +1 -0
- package/dist/commands/exec/index.js +154 -0
- package/dist/commands/exec/index.js.map +1 -0
- package/dist/commands/get/index.d.ts +11 -0
- package/dist/commands/get/index.d.ts.map +1 -0
- package/dist/commands/get/index.js +151 -0
- package/dist/commands/get/index.js.map +1 -0
- package/dist/commands/index.d.ts +25 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +28 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init/index.d.ts +11 -0
- package/dist/commands/init/index.d.ts.map +1 -0
- package/dist/commands/init/index.js +192 -0
- package/dist/commands/init/index.js.map +1 -0
- package/dist/commands/inspect/index.d.ts +13 -0
- package/dist/commands/inspect/index.d.ts.map +1 -0
- package/dist/commands/inspect/index.js +199 -0
- package/dist/commands/inspect/index.js.map +1 -0
- package/dist/commands/install/index.d.ts +16 -0
- package/dist/commands/install/index.d.ts.map +1 -0
- package/dist/commands/install/index.js +520 -0
- package/dist/commands/install/index.js.map +1 -0
- package/dist/commands/list/index.d.ts +15 -0
- package/dist/commands/list/index.d.ts.map +1 -0
- package/dist/commands/list/index.js +103 -0
- package/dist/commands/list/index.js.map +1 -0
- package/dist/commands/publish/index.d.ts +11 -0
- package/dist/commands/publish/index.d.ts.map +1 -0
- package/dist/commands/publish/index.js +274 -0
- package/dist/commands/publish/index.js.map +1 -0
- package/dist/commands/report/index.d.ts +12 -0
- package/dist/commands/report/index.d.ts.map +1 -0
- package/dist/commands/report/index.js +279 -0
- package/dist/commands/report/index.js.map +1 -0
- package/dist/commands/run/index.d.ts +16 -0
- package/dist/commands/run/index.d.ts.map +1 -0
- package/dist/commands/run/index.js +525 -0
- package/dist/commands/run/index.js.map +1 -0
- package/dist/commands/search/index.d.ts +12 -0
- package/dist/commands/search/index.d.ts.map +1 -0
- package/dist/commands/search/index.js +275 -0
- package/dist/commands/search/index.js.map +1 -0
- package/dist/commands/setup/index.d.ts +11 -0
- package/dist/commands/setup/index.d.ts.map +1 -0
- package/dist/commands/setup/index.js +241 -0
- package/dist/commands/setup/index.js.map +1 -0
- package/dist/commands/sign/index.d.ts +17 -0
- package/dist/commands/sign/index.d.ts.map +1 -0
- package/dist/commands/sign/index.js +507 -0
- package/dist/commands/sign/index.js.map +1 -0
- package/dist/commands/trust/index.d.ts +13 -0
- package/dist/commands/trust/index.d.ts.map +1 -0
- package/dist/commands/trust/index.js +366 -0
- package/dist/commands/trust/index.js.map +1 -0
- package/dist/commands/unyank/index.d.ts +11 -0
- package/dist/commands/unyank/index.d.ts.map +1 -0
- package/dist/commands/unyank/index.js +87 -0
- package/dist/commands/unyank/index.js.map +1 -0
- package/dist/commands/yank/index.d.ts +13 -0
- package/dist/commands/yank/index.d.ts.map +1 -0
- package/dist/commands/yank/index.js +109 -0
- package/dist/commands/yank/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/errors.d.ts +159 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +321 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/exit-codes.d.ts +83 -0
- package/dist/utils/exit-codes.d.ts.map +1 -0
- package/dist/utils/exit-codes.js +126 -0
- package/dist/utils/exit-codes.js.map +1 -0
- package/dist/utils/ignore.d.ts +25 -0
- package/dist/utils/ignore.d.ts.map +1 -0
- package/dist/utils/ignore.js +123 -0
- package/dist/utils/ignore.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/output.d.ts +103 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +201 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/spinner.d.ts +83 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +162 -0
- package/dist/utils/spinner.js.map +1 -0
- package/package.json +5 -5
- package/src/commands/index.ts +1 -0
- package/src/commands/init/index.ts +231 -0
- package/src/index.ts +7 -1
- package/tests/index.test.ts +1 -1
- package/tsconfig.json +1 -0
- package/tsconfig.tsbuildinfo +1 -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
|