moon-iq 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/auth.ts +0 -332
- package/src/client.ts +0 -68
- package/src/generated/client.ts +0 -556
- package/src/index.ts +0 -307
- package/tsconfig.json +0 -10
- package/tsup.config.ts +0 -10
package/src/auth.ts
DELETED
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
2
|
-
import * as crypto from "crypto";
|
|
3
|
-
import * as fs from "fs";
|
|
4
|
-
import * as http from "http";
|
|
5
|
-
import * as os from "os";
|
|
6
|
-
import * as path from "path";
|
|
7
|
-
|
|
8
|
-
function generateCodeVerifier(): string {
|
|
9
|
-
return crypto.randomBytes(32).toString("base64url");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function generateCodeChallenge(verifier: string): string {
|
|
13
|
-
return crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const CONFIG_DIR = path.join(os.homedir(), ".config", "moon-iq");
|
|
17
|
-
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
18
|
-
const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
|
|
19
|
-
const CALLBACK_PORT = 3847;
|
|
20
|
-
const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
21
|
-
|
|
22
|
-
/** Built-in config used when no ~/.config/moon-iq/config.json exists. */
|
|
23
|
-
export const DEFAULT_CONFIG: Config = {
|
|
24
|
-
baseUrl: "https://moon-iq.com",
|
|
25
|
-
clientId: "mooniq_zin3s5jz3olzkdfxpmbeaogv",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type Config = {
|
|
29
|
-
baseUrl: string;
|
|
30
|
-
clientId: string;
|
|
31
|
-
clientSecret?: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type Credentials = {
|
|
35
|
-
accessToken: string;
|
|
36
|
-
refreshToken: string | null;
|
|
37
|
-
expiresAt: number;
|
|
38
|
-
baseUrl: string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
function ensureConfigDir() {
|
|
42
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
43
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Returns config from file, or null if missing/invalid. Use getConfigOrDefault() for login flow. */
|
|
48
|
-
export function getConfig(): Config | null {
|
|
49
|
-
if (!fs.existsSync(CONFIG_PATH)) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
try {
|
|
53
|
-
const data = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
54
|
-
if (!data.baseUrl || !data.clientId) {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
return data as Config;
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Returns config from file, or DEFAULT_CONFIG if none exists. Creates config file on first use. */
|
|
64
|
-
export function getConfigOrDefault(baseUrl?: string): Config {
|
|
65
|
-
const fromFile = getConfig();
|
|
66
|
-
if (fromFile) return fromFile;
|
|
67
|
-
|
|
68
|
-
const config: Config = {
|
|
69
|
-
...DEFAULT_CONFIG,
|
|
70
|
-
baseUrl: baseUrl ?? DEFAULT_CONFIG.baseUrl,
|
|
71
|
-
};
|
|
72
|
-
saveConfig(config);
|
|
73
|
-
return config;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function getCredentials(): Credentials | null {
|
|
77
|
-
if (!fs.existsSync(CREDENTIALS_PATH)) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
const data = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf-8"));
|
|
82
|
-
if (!data.accessToken || !data.baseUrl) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
return data as Credentials;
|
|
86
|
-
} catch {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function saveCredentials(creds: Credentials) {
|
|
92
|
-
ensureConfigDir();
|
|
93
|
-
fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), "utf-8");
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function saveConfig(config: Config) {
|
|
97
|
-
ensureConfigDir();
|
|
98
|
-
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function clearCredentials() {
|
|
102
|
-
if (fs.existsSync(CREDENTIALS_PATH)) {
|
|
103
|
-
fs.unlinkSync(CREDENTIALS_PATH);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function openBrowser(url: string) {
|
|
108
|
-
const platform = process.platform;
|
|
109
|
-
const command =
|
|
110
|
-
platform === "darwin"
|
|
111
|
-
? `open "${url}"`
|
|
112
|
-
: platform === "win32"
|
|
113
|
-
? `start "" "${url}"`
|
|
114
|
-
: `xdg-open "${url}"`;
|
|
115
|
-
execSync(command);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export async function login(config: Config): Promise<Credentials> {
|
|
119
|
-
const state = crypto.randomUUID();
|
|
120
|
-
const usePkce = !config.clientSecret;
|
|
121
|
-
|
|
122
|
-
let codeVerifier: string | undefined;
|
|
123
|
-
if (usePkce) {
|
|
124
|
-
codeVerifier = generateCodeVerifier();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
let resolveCallback: (code: string) => void;
|
|
128
|
-
const codePromise = new Promise<string>((resolve) => {
|
|
129
|
-
resolveCallback = resolve;
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const sockets = new Set<import("net").Socket>();
|
|
133
|
-
|
|
134
|
-
const server = http.createServer((req, res) => {
|
|
135
|
-
const url = new URL(req.url ?? "/", `http://localhost:${CALLBACK_PORT}`);
|
|
136
|
-
if (url.pathname === "/callback") {
|
|
137
|
-
const code = url.searchParams.get("code");
|
|
138
|
-
const error = url.searchParams.get("error");
|
|
139
|
-
const errorDesc = url.searchParams.get("error_description");
|
|
140
|
-
|
|
141
|
-
if (error) {
|
|
142
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
143
|
-
res.end(
|
|
144
|
-
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>OAuth Error</title></head><body style="font-family:system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:2rem;text-align:center;background:#fef2f2"><h1 style="color:#b91c1c;margin:0 0 1rem">OAuth Error</h1><p style="color:#991b1b;margin:0">${error}: ${errorDesc ?? "Unknown error"}</p></body></html>`,
|
|
145
|
-
);
|
|
146
|
-
resolveCallback("");
|
|
147
|
-
setTimeout(() => server.close(), 2000);
|
|
148
|
-
} else if (code) {
|
|
149
|
-
// Serve success page inline - don't redirect to app (it may not be running)
|
|
150
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
151
|
-
res.end(
|
|
152
|
-
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Success</title></head><body style="font-family:system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:2rem;text-align:center;background:#f0fdf4"><h1 style="color:#166534;margin:0 0 1rem;font-size:1.5rem">✓ Success!</h1><p style="color:#15803d;margin:0;font-size:1.1rem">You can close this page and return to the terminal.</p></body></html>`,
|
|
153
|
-
);
|
|
154
|
-
resolveCallback(code);
|
|
155
|
-
} else {
|
|
156
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
157
|
-
res.end(
|
|
158
|
-
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Error</title></head><body style="font-family:system-ui,sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh;margin:0;padding:2rem;text-align:center;background:#fef2f2"><h1 style="color:#b91c1c;margin:0 0 1rem">Error</h1><p style="color:#991b1b;margin:0">No authorization code received.</p></body></html>`,
|
|
159
|
-
);
|
|
160
|
-
resolveCallback("");
|
|
161
|
-
setTimeout(() => server.close(), 2000);
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
// Favicon etc - serve minimal response to avoid "site can't be reached"
|
|
165
|
-
res.writeHead(204);
|
|
166
|
-
res.end();
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
await new Promise<void>((resolve, reject) => {
|
|
171
|
-
server.on("error", (err: NodeJS.ErrnoException) => {
|
|
172
|
-
if (err.code === "EADDRINUSE") {
|
|
173
|
-
reject(
|
|
174
|
-
new Error(
|
|
175
|
-
`Port ${CALLBACK_PORT} is in use. Close the other process or run: lsof -i :${CALLBACK_PORT}`,
|
|
176
|
-
),
|
|
177
|
-
);
|
|
178
|
-
} else {
|
|
179
|
-
reject(err);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
// Track all connections so we can destroy them and allow the process to exit
|
|
183
|
-
server.on("connection", (socket) => {
|
|
184
|
-
sockets.add(socket);
|
|
185
|
-
socket.on("close", () => sockets.delete(socket));
|
|
186
|
-
});
|
|
187
|
-
// Bind to all interfaces so both 127.0.0.1 and ::1 (localhost) can connect
|
|
188
|
-
server.listen(CALLBACK_PORT, "0.0.0.0", () => resolve());
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
console.log(
|
|
192
|
-
`Waiting for callback at http://localhost:${CALLBACK_PORT}/callback`,
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
const authorizeParams = new URLSearchParams({
|
|
196
|
-
client_id: config.clientId,
|
|
197
|
-
redirect_uri: CALLBACK_URL,
|
|
198
|
-
response_type: "code",
|
|
199
|
-
scope: "profile email",
|
|
200
|
-
state,
|
|
201
|
-
});
|
|
202
|
-
if (usePkce && codeVerifier) {
|
|
203
|
-
authorizeParams.set("code_challenge", generateCodeChallenge(codeVerifier));
|
|
204
|
-
authorizeParams.set("code_challenge_method", "S256");
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const authorizeUrl = `${config.baseUrl}/api/oauth/authorize?${authorizeParams.toString()}`;
|
|
208
|
-
console.log("Opening browser for authorization...");
|
|
209
|
-
console.log("(Make sure you're logged in to Moon IQ in your browser)\n");
|
|
210
|
-
openBrowser(authorizeUrl);
|
|
211
|
-
|
|
212
|
-
const code = await codePromise;
|
|
213
|
-
if (!code) {
|
|
214
|
-
throw new Error("No authorization code received");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const tokenParams: Record<string, string> = {
|
|
218
|
-
grant_type: "authorization_code",
|
|
219
|
-
code,
|
|
220
|
-
client_id: config.clientId,
|
|
221
|
-
redirect_uri: CALLBACK_URL,
|
|
222
|
-
};
|
|
223
|
-
if (config.clientSecret) {
|
|
224
|
-
tokenParams.client_secret = config.clientSecret;
|
|
225
|
-
}
|
|
226
|
-
if (usePkce && codeVerifier) {
|
|
227
|
-
tokenParams.code_verifier = codeVerifier;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const tokenResponse = await fetch(`${config.baseUrl}/api/oauth/token`, {
|
|
231
|
-
method: "POST",
|
|
232
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
233
|
-
body: new URLSearchParams(tokenParams),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
if (!tokenResponse.ok) {
|
|
237
|
-
const err = (await tokenResponse.json().catch(() => ({}))) as {
|
|
238
|
-
error?: string;
|
|
239
|
-
error_description?: string;
|
|
240
|
-
};
|
|
241
|
-
const msg = err.error_description ?? err.error ?? tokenResponse.statusText;
|
|
242
|
-
throw new Error(`Token exchange failed: ${msg}`);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const tokens = await tokenResponse.json();
|
|
246
|
-
const expiresAt = Date.now() + (tokens.expires_in ?? 3600) * 1000;
|
|
247
|
-
|
|
248
|
-
const creds: Credentials = {
|
|
249
|
-
accessToken: tokens.access_token,
|
|
250
|
-
refreshToken: tokens.refresh_token ?? null,
|
|
251
|
-
expiresAt,
|
|
252
|
-
baseUrl: config.baseUrl,
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
saveCredentials(creds);
|
|
256
|
-
|
|
257
|
-
// Close callback server and all connections so the process can exit (no hang after "Logged in successfully.")
|
|
258
|
-
server.close();
|
|
259
|
-
for (const socket of sockets) {
|
|
260
|
-
if ("destroy" in socket && typeof (socket as { destroy: () => void }).destroy === "function") {
|
|
261
|
-
(socket as { destroy: () => void }).destroy();
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
sockets.clear();
|
|
265
|
-
|
|
266
|
-
return creds;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export async function refreshCredentials(
|
|
270
|
-
creds: Credentials,
|
|
271
|
-
config: Config,
|
|
272
|
-
): Promise<Credentials> {
|
|
273
|
-
if (!creds.refreshToken) {
|
|
274
|
-
throw new Error("No refresh token available");
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const refreshParams: Record<string, string> = {
|
|
278
|
-
grant_type: "refresh_token",
|
|
279
|
-
refresh_token: creds.refreshToken,
|
|
280
|
-
client_id: config.clientId,
|
|
281
|
-
};
|
|
282
|
-
if (config.clientSecret) {
|
|
283
|
-
refreshParams.client_secret = config.clientSecret;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const tokenResponse = await fetch(`${config.baseUrl}/api/oauth/token`, {
|
|
287
|
-
method: "POST",
|
|
288
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
289
|
-
body: new URLSearchParams(refreshParams),
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
if (!tokenResponse.ok) {
|
|
293
|
-
throw new Error("Token refresh failed");
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const tokens = await tokenResponse.json();
|
|
297
|
-
const expiresAt = Date.now() + (tokens.expires_in ?? 3600) * 1000;
|
|
298
|
-
|
|
299
|
-
const newCreds: Credentials = {
|
|
300
|
-
accessToken: tokens.access_token,
|
|
301
|
-
refreshToken: tokens.refresh_token ?? creds.refreshToken,
|
|
302
|
-
expiresAt,
|
|
303
|
-
baseUrl: config.baseUrl,
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
saveCredentials(newCreds);
|
|
307
|
-
return newCreds;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; // 5 minutes
|
|
311
|
-
|
|
312
|
-
export async function getValidToken(): Promise<string | null> {
|
|
313
|
-
const creds = getCredentials();
|
|
314
|
-
if (!creds) return null;
|
|
315
|
-
|
|
316
|
-
const config = getConfig();
|
|
317
|
-
if (!config || config.baseUrl !== creds.baseUrl) return null;
|
|
318
|
-
|
|
319
|
-
if (Date.now() >= creds.expiresAt - TOKEN_EXPIRY_BUFFER_MS) {
|
|
320
|
-
if (creds.refreshToken) {
|
|
321
|
-
try {
|
|
322
|
-
const newCreds = await refreshCredentials(creds, config);
|
|
323
|
-
return newCreds.accessToken;
|
|
324
|
-
} catch {
|
|
325
|
-
return null;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return creds.accessToken;
|
|
332
|
-
}
|
package/src/client.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getConfig,
|
|
3
|
-
getCredentials,
|
|
4
|
-
getValidToken,
|
|
5
|
-
refreshCredentials,
|
|
6
|
-
} from "./auth";
|
|
7
|
-
import { TOOL_NAMES } from "./generated/client";
|
|
8
|
-
|
|
9
|
-
export { TOOL_NAMES } from "./generated/client";
|
|
10
|
-
|
|
11
|
-
async function callToolWithToken(
|
|
12
|
-
baseUrl: string,
|
|
13
|
-
token: string,
|
|
14
|
-
toolName: string,
|
|
15
|
-
params: Record<string, unknown>
|
|
16
|
-
): Promise<{ data: unknown; status: number }> {
|
|
17
|
-
const res = await fetch(`${baseUrl}/api/tools/${toolName}`, {
|
|
18
|
-
method: "POST",
|
|
19
|
-
headers: {
|
|
20
|
-
"Content-Type": "application/json",
|
|
21
|
-
Authorization: `Bearer ${token}`,
|
|
22
|
-
},
|
|
23
|
-
body: JSON.stringify(params),
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const data = await res.json();
|
|
27
|
-
return { data, status: res.status };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function callToolWithAuth(
|
|
31
|
-
baseUrl: string,
|
|
32
|
-
toolName: string,
|
|
33
|
-
params: Record<string, unknown>
|
|
34
|
-
): Promise<unknown> {
|
|
35
|
-
let token = await getValidToken();
|
|
36
|
-
if (!token) {
|
|
37
|
-
throw new Error(
|
|
38
|
-
"Not logged in. Run `mooniq login` first and ensure ~/.config/moon-iq/config.json has baseUrl and clientId."
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let result = await callToolWithToken(baseUrl, token, toolName, params);
|
|
43
|
-
|
|
44
|
-
if (result.status === 401) {
|
|
45
|
-
const creds = getCredentials();
|
|
46
|
-
const config = getConfig();
|
|
47
|
-
if (creds?.refreshToken && config && config.baseUrl === creds.baseUrl) {
|
|
48
|
-
try {
|
|
49
|
-
const newCreds = await refreshCredentials(creds, config);
|
|
50
|
-
result = await callToolWithToken(
|
|
51
|
-
baseUrl,
|
|
52
|
-
newCreds.accessToken,
|
|
53
|
-
toolName,
|
|
54
|
-
params
|
|
55
|
-
);
|
|
56
|
-
} catch {
|
|
57
|
-
// Refresh failed, fall through and throw the original 401
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (result.status < 200 || result.status >= 300) {
|
|
63
|
-
const err = result.data as { error?: string };
|
|
64
|
-
throw new Error(err?.error ?? "Tool call failed");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return result.data;
|
|
68
|
-
}
|