url-sanitizer 3.0.0 → 4.0.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/README.md CHANGED
@@ -7,31 +7,34 @@
7
7
 
8
8
  A robust URL sanitizer for Node.js, browsers, and websites.
9
9
  It sanitizes not only regular URLs but also deeply inspects `data` URLs and `blob` URLs.
10
- It also provides built-in utilities to parse URLs and verify URI schemes.
11
-
12
- ## Features
13
-
14
- * **Secure by Default**: Automatically denies `javascript:` and `vbscript:` schemes.
15
- * **Deep Data URL Inspection**: Parses, decodes (including base64), and sanitizes nested data URLs.
16
- * **Blob URL Support**: Converts and sanitizes blob URLs into safe data URLs (async).
17
- * **Relative & Absolute Path Support**: Safely allows same-origin absolute paths and relative paths via an opt-in parameter.
18
- * **DOMPurify Integration**: Purifies HTML/SVG content embedded within data URLs. **Note:** This library is optimized for URL sanitization and is **not intended** as a complete HTML sanitizer for arbitrary untrusted markup.
19
- * **Pure ESM with TypeScript Support**: Works seamlessly across modern environments like Node.js, Deno, websites, and browsers (including WebExtensions).
10
+ It also provides built-in utilities to inspect URLs and verify URI schemes.
20
11
 
21
12
  ## Table of Contents
13
+ * [Features](#features)
22
14
  * [Install](#install)
23
15
  * [Usage](#usage)
24
16
  * [API Reference](#api-reference)
25
17
  * [sanitizeURL(url, opt)](#sanitizeurlurl-opt)
26
18
  * [sanitizeURLSync(url, opt)](#sanitizeurlsyncurl-opt)
27
- * [parseURL(url)](#parseurlurl)
28
- * [parseURLSync(url)](#parseurlsyncurl)
19
+ * [inspectURL(url)](#inspecturlurl)
20
+ * [inspectURLSync(url)](#inspecturlsyncurl)
29
21
  * [isURI(uri)](#isuriuri)
30
22
  * [isURISync(uri)](#isurisyncuri)
31
23
  * [urlSanitizer Instance](#urlsanitizer)
24
+ * [Threat Model](#threat-model)
25
+ * [AI / LLM Application Security](#ai--llm-application-security)
32
26
  * [Performance](#performance)
33
27
  * [Acknowledgments](#acknowledgments)
34
28
 
29
+ ## Features
30
+
31
+ * **Secure by Default**: Strictly blocks `javascript:` and `vbscript:` schemes.
32
+ * **Deep Data URL Inspection**: Parses, decodes (including base64), and sanitizes nested data URLs.
33
+ * **Blob URL Support**: Converts and sanitizes blob URLs into safe data URLs (async).
34
+ * **Relative & Absolute Path Support**: Safely allows root-relative paths (e.g., `/foo`) and relative paths (e.g., `./foo`) via an opt-in parameter.
35
+ * **DOMPurify Integration**: Purifies HTML/SVG content embedded within data URLs.
36
+ * **Pure ESM with TypeScript Support**: Works seamlessly across modern environments like Node.js, Deno, websites, and browsers (including WebExtensions).
37
+
35
38
  ## Install
36
39
 
37
40
  ### Node.js
@@ -42,12 +45,13 @@ npm i url-sanitizer
42
45
 
43
46
  ### Deno
44
47
 
45
- You can import directly in your code without installing.
46
-
47
- ``` javascript
48
+ ```javascript
48
49
  import urlSanitizer from 'npm:url-sanitizer';
49
50
  ```
50
51
 
52
+ **Note:** This library assumes a Node.js/npm-compatible dependency resolution environment, so we recommend using the `npm:` specifier.
53
+ CDN-based ESM imports (e.g., `esm.sh`, `jsdelivr`, or `unpkg`) may work, but are not tested or guaranteed.
54
+
51
55
  ### Browsers and Websites
52
56
 
53
57
  Standalone ESM builds are available in the `dist/` directory:
@@ -64,12 +68,18 @@ Alternatively, download them from [Releases](https://github.com/asamuzaK/urlSani
64
68
 
65
69
  **Note:** `url-sanitizer-wo-dompurify.min.js` is a lightweight build without [DOMPurify](https://www.npmjs.com/package/dompurify) bundled.
66
70
  If you use this build, ensure DOMPurify is exposed globally (e.g., `window.DOMPurify`), otherwise the sanitizer will throw an error at runtime.
71
+ ``` html
72
+ <script src="https://unpkg.com/dompurify/dist/purify.min.js"></script>
73
+ <script type="module">
74
+ import urlSanitizer from 'path/to/url-sanitizer-wo-dompurify.min.js';
75
+ </script>
76
+ ```
67
77
 
68
78
  ## Usage
69
79
 
70
80
  ``` javascript
71
81
  import urlSanitizer, {
72
- isURI, isURISync, parseURL, parseURLSync, sanitizeURL, sanitizeURLSync
82
+ isURI, isURISync, inspectURL, inspectURLSync, sanitizeURL, sanitizeURLSync
73
83
  } from 'url-sanitizer';
74
84
  ```
75
85
 
@@ -80,19 +90,29 @@ import urlSanitizer, {
80
90
  Sanitizes the given URL asynchronously.
81
91
 
82
92
  * `blob`, `data`, and `file` schemes must be explicitly allowed.
83
- * Given a blob URL, it returns a sanitized **data** URL.
93
+ * <a name="about-file-scheme">**WARNING**</a><br>
94
+ **File URLs:** Allowing the `file` scheme can be extremely dangerous in web applications, as it may expose the host's local file system to attacks like Local File Inclusion (LFI). Only allow it if you fully trust the input or are operating in a strictly isolated local environment.
95
+ * **Blob URLs:** Given a **blob** URL, it converts and returns a sanitized **data** URL.
96
+ * The sanitized data URL will **not** be converted back to a blob URL.
97
+ * It is highly recommended to set the `opt.revokeObjectURL` option to `true` when you allow `blob` URLs.
98
+ * Converting a blob URL to a data URL consumes memory, so be aware of the risk of memory exhaustion when handling huge blobs.
99
+ * You can restrict the allowed blob size using the `opt.maxBlobSize` option.
100
+ Default is `16_777_216` (16MB).
101
+ While 16MB may be suitable for client-side processing, it is highly recommended to reduce this limit to `1_048_576` (1MB) or `2_097_152` (2MB) in server-side applications.
102
+ Parsing large HTML/SVG contents inside Data/Blob URLs via JSDOM can consume massive amounts of V8 heap memory.
84
103
 
85
104
  #### Parameters
86
105
 
87
106
  * url **string** URL input.
88
- * opt **object?** Options.
89
- * opt.allow **Array<string>?** Array of allowed schemes, e.g., ['data'].
90
- * opt.deny **Array<string>?** Array of denied schemes, e.g., ['web+foo'].
91
- * opt.only **Array<string>?** Array of specific schemes to allow, e.g., ['git', 'https'].
107
+ * opt **\[object\]** Options.
108
+ * opt.allow **\[Array&lt;string&gt;\]** Array of allowed schemes, e.g., \['data'\].
109
+ * opt.deny **\[Array&lt;string&gt;\]** Array of denied schemes, e.g., \['web+foo'\].
110
+ * opt.only **\[Array&lt;string&gt;\]** Array of specific schemes to allow, e.g., \['git', 'https'\].
92
111
  `only` takes precedence over `allow` and `deny`.
93
- * opt.allowRelative **boolean?** If `true`, allows safe same-origin absolute paths and relative paths. Default is `false`.
94
- * opt.debug **boolean?** If `true`, outputs internal error/warning logs to the console. Default is `false`.
95
- * opt.maxBlobSize **number?** Maximum allowed blob size in bytes. Default is `33554432` (32MB). Exceeding this limit will result in parsing failure to prevent memory exhaustion.
112
+ * opt.allowRelative **\[boolean\]** If `true`, allows root-relative paths (e.g. `/foo`) and relative paths (e.g. `./foo`). Default is `false`.
113
+ * opt.debug **\[boolean\]** If `true`, outputs internal error/warning logs to the console. Default is `false`.
114
+ * opt.revokeObjectURL **\[boolean\]** Revokes the blob URL after sanitization. Default is `false`. However, to prevent memory leaks, it is highly recommended to set this option to `true`.
115
+ * opt.maxBlobSize **\[number\]** Maximum allowed blob size in bytes. Default is `16_777_216` (16MB).
96
116
 
97
117
  **Returns** **Promise&lt;string?&gt;** Sanitized URL, nullable.
98
118
 
@@ -197,44 +217,48 @@ const res8 = await sanitizeURL('git+https://example.com/foo.git?<script>alert(1)
197
217
  Synchronous version of `sanitizeURL()`.
198
218
 
199
219
  * `data` and `file` schemes must be explicitly allowed.
220
+ * **File URLs:** Allowing the `file` scheme can be extremely dangerous in web applications. [See above](#about-file-scheme)
221
+ * **Blob URLs:** `blob` scheme is **not supported** and will return `null`.
222
+ Use the async version for `blob`.
223
+ However, to prevent memory leaks, it is highly recommended to set the `opt.revokeObjectURL` option to `true` so that the unsupported blob URL is properly revoked before returning `null`.
200
224
 
201
- **Note:** `blob` scheme is **not supported** and will return `null`.
202
- Use the async version for `blob`.
225
+ ### inspectURL(url)
203
226
 
204
- ### parseURL(url)
227
+ **Note:** `parseURL()` is deprecated. Please use `inspectURL()` instead.
205
228
 
206
- Parses the given URL asynchronously.
229
+ Inspects, parses, and sanitizes the given URL asynchronously.
207
230
 
208
- * **Data URLs**: Parsed, decoded, and **sanitized** during this process.
209
- For example, if the payload contains HTML/SVG, malicious attributes are removed and the content is safely re-encoded.
210
- * **Blob URLs**: Simply parsed and **not yet sanitized** at this stage.
231
+ * **Data URLs:** The embedded payload is fully decoded and sanitized (e.g., removing malicious HTML/SVG attributes) before being safely re-encoded.
232
+ * **Blob URLs:** Simply parsed, but **neither decoded nor sanitized** at this stage.
233
+ To process and sanitize the content of a blob URL, use [sanitizeURL()](#sanitizeurlurl-opt).
211
234
 
212
235
  #### Parameters
213
236
 
214
237
  * url **string** URL input.
215
238
 
216
- **Returns** **Promise&lt;ParsedURL&gt;** Result.
239
+ **Returns** **Promise&lt;InspectedURLResult&gt;** Result.
217
240
 
218
- #### ParsedURL
241
+ #### InspectedURLResult
219
242
 
220
- An object with additional properties based on the standard [URL](https://developer.mozilla.org/ja/docs/Web/API/URL) API.
243
+ An object extending the standard [URL](https://developer.mozilla.org/ja/docs/Web/API/URL) API with additional properties.
244
+ The properties except for `input` and `valid` are omitted from the object for invalid URLs.
221
245
 
222
- * `input` **string** URL input.
223
- * `valid` **boolean** Is valid URI.
224
- * `data` **object?** Parsed result of data URL, nullable.
225
- * `data.mime` **string?** MIME type.
226
- * `data.base64` **boolean?** Is base64 encoded.
227
- * `data.data` **string?** Data part of the data URL.
228
- * `href`, `origin`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, `hash` — **string?** Properties same as the standard URL API.
246
+ * `input` — **string** The original URL input.
247
+ * `valid` — **boolean** Indicates whether the URI is valid.
248
+ * `data` — **[{ mime: string; base64: boolean; data: string; } | null | undefined]** The parsed result of a data URL. Returns `null` if the URL is valid but not a data URL, and is omitted if the URL is invalid.
249
+ * `data.mime` — **string** The MIME type of the data.
250
+ * `data.base64` — **boolean** Indicates whether the data is base64-encoded.
251
+ * `data.data` — **string** The actual data part of the data URL.
252
+ * `href`, `origin`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, `hash` — **[string | undefined]** Properties identical to the standard URL API (omitted if the URL is invalid).
229
253
 
230
254
  ``` javascript
231
- const res1 = await parseURL('javascript:alert(1)');
255
+ const res1 = await inspectURL('javascript:alert(1)');
232
256
  /* => {
233
257
  input: 'javascript:alert(1)',
234
258
  valid: false
235
259
  } */
236
260
 
237
- const res2 = await parseURL('https://www.example.com/?foo=bar#baz');
261
+ const res2 = await inspectURL('https://www.example.com/?foo=bar#baz');
238
262
  /* => {
239
263
  input: 'https://www.example.com/?foo=bar#baz',
240
264
  valid: true,
@@ -250,7 +274,7 @@ const res2 = await parseURL('https://www.example.com/?foo=bar#baz');
250
274
  } */
251
275
 
252
276
  // base64 encoded SVG '<svg><g onclick="alert(1)"/></svg>'
253
- const res3 = await parseURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhbGVydCgxKSIvPjwvc3ZnPg==');
277
+ const res3 = await inspectURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhbGVydCgxKSIvPjwvc3ZnPg==');
254
278
  /* => {
255
279
  input: 'data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhbGVydCgxKSIvPjwvc3ZnPg==',
256
280
  valid: true,
@@ -267,7 +291,7 @@ const res3 = await parseURL('data:image/svg+xml;base64,PHN2Zz48ZyBvbmNsaWNrPSJhb
267
291
  } */
268
292
 
269
293
  // base64 encoded PNG
270
- const res4 = await parseURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
294
+ const res4 = await inspectURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==');
271
295
  /* => {
272
296
  input: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
273
297
  valid: true,
@@ -288,7 +312,7 @@ const blob5 = new Blob(['<svg><g onload="alert(1)"/></svg>'], {
288
312
  type: 'image/svg+xml'
289
313
  });
290
314
  const url5 = URL.createObjectURL(blob5);
291
- const res5 = await parseURL(url5);
315
+ const res5 = await inspectURL(url5);
292
316
  /* => {
293
317
  input: 'blob:nodedata:82ecc5a4-aea8-48d7-a407-64e2ef0913da',
294
318
  valid: true,
@@ -301,13 +325,15 @@ const res5 = await parseURL(url5);
301
325
  } */
302
326
  ```
303
327
 
304
- ### parseURLSync(url)
328
+ ### inspectURLSync(url)
329
+
330
+ **Note:** `parseURLSync()` is deprecated. Please use `inspectURLSync()` instead.
305
331
 
306
- Synchronous version of `parseURL()`.
332
+ Synchronous version of `inspectURL()`.
307
333
 
308
334
  ### isURI(uri)
309
335
 
310
- Checks if the given string is a valid URI and whether its scheme is recognized as safe and allowed.
336
+ Checks if the given string is a valid URI and whether its scheme is registered in the allowed list.
311
337
 
312
338
  Specifically, it verifies that the input has a correct URI syntax, is not a denied scheme (like `javascript:`), and meets one of the following criteria:
313
339
  * The scheme is registered in the default list (e.g., IANA registered schemes, `https`, `mailto`).
@@ -373,6 +399,42 @@ Remove a scheme from the list of registered URI schemes.
373
399
 
374
400
  Reset registered schemes to the default list.
375
401
 
402
+ ## Threat Model
403
+
404
+ To help you decide if this library fits your security requirements, here is the defined threat model detailing what `url-sanitizer` does and does not protect against.
405
+
406
+ ### In-Scope (What we protect against)
407
+
408
+ This library is primarily designed to prevent **Cross-Site Scripting (XSS)** and unauthorized protocol execution via malicious URLs.
409
+
410
+ * **Direct XSS Execution:** Blocks `javascript:` and `vbscript:` schemes outright, even if they are obfuscated with whitespaces or control characters.
411
+ * **Nested XSS in Data/Blob URLs:** Deeply inspects and sanitizes payloads within `data:` and `blob:` URLs. If an attacker tries to hide malicious HTML/SVG within a Base64-encoded data URL, the embedded content is purified using DOMPurify and re-encoded back into a sanitized URL.
412
+ * **Unauthorized Schemes:** Denies unknown or unregistered URI schemes by default, preventing application-specific or OS-level protocol hijacking (unless explicitly allowed).
413
+
414
+ ### Out-of-Scope (What we DO NOT protect against)
415
+
416
+ This library validates the *syntax and safe construction* of a URL, but it does not validate the *destination or intent* of the server it points to, nor does it secure a compromised host environment.
417
+
418
+ * **Server-Side Request Forgery (SSRF):** We do not check if a URL points to an internal IP address (e.g., `http://localhost`, `http://169.254.169.254`) or a malicious external server.
419
+ * **Phishing & Open Redirects:** A perfectly valid HTTP URL pointing to a phishing site (e.g., `https://evil-example.com/login`) will be considered safe. You must implement your own domain allow-listing if you need to restrict destinations.
420
+ * **General HTML Sanitization:** While we sanitize HTML/SVG *inside* data URLs, this library is not a general-purpose HTML body sanitizer. Do not use it to sanitize arbitrary user-generated DOM content.
421
+ * **Compromised Host Environment:** We assume the native `URL` API is intact. We do not protect against runtime attacks where the global `URL` object has been monkey-patched or tampered with. Additionally, when using the lightweight build (`url-sanitizer-wo-dompurify.min.js`), ensuring a secure and untampered global `DOMPurify` instance is the responsibility of the host environment.
422
+
423
+ ## AI / LLM Application Security
424
+
425
+ With the rise of AI agents, RAG (Retrieval-Augmented Generation), and LLM-driven applications, developers increasingly render content generated directly by AI.
426
+ Unpredictable AI outputs introduce unique security risks, and `url-sanitizer` is architected to serve as a reliable defense layer for these modern environments.
427
+
428
+ ### Defending Against Complex & Obfuscated Payloads
429
+ LLMs can generate highly complex, nested, or Base64-encoded `data:` URLs — either through prompt injection, RAG data poisoning, or simply reproducing obfuscated code from their training data.
430
+ Because `url-sanitizer` performs **Deep Data URL Inspection** — decoding the payload, purifying the inner HTML/SVG tree via DOMPurify, and re-encoding it — it physically neutralizes hidden XSS vectors, ensuring that AI-generated data URLs are safe to render.
431
+
432
+ ### Neutralizing Hallucinated Schemes
433
+ LLMs generate URLs based on statistical linguistic patterns rather than factual databases.
434
+ As a result, they frequently hallucinate plausible-looking but non-existent or hazardous URI schemes (e.g., `ai-agent://`, `host-settings:`).
435
+ By operating on a **Secure by Default** whitelist approach, `url-sanitizer` automatically denies any unrecognized or unregistered protocols.
436
+ This strict blocking prevents hallucinated schemes from inadvertently triggering application-specific or OS-level protocol hijacking.
437
+
376
438
  ## Performance
377
439
 
378
440
  Execution times were measured using [mitata](https://github.com/evanwashere/mitata) on Node.js.
@@ -380,10 +442,10 @@ Execution times were measured using [mitata](https://github.com/evanwashere/mita
380
442
  ### Benchmark Results
381
443
 
382
444
  | URL Type | `url-sanitizer` | [@braintree/sanitize-url](https://www.npmjs.com/package/@braintree/sanitize-url) | [strict-url-sanitise](https://www.npmjs.com/package/strict-url-sanitise) |
383
- | :--- | :--- | :--- | :--- |
384
- | **Normal HTTP URL** | ~2.63 µs/iter | ~4.24 µs/iter | ~4.02 µs/iter |
385
- | **XSS URL** | ~1.23 µs/iter | ~1.55 µs/iter | ~9.47 µs/iter |
386
- | **Complex Data URL** | ~246.46 µs/iter | ~2.76 µs/iter | ~9.60 µs/iter |
445
+ | :--- | :---: | :---: | :---: |
446
+ | **Normal HTTP URL** | ~0.94 µs/iter | ~4.32 µs/iter | ~4.02 µs/iter |
447
+ | **XSS URL** | ~5.28 µs/iter | ~1.52 µs/iter | ~9.44 µs/iter |
448
+ | **Complex Data URL** | ~169.41 µs/iter | ~2.65 µs/iter | ~9.46 µs/iter |
387
449
 
388
450
  ### Characteristics & Trade-offs
389
451
 
@@ -1,4 +1,4 @@
1
- var K={sanitize:(...t)=>{if(globalThis?.DOMPurify)return globalThis.DOMPurify.sanitize(...t);let e=["DOMPurify is not available.","Ensure DOMPurify is exposed globally (e.g., window.DOMPurify)."];throw new Error(e.join(" "))}};var V=/^[\w+/=-]+$/;var D=/data:[\w#&+./;=-]*,/,I=/data:[\w#&+./;=^]*base64,[\w+/=-]+/i,J=/data:[\w#&+./;=-]*,[^\x22]+/g,W=/&#(x(?:00)?[\dA-F]{2}|0?\d{1,3});?/gi,ee=/^(?:application\/(?:[\w#&.;-]+\+)?x|image\/svg\+x|text\/(?:ht|x))ml;?/;var te=/^[a-z][\da-z+.-]*$/,se=/^(?:ext|web)\+[a-z]+$/,O=/(?:java|vb)script/,P=/(?:java|vb)script|blob/,j=/%(?:22|27|3C|3E)|[\x22\x27<>]/,re=/^%[\dA-F]{2}$/i;var b=t=>Object.prototype.toString.call(t).slice(8,-1),m=t=>typeof t=="string"||t instanceof String;var B=[7,8,9,10,11,12,13,27,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255];var F=["aaa","aaas","about","acap","acct","acd","acr","adiumxtra","adt","afp","afs","agtp","aim","amss","android","appdata","apt","ar","ari","ark","at","attachment","aw","barion","beshare","bitcoin","bitcoincash","bl","blob","bluetooth","bolo","brid","browserext","cabal","caip","calculator","callto","cap","cast","casts","chrome","chrome-extension","cid","coap","coaps","com-eventbrite-attendee","content","content-type","crid","cstr","cttps","cvs","dab","dat","data","dav","dhttp","diaspora","dict","did","dilithium3","dis","dlna-playcontainer","dlna-playsingle","dnp","dns","dntp","doi","donau","dpp","drm","dtmi","dtn","dvb","dvx","dweb","ed2k","eid","elsi","embedded","ens","esim","ethereum","example","facetime","feed","feedready","fido","file","finger","first-run-pen-experience","fish","fm","ftp","fuchsia-pkg","geo","gg","git","gitoid","gizmoproject","go","gopher","graph","gtalk","h323","ham","hcap","hcp","hs20","http","https","hxxp","hxxps","hydrazone","hyper","i0","iax","ibi","ibi-","icap","icon","ilstring","im","imap","info","iotdisco","ipfs","ipn","ipns","ipp","ipps","irc","irc6","ircs","iris","iris.beep","iris.lwz","iris.xpc","iris.xpcs","isostore","itms","jabber","jar","jms","keyparc","lastfm","lbry","ldap","ldaps","leaptofrogans","lid","linkid","lorawan","lpa","lvlt","machineProvisioningProgressReporter","magnet","mailto","maps","market","matrix","message","microsoft.windows.camera","microsoft.windows.camera.multipicker","microsoft.windows.camera.picker","mid","mms","mongodb","moz","moz-extension","mqtt","mqtts","ms-access","ms-appinstaller","ms-browser-extension","ms-calculator","ms-drive-to","ms-enrollment","ms-excel","ms-eyecontrolspeech","ms-gamebarservices","ms-gamingoverlay","ms-getoffice","ms-help","ms-infopath","ms-inputapp","ms-launchremotedesktop","ms-lockscreencomponent-config","ms-media-stream-id","ms-meetnow","ms-mixedrealitycapture","ms-mobileplans","ms-newsandinterests","ms-officeapp","ms-people","ms-personacard","ms-powerpoint","ms-project","ms-publisher","ms-recall","ms-remotedesktop","ms-remotedesktop-launch","ms-restoretabcompanion","ms-screenclip","ms-screensketch","ms-search","ms-search-repair","ms-secondary-screen-controller","ms-secondary-screen-setup","ms-settings","ms-settings-airplanemode","ms-settings-bluetooth","ms-settings-camera","ms-settings-cellular","ms-settings-cloudstorage","ms-settings-connectabledevices","ms-settings-displays-topology","ms-settings-emailandaccounts","ms-settings-language","ms-settings-location","ms-settings-lock","ms-settings-nfctransactions","ms-settings-notifications","ms-settings-power","ms-settings-privacy","ms-settings-proximity","ms-settings-screenrotation","ms-settings-wifi","ms-settings-workplace","ms-spd","ms-stickers","ms-sttoverlay","ms-transit-to","ms-useractivityset","ms-uup","ms-virtualtouchpad","ms-visio","ms-walk-to","ms-whiteboard","ms-whiteboard-cmd","ms-widgetboard","ms-widgets","ms-word","msnim","msrp","msrps","mss","mt","mtqp","mtrust","mumble","mupdate","mvn","mvrp","mvrps","news","nfs","ni","nih","nntp","notes","num","ocf","oid","onenote","onenote-cmd","opaquelocktoken","openid","openpgp4fpr","otpauth","palm","paparazzi","payto","pkcs11","platform","pop","pres","proxy","psyc","pttp","pwid","qb","query","quic-transport","redis","rediss","reload","res","resource","rmi","rsync","rtmfp","rtmp","rtsp","rtsps","rtspu","sarif","secondlife","secret-token","service","session","sftp","sgn","shc","shelter","sieve","simpleledger","simplex","sip","sips","skype","smb","smp","sms","smtp","snmp","soap.beep","soap.beeps","soldat","spacify","spiffe","spotify","ssb","ssh","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","teapot","teapots","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","turn","turns","tv","udp","unreal","upn","urn","ut2004","uuid-in-package","v-event","vemmi","ventrilo","ves","view-source","vnc","vscode","vscode-insiders","vsls","w3","wasm","wasm-js","wcr","web3","webcal","wifi","ws","wss","wtai","wyciwyg","xcompute","xcon","xcon-userid","xfire","xftp","xmlrpc.beep","xmlrpc.beeps","xmpp","xrcp","xri","ymsgr","z39.50r","z39.50s","ztdnaid"];var{FileReader:ie}=globalThis;var ae=t=>{if(!m(t))throw new TypeError(`Expected String but got ${b(t)}.`);let e=[];for(let s of t)e.push(`%${s.charCodeAt(0).toString(16).toUpperCase()}`);return e.join("")},oe=t=>{m(t)&&re.test(t)&&(t=t.toUpperCase());let[e,s,r,i,a,n]=["&","#","<",">",'"',"'"].map(ae),o;return t===e?o=`${e}amp;`:t===r?o=`${e}lt;`:t===i?o=`${e}gt;`:t===a?o=`${e}quot;`:t===n?o=`${e}${s}39;`:o=t,o},ne=t=>{if(m(t)){if(!V.test(t))throw new Error(`Invalid base64 data: ${t}`)}else throw new TypeError(`Expected String but got ${b(t)}.`);let e=atob(t),s=Uint8Array.from([...e].map(a=>a.charCodeAt(0))),r=new Set(B),i;return s.every(a=>r.has(a))?i=e.replace(/\s/g,ae):i=t,i},N=(t,e=0)=>{if(!m(t))throw new TypeError(`Expected String but got ${b(t)}.`);if(Number.isInteger(e)){if(e>16)throw new Error("Character references nested too deeply.")}else throw new TypeError(`Expected Number but got ${b(e)}.`);let s=decodeURIComponent(t);if(/&#/.test(s)){let r=new Set(B),i=[...s.matchAll(W)].reverse();for(let a of i){let[n,o]=a,p;if(/^x[\dA-F]+/i.test(o)?p=parseInt(`0${o}`,16):/^\d+/.test(o)&&(p=parseInt(o)),Number.isInteger(p)){let{index:d}=a,[c,h]=[s.substring(0,d),s.substring(d+n.length)];r.has(p)?(s=`${c}${String.fromCharCode(p)}${h}`,(/#x?$/.test(c)||/^#(?:x(?:00)?[2-7]|\d)/.test(h))&&(s=N(s,++e))):p<256&&(s=`${c}${h}`)}}}return s},ce=(t,e=33554432)=>new Promise((s,r)=>{if(Number.isInteger(t?.size)){if(Number.isInteger(e)&&t.size>e)return r(new DOMException(`Blob size (${t.size} bytes) exceeds the maximum allowed size of ${e} bytes.`,"NotReadableError"))}else return s(null);let i=new ie;i.addEventListener("error",()=>r(i.error)),i.addEventListener("abort",()=>s(i.result)),i.addEventListener("load",()=>s(i.result)),i.readAsDataURL(t)}),C=class{#e=new Set(F);get(){return[...this.#e]}has(e){return this.#e.has(e)}add(e){if(m(e)){if(O.test(e)||!te.test(e))throw new Error(`Invalid scheme: ${e}`)}else throw new TypeError(`Expected String but got ${b(e)}.`);return this.#e.add(e),[...this.#e]}remove(e){return this.#e.delete(e)}verify(e){let s;if(m(e))try{let{protocol:r}=new URL(e),i=r.replace(/:$/,""),a=i.split("+");s=!O.test(i)&&se.test(i)||a.every(n=>this.#e.has(n))}catch{s=!1}return!!s}reset(){this.#e=new Set(F)}};var E=(t,e,s)=>{t&&console.warn(`[URLSanitizer Debug] ${e}`,s?s.message:"")},H=class extends C{#e;#t=new Set;constructor(){super(),this.#e=0}#s(e,s,r){if(!P.test(e)){if(super.has(e))r.schemeMap.set(e,!0);else{try{super.add(e)}catch(i){let a=`Failed to add scheme '${e}' in '${s}' list.`;E(r.debug,a,i)}super.has(e)&&(r.schemeMap.set(e,!0),r.tempScheme.add(e))}return!0}return!1}replace(e){if(!m(e))throw new TypeError(`Expected String but got ${b(e)}.`);let s=e;if(D.test(s)){let i=[...s.matchAll(J)].reverse();for(let a of i){let[n]=a;I.test(n)&&([n]=I.exec(n)),this.#e++,this.#t.add(n);let o;try{o=this.sanitize(n,{allow:["data"]})}finally{this.#e--,this.#t.delete(n)}let{index:p}=a,[d,c]=[s.substring(0,p),s.substring(p+n.length)];o?s=`${d}${o}${c}`:s=`${d}${c}`}}return s}purify(e){if(!m(e))throw new TypeError(`Expected String but got ${b(e)}.`);let s=e;try{s=decodeURIComponent(e)}catch{}let r=K.sanitize(s);r&&D.test(r)&&(r=this.replace(r)),r=r.replace(/(?:#|%23)$/,"").replace(/(?<!(?:#|%23).*)(?:\?|%3F)$/,"");try{return encodeURI(r)}catch{return r}}sanitize(e,s){if(this.#e>16)throw new Error("Data URLs nested too deeply.");let{allow:r,deny:i,only:a,allowRelative:n=!1,debug:o=!1}=s??{},p=new Map([["blob",!1],["data",!1],["file",!1],["javascript",!1],["vbscript",!1]]),d=new Set,c={schemeMap:p,tempScheme:d,debug:o},h=!1;if(Array.isArray(a)&&a.length){let l=super.get();for(let u of l)p.set(u,!1);for(let u of a)m(u)&&(u=u.trim(),this.#s(u,"only",c)&&!h&&p.has(u)&&(h=p.get(u)))}else{if(Array.isArray(r)&&r.length)for(let l of r)m(l)&&this.#s(l.trim(),"allow",c);if(Array.isArray(i)&&i.length)for(let l of i)m(l)&&(l=l.trim(),l&&p.set(l,!1))}let S,v=super.verify(e),y=!1;if(!v&&n)try{let{hostname:l,protocol:u}=new URL(e,"http://dummy.local");u==="http:"&&l==="dummy.local"&&(v=!0,y=!0)}catch(l){E(o,"Failed to parse relative URL.",l)}if(v){let l,u,k,q,X,L="",T=[],U=!1;if(y)U=!0;else if({hash:l,href:u,pathname:k,protocol:q,search:X}=new URL(e),L=q.replace(/:$/,""),T=L.split("+"),h)U=T.every(f=>p.get(f));else for(let[f,x]of p.entries())if(U=x||L!==f&&T.every($=>$!==f),!U)break;if(U){let _=y?!1:T.includes("data"),f=y?e:u;if(_){let[x,...$]=k.split(","),A=`${$.join(",")}${X}${l}`,z=x.split(";"),Y=z[z.length-1]==="base64",g=A;Y&&(g=ne(A));try{let G=N(g).trim(),{protocol:Q}=new URL(G);Q.replace(/:$/,"").split("+").some(pe=>P.test(pe))&&(f="")}catch(G){E(o,"Failed to parse inner data URL protocol.",G)}let Z=D.test(g);(g!==A||Z)&&Z&&(g=this.replace(g)),(!x||ee.test(x))&&(g=this.purify(g)),f&&g?(Y&&g!==A&&z.pop(),f=`${L}:${z.join(";")},${g}`):f=""}if(!_&&j.test(f)){let x=j.exec(f),{index:$}=x;f=f.substring(0,$).replace(/[?&]$/,"")}f&&(S=f.replace(/%26/g,oe))}d.size&&d.forEach(_=>{super.remove(_)})}return S||null}parse(e,s){if(!m(e))throw new TypeError(`Expected String but got ${b(e)}.`);let r=new Map([["input",e]]),i;if(this.verify(e)){let{protocol:a}=new URL(e);a==="blob:"?i=e:i=this.sanitize(e,s??{allow:["data","file"]})}if(i){let a=new URL(i),{pathname:n,protocol:o}=a,d=o.replace(/:$/,"").split("+").includes("data");if(r.set("valid",!0),d){let c=new Map,[h,...S]=n.split(","),v=`${S.join(",")}`,y=h.split(";"),l=y[y.length-1]==="base64";l&&y.pop(),c.set("mime",y.join(";")),c.set("base64",l),c.set("data",v),r.set("data",Object.fromEntries(c))}else r.set("data",null);for(let c in a){let h=a[c];m(h)&&r.set(c,h)}}else r.set("valid",!1);return Object.fromEntries(r)}reset(){super.reset(),this.#e=0,this.#t.clear()}};var w=new H,he=async(t,e={allow:[],deny:[],only:[],allowRelative:!1,debug:!1,maxBlobSize:33554432})=>{let s=!!e?.debug,r=Number.isInteger(e?.maxBlobSize)&&e.maxBlobSize>0?e.maxBlobSize:33554432,i;if(t&&m(t)){let a;try{let{protocol:n}=new URL(t);a=n.replace(/:$/,"")}catch(n){E(s,`Invalid URL input format: ${t}`,n)}if(a==="blob"){let{allow:n,deny:o,only:p}=e;if(Array.isArray(n)&&n.includes("blob")&&!(Array.isArray(o)&&o.includes("blob"))||Array.isArray(p)&&p.includes("blob")){let d;try{d=await fetch(t).then(c=>c.blob()).then(c=>ce(c,r))}catch(c){let h=`Failed to fetch and convert blob URL: ${t}`;E(s,h,c)}if(d){if(Array.isArray(p))p.includes("data")||p.push("data");else if(Array.isArray(n)&&(n.includes("data")||n.push("data"),Array.isArray(o)&&o.includes("data"))){let c=o.indexOf("data");o.splice(c,1)}i=w.sanitize(d,e)}}URL.revokeObjectURL(t)}else(a||e.allowRelative)&&(i=w.sanitize(t,e))}return i||null},ue=(t,e)=>{let s;if(t&&m(t)){let r;try{let{protocol:i}=new URL(t);r=i.replace(/:$/,"")}catch(i){let a=`Failed to parse URL: ${t}`;E(e?.debug,a,i)}r==="blob"?URL.revokeObjectURL(t):(r||e?.allowRelative)&&(s=w.sanitize(t,e))}return s||null},ge=async t=>w.parse(t),be=t=>w.parse(t),ye=async t=>w.verify(t),we=t=>w.verify(t),xe=w;export{xe as default,ye as isURI,we as isURISync,ge as parseURL,be as parseURLSync,he as sanitizeURL,ue as sanitizeURLSync};
1
+ var fe=Object.freeze(["addHook","removeHook","removeAllHooks","setConfig"]),O=[],I=new Proxy({},{get(t,e){let s=globalThis.DOMPurify||globalThis.window?.DOMPurify;if(s){if(O.length){for(let{prop:r,args:o}of O)typeof s[r]=="function"&&s[r](...o);O.length=0}let a=s[e];return typeof a=="function"?a.bind(s):a}return(...a)=>{if(fe.includes(e)){O.push({prop:e,args:a});return}throw new Error("DOMPurify is not available. Ensure DOMPurify is exposed globally (e.g., window.DOMPurify).")}}});var R=t=>Object.prototype.toString.call(t).slice(8,-1),m=t=>typeof t=="string"||t instanceof String;var K=["aaa","aaas","about","acap","acct","acd","acr","adiumxtra","adt","afp","afs","agtp","aim","amss","android","appdata","apt","ar","ari","ark","at","attachment","aw","barion","beshare","bitcoin","bitcoincash","bl","blob","bluetooth","bolo","brid","browserext","cabal","caip","calculator","callto","cap","cast","casts","chrome","chrome-extension","cid","coap","coaps","com-eventbrite-attendee","content","content-type","crid","cstr","cttps","cvs","dab","dat","data","dav","dhttp","diaspora","dict","did","dilithium3","dis","dlna-playcontainer","dlna-playsingle","dnp","dns","dntp","doi","donau","dpp","drm","dtmi","dtn","dvb","dvx","dweb","ed2k","eid","elsi","embedded","ens","esim","ethereum","example","facetime","feed","feedready","fido","file","finger","first-run-pen-experience","fish","fm","ftp","fuchsia-pkg","geo","gg","git","gitoid","gizmoproject","go","gopher","graph","gtalk","h323","ham","hcap","hcp","hs20","http","https","hxxp","hxxps","hydrazone","hyper","i0","iax","ibi","ibi-","icap","icon","ilstring","im","imap","info","iotdisco","ipfs","ipn","ipns","ipp","ipps","irc","irc6","ircs","iris","iris.beep","iris.lwz","iris.xpc","iris.xpcs","isostore","itms","jabber","jar","jms","keyparc","lastfm","lbry","ldap","ldaps","leaptofrogans","lid","linkid","lorawan","lpa","lvlt","machineProvisioningProgressReporter","magnet","mailto","maps","market","matrix","message","microsoft.windows.camera","microsoft.windows.camera.multipicker","microsoft.windows.camera.picker","mid","mms","mongodb","moz","moz-extension","mqtt","mqtts","ms-access","ms-appinstaller","ms-browser-extension","ms-calculator","ms-drive-to","ms-enrollment","ms-excel","ms-eyecontrolspeech","ms-gamebarservices","ms-gamingoverlay","ms-getoffice","ms-help","ms-infopath","ms-inputapp","ms-launchremotedesktop","ms-lockscreencomponent-config","ms-media-stream-id","ms-meetnow","ms-mixedrealitycapture","ms-mobileplans","ms-newsandinterests","ms-officeapp","ms-people","ms-personacard","ms-powerpoint","ms-project","ms-publisher","ms-recall","ms-remotedesktop","ms-remotedesktop-launch","ms-restoretabcompanion","ms-screenclip","ms-screensketch","ms-search","ms-search-repair","ms-secondary-screen-controller","ms-secondary-screen-setup","ms-settings","ms-settings-airplanemode","ms-settings-bluetooth","ms-settings-camera","ms-settings-cellular","ms-settings-cloudstorage","ms-settings-connectabledevices","ms-settings-displays-topology","ms-settings-emailandaccounts","ms-settings-language","ms-settings-location","ms-settings-lock","ms-settings-nfctransactions","ms-settings-notifications","ms-settings-power","ms-settings-privacy","ms-settings-proximity","ms-settings-screenrotation","ms-settings-wifi","ms-settings-workplace","ms-spd","ms-stickers","ms-sttoverlay","ms-transit-to","ms-useractivityset","ms-uup","ms-virtualtouchpad","ms-visio","ms-walk-to","ms-whiteboard","ms-whiteboard-cmd","ms-widgetboard","ms-widgets","ms-word","msnim","msrp","msrps","mss","mt","mtqp","mtrust","mumble","mupdate","mvn","mvrp","mvrps","news","nfs","ni","nih","nntp","notes","num","ocf","oid","onenote","onenote-cmd","opaquelocktoken","openid","openpgp4fpr","otpauth","palm","paparazzi","payto","pkcs11","platform","pop","pres","proxy","psyc","pttp","pwid","qb","query","quic-transport","redis","rediss","reload","res","resource","rmi","rsync","rtmfp","rtmp","rtsp","rtsps","rtspu","sarif","secondlife","secret-token","service","session","sftp","sgn","shc","shelter","sieve","simpleledger","simplex","sip","sips","skype","smb","smp","sms","smtp","snmp","soap.beep","soap.beeps","soldat","spacify","spiffe","spotify","ssb","ssh","starknet","steam","stun","stuns","submit","svn","swh","swid","swidpath","tag","taler","teamspeak","teapot","teapots","tel","teliaeid","telnet","tftp","things","thismessage","tip","tn3270","tool","turn","turns","tv","udp","unreal","upn","urn","ut2004","uuid-in-package","v-event","vemmi","ventrilo","ves","view-source","vnc","vscode","vscode-insiders","vsls","w3","wasm","wasm-js","wcr","web3","webcal","wifi","ws","wss","wtai","wyciwyg","xcompute","xcon","xcon-userid","xfire","xftp","xmlrpc.beep","xmlrpc.beeps","xmpp","xrcp","xri","ymsgr","z39.50r","z39.50s","ztdnaid"];var{FileReader:J}=globalThis;var ee=/(?:#|%23)$/,te=/^(?:application\/(?:[\w#&.;-]+\+)?x|image\/svg\+x|text\/(?:ht|x))ml;?/;var se=/(?<!(?:#|%23).*)(?:\?|%3F)$/,G=/^[a-z][\da-z+.-]*$/,re=/^(?:web\+[a-z]+|ext\+[a-z][\da-z.-]*)$/,C=/^(?:java|vb)script$/i,H=/^(?:(?:java|vb)script|blob)$/i,z=/["'<>]|%(?:22|27|3C|3E)|&(?:apos|lt|gt|quot|#0*(?:34|39|60|62)|#x0*(?:22|27|3C|3E));/i,ae=/^%[\dA-F]{2}$/i,oe=/^(?:\/{2,}|\\|[a-z][a-z\d+\-.]*:[^/])/i;var ne=[7,8,9,10,11,12,13,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255];var B=new Set(ne),P=new Map([[128,8364],[130,8218],[131,402],[132,8222],[133,8230],[134,8224],[135,8225],[136,710],[137,8240],[138,352],[139,8249],[140,338],[142,381],[145,8216],[146,8217],[147,8220],[148,8221],[149,8226],[150,8211],[151,8212],[152,732],[153,8482],[154,353],[155,8250],[156,339],[158,382],[159,376]]),be=()=>{let t=new Map;for(let e=0;e<256;e++)!B.has(e)&&!P.has(e)&&t.set(e,`\\x${e.toString(16).padStart(2,"0").toUpperCase()}`);return t},F=be();var[v,Re,xe,Ee,ve,Se]=["&","#","<",">",'"',"'"].map(t=>`%${t.charCodeAt(0).toString(16).toUpperCase()}`),Ue=new RegExp(`[${[...F.values()].join("")}]`),_e=/&#(x(?:00)?[\dA-F]{2}|0?\d{1,3});?/gi,M=class{#e=new Set(K);get(){return[...this.#e]}has(e){return this.#e.has(e)}verify(e,s=this.#e){if(!m(e))return!1;try{let{protocol:a}=new URL(e),r=a.replace(/:$/,""),o=r.split("+");return o.some(l=>C.test(l))?!1:re.test(r)||o.every(l=>s.has(l))}catch{return!1}}};var ie=t=>{let e=t;if(m(e))switch(ae.test(e)&&(e=e.toUpperCase()),e){case v:return`${v}amp;`;case xe:return`${v}lt;`;case Ee:return`${v}gt;`;case ve:return`${v}quot;`;case Se:return`${v}${Re}39;`;default:}return e},ce=t=>{if(!m(t))throw new TypeError(`Expected String but got ${R(t)}.`);let e=t.replace(/\s/g,""),s;try{s=atob(e)}catch{throw new Error(`Invalid base64 data: ${t}`)}let a=Uint8Array.from(s,r=>r.charCodeAt(0));try{let o=new TextDecoder("utf-8",{fatal:!0}).decode(a);return Ue.test(o)?e:o}catch{return e}},Te=(t,e)=>{let s=/x/i.test(e[0])?parseInt(e.substring(1),16):parseInt(e,10);if(Number.isInteger(s)){if(B.has(s))return String.fromCharCode(s);if(P.has(s)){let a=P.get(s);return String.fromCodePoint(a)}else if(F.has(s))return""}return t},le=(t,e=0)=>{if(!m(t))throw new TypeError(`Expected String but got ${R(t)}.`);if(!Number.isInteger(e))throw new TypeError(`Expected Number but got ${R(e)}.`);let s=decodeURIComponent(t);for(;/&#/.test(s);){if(e>16)throw new Error("Character references nested too deeply.");let a=s;if(s=s.replace(_e,Te),s===a)break;e++}return s},pe=(t,e=16777216)=>new Promise((s,a)=>{if(Number.isInteger(t?.size)){if(Number.isInteger(e)&&t.size>e){let o=`Blob size (${t.size} bytes) exceeds max (${e} bytes).`;return a(new DOMException(o,"NotReadableError"))}}else return s(null);let r=new J;r.addEventListener("error",()=>a(r.error)),r.addEventListener("abort",()=>s(r.result)),r.addEventListener("load",()=>s(r.result)),r.readAsDataURL(t)}),me=t=>m(t)?t.replace(ee,"").replace(se,""):t;var Ce=Object.freeze(["href","origin","protocol","username","password","host","hostname","port","pathname","search","hash"]),Le=Object.freeze({RETURN_DOM:!1,RETURN_DOM_FRAGMENT:!1,RETURN_TRUSTED_TYPE:!1}),h=(t,e,s)=>{t&&console.warn(`[URLSanitizer Debug] ${e}`,s?s.message:"")},V=class t extends M{#e;static#t=null;static#s=null;static#o(e,s){if(!s.attrValue||!/^\s*data:/i.test(s.attrValue))return;let a=t.#s,r=t.#t;if(!r||!a)return;let o;try{o=new URL(s.attrValue)}catch{return}if(o.protocol==="data:"){let n=s.attrValue;if(r.recurse||(r.recurse=new Set),r.recurse.has(n)){let l=`Circular Data URL detected and skipped: ${n}`;h(r.debug,l),s.attrValue="";return}r.nest++,r.recurse.add(n);try{let l=a.#a(n,{allow:["data"],deny:[],only:[],allowRelative:!1},r);s.attrValue=l||""}finally{r.nest--,r.recurse.delete(n)}}}static{I.setConfig(Le),I.addHook("uponSanitizeAttribute",t.#o)}constructor(){super(),this.#e=new Set(super.get())}#r(e,s,a,r,o){if(H.test(e))return!1;if(e.split("+").some(d=>C.test(d))||!G.test(e)){let d=`Failed to add scheme '${e}' in '${s}' list.`;return h(o.debug,d),!1}return r.set(e,!0),a.add(e),!0}#n(e,s){let a=e;try{a=decodeURIComponent(e)}catch{}let r=t.#s,o=t.#t;t.#s=this,t.#t=s;let n;try{n=s.domPurify.sanitize(a)}finally{t.#s=r,t.#t=o}n=me(n);try{return encodeURI(n)}catch{return n}}#a(e,s,a){if(a.nest>16)throw new Error("Data URLs nested too deeply.");let{allow:r,deny:o,only:n,allowRelative:l}=s,d=this.#e,u=!1,c=new Map(a.schemeMap);if(n.length){d=new Set;for(let i of n)m(i)&&(i=i.trim(),this.#r(i,"only",d,c,a)&&!u&&(u=!0))}else{if(r.length){d=new Set(this.#e);for(let i of r)m(i)&&this.#r(i.trim(),"allow",d,c,a)}if(o.length)for(let i of o)m(i)&&(i=i.trim(),i&&c.set(i,!1))}let f,S=super.verify(e,d),x=!1,g="";if(!S&&l&&!oe.test(e))try{let i=new URL(e,"http://dummy.local");i.protocol==="http:"&&i.hostname==="dummy.local"&&(S=!0,x=!0,g=`${i.pathname}${i.search}${i.hash}`)}catch(i){h(a.debug,"Failed to parse relative URL.",i)}if(S){let i,D,Y,q,Q,L="",A=[],U=!1;if(x)U=!0;else if({hash:i,href:D,pathname:Y,protocol:q,search:Q}=new URL(e),L=q.replace(/:$/,""),A=L.split("+"),u)U=A.every(p=>c.get(p));else for(let[p,b]of c.entries())if(U=b||L!==p&&A.every(_=>_!==p),!U)break;if(U){let N=x?!1:A.includes("data"),p=x?g:D;if(N){let[b,..._]=Y.split(","),j=`${_.join(",")}${Q}${i}`,$=b.split(";"),W=$[$.length-1]==="base64",w=j;if(W)try{w=ce(j)}catch(T){h(a.debug,"Failed to parse base64 data.",T),p=""}try{let T=le(w).trim(),{protocol:Z}=new URL(T,"http://dummy.local");Z.replace(/:$/,"").split("+").some(de=>H.test(de))&&(p="")}catch(T){h(a.debug,"Failed to parse inner data URL protocol.",T)}(!b||te.test(b))&&(w=this.#n(w,a)),p&&w?(W&&w!==j&&$.pop(),p=`${L}:${$.join(";")},${w}`):p=""}if(!N&&z.test(p)){let b=z.exec(p),{index:_}=b;p=p.substring(0,_).replace(/[?&]$/,"")}p&&(f=p)}}return f||null}sanitize(e,s){if(!e||!m(e))return null;if(!(s&&(Array.isArray(s.deny)&&s.deny.length>0||Array.isArray(s.only)&&s.only.length>0||s.allowRelative))&&(e.startsWith("https://")||e.startsWith("http://"))&&!z.test(e)&&!e.includes("data:"))try{let{href:n}=new URL(e);return n.replace(/%26/g,ie)}catch{return null}let r={allow:Array.isArray(s?.allow)?s.allow:[],deny:Array.isArray(s?.deny)?s.deny:[],only:Array.isArray(s?.only)?s.only:[],allowRelative:!!s?.allowRelative},o={debug:!!s?.debug,domPurify:I,nest:0,recurse:null,schemeMap:new Map([["blob",!1],["data",!1],["file",!1],["javascript",!1],["vbscript",!1]])};return this.#a(e,r,o)}inspect(e,s){if(!m(e))throw new TypeError(`Expected String but got ${R(e)}.`);let a=new Map([["input",e]]),r;if(this.verify(e)){let{protocol:o}=new URL(e);o==="blob:"?r=e:r=this.sanitize(e,s??{allow:["data","file"]})}if(r){let o=new URL(r),{pathname:n,protocol:l}=o,u=l.replace(/:$/,"").split("+").includes("data");if(a.set("valid",!0),u){let c=new Map,[f,...S]=n.split(","),x=`${S.join(",")}${o.search}${o.hash}`,g=f.split(";"),i=g[g.length-1]==="base64";i&&g.pop(),c.set("mime",g.join(";")),c.set("base64",i),c.set("data",x),a.set("data",Object.fromEntries(c))}else a.set("data",null);for(let c of Ce){let f=o[c];m(f)&&a.set(c,f)}}else a.set("valid",!1);return Object.fromEntries(a)}get(){return[...this.#e]}has(e){return this.#e.has(e)}add(e){if(!m(e))throw new TypeError(`Expected String but got ${R(e)}.`);if(e.split("+").some(r=>C.test(r))||!G.test(e))throw new Error(`Invalid scheme: ${e}`);return this.#e.add(e),[...this.#e]}remove(e){return this.#e.delete(e)}reset(){this.#e=new Set(super.get())}verify(e,s=this.#e){return super.verify(e,s)}};var y=new V,Ae=async(t,e={allow:[],deny:[],only:[],allowRelative:!1,debug:!1,revokeObjectURL:!1,maxBlobSize:16777216})=>{let s=!!e?.debug,a=Number.isInteger(e?.maxBlobSize)&&e.maxBlobSize>0?e.maxBlobSize:16777216,r;if(t&&m(t)){let o;try{let{protocol:n}=new URL(t);o=n.replace(/:$/,"")}catch(n){h(s,`Invalid URL input format: ${t}`,n)}if(o==="blob"){let{allow:n,deny:l,only:d}=e;if(Array.isArray(n)&&n.includes("blob")&&!(Array.isArray(l)&&l.includes("blob"))||Array.isArray(d)&&d.includes("blob")){let u;try{u=await fetch(t).then(c=>c.blob()).then(c=>pe(c,a))}catch(c){let f=`Failed to fetch and convert blob URL: ${t}`;h(s,f,c)}if(u){let c={...e};Array.isArray(e.only)?c.only=e.only.includes("data")?e.only:[...e.only,"data"]:Array.isArray(e.allow)&&(c.allow=e.allow.includes("data")?e.allow:[...e.allow,"data"],Array.isArray(e.deny)&&(c.deny=e.deny.filter(f=>f!=="data"))),r=y.sanitize(u,c)}}e?.revokeObjectURL&&URL.revokeObjectURL(t)}else(o||e.allowRelative)&&(r=y.sanitize(t,e))}return r||null},$e=(t,e={allow:[],deny:[],only:[],allowRelative:!1,debug:!1,revokeObjectURL:!1})=>{let s=!!e?.debug,a;if(t&&m(t)){let r;try{let{protocol:o}=new URL(t);r=o.replace(/:$/,"")}catch(o){let n=`Invalid URL input format: ${t}`;h(s,n,o)}if(r==="blob")return e?.revokeObjectURL&&URL.revokeObjectURL(t),null;(r||e.allowRelative)&&(a=y.sanitize(t,e))}return a||null},Oe=async t=>y.inspect(t),Ie=t=>Oe(t),ze=t=>y.inspect(t),Pe=t=>ze(t),Me=async t=>y.verify(t),ke=t=>y.verify(t),De=y;export{De as default,Me as isURI,ke as isURISync,Ie as parseURL,Pe as parseURLSync,Ae as sanitizeURL,$e as sanitizeURLSync};
2
2
  /*!
3
3
  * URL Sanitizer
4
4
  *