@url-sanitize/core 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/index.d.ts +39 -1
- package/dist/index.js +59 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,16 +5,16 @@ Pure TypeScript implementation for removing tracking parameters and unwrapping t
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
|
-
npm install @url-sanitize/core @url-sanitize/
|
|
8
|
+
npm install @url-sanitize/core @url-sanitize/merged
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
14
|
import { compileSanitizer } from '@url-sanitize/core';
|
|
15
|
-
import {
|
|
15
|
+
import { mergedCatalog } from '@url-sanitize/merged';
|
|
16
16
|
|
|
17
|
-
const sanitize = compileSanitizer(
|
|
17
|
+
const sanitize = compileSanitizer(mergedCatalog, {
|
|
18
18
|
stripReferralMarketing: false,
|
|
19
19
|
unwrapRedirects: true,
|
|
20
20
|
domainBlocking: false
|
|
@@ -38,7 +38,7 @@ type SanitizeResult =
|
|
|
38
38
|
|
|
39
39
|
- Zero runtime dependencies.
|
|
40
40
|
- Pure function; no I/O.
|
|
41
|
-
- Catalog-agnostic: any source (ClearURLs, AdGuard, custom) that produces a `SanitizerCatalog` works.
|
|
41
|
+
- Catalog-agnostic: any source (ClearURLs, AdGuard, Brave, Firefox, custom) that produces a `SanitizerCatalog` works.
|
|
42
42
|
- Factory pattern (`compileSanitizer(catalog)`) — no module-level cache, no singletons.
|
|
43
43
|
- Discriminated union return type — exhaustive `switch (result.kind)`.
|
|
44
44
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
type RuleSource = 'clearurls' | 'adguard' | 'brave' | 'firefox' | 'custom';
|
|
2
|
+
type RedirectMatchPart = 'url' | 'pathname';
|
|
3
|
+
type RedirectTargetEncoding = 'percent' | 'base64';
|
|
4
|
+
type RedirectPrependScheme = 'http' | 'https';
|
|
2
5
|
type SanitizerRule = {
|
|
3
6
|
kind: 'strip-param';
|
|
4
7
|
source: RuleSource;
|
|
5
8
|
provider: string;
|
|
6
9
|
urlPattern?: string;
|
|
7
10
|
paramPattern: string;
|
|
11
|
+
valuePattern?: string;
|
|
8
12
|
exceptions?: string[];
|
|
9
13
|
isReferralMarketing?: boolean;
|
|
10
14
|
} | {
|
|
@@ -22,6 +26,10 @@ type SanitizerRule = {
|
|
|
22
26
|
urlPattern?: string;
|
|
23
27
|
pattern: string;
|
|
24
28
|
captureGroup: number;
|
|
29
|
+
matchPart?: RedirectMatchPart;
|
|
30
|
+
targetEncoding?: RedirectTargetEncoding;
|
|
31
|
+
prependScheme?: RedirectPrependScheme;
|
|
32
|
+
targetTemplate?: string;
|
|
25
33
|
exceptions?: string[];
|
|
26
34
|
} | {
|
|
27
35
|
kind: 'block-domain';
|
|
@@ -150,6 +158,9 @@ declare const sanitizerCatalogJsonSchema: {
|
|
|
150
158
|
readonly paramPattern: {
|
|
151
159
|
readonly type: "string";
|
|
152
160
|
};
|
|
161
|
+
readonly valuePattern: {
|
|
162
|
+
readonly type: "string";
|
|
163
|
+
};
|
|
153
164
|
readonly isReferralMarketing: {
|
|
154
165
|
readonly type: "boolean";
|
|
155
166
|
};
|
|
@@ -214,6 +225,18 @@ declare const sanitizerCatalogJsonSchema: {
|
|
|
214
225
|
readonly type: "integer";
|
|
215
226
|
readonly minimum: 0;
|
|
216
227
|
};
|
|
228
|
+
readonly matchPart: {
|
|
229
|
+
readonly enum: readonly ["url", "pathname"];
|
|
230
|
+
};
|
|
231
|
+
readonly targetEncoding: {
|
|
232
|
+
readonly enum: readonly ["percent", "base64"];
|
|
233
|
+
};
|
|
234
|
+
readonly prependScheme: {
|
|
235
|
+
readonly enum: readonly ["http", "https"];
|
|
236
|
+
};
|
|
237
|
+
readonly targetTemplate: {
|
|
238
|
+
readonly type: "string";
|
|
239
|
+
};
|
|
217
240
|
readonly source: {
|
|
218
241
|
readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
|
|
219
242
|
};
|
|
@@ -439,6 +462,9 @@ declare const urlSanitizeJsonSchemas: {
|
|
|
439
462
|
readonly paramPattern: {
|
|
440
463
|
readonly type: "string";
|
|
441
464
|
};
|
|
465
|
+
readonly valuePattern: {
|
|
466
|
+
readonly type: "string";
|
|
467
|
+
};
|
|
442
468
|
readonly isReferralMarketing: {
|
|
443
469
|
readonly type: "boolean";
|
|
444
470
|
};
|
|
@@ -503,6 +529,18 @@ declare const urlSanitizeJsonSchemas: {
|
|
|
503
529
|
readonly type: "integer";
|
|
504
530
|
readonly minimum: 0;
|
|
505
531
|
};
|
|
532
|
+
readonly matchPart: {
|
|
533
|
+
readonly enum: readonly ["url", "pathname"];
|
|
534
|
+
};
|
|
535
|
+
readonly targetEncoding: {
|
|
536
|
+
readonly enum: readonly ["percent", "base64"];
|
|
537
|
+
};
|
|
538
|
+
readonly prependScheme: {
|
|
539
|
+
readonly enum: readonly ["http", "https"];
|
|
540
|
+
};
|
|
541
|
+
readonly targetTemplate: {
|
|
542
|
+
readonly type: "string";
|
|
543
|
+
};
|
|
506
544
|
readonly source: {
|
|
507
545
|
readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
|
|
508
546
|
};
|
|
@@ -694,4 +732,4 @@ declare const urlSanitizeJsonSchemas: {
|
|
|
694
732
|
};
|
|
695
733
|
};
|
|
696
734
|
|
|
697
|
-
export { type MatchedRule, type RuleSource, type SanitizeResult, type Sanitizer, type SanitizerCatalog, type SanitizerOptions, type SanitizerRule, compileSanitizer, defineCatalog, mergeCatalogs, sanitizeResultJsonSchema, sanitizerCatalogJsonSchema, sanitizerOptionsJsonSchema, urlSanitizeJsonSchemas };
|
|
735
|
+
export { type MatchedRule, type RedirectMatchPart, type RedirectPrependScheme, type RedirectTargetEncoding, type RuleSource, type SanitizeResult, type Sanitizer, type SanitizerCatalog, type SanitizerOptions, type SanitizerRule, compileSanitizer, defineCatalog, mergeCatalogs, sanitizeResultJsonSchema, sanitizerCatalogJsonSchema, sanitizerOptionsJsonSchema, urlSanitizeJsonSchemas };
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,8 @@ function compileRule(rule) {
|
|
|
40
40
|
case "strip-param": {
|
|
41
41
|
const paramPattern = safeRegex(`^(?:${rule.paramPattern})$`, "i");
|
|
42
42
|
if (!paramPattern) return null;
|
|
43
|
+
const valuePattern = rule.valuePattern ? safeRegex(`^(?:${rule.valuePattern})$`, "i") : null;
|
|
44
|
+
if (rule.valuePattern && !valuePattern) return null;
|
|
43
45
|
const urlPattern = rule.urlPattern ? safeRegex(rule.urlPattern, "i") : null;
|
|
44
46
|
if (rule.urlPattern && !urlPattern) return null;
|
|
45
47
|
return {
|
|
@@ -48,6 +50,7 @@ function compileRule(rule) {
|
|
|
48
50
|
provider: rule.provider,
|
|
49
51
|
urlPattern,
|
|
50
52
|
paramPattern,
|
|
53
|
+
valuePattern,
|
|
51
54
|
exceptions: compileExceptions(rule.exceptions),
|
|
52
55
|
isReferralMarketing: rule.isReferralMarketing ?? false
|
|
53
56
|
};
|
|
@@ -79,6 +82,10 @@ function compileRule(rule) {
|
|
|
79
82
|
urlPattern,
|
|
80
83
|
pattern,
|
|
81
84
|
captureGroup: rule.captureGroup,
|
|
85
|
+
matchPart: rule.matchPart ?? "url",
|
|
86
|
+
targetEncoding: rule.targetEncoding ?? "percent",
|
|
87
|
+
prependScheme: rule.prependScheme,
|
|
88
|
+
targetTemplate: rule.targetTemplate,
|
|
82
89
|
exceptions: compileExceptions(rule.exceptions)
|
|
83
90
|
};
|
|
84
91
|
}
|
|
@@ -102,6 +109,29 @@ function safeDecode(value) {
|
|
|
102
109
|
return value;
|
|
103
110
|
}
|
|
104
111
|
}
|
|
112
|
+
function safeBase64Decode(value) {
|
|
113
|
+
try {
|
|
114
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
115
|
+
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
116
|
+
const binary = globalThis.atob(padded);
|
|
117
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
118
|
+
return new TextDecoder().decode(bytes);
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function templateTarget(template, match) {
|
|
124
|
+
return template.replace(/\$([1-9])/g, (_, index) => match[Number(index)] ?? "");
|
|
125
|
+
}
|
|
126
|
+
function targetWithPrependedScheme(target, scheme) {
|
|
127
|
+
if (!scheme) return target;
|
|
128
|
+
try {
|
|
129
|
+
new URL(target);
|
|
130
|
+
return target;
|
|
131
|
+
} catch {
|
|
132
|
+
return `${scheme}://${target.replace(/^\/+/, "")}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
105
135
|
function matchesAny(url, patterns) {
|
|
106
136
|
return patterns.some((re) => re.test(url));
|
|
107
137
|
}
|
|
@@ -148,10 +178,22 @@ function compileSanitizer(catalog, options = {}) {
|
|
|
148
178
|
}
|
|
149
179
|
case "unwrap-redirect": {
|
|
150
180
|
if (!unwrapRedirects) continue;
|
|
151
|
-
|
|
152
|
-
|
|
181
|
+
let matchInput = current;
|
|
182
|
+
if (rule.matchPart === "pathname") {
|
|
183
|
+
try {
|
|
184
|
+
matchInput = new URL(current).pathname;
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const m = rule.pattern.exec(matchInput);
|
|
190
|
+
if (!m) continue;
|
|
191
|
+
const captured = rule.targetTemplate ? templateTarget(rule.targetTemplate, m) : m[rule.captureGroup];
|
|
153
192
|
if (typeof captured === "string" && captured.length > 0) {
|
|
154
|
-
const
|
|
193
|
+
const decodedTarget = rule.targetEncoding === "base64" ? safeBase64Decode(safeDecode(captured)) : safeDecode(captured);
|
|
194
|
+
if (!decodedTarget) continue;
|
|
195
|
+
const target = targetWithPrependedScheme(decodedTarget, rule.prependScheme);
|
|
196
|
+
if (!target) continue;
|
|
155
197
|
try {
|
|
156
198
|
const targetUrl = new URL(target);
|
|
157
199
|
return {
|
|
@@ -185,8 +227,8 @@ function compileSanitizer(catalog, options = {}) {
|
|
|
185
227
|
} catch {
|
|
186
228
|
continue;
|
|
187
229
|
}
|
|
188
|
-
const search = stripFromQueryString(urlObj.search, rule.paramPattern);
|
|
189
|
-
const hash = stripFromQueryString(urlObj.hash, rule.paramPattern);
|
|
230
|
+
const search = stripFromQueryString(urlObj.search, rule.paramPattern, rule.valuePattern);
|
|
231
|
+
const hash = stripFromQueryString(urlObj.hash, rule.paramPattern, rule.valuePattern);
|
|
190
232
|
const removed = [...search.removed, ...hash.removed];
|
|
191
233
|
if (removed.length === 0) continue;
|
|
192
234
|
urlObj.search = search.result;
|
|
@@ -213,7 +255,7 @@ function compileSanitizer(catalog, options = {}) {
|
|
|
213
255
|
};
|
|
214
256
|
};
|
|
215
257
|
}
|
|
216
|
-
function stripFromQueryString(searchOrHash, paramPattern) {
|
|
258
|
+
function stripFromQueryString(searchOrHash, paramPattern, valuePattern) {
|
|
217
259
|
if (!searchOrHash) return { result: searchOrHash, removed: [] };
|
|
218
260
|
const prefix = searchOrHash.charAt(0);
|
|
219
261
|
const body = prefix === "?" || prefix === "#" ? searchOrHash.slice(1) : searchOrHash;
|
|
@@ -224,7 +266,8 @@ function stripFromQueryString(searchOrHash, paramPattern) {
|
|
|
224
266
|
for (const pair of pairs) {
|
|
225
267
|
const eqIdx = pair.indexOf("=");
|
|
226
268
|
const name = eqIdx === -1 ? pair : pair.slice(0, eqIdx);
|
|
227
|
-
|
|
269
|
+
const value = eqIdx === -1 ? "" : pair.slice(eqIdx + 1);
|
|
270
|
+
if (paramPattern.test(name) && (!valuePattern || valuePattern.test(value))) {
|
|
228
271
|
removed.push(name);
|
|
229
272
|
} else {
|
|
230
273
|
kept.push(pair);
|
|
@@ -238,6 +281,9 @@ function stripFromQueryString(searchOrHash, paramPattern) {
|
|
|
238
281
|
// src/schemas.ts
|
|
239
282
|
var ruleSourceValues = ["clearurls", "adguard", "brave", "firefox", "custom"];
|
|
240
283
|
var ruleKindValues = ["strip-param", "raw-replace", "unwrap-redirect", "block-domain"];
|
|
284
|
+
var redirectMatchPartValues = ["url", "pathname"];
|
|
285
|
+
var redirectTargetEncodingValues = ["percent", "base64"];
|
|
286
|
+
var redirectPrependSchemeValues = ["http", "https"];
|
|
241
287
|
var catalogSourceSchema = {
|
|
242
288
|
type: "object",
|
|
243
289
|
required: ["name"],
|
|
@@ -308,6 +354,7 @@ var sanitizerCatalogJsonSchema = {
|
|
|
308
354
|
...ruleBaseProperties,
|
|
309
355
|
kind: { const: "strip-param" },
|
|
310
356
|
paramPattern: { type: "string" },
|
|
357
|
+
valuePattern: { type: "string" },
|
|
311
358
|
isReferralMarketing: { type: "boolean" }
|
|
312
359
|
}
|
|
313
360
|
},
|
|
@@ -330,7 +377,11 @@ var sanitizerCatalogJsonSchema = {
|
|
|
330
377
|
...ruleBaseProperties,
|
|
331
378
|
kind: { const: "unwrap-redirect" },
|
|
332
379
|
pattern: { type: "string" },
|
|
333
|
-
captureGroup: { type: "integer", minimum: 0 }
|
|
380
|
+
captureGroup: { type: "integer", minimum: 0 },
|
|
381
|
+
matchPart: { enum: redirectMatchPartValues },
|
|
382
|
+
targetEncoding: { enum: redirectTargetEncodingValues },
|
|
383
|
+
prependScheme: { enum: redirectPrependSchemeValues },
|
|
384
|
+
targetTemplate: { type: "string" }
|
|
334
385
|
}
|
|
335
386
|
},
|
|
336
387
|
{
|