@squidlerio/squidler-mcp 1.0.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.
@@ -0,0 +1,374 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/chrome/download.ts
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import * as os from "os";
8
+ import * as https from "https";
9
+ import * as unzipper from "unzipper";
10
+ var DEFAULT_CHROME_VERSION = "136.0.7103.59";
11
+ function getPlatform() {
12
+ const platform2 = os.platform();
13
+ const arch2 = os.arch();
14
+ if (platform2 === "darwin") {
15
+ return arch2 === "arm64" ? "mac-arm64" : "mac-x64";
16
+ } else if (platform2 === "linux") {
17
+ return "linux64";
18
+ } else if (platform2 === "win32") {
19
+ return "win64";
20
+ }
21
+ throw new Error(`Unsupported platform: ${platform2} ${arch2}`);
22
+ }
23
+ function getChromePath(extractDir, platform2) {
24
+ const baseName = "chrome-headless-shell-" + platform2;
25
+ switch (platform2) {
26
+ case "mac-arm64":
27
+ case "mac-x64":
28
+ return path.join(extractDir, baseName, "chrome-headless-shell");
29
+ case "linux64":
30
+ return path.join(extractDir, baseName, "chrome-headless-shell");
31
+ case "win64":
32
+ return path.join(extractDir, baseName, "chrome-headless-shell.exe");
33
+ default:
34
+ throw new Error(`Unknown platform: ${platform2}`);
35
+ }
36
+ }
37
+ async function downloadFile(url, destPath) {
38
+ return new Promise((resolve, reject) => {
39
+ const file = fs.createWriteStream(destPath);
40
+ const request = https.get(url, (response) => {
41
+ if (response.statusCode === 301 || response.statusCode === 302) {
42
+ const redirectUrl = response.headers.location;
43
+ if (redirectUrl) {
44
+ file.close();
45
+ fs.unlinkSync(destPath);
46
+ downloadFile(redirectUrl, destPath).then(resolve).catch(reject);
47
+ return;
48
+ }
49
+ }
50
+ if (response.statusCode !== 200) {
51
+ reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
52
+ return;
53
+ }
54
+ const totalSize = parseInt(response.headers["content-length"] || "0", 10);
55
+ let downloadedSize = 0;
56
+ response.on("data", (chunk) => {
57
+ downloadedSize += chunk.length;
58
+ if (totalSize > 0) {
59
+ const percent = Math.round(downloadedSize / totalSize * 100);
60
+ process.stderr.write(`\rDownloading Chrome... ${percent}%`);
61
+ }
62
+ });
63
+ response.pipe(file);
64
+ file.on("finish", () => {
65
+ file.close();
66
+ console.error(`
67
+ Download complete.`);
68
+ resolve();
69
+ });
70
+ });
71
+ request.on("error", (err) => {
72
+ fs.unlink(destPath, () => {});
73
+ reject(err);
74
+ });
75
+ file.on("error", (err) => {
76
+ fs.unlink(destPath, () => {});
77
+ reject(err);
78
+ });
79
+ });
80
+ }
81
+ async function extractZip(zipPath, destDir) {
82
+ console.error("Extracting Chrome...");
83
+ return new Promise((resolve, reject) => {
84
+ fs.createReadStream(zipPath).pipe(unzipper.Extract({ path: destDir })).on("close", () => {
85
+ console.error("Extraction complete.");
86
+ resolve();
87
+ }).on("error", reject);
88
+ });
89
+ }
90
+ async function downloadChrome(options = {}) {
91
+ const version = options.version || process.env.SQUIDLER_CHROME_VERSION || DEFAULT_CHROME_VERSION;
92
+ const defaultCacheDir = path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"), "squidler", "chrome");
93
+ const cacheDir = options.cacheDir || defaultCacheDir;
94
+ const platform2 = getPlatform();
95
+ fs.mkdirSync(cacheDir, { recursive: true });
96
+ const versionDir = path.join(cacheDir, version);
97
+ const executablePath = getChromePath(versionDir, platform2);
98
+ if (fs.existsSync(executablePath)) {
99
+ console.error(`Using cached Chrome ${version}`);
100
+ return { executablePath, version };
101
+ }
102
+ const url = `https://storage.googleapis.com/chrome-for-testing-public/${version}/${platform2}/chrome-headless-shell-${platform2}.zip`;
103
+ const zipPath = path.join(cacheDir, `chrome-${version}-${platform2}.zip`);
104
+ console.error(`Downloading Chrome ${version} for ${platform2}...`);
105
+ try {
106
+ await downloadFile(url, zipPath);
107
+ await extractZip(zipPath, versionDir);
108
+ fs.unlinkSync(zipPath);
109
+ if (platform2 !== "win64") {
110
+ fs.chmodSync(executablePath, 493);
111
+ }
112
+ console.error(`Chrome ${version} installed at: ${executablePath}`);
113
+ return { executablePath, version };
114
+ } catch (error) {
115
+ if (fs.existsSync(zipPath)) {
116
+ fs.unlinkSync(zipPath);
117
+ }
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ // src/version.ts
123
+ import { readFileSync } from "fs";
124
+ import { dirname, join as join2 } from "path";
125
+ import { fileURLToPath } from "url";
126
+ var __dirname2 = dirname(fileURLToPath(import.meta.url));
127
+ var pkg = JSON.parse(readFileSync(join2(__dirname2, "..", "package.json"), "utf-8"));
128
+ var VERSION = pkg.version;
129
+
130
+ // src/auth/token-store.ts
131
+ import * as fs2 from "fs";
132
+ import * as path2 from "path";
133
+ import * as os2 from "os";
134
+ function getDataDir() {
135
+ const base = process.env.XDG_DATA_HOME || path2.join(os2.homedir(), ".local", "share");
136
+ return path2.join(base, "squidler");
137
+ }
138
+ function getAuthFilePath() {
139
+ return path2.join(getDataDir(), "auth.json");
140
+ }
141
+ function readStore() {
142
+ const filePath = getAuthFilePath();
143
+ try {
144
+ const data = fs2.readFileSync(filePath, "utf-8");
145
+ return JSON.parse(data);
146
+ } catch {
147
+ return {};
148
+ }
149
+ }
150
+ function writeStore(store) {
151
+ const dir = getDataDir();
152
+ fs2.mkdirSync(dir, { recursive: true });
153
+ const filePath = getAuthFilePath();
154
+ fs2.writeFileSync(filePath, JSON.stringify(store, null, 2), { mode: 384 });
155
+ }
156
+ function loadStoredAuth(serverUrl) {
157
+ const store = readStore();
158
+ return store[serverUrl] || null;
159
+ }
160
+ function saveStoredAuth(auth) {
161
+ const store = readStore();
162
+ store[auth.server_url] = auth;
163
+ writeStore(store);
164
+ }
165
+ function clearStoredAuth(serverUrl) {
166
+ const store = readStore();
167
+ delete store[serverUrl];
168
+ writeStore(store);
169
+ }
170
+
171
+ // src/auth/oauth.ts
172
+ import * as crypto from "crypto";
173
+ import * as os3 from "os";
174
+ import { spawn } from "child_process";
175
+
176
+ // src/auth/callback-server.ts
177
+ import * as http from "http";
178
+ var SUCCESS_HTML = `<!DOCTYPE html>
179
+ <html><head><title>Authentication Successful</title></head>
180
+ <body style="font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
181
+ <div style="text-align:center"><h1>Authenticated!</h1><p>You can close this tab and return to your terminal.</p></div>
182
+ </body></html>`;
183
+ var ERROR_HTML = (msg) => `<!DOCTYPE html>
184
+ <html><head><title>Authentication Failed</title></head>
185
+ <body style="font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
186
+ <div style="text-align:center"><h1>Authentication Failed</h1><p>${msg}</p></div>
187
+ </body></html>`;
188
+ function startCallbackServer() {
189
+ return new Promise((resolve, reject) => {
190
+ let onResult;
191
+ let onError;
192
+ const resultPromise = new Promise((res, rej) => {
193
+ onResult = res;
194
+ onError = rej;
195
+ });
196
+ const server = http.createServer((req, res) => {
197
+ const url = new URL(req.url || "/", `http://127.0.0.1`);
198
+ if (url.pathname !== "/callback") {
199
+ res.writeHead(404);
200
+ res.end("Not found");
201
+ return;
202
+ }
203
+ const error = url.searchParams.get("error");
204
+ if (error) {
205
+ const desc = url.searchParams.get("error_description") || error;
206
+ res.writeHead(400, { "Content-Type": "text/html" });
207
+ res.end(ERROR_HTML(desc));
208
+ onError(new Error(`OAuth error: ${desc}`));
209
+ return;
210
+ }
211
+ const code = url.searchParams.get("code");
212
+ const state = url.searchParams.get("state");
213
+ if (!code || !state) {
214
+ res.writeHead(400, { "Content-Type": "text/html" });
215
+ res.end(ERROR_HTML("Missing code or state parameter"));
216
+ onError(new Error("Missing code or state in callback"));
217
+ return;
218
+ }
219
+ res.writeHead(200, { "Content-Type": "text/html" });
220
+ res.end(SUCCESS_HTML);
221
+ onResult({ code, state });
222
+ });
223
+ server.unref();
224
+ const timeout = setTimeout(() => {
225
+ server.close();
226
+ onError(new Error("Authentication timed out (5 minutes)"));
227
+ }, 5 * 60 * 1000);
228
+ server.on("error", reject);
229
+ server.listen(0, "127.0.0.1", () => {
230
+ const address = server.address();
231
+ if (!address || typeof address === "string") {
232
+ reject(new Error("Could not get callback server port"));
233
+ return;
234
+ }
235
+ resolve({
236
+ port: address.port,
237
+ waitForCallback: () => resultPromise,
238
+ close: () => {
239
+ clearTimeout(timeout);
240
+ server.close();
241
+ }
242
+ });
243
+ });
244
+ });
245
+ }
246
+
247
+ // src/auth/oauth.ts
248
+ async function discover(serverUrl) {
249
+ const res = await fetch(`${serverUrl}/.well-known/oauth-authorization-server`);
250
+ if (!res.ok) {
251
+ throw new Error(`OAuth discovery failed: HTTP ${res.status}`);
252
+ }
253
+ const metadata = await res.json();
254
+ if (!metadata.authorization_endpoint || !metadata.token_endpoint || !metadata.registration_endpoint) {
255
+ throw new Error("OAuth discovery response missing required endpoints");
256
+ }
257
+ return metadata;
258
+ }
259
+ async function registerClient(registrationEndpoint, redirectUri) {
260
+ const res = await fetch(registrationEndpoint, {
261
+ method: "POST",
262
+ headers: { "Content-Type": "application/json" },
263
+ body: JSON.stringify({
264
+ client_name: "squidler-mcp-cli",
265
+ redirect_uris: [redirectUri],
266
+ grant_types: ["authorization_code"],
267
+ response_types: ["code"],
268
+ token_endpoint_auth_method: "client_secret_post"
269
+ })
270
+ });
271
+ if (!res.ok) {
272
+ throw new Error(`Client registration failed: HTTP ${res.status}`);
273
+ }
274
+ return await res.json();
275
+ }
276
+ function generatePKCE() {
277
+ const verifier = crypto.randomBytes(32).toString("base64url");
278
+ const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
279
+ return { verifier, challenge };
280
+ }
281
+ function openBrowser(url) {
282
+ const platform3 = os3.platform();
283
+ const commands = platform3 === "darwin" ? [["open", [url]]] : platform3 === "win32" ? [["cmd", ["/c", "start", "", url]]] : [
284
+ ["xdg-open", [url]],
285
+ ["sensible-browser", [url]],
286
+ ["x-www-browser", [url]]
287
+ ];
288
+ for (const [cmd, args] of commands) {
289
+ try {
290
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
291
+ child.unref();
292
+ child.on("error", () => {});
293
+ return true;
294
+ } catch {
295
+ continue;
296
+ }
297
+ }
298
+ return false;
299
+ }
300
+ async function exchangeToken(tokenEndpoint, params) {
301
+ const body = new URLSearchParams({
302
+ grant_type: "authorization_code",
303
+ code: params.code,
304
+ redirect_uri: params.redirectUri,
305
+ client_id: params.clientId,
306
+ client_secret: params.clientSecret,
307
+ code_verifier: params.codeVerifier
308
+ });
309
+ const res = await fetch(tokenEndpoint, {
310
+ method: "POST",
311
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
312
+ body: body.toString()
313
+ });
314
+ if (!res.ok) {
315
+ const text = await res.text();
316
+ throw new Error(`Token exchange failed: HTTP ${res.status} - ${text}`);
317
+ }
318
+ const data = await res.json();
319
+ if (!data.access_token) {
320
+ throw new Error("Token response missing access_token");
321
+ }
322
+ return data.access_token;
323
+ }
324
+ async function authenticateWithOAuth(serverUrl) {
325
+ console.error("Discovering OAuth endpoints...");
326
+ const metadata = await discover(serverUrl);
327
+ const callback = await startCallbackServer();
328
+ const redirectUri = `http://127.0.0.1:${callback.port}/callback`;
329
+ try {
330
+ console.error("Registering client...");
331
+ const client = await registerClient(metadata.registration_endpoint, redirectUri);
332
+ const pkce = generatePKCE();
333
+ const state = crypto.randomBytes(16).toString("base64url");
334
+ const authUrl = new URL(metadata.authorization_endpoint);
335
+ authUrl.searchParams.set("response_type", "code");
336
+ authUrl.searchParams.set("client_id", client.client_id);
337
+ authUrl.searchParams.set("redirect_uri", redirectUri);
338
+ authUrl.searchParams.set("state", state);
339
+ authUrl.searchParams.set("code_challenge", pkce.challenge);
340
+ authUrl.searchParams.set("code_challenge_method", "S256");
341
+ const opened = openBrowser(authUrl.toString());
342
+ if (opened) {
343
+ console.error("Browser opened for authentication. Waiting...");
344
+ } else {
345
+ console.error(`Open this URL in your browser to authenticate:
346
+
347
+ ${authUrl.toString()}
348
+
349
+ Waiting for authentication...`);
350
+ }
351
+ const result = await callback.waitForCallback();
352
+ if (result.state !== state) {
353
+ throw new Error("OAuth state mismatch — possible CSRF attack");
354
+ }
355
+ console.error("Exchanging authorization code for token...");
356
+ const accessToken = await exchangeToken(metadata.token_endpoint, {
357
+ code: result.code,
358
+ redirectUri,
359
+ clientId: client.client_id,
360
+ clientSecret: client.client_secret,
361
+ codeVerifier: pkce.verifier
362
+ });
363
+ saveStoredAuth({
364
+ access_token: accessToken,
365
+ server_url: serverUrl,
366
+ created_at: new Date().toISOString()
367
+ });
368
+ return accessToken;
369
+ } finally {
370
+ callback.close();
371
+ }
372
+ }
373
+
374
+ export { __require, downloadChrome, VERSION, loadStoredAuth, clearStoredAuth, authenticateWithOAuth };