distopia 0.3.0 → 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 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.3.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,181 @@ 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
33
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
+ }
34
142
  async function safeFetch(input, init) {
35
- return await fetch(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
+ };
36
209
  }
37
210
  //#endregion
38
211
  //#region ../../src/infrastructure/http/src/safeurl.ts
@@ -49,8 +222,27 @@ function safeUrl(strings, ...values) {
49
222
  //#region src/index.ts
50
223
  const DISTOPIA_VERSION = version;
51
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;
52
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;
53
244
  exports.oxfmtCfg = config;
54
245
  exports.oxlintCfg = config$1;
55
246
  exports.safeFetch = safeFetch;
247
+ exports.safeFetchForDiscord = safeFetchForDiscord;
56
248
  exports.safeUrl = safeUrl;
package/dist/index.d.cts CHANGED
@@ -24,6 +24,36 @@ 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
27
57
  //#region ../../src/infrastructure/http/src/safeurl.d.ts
28
58
  type Branded<T, Brand> = T & {
29
59
  readonly __brand: Brand;
@@ -32,9 +62,24 @@ type SafeUrl = Branded<string, "distopiaSafeUrl">;
32
62
  declare function safeUrl(strings: TemplateStringsArray, ...values: (string | number)[]): SafeUrl;
33
63
  //#endregion
34
64
  //#region ../../src/infrastructure/http/src/safefetch.d.ts
35
- declare function safeFetch(input: SafeUrl, init?: RequestInit): Promise<Response>;
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>;
36
81
  //#endregion
37
82
  //#region src/index.d.ts
38
83
  declare const DISTOPIA_VERSION: string;
39
84
  //#endregion
40
- export { type Branded, DISTOPIA_VERSION, type SafeUrl, config as oxfmtCfg, config$1 as oxlintCfg, safeFetch, safeUrl };
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,6 +24,36 @@ 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
27
57
  //#region ../../src/infrastructure/http/src/safeurl.d.ts
28
58
  type Branded<T, Brand> = T & {
29
59
  readonly __brand: Brand;
@@ -32,9 +62,24 @@ type SafeUrl = Branded<string, "distopiaSafeUrl">;
32
62
  declare function safeUrl(strings: TemplateStringsArray, ...values: (string | number)[]): SafeUrl;
33
63
  //#endregion
34
64
  //#region ../../src/infrastructure/http/src/safefetch.d.ts
35
- declare function safeFetch(input: SafeUrl, init?: RequestInit): Promise<Response>;
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>;
36
81
  //#endregion
37
82
  //#region src/index.d.ts
38
83
  declare const DISTOPIA_VERSION: string;
39
84
  //#endregion
40
- export { type Branded, DISTOPIA_VERSION, type SafeUrl, config as oxfmtCfg, config$1 as oxlintCfg, safeFetch, safeUrl };
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.3.0";
6
+ var version = "0.4.0";
6
7
  //#endregion
7
8
  //#region ../template/src/oxfmt.ts
8
9
  const config = defineConfig({
@@ -29,9 +30,181 @@ 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
32
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
+ }
33
141
  async function safeFetch(input, init) {
34
- return await fetch(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
+ };
35
208
  }
36
209
  //#endregion
37
210
  //#region ../../src/infrastructure/http/src/safeurl.ts
@@ -48,4 +221,4 @@ function safeUrl(strings, ...values) {
48
221
  //#region src/index.ts
49
222
  const DISTOPIA_VERSION = version;
50
223
  //#endregion
51
- export { DISTOPIA_VERSION, config as oxfmtCfg, config$1 as oxlintCfg, safeFetch, safeUrl };
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.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",