xpandurl 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.
package/dist/index.cjs ADDED
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ ExpandError: () => ExpandError,
34
+ InvalidUrlError: () => InvalidUrlError,
35
+ MaxRedirectsError: () => MaxRedirectsError,
36
+ TimeoutError: () => TimeoutError,
37
+ expand: () => expand,
38
+ expandMany: () => expandMany
39
+ });
40
+ module.exports = __toCommonJS(src_exports);
41
+
42
+ // src/expand.ts
43
+ var import_node_http = __toESM(require("http"), 1);
44
+ var import_node_https = __toESM(require("https"), 1);
45
+
46
+ // src/errors.ts
47
+ var ExpandError = class extends Error {
48
+ constructor(message, url) {
49
+ super(message);
50
+ this.url = url;
51
+ this.name = "ExpandError";
52
+ Object.setPrototypeOf(this, new.target.prototype);
53
+ }
54
+ url;
55
+ };
56
+ var TimeoutError = class extends ExpandError {
57
+ constructor(url, timeoutMs) {
58
+ super(`Request timed out after ${timeoutMs}ms`, url);
59
+ this.name = "TimeoutError";
60
+ }
61
+ };
62
+ var MaxRedirectsError = class extends ExpandError {
63
+ constructor(url, max) {
64
+ super(`Exceeded maximum of ${max} redirects`, url);
65
+ this.name = "MaxRedirectsError";
66
+ }
67
+ };
68
+ var InvalidUrlError = class extends ExpandError {
69
+ constructor(url) {
70
+ super(`Invalid or unsupported URL: ${url}`, url);
71
+ this.name = "InvalidUrlError";
72
+ }
73
+ };
74
+
75
+ // src/expand.ts
76
+ var DEFAULT_TIMEOUT = 1e4;
77
+ var DEFAULT_MAX_REDIRECTS = 10;
78
+ var DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
79
+ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
80
+ "localhost",
81
+ "metadata.google.internal"
82
+ // GCP metadata
83
+ ]);
84
+ function isPrivateHost(hostname) {
85
+ const h = hostname.toLowerCase();
86
+ if (BLOCKED_HOSTNAMES.has(h)) return true;
87
+ if (h === "::1" || h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd")) {
88
+ return true;
89
+ }
90
+ const parts = h.split(".");
91
+ if (parts.length !== 4) return false;
92
+ const octets = parts.map(Number);
93
+ if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return false;
94
+ const [a, b] = octets;
95
+ return a === 0 || // 0.0.0.0/8 unspecified
96
+ a === 10 || // 10.0.0.0/8 RFC1918
97
+ a === 127 || // 127.0.0.0/8 loopback
98
+ a === 169 && b === 254 || // 169.254.0.0/16 link-local + cloud metadata
99
+ a === 172 && b >= 16 && b <= 31 || // 172.16.0.0/12 RFC1918
100
+ a === 192 && b === 168 || // 192.168.0.0/16 RFC1918
101
+ a === 255;
102
+ }
103
+ function makeRequest(url, options) {
104
+ return new Promise((resolve, reject) => {
105
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
106
+ const lib = url.startsWith("https") ? import_node_https.default : import_node_http.default;
107
+ const req = lib.get(
108
+ url,
109
+ {
110
+ headers: {
111
+ "User-Agent": options.userAgent ?? DEFAULT_UA,
112
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
113
+ ...options.headers
114
+ }
115
+ },
116
+ (res) => {
117
+ const result = {
118
+ statusCode: res.statusCode ?? 0,
119
+ location: Array.isArray(res.headers.location) ? res.headers.location[0] : res.headers.location
120
+ };
121
+ resolve(result);
122
+ res.destroy();
123
+ }
124
+ );
125
+ req.setTimeout(timeout, () => {
126
+ req.destroy(new TimeoutError(url, timeout));
127
+ });
128
+ req.on("error", (err) => {
129
+ if (err instanceof TimeoutError) {
130
+ reject(err);
131
+ return;
132
+ }
133
+ reject(new ExpandError(err.message, url));
134
+ });
135
+ });
136
+ }
137
+ function assertHttpProtocol(url) {
138
+ let protocol;
139
+ try {
140
+ protocol = new URL(url).protocol;
141
+ } catch {
142
+ throw new InvalidUrlError(url);
143
+ }
144
+ if (protocol !== "http:" && protocol !== "https:") {
145
+ throw new InvalidUrlError(url);
146
+ }
147
+ }
148
+ function assertHostPolicy(url, options) {
149
+ const { hostname } = new URL(url);
150
+ if (options.blockPrivateIPs !== false && isPrivateHost(hostname)) {
151
+ throw new ExpandError(
152
+ `Requests to private/local hosts are blocked ("${hostname}"). Set blockPrivateIPs: false to allow.`,
153
+ url
154
+ );
155
+ }
156
+ if (options.allowedHosts && options.allowedHosts.length > 0) {
157
+ if (!options.allowedHosts.includes(hostname)) {
158
+ throw new ExpandError(
159
+ `"${hostname}" is not in the allowedHosts list`,
160
+ url
161
+ );
162
+ }
163
+ }
164
+ }
165
+ function assertRedirectPolicy(from, to, options) {
166
+ if (options.allowHttpDowngrade === false) {
167
+ const fromProto = new URL(from).protocol;
168
+ const toProto = new URL(to).protocol;
169
+ if (fromProto === "https:" && toProto === "http:") {
170
+ throw new ExpandError(
171
+ `HTTPS to HTTP downgrade blocked (set allowHttpDowngrade: true to permit)`,
172
+ from
173
+ );
174
+ }
175
+ }
176
+ assertHostPolicy(to, options);
177
+ }
178
+ async function expand(url, options = {}) {
179
+ assertHttpProtocol(url);
180
+ assertHostPolicy(url, options);
181
+ const max = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
182
+ const chain = [];
183
+ let current = url;
184
+ for (let i = 0; i <= max; i++) {
185
+ const { statusCode, location } = await makeRequest(current, options);
186
+ const isRedirect = statusCode >= 300 && statusCode < 400;
187
+ if (isRedirect && location) {
188
+ chain.push(current);
189
+ const next = new URL(location, current).href;
190
+ assertHttpProtocol(next);
191
+ assertRedirectPolicy(current, next, options);
192
+ current = next;
193
+ continue;
194
+ }
195
+ return {
196
+ originalUrl: url,
197
+ expandedUrl: current,
198
+ statusCode,
199
+ redirectChain: chain
200
+ };
201
+ }
202
+ throw new MaxRedirectsError(url, max);
203
+ }
204
+
205
+ // src/expand-many.ts
206
+ var DEFAULT_CONCURRENCY = 5;
207
+ async function expandMany(urls, options = {}) {
208
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
209
+ const results = [];
210
+ for (let i = 0; i < urls.length; i += concurrency) {
211
+ const batch = urls.slice(i, i + concurrency);
212
+ const settled = await Promise.allSettled(batch.map((url) => expand(url, options)));
213
+ for (let j = 0; j < settled.length; j++) {
214
+ const outcome = settled[j];
215
+ if (outcome.status === "fulfilled") {
216
+ results.push(outcome.value);
217
+ } else {
218
+ results.push({
219
+ originalUrl: urls[i + j],
220
+ expandedUrl: urls[i + j],
221
+ statusCode: 0,
222
+ redirectChain: [],
223
+ error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason)
224
+ });
225
+ }
226
+ }
227
+ }
228
+ return results;
229
+ }
230
+ // Annotate the CommonJS export names for ESM import in node:
231
+ 0 && (module.exports = {
232
+ ExpandError,
233
+ InvalidUrlError,
234
+ MaxRedirectsError,
235
+ TimeoutError,
236
+ expand,
237
+ expandMany
238
+ });
239
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/expand.ts","../src/errors.ts","../src/expand-many.ts"],"sourcesContent":["export { expand } from './expand.js'\nexport { expandMany } from './expand-many.js'\nexport type { ExpandOptions, ExpandManyOptions, ExpandResult } from './types.js'\nexport { ExpandError, TimeoutError, MaxRedirectsError, InvalidUrlError } from './errors.js'\n","import http from 'node:http'\nimport https from 'node:https'\nimport { ExpandError, InvalidUrlError, MaxRedirectsError, TimeoutError } from './errors.js'\nimport type { ExpandOptions, ExpandResult } from './types.js'\n\nconst DEFAULT_TIMEOUT = 10_000\nconst DEFAULT_MAX_REDIRECTS = 10\nconst DEFAULT_UA =\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'\n\n// ─── Private / local IP detection ─────────────────────────────────────────────\n\nconst BLOCKED_HOSTNAMES = new Set([\n 'localhost',\n 'metadata.google.internal', // GCP metadata\n])\n\nfunction isPrivateHost(hostname: string): boolean {\n const h = hostname.toLowerCase()\n\n if (BLOCKED_HOSTNAMES.has(h)) return true\n\n // IPv6 loopback, link-local, ULA (fc00::/7)\n if (h === '::1' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) {\n return true\n }\n\n // IPv4: must be exactly 4 dot-separated octets\n const parts = h.split('.')\n if (parts.length !== 4) return false\n const octets = parts.map(Number)\n if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return false\n\n const [a, b] = octets as [number, number, number, number]\n return (\n a === 0 || // 0.0.0.0/8 unspecified\n a === 10 || // 10.0.0.0/8 RFC1918\n a === 127 || // 127.0.0.0/8 loopback\n (a === 169 && b === 254) || // 169.254.0.0/16 link-local + cloud metadata\n (a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12 RFC1918\n (a === 192 && b === 168) || // 192.168.0.0/16 RFC1918\n a === 255 // 255.x.x.x broadcast\n )\n}\n\n// ─── Request (headers only — body is never read) ───────────────────────────────\n\ninterface RequestResult {\n statusCode: number\n location: string | undefined\n}\n\nfunction makeRequest(url: string, options: ExpandOptions): Promise<RequestResult> {\n return new Promise((resolve, reject) => {\n const timeout = options.timeout ?? DEFAULT_TIMEOUT\n const lib = url.startsWith('https') ? https : http\n\n const req = lib.get(\n url,\n {\n headers: {\n 'User-Agent': options.userAgent ?? DEFAULT_UA,\n Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n ...options.headers,\n },\n },\n (res) => {\n // Capture headers then immediately close — never read the response body.\n // This prevents downloading large or malicious payloads (e.g. shell scripts).\n const result: RequestResult = {\n statusCode: res.statusCode ?? 0,\n location: Array.isArray(res.headers.location)\n ? res.headers.location[0]\n : res.headers.location,\n }\n resolve(result)\n res.destroy()\n },\n )\n\n req.setTimeout(timeout, () => {\n req.destroy(new TimeoutError(url, timeout))\n })\n\n req.on('error', (err) => {\n if (err instanceof TimeoutError) {\n reject(err)\n return\n }\n // Ignore socket errors that result from our own res.destroy() call\n // (promise is already resolved at that point, so reject is a no-op,\n // but we guard here to avoid unhandled-rejection noise in edge cases)\n reject(new ExpandError(err.message, url))\n })\n })\n}\n\n// ─── URL validation helpers ────────────────────────────────────────────────────\n\nfunction assertHttpProtocol(url: string): void {\n let protocol: string\n try {\n protocol = new URL(url).protocol\n } catch {\n throw new InvalidUrlError(url)\n }\n if (protocol !== 'http:' && protocol !== 'https:') {\n throw new InvalidUrlError(url)\n }\n}\n\nfunction assertHostPolicy(url: string, options: ExpandOptions): void {\n const { hostname } = new URL(url)\n\n if (options.blockPrivateIPs !== false && isPrivateHost(hostname)) {\n throw new ExpandError(\n `Requests to private/local hosts are blocked (\"${hostname}\"). Set blockPrivateIPs: false to allow.`,\n url,\n )\n }\n\n if (options.allowedHosts && options.allowedHosts.length > 0) {\n if (!options.allowedHosts.includes(hostname)) {\n throw new ExpandError(\n `\"${hostname}\" is not in the allowedHosts list`,\n url,\n )\n }\n }\n}\n\nfunction assertRedirectPolicy(from: string, to: string, options: ExpandOptions): void {\n if (options.allowHttpDowngrade === false) {\n const fromProto = new URL(from).protocol\n const toProto = new URL(to).protocol\n if (fromProto === 'https:' && toProto === 'http:') {\n throw new ExpandError(\n `HTTPS to HTTP downgrade blocked (set allowHttpDowngrade: true to permit)`,\n from,\n )\n }\n }\n\n assertHostPolicy(to, options)\n}\n\n// ─── Public API ────────────────────────────────────────────────────────────────\n\nexport async function expand(url: string, options: ExpandOptions = {}): Promise<ExpandResult> {\n assertHttpProtocol(url)\n assertHostPolicy(url, options)\n\n const max = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS\n const chain: string[] = []\n let current = url\n\n for (let i = 0; i <= max; i++) {\n const { statusCode, location } = await makeRequest(current, options)\n\n const isRedirect = statusCode >= 300 && statusCode < 400\n if (isRedirect && location) {\n chain.push(current)\n const next = new URL(location, current).href\n assertHttpProtocol(next)\n assertRedirectPolicy(current, next, options)\n current = next\n continue\n }\n\n return {\n originalUrl: url,\n expandedUrl: current,\n statusCode,\n redirectChain: chain,\n }\n }\n\n throw new MaxRedirectsError(url, max)\n}\n","export class ExpandError extends Error {\n constructor(\n message: string,\n public readonly url: string,\n ) {\n super(message)\n this.name = 'ExpandError'\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n\nexport class TimeoutError extends ExpandError {\n constructor(url: string, timeoutMs: number) {\n super(`Request timed out after ${timeoutMs}ms`, url)\n this.name = 'TimeoutError'\n }\n}\n\nexport class MaxRedirectsError extends ExpandError {\n constructor(url: string, max: number) {\n super(`Exceeded maximum of ${max} redirects`, url)\n this.name = 'MaxRedirectsError'\n }\n}\n\nexport class InvalidUrlError extends ExpandError {\n constructor(url: string) {\n super(`Invalid or unsupported URL: ${url}`, url)\n this.name = 'InvalidUrlError'\n }\n}\n","import { expand } from './expand.js'\nimport type { ExpandManyOptions, ExpandResult } from './types.js'\n\nconst DEFAULT_CONCURRENCY = 5\n\nexport async function expandMany(\n urls: string[],\n options: ExpandManyOptions = {},\n): Promise<ExpandResult[]> {\n const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY\n const results: ExpandResult[] = []\n\n for (let i = 0; i < urls.length; i += concurrency) {\n const batch = urls.slice(i, i + concurrency)\n const settled = await Promise.allSettled(batch.map((url) => expand(url, options)))\n\n for (let j = 0; j < settled.length; j++) {\n const outcome = settled[j]!\n if (outcome.status === 'fulfilled') {\n results.push(outcome.value)\n } else {\n results.push({\n originalUrl: urls[i + j]!,\n expandedUrl: urls[i + j]!,\n statusCode: 0,\n redirectChain: [],\n error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason),\n })\n }\n }\n }\n\n return results\n}\n"],"mappings":"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","names":["https","http"]}
@@ -0,0 +1,70 @@
1
+ interface ExpandOptions {
2
+ /** Request timeout in milliseconds. Default: 10_000 */
3
+ timeout?: number;
4
+ /** Maximum number of redirects to follow. Default: 10 */
5
+ maxRedirects?: number;
6
+ /** User-Agent header value. Default: realistic browser UA */
7
+ userAgent?: string;
8
+ /** Additional headers to send with the request */
9
+ headers?: Record<string, string>;
10
+ /**
11
+ * Allow redirects from https: to http:.
12
+ * Default: true (backward compatible). Set to false in server-side contexts
13
+ * where downgrade attacks are a concern.
14
+ */
15
+ allowHttpDowngrade?: boolean;
16
+ /**
17
+ * If set, only follow redirects to hostnames in this list.
18
+ * Requests to any other hostname will throw an ExpandError.
19
+ * Useful for restricting expansion to known URL shorteners.
20
+ */
21
+ allowedHosts?: string[];
22
+ /**
23
+ * Block requests to private/local IP ranges and metadata endpoints.
24
+ * Covers: loopback (127.x, ::1), RFC1918 (10.x, 172.16-31.x, 192.168.x),
25
+ * link-local (169.254.x.x), cloud metadata (169.254.169.254), and common
26
+ * metadata hostnames (localhost, metadata.google.internal).
27
+ *
28
+ * Default: true. Set to false only if you need to expand internal URLs.
29
+ *
30
+ * Note: hostname-only check — does not resolve DNS. A hostname that resolves
31
+ * to a private IP at connection time is not blocked (DNS rebinding gap).
32
+ */
33
+ blockPrivateIPs?: boolean;
34
+ }
35
+ interface ExpandManyOptions extends ExpandOptions {
36
+ /** Maximum number of concurrent expansions. Default: 5 */
37
+ concurrency?: number;
38
+ }
39
+ interface ExpandResult {
40
+ /** The original URL passed in */
41
+ originalUrl: string;
42
+ /** The final URL after all redirects */
43
+ expandedUrl: string;
44
+ /** HTTP status code of the final response */
45
+ statusCode: number;
46
+ /** Each intermediate URL visited, not including the final destination */
47
+ redirectChain: string[];
48
+ /** Set when expansion failed — only present in expandMany results */
49
+ error?: string;
50
+ }
51
+
52
+ declare function expand(url: string, options?: ExpandOptions): Promise<ExpandResult>;
53
+
54
+ declare function expandMany(urls: string[], options?: ExpandManyOptions): Promise<ExpandResult[]>;
55
+
56
+ declare class ExpandError extends Error {
57
+ readonly url: string;
58
+ constructor(message: string, url: string);
59
+ }
60
+ declare class TimeoutError extends ExpandError {
61
+ constructor(url: string, timeoutMs: number);
62
+ }
63
+ declare class MaxRedirectsError extends ExpandError {
64
+ constructor(url: string, max: number);
65
+ }
66
+ declare class InvalidUrlError extends ExpandError {
67
+ constructor(url: string);
68
+ }
69
+
70
+ export { ExpandError, type ExpandManyOptions, type ExpandOptions, type ExpandResult, InvalidUrlError, MaxRedirectsError, TimeoutError, expand, expandMany };
@@ -0,0 +1,70 @@
1
+ interface ExpandOptions {
2
+ /** Request timeout in milliseconds. Default: 10_000 */
3
+ timeout?: number;
4
+ /** Maximum number of redirects to follow. Default: 10 */
5
+ maxRedirects?: number;
6
+ /** User-Agent header value. Default: realistic browser UA */
7
+ userAgent?: string;
8
+ /** Additional headers to send with the request */
9
+ headers?: Record<string, string>;
10
+ /**
11
+ * Allow redirects from https: to http:.
12
+ * Default: true (backward compatible). Set to false in server-side contexts
13
+ * where downgrade attacks are a concern.
14
+ */
15
+ allowHttpDowngrade?: boolean;
16
+ /**
17
+ * If set, only follow redirects to hostnames in this list.
18
+ * Requests to any other hostname will throw an ExpandError.
19
+ * Useful for restricting expansion to known URL shorteners.
20
+ */
21
+ allowedHosts?: string[];
22
+ /**
23
+ * Block requests to private/local IP ranges and metadata endpoints.
24
+ * Covers: loopback (127.x, ::1), RFC1918 (10.x, 172.16-31.x, 192.168.x),
25
+ * link-local (169.254.x.x), cloud metadata (169.254.169.254), and common
26
+ * metadata hostnames (localhost, metadata.google.internal).
27
+ *
28
+ * Default: true. Set to false only if you need to expand internal URLs.
29
+ *
30
+ * Note: hostname-only check — does not resolve DNS. A hostname that resolves
31
+ * to a private IP at connection time is not blocked (DNS rebinding gap).
32
+ */
33
+ blockPrivateIPs?: boolean;
34
+ }
35
+ interface ExpandManyOptions extends ExpandOptions {
36
+ /** Maximum number of concurrent expansions. Default: 5 */
37
+ concurrency?: number;
38
+ }
39
+ interface ExpandResult {
40
+ /** The original URL passed in */
41
+ originalUrl: string;
42
+ /** The final URL after all redirects */
43
+ expandedUrl: string;
44
+ /** HTTP status code of the final response */
45
+ statusCode: number;
46
+ /** Each intermediate URL visited, not including the final destination */
47
+ redirectChain: string[];
48
+ /** Set when expansion failed — only present in expandMany results */
49
+ error?: string;
50
+ }
51
+
52
+ declare function expand(url: string, options?: ExpandOptions): Promise<ExpandResult>;
53
+
54
+ declare function expandMany(urls: string[], options?: ExpandManyOptions): Promise<ExpandResult[]>;
55
+
56
+ declare class ExpandError extends Error {
57
+ readonly url: string;
58
+ constructor(message: string, url: string);
59
+ }
60
+ declare class TimeoutError extends ExpandError {
61
+ constructor(url: string, timeoutMs: number);
62
+ }
63
+ declare class MaxRedirectsError extends ExpandError {
64
+ constructor(url: string, max: number);
65
+ }
66
+ declare class InvalidUrlError extends ExpandError {
67
+ constructor(url: string);
68
+ }
69
+
70
+ export { ExpandError, type ExpandManyOptions, type ExpandOptions, type ExpandResult, InvalidUrlError, MaxRedirectsError, TimeoutError, expand, expandMany };
package/dist/index.js ADDED
@@ -0,0 +1,197 @@
1
+ // src/expand.ts
2
+ import http from "http";
3
+ import https from "https";
4
+
5
+ // src/errors.ts
6
+ var ExpandError = class extends Error {
7
+ constructor(message, url) {
8
+ super(message);
9
+ this.url = url;
10
+ this.name = "ExpandError";
11
+ Object.setPrototypeOf(this, new.target.prototype);
12
+ }
13
+ url;
14
+ };
15
+ var TimeoutError = class extends ExpandError {
16
+ constructor(url, timeoutMs) {
17
+ super(`Request timed out after ${timeoutMs}ms`, url);
18
+ this.name = "TimeoutError";
19
+ }
20
+ };
21
+ var MaxRedirectsError = class extends ExpandError {
22
+ constructor(url, max) {
23
+ super(`Exceeded maximum of ${max} redirects`, url);
24
+ this.name = "MaxRedirectsError";
25
+ }
26
+ };
27
+ var InvalidUrlError = class extends ExpandError {
28
+ constructor(url) {
29
+ super(`Invalid or unsupported URL: ${url}`, url);
30
+ this.name = "InvalidUrlError";
31
+ }
32
+ };
33
+
34
+ // src/expand.ts
35
+ var DEFAULT_TIMEOUT = 1e4;
36
+ var DEFAULT_MAX_REDIRECTS = 10;
37
+ var DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
38
+ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
39
+ "localhost",
40
+ "metadata.google.internal"
41
+ // GCP metadata
42
+ ]);
43
+ function isPrivateHost(hostname) {
44
+ const h = hostname.toLowerCase();
45
+ if (BLOCKED_HOSTNAMES.has(h)) return true;
46
+ if (h === "::1" || h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd")) {
47
+ return true;
48
+ }
49
+ const parts = h.split(".");
50
+ if (parts.length !== 4) return false;
51
+ const octets = parts.map(Number);
52
+ if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return false;
53
+ const [a, b] = octets;
54
+ return a === 0 || // 0.0.0.0/8 unspecified
55
+ a === 10 || // 10.0.0.0/8 RFC1918
56
+ a === 127 || // 127.0.0.0/8 loopback
57
+ a === 169 && b === 254 || // 169.254.0.0/16 link-local + cloud metadata
58
+ a === 172 && b >= 16 && b <= 31 || // 172.16.0.0/12 RFC1918
59
+ a === 192 && b === 168 || // 192.168.0.0/16 RFC1918
60
+ a === 255;
61
+ }
62
+ function makeRequest(url, options) {
63
+ return new Promise((resolve, reject) => {
64
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
65
+ const lib = url.startsWith("https") ? https : http;
66
+ const req = lib.get(
67
+ url,
68
+ {
69
+ headers: {
70
+ "User-Agent": options.userAgent ?? DEFAULT_UA,
71
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
72
+ ...options.headers
73
+ }
74
+ },
75
+ (res) => {
76
+ const result = {
77
+ statusCode: res.statusCode ?? 0,
78
+ location: Array.isArray(res.headers.location) ? res.headers.location[0] : res.headers.location
79
+ };
80
+ resolve(result);
81
+ res.destroy();
82
+ }
83
+ );
84
+ req.setTimeout(timeout, () => {
85
+ req.destroy(new TimeoutError(url, timeout));
86
+ });
87
+ req.on("error", (err) => {
88
+ if (err instanceof TimeoutError) {
89
+ reject(err);
90
+ return;
91
+ }
92
+ reject(new ExpandError(err.message, url));
93
+ });
94
+ });
95
+ }
96
+ function assertHttpProtocol(url) {
97
+ let protocol;
98
+ try {
99
+ protocol = new URL(url).protocol;
100
+ } catch {
101
+ throw new InvalidUrlError(url);
102
+ }
103
+ if (protocol !== "http:" && protocol !== "https:") {
104
+ throw new InvalidUrlError(url);
105
+ }
106
+ }
107
+ function assertHostPolicy(url, options) {
108
+ const { hostname } = new URL(url);
109
+ if (options.blockPrivateIPs !== false && isPrivateHost(hostname)) {
110
+ throw new ExpandError(
111
+ `Requests to private/local hosts are blocked ("${hostname}"). Set blockPrivateIPs: false to allow.`,
112
+ url
113
+ );
114
+ }
115
+ if (options.allowedHosts && options.allowedHosts.length > 0) {
116
+ if (!options.allowedHosts.includes(hostname)) {
117
+ throw new ExpandError(
118
+ `"${hostname}" is not in the allowedHosts list`,
119
+ url
120
+ );
121
+ }
122
+ }
123
+ }
124
+ function assertRedirectPolicy(from, to, options) {
125
+ if (options.allowHttpDowngrade === false) {
126
+ const fromProto = new URL(from).protocol;
127
+ const toProto = new URL(to).protocol;
128
+ if (fromProto === "https:" && toProto === "http:") {
129
+ throw new ExpandError(
130
+ `HTTPS to HTTP downgrade blocked (set allowHttpDowngrade: true to permit)`,
131
+ from
132
+ );
133
+ }
134
+ }
135
+ assertHostPolicy(to, options);
136
+ }
137
+ async function expand(url, options = {}) {
138
+ assertHttpProtocol(url);
139
+ assertHostPolicy(url, options);
140
+ const max = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
141
+ const chain = [];
142
+ let current = url;
143
+ for (let i = 0; i <= max; i++) {
144
+ const { statusCode, location } = await makeRequest(current, options);
145
+ const isRedirect = statusCode >= 300 && statusCode < 400;
146
+ if (isRedirect && location) {
147
+ chain.push(current);
148
+ const next = new URL(location, current).href;
149
+ assertHttpProtocol(next);
150
+ assertRedirectPolicy(current, next, options);
151
+ current = next;
152
+ continue;
153
+ }
154
+ return {
155
+ originalUrl: url,
156
+ expandedUrl: current,
157
+ statusCode,
158
+ redirectChain: chain
159
+ };
160
+ }
161
+ throw new MaxRedirectsError(url, max);
162
+ }
163
+
164
+ // src/expand-many.ts
165
+ var DEFAULT_CONCURRENCY = 5;
166
+ async function expandMany(urls, options = {}) {
167
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
168
+ const results = [];
169
+ for (let i = 0; i < urls.length; i += concurrency) {
170
+ const batch = urls.slice(i, i + concurrency);
171
+ const settled = await Promise.allSettled(batch.map((url) => expand(url, options)));
172
+ for (let j = 0; j < settled.length; j++) {
173
+ const outcome = settled[j];
174
+ if (outcome.status === "fulfilled") {
175
+ results.push(outcome.value);
176
+ } else {
177
+ results.push({
178
+ originalUrl: urls[i + j],
179
+ expandedUrl: urls[i + j],
180
+ statusCode: 0,
181
+ redirectChain: [],
182
+ error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason)
183
+ });
184
+ }
185
+ }
186
+ }
187
+ return results;
188
+ }
189
+ export {
190
+ ExpandError,
191
+ InvalidUrlError,
192
+ MaxRedirectsError,
193
+ TimeoutError,
194
+ expand,
195
+ expandMany
196
+ };
197
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/expand.ts","../src/errors.ts","../src/expand-many.ts"],"sourcesContent":["import http from 'node:http'\nimport https from 'node:https'\nimport { ExpandError, InvalidUrlError, MaxRedirectsError, TimeoutError } from './errors.js'\nimport type { ExpandOptions, ExpandResult } from './types.js'\n\nconst DEFAULT_TIMEOUT = 10_000\nconst DEFAULT_MAX_REDIRECTS = 10\nconst DEFAULT_UA =\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'\n\n// ─── Private / local IP detection ─────────────────────────────────────────────\n\nconst BLOCKED_HOSTNAMES = new Set([\n 'localhost',\n 'metadata.google.internal', // GCP metadata\n])\n\nfunction isPrivateHost(hostname: string): boolean {\n const h = hostname.toLowerCase()\n\n if (BLOCKED_HOSTNAMES.has(h)) return true\n\n // IPv6 loopback, link-local, ULA (fc00::/7)\n if (h === '::1' || h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) {\n return true\n }\n\n // IPv4: must be exactly 4 dot-separated octets\n const parts = h.split('.')\n if (parts.length !== 4) return false\n const octets = parts.map(Number)\n if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return false\n\n const [a, b] = octets as [number, number, number, number]\n return (\n a === 0 || // 0.0.0.0/8 unspecified\n a === 10 || // 10.0.0.0/8 RFC1918\n a === 127 || // 127.0.0.0/8 loopback\n (a === 169 && b === 254) || // 169.254.0.0/16 link-local + cloud metadata\n (a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12 RFC1918\n (a === 192 && b === 168) || // 192.168.0.0/16 RFC1918\n a === 255 // 255.x.x.x broadcast\n )\n}\n\n// ─── Request (headers only — body is never read) ───────────────────────────────\n\ninterface RequestResult {\n statusCode: number\n location: string | undefined\n}\n\nfunction makeRequest(url: string, options: ExpandOptions): Promise<RequestResult> {\n return new Promise((resolve, reject) => {\n const timeout = options.timeout ?? DEFAULT_TIMEOUT\n const lib = url.startsWith('https') ? https : http\n\n const req = lib.get(\n url,\n {\n headers: {\n 'User-Agent': options.userAgent ?? DEFAULT_UA,\n Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n ...options.headers,\n },\n },\n (res) => {\n // Capture headers then immediately close — never read the response body.\n // This prevents downloading large or malicious payloads (e.g. shell scripts).\n const result: RequestResult = {\n statusCode: res.statusCode ?? 0,\n location: Array.isArray(res.headers.location)\n ? res.headers.location[0]\n : res.headers.location,\n }\n resolve(result)\n res.destroy()\n },\n )\n\n req.setTimeout(timeout, () => {\n req.destroy(new TimeoutError(url, timeout))\n })\n\n req.on('error', (err) => {\n if (err instanceof TimeoutError) {\n reject(err)\n return\n }\n // Ignore socket errors that result from our own res.destroy() call\n // (promise is already resolved at that point, so reject is a no-op,\n // but we guard here to avoid unhandled-rejection noise in edge cases)\n reject(new ExpandError(err.message, url))\n })\n })\n}\n\n// ─── URL validation helpers ────────────────────────────────────────────────────\n\nfunction assertHttpProtocol(url: string): void {\n let protocol: string\n try {\n protocol = new URL(url).protocol\n } catch {\n throw new InvalidUrlError(url)\n }\n if (protocol !== 'http:' && protocol !== 'https:') {\n throw new InvalidUrlError(url)\n }\n}\n\nfunction assertHostPolicy(url: string, options: ExpandOptions): void {\n const { hostname } = new URL(url)\n\n if (options.blockPrivateIPs !== false && isPrivateHost(hostname)) {\n throw new ExpandError(\n `Requests to private/local hosts are blocked (\"${hostname}\"). Set blockPrivateIPs: false to allow.`,\n url,\n )\n }\n\n if (options.allowedHosts && options.allowedHosts.length > 0) {\n if (!options.allowedHosts.includes(hostname)) {\n throw new ExpandError(\n `\"${hostname}\" is not in the allowedHosts list`,\n url,\n )\n }\n }\n}\n\nfunction assertRedirectPolicy(from: string, to: string, options: ExpandOptions): void {\n if (options.allowHttpDowngrade === false) {\n const fromProto = new URL(from).protocol\n const toProto = new URL(to).protocol\n if (fromProto === 'https:' && toProto === 'http:') {\n throw new ExpandError(\n `HTTPS to HTTP downgrade blocked (set allowHttpDowngrade: true to permit)`,\n from,\n )\n }\n }\n\n assertHostPolicy(to, options)\n}\n\n// ─── Public API ────────────────────────────────────────────────────────────────\n\nexport async function expand(url: string, options: ExpandOptions = {}): Promise<ExpandResult> {\n assertHttpProtocol(url)\n assertHostPolicy(url, options)\n\n const max = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS\n const chain: string[] = []\n let current = url\n\n for (let i = 0; i <= max; i++) {\n const { statusCode, location } = await makeRequest(current, options)\n\n const isRedirect = statusCode >= 300 && statusCode < 400\n if (isRedirect && location) {\n chain.push(current)\n const next = new URL(location, current).href\n assertHttpProtocol(next)\n assertRedirectPolicy(current, next, options)\n current = next\n continue\n }\n\n return {\n originalUrl: url,\n expandedUrl: current,\n statusCode,\n redirectChain: chain,\n }\n }\n\n throw new MaxRedirectsError(url, max)\n}\n","export class ExpandError extends Error {\n constructor(\n message: string,\n public readonly url: string,\n ) {\n super(message)\n this.name = 'ExpandError'\n Object.setPrototypeOf(this, new.target.prototype)\n }\n}\n\nexport class TimeoutError extends ExpandError {\n constructor(url: string, timeoutMs: number) {\n super(`Request timed out after ${timeoutMs}ms`, url)\n this.name = 'TimeoutError'\n }\n}\n\nexport class MaxRedirectsError extends ExpandError {\n constructor(url: string, max: number) {\n super(`Exceeded maximum of ${max} redirects`, url)\n this.name = 'MaxRedirectsError'\n }\n}\n\nexport class InvalidUrlError extends ExpandError {\n constructor(url: string) {\n super(`Invalid or unsupported URL: ${url}`, url)\n this.name = 'InvalidUrlError'\n }\n}\n","import { expand } from './expand.js'\nimport type { ExpandManyOptions, ExpandResult } from './types.js'\n\nconst DEFAULT_CONCURRENCY = 5\n\nexport async function expandMany(\n urls: string[],\n options: ExpandManyOptions = {},\n): Promise<ExpandResult[]> {\n const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY\n const results: ExpandResult[] = []\n\n for (let i = 0; i < urls.length; i += concurrency) {\n const batch = urls.slice(i, i + concurrency)\n const settled = await Promise.allSettled(batch.map((url) => expand(url, options)))\n\n for (let j = 0; j < settled.length; j++) {\n const outcome = settled[j]!\n if (outcome.status === 'fulfilled') {\n results.push(outcome.value)\n } else {\n results.push({\n originalUrl: urls[i + j]!,\n expandedUrl: urls[i + j]!,\n statusCode: 0,\n redirectChain: [],\n error: outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason),\n })\n }\n }\n }\n\n return results\n}\n"],"mappings":";AAAA,OAAO,UAAU;AACjB,OAAO,WAAW;;;ACDX,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YACE,SACgB,KAChB;AACA,UAAM,OAAO;AAFG;AAGhB,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,WAAW,SAAS;AAAA,EAClD;AAAA,EALkB;AAMpB;AAEO,IAAM,eAAN,cAA2B,YAAY;AAAA,EAC5C,YAAY,KAAa,WAAmB;AAC1C,UAAM,2BAA2B,SAAS,MAAM,GAAG;AACnD,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,oBAAN,cAAgC,YAAY;AAAA,EACjD,YAAY,KAAa,KAAa;AACpC,UAAM,uBAAuB,GAAG,cAAc,GAAG;AACjD,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,kBAAN,cAA8B,YAAY;AAAA,EAC/C,YAAY,KAAa;AACvB,UAAM,+BAA+B,GAAG,IAAI,GAAG;AAC/C,SAAK,OAAO;AAAA,EACd;AACF;;;ADzBA,IAAM,kBAAkB;AACxB,IAAM,wBAAwB;AAC9B,IAAM,aACJ;AAIF,IAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EACA;AAAA;AACF,CAAC;AAED,SAAS,cAAc,UAA2B;AAChD,QAAM,IAAI,SAAS,YAAY;AAE/B,MAAI,kBAAkB,IAAI,CAAC,EAAG,QAAO;AAGrC,MAAI,MAAM,SAAS,EAAE,WAAW,OAAO,KAAK,EAAE,WAAW,IAAI,KAAK,EAAE,WAAW,IAAI,GAAG;AACpF,WAAO;AAAA,EACT;AAGA,QAAM,QAAQ,EAAE,MAAM,GAAG;AACzB,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,SAAS,MAAM,IAAI,MAAM;AAC/B,MAAI,OAAO,KAAK,CAAC,MAAM,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,KAAK,IAAI,GAAG,EAAG,QAAO;AAEzE,QAAM,CAAC,GAAG,CAAC,IAAI;AACf,SACE,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACL,MAAM,OAAO,MAAM;AAAA,EACnB,MAAM,OAAO,KAAK,MAAM,KAAK;AAAA,EAC7B,MAAM,OAAO,MAAM;AAAA,EACpB,MAAM;AAEV;AASA,SAAS,YAAY,KAAa,SAAgD;AAChF,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,UAAU,QAAQ,WAAW;AACnC,UAAM,MAAM,IAAI,WAAW,OAAO,IAAI,QAAQ;AAE9C,UAAM,MAAM,IAAI;AAAA,MACd;AAAA,MACA;AAAA,QACE,SAAS;AAAA,UACP,cAAc,QAAQ,aAAa;AAAA,UACnC,QAAQ;AAAA,UACR,GAAG,QAAQ;AAAA,QACb;AAAA,MACF;AAAA,MACA,CAAC,QAAQ;AAGP,cAAM,SAAwB;AAAA,UAC5B,YAAY,IAAI,cAAc;AAAA,UAC9B,UAAU,MAAM,QAAQ,IAAI,QAAQ,QAAQ,IACxC,IAAI,QAAQ,SAAS,CAAC,IACtB,IAAI,QAAQ;AAAA,QAClB;AACA,gBAAQ,MAAM;AACd,YAAI,QAAQ;AAAA,MACd;AAAA,IACF;AAEA,QAAI,WAAW,SAAS,MAAM;AAC5B,UAAI,QAAQ,IAAI,aAAa,KAAK,OAAO,CAAC;AAAA,IAC5C,CAAC;AAED,QAAI,GAAG,SAAS,CAAC,QAAQ;AACvB,UAAI,eAAe,cAAc;AAC/B,eAAO,GAAG;AACV;AAAA,MACF;AAIA,aAAO,IAAI,YAAY,IAAI,SAAS,GAAG,CAAC;AAAA,IAC1C,CAAC;AAAA,EACH,CAAC;AACH;AAIA,SAAS,mBAAmB,KAAmB;AAC7C,MAAI;AACJ,MAAI;AACF,eAAW,IAAI,IAAI,GAAG,EAAE;AAAA,EAC1B,QAAQ;AACN,UAAM,IAAI,gBAAgB,GAAG;AAAA,EAC/B;AACA,MAAI,aAAa,WAAW,aAAa,UAAU;AACjD,UAAM,IAAI,gBAAgB,GAAG;AAAA,EAC/B;AACF;AAEA,SAAS,iBAAiB,KAAa,SAA8B;AACnE,QAAM,EAAE,SAAS,IAAI,IAAI,IAAI,GAAG;AAEhC,MAAI,QAAQ,oBAAoB,SAAS,cAAc,QAAQ,GAAG;AAChE,UAAM,IAAI;AAAA,MACR,iDAAiD,QAAQ;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAEA,MAAI,QAAQ,gBAAgB,QAAQ,aAAa,SAAS,GAAG;AAC3D,QAAI,CAAC,QAAQ,aAAa,SAAS,QAAQ,GAAG;AAC5C,YAAM,IAAI;AAAA,QACR,IAAI,QAAQ;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,qBAAqB,MAAc,IAAY,SAA8B;AACpF,MAAI,QAAQ,uBAAuB,OAAO;AACxC,UAAM,YAAY,IAAI,IAAI,IAAI,EAAE;AAChC,UAAM,UAAY,IAAI,IAAI,EAAE,EAAE;AAC9B,QAAI,cAAc,YAAY,YAAY,SAAS;AACjD,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,mBAAiB,IAAI,OAAO;AAC9B;AAIA,eAAsB,OAAO,KAAa,UAAyB,CAAC,GAA0B;AAC5F,qBAAmB,GAAG;AACtB,mBAAiB,KAAK,OAAO;AAE7B,QAAM,MAAQ,QAAQ,gBAAgB;AACtC,QAAM,QAAkB,CAAC;AACzB,MAAI,UAAU;AAEd,WAAS,IAAI,GAAG,KAAK,KAAK,KAAK;AAC7B,UAAM,EAAE,YAAY,SAAS,IAAI,MAAM,YAAY,SAAS,OAAO;AAEnE,UAAM,aAAa,cAAc,OAAO,aAAa;AACrD,QAAI,cAAc,UAAU;AAC1B,YAAM,KAAK,OAAO;AAClB,YAAM,OAAO,IAAI,IAAI,UAAU,OAAO,EAAE;AACxC,yBAAmB,IAAI;AACvB,2BAAqB,SAAS,MAAM,OAAO;AAC3C,gBAAU;AACV;AAAA,IACF;AAEA,WAAO;AAAA,MACL,aAAa;AAAA,MACb,aAAa;AAAA,MACb;AAAA,MACA,eAAe;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,IAAI,kBAAkB,KAAK,GAAG;AACtC;;;AE/KA,IAAM,sBAAsB;AAE5B,eAAsB,WACpB,MACA,UAA6B,CAAC,GACL;AACzB,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,UAA0B,CAAC;AAEjC,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK,aAAa;AACjD,UAAM,QAAQ,KAAK,MAAM,GAAG,IAAI,WAAW;AAC3C,UAAM,UAAU,MAAM,QAAQ,WAAW,MAAM,IAAI,CAAC,QAAQ,OAAO,KAAK,OAAO,CAAC,CAAC;AAEjF,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,UAAU,QAAQ,CAAC;AACzB,UAAI,QAAQ,WAAW,aAAa;AAClC,gBAAQ,KAAK,QAAQ,KAAK;AAAA,MAC5B,OAAO;AACL,gBAAQ,KAAK;AAAA,UACX,aAAa,KAAK,IAAI,CAAC;AAAA,UACvB,aAAa,KAAK,IAAI,CAAC;AAAA,UACvB,YAAY;AAAA,UACZ,eAAe,CAAC;AAAA,UAChB,OAAO,QAAQ,kBAAkB,QAAQ,QAAQ,OAAO,UAAU,OAAO,QAAQ,MAAM;AAAA,QACzF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}