company-dossier 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/cli.js +57 -7
- package/dist/index.js +57 -7
- package/dist/mcp-http.js +76 -11
- package/dist/mcp-server.js +57 -7
- package/dist/mcp.js +57 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promises as dns } from "dns";
|
|
|
6
6
|
// src/utils.ts
|
|
7
7
|
import * as fs from "fs";
|
|
8
8
|
import * as path from "path";
|
|
9
|
+
import { lookup } from "dns/promises";
|
|
9
10
|
var USER_AGENT = "company-dossier/0.1 (+https://companydossier.lol)";
|
|
10
11
|
function mkdirp(dirPath) {
|
|
11
12
|
if (!fs.existsSync(dirPath)) {
|
|
@@ -22,18 +23,67 @@ function todayISO() {
|
|
|
22
23
|
function titleCase(str) {
|
|
23
24
|
return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
24
25
|
}
|
|
26
|
+
function isPrivateIp(ip) {
|
|
27
|
+
const v = ip.replace(/^\[|\]$/g, "").toLowerCase();
|
|
28
|
+
if (v.includes(":")) {
|
|
29
|
+
if (v === "::1" || v === "::") return true;
|
|
30
|
+
if (v.startsWith("fe80") || v.startsWith("fc") || v.startsWith("fd")) return true;
|
|
31
|
+
const m = v.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
32
|
+
if (m) return isPrivateIp(m[1]);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const p = v.split(".").map(Number);
|
|
36
|
+
if (p.length !== 4 || p.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return true;
|
|
37
|
+
const [a, b] = p;
|
|
38
|
+
if (a === 10 || a === 127 || a === 0) return true;
|
|
39
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
40
|
+
if (a === 192 && b === 168) return true;
|
|
41
|
+
if (a === 169 && b === 254) return true;
|
|
42
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
43
|
+
if (a === 192 && b === 0) return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
async function assertPublicUrl(url) {
|
|
47
|
+
let u;
|
|
48
|
+
try {
|
|
49
|
+
u = new URL(url);
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error("Invalid URL");
|
|
52
|
+
}
|
|
53
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error(`Blocked protocol: ${u.protocol}`);
|
|
54
|
+
const host = u.hostname.toLowerCase();
|
|
55
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
56
|
+
throw new Error(`Blocked host: ${host}`);
|
|
57
|
+
}
|
|
58
|
+
if (/^[0-9.]+$/.test(host) || host.includes(":")) {
|
|
59
|
+
if (isPrivateIp(host)) throw new Error(`Blocked private address: ${host}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const addrs = await lookup(host, { all: true });
|
|
63
|
+
for (const a of addrs) if (isPrivateIp(a.address)) throw new Error(`Blocked private address for ${host}: ${a.address}`);
|
|
64
|
+
}
|
|
25
65
|
async function fetchText(url, timeoutMs = 1e4) {
|
|
26
66
|
const controller = new AbortController();
|
|
27
67
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
28
68
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
69
|
+
let current = url;
|
|
70
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
71
|
+
await assertPublicUrl(current);
|
|
72
|
+
const resp = await fetch(current, {
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
redirect: "manual",
|
|
75
|
+
headers: { "User-Agent": USER_AGENT }
|
|
76
|
+
});
|
|
77
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
78
|
+
const loc = resp.headers.get("location");
|
|
79
|
+
if (!loc) throw new Error(`HTTP ${resp.status} without Location`);
|
|
80
|
+
current = new URL(loc, current).toString();
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
84
|
+
return await resp.text();
|
|
35
85
|
}
|
|
36
|
-
|
|
86
|
+
throw new Error("Too many redirects");
|
|
37
87
|
} finally {
|
|
38
88
|
clearTimeout(timer);
|
|
39
89
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as path2 from "path";
|
|
|
4
4
|
// src/utils.ts
|
|
5
5
|
import * as fs from "fs";
|
|
6
6
|
import * as path from "path";
|
|
7
|
+
import { lookup } from "dns/promises";
|
|
7
8
|
var USER_AGENT = "company-dossier/0.1 (+https://companydossier.lol)";
|
|
8
9
|
function mkdirp(dirPath) {
|
|
9
10
|
if (!fs.existsSync(dirPath)) {
|
|
@@ -23,18 +24,67 @@ function slugify(name) {
|
|
|
23
24
|
function titleCase(str) {
|
|
24
25
|
return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
25
26
|
}
|
|
27
|
+
function isPrivateIp(ip) {
|
|
28
|
+
const v = ip.replace(/^\[|\]$/g, "").toLowerCase();
|
|
29
|
+
if (v.includes(":")) {
|
|
30
|
+
if (v === "::1" || v === "::") return true;
|
|
31
|
+
if (v.startsWith("fe80") || v.startsWith("fc") || v.startsWith("fd")) return true;
|
|
32
|
+
const m = v.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
33
|
+
if (m) return isPrivateIp(m[1]);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const p = v.split(".").map(Number);
|
|
37
|
+
if (p.length !== 4 || p.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return true;
|
|
38
|
+
const [a, b] = p;
|
|
39
|
+
if (a === 10 || a === 127 || a === 0) return true;
|
|
40
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
41
|
+
if (a === 192 && b === 168) return true;
|
|
42
|
+
if (a === 169 && b === 254) return true;
|
|
43
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
44
|
+
if (a === 192 && b === 0) return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
async function assertPublicUrl(url) {
|
|
48
|
+
let u;
|
|
49
|
+
try {
|
|
50
|
+
u = new URL(url);
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error("Invalid URL");
|
|
53
|
+
}
|
|
54
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error(`Blocked protocol: ${u.protocol}`);
|
|
55
|
+
const host = u.hostname.toLowerCase();
|
|
56
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
57
|
+
throw new Error(`Blocked host: ${host}`);
|
|
58
|
+
}
|
|
59
|
+
if (/^[0-9.]+$/.test(host) || host.includes(":")) {
|
|
60
|
+
if (isPrivateIp(host)) throw new Error(`Blocked private address: ${host}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const addrs = await lookup(host, { all: true });
|
|
64
|
+
for (const a of addrs) if (isPrivateIp(a.address)) throw new Error(`Blocked private address for ${host}: ${a.address}`);
|
|
65
|
+
}
|
|
26
66
|
async function fetchText(url, timeoutMs = 1e4) {
|
|
27
67
|
const controller = new AbortController();
|
|
28
68
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
29
69
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
70
|
+
let current = url;
|
|
71
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
72
|
+
await assertPublicUrl(current);
|
|
73
|
+
const resp = await fetch(current, {
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
redirect: "manual",
|
|
76
|
+
headers: { "User-Agent": USER_AGENT }
|
|
77
|
+
});
|
|
78
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
79
|
+
const loc = resp.headers.get("location");
|
|
80
|
+
if (!loc) throw new Error(`HTTP ${resp.status} without Location`);
|
|
81
|
+
current = new URL(loc, current).toString();
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
85
|
+
return await resp.text();
|
|
36
86
|
}
|
|
37
|
-
|
|
87
|
+
throw new Error("Too many redirects");
|
|
38
88
|
} finally {
|
|
39
89
|
clearTimeout(timer);
|
|
40
90
|
}
|
package/dist/mcp-http.js
CHANGED
|
@@ -16,6 +16,7 @@ import { promises as dns } from "dns";
|
|
|
16
16
|
// src/utils.ts
|
|
17
17
|
import * as fs from "fs";
|
|
18
18
|
import * as path from "path";
|
|
19
|
+
import { lookup } from "dns/promises";
|
|
19
20
|
var USER_AGENT = "company-dossier/0.1 (+https://companydossier.lol)";
|
|
20
21
|
function todayISO() {
|
|
21
22
|
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -23,18 +24,67 @@ function todayISO() {
|
|
|
23
24
|
function titleCase(str) {
|
|
24
25
|
return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
25
26
|
}
|
|
27
|
+
function isPrivateIp(ip) {
|
|
28
|
+
const v = ip.replace(/^\[|\]$/g, "").toLowerCase();
|
|
29
|
+
if (v.includes(":")) {
|
|
30
|
+
if (v === "::1" || v === "::") return true;
|
|
31
|
+
if (v.startsWith("fe80") || v.startsWith("fc") || v.startsWith("fd")) return true;
|
|
32
|
+
const m = v.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
33
|
+
if (m) return isPrivateIp(m[1]);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const p = v.split(".").map(Number);
|
|
37
|
+
if (p.length !== 4 || p.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return true;
|
|
38
|
+
const [a, b] = p;
|
|
39
|
+
if (a === 10 || a === 127 || a === 0) return true;
|
|
40
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
41
|
+
if (a === 192 && b === 168) return true;
|
|
42
|
+
if (a === 169 && b === 254) return true;
|
|
43
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
44
|
+
if (a === 192 && b === 0) return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
async function assertPublicUrl(url) {
|
|
48
|
+
let u;
|
|
49
|
+
try {
|
|
50
|
+
u = new URL(url);
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error("Invalid URL");
|
|
53
|
+
}
|
|
54
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error(`Blocked protocol: ${u.protocol}`);
|
|
55
|
+
const host = u.hostname.toLowerCase();
|
|
56
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
57
|
+
throw new Error(`Blocked host: ${host}`);
|
|
58
|
+
}
|
|
59
|
+
if (/^[0-9.]+$/.test(host) || host.includes(":")) {
|
|
60
|
+
if (isPrivateIp(host)) throw new Error(`Blocked private address: ${host}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const addrs = await lookup(host, { all: true });
|
|
64
|
+
for (const a of addrs) if (isPrivateIp(a.address)) throw new Error(`Blocked private address for ${host}: ${a.address}`);
|
|
65
|
+
}
|
|
26
66
|
async function fetchText(url, timeoutMs = 1e4) {
|
|
27
67
|
const controller = new AbortController();
|
|
28
68
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
29
69
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
70
|
+
let current = url;
|
|
71
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
72
|
+
await assertPublicUrl(current);
|
|
73
|
+
const resp = await fetch(current, {
|
|
74
|
+
signal: controller.signal,
|
|
75
|
+
redirect: "manual",
|
|
76
|
+
headers: { "User-Agent": USER_AGENT }
|
|
77
|
+
});
|
|
78
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
79
|
+
const loc = resp.headers.get("location");
|
|
80
|
+
if (!loc) throw new Error(`HTTP ${resp.status} without Location`);
|
|
81
|
+
current = new URL(loc, current).toString();
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
85
|
+
return await resp.text();
|
|
36
86
|
}
|
|
37
|
-
|
|
87
|
+
throw new Error("Too many redirects");
|
|
38
88
|
} finally {
|
|
39
89
|
clearTimeout(timer);
|
|
40
90
|
}
|
|
@@ -1396,10 +1446,17 @@ function createServer() {
|
|
|
1396
1446
|
|
|
1397
1447
|
// src/mcp-http.ts
|
|
1398
1448
|
var PORT = Number(process.env.PORT) || 8787;
|
|
1449
|
+
var HOST = process.env.HOST || "0.0.0.0";
|
|
1450
|
+
var AUTH_TOKEN = process.env.COMPANY_DOSSIER_MCP_TOKEN || "";
|
|
1451
|
+
var ALLOWED_ORIGINS = (process.env.COMPANY_DOSSIER_MCP_ALLOWED_ORIGINS || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1399
1452
|
var transports = /* @__PURE__ */ new Map();
|
|
1400
1453
|
function applyCors(req, res) {
|
|
1401
1454
|
const origin = req.headers.origin;
|
|
1402
|
-
|
|
1455
|
+
if (ALLOWED_ORIGINS.length) {
|
|
1456
|
+
if (origin && ALLOWED_ORIGINS.includes(origin)) res.setHeader("Access-Control-Allow-Origin", origin);
|
|
1457
|
+
} else {
|
|
1458
|
+
res.setHeader("Access-Control-Allow-Origin", origin || "*");
|
|
1459
|
+
}
|
|
1403
1460
|
res.setHeader("Vary", "Origin");
|
|
1404
1461
|
res.setHeader(
|
|
1405
1462
|
"Access-Control-Allow-Methods",
|
|
@@ -1498,6 +1555,13 @@ var httpServer = createHttpServer((req, res) => {
|
|
|
1498
1555
|
return;
|
|
1499
1556
|
}
|
|
1500
1557
|
if (pathname === "/mcp") {
|
|
1558
|
+
if (AUTH_TOKEN) {
|
|
1559
|
+
const auth = req.headers.authorization || "";
|
|
1560
|
+
if (auth !== `Bearer ${AUTH_TOKEN}`) {
|
|
1561
|
+
sendJson(res, 401, { jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized" }, id: null });
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1501
1565
|
if (req.method === "POST") {
|
|
1502
1566
|
await handleMcpPost(req, res);
|
|
1503
1567
|
return;
|
|
@@ -1528,10 +1592,11 @@ var httpServer = createHttpServer((req, res) => {
|
|
|
1528
1592
|
}
|
|
1529
1593
|
})();
|
|
1530
1594
|
});
|
|
1531
|
-
httpServer.listen(PORT, () => {
|
|
1595
|
+
httpServer.listen(PORT, HOST, () => {
|
|
1532
1596
|
process.stdout.write(
|
|
1533
|
-
`company-dossier remote MCP server listening on http
|
|
1534
|
-
MCP endpoint: POST/GET/DELETE /mcp
|
|
1597
|
+
`company-dossier remote MCP server listening on http://${HOST}:${PORT}
|
|
1598
|
+
MCP endpoint: POST/GET/DELETE /mcp${AUTH_TOKEN ? " (bearer auth required)" : " (no auth \u2014 set COMPANY_DOSSIER_MCP_TOKEN)"}
|
|
1599
|
+
CORS: ${ALLOWED_ORIGINS.length ? ALLOWED_ORIGINS.join(", ") : "reflect (set COMPANY_DOSSIER_MCP_ALLOWED_ORIGINS to restrict)"}
|
|
1535
1600
|
Health check: GET /health
|
|
1536
1601
|
`
|
|
1537
1602
|
);
|
package/dist/mcp-server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { promises as dns } from "dns";
|
|
|
8
8
|
// src/utils.ts
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as path from "path";
|
|
11
|
+
import { lookup } from "dns/promises";
|
|
11
12
|
var USER_AGENT = "company-dossier/0.1 (+https://companydossier.lol)";
|
|
12
13
|
function todayISO() {
|
|
13
14
|
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -15,18 +16,67 @@ function todayISO() {
|
|
|
15
16
|
function titleCase(str) {
|
|
16
17
|
return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
17
18
|
}
|
|
19
|
+
function isPrivateIp(ip) {
|
|
20
|
+
const v = ip.replace(/^\[|\]$/g, "").toLowerCase();
|
|
21
|
+
if (v.includes(":")) {
|
|
22
|
+
if (v === "::1" || v === "::") return true;
|
|
23
|
+
if (v.startsWith("fe80") || v.startsWith("fc") || v.startsWith("fd")) return true;
|
|
24
|
+
const m = v.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
25
|
+
if (m) return isPrivateIp(m[1]);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const p = v.split(".").map(Number);
|
|
29
|
+
if (p.length !== 4 || p.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return true;
|
|
30
|
+
const [a, b] = p;
|
|
31
|
+
if (a === 10 || a === 127 || a === 0) return true;
|
|
32
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
33
|
+
if (a === 192 && b === 168) return true;
|
|
34
|
+
if (a === 169 && b === 254) return true;
|
|
35
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
36
|
+
if (a === 192 && b === 0) return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
async function assertPublicUrl(url) {
|
|
40
|
+
let u;
|
|
41
|
+
try {
|
|
42
|
+
u = new URL(url);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error("Invalid URL");
|
|
45
|
+
}
|
|
46
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error(`Blocked protocol: ${u.protocol}`);
|
|
47
|
+
const host = u.hostname.toLowerCase();
|
|
48
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
49
|
+
throw new Error(`Blocked host: ${host}`);
|
|
50
|
+
}
|
|
51
|
+
if (/^[0-9.]+$/.test(host) || host.includes(":")) {
|
|
52
|
+
if (isPrivateIp(host)) throw new Error(`Blocked private address: ${host}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const addrs = await lookup(host, { all: true });
|
|
56
|
+
for (const a of addrs) if (isPrivateIp(a.address)) throw new Error(`Blocked private address for ${host}: ${a.address}`);
|
|
57
|
+
}
|
|
18
58
|
async function fetchText(url, timeoutMs = 1e4) {
|
|
19
59
|
const controller = new AbortController();
|
|
20
60
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
21
61
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
62
|
+
let current = url;
|
|
63
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
64
|
+
await assertPublicUrl(current);
|
|
65
|
+
const resp = await fetch(current, {
|
|
66
|
+
signal: controller.signal,
|
|
67
|
+
redirect: "manual",
|
|
68
|
+
headers: { "User-Agent": USER_AGENT }
|
|
69
|
+
});
|
|
70
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
71
|
+
const loc = resp.headers.get("location");
|
|
72
|
+
if (!loc) throw new Error(`HTTP ${resp.status} without Location`);
|
|
73
|
+
current = new URL(loc, current).toString();
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
77
|
+
return await resp.text();
|
|
28
78
|
}
|
|
29
|
-
|
|
79
|
+
throw new Error("Too many redirects");
|
|
30
80
|
} finally {
|
|
31
81
|
clearTimeout(timer);
|
|
32
82
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -13,6 +13,7 @@ import { promises as dns } from "dns";
|
|
|
13
13
|
// src/utils.ts
|
|
14
14
|
import * as fs from "fs";
|
|
15
15
|
import * as path from "path";
|
|
16
|
+
import { lookup } from "dns/promises";
|
|
16
17
|
var USER_AGENT = "company-dossier/0.1 (+https://companydossier.lol)";
|
|
17
18
|
function todayISO() {
|
|
18
19
|
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -20,18 +21,67 @@ function todayISO() {
|
|
|
20
21
|
function titleCase(str) {
|
|
21
22
|
return str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
22
23
|
}
|
|
24
|
+
function isPrivateIp(ip) {
|
|
25
|
+
const v = ip.replace(/^\[|\]$/g, "").toLowerCase();
|
|
26
|
+
if (v.includes(":")) {
|
|
27
|
+
if (v === "::1" || v === "::") return true;
|
|
28
|
+
if (v.startsWith("fe80") || v.startsWith("fc") || v.startsWith("fd")) return true;
|
|
29
|
+
const m = v.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
30
|
+
if (m) return isPrivateIp(m[1]);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const p = v.split(".").map(Number);
|
|
34
|
+
if (p.length !== 4 || p.some((n) => Number.isNaN(n) || n < 0 || n > 255)) return true;
|
|
35
|
+
const [a, b] = p;
|
|
36
|
+
if (a === 10 || a === 127 || a === 0) return true;
|
|
37
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
38
|
+
if (a === 192 && b === 168) return true;
|
|
39
|
+
if (a === 169 && b === 254) return true;
|
|
40
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
41
|
+
if (a === 192 && b === 0) return true;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
async function assertPublicUrl(url) {
|
|
45
|
+
let u;
|
|
46
|
+
try {
|
|
47
|
+
u = new URL(url);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error("Invalid URL");
|
|
50
|
+
}
|
|
51
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error(`Blocked protocol: ${u.protocol}`);
|
|
52
|
+
const host = u.hostname.toLowerCase();
|
|
53
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".internal") || host.endsWith(".local")) {
|
|
54
|
+
throw new Error(`Blocked host: ${host}`);
|
|
55
|
+
}
|
|
56
|
+
if (/^[0-9.]+$/.test(host) || host.includes(":")) {
|
|
57
|
+
if (isPrivateIp(host)) throw new Error(`Blocked private address: ${host}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const addrs = await lookup(host, { all: true });
|
|
61
|
+
for (const a of addrs) if (isPrivateIp(a.address)) throw new Error(`Blocked private address for ${host}: ${a.address}`);
|
|
62
|
+
}
|
|
23
63
|
async function fetchText(url, timeoutMs = 1e4) {
|
|
24
64
|
const controller = new AbortController();
|
|
25
65
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
26
66
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
67
|
+
let current = url;
|
|
68
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
69
|
+
await assertPublicUrl(current);
|
|
70
|
+
const resp = await fetch(current, {
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
redirect: "manual",
|
|
73
|
+
headers: { "User-Agent": USER_AGENT }
|
|
74
|
+
});
|
|
75
|
+
if (resp.status >= 300 && resp.status < 400) {
|
|
76
|
+
const loc = resp.headers.get("location");
|
|
77
|
+
if (!loc) throw new Error(`HTTP ${resp.status} without Location`);
|
|
78
|
+
current = new URL(loc, current).toString();
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
82
|
+
return await resp.text();
|
|
33
83
|
}
|
|
34
|
-
|
|
84
|
+
throw new Error("Too many redirects");
|
|
35
85
|
} finally {
|
|
36
86
|
clearTimeout(timer);
|
|
37
87
|
}
|
package/package.json
CHANGED