@ogment-ai/cli 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -9
- package/dist/cli/commands.d.ts +37 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +56 -0
- package/dist/cli/execute.d.ts +11 -0
- package/dist/cli/execute.d.ts.map +1 -0
- package/dist/cli/execute.js +468 -0
- package/dist/cli/invocations.d.ts +31 -0
- package/dist/cli/invocations.d.ts.map +1 -0
- package/dist/cli/invocations.js +1 -0
- package/dist/cli/parse-errors.d.ts +17 -0
- package/dist/cli/parse-errors.d.ts.map +1 -0
- package/dist/cli/parse-errors.js +184 -0
- package/dist/cli/program.d.ts +10 -0
- package/dist/cli/program.d.ts.map +1 -0
- package/dist/cli/program.js +174 -0
- package/dist/cli/run.d.ts +6 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +83 -0
- package/dist/cli/runtime.d.ts +21 -0
- package/dist/cli/runtime.d.ts.map +1 -0
- package/dist/cli/runtime.js +80 -0
- package/dist/cli.d.ts +2 -20
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -737
- package/dist/commands/auth.d.ts +1 -4
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +0 -8
- package/dist/commands/catalog.d.ts +3 -1
- package/dist/commands/catalog.d.ts.map +1 -1
- package/dist/commands/catalog.js +19 -2
- package/dist/commands/invoke.d.ts.map +1 -1
- package/dist/commands/invoke.js +53 -3
- package/dist/infra/credentials.d.ts.map +1 -1
- package/dist/infra/credentials.js +0 -7
- package/dist/infra/http.d.ts +5 -1
- package/dist/infra/http.d.ts.map +1 -1
- package/dist/infra/http.js +62 -5
- package/dist/output/envelope.d.ts +5 -2
- package/dist/output/envelope.d.ts.map +1 -1
- package/dist/output/envelope.js +39 -23
- package/dist/output/manager.d.ts +9 -5
- package/dist/output/manager.d.ts.map +1 -1
- package/dist/output/manager.js +52 -9
- package/dist/services/account.d.ts.map +1 -1
- package/dist/services/account.js +9 -16
- package/dist/services/auth.d.ts +3 -15
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/auth.js +32 -483
- package/dist/services/info.d.ts.map +1 -1
- package/dist/services/info.js +62 -0
- package/dist/services/mcp-error-mapping.d.ts +9 -0
- package/dist/services/mcp-error-mapping.d.ts.map +1 -0
- package/dist/services/mcp-error-mapping.js +129 -0
- package/dist/services/mcp.d.ts +8 -2
- package/dist/services/mcp.d.ts.map +1 -1
- package/dist/services/mcp.js +24 -14
- package/dist/shared/constants.d.ts +0 -2
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +0 -2
- package/dist/shared/error-codes.d.ts +4 -1
- package/dist/shared/error-codes.d.ts.map +1 -1
- package/dist/shared/error-presentation.d.ts +17 -0
- package/dist/shared/error-presentation.d.ts.map +1 -0
- package/dist/shared/error-presentation.js +151 -0
- package/dist/shared/errors.d.ts +34 -14
- package/dist/shared/errors.d.ts.map +1 -1
- package/dist/shared/errors.js +126 -25
- package/dist/shared/guards.d.ts +2 -1
- package/dist/shared/guards.d.ts.map +1 -1
- package/dist/shared/guards.js +1 -3
- package/dist/shared/recovery.d.ts +5 -0
- package/dist/shared/recovery.d.ts.map +1 -0
- package/dist/shared/recovery.js +123 -0
- package/dist/shared/schemas.d.ts +2 -3
- package/dist/shared/schemas.d.ts.map +1 -1
- package/dist/shared/schemas.js +2 -3
- package/dist/shared/types.d.ts +53 -13
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +1 -1
- package/package.json +2 -4
- package/dist/infra/browser.d.ts +0 -12
- package/dist/infra/browser.d.ts.map +0 -1
- package/dist/infra/browser.js +0 -20
- package/dist/shared/retry.d.ts +0 -17
- package/dist/shared/retry.d.ts.map +0 -1
- package/dist/shared/retry.js +0 -27
package/dist/services/auth.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { on } from "node:events";
|
|
3
|
-
import { createServer } from "node:http";
|
|
4
|
-
import { hostname } from "node:os";
|
|
5
1
|
import { Result } from "better-result";
|
|
6
|
-
import { detectExecutionEnvironment } from "../infra/env.js";
|
|
7
2
|
import { readResponseText } from "../infra/http.js";
|
|
8
|
-
import { CLI_CLIENT_NAME, CLI_REDIRECT_HOST } from "../shared/constants.js";
|
|
9
3
|
import { ERROR_CODE } from "../shared/error-codes.js";
|
|
10
4
|
import { AuthError, RemoteRequestError, UnexpectedError, ValidationError, } from "../shared/errors.js";
|
|
11
5
|
import { parseWithSchema } from "../shared/guards.js";
|
|
12
|
-
import {
|
|
13
|
-
import { browserAgentCallbackSchema, cliExchangeErrorSchema, cliExchangeRequestSchema, cliExchangeSuccessSchema, deviceCodeStartSchema, deviceTokenApprovedSchema, oauthClientRegistrationSchema, oauthTokenSchema, } from "../shared/schemas.js";
|
|
14
|
-
const CALLBACK_TIMEOUT_MILLISECONDS = 5 * 60 * 1000;
|
|
6
|
+
import { deviceCodeStartSchema, deviceTokenApprovedSchema } from "../shared/schemas.js";
|
|
15
7
|
const defaultSleep = async (milliseconds) => {
|
|
16
8
|
await new Promise((resolve) => {
|
|
17
9
|
setTimeout(resolve, milliseconds);
|
|
@@ -30,237 +22,17 @@ const toPendingPayload = (payload) => {
|
|
|
30
22
|
}
|
|
31
23
|
return null;
|
|
32
24
|
};
|
|
33
|
-
const generateCodeVerifier = () => {
|
|
34
|
-
return randomBytes(32).toString("base64url");
|
|
35
|
-
};
|
|
36
|
-
const generateCodeChallenge = (verifier) => {
|
|
37
|
-
return createHash("sha256").update(verifier).digest("base64url");
|
|
38
|
-
};
|
|
39
|
-
const closeServer = async (server) => {
|
|
40
|
-
await Result.tryPromise({
|
|
41
|
-
catch: () => undefined,
|
|
42
|
-
try: async () => {
|
|
43
|
-
await new Promise((resolve) => {
|
|
44
|
-
server.close(() => {
|
|
45
|
-
resolve();
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
Result.try({
|
|
51
|
-
catch: () => undefined,
|
|
52
|
-
try: () => {
|
|
53
|
-
server.unref();
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
};
|
|
57
|
-
const successPage = (agentName) => {
|
|
58
|
-
return `<!DOCTYPE html>
|
|
59
|
-
<html lang="en">
|
|
60
|
-
<head><meta charset="utf-8"><title>Ogment - Agent configured</title></head>
|
|
61
|
-
<body>
|
|
62
|
-
<h1>Agent configured</h1>
|
|
63
|
-
<p>Agent ${agentName} is now active. You can close this tab.</p>
|
|
64
|
-
</body>
|
|
65
|
-
</html>`;
|
|
66
|
-
};
|
|
67
|
-
const errorPage = (message) => {
|
|
68
|
-
return `<!DOCTYPE html>
|
|
69
|
-
<html lang="en">
|
|
70
|
-
<head><meta charset="utf-8"><title>Ogment - Login failed</title></head>
|
|
71
|
-
<body>
|
|
72
|
-
<h1>Login failed</h1>
|
|
73
|
-
<p>${message}</p>
|
|
74
|
-
</body>
|
|
75
|
-
</html>`;
|
|
76
|
-
};
|
|
77
|
-
const startCallbackServerWithPort = async (createServerFn) => {
|
|
78
|
-
return Result.tryPromise({
|
|
79
|
-
catch: (cause) => new UnexpectedError({
|
|
80
|
-
cause,
|
|
81
|
-
message: "Failed to start local callback server",
|
|
82
|
-
}),
|
|
83
|
-
try: async () => {
|
|
84
|
-
return new Promise((resolve, reject) => {
|
|
85
|
-
const server = createServerFn();
|
|
86
|
-
const onError = (error) => {
|
|
87
|
-
server.removeListener("error", onError);
|
|
88
|
-
reject(error);
|
|
89
|
-
};
|
|
90
|
-
server.once("error", onError);
|
|
91
|
-
server.listen(0, CLI_REDIRECT_HOST, () => {
|
|
92
|
-
server.removeListener("error", onError);
|
|
93
|
-
const address = server.address();
|
|
94
|
-
if (address === null || typeof address === "string") {
|
|
95
|
-
reject(new Error("Could not resolve callback server port"));
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
resolve({
|
|
99
|
-
port: address.port,
|
|
100
|
-
server,
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
const waitForOAuthCallback = async (server, port) => {
|
|
108
|
-
const abortController = new AbortController();
|
|
109
|
-
const timeout = setTimeout(() => {
|
|
110
|
-
abortController.abort();
|
|
111
|
-
}, CALLBACK_TIMEOUT_MILLISECONDS);
|
|
112
|
-
timeout.unref();
|
|
113
|
-
try {
|
|
114
|
-
for await (const event of on(server, "request", { signal: abortController.signal })) {
|
|
115
|
-
const request = event[0];
|
|
116
|
-
const response = event[1];
|
|
117
|
-
const url = new URL(request.url ?? "/", `http://${CLI_REDIRECT_HOST}:${port}`);
|
|
118
|
-
if (url.pathname !== "/callback") {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
const oauthError = url.searchParams.get("error");
|
|
122
|
-
if (oauthError !== null) {
|
|
123
|
-
response.writeHead(200, {
|
|
124
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
125
|
-
});
|
|
126
|
-
response.end(errorPage(`OAuth error: ${oauthError}`));
|
|
127
|
-
return Result.err(new AuthError({
|
|
128
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
129
|
-
message: `OAuth error: ${oauthError}`,
|
|
130
|
-
suggestedCommand: "ogment auth login --browser",
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
const code = url.searchParams.get("code");
|
|
134
|
-
if (typeof code !== "string" || code.length === 0) {
|
|
135
|
-
response.writeHead(400, {
|
|
136
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
137
|
-
});
|
|
138
|
-
response.end(errorPage("No authorization code received."));
|
|
139
|
-
return Result.err(new AuthError({
|
|
140
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
141
|
-
message: "No authorization code in callback",
|
|
142
|
-
suggestedCommand: "ogment auth login --browser",
|
|
143
|
-
}));
|
|
144
|
-
}
|
|
145
|
-
return Result.ok({
|
|
146
|
-
code,
|
|
147
|
-
response,
|
|
148
|
-
state: url.searchParams.get("state") ?? "",
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
154
|
-
return Result.err(new AuthError({
|
|
155
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
156
|
-
message: "Login timed out. No callback received within 5 minutes.",
|
|
157
|
-
suggestedCommand: "ogment auth login --browser",
|
|
158
|
-
}));
|
|
159
|
-
}
|
|
160
|
-
return Result.err(new AuthError({
|
|
161
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
162
|
-
message: "Failed while waiting for OAuth callback.",
|
|
163
|
-
suggestedCommand: "ogment auth login --browser",
|
|
164
|
-
}));
|
|
165
|
-
}
|
|
166
|
-
finally {
|
|
167
|
-
clearTimeout(timeout);
|
|
168
|
-
}
|
|
169
|
-
return Result.err(new AuthError({
|
|
170
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
171
|
-
message: "Login timed out. No callback received within 5 minutes.",
|
|
172
|
-
suggestedCommand: "ogment auth login --browser",
|
|
173
|
-
}));
|
|
174
|
-
};
|
|
175
|
-
const waitForAgentCallback = async (server, port) => {
|
|
176
|
-
const abortController = new AbortController();
|
|
177
|
-
const timeout = setTimeout(() => {
|
|
178
|
-
abortController.abort();
|
|
179
|
-
}, CALLBACK_TIMEOUT_MILLISECONDS);
|
|
180
|
-
timeout.unref();
|
|
181
|
-
try {
|
|
182
|
-
for await (const event of on(server, "request", { signal: abortController.signal })) {
|
|
183
|
-
const request = event[0];
|
|
184
|
-
const response = event[1];
|
|
185
|
-
const url = new URL(request.url ?? "/", `http://${CLI_REDIRECT_HOST}:${port}`);
|
|
186
|
-
if (url.pathname !== "/agent-callback") {
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const parsedCallback = parseWithSchema(browserAgentCallbackSchema, Object.fromEntries(url.searchParams.entries()), "agent callback");
|
|
190
|
-
if (Result.isError(parsedCallback)) {
|
|
191
|
-
response.writeHead(400, {
|
|
192
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
193
|
-
});
|
|
194
|
-
response.end(errorPage("No exchange code received."));
|
|
195
|
-
return Result.err(new AuthError({
|
|
196
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
197
|
-
message: "No exchange code in callback",
|
|
198
|
-
suggestedCommand: "ogment auth login --browser",
|
|
199
|
-
}));
|
|
200
|
-
}
|
|
201
|
-
const agentName = parsedCallback.value.agent_name ?? "CLI Agent";
|
|
202
|
-
response.writeHead(200, {
|
|
203
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
204
|
-
});
|
|
205
|
-
response.end(successPage(agentName));
|
|
206
|
-
return Result.ok({
|
|
207
|
-
agentName,
|
|
208
|
-
exchangeCode: parsedCallback.value.exchange_code,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
214
|
-
return Result.err(new AuthError({
|
|
215
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
216
|
-
message: "Agent selection timed out.",
|
|
217
|
-
suggestedCommand: "ogment auth login --browser",
|
|
218
|
-
}));
|
|
219
|
-
}
|
|
220
|
-
return Result.err(new AuthError({
|
|
221
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
222
|
-
message: "Failed while waiting for agent callback.",
|
|
223
|
-
suggestedCommand: "ogment auth login --browser",
|
|
224
|
-
}));
|
|
225
|
-
}
|
|
226
|
-
finally {
|
|
227
|
-
clearTimeout(timeout);
|
|
228
|
-
}
|
|
229
|
-
return Result.err(new AuthError({
|
|
230
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
231
|
-
message: "Agent selection timed out.",
|
|
232
|
-
suggestedCommand: "ogment auth login --browser",
|
|
233
|
-
}));
|
|
234
|
-
};
|
|
235
25
|
const deviceCodeUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/device/code`;
|
|
236
26
|
const deviceTokenUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/device/token`;
|
|
237
27
|
const revokeUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/cli/revoke-self`;
|
|
238
|
-
const oauthAuthorizeUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/authorize`;
|
|
239
|
-
const oauthRegisterUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/register`;
|
|
240
|
-
const oauthTokenUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/token`;
|
|
241
|
-
const cliExchangeUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/cli/exchange`;
|
|
242
|
-
const agentSelectUrl = (baseUrl) => `${baseUrl}/cli/agent-select`;
|
|
243
28
|
export const createAuthService = (deps) => {
|
|
244
29
|
const now = deps.now ?? Date.now;
|
|
245
30
|
const sleep = deps.sleep ?? defaultSleep;
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
const hostnameFn = deps.hostnameFn ?? hostname;
|
|
249
|
-
const retryConfig = remoteRetryConfig();
|
|
250
|
-
const requestWithRetry = async (input, init) => {
|
|
251
|
-
return Result.tryPromise({
|
|
252
|
-
catch: (cause) => cause,
|
|
253
|
-
try: async () => {
|
|
254
|
-
const response = await deps.httpClient.request(input, init);
|
|
255
|
-
if (Result.isError(response)) {
|
|
256
|
-
throw response.error;
|
|
257
|
-
}
|
|
258
|
-
return response.value;
|
|
259
|
-
},
|
|
260
|
-
}, retryConfig);
|
|
31
|
+
const requestRemote = async (input, init) => {
|
|
32
|
+
return deps.httpClient.request(input, init);
|
|
261
33
|
};
|
|
262
34
|
const loginWithDevice = async (options) => {
|
|
263
|
-
const startFlowResponse = await
|
|
35
|
+
const startFlowResponse = await requestRemote(deviceCodeUrl(deps.baseUrl), {
|
|
264
36
|
headers: {
|
|
265
37
|
"Content-Type": "application/json",
|
|
266
38
|
},
|
|
@@ -273,14 +45,19 @@ export const createAuthService = (deps) => {
|
|
|
273
45
|
const body = await readResponseText(startFlowResponse.value);
|
|
274
46
|
return Result.err(new RemoteRequestError({
|
|
275
47
|
body,
|
|
48
|
+
httpStatus: startFlowResponse.value.status,
|
|
276
49
|
message: "Failed to start device login",
|
|
277
|
-
|
|
50
|
+
operation: "auth/device/start",
|
|
51
|
+
raw: body,
|
|
52
|
+
source: "http",
|
|
278
53
|
}));
|
|
279
54
|
}
|
|
280
55
|
const startPayload = await Result.tryPromise({
|
|
281
56
|
catch: () => new RemoteRequestError({
|
|
57
|
+
httpStatus: startFlowResponse.value.status,
|
|
282
58
|
message: "Failed to parse device login start payload",
|
|
283
|
-
|
|
59
|
+
operation: "auth/device/start",
|
|
60
|
+
source: "http",
|
|
284
61
|
}),
|
|
285
62
|
try: async () => startFlowResponse.value.json(),
|
|
286
63
|
});
|
|
@@ -299,7 +76,7 @@ export const createAuthService = (deps) => {
|
|
|
299
76
|
const interval = parsedStartPayload.value.data.interval * 1000;
|
|
300
77
|
while (now() < deadline) {
|
|
301
78
|
await sleep(interval);
|
|
302
|
-
const pollResponse = await
|
|
79
|
+
const pollResponse = await requestRemote(deviceTokenUrl(deps.baseUrl), {
|
|
303
80
|
body: JSON.stringify({
|
|
304
81
|
device_code: parsedStartPayload.value.data.device_code,
|
|
305
82
|
}),
|
|
@@ -315,28 +92,33 @@ export const createAuthService = (deps) => {
|
|
|
315
92
|
return Result.err(new AuthError({
|
|
316
93
|
code: ERROR_CODE.authDeviceExpired,
|
|
317
94
|
message: "Device login code expired. Run `ogment auth login` again.",
|
|
318
|
-
|
|
95
|
+
recovery: { command: "ogment auth login" },
|
|
319
96
|
}));
|
|
320
97
|
}
|
|
321
98
|
if (pollResponse.value.status === 404) {
|
|
322
99
|
return Result.err(new AuthError({
|
|
323
100
|
code: ERROR_CODE.authInvalidCredentials,
|
|
324
101
|
message: "Invalid device login code. Run `ogment auth login` again.",
|
|
325
|
-
|
|
102
|
+
recovery: { command: "ogment auth login" },
|
|
326
103
|
}));
|
|
327
104
|
}
|
|
328
105
|
if (!pollResponse.value.ok) {
|
|
329
106
|
const body = await readResponseText(pollResponse.value);
|
|
330
107
|
return Result.err(new RemoteRequestError({
|
|
331
108
|
body,
|
|
109
|
+
httpStatus: pollResponse.value.status,
|
|
332
110
|
message: "Failed to poll device login status",
|
|
333
|
-
|
|
111
|
+
operation: "auth/device/poll",
|
|
112
|
+
raw: body,
|
|
113
|
+
source: "http",
|
|
334
114
|
}));
|
|
335
115
|
}
|
|
336
116
|
const pollPayload = await Result.tryPromise({
|
|
337
117
|
catch: () => new RemoteRequestError({
|
|
118
|
+
httpStatus: pollResponse.value.status,
|
|
338
119
|
message: "Failed to parse device login poll payload",
|
|
339
|
-
|
|
120
|
+
operation: "auth/device/poll",
|
|
121
|
+
source: "http",
|
|
340
122
|
}),
|
|
341
123
|
try: async () => pollResponse.value.json(),
|
|
342
124
|
});
|
|
@@ -359,241 +141,16 @@ export const createAuthService = (deps) => {
|
|
|
359
141
|
}
|
|
360
142
|
return Result.ok({
|
|
361
143
|
agentName: approvedPayload.value.data.agent_name ?? "CLI Agent",
|
|
362
|
-
|
|
144
|
+
loggedIn: true,
|
|
145
|
+
outcome: "authenticated",
|
|
363
146
|
});
|
|
364
147
|
}
|
|
365
148
|
return Result.err(new AuthError({
|
|
366
149
|
code: ERROR_CODE.authDeviceExpired,
|
|
367
150
|
message: "Device login code expired. Run `ogment auth login` again.",
|
|
368
|
-
|
|
151
|
+
recovery: { command: "ogment auth login" },
|
|
369
152
|
}));
|
|
370
153
|
};
|
|
371
|
-
const loginWithBrowser = async () => {
|
|
372
|
-
const callbackServerResult = await startCallbackServerWithPort(createServerFn);
|
|
373
|
-
if (Result.isError(callbackServerResult)) {
|
|
374
|
-
return callbackServerResult;
|
|
375
|
-
}
|
|
376
|
-
const { port, server } = callbackServerResult.value;
|
|
377
|
-
const redirectUri = `http://${CLI_REDIRECT_HOST}:${port}/callback`;
|
|
378
|
-
const registerResponse = await requestWithRetry(oauthRegisterUrl(deps.baseUrl), {
|
|
379
|
-
body: JSON.stringify({
|
|
380
|
-
client_name: CLI_CLIENT_NAME,
|
|
381
|
-
grant_types: ["authorization_code"],
|
|
382
|
-
redirect_uris: [redirectUri],
|
|
383
|
-
response_types: ["code"],
|
|
384
|
-
token_endpoint_auth_method: "client_secret_post",
|
|
385
|
-
}),
|
|
386
|
-
headers: {
|
|
387
|
-
"Content-Type": "application/json",
|
|
388
|
-
},
|
|
389
|
-
method: "POST",
|
|
390
|
-
});
|
|
391
|
-
if (Result.isError(registerResponse)) {
|
|
392
|
-
await closeServer(server);
|
|
393
|
-
return registerResponse;
|
|
394
|
-
}
|
|
395
|
-
if (!registerResponse.value.ok) {
|
|
396
|
-
const body = await readResponseText(registerResponse.value);
|
|
397
|
-
await closeServer(server);
|
|
398
|
-
return Result.err(new RemoteRequestError({
|
|
399
|
-
body,
|
|
400
|
-
message: "Client registration failed",
|
|
401
|
-
status: registerResponse.value.status,
|
|
402
|
-
}));
|
|
403
|
-
}
|
|
404
|
-
const registerPayload = await Result.tryPromise({
|
|
405
|
-
catch: () => new RemoteRequestError({
|
|
406
|
-
message: "Failed to parse client registration payload",
|
|
407
|
-
status: registerResponse.value.status,
|
|
408
|
-
}),
|
|
409
|
-
try: async () => registerResponse.value.json(),
|
|
410
|
-
});
|
|
411
|
-
if (Result.isError(registerPayload)) {
|
|
412
|
-
await closeServer(server);
|
|
413
|
-
return registerPayload;
|
|
414
|
-
}
|
|
415
|
-
const parsedClient = parseWithSchema(oauthClientRegistrationSchema, registerPayload.value, "oauth client registration response");
|
|
416
|
-
if (Result.isError(parsedClient)) {
|
|
417
|
-
await closeServer(server);
|
|
418
|
-
return parsedClient;
|
|
419
|
-
}
|
|
420
|
-
const codeVerifier = generateCodeVerifier();
|
|
421
|
-
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
422
|
-
const state = randomBytes(16).toString("hex");
|
|
423
|
-
const authorizeUrl = new URL(oauthAuthorizeUrl(deps.baseUrl));
|
|
424
|
-
authorizeUrl.searchParams.set("client_id", parsedClient.value.client_id);
|
|
425
|
-
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
426
|
-
authorizeUrl.searchParams.set("response_type", "code");
|
|
427
|
-
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
428
|
-
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
429
|
-
authorizeUrl.searchParams.set("state", state);
|
|
430
|
-
const oauthCallbackPromise = waitForOAuthCallback(server, port);
|
|
431
|
-
const openResult = await deps.browserOpener.open(authorizeUrl.toString());
|
|
432
|
-
if (Result.isError(openResult)) {
|
|
433
|
-
await closeServer(server);
|
|
434
|
-
return openResult;
|
|
435
|
-
}
|
|
436
|
-
const oauthCallbackResult = await oauthCallbackPromise;
|
|
437
|
-
if (Result.isError(oauthCallbackResult)) {
|
|
438
|
-
await closeServer(server);
|
|
439
|
-
return oauthCallbackResult;
|
|
440
|
-
}
|
|
441
|
-
if (oauthCallbackResult.value.state !== state) {
|
|
442
|
-
oauthCallbackResult.value.response.writeHead(302, {
|
|
443
|
-
Location: "about:blank",
|
|
444
|
-
});
|
|
445
|
-
oauthCallbackResult.value.response.end();
|
|
446
|
-
await closeServer(server);
|
|
447
|
-
return Result.err(new AuthError({
|
|
448
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
449
|
-
message: "OAuth state mismatch - possible CSRF attack.",
|
|
450
|
-
suggestedCommand: "ogment auth login --browser",
|
|
451
|
-
}));
|
|
452
|
-
}
|
|
453
|
-
const tokenBody = new URLSearchParams({
|
|
454
|
-
client_id: parsedClient.value.client_id,
|
|
455
|
-
code: oauthCallbackResult.value.code,
|
|
456
|
-
code_verifier: codeVerifier,
|
|
457
|
-
grant_type: "authorization_code",
|
|
458
|
-
redirect_uri: redirectUri,
|
|
459
|
-
});
|
|
460
|
-
if (typeof parsedClient.value.client_secret === "string" &&
|
|
461
|
-
parsedClient.value.client_secret.length > 0) {
|
|
462
|
-
tokenBody.set("client_secret", parsedClient.value.client_secret);
|
|
463
|
-
}
|
|
464
|
-
const tokenResponse = await requestWithRetry(oauthTokenUrl(deps.baseUrl), {
|
|
465
|
-
body: tokenBody.toString(),
|
|
466
|
-
headers: {
|
|
467
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
468
|
-
},
|
|
469
|
-
method: "POST",
|
|
470
|
-
});
|
|
471
|
-
if (Result.isError(tokenResponse)) {
|
|
472
|
-
await closeServer(server);
|
|
473
|
-
return tokenResponse;
|
|
474
|
-
}
|
|
475
|
-
if (!tokenResponse.value.ok) {
|
|
476
|
-
const body = await readResponseText(tokenResponse.value);
|
|
477
|
-
await closeServer(server);
|
|
478
|
-
return Result.err(new RemoteRequestError({
|
|
479
|
-
body,
|
|
480
|
-
message: "Token exchange failed",
|
|
481
|
-
status: tokenResponse.value.status,
|
|
482
|
-
}));
|
|
483
|
-
}
|
|
484
|
-
const tokenPayload = await Result.tryPromise({
|
|
485
|
-
catch: () => new RemoteRequestError({
|
|
486
|
-
message: "Failed to parse token payload",
|
|
487
|
-
status: tokenResponse.value.status,
|
|
488
|
-
}),
|
|
489
|
-
try: async () => tokenResponse.value.json(),
|
|
490
|
-
});
|
|
491
|
-
if (Result.isError(tokenPayload)) {
|
|
492
|
-
await closeServer(server);
|
|
493
|
-
return tokenPayload;
|
|
494
|
-
}
|
|
495
|
-
const parsedToken = parseWithSchema(oauthTokenSchema, tokenPayload.value, "oauth token response");
|
|
496
|
-
if (Result.isError(parsedToken)) {
|
|
497
|
-
await closeServer(server);
|
|
498
|
-
return parsedToken;
|
|
499
|
-
}
|
|
500
|
-
const pickerUrl = new URL(agentSelectUrl(deps.baseUrl));
|
|
501
|
-
pickerUrl.searchParams.set("token", parsedToken.value.access_token);
|
|
502
|
-
pickerUrl.searchParams.set("callback", `http://${CLI_REDIRECT_HOST}:${port}/agent-callback`);
|
|
503
|
-
pickerUrl.searchParams.set("env", detectEnvironment());
|
|
504
|
-
pickerUrl.searchParams.set("host", hostnameFn());
|
|
505
|
-
oauthCallbackResult.value.response.writeHead(302, {
|
|
506
|
-
Location: pickerUrl.toString(),
|
|
507
|
-
});
|
|
508
|
-
oauthCallbackResult.value.response.end();
|
|
509
|
-
const agentCallbackResult = await waitForAgentCallback(server, port);
|
|
510
|
-
await closeServer(server);
|
|
511
|
-
if (Result.isError(agentCallbackResult)) {
|
|
512
|
-
return agentCallbackResult;
|
|
513
|
-
}
|
|
514
|
-
const exchangeRequestPayload = {
|
|
515
|
-
exchange_code: agentCallbackResult.value.exchangeCode,
|
|
516
|
-
};
|
|
517
|
-
const parsedExchangeRequestPayload = parseWithSchema(cliExchangeRequestSchema, exchangeRequestPayload, "browser exchange request");
|
|
518
|
-
if (Result.isError(parsedExchangeRequestPayload)) {
|
|
519
|
-
return parsedExchangeRequestPayload;
|
|
520
|
-
}
|
|
521
|
-
const exchangeResponse = await requestWithRetry(cliExchangeUrl(deps.baseUrl), {
|
|
522
|
-
body: JSON.stringify(parsedExchangeRequestPayload.value),
|
|
523
|
-
headers: {
|
|
524
|
-
"Content-Type": "application/json",
|
|
525
|
-
},
|
|
526
|
-
method: "POST",
|
|
527
|
-
});
|
|
528
|
-
if (Result.isError(exchangeResponse)) {
|
|
529
|
-
return exchangeResponse;
|
|
530
|
-
}
|
|
531
|
-
if (!exchangeResponse.value.ok) {
|
|
532
|
-
const body = await readResponseText(exchangeResponse.value);
|
|
533
|
-
const parsedExchangeErrorPayload = Result.try({
|
|
534
|
-
catch: () => null,
|
|
535
|
-
try: () => JSON.parse(body),
|
|
536
|
-
});
|
|
537
|
-
if (Result.isOk(parsedExchangeErrorPayload)) {
|
|
538
|
-
const parsedExchangeError = parseWithSchema(cliExchangeErrorSchema, parsedExchangeErrorPayload.value, "browser exchange error response");
|
|
539
|
-
if (Result.isOk(parsedExchangeError)) {
|
|
540
|
-
const exchangeErrorCode = parsedExchangeError.value.error;
|
|
541
|
-
if (exchangeErrorCode === "expired_exchange_code") {
|
|
542
|
-
return Result.err(new AuthError({
|
|
543
|
-
code: ERROR_CODE.authDeviceExpired,
|
|
544
|
-
message: "Browser login code expired. Run `ogment auth login --browser` again.",
|
|
545
|
-
suggestedCommand: "ogment auth login --browser",
|
|
546
|
-
}));
|
|
547
|
-
}
|
|
548
|
-
if (exchangeErrorCode === "invalid_exchange_code") {
|
|
549
|
-
return Result.err(new AuthError({
|
|
550
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
551
|
-
message: "Invalid browser login code. Run `ogment auth login --browser` again.",
|
|
552
|
-
suggestedCommand: "ogment auth login --browser",
|
|
553
|
-
}));
|
|
554
|
-
}
|
|
555
|
-
if (exchangeErrorCode === "authorization_pending") {
|
|
556
|
-
return Result.err(new AuthError({
|
|
557
|
-
code: ERROR_CODE.authDevicePending,
|
|
558
|
-
message: "Browser login authorization is still pending.",
|
|
559
|
-
retryable: true,
|
|
560
|
-
suggestedCommand: "ogment auth login --browser",
|
|
561
|
-
}));
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return Result.err(new RemoteRequestError({
|
|
566
|
-
body,
|
|
567
|
-
message: "Failed to exchange browser login code",
|
|
568
|
-
status: exchangeResponse.value.status,
|
|
569
|
-
}));
|
|
570
|
-
}
|
|
571
|
-
const exchangePayload = await Result.tryPromise({
|
|
572
|
-
catch: () => new RemoteRequestError({
|
|
573
|
-
message: "Failed to parse browser exchange payload",
|
|
574
|
-
status: exchangeResponse.value.status,
|
|
575
|
-
}),
|
|
576
|
-
try: async () => exchangeResponse.value.json(),
|
|
577
|
-
});
|
|
578
|
-
if (Result.isError(exchangePayload)) {
|
|
579
|
-
return exchangePayload;
|
|
580
|
-
}
|
|
581
|
-
const parsedExchangePayload = parseWithSchema(cliExchangeSuccessSchema, exchangePayload.value, "browser exchange response");
|
|
582
|
-
if (Result.isError(parsedExchangePayload)) {
|
|
583
|
-
return parsedExchangePayload;
|
|
584
|
-
}
|
|
585
|
-
const saveResult = deps.credentialsStore.save({
|
|
586
|
-
agentName: parsedExchangePayload.value.data.name,
|
|
587
|
-
apiKey: parsedExchangePayload.value.data.apiKey,
|
|
588
|
-
});
|
|
589
|
-
if (Result.isError(saveResult)) {
|
|
590
|
-
return saveResult;
|
|
591
|
-
}
|
|
592
|
-
return Result.ok({
|
|
593
|
-
agentName: parsedExchangePayload.value.data.name,
|
|
594
|
-
alreadyLoggedIn: false,
|
|
595
|
-
});
|
|
596
|
-
};
|
|
597
154
|
return {
|
|
598
155
|
login: async (options) => {
|
|
599
156
|
if (options.mode === "apiKey" && options.apiKey.length > 0) {
|
|
@@ -606,21 +163,15 @@ export const createAuthService = (deps) => {
|
|
|
606
163
|
}
|
|
607
164
|
return Result.ok({
|
|
608
165
|
agentName: "CLI Agent",
|
|
609
|
-
|
|
166
|
+
loggedIn: true,
|
|
167
|
+
outcome: "authenticated",
|
|
610
168
|
});
|
|
611
169
|
}
|
|
612
170
|
if (options.mode === "apiKey") {
|
|
613
171
|
return Result.err(new ValidationError({
|
|
614
172
|
code: ERROR_CODE.validationInvalidInput,
|
|
615
173
|
message: "Missing API key value. Provide a non-empty API key.",
|
|
616
|
-
|
|
617
|
-
}));
|
|
618
|
-
}
|
|
619
|
-
if (options.nonInteractive && options.mode === "browser") {
|
|
620
|
-
return Result.err(new ValidationError({
|
|
621
|
-
code: ERROR_CODE.validationInvalidInput,
|
|
622
|
-
message: "Use `ogment auth login` in non-interactive mode.",
|
|
623
|
-
suggestedCommand: "ogment auth login",
|
|
174
|
+
recovery: { command: "ogment auth login --api-key <key>" },
|
|
624
175
|
}));
|
|
625
176
|
}
|
|
626
177
|
const stored = deps.credentialsStore.load();
|
|
@@ -630,13 +181,11 @@ export const createAuthService = (deps) => {
|
|
|
630
181
|
if (stored.value !== null) {
|
|
631
182
|
return Result.ok({
|
|
632
183
|
agentName: stored.value.agentName ?? "CLI Agent",
|
|
633
|
-
|
|
184
|
+
loggedIn: true,
|
|
185
|
+
outcome: "already_authenticated",
|
|
634
186
|
});
|
|
635
187
|
}
|
|
636
|
-
|
|
637
|
-
return loginWithDevice(options);
|
|
638
|
-
}
|
|
639
|
-
return loginWithBrowser();
|
|
188
|
+
return loginWithDevice(options);
|
|
640
189
|
},
|
|
641
190
|
logout: async () => {
|
|
642
191
|
const stored = deps.credentialsStore.load();
|
|
@@ -649,7 +198,7 @@ export const createAuthService = (deps) => {
|
|
|
649
198
|
revoked: false,
|
|
650
199
|
});
|
|
651
200
|
}
|
|
652
|
-
const revokeResult = await
|
|
201
|
+
const revokeResult = await requestRemote(revokeUrl(deps.baseUrl), {
|
|
653
202
|
headers: {
|
|
654
203
|
Authorization: `Bearer ${stored.value.apiKey}`,
|
|
655
204
|
},
|
|
@@ -682,7 +231,7 @@ export const createAuthService = (deps) => {
|
|
|
682
231
|
return Result.err(new AuthError({
|
|
683
232
|
code: ERROR_CODE.authRequired,
|
|
684
233
|
message: "Not logged in. Run `ogment auth login` or set OGMENT_API_KEY.",
|
|
685
|
-
|
|
234
|
+
recovery: { command: "ogment auth login" },
|
|
686
235
|
}));
|
|
687
236
|
},
|
|
688
237
|
status: async (overrideApiKey) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"info.d.ts","sourceRoot":"","sources":["../../src/services/info.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EACV,aAAa,EAGb,WAAW,EAEZ,MAAM,oBAAoB,CAAC;AAsB5B,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACxD;AAED,UAAU,eAAe;IACvB,cAAc,EAAE,cAAc,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,4BAA4B,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5C,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"info.d.ts","sourceRoot":"","sources":["../../src/services/info.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,KAAK,EACV,aAAa,EAGb,WAAW,EAEZ,MAAM,oBAAoB,CAAC;AAsB5B,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACxD;AAED,UAAU,eAAe;IACvB,cAAc,EAAE,cAAc,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,4BAA4B,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5C,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACzC,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAmGD,eAAO,MAAM,iBAAiB,GAAI,MAAM,eAAe,KAAG,WAwQzD,CAAC"}
|