@vucinatim/agentic-devtools 0.1.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/SECURITY.md +47 -0
  4. package/adapters/claude/namecheap/README.md +13 -0
  5. package/adapters/claude/npm/README.md +11 -0
  6. package/adapters/claude/railway/README.md +11 -0
  7. package/adapters/codex/namecheap/.codex-plugin/plugin.json +40 -0
  8. package/adapters/codex/namecheap/.mcp.json +21 -0
  9. package/adapters/codex/namecheap/SKILL.md +40 -0
  10. package/adapters/codex/npm/.codex-plugin/plugin.json +41 -0
  11. package/adapters/codex/npm/.mcp.json +18 -0
  12. package/adapters/codex/npm/SKILL.md +54 -0
  13. package/adapters/codex/railway/.codex-plugin/plugin.json +39 -0
  14. package/adapters/codex/railway/.mcp.json +20 -0
  15. package/adapters/codex/railway/SKILL.md +44 -0
  16. package/docs/README.md +14 -0
  17. package/docs/architecture.md +208 -0
  18. package/docs/auth-and-setup-guidelines.md +261 -0
  19. package/docs/migration-plan.md +55 -0
  20. package/docs/open-source-readiness.md +119 -0
  21. package/docs/publishing.md +211 -0
  22. package/docs/testing.md +61 -0
  23. package/docs/usage.md +144 -0
  24. package/package.json +78 -0
  25. package/src/cli.mjs +158 -0
  26. package/src/core/config-store.mjs +106 -0
  27. package/src/core/result.mjs +13 -0
  28. package/src/core/tool-registry.mjs +29 -0
  29. package/src/index.mjs +47 -0
  30. package/src/tools/namecheap/auth.mjs +429 -0
  31. package/src/tools/namecheap/client.mjs +655 -0
  32. package/src/tools/namecheap/mcp.mjs +298 -0
  33. package/src/tools/npm/auth.mjs +367 -0
  34. package/src/tools/npm/client.mjs +317 -0
  35. package/src/tools/npm/mcp.mjs +343 -0
  36. package/src/tools/railway/auth.mjs +402 -0
  37. package/src/tools/railway/client.mjs +388 -0
  38. package/src/tools/railway/mcp.mjs +282 -0
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { z } from "zod";
6
+ import {
7
+ AUTH_CONFIG_PATH,
8
+ clearStoredAuthConfig,
9
+ getAuthStatus,
10
+ runBrowserAuthFlow,
11
+ } from "./auth.mjs";
12
+ import { createResolvedNamecheapClient } from "./client.mjs";
13
+
14
+ const HELP_TEXT = `Usage: agentic-devtools mcp namecheap
15
+
16
+ Namecheap MCP server
17
+
18
+ Optional environment variables:
19
+ NAMECHEAP_API_USER
20
+ NAMECHEAP_API_KEY
21
+ NAMECHEAP_USERNAME
22
+ NAMECHEAP_CLIENT_IP
23
+ NAMECHEAP_API_BASE_URL
24
+ NAMECHEAP_API_SANDBOX=1
25
+
26
+ If env vars are not provided, use the connectNamecheap tool to open the browser-based setup flow and save local plugin credentials.
27
+ `;
28
+
29
+ const recordSchema = z.object({
30
+ name: z.string().min(1),
31
+ type: z.enum([
32
+ "A",
33
+ "AAAA",
34
+ "ALIAS",
35
+ "CAA",
36
+ "CNAME",
37
+ "FRAME",
38
+ "MX",
39
+ "MXE",
40
+ "NS",
41
+ "TXT",
42
+ "URL",
43
+ "URL301",
44
+ ]),
45
+ address: z.string().min(1),
46
+ ttl: z.number().int().min(60).max(60000).optional(),
47
+ mxPref: z.number().int().min(0).max(65535).optional(),
48
+ emailType: z.enum(["FWD", "MX", "MXE", "OX"]).optional(),
49
+ flag: z.number().int().min(0).max(255).optional(),
50
+ tag: z.enum(["issue", "issuewild", "iodef"]).optional(),
51
+ });
52
+
53
+ const createToolResult = (value) => ({
54
+ content: [
55
+ {
56
+ type: "text",
57
+ text: JSON.stringify(value, null, 2),
58
+ },
59
+ ],
60
+ structuredContent: value,
61
+ });
62
+
63
+ const createServer = () => {
64
+ const server = new McpServer(
65
+ {
66
+ name: "namecheap",
67
+ version: "0.1.0",
68
+ },
69
+ {
70
+ instructions:
71
+ "Use these tools for Namecheap-managed domains and DNS. Prefer getDomainDns before mutating records because Namecheap setHosts replaces the full record set. Namecheap auth is API-key based, not OAuth.",
72
+ },
73
+ );
74
+
75
+ const withClient = async (callback) => {
76
+ const client = await createResolvedNamecheapClient();
77
+ return callback(client);
78
+ };
79
+
80
+ server.registerTool(
81
+ "getAuthStatus",
82
+ {
83
+ description:
84
+ "Show whether Namecheap credentials are configured through environment variables or the local plugin auth file.",
85
+ },
86
+ async () => createToolResult(await getAuthStatus()),
87
+ );
88
+
89
+ server.registerTool(
90
+ "connectNamecheap",
91
+ {
92
+ description:
93
+ "Open a browser-based setup flow for Namecheap API credentials. This stores credentials locally in the plugin auth file because Namecheap uses API keys and IP whitelisting rather than OAuth tokens.",
94
+ inputSchema: {
95
+ sandbox: z.boolean().optional(),
96
+ },
97
+ },
98
+ async ({ sandbox }) =>
99
+ createToolResult({
100
+ ...(await runBrowserAuthFlow({
101
+ defaultSandbox: sandbox ?? true,
102
+ })),
103
+ configPath: AUTH_CONFIG_PATH,
104
+ }),
105
+ );
106
+
107
+ server.registerTool(
108
+ "disconnectNamecheap",
109
+ {
110
+ description:
111
+ "Remove locally stored Namecheap credentials from the plugin auth file.",
112
+ },
113
+ async () => {
114
+ await clearStoredAuthConfig();
115
+ return createToolResult({
116
+ disconnected: true,
117
+ configPath: AUTH_CONFIG_PATH,
118
+ });
119
+ },
120
+ );
121
+
122
+ server.registerTool(
123
+ "testNamecheapConnection",
124
+ {
125
+ description:
126
+ "Verify that the configured Namecheap credentials can successfully call the API.",
127
+ },
128
+ async () =>
129
+ createToolResult(
130
+ await withClient(async (client) => {
131
+ const result = await client.listDomains({ page: 1, pageSize: 1 });
132
+ return {
133
+ ok: true,
134
+ domainCount: result.paging.totalItems,
135
+ sampleDomains: result.domains.slice(0, 1).map((domain) => domain.name),
136
+ baseUrl: client.baseUrl,
137
+ };
138
+ }),
139
+ ),
140
+ );
141
+
142
+ server.registerTool(
143
+ "listDomains",
144
+ {
145
+ description:
146
+ "List domains in the configured Namecheap account. Use searchTerm to narrow results.",
147
+ inputSchema: {
148
+ searchTerm: z.string().optional(),
149
+ page: z.number().int().min(1).optional(),
150
+ pageSize: z.number().int().min(1).max(100).optional(),
151
+ listType: z.enum(["ALL", "EXPIRING", "EXPIRED"]).optional(),
152
+ sortBy: z
153
+ .enum([
154
+ "NAME",
155
+ "NAME_DESC",
156
+ "EXPIREDATE",
157
+ "EXPIREDATE_DESC",
158
+ "CREATEDATE",
159
+ "CREATEDATE_DESC",
160
+ ])
161
+ .optional(),
162
+ },
163
+ },
164
+ async (args) =>
165
+ createToolResult(await withClient((client) => client.listDomains(args))),
166
+ );
167
+
168
+ server.registerTool(
169
+ "getDomainDns",
170
+ {
171
+ description:
172
+ "Get Namecheap DNS status, nameservers, and host records for a registered domain.",
173
+ inputSchema: {
174
+ domain: z.string().min(1),
175
+ },
176
+ },
177
+ async ({ domain }) =>
178
+ createToolResult(await withClient((client) => client.getDomainDns(domain))),
179
+ );
180
+
181
+ server.registerTool(
182
+ "replaceDomainDns",
183
+ {
184
+ description:
185
+ "Replace the full DNS host record set for a Namecheap-managed domain. This is destructive and should only be used with the complete intended record list.",
186
+ inputSchema: {
187
+ domain: z.string().min(1),
188
+ records: z.array(recordSchema).min(1),
189
+ emailType: z.enum(["FWD", "MX", "MXE", "OX"]).optional(),
190
+ },
191
+ },
192
+ async ({ domain, records, emailType }) =>
193
+ createToolResult(
194
+ await withClient((client) =>
195
+ client.replaceDomainDns({ domain, records, emailType }),
196
+ ),
197
+ ),
198
+ );
199
+
200
+ server.registerTool(
201
+ "addDomainDnsRecord",
202
+ {
203
+ description:
204
+ "Add a single DNS record while preserving the existing Namecheap DNS zone. No change is made if an identical record already exists.",
205
+ inputSchema: {
206
+ domain: z.string().min(1),
207
+ record: recordSchema,
208
+ },
209
+ },
210
+ async ({ domain, record }) =>
211
+ createToolResult(
212
+ await withClient((client) =>
213
+ client.addDomainDnsRecord({ domain, record }),
214
+ ),
215
+ ),
216
+ );
217
+
218
+ server.registerTool(
219
+ "removeDomainDnsRecord",
220
+ {
221
+ description:
222
+ "Remove a single DNS record by exact match while preserving the rest of the zone.",
223
+ inputSchema: {
224
+ domain: z.string().min(1),
225
+ record: recordSchema,
226
+ },
227
+ },
228
+ async ({ domain, record }) =>
229
+ createToolResult(
230
+ await withClient((client) =>
231
+ client.removeDomainDnsRecord({ domain, record }),
232
+ ),
233
+ ),
234
+ );
235
+
236
+ server.registerTool(
237
+ "updateDomainDnsRecord",
238
+ {
239
+ description:
240
+ "Update exactly one DNS record by explicit match while preserving the rest of the zone. Fails if zero or multiple records match.",
241
+ inputSchema: {
242
+ domain: z.string().min(1),
243
+ matchRecord: recordSchema,
244
+ newRecord: recordSchema,
245
+ },
246
+ },
247
+ async ({ domain, matchRecord, newRecord }) =>
248
+ createToolResult(
249
+ await withClient((client) =>
250
+ client.updateDomainDnsRecord({ domain, matchRecord, newRecord }),
251
+ ),
252
+ ),
253
+ );
254
+
255
+ return server;
256
+ };
257
+
258
+ const argv = process.argv.slice(2);
259
+
260
+ if (argv.includes("--help") || argv.includes("-h")) {
261
+ process.stdout.write(HELP_TEXT);
262
+ process.exit(0);
263
+ }
264
+
265
+ if (argv.includes("--auth-status")) {
266
+ process.stdout.write(`${JSON.stringify(await getAuthStatus(), null, 2)}\n`);
267
+ process.exit(0);
268
+ }
269
+
270
+ if (argv.includes("--connect")) {
271
+ process.stdout.write("Opening Namecheap browser setup flow...\n");
272
+ const result = await runBrowserAuthFlow({ defaultSandbox: true });
273
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
274
+ process.exit(0);
275
+ }
276
+
277
+ if (argv.includes("--test-connection")) {
278
+ const client = await createResolvedNamecheapClient();
279
+ const result = await client.listDomains({ page: 1, pageSize: 1 });
280
+ process.stdout.write(
281
+ `${JSON.stringify(
282
+ {
283
+ ok: true,
284
+ domainCount: result.paging.totalItems,
285
+ sampleDomains: result.domains.slice(0, 1).map((domain) => domain.name),
286
+ baseUrl: client.baseUrl,
287
+ },
288
+ null,
289
+ 2,
290
+ )}\n`,
291
+ );
292
+ process.exit(0);
293
+ }
294
+
295
+ const server = createServer();
296
+ const transport = new StdioServerTransport();
297
+
298
+ await server.connect(transport);
@@ -0,0 +1,367 @@
1
+ import { createServer } from "node:http";
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import { readFileSync } from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import {
8
+ escapeHtml,
9
+ openBrowser,
10
+ parseFormBody,
11
+ pickString,
12
+ readJsonConfig,
13
+ readJsonConfigSync,
14
+ removeJsonConfig,
15
+ resolveConfigPath,
16
+ writeJsonConfig,
17
+ } from "../../core/config-store.mjs";
18
+
19
+ export const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
20
+ export const NPM_AUTH_CONFIG_PATH = resolveConfigPath({
21
+ env: process.env,
22
+ envVar: "AGENTIC_DEVTOOLS_NPM_AUTH_CONFIG_PATH",
23
+ fileName: "npm.json",
24
+ });
25
+
26
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
27
+
28
+ const normalizeRegistry = (registry = DEFAULT_NPM_REGISTRY) =>
29
+ String(registry || DEFAULT_NPM_REGISTRY).replace(/\/+$/, "");
30
+
31
+ const npmrcPaths = (env = process.env) => [
32
+ pickString(env.NPM_CONFIG_USERCONFIG),
33
+ path.join(os.homedir(), ".npmrc"),
34
+ ].filter(Boolean);
35
+
36
+ const npmrcTokenKeyForRegistry = (registry) => {
37
+ const url = new URL(normalizeRegistry(registry));
38
+ return `//${url.host}${url.pathname === "/" ? "" : url.pathname}/:_authToken`;
39
+ };
40
+
41
+ const parseNpmrc = (content, registry) => {
42
+ const expectedKey = npmrcTokenKeyForRegistry(registry);
43
+ const lines = content.split(/\r?\n/);
44
+
45
+ for (const line of lines) {
46
+ const trimmed = line.trim();
47
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) {
48
+ continue;
49
+ }
50
+ const separatorIndex = trimmed.indexOf("=");
51
+ if (separatorIndex < 0) {
52
+ continue;
53
+ }
54
+ const key = trimmed.slice(0, separatorIndex).trim();
55
+ const value = trimmed.slice(separatorIndex + 1).trim();
56
+ if (key === expectedKey && value) {
57
+ return value.replace(/^["']|["']$/g, "");
58
+ }
59
+ }
60
+
61
+ return null;
62
+ };
63
+
64
+ const readNpmrcToken = async (env = process.env, registry = DEFAULT_NPM_REGISTRY) => {
65
+ if (env !== process.env && !env.NPM_CONFIG_USERCONFIG) {
66
+ return null;
67
+ }
68
+
69
+ for (const filePath of npmrcPaths(env)) {
70
+ try {
71
+ const token = parseNpmrc(await readFile(filePath, "utf8"), registry);
72
+ if (token) {
73
+ return { token, source: `npmrc:${filePath}` };
74
+ }
75
+ } catch (error) {
76
+ if (error && typeof error === "object" && error.code === "ENOENT") {
77
+ continue;
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ return null;
84
+ };
85
+
86
+ const readNpmrcTokenSync = (env = process.env, registry = DEFAULT_NPM_REGISTRY) => {
87
+ if (env !== process.env && !env.NPM_CONFIG_USERCONFIG) {
88
+ return null;
89
+ }
90
+
91
+ for (const filePath of npmrcPaths(env)) {
92
+ try {
93
+ const token = parseNpmrc(readFileSync(filePath, "utf8"), registry);
94
+ if (token) {
95
+ return { token, source: `npmrc:${filePath}` };
96
+ }
97
+ } catch (error) {
98
+ if (error && typeof error === "object" && error.code === "ENOENT") {
99
+ continue;
100
+ }
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ return null;
106
+ };
107
+
108
+ const shouldReadStoredConfig = (env) =>
109
+ env === process.env ||
110
+ typeof env.AGENTIC_DEVTOOLS_NPM_AUTH_CONFIG_PATH === "string";
111
+
112
+ const configPathForEnv = (env = process.env) =>
113
+ resolveConfigPath({
114
+ env,
115
+ envVar: "AGENTIC_DEVTOOLS_NPM_AUTH_CONFIG_PATH",
116
+ fileName: "npm.json",
117
+ });
118
+
119
+ const readStoredNpmAuthConfig = (env = process.env) => {
120
+ if (!shouldReadStoredConfig(env)) {
121
+ return null;
122
+ }
123
+ return readJsonConfigSync(configPathForEnv(env));
124
+ };
125
+
126
+ export const saveNpmAuthConfig = async ({
127
+ token,
128
+ registry = DEFAULT_NPM_REGISTRY,
129
+ } = {}) => {
130
+ const config = {
131
+ token: String(token ?? "").trim(),
132
+ registry: normalizeRegistry(registry),
133
+ savedAt: new Date().toISOString(),
134
+ };
135
+
136
+ if (!config.token) {
137
+ throw new Error("Missing token in npm auth config.");
138
+ }
139
+
140
+ await writeJsonConfig(NPM_AUTH_CONFIG_PATH, config);
141
+
142
+ return {
143
+ configPath: NPM_AUTH_CONFIG_PATH,
144
+ registry: config.registry,
145
+ };
146
+ };
147
+
148
+ export const resolveNpmAuthConfig = (env = process.env) => {
149
+ const registry =
150
+ pickString(env.NPM_CONFIG_REGISTRY) ||
151
+ pickString(env.npm_config_registry) ||
152
+ DEFAULT_NPM_REGISTRY;
153
+
154
+ const envToken = pickString(env.NPM_TOKEN) || pickString(env.NODE_AUTH_TOKEN);
155
+ if (envToken) {
156
+ return {
157
+ token: envToken,
158
+ registry: normalizeRegistry(registry),
159
+ source: pickString(env.NPM_TOKEN) ? "env:NPM_TOKEN" : "env:NODE_AUTH_TOKEN",
160
+ };
161
+ }
162
+
163
+ const stored = readStoredNpmAuthConfig(env);
164
+ if (stored?.token) {
165
+ return {
166
+ token: stored.token,
167
+ registry: normalizeRegistry(stored.registry || registry),
168
+ source: "file",
169
+ };
170
+ }
171
+
172
+ const npmrc = readNpmrcTokenSync(env, registry);
173
+ if (npmrc?.token) {
174
+ return {
175
+ token: npmrc.token,
176
+ registry: normalizeRegistry(registry),
177
+ source: npmrc.source,
178
+ };
179
+ }
180
+
181
+ return {
182
+ token: null,
183
+ registry: normalizeRegistry(registry),
184
+ source: null,
185
+ };
186
+ };
187
+
188
+ export const getNpmAuthStatus = (env = process.env) => {
189
+ const auth = resolveNpmAuthConfig(env);
190
+ return {
191
+ configured: Boolean(auth.token),
192
+ source: auth.source,
193
+ registry: auth.registry,
194
+ configPath: configPathForEnv(env),
195
+ };
196
+ };
197
+
198
+ export const clearStoredNpmAuthConfig = async () => {
199
+ await removeJsonConfig(NPM_AUTH_CONFIG_PATH);
200
+ };
201
+
202
+ export const disconnectNpm = async () => {
203
+ await clearStoredNpmAuthConfig();
204
+ return {
205
+ disconnected: true,
206
+ configPath: NPM_AUTH_CONFIG_PATH,
207
+ };
208
+ };
209
+
210
+ /* v8 ignore start */
211
+ const renderNpmPage = ({ csrfToken, message = "", defaults = {} }) => `<!doctype html>
212
+ <html lang="en">
213
+ <head>
214
+ <meta charset="utf-8" />
215
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
216
+ <title>npm Setup</title>
217
+ <style>
218
+ body { margin: 0; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f7f7f8; color: #191919; }
219
+ .wrap { max-width: 760px; margin: 32px auto; padding: 24px; }
220
+ .panel { background: white; border: 1px solid #dedee3; border-radius: 16px; padding: 24px; }
221
+ h1 { margin-top: 0; font-size: 28px; }
222
+ p, li { line-height: 1.5; color: #5f6068; }
223
+ a { color: #cb3837; }
224
+ form { display: grid; gap: 16px; margin-top: 24px; }
225
+ label { display: grid; gap: 6px; font-weight: 600; }
226
+ input { padding: 12px 14px; border-radius: 10px; border: 1px solid #dedee3; font: inherit; background: white; }
227
+ .message { margin-top: 16px; padding: 12px 14px; border-radius: 10px; background: #fff1ec; color: #8f2719; }
228
+ button { border: 0; border-radius: 999px; padding: 12px 18px; font: inherit; font-weight: 700; color: white; background: #cb3837; cursor: pointer; width: fit-content; }
229
+ code { background: #eeeeef; padding: 2px 6px; border-radius: 6px; }
230
+ </style>
231
+ </head>
232
+ <body>
233
+ <div class="wrap">
234
+ <div class="panel">
235
+ <h1>Connect npm</h1>
236
+ <p>npm does not provide a normal third-party OAuth login for local tools. Create a granular token, paste it here, and Agentic Devtools will store it locally.</p>
237
+ <ol>
238
+ <li>Open <a href="https://www.npmjs.com/settings/-/tokens" target="_blank" rel="noreferrer">npm access tokens</a>.</li>
239
+ <li>Create the narrowest granular token that matches your task.</li>
240
+ <li>For publishing, prefer GitHub Actions Trusted Publishing instead of long-lived write tokens.</li>
241
+ </ol>
242
+ ${message ? `<div class="message">${escapeHtml(message)}</div>` : ""}
243
+ <form method="post" action="/save">
244
+ <input type="hidden" name="csrfToken" value="${escapeHtml(csrfToken)}" />
245
+ <label>
246
+ npm Token
247
+ <input name="token" type="password" required />
248
+ </label>
249
+ <label>
250
+ Registry
251
+ <input name="registry" type="text" value="${escapeHtml(defaults.registry ?? DEFAULT_NPM_REGISTRY)}" />
252
+ </label>
253
+ <button type="submit">Save npm Token</button>
254
+ </form>
255
+ </div>
256
+ </div>
257
+ </body>
258
+ </html>`;
259
+
260
+ export const runNpmBrowserAuthFlow = async ({
261
+ onReady,
262
+ validateConnection = true,
263
+ timeoutMs = DEFAULT_TIMEOUT_MS,
264
+ } = {}) => {
265
+ const existing = await readJsonConfig(NPM_AUTH_CONFIG_PATH);
266
+ const csrfToken = randomUUID();
267
+
268
+ return await new Promise((resolve, reject) => {
269
+ let closed = false;
270
+ let timeoutId;
271
+
272
+ const finish = (callback) => {
273
+ if (closed) {
274
+ return;
275
+ }
276
+ closed = true;
277
+ clearTimeout(timeoutId);
278
+ server.close(() => callback());
279
+ };
280
+
281
+ const server = createServer(async (request, response) => {
282
+ const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
283
+
284
+ if (request.method === "GET" && requestUrl.pathname === "/") {
285
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
286
+ response.end(
287
+ renderNpmPage({
288
+ csrfToken,
289
+ defaults: { registry: existing?.registry ?? DEFAULT_NPM_REGISTRY },
290
+ }),
291
+ );
292
+ return;
293
+ }
294
+
295
+ if (request.method === "POST" && requestUrl.pathname === "/save") {
296
+ let body = {};
297
+ try {
298
+ body = await parseFormBody(request);
299
+ if (body.csrfToken !== csrfToken) {
300
+ response.writeHead(403, { "content-type": "text/html; charset=utf-8" });
301
+ response.end("Invalid CSRF token.");
302
+ return;
303
+ }
304
+
305
+ if (validateConnection) {
306
+ const { createNpmClient } = await import("./client.mjs");
307
+ const client = createNpmClient({
308
+ env: {
309
+ NPM_TOKEN: String(body.token ?? ""),
310
+ NPM_CONFIG_REGISTRY: String(body.registry ?? DEFAULT_NPM_REGISTRY),
311
+ },
312
+ });
313
+ await client.getCurrentUser();
314
+ }
315
+
316
+ const saved = await saveNpmAuthConfig({
317
+ token: body.token,
318
+ registry: body.registry,
319
+ });
320
+
321
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
322
+ response.end(`<!doctype html><html><body style="font-family: sans-serif; padding: 24px;"><h1>npm connected</h1><p>Token was saved to <code>${escapeHtml(saved.configPath)}</code>. You can close this tab.</p></body></html>`);
323
+ finish(() => resolve(saved));
324
+ } catch (error) {
325
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
326
+ response.end(
327
+ renderNpmPage({
328
+ csrfToken,
329
+ message: error instanceof Error ? error.message : String(error),
330
+ defaults: { registry: body.registry ?? DEFAULT_NPM_REGISTRY },
331
+ }),
332
+ );
333
+ }
334
+ return;
335
+ }
336
+
337
+ response.writeHead(404);
338
+ response.end("Not found");
339
+ });
340
+
341
+ server.listen(0, "127.0.0.1", async () => {
342
+ try {
343
+ const address = server.address();
344
+ if (!address || typeof address === "string") {
345
+ throw new Error("Failed to bind local npm auth server.");
346
+ }
347
+
348
+ const url = `http://127.0.0.1:${address.port}/`;
349
+ if (typeof onReady === "function") {
350
+ onReady({ url });
351
+ }
352
+ await openBrowser(url, {
353
+ skipEnvVar: "NPM_SKIP_BROWSER_OPEN",
354
+ });
355
+ } catch (error) {
356
+ finish(() => reject(error));
357
+ }
358
+ });
359
+
360
+ timeoutId = setTimeout(() => {
361
+ finish(() => reject(new Error("Timed out waiting for npm browser setup to complete.")));
362
+ }, timeoutMs);
363
+ });
364
+ };
365
+ /* v8 ignore stop */
366
+
367
+ export const connectNpm = runNpmBrowserAuthFlow;