@url-sanitize/core 0.1.4 → 2.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
@@ -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,10 +38,29 @@ 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
 
45
+ ## Custom catalogs
46
+
47
+ ```ts
48
+ import { defineCatalog, mergeCatalogs, sanitizerCatalogJsonSchema } from '@url-sanitize/core';
49
+
50
+ const custom = defineCatalog({
51
+ version: 'custom-1',
52
+ generatedAt: new Date().toISOString(),
53
+ sources: [{ name: 'custom' }],
54
+ rules: [{ kind: 'strip-param', source: 'custom', provider: 'local', paramPattern: 'utm_.+' }]
55
+ });
56
+
57
+ const merged = mergeCatalogs(custom);
58
+ console.log(sanitizerCatalogJsonSchema.$id);
59
+ ```
60
+
61
+ `mergeCatalogs()` preserves input order and concatenates sources/rules. It does
62
+ not dedupe, reorder, or apply source precedence.
63
+
45
64
  ## License
46
65
 
47
66
  MIT.
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';
@@ -73,7 +81,655 @@ interface SanitizerOptions {
73
81
  domainBlocking?: boolean;
74
82
  }
75
83
 
84
+ declare function defineCatalog(catalog: SanitizerCatalog): SanitizerCatalog;
85
+ declare function mergeCatalogs(first: SanitizerCatalog, ...rest: SanitizerCatalog[]): SanitizerCatalog;
86
+
76
87
  type Sanitizer = (input: string) => SanitizeResult;
77
88
  declare function compileSanitizer(catalog: SanitizerCatalog, options?: SanitizerOptions): Sanitizer;
78
89
 
79
- export { type MatchedRule, type RuleSource, type SanitizeResult, type Sanitizer, type SanitizerCatalog, type SanitizerOptions, type SanitizerRule, compileSanitizer };
90
+ declare const sanitizerOptionsJsonSchema: {
91
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
92
+ readonly $id: "https://url-sanitize.dev/schema/v1/options.json";
93
+ readonly title: "url-sanitize options";
94
+ readonly type: "object";
95
+ readonly additionalProperties: false;
96
+ readonly properties: {
97
+ readonly stripReferralMarketing: {
98
+ readonly type: "boolean";
99
+ };
100
+ readonly unwrapRedirects: {
101
+ readonly type: "boolean";
102
+ };
103
+ readonly domainBlocking: {
104
+ readonly type: "boolean";
105
+ };
106
+ };
107
+ };
108
+ declare const sanitizerCatalogJsonSchema: {
109
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
110
+ readonly $id: "https://url-sanitize.dev/schema/v1/catalog.json";
111
+ readonly title: "url-sanitize catalog";
112
+ readonly type: "object";
113
+ readonly required: readonly ["version", "generatedAt", "sources", "rules"];
114
+ readonly additionalProperties: false;
115
+ readonly properties: {
116
+ readonly version: {
117
+ readonly type: "string";
118
+ };
119
+ readonly generatedAt: {
120
+ readonly type: "string";
121
+ };
122
+ readonly sources: {
123
+ readonly type: "array";
124
+ readonly items: {
125
+ readonly type: "object";
126
+ readonly required: readonly ["name"];
127
+ readonly additionalProperties: false;
128
+ readonly properties: {
129
+ readonly name: {
130
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
131
+ };
132
+ readonly version: {
133
+ readonly type: "string";
134
+ };
135
+ readonly hash: {
136
+ readonly type: "string";
137
+ };
138
+ readonly license: {
139
+ readonly type: "string";
140
+ };
141
+ readonly upstream: {
142
+ readonly type: "string";
143
+ };
144
+ };
145
+ };
146
+ };
147
+ readonly rules: {
148
+ readonly type: "array";
149
+ readonly items: {
150
+ readonly oneOf: readonly [{
151
+ readonly type: "object";
152
+ readonly required: readonly ["kind", "source", "provider", "paramPattern"];
153
+ readonly additionalProperties: false;
154
+ readonly properties: {
155
+ readonly kind: {
156
+ readonly const: "strip-param";
157
+ };
158
+ readonly paramPattern: {
159
+ readonly type: "string";
160
+ };
161
+ readonly valuePattern: {
162
+ readonly type: "string";
163
+ };
164
+ readonly isReferralMarketing: {
165
+ readonly type: "boolean";
166
+ };
167
+ readonly source: {
168
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
169
+ };
170
+ readonly provider: {
171
+ readonly type: "string";
172
+ };
173
+ readonly urlPattern: {
174
+ readonly type: "string";
175
+ };
176
+ readonly exceptions: {
177
+ readonly type: "array";
178
+ readonly items: {
179
+ readonly type: "string";
180
+ };
181
+ };
182
+ };
183
+ }, {
184
+ readonly type: "object";
185
+ readonly required: readonly ["kind", "source", "provider", "pattern", "replacement"];
186
+ readonly additionalProperties: false;
187
+ readonly properties: {
188
+ readonly kind: {
189
+ readonly const: "raw-replace";
190
+ };
191
+ readonly pattern: {
192
+ readonly type: "string";
193
+ };
194
+ readonly replacement: {
195
+ readonly type: "string";
196
+ };
197
+ readonly source: {
198
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
199
+ };
200
+ readonly provider: {
201
+ readonly type: "string";
202
+ };
203
+ readonly urlPattern: {
204
+ readonly type: "string";
205
+ };
206
+ readonly exceptions: {
207
+ readonly type: "array";
208
+ readonly items: {
209
+ readonly type: "string";
210
+ };
211
+ };
212
+ };
213
+ }, {
214
+ readonly type: "object";
215
+ readonly required: readonly ["kind", "source", "provider", "pattern", "captureGroup"];
216
+ readonly additionalProperties: false;
217
+ readonly properties: {
218
+ readonly kind: {
219
+ readonly const: "unwrap-redirect";
220
+ };
221
+ readonly pattern: {
222
+ readonly type: "string";
223
+ };
224
+ readonly captureGroup: {
225
+ readonly type: "integer";
226
+ readonly minimum: 0;
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
+ };
240
+ readonly source: {
241
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
242
+ };
243
+ readonly provider: {
244
+ readonly type: "string";
245
+ };
246
+ readonly urlPattern: {
247
+ readonly type: "string";
248
+ };
249
+ readonly exceptions: {
250
+ readonly type: "array";
251
+ readonly items: {
252
+ readonly type: "string";
253
+ };
254
+ };
255
+ };
256
+ }, {
257
+ readonly type: "object";
258
+ readonly required: readonly ["kind", "source", "provider", "urlPattern"];
259
+ readonly additionalProperties: false;
260
+ readonly properties: {
261
+ readonly kind: {
262
+ readonly const: "block-domain";
263
+ };
264
+ readonly source: {
265
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
266
+ };
267
+ readonly provider: {
268
+ readonly type: "string";
269
+ };
270
+ readonly urlPattern: {
271
+ readonly type: "string";
272
+ };
273
+ readonly exceptions: {
274
+ readonly type: "array";
275
+ readonly items: {
276
+ readonly type: "string";
277
+ };
278
+ };
279
+ };
280
+ }];
281
+ };
282
+ };
283
+ };
284
+ };
285
+ declare const sanitizeResultJsonSchema: {
286
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
287
+ readonly $id: "https://url-sanitize.dev/schema/v1/result.json";
288
+ readonly title: "url-sanitize result";
289
+ readonly oneOf: readonly [{
290
+ readonly type: "object";
291
+ readonly required: readonly ["kind", "url"];
292
+ readonly additionalProperties: false;
293
+ readonly properties: {
294
+ readonly kind: {
295
+ readonly const: "unchanged";
296
+ };
297
+ readonly url: {
298
+ readonly type: "string";
299
+ };
300
+ };
301
+ }, {
302
+ readonly type: "object";
303
+ readonly required: readonly ["kind", "original", "url", "strippedParams", "matchedRules"];
304
+ readonly additionalProperties: false;
305
+ readonly properties: {
306
+ readonly kind: {
307
+ readonly const: "cleaned";
308
+ };
309
+ readonly original: {
310
+ readonly type: "string";
311
+ };
312
+ readonly url: {
313
+ readonly type: "string";
314
+ };
315
+ readonly strippedParams: {
316
+ readonly type: "array";
317
+ readonly items: {
318
+ readonly type: "string";
319
+ };
320
+ };
321
+ readonly matchedRules: {
322
+ readonly type: "array";
323
+ readonly items: {
324
+ readonly type: "object";
325
+ readonly required: readonly ["source", "provider", "kind"];
326
+ readonly additionalProperties: false;
327
+ readonly properties: {
328
+ readonly source: {
329
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
330
+ };
331
+ readonly provider: {
332
+ readonly type: "string";
333
+ };
334
+ readonly kind: {
335
+ readonly enum: readonly ["strip-param", "raw-replace", "unwrap-redirect", "block-domain"];
336
+ };
337
+ readonly detail: {
338
+ readonly type: "string";
339
+ };
340
+ };
341
+ };
342
+ };
343
+ };
344
+ }, {
345
+ readonly type: "object";
346
+ readonly required: readonly ["kind", "original", "url", "via"];
347
+ readonly additionalProperties: false;
348
+ readonly properties: {
349
+ readonly kind: {
350
+ readonly const: "redirected";
351
+ };
352
+ readonly original: {
353
+ readonly type: "string";
354
+ };
355
+ readonly url: {
356
+ readonly type: "string";
357
+ };
358
+ readonly via: {
359
+ readonly type: "object";
360
+ readonly required: readonly ["source", "provider", "kind"];
361
+ readonly additionalProperties: false;
362
+ readonly properties: {
363
+ readonly source: {
364
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
365
+ };
366
+ readonly provider: {
367
+ readonly type: "string";
368
+ };
369
+ readonly kind: {
370
+ readonly enum: readonly ["strip-param", "raw-replace", "unwrap-redirect", "block-domain"];
371
+ };
372
+ readonly detail: {
373
+ readonly type: "string";
374
+ };
375
+ };
376
+ };
377
+ };
378
+ }, {
379
+ readonly type: "object";
380
+ readonly required: readonly ["kind", "original", "via"];
381
+ readonly additionalProperties: false;
382
+ readonly properties: {
383
+ readonly kind: {
384
+ readonly const: "blocked";
385
+ };
386
+ readonly original: {
387
+ readonly type: "string";
388
+ };
389
+ readonly via: {
390
+ readonly type: "object";
391
+ readonly required: readonly ["source", "provider", "kind"];
392
+ readonly additionalProperties: false;
393
+ readonly properties: {
394
+ readonly source: {
395
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
396
+ };
397
+ readonly provider: {
398
+ readonly type: "string";
399
+ };
400
+ readonly kind: {
401
+ readonly enum: readonly ["strip-param", "raw-replace", "unwrap-redirect", "block-domain"];
402
+ };
403
+ readonly detail: {
404
+ readonly type: "string";
405
+ };
406
+ };
407
+ };
408
+ };
409
+ }];
410
+ };
411
+ declare const urlSanitizeJsonSchemas: {
412
+ readonly catalog: {
413
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
414
+ readonly $id: "https://url-sanitize.dev/schema/v1/catalog.json";
415
+ readonly title: "url-sanitize catalog";
416
+ readonly type: "object";
417
+ readonly required: readonly ["version", "generatedAt", "sources", "rules"];
418
+ readonly additionalProperties: false;
419
+ readonly properties: {
420
+ readonly version: {
421
+ readonly type: "string";
422
+ };
423
+ readonly generatedAt: {
424
+ readonly type: "string";
425
+ };
426
+ readonly sources: {
427
+ readonly type: "array";
428
+ readonly items: {
429
+ readonly type: "object";
430
+ readonly required: readonly ["name"];
431
+ readonly additionalProperties: false;
432
+ readonly properties: {
433
+ readonly name: {
434
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
435
+ };
436
+ readonly version: {
437
+ readonly type: "string";
438
+ };
439
+ readonly hash: {
440
+ readonly type: "string";
441
+ };
442
+ readonly license: {
443
+ readonly type: "string";
444
+ };
445
+ readonly upstream: {
446
+ readonly type: "string";
447
+ };
448
+ };
449
+ };
450
+ };
451
+ readonly rules: {
452
+ readonly type: "array";
453
+ readonly items: {
454
+ readonly oneOf: readonly [{
455
+ readonly type: "object";
456
+ readonly required: readonly ["kind", "source", "provider", "paramPattern"];
457
+ readonly additionalProperties: false;
458
+ readonly properties: {
459
+ readonly kind: {
460
+ readonly const: "strip-param";
461
+ };
462
+ readonly paramPattern: {
463
+ readonly type: "string";
464
+ };
465
+ readonly valuePattern: {
466
+ readonly type: "string";
467
+ };
468
+ readonly isReferralMarketing: {
469
+ readonly type: "boolean";
470
+ };
471
+ readonly source: {
472
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
473
+ };
474
+ readonly provider: {
475
+ readonly type: "string";
476
+ };
477
+ readonly urlPattern: {
478
+ readonly type: "string";
479
+ };
480
+ readonly exceptions: {
481
+ readonly type: "array";
482
+ readonly items: {
483
+ readonly type: "string";
484
+ };
485
+ };
486
+ };
487
+ }, {
488
+ readonly type: "object";
489
+ readonly required: readonly ["kind", "source", "provider", "pattern", "replacement"];
490
+ readonly additionalProperties: false;
491
+ readonly properties: {
492
+ readonly kind: {
493
+ readonly const: "raw-replace";
494
+ };
495
+ readonly pattern: {
496
+ readonly type: "string";
497
+ };
498
+ readonly replacement: {
499
+ readonly type: "string";
500
+ };
501
+ readonly source: {
502
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
503
+ };
504
+ readonly provider: {
505
+ readonly type: "string";
506
+ };
507
+ readonly urlPattern: {
508
+ readonly type: "string";
509
+ };
510
+ readonly exceptions: {
511
+ readonly type: "array";
512
+ readonly items: {
513
+ readonly type: "string";
514
+ };
515
+ };
516
+ };
517
+ }, {
518
+ readonly type: "object";
519
+ readonly required: readonly ["kind", "source", "provider", "pattern", "captureGroup"];
520
+ readonly additionalProperties: false;
521
+ readonly properties: {
522
+ readonly kind: {
523
+ readonly const: "unwrap-redirect";
524
+ };
525
+ readonly pattern: {
526
+ readonly type: "string";
527
+ };
528
+ readonly captureGroup: {
529
+ readonly type: "integer";
530
+ readonly minimum: 0;
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
+ };
544
+ readonly source: {
545
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
546
+ };
547
+ readonly provider: {
548
+ readonly type: "string";
549
+ };
550
+ readonly urlPattern: {
551
+ readonly type: "string";
552
+ };
553
+ readonly exceptions: {
554
+ readonly type: "array";
555
+ readonly items: {
556
+ readonly type: "string";
557
+ };
558
+ };
559
+ };
560
+ }, {
561
+ readonly type: "object";
562
+ readonly required: readonly ["kind", "source", "provider", "urlPattern"];
563
+ readonly additionalProperties: false;
564
+ readonly properties: {
565
+ readonly kind: {
566
+ readonly const: "block-domain";
567
+ };
568
+ readonly source: {
569
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
570
+ };
571
+ readonly provider: {
572
+ readonly type: "string";
573
+ };
574
+ readonly urlPattern: {
575
+ readonly type: "string";
576
+ };
577
+ readonly exceptions: {
578
+ readonly type: "array";
579
+ readonly items: {
580
+ readonly type: "string";
581
+ };
582
+ };
583
+ };
584
+ }];
585
+ };
586
+ };
587
+ };
588
+ };
589
+ readonly options: {
590
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
591
+ readonly $id: "https://url-sanitize.dev/schema/v1/options.json";
592
+ readonly title: "url-sanitize options";
593
+ readonly type: "object";
594
+ readonly additionalProperties: false;
595
+ readonly properties: {
596
+ readonly stripReferralMarketing: {
597
+ readonly type: "boolean";
598
+ };
599
+ readonly unwrapRedirects: {
600
+ readonly type: "boolean";
601
+ };
602
+ readonly domainBlocking: {
603
+ readonly type: "boolean";
604
+ };
605
+ };
606
+ };
607
+ readonly result: {
608
+ readonly $schema: "https://json-schema.org/draft/2020-12/schema";
609
+ readonly $id: "https://url-sanitize.dev/schema/v1/result.json";
610
+ readonly title: "url-sanitize result";
611
+ readonly oneOf: readonly [{
612
+ readonly type: "object";
613
+ readonly required: readonly ["kind", "url"];
614
+ readonly additionalProperties: false;
615
+ readonly properties: {
616
+ readonly kind: {
617
+ readonly const: "unchanged";
618
+ };
619
+ readonly url: {
620
+ readonly type: "string";
621
+ };
622
+ };
623
+ }, {
624
+ readonly type: "object";
625
+ readonly required: readonly ["kind", "original", "url", "strippedParams", "matchedRules"];
626
+ readonly additionalProperties: false;
627
+ readonly properties: {
628
+ readonly kind: {
629
+ readonly const: "cleaned";
630
+ };
631
+ readonly original: {
632
+ readonly type: "string";
633
+ };
634
+ readonly url: {
635
+ readonly type: "string";
636
+ };
637
+ readonly strippedParams: {
638
+ readonly type: "array";
639
+ readonly items: {
640
+ readonly type: "string";
641
+ };
642
+ };
643
+ readonly matchedRules: {
644
+ readonly type: "array";
645
+ readonly items: {
646
+ readonly type: "object";
647
+ readonly required: readonly ["source", "provider", "kind"];
648
+ readonly additionalProperties: false;
649
+ readonly properties: {
650
+ readonly source: {
651
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
652
+ };
653
+ readonly provider: {
654
+ readonly type: "string";
655
+ };
656
+ readonly kind: {
657
+ readonly enum: readonly ["strip-param", "raw-replace", "unwrap-redirect", "block-domain"];
658
+ };
659
+ readonly detail: {
660
+ readonly type: "string";
661
+ };
662
+ };
663
+ };
664
+ };
665
+ };
666
+ }, {
667
+ readonly type: "object";
668
+ readonly required: readonly ["kind", "original", "url", "via"];
669
+ readonly additionalProperties: false;
670
+ readonly properties: {
671
+ readonly kind: {
672
+ readonly const: "redirected";
673
+ };
674
+ readonly original: {
675
+ readonly type: "string";
676
+ };
677
+ readonly url: {
678
+ readonly type: "string";
679
+ };
680
+ readonly via: {
681
+ readonly type: "object";
682
+ readonly required: readonly ["source", "provider", "kind"];
683
+ readonly additionalProperties: false;
684
+ readonly properties: {
685
+ readonly source: {
686
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
687
+ };
688
+ readonly provider: {
689
+ readonly type: "string";
690
+ };
691
+ readonly kind: {
692
+ readonly enum: readonly ["strip-param", "raw-replace", "unwrap-redirect", "block-domain"];
693
+ };
694
+ readonly detail: {
695
+ readonly type: "string";
696
+ };
697
+ };
698
+ };
699
+ };
700
+ }, {
701
+ readonly type: "object";
702
+ readonly required: readonly ["kind", "original", "via"];
703
+ readonly additionalProperties: false;
704
+ readonly properties: {
705
+ readonly kind: {
706
+ readonly const: "blocked";
707
+ };
708
+ readonly original: {
709
+ readonly type: "string";
710
+ };
711
+ readonly via: {
712
+ readonly type: "object";
713
+ readonly required: readonly ["source", "provider", "kind"];
714
+ readonly additionalProperties: false;
715
+ readonly properties: {
716
+ readonly source: {
717
+ readonly enum: readonly ["clearurls", "adguard", "brave", "firefox", "custom"];
718
+ };
719
+ readonly provider: {
720
+ readonly type: "string";
721
+ };
722
+ readonly kind: {
723
+ readonly enum: readonly ["strip-param", "raw-replace", "unwrap-redirect", "block-domain"];
724
+ };
725
+ readonly detail: {
726
+ readonly type: "string";
727
+ };
728
+ };
729
+ };
730
+ };
731
+ }];
732
+ };
733
+ };
734
+
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
@@ -1,3 +1,23 @@
1
+ // src/catalog.ts
2
+ function defineCatalog(catalog) {
3
+ return catalog;
4
+ }
5
+ function mergeCatalogs(first, ...rest) {
6
+ const catalogs = [first, ...rest];
7
+ return {
8
+ version: catalogs.length === 1 ? first.version : `merged(${catalogs.map((catalog) => catalog.version).join(",")})`,
9
+ generatedAt: latestGeneratedAt(catalogs),
10
+ sources: catalogs.flatMap((catalog) => catalog.sources.map((source) => ({ ...source }))),
11
+ rules: catalogs.flatMap((catalog) => catalog.rules.map(cloneRule))
12
+ };
13
+ }
14
+ function latestGeneratedAt(catalogs) {
15
+ return catalogs.map((catalog) => catalog.generatedAt).reduce((latest, generatedAt) => generatedAt > latest ? generatedAt : latest);
16
+ }
17
+ function cloneRule(rule) {
18
+ return rule.exceptions ? { ...rule, exceptions: [...rule.exceptions] } : { ...rule };
19
+ }
20
+
1
21
  // src/sanitize.ts
2
22
  function safeRegex(pattern, flags = "") {
3
23
  try {
@@ -20,6 +40,8 @@ function compileRule(rule) {
20
40
  case "strip-param": {
21
41
  const paramPattern = safeRegex(`^(?:${rule.paramPattern})$`, "i");
22
42
  if (!paramPattern) return null;
43
+ const valuePattern = rule.valuePattern ? safeRegex(`^(?:${rule.valuePattern})$`, "i") : null;
44
+ if (rule.valuePattern && !valuePattern) return null;
23
45
  const urlPattern = rule.urlPattern ? safeRegex(rule.urlPattern, "i") : null;
24
46
  if (rule.urlPattern && !urlPattern) return null;
25
47
  return {
@@ -28,6 +50,7 @@ function compileRule(rule) {
28
50
  provider: rule.provider,
29
51
  urlPattern,
30
52
  paramPattern,
53
+ valuePattern,
31
54
  exceptions: compileExceptions(rule.exceptions),
32
55
  isReferralMarketing: rule.isReferralMarketing ?? false
33
56
  };
@@ -59,6 +82,10 @@ function compileRule(rule) {
59
82
  urlPattern,
60
83
  pattern,
61
84
  captureGroup: rule.captureGroup,
85
+ matchPart: rule.matchPart ?? "url",
86
+ targetEncoding: rule.targetEncoding ?? "percent",
87
+ prependScheme: rule.prependScheme,
88
+ targetTemplate: rule.targetTemplate,
62
89
  exceptions: compileExceptions(rule.exceptions)
63
90
  };
64
91
  }
@@ -82,6 +109,29 @@ function safeDecode(value) {
82
109
  return value;
83
110
  }
84
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
+ }
85
135
  function matchesAny(url, patterns) {
86
136
  return patterns.some((re) => re.test(url));
87
137
  }
@@ -128,10 +178,22 @@ function compileSanitizer(catalog, options = {}) {
128
178
  }
129
179
  case "unwrap-redirect": {
130
180
  if (!unwrapRedirects) continue;
131
- const m = rule.pattern.exec(current);
132
- 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];
133
192
  if (typeof captured === "string" && captured.length > 0) {
134
- 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;
135
197
  try {
136
198
  const targetUrl = new URL(target);
137
199
  return {
@@ -165,8 +227,8 @@ function compileSanitizer(catalog, options = {}) {
165
227
  } catch {
166
228
  continue;
167
229
  }
168
- const search = stripFromQueryString(urlObj.search, rule.paramPattern);
169
- 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);
170
232
  const removed = [...search.removed, ...hash.removed];
171
233
  if (removed.length === 0) continue;
172
234
  urlObj.search = search.result;
@@ -193,7 +255,7 @@ function compileSanitizer(catalog, options = {}) {
193
255
  };
194
256
  };
195
257
  }
196
- function stripFromQueryString(searchOrHash, paramPattern) {
258
+ function stripFromQueryString(searchOrHash, paramPattern, valuePattern) {
197
259
  if (!searchOrHash) return { result: searchOrHash, removed: [] };
198
260
  const prefix = searchOrHash.charAt(0);
199
261
  const body = prefix === "?" || prefix === "#" ? searchOrHash.slice(1) : searchOrHash;
@@ -204,7 +266,8 @@ function stripFromQueryString(searchOrHash, paramPattern) {
204
266
  for (const pair of pairs) {
205
267
  const eqIdx = pair.indexOf("=");
206
268
  const name = eqIdx === -1 ? pair : pair.slice(0, eqIdx);
207
- if (paramPattern.test(name)) {
269
+ const value = eqIdx === -1 ? "" : pair.slice(eqIdx + 1);
270
+ if (paramPattern.test(name) && (!valuePattern || valuePattern.test(value))) {
208
271
  removed.push(name);
209
272
  } else {
210
273
  kept.push(pair);
@@ -214,6 +277,193 @@ function stripFromQueryString(searchOrHash, paramPattern) {
214
277
  if (kept.length === 0) return { result: "", removed };
215
278
  return { result: prefix + kept.join("&"), removed };
216
279
  }
280
+
281
+ // src/schemas.ts
282
+ var ruleSourceValues = ["clearurls", "adguard", "brave", "firefox", "custom"];
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"];
287
+ var catalogSourceSchema = {
288
+ type: "object",
289
+ required: ["name"],
290
+ additionalProperties: false,
291
+ properties: {
292
+ name: { enum: ruleSourceValues },
293
+ version: { type: "string" },
294
+ hash: { type: "string" },
295
+ license: { type: "string" },
296
+ upstream: { type: "string" }
297
+ }
298
+ };
299
+ var ruleBaseProperties = {
300
+ source: { enum: ruleSourceValues },
301
+ provider: { type: "string" },
302
+ urlPattern: { type: "string" },
303
+ exceptions: {
304
+ type: "array",
305
+ items: { type: "string" }
306
+ }
307
+ };
308
+ var matchedRuleSchema = {
309
+ type: "object",
310
+ required: ["source", "provider", "kind"],
311
+ additionalProperties: false,
312
+ properties: {
313
+ source: { enum: ruleSourceValues },
314
+ provider: { type: "string" },
315
+ kind: { enum: ruleKindValues },
316
+ detail: { type: "string" }
317
+ }
318
+ };
319
+ var sanitizerOptionsJsonSchema = {
320
+ $schema: "https://json-schema.org/draft/2020-12/schema",
321
+ $id: "https://url-sanitize.dev/schema/v1/options.json",
322
+ title: "url-sanitize options",
323
+ type: "object",
324
+ additionalProperties: false,
325
+ properties: {
326
+ stripReferralMarketing: { type: "boolean" },
327
+ unwrapRedirects: { type: "boolean" },
328
+ domainBlocking: { type: "boolean" }
329
+ }
330
+ };
331
+ var sanitizerCatalogJsonSchema = {
332
+ $schema: "https://json-schema.org/draft/2020-12/schema",
333
+ $id: "https://url-sanitize.dev/schema/v1/catalog.json",
334
+ title: "url-sanitize catalog",
335
+ type: "object",
336
+ required: ["version", "generatedAt", "sources", "rules"],
337
+ additionalProperties: false,
338
+ properties: {
339
+ version: { type: "string" },
340
+ generatedAt: { type: "string" },
341
+ sources: {
342
+ type: "array",
343
+ items: catalogSourceSchema
344
+ },
345
+ rules: {
346
+ type: "array",
347
+ items: {
348
+ oneOf: [
349
+ {
350
+ type: "object",
351
+ required: ["kind", "source", "provider", "paramPattern"],
352
+ additionalProperties: false,
353
+ properties: {
354
+ ...ruleBaseProperties,
355
+ kind: { const: "strip-param" },
356
+ paramPattern: { type: "string" },
357
+ valuePattern: { type: "string" },
358
+ isReferralMarketing: { type: "boolean" }
359
+ }
360
+ },
361
+ {
362
+ type: "object",
363
+ required: ["kind", "source", "provider", "pattern", "replacement"],
364
+ additionalProperties: false,
365
+ properties: {
366
+ ...ruleBaseProperties,
367
+ kind: { const: "raw-replace" },
368
+ pattern: { type: "string" },
369
+ replacement: { type: "string" }
370
+ }
371
+ },
372
+ {
373
+ type: "object",
374
+ required: ["kind", "source", "provider", "pattern", "captureGroup"],
375
+ additionalProperties: false,
376
+ properties: {
377
+ ...ruleBaseProperties,
378
+ kind: { const: "unwrap-redirect" },
379
+ pattern: { type: "string" },
380
+ captureGroup: { type: "integer", minimum: 0 },
381
+ matchPart: { enum: redirectMatchPartValues },
382
+ targetEncoding: { enum: redirectTargetEncodingValues },
383
+ prependScheme: { enum: redirectPrependSchemeValues },
384
+ targetTemplate: { type: "string" }
385
+ }
386
+ },
387
+ {
388
+ type: "object",
389
+ required: ["kind", "source", "provider", "urlPattern"],
390
+ additionalProperties: false,
391
+ properties: {
392
+ ...ruleBaseProperties,
393
+ kind: { const: "block-domain" }
394
+ }
395
+ }
396
+ ]
397
+ }
398
+ }
399
+ }
400
+ };
401
+ var sanitizeResultJsonSchema = {
402
+ $schema: "https://json-schema.org/draft/2020-12/schema",
403
+ $id: "https://url-sanitize.dev/schema/v1/result.json",
404
+ title: "url-sanitize result",
405
+ oneOf: [
406
+ {
407
+ type: "object",
408
+ required: ["kind", "url"],
409
+ additionalProperties: false,
410
+ properties: {
411
+ kind: { const: "unchanged" },
412
+ url: { type: "string" }
413
+ }
414
+ },
415
+ {
416
+ type: "object",
417
+ required: ["kind", "original", "url", "strippedParams", "matchedRules"],
418
+ additionalProperties: false,
419
+ properties: {
420
+ kind: { const: "cleaned" },
421
+ original: { type: "string" },
422
+ url: { type: "string" },
423
+ strippedParams: {
424
+ type: "array",
425
+ items: { type: "string" }
426
+ },
427
+ matchedRules: {
428
+ type: "array",
429
+ items: matchedRuleSchema
430
+ }
431
+ }
432
+ },
433
+ {
434
+ type: "object",
435
+ required: ["kind", "original", "url", "via"],
436
+ additionalProperties: false,
437
+ properties: {
438
+ kind: { const: "redirected" },
439
+ original: { type: "string" },
440
+ url: { type: "string" },
441
+ via: matchedRuleSchema
442
+ }
443
+ },
444
+ {
445
+ type: "object",
446
+ required: ["kind", "original", "via"],
447
+ additionalProperties: false,
448
+ properties: {
449
+ kind: { const: "blocked" },
450
+ original: { type: "string" },
451
+ via: matchedRuleSchema
452
+ }
453
+ }
454
+ ]
455
+ };
456
+ var urlSanitizeJsonSchemas = {
457
+ catalog: sanitizerCatalogJsonSchema,
458
+ options: sanitizerOptionsJsonSchema,
459
+ result: sanitizeResultJsonSchema
460
+ };
217
461
  export {
218
- compileSanitizer
462
+ compileSanitizer,
463
+ defineCatalog,
464
+ mergeCatalogs,
465
+ sanitizeResultJsonSchema,
466
+ sanitizerCatalogJsonSchema,
467
+ sanitizerOptionsJsonSchema,
468
+ urlSanitizeJsonSchemas
219
469
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@url-sanitize/core",
3
- "version": "0.1.4",
3
+ "version": "2.0.0",
4
4
  "description": "Pure TypeScript library for removing tracking parameters and unwrapping tracking redirects.",
5
5
  "license": "MIT",
6
6
  "type": "module",