@nproxy/cli 0.0.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 +362 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Cli, z } from "incur";
|
|
5
|
+
|
|
6
|
+
// src/npmrc.ts
|
|
7
|
+
import { readFile, writeFile } from "fs/promises";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
async function readSlugFromNpmrc(dir = process.cwd()) {
|
|
11
|
+
const file = join(dir, ".npmrc");
|
|
12
|
+
let content;
|
|
13
|
+
try {
|
|
14
|
+
content = await readFile(file, "utf-8");
|
|
15
|
+
} catch {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
const match = content.match(
|
|
19
|
+
/registry\s*=\s*https:\/\/([^.]+)\.nproxy\.app\/?/
|
|
20
|
+
);
|
|
21
|
+
return match?.[1];
|
|
22
|
+
}
|
|
23
|
+
async function readTokenFromNpmrc(slug, dir = process.cwd()) {
|
|
24
|
+
const file = join(dir, ".npmrc");
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = await readFile(file, "utf-8");
|
|
28
|
+
} catch {
|
|
29
|
+
return void 0;
|
|
30
|
+
}
|
|
31
|
+
const escapedSlug = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
32
|
+
const pattern = new RegExp(
|
|
33
|
+
`\\/\\/${escapedSlug}\\.nproxy\\.app\\/:_authToken=(.+)`
|
|
34
|
+
);
|
|
35
|
+
const match = content.match(pattern);
|
|
36
|
+
return match?.[1]?.trim();
|
|
37
|
+
}
|
|
38
|
+
function upsertLine(lines, prefix, full) {
|
|
39
|
+
const idx = lines.findIndex((l) => l.startsWith(prefix));
|
|
40
|
+
if (idx >= 0) {
|
|
41
|
+
lines[idx] = full;
|
|
42
|
+
} else {
|
|
43
|
+
lines.push(full);
|
|
44
|
+
}
|
|
45
|
+
return lines;
|
|
46
|
+
}
|
|
47
|
+
async function writeNpmrc(options) {
|
|
48
|
+
const { slug, token } = options;
|
|
49
|
+
const isGlobal = options.global ?? false;
|
|
50
|
+
const dir = isGlobal ? homedir() : options.dir ?? process.cwd();
|
|
51
|
+
const file = join(dir, ".npmrc");
|
|
52
|
+
let content = "";
|
|
53
|
+
try {
|
|
54
|
+
content = await readFile(file, "utf-8");
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
if (content.length > 0) {
|
|
58
|
+
const timestamp = Date.now();
|
|
59
|
+
await writeFile(join(dir, `.npmrc.bak.${timestamp}`), content);
|
|
60
|
+
}
|
|
61
|
+
const registry = `https://${slug}.nproxy.app/`;
|
|
62
|
+
let lines = content.split("\n");
|
|
63
|
+
if (lines.at(-1) === "") lines.pop();
|
|
64
|
+
lines = upsertLine(lines, "registry=", `registry=${registry}`);
|
|
65
|
+
if (token) {
|
|
66
|
+
const authPrefix = `//${slug}.nproxy.app/:_authToken=`;
|
|
67
|
+
lines = upsertLine(lines, authPrefix, `${authPrefix}${token}`);
|
|
68
|
+
}
|
|
69
|
+
await writeFile(file, lines.join("\n") + "\n");
|
|
70
|
+
return file;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/setup.ts
|
|
74
|
+
async function runSetup(options) {
|
|
75
|
+
const { slug, global: isGlobal, token } = options;
|
|
76
|
+
const registry = `https://${slug}.nproxy.app/`;
|
|
77
|
+
const file = await writeNpmrc({ slug, global: isGlobal, token });
|
|
78
|
+
return { file, registry };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/verify.ts
|
|
82
|
+
import { resolve4 } from "dns/promises";
|
|
83
|
+
async function timed(fn) {
|
|
84
|
+
const start = performance.now();
|
|
85
|
+
const value = await fn();
|
|
86
|
+
return { value, duration: Math.round(performance.now() - start) };
|
|
87
|
+
}
|
|
88
|
+
async function checkDns(slug) {
|
|
89
|
+
const host = `${slug}.nproxy.app`;
|
|
90
|
+
try {
|
|
91
|
+
const { value: addresses, duration } = await timed(() => resolve4(host));
|
|
92
|
+
return {
|
|
93
|
+
name: "dns",
|
|
94
|
+
status: addresses.length > 0 ? "pass" : "fail",
|
|
95
|
+
message: addresses.length > 0 ? `Resolved to ${addresses[0]}` : "No addresses returned",
|
|
96
|
+
duration
|
|
97
|
+
};
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return {
|
|
100
|
+
name: "dns",
|
|
101
|
+
status: "fail",
|
|
102
|
+
message: `DNS lookup failed: ${err.message}`,
|
|
103
|
+
duration: 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function checkPing(slug) {
|
|
108
|
+
const url = `https://${slug}.nproxy.app/-/ping`;
|
|
109
|
+
try {
|
|
110
|
+
const { value: res, duration } = await timed(() => fetch(url));
|
|
111
|
+
return {
|
|
112
|
+
name: "ping",
|
|
113
|
+
status: res.status === 200 ? "pass" : "fail",
|
|
114
|
+
message: res.status === 200 ? `OK (${res.status})` : `Unexpected status ${res.status}`,
|
|
115
|
+
duration
|
|
116
|
+
};
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
name: "ping",
|
|
120
|
+
status: "fail",
|
|
121
|
+
message: `Ping failed: ${err.message}`,
|
|
122
|
+
duration: 0
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function checkDashboard() {
|
|
127
|
+
const url = "https://nproxy.app/api/health";
|
|
128
|
+
try {
|
|
129
|
+
const { value: res, duration } = await timed(() => fetch(url));
|
|
130
|
+
if (res.status !== 200) {
|
|
131
|
+
return {
|
|
132
|
+
name: "dashboard",
|
|
133
|
+
status: "warn",
|
|
134
|
+
message: `Dashboard returned status ${res.status}`,
|
|
135
|
+
duration
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const body = await res.json();
|
|
139
|
+
return {
|
|
140
|
+
name: "dashboard",
|
|
141
|
+
status: body.status === "ok" ? "pass" : "warn",
|
|
142
|
+
message: body.status === "ok" ? "Dashboard healthy" : `Dashboard status: ${body.status}`,
|
|
143
|
+
duration
|
|
144
|
+
};
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return {
|
|
147
|
+
name: "dashboard",
|
|
148
|
+
status: "warn",
|
|
149
|
+
message: `Dashboard unreachable: ${err.message}`,
|
|
150
|
+
duration: 0
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function fetchPackageData(slug, token) {
|
|
155
|
+
const url = `https://${slug}.nproxy.app/express`;
|
|
156
|
+
const headers = { Accept: "application/json" };
|
|
157
|
+
if (token) {
|
|
158
|
+
headers.Authorization = `Bearer ${token}`;
|
|
159
|
+
}
|
|
160
|
+
const { value: res, duration } = await timed(
|
|
161
|
+
() => fetch(url, { headers })
|
|
162
|
+
);
|
|
163
|
+
const body = await res.json();
|
|
164
|
+
return { response: res, body, duration };
|
|
165
|
+
}
|
|
166
|
+
function checkPackageFetch(slug, data) {
|
|
167
|
+
const raw = JSON.stringify(data.body);
|
|
168
|
+
const expected = `${slug}.nproxy.app`;
|
|
169
|
+
const hasTarballRewrite = raw.includes(expected);
|
|
170
|
+
return {
|
|
171
|
+
name: "package_fetch",
|
|
172
|
+
status: hasTarballRewrite ? "pass" : "fail",
|
|
173
|
+
message: hasTarballRewrite ? `Tarball URLs rewritten to ${expected}` : `Tarball URLs not rewritten to ${expected}`,
|
|
174
|
+
duration: data.duration
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function checkProxyHeaders(slug, data) {
|
|
178
|
+
const tenant = data.response.headers.get("x-nproxy-tenant");
|
|
179
|
+
return {
|
|
180
|
+
name: "proxy_headers",
|
|
181
|
+
status: tenant === slug ? "pass" : "fail",
|
|
182
|
+
message: tenant === slug ? `x-nproxy-tenant: ${tenant}` : `Expected x-nproxy-tenant="${slug}", got "${tenant}"`,
|
|
183
|
+
duration: 0
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async function checkTokenAuth(slug, token) {
|
|
187
|
+
const url = `https://${slug}.nproxy.app/-/ping`;
|
|
188
|
+
try {
|
|
189
|
+
const { value: res, duration } = await timed(
|
|
190
|
+
() => fetch(url, {
|
|
191
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
if (res.status === 200) {
|
|
195
|
+
return {
|
|
196
|
+
name: "token_auth",
|
|
197
|
+
status: "pass",
|
|
198
|
+
message: "Token accepted by proxy",
|
|
199
|
+
duration
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
name: "token_auth",
|
|
204
|
+
status: "fail",
|
|
205
|
+
message: `Token rejected with status ${res.status}`,
|
|
206
|
+
duration
|
|
207
|
+
};
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return {
|
|
210
|
+
name: "token_auth",
|
|
211
|
+
status: "fail",
|
|
212
|
+
message: `Token auth check failed: ${err.message}`,
|
|
213
|
+
duration: 0
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function runVerify(slug, quick = false, token) {
|
|
218
|
+
const checks = [];
|
|
219
|
+
const [dns, ping] = await Promise.all([checkDns(slug), checkPing(slug)]);
|
|
220
|
+
checks.push(dns, ping);
|
|
221
|
+
if (!quick) {
|
|
222
|
+
const dashboard = await checkDashboard();
|
|
223
|
+
checks.push(dashboard);
|
|
224
|
+
try {
|
|
225
|
+
const data = await fetchPackageData(slug, token);
|
|
226
|
+
checks.push(checkPackageFetch(slug, data));
|
|
227
|
+
checks.push(checkProxyHeaders(slug, data));
|
|
228
|
+
} catch (err) {
|
|
229
|
+
checks.push({
|
|
230
|
+
name: "package_fetch",
|
|
231
|
+
status: "fail",
|
|
232
|
+
message: `Fetch failed: ${err.message}`,
|
|
233
|
+
duration: 0
|
|
234
|
+
});
|
|
235
|
+
checks.push({
|
|
236
|
+
name: "proxy_headers",
|
|
237
|
+
status: "fail",
|
|
238
|
+
message: `Fetch failed: ${err.message}`,
|
|
239
|
+
duration: 0
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (token) {
|
|
243
|
+
const tokenCheck = await checkTokenAuth(slug, token);
|
|
244
|
+
checks.push(tokenCheck);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const ok = checks.every((c) => c.status !== "fail");
|
|
248
|
+
return { slug, checks, ok };
|
|
249
|
+
}
|
|
250
|
+
async function runStatus(slug) {
|
|
251
|
+
const ping = await checkPing(slug);
|
|
252
|
+
return {
|
|
253
|
+
slug,
|
|
254
|
+
reachable: ping.status === "pass",
|
|
255
|
+
latency: ping.duration
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/index.ts
|
|
260
|
+
var cli = Cli.create("nproxy", {
|
|
261
|
+
version: "0.0.1",
|
|
262
|
+
description: "Configure and verify your nproxy registry proxy."
|
|
263
|
+
});
|
|
264
|
+
cli.command("setup", {
|
|
265
|
+
description: "Write/update .npmrc to use your nproxy registry.",
|
|
266
|
+
args: z.object({
|
|
267
|
+
slug: z.string().describe("Your organization slug")
|
|
268
|
+
}),
|
|
269
|
+
options: z.object({
|
|
270
|
+
global: z.boolean().optional().describe("Write to ~/.npmrc"),
|
|
271
|
+
token: z.string().optional().describe("Auth token to include")
|
|
272
|
+
}),
|
|
273
|
+
alias: { global: "g" },
|
|
274
|
+
output: z.object({
|
|
275
|
+
file: z.string().describe("Path to the .npmrc that was written"),
|
|
276
|
+
registry: z.string().describe("The registry URL that was set")
|
|
277
|
+
}),
|
|
278
|
+
examples: [
|
|
279
|
+
{
|
|
280
|
+
args: { slug: "acme" },
|
|
281
|
+
description: "Set up local .npmrc for acme"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
args: { slug: "acme" },
|
|
285
|
+
options: { global: true },
|
|
286
|
+
description: "Set up global .npmrc for acme"
|
|
287
|
+
}
|
|
288
|
+
],
|
|
289
|
+
async run({ args, options }) {
|
|
290
|
+
return runSetup({
|
|
291
|
+
slug: args.slug,
|
|
292
|
+
global: options.global,
|
|
293
|
+
token: options.token
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
cli.command("verify", {
|
|
298
|
+
description: "Run connectivity checks against your nproxy registry.",
|
|
299
|
+
args: z.object({
|
|
300
|
+
slug: z.string().optional().describe("Org slug (reads from .npmrc if omitted)")
|
|
301
|
+
}),
|
|
302
|
+
options: z.object({
|
|
303
|
+
quick: z.boolean().optional().describe("Only run dns + ping checks")
|
|
304
|
+
}),
|
|
305
|
+
output: z.object({
|
|
306
|
+
slug: z.string().describe("Organization slug that was verified"),
|
|
307
|
+
checks: z.array(
|
|
308
|
+
z.object({
|
|
309
|
+
name: z.string().describe("Check name (dns, ping, dashboard, package_fetch, proxy_headers)"),
|
|
310
|
+
status: z.enum(["pass", "fail", "warn"]).describe("Check result"),
|
|
311
|
+
message: z.string().describe("Human-readable result detail"),
|
|
312
|
+
duration: z.number().describe("Check duration in milliseconds")
|
|
313
|
+
})
|
|
314
|
+
).describe("List of check results"),
|
|
315
|
+
ok: z.boolean().describe("True if no checks failed")
|
|
316
|
+
}),
|
|
317
|
+
examples: [
|
|
318
|
+
{ args: { slug: "acme" }, description: "Verify acme proxy" },
|
|
319
|
+
{
|
|
320
|
+
options: { quick: true },
|
|
321
|
+
description: "Quick check (dns + ping only)"
|
|
322
|
+
}
|
|
323
|
+
],
|
|
324
|
+
async run({ args, options, error }) {
|
|
325
|
+
const slug = args.slug ?? await readSlugFromNpmrc();
|
|
326
|
+
if (!slug) {
|
|
327
|
+
return error({
|
|
328
|
+
code: "NO_SLUG",
|
|
329
|
+
message: "No slug provided and none found in .npmrc. Pass a slug or run `nproxy setup <slug>` first."
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const token = await readTokenFromNpmrc(slug);
|
|
333
|
+
return runVerify(slug, options.quick, token);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
cli.command("status", {
|
|
337
|
+
description: "Quick reachability check for your nproxy registry.",
|
|
338
|
+
args: z.object({
|
|
339
|
+
slug: z.string().optional().describe("Org slug (reads from .npmrc if omitted)")
|
|
340
|
+
}),
|
|
341
|
+
output: z.object({
|
|
342
|
+
slug: z.string().describe("Organization slug that was checked"),
|
|
343
|
+
reachable: z.boolean().describe("Whether the proxy responded to ping"),
|
|
344
|
+
latency: z.number().describe("Ping response time in milliseconds")
|
|
345
|
+
}),
|
|
346
|
+
examples: [{ args: { slug: "acme" }, description: "Check acme status" }],
|
|
347
|
+
async run({ args, error }) {
|
|
348
|
+
const slug = args.slug ?? await readSlugFromNpmrc();
|
|
349
|
+
if (!slug) {
|
|
350
|
+
return error({
|
|
351
|
+
code: "NO_SLUG",
|
|
352
|
+
message: "No slug provided and none found in .npmrc. Pass a slug or run `nproxy setup <slug>` first."
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return runStatus(slug);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
cli.serve();
|
|
359
|
+
var index_default = cli;
|
|
360
|
+
export {
|
|
361
|
+
index_default as default
|
|
362
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nproxy/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Configure and verify your nproxy registry proxy.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nproxy": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"prepublishOnly": "pnpm build"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"npm",
|
|
26
|
+
"proxy",
|
|
27
|
+
"registry",
|
|
28
|
+
"supply-chain",
|
|
29
|
+
"security"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/nproxy-app/nproxy",
|
|
35
|
+
"directory": "packages/cli"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"incur": "^0.3.13"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"tsup": "^8.4.0",
|
|
42
|
+
"tsx": "^4.19.0",
|
|
43
|
+
"typescript": "^5.8.0",
|
|
44
|
+
"vitest": "^3.1.0"
|
|
45
|
+
}
|
|
46
|
+
}
|