@ogment-ai/cli 0.5.0 → 0.7.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/dist/cli.js +21382 -4
- package/dist/cli.js.map +1 -0
- package/package.json +46 -55
- package/README.md +0 -143
- package/dist/cli/commands.d.ts +0 -38
- package/dist/cli/commands.d.ts.map +0 -1
- package/dist/cli/commands.js +0 -57
- package/dist/cli/execute.d.ts +0 -11
- package/dist/cli/execute.d.ts.map +0 -1
- package/dist/cli/execute.js +0 -487
- package/dist/cli/invocations.d.ts +0 -32
- package/dist/cli/invocations.d.ts.map +0 -1
- package/dist/cli/invocations.js +0 -1
- package/dist/cli/parse-errors.d.ts +0 -17
- package/dist/cli/parse-errors.d.ts.map +0 -1
- package/dist/cli/parse-errors.js +0 -184
- package/dist/cli/program.d.ts +0 -10
- package/dist/cli/program.d.ts.map +0 -1
- package/dist/cli/program.js +0 -183
- package/dist/cli/run.d.ts +0 -6
- package/dist/cli/run.d.ts.map +0 -1
- package/dist/cli/run.js +0 -83
- package/dist/cli/runtime.d.ts +0 -22
- package/dist/cli/runtime.d.ts.map +0 -1
- package/dist/cli/runtime.js +0 -86
- package/dist/cli.d.ts +0 -4
- package/dist/cli.d.ts.map +0 -1
- package/dist/commands/auth.d.ts +0 -22
- package/dist/commands/auth.d.ts.map +0 -1
- package/dist/commands/auth.js +0 -29
- package/dist/commands/catalog.d.ts +0 -31
- package/dist/commands/catalog.d.ts.map +0 -1
- package/dist/commands/catalog.js +0 -167
- package/dist/commands/context.d.ts +0 -15
- package/dist/commands/context.d.ts.map +0 -1
- package/dist/commands/context.js +0 -1
- package/dist/commands/invoke.d.ts +0 -17
- package/dist/commands/invoke.d.ts.map +0 -1
- package/dist/commands/invoke.js +0 -173
- package/dist/commands/server-context.d.ts +0 -14
- package/dist/commands/server-context.d.ts.map +0 -1
- package/dist/commands/server-context.js +0 -26
- package/dist/commands/status.d.ts +0 -12
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js +0 -5
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- 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/infra/credentials.d.ts +0 -22
- package/dist/infra/credentials.d.ts.map +0 -1
- package/dist/infra/credentials.js +0 -81
- package/dist/infra/env.d.ts +0 -15
- package/dist/infra/env.d.ts.map +0 -1
- package/dist/infra/env.js +0 -100
- package/dist/infra/http.d.ts +0 -16
- package/dist/infra/http.d.ts.map +0 -1
- package/dist/infra/http.js +0 -84
- package/dist/output/envelope.d.ts +0 -22
- package/dist/output/envelope.d.ts.map +0 -1
- package/dist/output/envelope.js +0 -67
- package/dist/output/manager.d.ts +0 -49
- package/dist/output/manager.d.ts.map +0 -1
- package/dist/output/manager.js +0 -128
- package/dist/services/account.d.ts +0 -16
- package/dist/services/account.d.ts.map +0 -1
- package/dist/services/account.js +0 -75
- package/dist/services/auth.d.ts +0 -49
- package/dist/services/auth.d.ts.map +0 -1
- package/dist/services/auth.js +0 -739
- package/dist/services/info.d.ts +0 -24
- package/dist/services/info.d.ts.map +0 -1
- package/dist/services/info.js +0 -316
- package/dist/services/mcp-error-mapping.d.ts +0 -9
- package/dist/services/mcp-error-mapping.d.ts.map +0 -1
- package/dist/services/mcp-error-mapping.js +0 -129
- package/dist/services/mcp.d.ts +0 -39
- package/dist/services/mcp.d.ts.map +0 -1
- package/dist/services/mcp.js +0 -169
- package/dist/shared/constants.d.ts +0 -7
- package/dist/shared/constants.d.ts.map +0 -1
- package/dist/shared/constants.js +0 -8
- package/dist/shared/error-codes.d.ts +0 -31
- package/dist/shared/error-codes.d.ts.map +0 -1
- package/dist/shared/error-codes.js +0 -25
- package/dist/shared/error-presentation.d.ts +0 -17
- package/dist/shared/error-presentation.d.ts.map +0 -1
- package/dist/shared/error-presentation.js +0 -151
- package/dist/shared/errors.d.ts +0 -146
- package/dist/shared/errors.d.ts.map +0 -1
- package/dist/shared/errors.js +0 -233
- package/dist/shared/exit-codes.d.ts +0 -15
- package/dist/shared/exit-codes.d.ts.map +0 -1
- package/dist/shared/exit-codes.js +0 -44
- package/dist/shared/guards.d.ts +0 -11
- package/dist/shared/guards.d.ts.map +0 -1
- package/dist/shared/guards.js +0 -29
- package/dist/shared/recovery.d.ts +0 -5
- package/dist/shared/recovery.d.ts.map +0 -1
- package/dist/shared/recovery.js +0 -123
- package/dist/shared/schema-example.d.ts +0 -3
- package/dist/shared/schema-example.d.ts.map +0 -1
- package/dist/shared/schema-example.js +0 -105
- package/dist/shared/schemas.d.ts +0 -15
- package/dist/shared/schemas.d.ts.map +0 -1
- package/dist/shared/schemas.js +0 -15
- package/dist/shared/types.d.ts +0 -225
- package/dist/shared/types.d.ts.map +0 -1
- package/dist/shared/types.js +0 -1
package/dist/services/auth.js
DELETED
|
@@ -1,739 +0,0 @@
|
|
|
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
|
-
import { Result } from "better-result";
|
|
6
|
-
import { detectExecutionEnvironment } from "../infra/env.js";
|
|
7
|
-
import { readResponseText } from "../infra/http.js";
|
|
8
|
-
import { CLI_CLIENT_NAME, CLI_REDIRECT_HOST } from "../shared/constants.js";
|
|
9
|
-
import { ERROR_CODE } from "../shared/error-codes.js";
|
|
10
|
-
import { AuthError, RemoteRequestError, UnexpectedError, ValidationError, } from "../shared/errors.js";
|
|
11
|
-
import { parseWithSchema } from "../shared/guards.js";
|
|
12
|
-
import { browserAgentCallbackSchema, cliExchangeErrorSchema, cliExchangeRequestSchema, cliExchangeSuccessSchema, deviceCodeStartSchema, deviceTokenApprovedSchema, oauthClientRegistrationSchema, oauthTokenSchema, } from "../shared/schemas.js";
|
|
13
|
-
const CALLBACK_TIMEOUT_MILLISECONDS = 5 * 60 * 1000;
|
|
14
|
-
const defaultSleep = async (milliseconds) => {
|
|
15
|
-
await new Promise((resolve) => {
|
|
16
|
-
setTimeout(resolve, milliseconds);
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
|
-
const toPendingPayload = (payload) => {
|
|
20
|
-
if (typeof payload !== "object" || payload === null) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
if (!Object.hasOwn(payload, "error")) {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
const error = payload.error;
|
|
27
|
-
if (error === "authorization_pending") {
|
|
28
|
-
return "authorization_pending";
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
};
|
|
32
|
-
const generateCodeVerifier = () => {
|
|
33
|
-
return randomBytes(32).toString("base64url");
|
|
34
|
-
};
|
|
35
|
-
const generateCodeChallenge = (verifier) => {
|
|
36
|
-
return createHash("sha256").update(verifier).digest("base64url");
|
|
37
|
-
};
|
|
38
|
-
const closeServer = async (server) => {
|
|
39
|
-
await Result.tryPromise({
|
|
40
|
-
catch: () => undefined,
|
|
41
|
-
try: async () => {
|
|
42
|
-
await new Promise((resolve) => {
|
|
43
|
-
server.close(() => {
|
|
44
|
-
resolve();
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
Result.try({
|
|
50
|
-
catch: () => undefined,
|
|
51
|
-
try: () => {
|
|
52
|
-
server.unref();
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
};
|
|
56
|
-
const successPage = (agentName) => {
|
|
57
|
-
return `<!DOCTYPE html>
|
|
58
|
-
<html lang="en">
|
|
59
|
-
<head><meta charset="utf-8"><title>Ogment - Agent configured</title></head>
|
|
60
|
-
<body>
|
|
61
|
-
<h1>Agent configured</h1>
|
|
62
|
-
<p>Agent ${agentName} is now active. You can close this tab.</p>
|
|
63
|
-
</body>
|
|
64
|
-
</html>`;
|
|
65
|
-
};
|
|
66
|
-
const errorPage = (message) => {
|
|
67
|
-
return `<!DOCTYPE html>
|
|
68
|
-
<html lang="en">
|
|
69
|
-
<head><meta charset="utf-8"><title>Ogment - Login failed</title></head>
|
|
70
|
-
<body>
|
|
71
|
-
<h1>Login failed</h1>
|
|
72
|
-
<p>${message}</p>
|
|
73
|
-
</body>
|
|
74
|
-
</html>`;
|
|
75
|
-
};
|
|
76
|
-
const startCallbackServerWithPort = async (createServerFn) => {
|
|
77
|
-
return Result.tryPromise({
|
|
78
|
-
catch: (cause) => new UnexpectedError({
|
|
79
|
-
cause,
|
|
80
|
-
message: "Failed to start local callback server",
|
|
81
|
-
}),
|
|
82
|
-
try: async () => {
|
|
83
|
-
return new Promise((resolve, reject) => {
|
|
84
|
-
const server = createServerFn();
|
|
85
|
-
const onError = (error) => {
|
|
86
|
-
server.removeListener("error", onError);
|
|
87
|
-
reject(error);
|
|
88
|
-
};
|
|
89
|
-
server.once("error", onError);
|
|
90
|
-
server.listen(0, CLI_REDIRECT_HOST, () => {
|
|
91
|
-
server.removeListener("error", onError);
|
|
92
|
-
const address = server.address();
|
|
93
|
-
if (address === null || typeof address === "string") {
|
|
94
|
-
reject(new Error("Could not resolve callback server port"));
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
resolve({
|
|
98
|
-
port: address.port,
|
|
99
|
-
server,
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
};
|
|
106
|
-
const waitForOAuthCallback = async (server, port) => {
|
|
107
|
-
const abortController = new AbortController();
|
|
108
|
-
const timeout = setTimeout(() => {
|
|
109
|
-
abortController.abort();
|
|
110
|
-
}, CALLBACK_TIMEOUT_MILLISECONDS);
|
|
111
|
-
timeout.unref();
|
|
112
|
-
try {
|
|
113
|
-
for await (const event of on(server, "request", { signal: abortController.signal })) {
|
|
114
|
-
const request = event[0];
|
|
115
|
-
const response = event[1];
|
|
116
|
-
const url = new URL(request.url ?? "/", `http://${CLI_REDIRECT_HOST}:${port}`);
|
|
117
|
-
if (url.pathname !== "/callback") {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const oauthError = url.searchParams.get("error");
|
|
121
|
-
if (oauthError !== null) {
|
|
122
|
-
response.writeHead(200, {
|
|
123
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
124
|
-
});
|
|
125
|
-
response.end(errorPage(`OAuth error: ${oauthError}`));
|
|
126
|
-
return Result.err(new AuthError({
|
|
127
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
128
|
-
message: `OAuth error: ${oauthError}`,
|
|
129
|
-
recovery: { command: "ogment auth login --browser" },
|
|
130
|
-
}));
|
|
131
|
-
}
|
|
132
|
-
const code = url.searchParams.get("code");
|
|
133
|
-
if (typeof code !== "string" || code.length === 0) {
|
|
134
|
-
response.writeHead(400, {
|
|
135
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
136
|
-
});
|
|
137
|
-
response.end(errorPage("No authorization code received."));
|
|
138
|
-
return Result.err(new AuthError({
|
|
139
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
140
|
-
message: "No authorization code in callback",
|
|
141
|
-
recovery: { command: "ogment auth login --browser" },
|
|
142
|
-
}));
|
|
143
|
-
}
|
|
144
|
-
return Result.ok({
|
|
145
|
-
code,
|
|
146
|
-
response,
|
|
147
|
-
state: url.searchParams.get("state") ?? "",
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
153
|
-
return Result.err(new AuthError({
|
|
154
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
155
|
-
message: "Login timed out. No callback received within 5 minutes.",
|
|
156
|
-
recovery: { command: "ogment auth login --browser" },
|
|
157
|
-
}));
|
|
158
|
-
}
|
|
159
|
-
return Result.err(new AuthError({
|
|
160
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
161
|
-
message: "Failed while waiting for OAuth callback.",
|
|
162
|
-
recovery: { command: "ogment auth login --browser" },
|
|
163
|
-
}));
|
|
164
|
-
}
|
|
165
|
-
finally {
|
|
166
|
-
clearTimeout(timeout);
|
|
167
|
-
}
|
|
168
|
-
return Result.err(new AuthError({
|
|
169
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
170
|
-
message: "Login timed out. No callback received within 5 minutes.",
|
|
171
|
-
recovery: { command: "ogment auth login --browser" },
|
|
172
|
-
}));
|
|
173
|
-
};
|
|
174
|
-
const waitForAgentCallback = async (server, port) => {
|
|
175
|
-
const abortController = new AbortController();
|
|
176
|
-
const timeout = setTimeout(() => {
|
|
177
|
-
abortController.abort();
|
|
178
|
-
}, CALLBACK_TIMEOUT_MILLISECONDS);
|
|
179
|
-
timeout.unref();
|
|
180
|
-
try {
|
|
181
|
-
for await (const event of on(server, "request", { signal: abortController.signal })) {
|
|
182
|
-
const request = event[0];
|
|
183
|
-
const response = event[1];
|
|
184
|
-
const url = new URL(request.url ?? "/", `http://${CLI_REDIRECT_HOST}:${port}`);
|
|
185
|
-
if (url.pathname !== "/agent-callback") {
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
const parsedCallback = parseWithSchema(browserAgentCallbackSchema, Object.fromEntries(url.searchParams.entries()), "agent callback");
|
|
189
|
-
if (Result.isError(parsedCallback)) {
|
|
190
|
-
response.writeHead(400, {
|
|
191
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
192
|
-
});
|
|
193
|
-
response.end(errorPage("No exchange code received."));
|
|
194
|
-
return Result.err(new AuthError({
|
|
195
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
196
|
-
message: "No exchange code in callback",
|
|
197
|
-
recovery: { command: "ogment auth login --browser" },
|
|
198
|
-
}));
|
|
199
|
-
}
|
|
200
|
-
const agentName = parsedCallback.value.agent_name ?? "CLI Agent";
|
|
201
|
-
response.writeHead(200, {
|
|
202
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
203
|
-
});
|
|
204
|
-
response.end(successPage(agentName));
|
|
205
|
-
return Result.ok({
|
|
206
|
-
agentName,
|
|
207
|
-
exchangeCode: parsedCallback.value.exchange_code,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
213
|
-
return Result.err(new AuthError({
|
|
214
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
215
|
-
message: "Agent selection timed out.",
|
|
216
|
-
recovery: { command: "ogment auth login --browser" },
|
|
217
|
-
}));
|
|
218
|
-
}
|
|
219
|
-
return Result.err(new AuthError({
|
|
220
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
221
|
-
message: "Failed while waiting for agent callback.",
|
|
222
|
-
recovery: { command: "ogment auth login --browser" },
|
|
223
|
-
}));
|
|
224
|
-
}
|
|
225
|
-
finally {
|
|
226
|
-
clearTimeout(timeout);
|
|
227
|
-
}
|
|
228
|
-
return Result.err(new AuthError({
|
|
229
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
230
|
-
message: "Agent selection timed out.",
|
|
231
|
-
recovery: { command: "ogment auth login --browser" },
|
|
232
|
-
}));
|
|
233
|
-
};
|
|
234
|
-
const deviceCodeUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/device/code`;
|
|
235
|
-
const deviceTokenUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/device/token`;
|
|
236
|
-
const revokeUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/cli/revoke-self`;
|
|
237
|
-
const oauthAuthorizeUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/authorize`;
|
|
238
|
-
const oauthRegisterUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/register`;
|
|
239
|
-
const oauthTokenUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/token`;
|
|
240
|
-
const cliExchangeUrl = (baseUrl) => `${baseUrl}/api/v1/mcp-auth/cli/exchange`;
|
|
241
|
-
const agentSelectUrl = (baseUrl) => `${baseUrl}/cli/agent-select`;
|
|
242
|
-
export const createAuthService = (deps) => {
|
|
243
|
-
const now = deps.now ?? Date.now;
|
|
244
|
-
const sleep = deps.sleep ?? defaultSleep;
|
|
245
|
-
const createServerFn = deps.createServerFn ?? createServer;
|
|
246
|
-
const detectEnvironment = deps.detectEnvironment ?? detectExecutionEnvironment;
|
|
247
|
-
const hostnameFn = deps.hostnameFn ?? hostname;
|
|
248
|
-
const requestRemote = async (input, init) => {
|
|
249
|
-
return deps.httpClient.request(input, init);
|
|
250
|
-
};
|
|
251
|
-
const loginWithDevice = async (options) => {
|
|
252
|
-
const startFlowResponse = await requestRemote(deviceCodeUrl(deps.baseUrl), {
|
|
253
|
-
headers: {
|
|
254
|
-
"Content-Type": "application/json",
|
|
255
|
-
},
|
|
256
|
-
method: "POST",
|
|
257
|
-
});
|
|
258
|
-
if (Result.isError(startFlowResponse)) {
|
|
259
|
-
return startFlowResponse;
|
|
260
|
-
}
|
|
261
|
-
if (!startFlowResponse.value.ok) {
|
|
262
|
-
const body = await readResponseText(startFlowResponse.value);
|
|
263
|
-
return Result.err(new RemoteRequestError({
|
|
264
|
-
body,
|
|
265
|
-
httpStatus: startFlowResponse.value.status,
|
|
266
|
-
message: "Failed to start device login",
|
|
267
|
-
operation: "auth/device/start",
|
|
268
|
-
raw: body,
|
|
269
|
-
source: "http",
|
|
270
|
-
}));
|
|
271
|
-
}
|
|
272
|
-
const startPayload = await Result.tryPromise({
|
|
273
|
-
catch: () => new RemoteRequestError({
|
|
274
|
-
httpStatus: startFlowResponse.value.status,
|
|
275
|
-
message: "Failed to parse device login start payload",
|
|
276
|
-
operation: "auth/device/start",
|
|
277
|
-
source: "http",
|
|
278
|
-
}),
|
|
279
|
-
try: async () => startFlowResponse.value.json(),
|
|
280
|
-
});
|
|
281
|
-
if (Result.isError(startPayload)) {
|
|
282
|
-
return startPayload;
|
|
283
|
-
}
|
|
284
|
-
const parsedStartPayload = parseWithSchema(deviceCodeStartSchema, startPayload.value, "device login start response");
|
|
285
|
-
if (Result.isError(parsedStartPayload)) {
|
|
286
|
-
return parsedStartPayload;
|
|
287
|
-
}
|
|
288
|
-
options.onPending?.({
|
|
289
|
-
userCode: parsedStartPayload.value.data.user_code,
|
|
290
|
-
verificationUri: parsedStartPayload.value.data.verification_uri,
|
|
291
|
-
});
|
|
292
|
-
const deadline = now() + parsedStartPayload.value.data.expires_in * 1000;
|
|
293
|
-
const interval = parsedStartPayload.value.data.interval * 1000;
|
|
294
|
-
while (now() < deadline) {
|
|
295
|
-
await sleep(interval);
|
|
296
|
-
const pollResponse = await requestRemote(deviceTokenUrl(deps.baseUrl), {
|
|
297
|
-
body: JSON.stringify({
|
|
298
|
-
device_code: parsedStartPayload.value.data.device_code,
|
|
299
|
-
}),
|
|
300
|
-
headers: {
|
|
301
|
-
"Content-Type": "application/json",
|
|
302
|
-
},
|
|
303
|
-
method: "POST",
|
|
304
|
-
});
|
|
305
|
-
if (Result.isError(pollResponse)) {
|
|
306
|
-
return pollResponse;
|
|
307
|
-
}
|
|
308
|
-
if (pollResponse.value.status === 410) {
|
|
309
|
-
return Result.err(new AuthError({
|
|
310
|
-
code: ERROR_CODE.authDeviceExpired,
|
|
311
|
-
message: "Device login code expired. Run `ogment auth login` again.",
|
|
312
|
-
recovery: { command: "ogment auth login" },
|
|
313
|
-
}));
|
|
314
|
-
}
|
|
315
|
-
if (pollResponse.value.status === 404) {
|
|
316
|
-
return Result.err(new AuthError({
|
|
317
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
318
|
-
message: "Invalid device login code. Run `ogment auth login` again.",
|
|
319
|
-
recovery: { command: "ogment auth login" },
|
|
320
|
-
}));
|
|
321
|
-
}
|
|
322
|
-
if (!pollResponse.value.ok) {
|
|
323
|
-
const body = await readResponseText(pollResponse.value);
|
|
324
|
-
return Result.err(new RemoteRequestError({
|
|
325
|
-
body,
|
|
326
|
-
httpStatus: pollResponse.value.status,
|
|
327
|
-
message: "Failed to poll device login status",
|
|
328
|
-
operation: "auth/device/poll",
|
|
329
|
-
raw: body,
|
|
330
|
-
source: "http",
|
|
331
|
-
}));
|
|
332
|
-
}
|
|
333
|
-
const pollPayload = await Result.tryPromise({
|
|
334
|
-
catch: () => new RemoteRequestError({
|
|
335
|
-
httpStatus: pollResponse.value.status,
|
|
336
|
-
message: "Failed to parse device login poll payload",
|
|
337
|
-
operation: "auth/device/poll",
|
|
338
|
-
source: "http",
|
|
339
|
-
}),
|
|
340
|
-
try: async () => pollResponse.value.json(),
|
|
341
|
-
});
|
|
342
|
-
if (Result.isError(pollPayload)) {
|
|
343
|
-
return pollPayload;
|
|
344
|
-
}
|
|
345
|
-
if (toPendingPayload(pollPayload.value) === "authorization_pending") {
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
const approvedPayload = parseWithSchema(deviceTokenApprovedSchema, pollPayload.value, "device login poll response");
|
|
349
|
-
if (Result.isError(approvedPayload)) {
|
|
350
|
-
return approvedPayload;
|
|
351
|
-
}
|
|
352
|
-
const saveResult = deps.credentialsStore.save({
|
|
353
|
-
agentName: approvedPayload.value.data.agent_name ?? "CLI Agent",
|
|
354
|
-
apiKey: approvedPayload.value.data.api_key,
|
|
355
|
-
});
|
|
356
|
-
if (Result.isError(saveResult)) {
|
|
357
|
-
return saveResult;
|
|
358
|
-
}
|
|
359
|
-
return Result.ok({
|
|
360
|
-
agentName: approvedPayload.value.data.agent_name ?? "CLI Agent",
|
|
361
|
-
loggedIn: true,
|
|
362
|
-
outcome: "authenticated",
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
return Result.err(new AuthError({
|
|
366
|
-
code: ERROR_CODE.authDeviceExpired,
|
|
367
|
-
message: "Device login code expired. Run `ogment auth login` again.",
|
|
368
|
-
recovery: { command: "ogment auth login" },
|
|
369
|
-
}));
|
|
370
|
-
};
|
|
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 requestRemote(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
|
-
httpStatus: registerResponse.value.status,
|
|
401
|
-
message: "Client registration failed",
|
|
402
|
-
operation: "auth/browser/register",
|
|
403
|
-
raw: body,
|
|
404
|
-
source: "http",
|
|
405
|
-
}));
|
|
406
|
-
}
|
|
407
|
-
const registerPayload = await Result.tryPromise({
|
|
408
|
-
catch: () => new RemoteRequestError({
|
|
409
|
-
httpStatus: registerResponse.value.status,
|
|
410
|
-
message: "Failed to parse client registration payload",
|
|
411
|
-
operation: "auth/browser/register",
|
|
412
|
-
source: "http",
|
|
413
|
-
}),
|
|
414
|
-
try: async () => registerResponse.value.json(),
|
|
415
|
-
});
|
|
416
|
-
if (Result.isError(registerPayload)) {
|
|
417
|
-
await closeServer(server);
|
|
418
|
-
return registerPayload;
|
|
419
|
-
}
|
|
420
|
-
const parsedClient = parseWithSchema(oauthClientRegistrationSchema, registerPayload.value, "oauth client registration response");
|
|
421
|
-
if (Result.isError(parsedClient)) {
|
|
422
|
-
await closeServer(server);
|
|
423
|
-
return parsedClient;
|
|
424
|
-
}
|
|
425
|
-
const codeVerifier = generateCodeVerifier();
|
|
426
|
-
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
427
|
-
const state = randomBytes(16).toString("hex");
|
|
428
|
-
const authorizeUrl = new URL(oauthAuthorizeUrl(deps.baseUrl));
|
|
429
|
-
authorizeUrl.searchParams.set("client_id", parsedClient.value.client_id);
|
|
430
|
-
authorizeUrl.searchParams.set("redirect_uri", redirectUri);
|
|
431
|
-
authorizeUrl.searchParams.set("response_type", "code");
|
|
432
|
-
authorizeUrl.searchParams.set("code_challenge", codeChallenge);
|
|
433
|
-
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
434
|
-
authorizeUrl.searchParams.set("state", state);
|
|
435
|
-
const oauthCallbackPromise = waitForOAuthCallback(server, port);
|
|
436
|
-
const openResult = await deps.browserOpener.open(authorizeUrl.toString());
|
|
437
|
-
if (Result.isError(openResult)) {
|
|
438
|
-
await closeServer(server);
|
|
439
|
-
return openResult;
|
|
440
|
-
}
|
|
441
|
-
const oauthCallbackResult = await oauthCallbackPromise;
|
|
442
|
-
if (Result.isError(oauthCallbackResult)) {
|
|
443
|
-
await closeServer(server);
|
|
444
|
-
return oauthCallbackResult;
|
|
445
|
-
}
|
|
446
|
-
if (oauthCallbackResult.value.state !== state) {
|
|
447
|
-
oauthCallbackResult.value.response.writeHead(302, {
|
|
448
|
-
Location: "about:blank",
|
|
449
|
-
});
|
|
450
|
-
oauthCallbackResult.value.response.end();
|
|
451
|
-
await closeServer(server);
|
|
452
|
-
return Result.err(new AuthError({
|
|
453
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
454
|
-
message: "OAuth state mismatch - possible CSRF attack.",
|
|
455
|
-
recovery: { command: "ogment auth login --browser" },
|
|
456
|
-
}));
|
|
457
|
-
}
|
|
458
|
-
const tokenBody = new URLSearchParams({
|
|
459
|
-
client_id: parsedClient.value.client_id,
|
|
460
|
-
code: oauthCallbackResult.value.code,
|
|
461
|
-
code_verifier: codeVerifier,
|
|
462
|
-
grant_type: "authorization_code",
|
|
463
|
-
redirect_uri: redirectUri,
|
|
464
|
-
});
|
|
465
|
-
if (typeof parsedClient.value.client_secret === "string" &&
|
|
466
|
-
parsedClient.value.client_secret.length > 0) {
|
|
467
|
-
tokenBody.set("client_secret", parsedClient.value.client_secret);
|
|
468
|
-
}
|
|
469
|
-
const tokenResponse = await requestRemote(oauthTokenUrl(deps.baseUrl), {
|
|
470
|
-
body: tokenBody.toString(),
|
|
471
|
-
headers: {
|
|
472
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
473
|
-
},
|
|
474
|
-
method: "POST",
|
|
475
|
-
});
|
|
476
|
-
if (Result.isError(tokenResponse)) {
|
|
477
|
-
await closeServer(server);
|
|
478
|
-
return tokenResponse;
|
|
479
|
-
}
|
|
480
|
-
if (!tokenResponse.value.ok) {
|
|
481
|
-
const body = await readResponseText(tokenResponse.value);
|
|
482
|
-
await closeServer(server);
|
|
483
|
-
return Result.err(new RemoteRequestError({
|
|
484
|
-
body,
|
|
485
|
-
httpStatus: tokenResponse.value.status,
|
|
486
|
-
message: "Token exchange failed",
|
|
487
|
-
operation: "auth/browser/token",
|
|
488
|
-
raw: body,
|
|
489
|
-
source: "http",
|
|
490
|
-
}));
|
|
491
|
-
}
|
|
492
|
-
const tokenPayload = await Result.tryPromise({
|
|
493
|
-
catch: () => new RemoteRequestError({
|
|
494
|
-
httpStatus: tokenResponse.value.status,
|
|
495
|
-
message: "Failed to parse token payload",
|
|
496
|
-
operation: "auth/browser/token",
|
|
497
|
-
source: "http",
|
|
498
|
-
}),
|
|
499
|
-
try: async () => tokenResponse.value.json(),
|
|
500
|
-
});
|
|
501
|
-
if (Result.isError(tokenPayload)) {
|
|
502
|
-
await closeServer(server);
|
|
503
|
-
return tokenPayload;
|
|
504
|
-
}
|
|
505
|
-
const parsedToken = parseWithSchema(oauthTokenSchema, tokenPayload.value, "oauth token response");
|
|
506
|
-
if (Result.isError(parsedToken)) {
|
|
507
|
-
await closeServer(server);
|
|
508
|
-
return parsedToken;
|
|
509
|
-
}
|
|
510
|
-
const pickerUrl = new URL(agentSelectUrl(deps.baseUrl));
|
|
511
|
-
pickerUrl.searchParams.set("token", parsedToken.value.access_token);
|
|
512
|
-
pickerUrl.searchParams.set("callback", `http://${CLI_REDIRECT_HOST}:${port}/agent-callback`);
|
|
513
|
-
pickerUrl.searchParams.set("env", detectEnvironment());
|
|
514
|
-
pickerUrl.searchParams.set("host", hostnameFn());
|
|
515
|
-
oauthCallbackResult.value.response.writeHead(302, {
|
|
516
|
-
Location: pickerUrl.toString(),
|
|
517
|
-
});
|
|
518
|
-
oauthCallbackResult.value.response.end();
|
|
519
|
-
const agentCallbackResult = await waitForAgentCallback(server, port);
|
|
520
|
-
await closeServer(server);
|
|
521
|
-
if (Result.isError(agentCallbackResult)) {
|
|
522
|
-
return agentCallbackResult;
|
|
523
|
-
}
|
|
524
|
-
const exchangeRequestPayload = {
|
|
525
|
-
exchange_code: agentCallbackResult.value.exchangeCode,
|
|
526
|
-
};
|
|
527
|
-
const parsedExchangeRequestPayload = parseWithSchema(cliExchangeRequestSchema, exchangeRequestPayload, "browser exchange request");
|
|
528
|
-
if (Result.isError(parsedExchangeRequestPayload)) {
|
|
529
|
-
return parsedExchangeRequestPayload;
|
|
530
|
-
}
|
|
531
|
-
const exchangeResponse = await requestRemote(cliExchangeUrl(deps.baseUrl), {
|
|
532
|
-
body: JSON.stringify(parsedExchangeRequestPayload.value),
|
|
533
|
-
headers: {
|
|
534
|
-
"Content-Type": "application/json",
|
|
535
|
-
},
|
|
536
|
-
method: "POST",
|
|
537
|
-
});
|
|
538
|
-
if (Result.isError(exchangeResponse)) {
|
|
539
|
-
return exchangeResponse;
|
|
540
|
-
}
|
|
541
|
-
if (!exchangeResponse.value.ok) {
|
|
542
|
-
const body = await readResponseText(exchangeResponse.value);
|
|
543
|
-
const parsedExchangeErrorPayload = Result.try({
|
|
544
|
-
catch: () => null,
|
|
545
|
-
try: () => JSON.parse(body),
|
|
546
|
-
});
|
|
547
|
-
if (Result.isOk(parsedExchangeErrorPayload)) {
|
|
548
|
-
const parsedExchangeError = parseWithSchema(cliExchangeErrorSchema, parsedExchangeErrorPayload.value, "browser exchange error response");
|
|
549
|
-
if (Result.isOk(parsedExchangeError)) {
|
|
550
|
-
const exchangeErrorCode = parsedExchangeError.value.error;
|
|
551
|
-
if (exchangeErrorCode === "expired_exchange_code") {
|
|
552
|
-
return Result.err(new AuthError({
|
|
553
|
-
code: ERROR_CODE.authDeviceExpired,
|
|
554
|
-
message: "Browser login code expired. Run `ogment auth login --browser` again.",
|
|
555
|
-
recovery: { command: "ogment auth login --browser" },
|
|
556
|
-
}));
|
|
557
|
-
}
|
|
558
|
-
if (exchangeErrorCode === "invalid_exchange_code") {
|
|
559
|
-
return Result.err(new AuthError({
|
|
560
|
-
code: ERROR_CODE.authInvalidCredentials,
|
|
561
|
-
message: "Invalid browser login code. Run `ogment auth login --browser` again.",
|
|
562
|
-
recovery: { command: "ogment auth login --browser" },
|
|
563
|
-
}));
|
|
564
|
-
}
|
|
565
|
-
if (exchangeErrorCode === "authorization_pending") {
|
|
566
|
-
return Result.err(new AuthError({
|
|
567
|
-
code: ERROR_CODE.authDevicePending,
|
|
568
|
-
message: "Browser login authorization is still pending.",
|
|
569
|
-
recovery: { command: "ogment auth login --browser" },
|
|
570
|
-
retryable: true,
|
|
571
|
-
}));
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
return Result.err(new RemoteRequestError({
|
|
576
|
-
body,
|
|
577
|
-
httpStatus: exchangeResponse.value.status,
|
|
578
|
-
message: "Failed to exchange browser login code",
|
|
579
|
-
operation: "auth/browser/exchange",
|
|
580
|
-
raw: body,
|
|
581
|
-
source: "http",
|
|
582
|
-
}));
|
|
583
|
-
}
|
|
584
|
-
const exchangePayload = await Result.tryPromise({
|
|
585
|
-
catch: () => new RemoteRequestError({
|
|
586
|
-
httpStatus: exchangeResponse.value.status,
|
|
587
|
-
message: "Failed to parse browser exchange payload",
|
|
588
|
-
operation: "auth/browser/exchange",
|
|
589
|
-
source: "http",
|
|
590
|
-
}),
|
|
591
|
-
try: async () => exchangeResponse.value.json(),
|
|
592
|
-
});
|
|
593
|
-
if (Result.isError(exchangePayload)) {
|
|
594
|
-
return exchangePayload;
|
|
595
|
-
}
|
|
596
|
-
const parsedExchangePayload = parseWithSchema(cliExchangeSuccessSchema, exchangePayload.value, "browser exchange response");
|
|
597
|
-
if (Result.isError(parsedExchangePayload)) {
|
|
598
|
-
return parsedExchangePayload;
|
|
599
|
-
}
|
|
600
|
-
const saveResult = deps.credentialsStore.save({
|
|
601
|
-
agentName: parsedExchangePayload.value.data.name,
|
|
602
|
-
apiKey: parsedExchangePayload.value.data.apiKey,
|
|
603
|
-
});
|
|
604
|
-
if (Result.isError(saveResult)) {
|
|
605
|
-
return saveResult;
|
|
606
|
-
}
|
|
607
|
-
return Result.ok({
|
|
608
|
-
agentName: parsedExchangePayload.value.data.name,
|
|
609
|
-
loggedIn: true,
|
|
610
|
-
outcome: "authenticated",
|
|
611
|
-
});
|
|
612
|
-
};
|
|
613
|
-
return {
|
|
614
|
-
login: async (options) => {
|
|
615
|
-
if (options.mode === "apiKey" && options.apiKey.length > 0) {
|
|
616
|
-
const saveResult = deps.credentialsStore.save({
|
|
617
|
-
agentName: "CLI Agent",
|
|
618
|
-
apiKey: options.apiKey,
|
|
619
|
-
});
|
|
620
|
-
if (Result.isError(saveResult)) {
|
|
621
|
-
return saveResult;
|
|
622
|
-
}
|
|
623
|
-
return Result.ok({
|
|
624
|
-
agentName: "CLI Agent",
|
|
625
|
-
loggedIn: true,
|
|
626
|
-
outcome: "authenticated",
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
if (options.mode === "apiKey") {
|
|
630
|
-
return Result.err(new ValidationError({
|
|
631
|
-
code: ERROR_CODE.validationInvalidInput,
|
|
632
|
-
message: "Missing API key value. Provide a non-empty API key.",
|
|
633
|
-
recovery: { command: "ogment auth login --api-key <key>" },
|
|
634
|
-
}));
|
|
635
|
-
}
|
|
636
|
-
if (options.nonInteractive && options.mode === "browser") {
|
|
637
|
-
return Result.err(new ValidationError({
|
|
638
|
-
code: ERROR_CODE.validationInvalidInput,
|
|
639
|
-
message: "Use `ogment auth login` in non-interactive mode.",
|
|
640
|
-
recovery: { command: "ogment auth login" },
|
|
641
|
-
}));
|
|
642
|
-
}
|
|
643
|
-
const stored = deps.credentialsStore.load();
|
|
644
|
-
if (Result.isError(stored)) {
|
|
645
|
-
return stored;
|
|
646
|
-
}
|
|
647
|
-
if (stored.value !== null) {
|
|
648
|
-
return Result.ok({
|
|
649
|
-
agentName: stored.value.agentName ?? "CLI Agent",
|
|
650
|
-
loggedIn: true,
|
|
651
|
-
outcome: "already_authenticated",
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
if (options.mode === "device") {
|
|
655
|
-
return loginWithDevice(options);
|
|
656
|
-
}
|
|
657
|
-
return loginWithBrowser();
|
|
658
|
-
},
|
|
659
|
-
logout: async () => {
|
|
660
|
-
const stored = deps.credentialsStore.load();
|
|
661
|
-
if (Result.isError(stored)) {
|
|
662
|
-
return stored;
|
|
663
|
-
}
|
|
664
|
-
if (stored.value === null) {
|
|
665
|
-
return Result.ok({
|
|
666
|
-
localCredentialsDeleted: false,
|
|
667
|
-
revoked: false,
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
const revokeResult = await requestRemote(revokeUrl(deps.baseUrl), {
|
|
671
|
-
headers: {
|
|
672
|
-
Authorization: `Bearer ${stored.value.apiKey}`,
|
|
673
|
-
},
|
|
674
|
-
method: "POST",
|
|
675
|
-
});
|
|
676
|
-
const deleteResult = deps.credentialsStore.delete();
|
|
677
|
-
if (Result.isError(deleteResult)) {
|
|
678
|
-
return deleteResult;
|
|
679
|
-
}
|
|
680
|
-
const revoked = Result.isOk(revokeResult) && revokeResult.value.ok;
|
|
681
|
-
return Result.ok({
|
|
682
|
-
localCredentialsDeleted: true,
|
|
683
|
-
revoked,
|
|
684
|
-
});
|
|
685
|
-
},
|
|
686
|
-
resolveApiKey: async (overrideApiKey) => {
|
|
687
|
-
if (typeof overrideApiKey === "string" && overrideApiKey.length > 0) {
|
|
688
|
-
return Result.ok(overrideApiKey);
|
|
689
|
-
}
|
|
690
|
-
if (typeof deps.envApiKey === "string" && deps.envApiKey.length > 0) {
|
|
691
|
-
return Result.ok(deps.envApiKey);
|
|
692
|
-
}
|
|
693
|
-
const stored = deps.credentialsStore.load();
|
|
694
|
-
if (Result.isError(stored)) {
|
|
695
|
-
return stored;
|
|
696
|
-
}
|
|
697
|
-
if (stored.value !== null) {
|
|
698
|
-
return Result.ok(stored.value.apiKey);
|
|
699
|
-
}
|
|
700
|
-
return Result.err(new AuthError({
|
|
701
|
-
code: ERROR_CODE.authRequired,
|
|
702
|
-
message: "Not logged in. Run `ogment auth login` or set OGMENT_API_KEY.",
|
|
703
|
-
recovery: { command: "ogment auth login" },
|
|
704
|
-
}));
|
|
705
|
-
},
|
|
706
|
-
status: async (overrideApiKey) => {
|
|
707
|
-
if (typeof overrideApiKey === "string" && overrideApiKey.length > 0) {
|
|
708
|
-
return Result.ok({
|
|
709
|
-
agentName: null,
|
|
710
|
-
apiKeySource: "apiKeyOption",
|
|
711
|
-
loggedIn: true,
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
if (typeof deps.envApiKey === "string" && deps.envApiKey.length > 0) {
|
|
715
|
-
return Result.ok({
|
|
716
|
-
agentName: null,
|
|
717
|
-
apiKeySource: "env",
|
|
718
|
-
loggedIn: true,
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
const stored = deps.credentialsStore.load();
|
|
722
|
-
if (Result.isError(stored)) {
|
|
723
|
-
return stored;
|
|
724
|
-
}
|
|
725
|
-
if (stored.value === null) {
|
|
726
|
-
return Result.ok({
|
|
727
|
-
agentName: null,
|
|
728
|
-
apiKeySource: "none",
|
|
729
|
-
loggedIn: false,
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
return Result.ok({
|
|
733
|
-
agentName: stored.value.agentName ?? null,
|
|
734
|
-
apiKeySource: "credentialsFile",
|
|
735
|
-
loggedIn: true,
|
|
736
|
-
});
|
|
737
|
-
},
|
|
738
|
-
};
|
|
739
|
-
};
|