distopia 0.2.1 → 0.4.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.
- package/dist/index.cjs +211 -1
- package/dist/index.d.cts +56 -1
- package/dist/index.d.mts +56 -1
- package/dist/index.mjs +191 -2
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
let oxfmt = require("oxfmt");
|
|
4
4
|
let oxlint = require("oxlint");
|
|
5
|
+
let node_dns = require("node:dns");
|
|
5
6
|
//#region package.json
|
|
6
|
-
var version = "0.
|
|
7
|
+
var version = "0.4.0";
|
|
7
8
|
//#endregion
|
|
8
9
|
//#region ../template/src/oxfmt.ts
|
|
9
10
|
const config = (0, oxfmt.defineConfig)({
|
|
@@ -30,9 +31,218 @@ const config$1 = (0, oxlint.defineConfig)({
|
|
|
30
31
|
ignorePatterns: ["*.auto.ts", "dist/**"]
|
|
31
32
|
});
|
|
32
33
|
//#endregion
|
|
34
|
+
//#region ../../src/infrastructure/http/src/Error/BodySizeError.ts
|
|
35
|
+
var BodySizeError = class extends Error {};
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region ../../src/infrastructure/http/src/Error/HeaderError.ts
|
|
38
|
+
var HeaderError = class extends Error {};
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region ../../src/infrastructure/http/src/Error/InvalidDomainError.ts
|
|
41
|
+
var InvalidDomainError = class extends Error {};
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region ../../src/infrastructure/http/src/Error/LocalAddressError.ts
|
|
44
|
+
var LocalAddressError = class extends Error {};
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region ../../src/infrastructure/http/src/Error/RedirectError.ts
|
|
47
|
+
var RedirectError = class extends Error {};
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region ../../src/infrastructure/http/src/url.ts
|
|
50
|
+
function isLocalIPv4(host) {
|
|
51
|
+
const parts = host.split(".");
|
|
52
|
+
if (parts.length !== 4) return false;
|
|
53
|
+
const [aStr, bStr, cStr, dStr] = parts;
|
|
54
|
+
const a = Number(aStr);
|
|
55
|
+
const b = Number(bStr);
|
|
56
|
+
const c = Number(cStr);
|
|
57
|
+
const d = Number(dStr);
|
|
58
|
+
for (const v of [
|
|
59
|
+
a,
|
|
60
|
+
b,
|
|
61
|
+
c,
|
|
62
|
+
d
|
|
63
|
+
]) if (!Number.isInteger(v) || v < 0 || v > 255) return false;
|
|
64
|
+
return a === 0 || a === 127 || a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
|
|
65
|
+
}
|
|
66
|
+
function isLocalIPv6(addr) {
|
|
67
|
+
const h = addr.toLowerCase();
|
|
68
|
+
if (h === "::1" || h === "::") return true;
|
|
69
|
+
if (/^fe[89ab]/.test(h)) return true;
|
|
70
|
+
if (/^f[cd]/.test(h)) return true;
|
|
71
|
+
const rest = h.match(/^::ffff:(.+)$/)?.[1];
|
|
72
|
+
if (rest !== void 0) {
|
|
73
|
+
if (rest.includes(".")) return isLocalIPv4(rest);
|
|
74
|
+
const segs = rest.split(":");
|
|
75
|
+
if (segs.length === 2) {
|
|
76
|
+
const hi = parseInt(segs[0] ?? "", 16);
|
|
77
|
+
const lo = parseInt(segs[1] ?? "", 16);
|
|
78
|
+
if (Number.isInteger(hi) && Number.isInteger(lo)) return isLocalIPv4(`${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
async function isLocalUrl(url) {
|
|
84
|
+
let s = url.replace(/\\/g, "/");
|
|
85
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) s = "http://" + s;
|
|
86
|
+
const hostname = URL.parse(s)?.hostname.toLowerCase();
|
|
87
|
+
if (hostname === void 0) return false;
|
|
88
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) return true;
|
|
89
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) return isLocalIPv6(hostname.slice(1, -1));
|
|
90
|
+
if (/^\d/.test(hostname)) return isLocalIPv4(hostname);
|
|
91
|
+
return await isLocalHostname(hostname);
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region ../../src/infrastructure/http/src/dns.ts
|
|
95
|
+
async function isLocalHostname(hostname) {
|
|
96
|
+
const [v4, v6] = await Promise.all([node_dns.promises.resolve4(hostname).catch(() => []), node_dns.promises.resolve6(hostname).catch(() => [])]);
|
|
97
|
+
return v4.some((ip) => isLocalIPv4(ip)) || v6.some((ip) => isLocalIPv6(ip));
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region ../../src/infrastructure/http/src/redirect.ts
|
|
101
|
+
const DEFAULT_MAX_REDIRECT = 10;
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region ../../src/infrastructure/http/src/size.ts
|
|
104
|
+
const MAX_BYTES = 1024 * 1024;
|
|
105
|
+
async function isValidSize(response) {
|
|
106
|
+
const body = response.body;
|
|
107
|
+
if (body === null) return true;
|
|
108
|
+
const reader = body.getReader();
|
|
109
|
+
let received = 0;
|
|
110
|
+
let isOk = true;
|
|
111
|
+
while (true) {
|
|
112
|
+
const { done, value } = await reader.read();
|
|
113
|
+
if (done) break;
|
|
114
|
+
received += value.byteLength;
|
|
115
|
+
if (received > 1048576) {
|
|
116
|
+
await reader.cancel();
|
|
117
|
+
isOk = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return isOk;
|
|
121
|
+
}
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region ../../src/infrastructure/http/src/timeout.ts
|
|
124
|
+
const DEFAULT_TIMEOUT = 10 * 1e3;
|
|
125
|
+
const DISCORD_TIMEOUT = 30 * 1e3;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region ../../src/infrastructure/http/src/safefetch.ts
|
|
128
|
+
const DISCORD_DOMAINS = [
|
|
129
|
+
"discord.com",
|
|
130
|
+
"discordapp.com",
|
|
131
|
+
"discord.gg"
|
|
132
|
+
];
|
|
133
|
+
async function safeFetchForDiscord(input, init) {
|
|
134
|
+
if (await isLocalUrl(input)) return new LocalAddressError(`${input} is local address.`);
|
|
135
|
+
const hostname = new URL(input).hostname;
|
|
136
|
+
if (!DISCORD_DOMAINS.includes(hostname)) return new InvalidDomainError(`${hostname} is not discord domain.`);
|
|
137
|
+
return await fetch(input, {
|
|
138
|
+
...init,
|
|
139
|
+
signal: AbortSignal.timeout(DISCORD_TIMEOUT)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function safeFetch(input, init) {
|
|
143
|
+
let reqUrl = input;
|
|
144
|
+
let currentInit = init;
|
|
145
|
+
let redirectCount = 0;
|
|
146
|
+
let response;
|
|
147
|
+
while (true) {
|
|
148
|
+
if (await isLocalUrl(reqUrl)) return new LocalAddressError(`${reqUrl} is local address.`);
|
|
149
|
+
response = await fetch(reqUrl, {
|
|
150
|
+
...currentInit,
|
|
151
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT),
|
|
152
|
+
redirect: "manual"
|
|
153
|
+
});
|
|
154
|
+
if (!await isValidSize(response.clone())) return new BodySizeError("Body size Error");
|
|
155
|
+
const isRedirect = response.status >= 300 && response.status < 400 && response.status !== 304;
|
|
156
|
+
const location = response.headers.get("location");
|
|
157
|
+
if (isRedirect) {
|
|
158
|
+
if (location === null) return new HeaderError("location is not found.");
|
|
159
|
+
let url;
|
|
160
|
+
try {
|
|
161
|
+
url = new URL(location, reqUrl);
|
|
162
|
+
} catch {
|
|
163
|
+
return new HeaderError(`${location} is invalid.`);
|
|
164
|
+
}
|
|
165
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return new HeaderError(`${url.protocol} is not allowed.`);
|
|
166
|
+
if (new URL(reqUrl).origin !== url.origin) {
|
|
167
|
+
const headers = new Headers(currentInit?.headers);
|
|
168
|
+
headers.delete("authorization");
|
|
169
|
+
headers.delete("cookie");
|
|
170
|
+
currentInit = {
|
|
171
|
+
...currentInit,
|
|
172
|
+
headers
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
reqUrl = url.href;
|
|
176
|
+
redirectCount += 1;
|
|
177
|
+
} else return response;
|
|
178
|
+
if (redirectCount > 10) return new RedirectError(`Redirect count is over 10`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region ../../src/infrastructure/http/src/invite.ts
|
|
183
|
+
const DISCORD_INVITE_LINK_START = [
|
|
184
|
+
"https://discord.com/invite/",
|
|
185
|
+
"https://ptb.discord.com/invite/",
|
|
186
|
+
"https://canary.discord.com/invite/"
|
|
187
|
+
];
|
|
188
|
+
async function isUsedCf(res) {
|
|
189
|
+
try {
|
|
190
|
+
const response = res.clone();
|
|
191
|
+
if (response.status !== 403) return false;
|
|
192
|
+
const { JSDOM } = await import("jsdom");
|
|
193
|
+
return new JSDOM(await response.text()).window.document.querySelector("title")?.textContent === "Just a moment...";
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function isInviteLink(url) {
|
|
199
|
+
const response = await safeFetch(url, {
|
|
200
|
+
method: "GET",
|
|
201
|
+
headers: { "User-Agent": "Mozilla/5.0" }
|
|
202
|
+
});
|
|
203
|
+
if (response instanceof Error) return response;
|
|
204
|
+
const resUrl = response.url;
|
|
205
|
+
return {
|
|
206
|
+
content: DISCORD_INVITE_LINK_START.some((value) => resUrl.startsWith(value)),
|
|
207
|
+
isUsedCf: await isUsedCf(response)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region ../../src/infrastructure/http/src/safeurl.ts
|
|
212
|
+
function safeUrl(strings, ...values) {
|
|
213
|
+
let result = "";
|
|
214
|
+
for (const [index, str] of strings.entries()) {
|
|
215
|
+
result += str;
|
|
216
|
+
const value = values[index];
|
|
217
|
+
if (value !== void 0) result += encodeURIComponent(value);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
33
222
|
//#region src/index.ts
|
|
34
223
|
const DISTOPIA_VERSION = version;
|
|
35
224
|
//#endregion
|
|
225
|
+
exports.BodySizeError = BodySizeError;
|
|
226
|
+
exports.DEFAULT_MAX_REDIRECT = DEFAULT_MAX_REDIRECT;
|
|
227
|
+
exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
|
|
228
|
+
exports.DISCORD_DOMAINS = DISCORD_DOMAINS;
|
|
229
|
+
exports.DISCORD_INVITE_LINK_START = DISCORD_INVITE_LINK_START;
|
|
230
|
+
exports.DISCORD_TIMEOUT = DISCORD_TIMEOUT;
|
|
36
231
|
exports.DISTOPIA_VERSION = DISTOPIA_VERSION;
|
|
232
|
+
exports.HeaderError = HeaderError;
|
|
233
|
+
exports.InvalidDomainError = InvalidDomainError;
|
|
234
|
+
exports.LocalAddressError = LocalAddressError;
|
|
235
|
+
exports.MAX_BYTES = MAX_BYTES;
|
|
236
|
+
exports.RedirectError = RedirectError;
|
|
237
|
+
exports.isInviteLink = isInviteLink;
|
|
238
|
+
exports.isLocalHostname = isLocalHostname;
|
|
239
|
+
exports.isLocalIPv4 = isLocalIPv4;
|
|
240
|
+
exports.isLocalIPv6 = isLocalIPv6;
|
|
241
|
+
exports.isLocalUrl = isLocalUrl;
|
|
242
|
+
exports.isUsedCf = isUsedCf;
|
|
243
|
+
exports.isValidSize = isValidSize;
|
|
37
244
|
exports.oxfmtCfg = config;
|
|
38
245
|
exports.oxlintCfg = config$1;
|
|
246
|
+
exports.safeFetch = safeFetch;
|
|
247
|
+
exports.safeFetchForDiscord = safeFetchForDiscord;
|
|
248
|
+
exports.safeUrl = safeUrl;
|
package/dist/index.d.cts
CHANGED
|
@@ -24,7 +24,62 @@ declare const config$1: {
|
|
|
24
24
|
ignorePatterns: string[];
|
|
25
25
|
};
|
|
26
26
|
//#endregion
|
|
27
|
+
//#region ../../src/infrastructure/http/src/Error/BodySizeError.d.ts
|
|
28
|
+
declare class BodySizeError extends Error {}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region ../../src/infrastructure/http/src/Error/HeaderError.d.ts
|
|
31
|
+
declare class HeaderError extends Error {}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region ../../src/infrastructure/http/src/Error/InvalidDomainError.d.ts
|
|
34
|
+
declare class InvalidDomainError extends Error {}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region ../../src/infrastructure/http/src/Error/LocalAddressError.d.ts
|
|
37
|
+
declare class LocalAddressError extends Error {}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region ../../src/infrastructure/http/src/Error/RedirectError.d.ts
|
|
40
|
+
declare class RedirectError extends Error {}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region ../../src/infrastructure/http/src/dns.d.ts
|
|
43
|
+
declare function isLocalHostname(hostname: string): Promise<boolean>;
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region ../../src/infrastructure/http/src/invite.d.ts
|
|
46
|
+
type IsInviteLink = {
|
|
47
|
+
content: boolean;
|
|
48
|
+
isUsedCf: boolean;
|
|
49
|
+
};
|
|
50
|
+
declare const DISCORD_INVITE_LINK_START: string[];
|
|
51
|
+
declare function isUsedCf(res: Response): Promise<boolean>;
|
|
52
|
+
declare function isInviteLink(url: string): Promise<IsInviteLink | LocalAddressError | HeaderError | RedirectError | BodySizeError>;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region ../../src/infrastructure/http/src/redirect.d.ts
|
|
55
|
+
declare const DEFAULT_MAX_REDIRECT = 10;
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region ../../src/infrastructure/http/src/safeurl.d.ts
|
|
58
|
+
type Branded<T, Brand> = T & {
|
|
59
|
+
readonly __brand: Brand;
|
|
60
|
+
};
|
|
61
|
+
type SafeUrl = Branded<string, "distopiaSafeUrl">;
|
|
62
|
+
declare function safeUrl(strings: TemplateStringsArray, ...values: (string | number)[]): SafeUrl;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region ../../src/infrastructure/http/src/safefetch.d.ts
|
|
65
|
+
declare const DISCORD_DOMAINS: string[];
|
|
66
|
+
declare function safeFetchForDiscord(input: SafeUrl, init?: RequestInit): Promise<Response | LocalAddressError | InvalidDomainError>;
|
|
67
|
+
declare function safeFetch(input: SafeUrl, init?: RequestInit): Promise<Response | LocalAddressError | HeaderError | RedirectError | BodySizeError>;
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region ../../src/infrastructure/http/src/size.d.ts
|
|
70
|
+
declare const MAX_BYTES: number;
|
|
71
|
+
declare function isValidSize(response: Response): Promise<boolean>;
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region ../../src/infrastructure/http/src/timeout.d.ts
|
|
74
|
+
declare const DEFAULT_TIMEOUT: number;
|
|
75
|
+
declare const DISCORD_TIMEOUT: number;
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region ../../src/infrastructure/http/src/url.d.ts
|
|
78
|
+
declare function isLocalIPv4(host: string): boolean;
|
|
79
|
+
declare function isLocalIPv6(addr: string): boolean;
|
|
80
|
+
declare function isLocalUrl(url: string): Promise<boolean>;
|
|
81
|
+
//#endregion
|
|
27
82
|
//#region src/index.d.ts
|
|
28
83
|
declare const DISTOPIA_VERSION: string;
|
|
29
84
|
//#endregion
|
|
30
|
-
export { DISTOPIA_VERSION, config as oxfmtCfg, config$1 as oxlintCfg };
|
|
85
|
+
export { BodySizeError, Branded, DEFAULT_MAX_REDIRECT, DEFAULT_TIMEOUT, DISCORD_DOMAINS, DISCORD_INVITE_LINK_START, DISCORD_TIMEOUT, DISTOPIA_VERSION, HeaderError, InvalidDomainError, IsInviteLink, LocalAddressError, MAX_BYTES, RedirectError, SafeUrl, isInviteLink, isLocalHostname, isLocalIPv4, isLocalIPv6, isLocalUrl, isUsedCf, isValidSize, config as oxfmtCfg, config$1 as oxlintCfg, safeFetch, safeFetchForDiscord, safeUrl };
|
package/dist/index.d.mts
CHANGED
|
@@ -24,7 +24,62 @@ declare const config$1: {
|
|
|
24
24
|
ignorePatterns: string[];
|
|
25
25
|
};
|
|
26
26
|
//#endregion
|
|
27
|
+
//#region ../../src/infrastructure/http/src/Error/BodySizeError.d.ts
|
|
28
|
+
declare class BodySizeError extends Error {}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region ../../src/infrastructure/http/src/Error/HeaderError.d.ts
|
|
31
|
+
declare class HeaderError extends Error {}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region ../../src/infrastructure/http/src/Error/InvalidDomainError.d.ts
|
|
34
|
+
declare class InvalidDomainError extends Error {}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region ../../src/infrastructure/http/src/Error/LocalAddressError.d.ts
|
|
37
|
+
declare class LocalAddressError extends Error {}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region ../../src/infrastructure/http/src/Error/RedirectError.d.ts
|
|
40
|
+
declare class RedirectError extends Error {}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region ../../src/infrastructure/http/src/dns.d.ts
|
|
43
|
+
declare function isLocalHostname(hostname: string): Promise<boolean>;
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region ../../src/infrastructure/http/src/invite.d.ts
|
|
46
|
+
type IsInviteLink = {
|
|
47
|
+
content: boolean;
|
|
48
|
+
isUsedCf: boolean;
|
|
49
|
+
};
|
|
50
|
+
declare const DISCORD_INVITE_LINK_START: string[];
|
|
51
|
+
declare function isUsedCf(res: Response): Promise<boolean>;
|
|
52
|
+
declare function isInviteLink(url: string): Promise<IsInviteLink | LocalAddressError | HeaderError | RedirectError | BodySizeError>;
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region ../../src/infrastructure/http/src/redirect.d.ts
|
|
55
|
+
declare const DEFAULT_MAX_REDIRECT = 10;
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region ../../src/infrastructure/http/src/safeurl.d.ts
|
|
58
|
+
type Branded<T, Brand> = T & {
|
|
59
|
+
readonly __brand: Brand;
|
|
60
|
+
};
|
|
61
|
+
type SafeUrl = Branded<string, "distopiaSafeUrl">;
|
|
62
|
+
declare function safeUrl(strings: TemplateStringsArray, ...values: (string | number)[]): SafeUrl;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region ../../src/infrastructure/http/src/safefetch.d.ts
|
|
65
|
+
declare const DISCORD_DOMAINS: string[];
|
|
66
|
+
declare function safeFetchForDiscord(input: SafeUrl, init?: RequestInit): Promise<Response | LocalAddressError | InvalidDomainError>;
|
|
67
|
+
declare function safeFetch(input: SafeUrl, init?: RequestInit): Promise<Response | LocalAddressError | HeaderError | RedirectError | BodySizeError>;
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region ../../src/infrastructure/http/src/size.d.ts
|
|
70
|
+
declare const MAX_BYTES: number;
|
|
71
|
+
declare function isValidSize(response: Response): Promise<boolean>;
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region ../../src/infrastructure/http/src/timeout.d.ts
|
|
74
|
+
declare const DEFAULT_TIMEOUT: number;
|
|
75
|
+
declare const DISCORD_TIMEOUT: number;
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region ../../src/infrastructure/http/src/url.d.ts
|
|
78
|
+
declare function isLocalIPv4(host: string): boolean;
|
|
79
|
+
declare function isLocalIPv6(addr: string): boolean;
|
|
80
|
+
declare function isLocalUrl(url: string): Promise<boolean>;
|
|
81
|
+
//#endregion
|
|
27
82
|
//#region src/index.d.ts
|
|
28
83
|
declare const DISTOPIA_VERSION: string;
|
|
29
84
|
//#endregion
|
|
30
|
-
export { DISTOPIA_VERSION, config as oxfmtCfg, config$1 as oxlintCfg };
|
|
85
|
+
export { BodySizeError, Branded, DEFAULT_MAX_REDIRECT, DEFAULT_TIMEOUT, DISCORD_DOMAINS, DISCORD_INVITE_LINK_START, DISCORD_TIMEOUT, DISTOPIA_VERSION, HeaderError, InvalidDomainError, IsInviteLink, LocalAddressError, MAX_BYTES, RedirectError, SafeUrl, isInviteLink, isLocalHostname, isLocalIPv4, isLocalIPv6, isLocalUrl, isUsedCf, isValidSize, config as oxfmtCfg, config$1 as oxlintCfg, safeFetch, safeFetchForDiscord, safeUrl };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/* @ts-self-types="./index.d.mts" */
|
|
2
2
|
import { defineConfig } from "oxfmt";
|
|
3
3
|
import { defineConfig as defineConfig$1 } from "oxlint";
|
|
4
|
+
import { promises } from "node:dns";
|
|
4
5
|
//#region package.json
|
|
5
|
-
var version = "0.
|
|
6
|
+
var version = "0.4.0";
|
|
6
7
|
//#endregion
|
|
7
8
|
//#region ../template/src/oxfmt.ts
|
|
8
9
|
const config = defineConfig({
|
|
@@ -29,7 +30,195 @@ const config$1 = defineConfig$1({
|
|
|
29
30
|
ignorePatterns: ["*.auto.ts", "dist/**"]
|
|
30
31
|
});
|
|
31
32
|
//#endregion
|
|
33
|
+
//#region ../../src/infrastructure/http/src/Error/BodySizeError.ts
|
|
34
|
+
var BodySizeError = class extends Error {};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region ../../src/infrastructure/http/src/Error/HeaderError.ts
|
|
37
|
+
var HeaderError = class extends Error {};
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region ../../src/infrastructure/http/src/Error/InvalidDomainError.ts
|
|
40
|
+
var InvalidDomainError = class extends Error {};
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region ../../src/infrastructure/http/src/Error/LocalAddressError.ts
|
|
43
|
+
var LocalAddressError = class extends Error {};
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region ../../src/infrastructure/http/src/Error/RedirectError.ts
|
|
46
|
+
var RedirectError = class extends Error {};
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region ../../src/infrastructure/http/src/url.ts
|
|
49
|
+
function isLocalIPv4(host) {
|
|
50
|
+
const parts = host.split(".");
|
|
51
|
+
if (parts.length !== 4) return false;
|
|
52
|
+
const [aStr, bStr, cStr, dStr] = parts;
|
|
53
|
+
const a = Number(aStr);
|
|
54
|
+
const b = Number(bStr);
|
|
55
|
+
const c = Number(cStr);
|
|
56
|
+
const d = Number(dStr);
|
|
57
|
+
for (const v of [
|
|
58
|
+
a,
|
|
59
|
+
b,
|
|
60
|
+
c,
|
|
61
|
+
d
|
|
62
|
+
]) if (!Number.isInteger(v) || v < 0 || v > 255) return false;
|
|
63
|
+
return a === 0 || a === 127 || a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
|
|
64
|
+
}
|
|
65
|
+
function isLocalIPv6(addr) {
|
|
66
|
+
const h = addr.toLowerCase();
|
|
67
|
+
if (h === "::1" || h === "::") return true;
|
|
68
|
+
if (/^fe[89ab]/.test(h)) return true;
|
|
69
|
+
if (/^f[cd]/.test(h)) return true;
|
|
70
|
+
const rest = h.match(/^::ffff:(.+)$/)?.[1];
|
|
71
|
+
if (rest !== void 0) {
|
|
72
|
+
if (rest.includes(".")) return isLocalIPv4(rest);
|
|
73
|
+
const segs = rest.split(":");
|
|
74
|
+
if (segs.length === 2) {
|
|
75
|
+
const hi = parseInt(segs[0] ?? "", 16);
|
|
76
|
+
const lo = parseInt(segs[1] ?? "", 16);
|
|
77
|
+
if (Number.isInteger(hi) && Number.isInteger(lo)) return isLocalIPv4(`${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
async function isLocalUrl(url) {
|
|
83
|
+
let s = url.replace(/\\/g, "/");
|
|
84
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(s)) s = "http://" + s;
|
|
85
|
+
const hostname = URL.parse(s)?.hostname.toLowerCase();
|
|
86
|
+
if (hostname === void 0) return false;
|
|
87
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) return true;
|
|
88
|
+
if (hostname.startsWith("[") && hostname.endsWith("]")) return isLocalIPv6(hostname.slice(1, -1));
|
|
89
|
+
if (/^\d/.test(hostname)) return isLocalIPv4(hostname);
|
|
90
|
+
return await isLocalHostname(hostname);
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region ../../src/infrastructure/http/src/dns.ts
|
|
94
|
+
async function isLocalHostname(hostname) {
|
|
95
|
+
const [v4, v6] = await Promise.all([promises.resolve4(hostname).catch(() => []), promises.resolve6(hostname).catch(() => [])]);
|
|
96
|
+
return v4.some((ip) => isLocalIPv4(ip)) || v6.some((ip) => isLocalIPv6(ip));
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region ../../src/infrastructure/http/src/redirect.ts
|
|
100
|
+
const DEFAULT_MAX_REDIRECT = 10;
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region ../../src/infrastructure/http/src/size.ts
|
|
103
|
+
const MAX_BYTES = 1024 * 1024;
|
|
104
|
+
async function isValidSize(response) {
|
|
105
|
+
const body = response.body;
|
|
106
|
+
if (body === null) return true;
|
|
107
|
+
const reader = body.getReader();
|
|
108
|
+
let received = 0;
|
|
109
|
+
let isOk = true;
|
|
110
|
+
while (true) {
|
|
111
|
+
const { done, value } = await reader.read();
|
|
112
|
+
if (done) break;
|
|
113
|
+
received += value.byteLength;
|
|
114
|
+
if (received > 1048576) {
|
|
115
|
+
await reader.cancel();
|
|
116
|
+
isOk = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return isOk;
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region ../../src/infrastructure/http/src/timeout.ts
|
|
123
|
+
const DEFAULT_TIMEOUT = 10 * 1e3;
|
|
124
|
+
const DISCORD_TIMEOUT = 30 * 1e3;
|
|
125
|
+
//#endregion
|
|
126
|
+
//#region ../../src/infrastructure/http/src/safefetch.ts
|
|
127
|
+
const DISCORD_DOMAINS = [
|
|
128
|
+
"discord.com",
|
|
129
|
+
"discordapp.com",
|
|
130
|
+
"discord.gg"
|
|
131
|
+
];
|
|
132
|
+
async function safeFetchForDiscord(input, init) {
|
|
133
|
+
if (await isLocalUrl(input)) return new LocalAddressError(`${input} is local address.`);
|
|
134
|
+
const hostname = new URL(input).hostname;
|
|
135
|
+
if (!DISCORD_DOMAINS.includes(hostname)) return new InvalidDomainError(`${hostname} is not discord domain.`);
|
|
136
|
+
return await fetch(input, {
|
|
137
|
+
...init,
|
|
138
|
+
signal: AbortSignal.timeout(DISCORD_TIMEOUT)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async function safeFetch(input, init) {
|
|
142
|
+
let reqUrl = input;
|
|
143
|
+
let currentInit = init;
|
|
144
|
+
let redirectCount = 0;
|
|
145
|
+
let response;
|
|
146
|
+
while (true) {
|
|
147
|
+
if (await isLocalUrl(reqUrl)) return new LocalAddressError(`${reqUrl} is local address.`);
|
|
148
|
+
response = await fetch(reqUrl, {
|
|
149
|
+
...currentInit,
|
|
150
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT),
|
|
151
|
+
redirect: "manual"
|
|
152
|
+
});
|
|
153
|
+
if (!await isValidSize(response.clone())) return new BodySizeError("Body size Error");
|
|
154
|
+
const isRedirect = response.status >= 300 && response.status < 400 && response.status !== 304;
|
|
155
|
+
const location = response.headers.get("location");
|
|
156
|
+
if (isRedirect) {
|
|
157
|
+
if (location === null) return new HeaderError("location is not found.");
|
|
158
|
+
let url;
|
|
159
|
+
try {
|
|
160
|
+
url = new URL(location, reqUrl);
|
|
161
|
+
} catch {
|
|
162
|
+
return new HeaderError(`${location} is invalid.`);
|
|
163
|
+
}
|
|
164
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return new HeaderError(`${url.protocol} is not allowed.`);
|
|
165
|
+
if (new URL(reqUrl).origin !== url.origin) {
|
|
166
|
+
const headers = new Headers(currentInit?.headers);
|
|
167
|
+
headers.delete("authorization");
|
|
168
|
+
headers.delete("cookie");
|
|
169
|
+
currentInit = {
|
|
170
|
+
...currentInit,
|
|
171
|
+
headers
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
reqUrl = url.href;
|
|
175
|
+
redirectCount += 1;
|
|
176
|
+
} else return response;
|
|
177
|
+
if (redirectCount > 10) return new RedirectError(`Redirect count is over 10`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region ../../src/infrastructure/http/src/invite.ts
|
|
182
|
+
const DISCORD_INVITE_LINK_START = [
|
|
183
|
+
"https://discord.com/invite/",
|
|
184
|
+
"https://ptb.discord.com/invite/",
|
|
185
|
+
"https://canary.discord.com/invite/"
|
|
186
|
+
];
|
|
187
|
+
async function isUsedCf(res) {
|
|
188
|
+
try {
|
|
189
|
+
const response = res.clone();
|
|
190
|
+
if (response.status !== 403) return false;
|
|
191
|
+
const { JSDOM } = await import("jsdom");
|
|
192
|
+
return new JSDOM(await response.text()).window.document.querySelector("title")?.textContent === "Just a moment...";
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function isInviteLink(url) {
|
|
198
|
+
const response = await safeFetch(url, {
|
|
199
|
+
method: "GET",
|
|
200
|
+
headers: { "User-Agent": "Mozilla/5.0" }
|
|
201
|
+
});
|
|
202
|
+
if (response instanceof Error) return response;
|
|
203
|
+
const resUrl = response.url;
|
|
204
|
+
return {
|
|
205
|
+
content: DISCORD_INVITE_LINK_START.some((value) => resUrl.startsWith(value)),
|
|
206
|
+
isUsedCf: await isUsedCf(response)
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region ../../src/infrastructure/http/src/safeurl.ts
|
|
211
|
+
function safeUrl(strings, ...values) {
|
|
212
|
+
let result = "";
|
|
213
|
+
for (const [index, str] of strings.entries()) {
|
|
214
|
+
result += str;
|
|
215
|
+
const value = values[index];
|
|
216
|
+
if (value !== void 0) result += encodeURIComponent(value);
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
32
221
|
//#region src/index.ts
|
|
33
222
|
const DISTOPIA_VERSION = version;
|
|
34
223
|
//#endregion
|
|
35
|
-
export { DISTOPIA_VERSION, config as oxfmtCfg, config$1 as oxlintCfg };
|
|
224
|
+
export { BodySizeError, DEFAULT_MAX_REDIRECT, DEFAULT_TIMEOUT, DISCORD_DOMAINS, DISCORD_INVITE_LINK_START, DISCORD_TIMEOUT, DISTOPIA_VERSION, HeaderError, InvalidDomainError, LocalAddressError, MAX_BYTES, RedirectError, isInviteLink, isLocalHostname, isLocalIPv4, isLocalIPv6, isLocalUrl, isUsedCf, isValidSize, config as oxfmtCfg, config$1 as oxlintCfg, safeFetch, safeFetchForDiscord, safeUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "distopia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Library for Distopia.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"distopia",
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"prepublishOnly": "bun run build"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"@types/jsdom": "28.0.3",
|
|
41
42
|
"@types/node": "24.13.2",
|
|
43
|
+
"jsdom": "29.1.1",
|
|
42
44
|
"jsr": "0.14.3",
|
|
43
45
|
"oxfmt": "0.54.0",
|
|
44
46
|
"oxlint": "1.60.0",
|