@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 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/clearurls
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 { clearurlsCatalog } from '@url-sanitize/clearurls';
15
+ import { mergedCatalog } from '@url-sanitize/merged';
16
16
 
17
- const sanitize = compileSanitizer(clearurlsCatalog, {
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
- const m = rule.pattern.exec(current);
152
- const captured = m?.[rule.captureGroup];
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 target = safeDecode(captured);
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
- if (paramPattern.test(name)) {
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
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@url-sanitize/core",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Pure TypeScript library for removing tracking parameters and unwrapping tracking redirects.",
5
5
  "license": "MIT",
6
6
  "type": "module",