universal-parcel-scraper 0.4.0-main.320 → 0.5.0-main.322
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/data/catalog.json +12 -3
- package/data/detection-golden.json +75 -1
- package/dist/browser/scraper.js +25 -10
- package/dist/carriers/dhl-ecommerce/adapter.d.ts +13 -11
- package/dist/carriers/dhl-ecommerce/adapter.js +127 -14
- package/dist/carriers/dhl-ecommerce/carrier.json +5 -1
- package/dist/carriers/dpd/adapter.d.ts +1 -1
- package/dist/carriers/dpd/adapter.js +8 -3
- package/dist/carriers/posti/adapter.d.ts +1 -1
- package/dist/carriers/posti/adapter.js +6 -1
- package/dist/carriers/posti/carrier.json +13 -2
- package/dist/carriers/posti/numbers.json +28 -0
- package/dist/core/catalog/recognition.d.ts +2 -1
- package/dist/core/catalog/recognition.js +27 -7
- package/dist/core/recognition/index.js +2 -1
- package/dist/data/catalog.json +12 -3
- package/dist/facade/index.js +1 -1
- package/dist/generated/catalog.d.ts +8 -1
- package/dist/generated/catalog.js +12 -3
- package/dist/generated/recognition.js +2 -0
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
package/data/catalog.json
CHANGED
|
@@ -468,7 +468,8 @@
|
|
|
468
468
|
"timezone": "UTC",
|
|
469
469
|
"tracking": {
|
|
470
470
|
"mode": "automatic",
|
|
471
|
-
"adapter": "dhl-ecommerce"
|
|
471
|
+
"adapter": "dhl-ecommerce",
|
|
472
|
+
"recognitionRank": 23
|
|
472
473
|
},
|
|
473
474
|
"canaryUrl": "https://www.dhl.com/ch-en/home/tracking.html",
|
|
474
475
|
"trackingUrlTemplate": "https://www.dhl.com/ch-en/home/tracking.html?tracking-id={trackingNumber}&submit=1",
|
|
@@ -1973,7 +1974,8 @@
|
|
|
1973
1974
|
"timezone": "Europe/Helsinki",
|
|
1974
1975
|
"tracking": {
|
|
1975
1976
|
"mode": "automatic",
|
|
1976
|
-
"adapter": "posti"
|
|
1977
|
+
"adapter": "posti",
|
|
1978
|
+
"recognitionRank": 27
|
|
1977
1979
|
},
|
|
1978
1980
|
"canaryUrl": "https://www.posti.fi/en/tracking",
|
|
1979
1981
|
"trackingUrlTemplate": "https://www.posti.fi/en/tracking/{trackingNumber}",
|
|
@@ -1985,7 +1987,14 @@
|
|
|
1985
1987
|
"path": "^/(?:[a-z]{2}/)?tracking/([^/?#]+)(?:/[0-9]+)?/?$"
|
|
1986
1988
|
}
|
|
1987
1989
|
],
|
|
1988
|
-
"detectionRules": [
|
|
1990
|
+
"detectionRules": [
|
|
1991
|
+
{
|
|
1992
|
+
"pattern": "^[A-Z]{2}\\d{9}FI$",
|
|
1993
|
+
"confidence": "low",
|
|
1994
|
+
"checksum": "s10",
|
|
1995
|
+
"preferred": true
|
|
1996
|
+
}
|
|
1997
|
+
]
|
|
1989
1998
|
},
|
|
1990
1999
|
"correos-express": {
|
|
1991
2000
|
"displayName": "Correos Express",
|
|
@@ -142,6 +142,7 @@
|
|
|
142
142
|
"asked": [
|
|
143
143
|
"tnt",
|
|
144
144
|
"correos-express",
|
|
145
|
+
"dhl-ecommerce",
|
|
145
146
|
"canada-post"
|
|
146
147
|
]
|
|
147
148
|
},
|
|
@@ -1124,6 +1125,7 @@
|
|
|
1124
1125
|
"asked": [
|
|
1125
1126
|
"tnt",
|
|
1126
1127
|
"correos-express",
|
|
1128
|
+
"dhl-ecommerce",
|
|
1127
1129
|
"canada-post"
|
|
1128
1130
|
]
|
|
1129
1131
|
},
|
|
@@ -1140,6 +1142,7 @@
|
|
|
1140
1142
|
"asked": [
|
|
1141
1143
|
"tnt",
|
|
1142
1144
|
"correos-express",
|
|
1145
|
+
"dhl-ecommerce",
|
|
1143
1146
|
"canada-post"
|
|
1144
1147
|
]
|
|
1145
1148
|
},
|
|
@@ -1622,6 +1625,7 @@
|
|
|
1622
1625
|
"asked": [
|
|
1623
1626
|
"tnt",
|
|
1624
1627
|
"correos-express",
|
|
1628
|
+
"dhl-ecommerce",
|
|
1625
1629
|
"canada-post"
|
|
1626
1630
|
]
|
|
1627
1631
|
},
|
|
@@ -1638,6 +1642,7 @@
|
|
|
1638
1642
|
"asked": [
|
|
1639
1643
|
"tnt",
|
|
1640
1644
|
"correos-express",
|
|
1645
|
+
"dhl-ecommerce",
|
|
1641
1646
|
"canada-post"
|
|
1642
1647
|
]
|
|
1643
1648
|
},
|
|
@@ -1731,6 +1736,9 @@
|
|
|
1731
1736
|
"confidence": "low",
|
|
1732
1737
|
"candidates": [
|
|
1733
1738
|
"dhl-ecommerce"
|
|
1739
|
+
],
|
|
1740
|
+
"asked": [
|
|
1741
|
+
"dhl-ecommerce"
|
|
1734
1742
|
]
|
|
1735
1743
|
},
|
|
1736
1744
|
{
|
|
@@ -1746,6 +1754,7 @@
|
|
|
1746
1754
|
"asked": [
|
|
1747
1755
|
"tnt",
|
|
1748
1756
|
"correos-express",
|
|
1757
|
+
"dhl-ecommerce",
|
|
1749
1758
|
"canada-post"
|
|
1750
1759
|
]
|
|
1751
1760
|
},
|
|
@@ -1922,6 +1931,7 @@
|
|
|
1922
1931
|
"asked": [
|
|
1923
1932
|
"tnt",
|
|
1924
1933
|
"correos-express",
|
|
1934
|
+
"dhl-ecommerce",
|
|
1925
1935
|
"canada-post"
|
|
1926
1936
|
]
|
|
1927
1937
|
},
|
|
@@ -2345,6 +2355,9 @@
|
|
|
2345
2355
|
"confidence": "low",
|
|
2346
2356
|
"candidates": [
|
|
2347
2357
|
"dhl-ecommerce"
|
|
2358
|
+
],
|
|
2359
|
+
"asked": [
|
|
2360
|
+
"dhl-ecommerce"
|
|
2348
2361
|
]
|
|
2349
2362
|
},
|
|
2350
2363
|
{
|
|
@@ -2438,6 +2451,7 @@
|
|
|
2438
2451
|
"asked": [
|
|
2439
2452
|
"tnt",
|
|
2440
2453
|
"correos-express",
|
|
2454
|
+
"dhl-ecommerce",
|
|
2441
2455
|
"canada-post"
|
|
2442
2456
|
]
|
|
2443
2457
|
},
|
|
@@ -2454,6 +2468,7 @@
|
|
|
2454
2468
|
"asked": [
|
|
2455
2469
|
"tnt",
|
|
2456
2470
|
"correos-express",
|
|
2471
|
+
"dhl-ecommerce",
|
|
2457
2472
|
"canada-post"
|
|
2458
2473
|
]
|
|
2459
2474
|
},
|
|
@@ -2594,6 +2609,7 @@
|
|
|
2594
2609
|
"asked": [
|
|
2595
2610
|
"tnt",
|
|
2596
2611
|
"correos-express",
|
|
2612
|
+
"dhl-ecommerce",
|
|
2597
2613
|
"canada-post"
|
|
2598
2614
|
]
|
|
2599
2615
|
},
|
|
@@ -2970,6 +2986,7 @@
|
|
|
2970
2986
|
"asked": [
|
|
2971
2987
|
"tnt",
|
|
2972
2988
|
"correos-express",
|
|
2989
|
+
"dhl-ecommerce",
|
|
2973
2990
|
"canada-post"
|
|
2974
2991
|
]
|
|
2975
2992
|
},
|
|
@@ -3101,6 +3118,9 @@
|
|
|
3101
3118
|
"confidence": "low",
|
|
3102
3119
|
"candidates": [
|
|
3103
3120
|
"dhl-ecommerce"
|
|
3121
|
+
],
|
|
3122
|
+
"asked": [
|
|
3123
|
+
"dhl-ecommerce"
|
|
3104
3124
|
]
|
|
3105
3125
|
},
|
|
3106
3126
|
{
|
|
@@ -3112,7 +3132,8 @@
|
|
|
3112
3132
|
"colis-prive"
|
|
3113
3133
|
],
|
|
3114
3134
|
"asked": [
|
|
3115
|
-
"colis-prive"
|
|
3135
|
+
"colis-prive",
|
|
3136
|
+
"dhl-ecommerce"
|
|
3116
3137
|
]
|
|
3117
3138
|
},
|
|
3118
3139
|
{
|
|
@@ -3281,6 +3302,9 @@
|
|
|
3281
3302
|
"confidence": "high",
|
|
3282
3303
|
"candidates": [
|
|
3283
3304
|
"intl-post"
|
|
3305
|
+
],
|
|
3306
|
+
"asked": [
|
|
3307
|
+
"chronopost"
|
|
3284
3308
|
]
|
|
3285
3309
|
},
|
|
3286
3310
|
{
|
|
@@ -3428,6 +3452,9 @@
|
|
|
3428
3452
|
"confidence": "high",
|
|
3429
3453
|
"candidates": [
|
|
3430
3454
|
"intl-post"
|
|
3455
|
+
],
|
|
3456
|
+
"asked": [
|
|
3457
|
+
"chronopost"
|
|
3431
3458
|
]
|
|
3432
3459
|
},
|
|
3433
3460
|
{
|
|
@@ -3473,6 +3500,29 @@
|
|
|
3473
3500
|
"spring-gds"
|
|
3474
3501
|
]
|
|
3475
3502
|
},
|
|
3503
|
+
{
|
|
3504
|
+
"input": "CP123456785FI",
|
|
3505
|
+
"carrier": "intl-post",
|
|
3506
|
+
"confidence": "high",
|
|
3507
|
+
"candidates": [
|
|
3508
|
+
"intl-post"
|
|
3509
|
+
],
|
|
3510
|
+
"asked": [
|
|
3511
|
+
"posti",
|
|
3512
|
+
"chronopost"
|
|
3513
|
+
]
|
|
3514
|
+
},
|
|
3515
|
+
{
|
|
3516
|
+
"input": "CP123456789FI",
|
|
3517
|
+
"carrier": "unknown",
|
|
3518
|
+
"confidence": "low",
|
|
3519
|
+
"candidates": [
|
|
3520
|
+
"chronopost"
|
|
3521
|
+
],
|
|
3522
|
+
"asked": [
|
|
3523
|
+
"chronopost"
|
|
3524
|
+
]
|
|
3525
|
+
},
|
|
3476
3526
|
{
|
|
3477
3527
|
"input": "CP123456789PL",
|
|
3478
3528
|
"carrier": "unknown",
|
|
@@ -3490,6 +3540,9 @@
|
|
|
3490
3540
|
"confidence": "high",
|
|
3491
3541
|
"candidates": [
|
|
3492
3542
|
"intl-post"
|
|
3543
|
+
],
|
|
3544
|
+
"asked": [
|
|
3545
|
+
"chronopost"
|
|
3493
3546
|
]
|
|
3494
3547
|
},
|
|
3495
3548
|
{
|
|
@@ -4177,6 +4230,9 @@
|
|
|
4177
4230
|
"confidence": "high",
|
|
4178
4231
|
"candidates": [
|
|
4179
4232
|
"intl-post"
|
|
4233
|
+
],
|
|
4234
|
+
"asked": [
|
|
4235
|
+
"chronopost"
|
|
4180
4236
|
]
|
|
4181
4237
|
},
|
|
4182
4238
|
{
|
|
@@ -4204,6 +4260,9 @@
|
|
|
4204
4260
|
"confidence": "high",
|
|
4205
4261
|
"candidates": [
|
|
4206
4262
|
"intl-post"
|
|
4263
|
+
],
|
|
4264
|
+
"asked": [
|
|
4265
|
+
"chronopost"
|
|
4207
4266
|
]
|
|
4208
4267
|
},
|
|
4209
4268
|
{
|
|
@@ -4220,6 +4279,9 @@
|
|
|
4220
4279
|
"confidence": "high",
|
|
4221
4280
|
"candidates": [
|
|
4222
4281
|
"intl-post"
|
|
4282
|
+
],
|
|
4283
|
+
"asked": [
|
|
4284
|
+
"chronopost"
|
|
4223
4285
|
]
|
|
4224
4286
|
},
|
|
4225
4287
|
{
|
|
@@ -4236,6 +4298,9 @@
|
|
|
4236
4298
|
"confidence": "high",
|
|
4237
4299
|
"candidates": [
|
|
4238
4300
|
"intl-post"
|
|
4301
|
+
],
|
|
4302
|
+
"asked": [
|
|
4303
|
+
"chronopost"
|
|
4239
4304
|
]
|
|
4240
4305
|
},
|
|
4241
4306
|
{
|
|
@@ -4332,6 +4397,9 @@
|
|
|
4332
4397
|
"confidence": "high",
|
|
4333
4398
|
"candidates": [
|
|
4334
4399
|
"intl-post"
|
|
4400
|
+
],
|
|
4401
|
+
"asked": [
|
|
4402
|
+
"chronopost"
|
|
4335
4403
|
]
|
|
4336
4404
|
},
|
|
4337
4405
|
{
|
|
@@ -4639,6 +4707,9 @@
|
|
|
4639
4707
|
"confidence": "high",
|
|
4640
4708
|
"candidates": [
|
|
4641
4709
|
"intl-post"
|
|
4710
|
+
],
|
|
4711
|
+
"asked": [
|
|
4712
|
+
"chronopost"
|
|
4642
4713
|
]
|
|
4643
4714
|
},
|
|
4644
4715
|
{
|
|
@@ -5040,6 +5111,9 @@
|
|
|
5040
5111
|
"confidence": "high",
|
|
5041
5112
|
"candidates": [
|
|
5042
5113
|
"intl-post"
|
|
5114
|
+
],
|
|
5115
|
+
"asked": [
|
|
5116
|
+
"chronopost"
|
|
5043
5117
|
]
|
|
5044
5118
|
},
|
|
5045
5119
|
{
|
package/dist/browser/scraper.js
CHANGED
|
@@ -479,7 +479,8 @@ var ue = {
|
|
|
479
479
|
timezone: "UTC",
|
|
480
480
|
tracking: {
|
|
481
481
|
mode: "automatic",
|
|
482
|
-
adapter: "dhl-ecommerce"
|
|
482
|
+
adapter: "dhl-ecommerce",
|
|
483
|
+
recognitionRank: 23
|
|
483
484
|
},
|
|
484
485
|
canaryUrl: "https://www.dhl.com/ch-en/home/tracking.html",
|
|
485
486
|
trackingUrlTemplate: "https://www.dhl.com/ch-en/home/tracking.html?tracking-id={trackingNumber}&submit=1",
|
|
@@ -1595,7 +1596,8 @@ var ue = {
|
|
|
1595
1596
|
timezone: "Europe/Helsinki",
|
|
1596
1597
|
tracking: {
|
|
1597
1598
|
mode: "automatic",
|
|
1598
|
-
adapter: "posti"
|
|
1599
|
+
adapter: "posti",
|
|
1600
|
+
recognitionRank: 27
|
|
1599
1601
|
},
|
|
1600
1602
|
canaryUrl: "https://www.posti.fi/en/tracking",
|
|
1601
1603
|
trackingUrlTemplate: "https://www.posti.fi/en/tracking/{trackingNumber}",
|
|
@@ -1603,7 +1605,12 @@ var ue = {
|
|
|
1603
1605
|
domains: ["posti.fi"],
|
|
1604
1606
|
path: "^/(?:[a-z]{2}/)?tracking/([^/?#]+)(?:/[0-9]+)?/?$"
|
|
1605
1607
|
}],
|
|
1606
|
-
detectionRules: [
|
|
1608
|
+
detectionRules: [{
|
|
1609
|
+
pattern: "^[A-Z]{2}\\d{9}FI$",
|
|
1610
|
+
confidence: "low",
|
|
1611
|
+
checksum: "s10",
|
|
1612
|
+
preferred: !0
|
|
1613
|
+
}]
|
|
1607
1614
|
},
|
|
1608
1615
|
"correos-express": {
|
|
1609
1616
|
displayName: "Correos Express",
|
|
@@ -7427,6 +7434,7 @@ var Lo = {
|
|
|
7427
7434
|
"colis-prive": 30,
|
|
7428
7435
|
"correos-express": 36,
|
|
7429
7436
|
"ctt-express": 35,
|
|
7437
|
+
"dhl-ecommerce": 23,
|
|
7430
7438
|
dpd: 70,
|
|
7431
7439
|
"gls-ch": 60,
|
|
7432
7440
|
"gls-de": 50,
|
|
@@ -7437,6 +7445,7 @@ var Lo = {
|
|
|
7437
7445
|
"ninja-van": 28,
|
|
7438
7446
|
"nz-post": 32,
|
|
7439
7447
|
"poczta-polska": 31,
|
|
7448
|
+
posti: 27,
|
|
7440
7449
|
postlogistics: 26,
|
|
7441
7450
|
"relais-colis": 25,
|
|
7442
7451
|
seur: 43,
|
|
@@ -7446,20 +7455,26 @@ var Lo = {
|
|
|
7446
7455
|
uniuni: 33
|
|
7447
7456
|
}, Ro = 5;
|
|
7448
7457
|
function zo(e, t = {}) {
|
|
7449
|
-
let n = b(e);
|
|
7450
|
-
if (n.confidence !== "low") return [];
|
|
7451
|
-
let
|
|
7458
|
+
let n = b(e), r = n.carrier === "intl-post";
|
|
7459
|
+
if (n.confidence !== "low" && !r) return [];
|
|
7460
|
+
let i = g(e), a = r ? Object.entries(_).flatMap(([t, n]) => {
|
|
7461
|
+
let r = n.detectionRules.find((t) => t.confidence === "low" && (!t.checksum || t.checksum === "s10") && new RegExp(t.pattern).test(i) && (!t.rawPattern || new RegExp(t.rawPattern).test(e.trim().toUpperCase())));
|
|
7462
|
+
return r ? [{
|
|
7463
|
+
carrier: t,
|
|
7464
|
+
preferred: r.preferred === !0
|
|
7465
|
+
}] : [];
|
|
7466
|
+
}) : [], o = r ? a.map(({ carrier: e }) => e) : n.candidates, s = r ? a.filter((e) => e.preferred).map(({ carrier: e }) => e) : n.preferred, c = new Set(s.filter((e) => Lo[e] === void 0).map((e) => Io(e)).filter(Boolean)), l = (e) => [
|
|
7452
7467
|
+(e === t.hint),
|
|
7453
|
-
+!!
|
|
7468
|
+
+!!s.includes(e),
|
|
7454
7469
|
Lo[e] ?? 0
|
|
7455
7470
|
];
|
|
7456
|
-
return
|
|
7471
|
+
return o.filter((e) => Lo[e] !== void 0 && we.has(e) && xe(e) !== "universal" && !c.has(Io(e)) && !t.skip?.(e)).map((e) => ({
|
|
7457
7472
|
carrier: e,
|
|
7458
|
-
score:
|
|
7473
|
+
score: l(e)
|
|
7459
7474
|
})).sort((e, t) => t.score[0] - e.score[0] || t.score[1] - e.score[1] || t.score[2] - e.score[2]).map(({ carrier: t }) => ({
|
|
7460
7475
|
carrier: t,
|
|
7461
7476
|
needsInput: Ce(t, e)[0]?.field ?? null,
|
|
7462
|
-
preferred:
|
|
7477
|
+
preferred: s.includes(t)
|
|
7463
7478
|
}));
|
|
7464
7479
|
}
|
|
7465
7480
|
//#endregion
|
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DHL eCommerce, through
|
|
3
|
-
* page
|
|
4
|
-
*
|
|
5
|
-
* The endpoint answers every direct server request with an Akamai crypto
|
|
6
|
-
* proof-of-work challenge (HTTP 428) that plain HTTP cannot solve (verified
|
|
7
|
-
* 2026-09-10: cookie replay and visiting the page first both still return
|
|
8
|
-
* 428), and browser clearance is not transferable back to Node. The lookup
|
|
9
|
-
* therefore has a single step: a local Chromium session loads the tracking
|
|
10
|
-
* page, the site solves its own challenge and calls the API in that session,
|
|
11
|
-
* and the response to the exact requested URL is parsed.
|
|
2
|
+
* DHL eCommerce, through its anonymous Americas Webtrack API first, then
|
|
3
|
+
* the global tracking page's UTAPI for shipments outside Webtrack's scope.
|
|
12
4
|
*
|
|
13
5
|
* DHL may answer with a customer-confirmation id instead of the queried
|
|
14
6
|
* alias, so only one eCommerce shipment from that exact request URL is
|
|
15
7
|
* accepted, and the id itself is never retained.
|
|
16
8
|
*/
|
|
17
|
-
import type
|
|
9
|
+
import { type AdapterFactory, type Recognition, type TrackingContext } from '../../core/adapter/index.js';
|
|
18
10
|
import { ChallengeError, type CarrierErrorOptions } from '../../core/errors/index.js';
|
|
19
11
|
import type { CarrierResult } from '../../core/result/index.js';
|
|
20
12
|
import { type StepRecorder } from '../../core/telemetry/index.js';
|
|
@@ -24,6 +16,11 @@ export declare function dhlEcommerceTrackingUrl(number: string): string;
|
|
|
24
16
|
/** Only call for the response to the exact requested UTAPI URL. DHL can return
|
|
25
17
|
* a customer-confirmation id which differs from every queried parcel alias. */
|
|
26
18
|
export declare function parseDHLEcommerceResponse(payload: unknown): CarrierResult;
|
|
19
|
+
/** Webtrack scopes the read to the requested alias and returns that alias on
|
|
20
|
+
* each package, as its own client requires. An alias can differ from all of
|
|
21
|
+
* the package's carrier identifiers, so accept it only on one identified,
|
|
22
|
+
* unambiguous package. Never accept a bare query echo or a fabricated shell. */
|
|
23
|
+
export declare function parseDHLEcommerceWebtrackResponse(payload: unknown, trackingNumber: string): CarrierResult;
|
|
27
24
|
/**
|
|
28
25
|
* The tracking page answered with a challenge status instead of the tracking
|
|
29
26
|
* application. The name is part of the host's error metadata contract: it
|
|
@@ -37,6 +34,8 @@ export declare class DHLEcommerceSessionError extends ChallengeError {
|
|
|
37
34
|
export interface DHLEcommerceTrackerOptions extends Omit<UniversalBrowserOptions, 'signal'> {
|
|
38
35
|
budgetMs?: number;
|
|
39
36
|
recorder?: StepRecorder;
|
|
37
|
+
fetcher?: typeof fetch;
|
|
38
|
+
userAgent?: string;
|
|
40
39
|
}
|
|
41
40
|
export declare class DHLEcommerceTracker {
|
|
42
41
|
readonly options: DHLEcommerceTrackerOptions;
|
|
@@ -45,6 +44,9 @@ export declare class DHLEcommerceTracker {
|
|
|
45
44
|
constructor(options?: DHLEcommerceTrackerOptions);
|
|
46
45
|
/** One browser at a time per instance: a batch must not spawn a Chromium per parcel. */
|
|
47
46
|
fetch(trackingNumber: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
47
|
+
/** Recognition never starts the global page or spends a browser budget. */
|
|
48
|
+
recognize(trackingNumber: string, context?: TrackingContext): Promise<Recognition>;
|
|
49
|
+
private direct;
|
|
48
50
|
private browser;
|
|
49
51
|
}
|
|
50
52
|
export declare const adapter: AdapterFactory;
|
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DHL eCommerce, through
|
|
3
|
-
* page
|
|
4
|
-
*
|
|
5
|
-
* The endpoint answers every direct server request with an Akamai crypto
|
|
6
|
-
* proof-of-work challenge (HTTP 428) that plain HTTP cannot solve (verified
|
|
7
|
-
* 2026-09-10: cookie replay and visiting the page first both still return
|
|
8
|
-
* 428), and browser clearance is not transferable back to Node. The lookup
|
|
9
|
-
* therefore has a single step: a local Chromium session loads the tracking
|
|
10
|
-
* page, the site solves its own challenge and calls the API in that session,
|
|
11
|
-
* and the response to the exact requested URL is parsed.
|
|
2
|
+
* DHL eCommerce, through its anonymous Americas Webtrack API first, then
|
|
3
|
+
* the global tracking page's UTAPI for shipments outside Webtrack's scope.
|
|
12
4
|
*
|
|
13
5
|
* DHL may answer with a customer-confirmation id instead of the queried
|
|
14
6
|
* alias, so only one eCommerce shipment from that exact request URL is
|
|
15
7
|
* accepted, and the id itself is never retained.
|
|
16
8
|
*/
|
|
17
9
|
import { DateTime } from 'luxon';
|
|
18
|
-
import {
|
|
10
|
+
import { accepted, lookupBudget, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
11
|
+
import { ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
19
12
|
import { runSteps, singleFlight, takeTurn } from '../../core/runner/index.js';
|
|
20
13
|
import { countryTimeZone } from '../../core/time/index.js';
|
|
21
14
|
import { NOOP_RECORDER } from '../../core/telemetry/index.js';
|
|
22
|
-
import { clean as cleanText, UpstreamHttpError } from '../../core/transport/index.js';
|
|
15
|
+
import { clean as cleanText, fetchBounded, parseJsonBytes, UpstreamHttpError, UpstreamNetworkError, userAgentOf } from '../../core/transport/index.js';
|
|
23
16
|
import { scrapeUniversalPage } from '../../core/transport/browser.js';
|
|
24
17
|
import { isRecord } from '../../core/types.js';
|
|
25
18
|
import { stageFor, statusFor } from './status.js';
|
|
26
19
|
const PROVIDER = 'DHL eCommerce';
|
|
27
20
|
const API = 'https://www.dhl.com/utapi';
|
|
21
|
+
const WEBTRACK_API = 'https://api.dhlecs.com/webtrack/v4/tracking';
|
|
22
|
+
const DIRECT_TIMEOUT_MS = 15_000;
|
|
28
23
|
/** Statuses DHL answers with while its challenge is unsolved. */
|
|
29
24
|
const CHALLENGE_STATUSES = [401, 403, 419, 428];
|
|
30
25
|
/**
|
|
@@ -117,6 +112,82 @@ export function parseDHLEcommerceResponse(payload) {
|
|
|
117
112
|
...(deliveredAt ? { delivered_at: deliveredAt } : {}),
|
|
118
113
|
};
|
|
119
114
|
}
|
|
115
|
+
function webtrackClock(event) {
|
|
116
|
+
const date = clean(event.date, 32);
|
|
117
|
+
const time = clean(event.time, 32);
|
|
118
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)
|
|
119
|
+
|| !/^\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)?$/.test(time))
|
|
120
|
+
return null;
|
|
121
|
+
const raw = `${date}T${time}`;
|
|
122
|
+
if (!DateTime.fromISO(raw, { zone: 'UTC', setZone: true }).isValid)
|
|
123
|
+
return null;
|
|
124
|
+
const location = clean(event.location, 160);
|
|
125
|
+
const country = location.split(',').at(-1)?.trim().toUpperCase() ?? '';
|
|
126
|
+
const instant = eventTime({ timestamp: raw, location: { address: {
|
|
127
|
+
addressLocality: location, ...(/^[A-Z]{2}$/.test(country) ? { countryCode: country } : {}),
|
|
128
|
+
} } });
|
|
129
|
+
return instant ? { time: instant } : { local_time: raw };
|
|
130
|
+
}
|
|
131
|
+
/** Webtrack scopes the read to the requested alias and returns that alias on
|
|
132
|
+
* each package, as its own client requires. An alias can differ from all of
|
|
133
|
+
* the package's carrier identifiers, so accept it only on one identified,
|
|
134
|
+
* unambiguous package. Never accept a bare query echo or a fabricated shell. */
|
|
135
|
+
export function parseDHLEcommerceWebtrackResponse(payload, trackingNumber) {
|
|
136
|
+
const number = normalizeDHLEcommerceNumber(trackingNumber);
|
|
137
|
+
if (!isRecord(payload) || !Array.isArray(payload.packages) || !Number.isInteger(payload.total)
|
|
138
|
+
|| Number(payload.total) < 0 || payload.packages.length > 50 || payload.offset !== 0
|
|
139
|
+
|| (payload.errors !== undefined && (!Array.isArray(payload.errors) || payload.errors.length > 0))) {
|
|
140
|
+
throw new SchemaError(PROVIDER, 'DHL eCommerce returned an invalid Webtrack response');
|
|
141
|
+
}
|
|
142
|
+
if (payload.total === 0 && payload.packages.length === 0) {
|
|
143
|
+
// Webtrack covers the Americas network; the global endpoint may still
|
|
144
|
+
// know an item this regional endpoint does not list.
|
|
145
|
+
throw new IndeterminateError(PROVIDER, 'DHL eCommerce Webtrack does not list this shipment', { reason: 'webtrack_not_found' });
|
|
146
|
+
}
|
|
147
|
+
if (payload.total !== 1 || payload.packages.length !== 1 || !isRecord(payload.packages[0])) {
|
|
148
|
+
throw new SchemaError(PROVIDER, 'DHL eCommerce did not return one unambiguous Webtrack package');
|
|
149
|
+
}
|
|
150
|
+
const shipment = payload.packages[0];
|
|
151
|
+
const identifiers = [shipment.trackingId, shipment.packageId, shipment.dhlPackageId, shipment.deliveryConfirmationNumber];
|
|
152
|
+
const matches = (value) => typeof value === 'string'
|
|
153
|
+
&& value.toUpperCase().replace(/[\s.-]/g, '') === number;
|
|
154
|
+
if (!clean(shipment.tmiUid) || !identifiers.some((value) => clean(value))
|
|
155
|
+
|| ![shipment.trackedValue, ...identifiers].some(matches)) {
|
|
156
|
+
throw new SchemaError(PROVIDER, 'DHL eCommerce returned a different or unidentified Webtrack package');
|
|
157
|
+
}
|
|
158
|
+
const summary = clean(shipment.status);
|
|
159
|
+
if (!summary || !Array.isArray(shipment.events) || shipment.events.length > 500
|
|
160
|
+
|| shipment.events.some((event) => !isRecord(event))) {
|
|
161
|
+
throw new SchemaError(PROVIDER, 'DHL eCommerce returned invalid Webtrack tracking history');
|
|
162
|
+
}
|
|
163
|
+
const events = shipment.events.flatMap((event) => {
|
|
164
|
+
const description = clean(event.primaryEventDescription);
|
|
165
|
+
const clock = webtrackClock(event);
|
|
166
|
+
if (!description || !clock)
|
|
167
|
+
return [];
|
|
168
|
+
const stage = stageFor({ description });
|
|
169
|
+
return [{ ...clock, description: stage === 'delivered' ? 'Delivered' : description,
|
|
170
|
+
location: clean(event.location, 160), stage }];
|
|
171
|
+
}).sort((left, right) => left.time && right.time ? right.time.localeCompare(left.time) : 0).slice(0, 100);
|
|
172
|
+
const summaryStage = stageFor({ description: summary, statusCode: summary === 'Delivered' ? 'delivered' : undefined });
|
|
173
|
+
// A published scan carries finer semantics than Webtrack's coarse summary.
|
|
174
|
+
const stage = ['delivered', 'returned'].includes(summaryStage) ? summaryStage : events[0]?.stage ?? summaryStage;
|
|
175
|
+
if (events.length === 0) {
|
|
176
|
+
throw new IndeterminateError(PROVIDER, 'DHL eCommerce has not published Webtrack tracking history', { reason: 'webtrack_no_history' });
|
|
177
|
+
}
|
|
178
|
+
const expected = clean(shipment.estimatedDeliveryDate, 32);
|
|
179
|
+
const sender = clean(isRecord(shipment.sender) ? shipment.sender.name : null, 200);
|
|
180
|
+
return {
|
|
181
|
+
status: statusFor(stage), current_stage: stage,
|
|
182
|
+
last_status_text: stage === 'delivered' ? 'Delivered' : summary,
|
|
183
|
+
last_update: events.find((event) => event.time)?.time ?? null,
|
|
184
|
+
expected_delivery: !['delivered', 'returned'].includes(stage) && /^\d{4}-\d{2}-\d{2}$/.test(expected)
|
|
185
|
+
&& DateTime.fromISO(expected).isValid ? expected : null,
|
|
186
|
+
timezone: 'UTC', events,
|
|
187
|
+
...(sender ? { sender_name: sender } : {}),
|
|
188
|
+
...(stage === 'delivered' && events[0]?.stage === 'delivered' && events[0].time ? { delivered_at: events[0].time } : {}),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
120
191
|
/**
|
|
121
192
|
* The tracking page answered with a challenge status instead of the tracking
|
|
122
193
|
* application. The name is part of the host's error metadata contract: it
|
|
@@ -149,7 +220,47 @@ export class DHLEcommerceTracker {
|
|
|
149
220
|
async fetch(trackingNumber, context = {}) {
|
|
150
221
|
const number = normalizeDHLEcommerceNumber(trackingNumber);
|
|
151
222
|
const timeoutMs = this.options.timeoutMs ?? 45_000;
|
|
152
|
-
return takeTurn(this.serialize, PROVIDER, context, ({ signal, budgetMs }) => runSteps({ carrier: 'dhl-ecommerce', budgetMs: budgetMs ?? this.options.budgetMs ?? timeoutMs + 15_000, signal, recorder: this.recorder }, [
|
|
223
|
+
return takeTurn(this.serialize, PROVIDER, context, ({ signal, budgetMs }) => runSteps({ carrier: 'dhl-ecommerce', budgetMs: budgetMs ?? this.options.budgetMs ?? timeoutMs + 15_000, signal, recorder: this.recorder }, [
|
|
224
|
+
{ id: 'direct', run: (step) => this.direct(number, Math.min(DIRECT_TIMEOUT_MS, step.remainingMs), step.signal) },
|
|
225
|
+
{ id: 'browser', recovers: (error) => error instanceof ChallengeError || error instanceof UpstreamNetworkError
|
|
226
|
+
|| error instanceof UpstreamHttpError && error.status >= 500
|
|
227
|
+
|| error instanceof IndeterminateError && ['webtrack_not_found', 'webtrack_no_history'].includes(error.reason ?? ''),
|
|
228
|
+
run: (step) => this.browser(number, Math.max(1, Math.floor(Math.min(timeoutMs, step.remainingMs))), step.signal) },
|
|
229
|
+
]));
|
|
230
|
+
}
|
|
231
|
+
/** Recognition never starts the global page or spends a browser budget. */
|
|
232
|
+
async recognize(trackingNumber, context = {}) {
|
|
233
|
+
return recognizeFromLookup(async () => {
|
|
234
|
+
const number = normalizeDHLEcommerceNumber(trackingNumber);
|
|
235
|
+
const budget = lookupBudget(context, DIRECT_TIMEOUT_MS, PROVIDER);
|
|
236
|
+
try {
|
|
237
|
+
return await this.direct(number, Math.min(DIRECT_TIMEOUT_MS, budget.remainingMs()), budget.signal);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
// A clean regional miss is unknown to this recognition check; it is
|
|
241
|
+
// never a claim that the global DHL network does not know the number.
|
|
242
|
+
if (error instanceof IndeterminateError && error.reason === 'webtrack_not_found')
|
|
243
|
+
throw new NotFoundError(PROVIDER);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}, () => accepted(() => normalizeDHLEcommerceNumber(trackingNumber)));
|
|
247
|
+
}
|
|
248
|
+
async direct(number, timeoutMs, signal) {
|
|
249
|
+
try {
|
|
250
|
+
const { response, bytes } = await fetchBounded(WEBTRACK_API, { method: 'POST', signal,
|
|
251
|
+
headers: { Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': userAgentOf(this.options.userAgent) },
|
|
252
|
+
body: JSON.stringify({ trackedValue: number, offset: 0, locale: 'en-US' }),
|
|
253
|
+
}, { provider: PROVIDER, timeoutMs, maxBytes: 1_000_000, fetcher: this.options.fetcher, allowHttpStatuses: [404, 410] });
|
|
254
|
+
if (!response.ok)
|
|
255
|
+
throw new IndeterminateError(PROVIDER, 'DHL eCommerce Webtrack endpoint is unavailable');
|
|
256
|
+
return parseDHLEcommerceWebtrackResponse(parseJsonBytes(bytes, PROVIDER), number);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (error instanceof UpstreamHttpError && CHALLENGE_STATUSES.includes(error.status)) {
|
|
260
|
+
throw new DHLEcommerceSessionError(error.status, { cause: error });
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
153
264
|
}
|
|
154
265
|
async browser(number, timeoutMs, signal) {
|
|
155
266
|
try {
|
|
@@ -170,10 +281,12 @@ export class DHLEcommerceTracker {
|
|
|
170
281
|
export const adapter = (environment) => {
|
|
171
282
|
const tracker = new DHLEcommerceTracker({
|
|
172
283
|
executablePath: environment.browserExecutablePath ?? undefined, recorder: environment.recorder,
|
|
284
|
+
fetcher: environment.fetcher, userAgent: environment.userAgent,
|
|
173
285
|
});
|
|
174
286
|
return {
|
|
175
287
|
id: 'dhl-ecommerce', recordsSteps: true,
|
|
176
|
-
steps: ['browser'],
|
|
288
|
+
steps: ['direct', 'browser'],
|
|
177
289
|
track: (input, context) => tracker.fetch(input.number, context),
|
|
290
|
+
recognize: (number, context) => tracker.recognize(number, context),
|
|
178
291
|
};
|
|
179
292
|
};
|
|
@@ -10,7 +10,7 @@ export declare class DPDChallengeError extends ChallengeError {
|
|
|
10
10
|
/**
|
|
11
11
|
* The guest API answered, but the answer proves nothing about the shipment
|
|
12
12
|
* (unreachable, malformed, unauthenticated, or an HTTP status the guest flow
|
|
13
|
-
* handles itself). The rendered page is allowed to recover from it.
|
|
13
|
+
* handles itself, except HTTP 503). The rendered page is allowed to recover from it.
|
|
14
14
|
*/
|
|
15
15
|
export declare class DPDAPIError extends IndeterminateError {
|
|
16
16
|
constructor(message: string, options?: CarrierErrorOptions);
|
|
@@ -2,7 +2,7 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { load } from 'cheerio';
|
|
4
4
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
5
|
-
import { BudgetExceededError, carrierErrorKind, ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
5
|
+
import { BudgetExceededError, carrierErrorKind, ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
6
6
|
import { runSteps } from '../../core/runner/index.js';
|
|
7
7
|
import { isoTime, explicitOffsetTime, zonedTime } from '../../core/time/index.js';
|
|
8
8
|
import { TrawlClient, decodeText, fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
|
|
@@ -19,7 +19,8 @@ import { API_LABELS, PROOF_OF_DELIVERY_SCAN, apiStage, apiStatus, scanStage, wor
|
|
|
19
19
|
// `continueWithoutVerification=true`. A rejected postcode (HTTP 400) is
|
|
20
20
|
// retried once without verification and reported as unverified rather than
|
|
21
21
|
// failing the lookup.
|
|
22
|
-
// - The consignee web page is the fallback when the guest API is inconclusive
|
|
22
|
+
// - The consignee web page is the fallback when the guest API is inconclusive,
|
|
23
|
+
// except HTTP 503: the service is down, irrespective of the parcel number.
|
|
23
24
|
// It sits behind Cloudflare, so it is fetched through the browser service's
|
|
24
25
|
// legacy command API when one is configured and directly otherwise.
|
|
25
26
|
const TRACKING_BASE = 'https://www.dpdgroup.com/ch/mydpd/my-parcels/incoming';
|
|
@@ -54,7 +55,7 @@ export class DPDChallengeError extends ChallengeError {
|
|
|
54
55
|
/**
|
|
55
56
|
* The guest API answered, but the answer proves nothing about the shipment
|
|
56
57
|
* (unreachable, malformed, unauthenticated, or an HTTP status the guest flow
|
|
57
|
-
* handles itself). The rendered page is allowed to recover from it.
|
|
58
|
+
* handles itself, except HTTP 503). The rendered page is allowed to recover from it.
|
|
58
59
|
*/
|
|
59
60
|
export class DPDAPIError extends IndeterminateError {
|
|
60
61
|
constructor(message, options) {
|
|
@@ -778,6 +779,10 @@ export class DPDTracker {
|
|
|
778
779
|
catch (error) {
|
|
779
780
|
throw new DPDAPIError('DPD guest API is unreachable', { cause: error });
|
|
780
781
|
}
|
|
782
|
+
// A 503 describes DPD's availability, so the page must not turn it into
|
|
783
|
+
// an answer about the parcel or hold up the caller's other lookups.
|
|
784
|
+
if (result.response.status === 503)
|
|
785
|
+
throw new UpstreamHttpError('DPD guest API', 503);
|
|
781
786
|
if (!result.response.ok)
|
|
782
787
|
throw new DPDAPIHttpError(result.response.status);
|
|
783
788
|
let payload;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
|
|
2
2
|
import type { CarrierResult } from '../../core/result/index.js';
|
|
3
3
|
import type { StepRecorder } from '../../core/telemetry/index.js';
|
|
4
4
|
export declare function normalizePostiTrackingNumber(raw: string): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
1
2
|
import { CarrierError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
2
3
|
import { runSteps } from '../../core/runner/index.js';
|
|
3
4
|
import { explicitOffsetTime } from '../../core/time/index.js';
|
|
@@ -191,5 +192,9 @@ export class PostiTracker {
|
|
|
191
192
|
}
|
|
192
193
|
export const adapter = (environment) => {
|
|
193
194
|
const tracker = new PostiTracker({ fetcher: environment.fetcher, recorder: environment.recorder });
|
|
194
|
-
return {
|
|
195
|
+
return {
|
|
196
|
+
id: 'posti', recordsSteps: true, steps: ['direct', 'refresh'],
|
|
197
|
+
track: (input, context) => tracker.fetch(input.number, context),
|
|
198
|
+
recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizePostiTrackingNumber(number))),
|
|
199
|
+
};
|
|
195
200
|
};
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
"steps": [
|
|
19
19
|
"direct",
|
|
20
20
|
"refresh"
|
|
21
|
-
]
|
|
21
|
+
],
|
|
22
|
+
"recognition": {
|
|
23
|
+
"rank": 27
|
|
24
|
+
}
|
|
22
25
|
},
|
|
23
26
|
"capabilities": [
|
|
24
27
|
"history",
|
|
@@ -65,5 +68,13 @@
|
|
|
65
68
|
"path": "^/(?:[a-z]{2}/)?tracking/([^/?#]+)(?:/[0-9]+)?/?$"
|
|
66
69
|
}
|
|
67
70
|
],
|
|
68
|
-
"detection": [
|
|
71
|
+
"detection": [
|
|
72
|
+
{
|
|
73
|
+
"id": "posti-1",
|
|
74
|
+
"pattern": "^[A-Z]{2}\\d{9}FI$",
|
|
75
|
+
"confidence": "low",
|
|
76
|
+
"preferred": true,
|
|
77
|
+
"checksum": "s10"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
69
80
|
}
|
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"carrier": "posti",
|
|
3
3
|
"records": [
|
|
4
|
+
{
|
|
5
|
+
"number": "CP123456785FI",
|
|
6
|
+
"role": "shipment",
|
|
7
|
+
"evidence": "synthetic",
|
|
8
|
+
"derivedFrom": "official_shape",
|
|
9
|
+
"expect": {
|
|
10
|
+
"carrier": "intl-post",
|
|
11
|
+
"confidence": "high"
|
|
12
|
+
},
|
|
13
|
+
"note": "Invented checksum-valid Finnish-issued S10 number. The issuer makes Posti a recognition candidate; offline detection stays with the unknown postal carrier."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"number": "CP123456789FI",
|
|
17
|
+
"role": "negative",
|
|
18
|
+
"evidence": "synthetic",
|
|
19
|
+
"derivedFrom": "official_shape",
|
|
20
|
+
"expect": {
|
|
21
|
+
"carrier": "unknown",
|
|
22
|
+
"confidence": "low",
|
|
23
|
+
"candidates": [
|
|
24
|
+
"chronopost"
|
|
25
|
+
],
|
|
26
|
+
"not": [
|
|
27
|
+
"posti"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"note": "Invented Finnish-shaped identifier with a failed S10 check digit. It cannot suggest Posti."
|
|
31
|
+
},
|
|
4
32
|
{
|
|
5
33
|
"number": "00264300487130023858",
|
|
6
34
|
"role": "shipment",
|
|
@@ -12,7 +12,8 @@ export interface RecognitionCandidate {
|
|
|
12
12
|
* The low-confidence candidates worth asking, best first: the carrier a
|
|
13
13
|
* universal provider named, then the ones number evidence backs, then the
|
|
14
14
|
* catalog's popularity rank. Only carriers that declare `tracking.recognition`
|
|
15
|
-
* qualify. A high-confidence
|
|
15
|
+
* qualify. A high-confidence dedicated carrier needs no recognition; the
|
|
16
|
+
* unknown postal carrier still needs a direct carrier to confirm it.
|
|
16
17
|
*/
|
|
17
18
|
export declare function recognitionCandidates(number: string, options?: {
|
|
18
19
|
hint?: string;
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { CARRIER_RECOGNITION_RANKS } from '../../generated/recognition.js';
|
|
11
11
|
import { detectCarrierMatch } from '../detection/detect.js';
|
|
12
|
-
import {
|
|
12
|
+
import { normalizeTrackingNumber } from '../detection/normalize.js';
|
|
13
|
+
import { AUTOMATIC_CARRIER_IDS, CARRIER_DEFINITIONS, carrierAdapter, requiredRequirements } from './definitions.js';
|
|
13
14
|
import { carrierBrand } from './networks.js';
|
|
14
15
|
/** Carriers asked at once. */
|
|
15
16
|
export const MAX_RECOGNITIONS = 5;
|
|
@@ -17,24 +18,43 @@ export const MAX_RECOGNITIONS = 5;
|
|
|
17
18
|
* The low-confidence candidates worth asking, best first: the carrier a
|
|
18
19
|
* universal provider named, then the ones number evidence backs, then the
|
|
19
20
|
* catalog's popularity rank. Only carriers that declare `tracking.recognition`
|
|
20
|
-
* qualify. A high-confidence
|
|
21
|
+
* qualify. A high-confidence dedicated carrier needs no recognition; the
|
|
22
|
+
* unknown postal carrier still needs a direct carrier to confirm it.
|
|
21
23
|
*/
|
|
22
24
|
export function recognitionCandidates(number, options = {}) {
|
|
23
25
|
const detected = detectCarrierMatch(number);
|
|
24
|
-
|
|
26
|
+
const unknownPostalCarrier = detected.carrier === 'intl-post';
|
|
27
|
+
if (detected.confidence !== 'low' && !unknownPostalCarrier)
|
|
25
28
|
return [];
|
|
29
|
+
// The high-confidence S10 fallback hides low-confidence carrier rules from
|
|
30
|
+
// detection. Recover those candidates without assigning the issuer's
|
|
31
|
+
// postal carrier as the deliverer: each candidate must confirm this number.
|
|
32
|
+
// intl-post already checked the S10 checksum, so only S10 or shape-only rules
|
|
33
|
+
// can contribute here.
|
|
34
|
+
const normalized = normalizeTrackingNumber(number);
|
|
35
|
+
const postalMatches = unknownPostalCarrier ? Object.entries(CARRIER_DEFINITIONS)
|
|
36
|
+
.flatMap(([carrier, definition]) => {
|
|
37
|
+
const rule = definition.detectionRules.find((candidate) => candidate.confidence === 'low'
|
|
38
|
+
&& (!candidate.checksum || candidate.checksum === 's10')
|
|
39
|
+
&& new RegExp(candidate.pattern).test(normalized)
|
|
40
|
+
&& (!candidate.rawPattern || new RegExp(candidate.rawPattern).test(number.trim().toUpperCase())));
|
|
41
|
+
return rule ? [{ carrier, preferred: rule.preferred === true }] : [];
|
|
42
|
+
}) : [];
|
|
43
|
+
const candidates = unknownPostalCarrier ? postalMatches.map(({ carrier }) => carrier) : detected.candidates;
|
|
44
|
+
const preferred = unknownPostalCarrier
|
|
45
|
+
? postalMatches.filter((match) => match.preferred).map(({ carrier }) => carrier) : detected.preferred;
|
|
26
46
|
// A carrier the number points to but that cannot be asked (DPD France) keeps
|
|
27
47
|
// its brand's other networks out: DPD's guest API also answers for DPD
|
|
28
48
|
// France parcels, and would file one under DPD Switzerland.
|
|
29
|
-
const shadowed = new Set(
|
|
49
|
+
const shadowed = new Set(preferred
|
|
30
50
|
.filter((carrier) => CARRIER_RECOGNITION_RANKS[carrier] === undefined)
|
|
31
51
|
.map((carrier) => carrierBrand(carrier)).filter(Boolean));
|
|
32
52
|
const score = (carrier) => [
|
|
33
53
|
carrier === options.hint ? 1 : 0,
|
|
34
|
-
|
|
54
|
+
preferred.includes(carrier) ? 1 : 0,
|
|
35
55
|
CARRIER_RECOGNITION_RANKS[carrier] ?? 0,
|
|
36
56
|
];
|
|
37
|
-
return
|
|
57
|
+
return candidates
|
|
38
58
|
.filter((carrier) => CARRIER_RECOGNITION_RANKS[carrier] !== undefined && AUTOMATIC_CARRIER_IDS.has(carrier)
|
|
39
59
|
&& carrierAdapter(carrier) !== 'universal' && !shadowed.has(carrierBrand(carrier)) && !options.skip?.(carrier))
|
|
40
60
|
.map((carrier) => ({ carrier, score: score(carrier) }))
|
|
@@ -43,7 +63,7 @@ export function recognitionCandidates(number, options = {}) {
|
|
|
43
63
|
.map(({ carrier }) => ({
|
|
44
64
|
carrier,
|
|
45
65
|
needsInput: requiredRequirements(carrier, number)[0]?.field ?? null,
|
|
46
|
-
preferred:
|
|
66
|
+
preferred: preferred.includes(carrier),
|
|
47
67
|
}));
|
|
48
68
|
}
|
|
49
69
|
/** The carriers the detect route asks about a number, best first; empty when none can answer. */
|
|
@@ -2,7 +2,8 @@ import { carrierBrand } from '../catalog/networks.js';
|
|
|
2
2
|
import { CARRIER_RECOGNITION_RANKS } from '../../generated/recognition.js';
|
|
3
3
|
export { MAX_RECOGNITIONS, recognitionCandidates, } from '../catalog/recognition.js';
|
|
4
4
|
/**
|
|
5
|
-
* Carrier recognition: when a number's shape fits several carriers
|
|
5
|
+
* Carrier recognition: when a number's shape fits several carriers or names
|
|
6
|
+
* only an international postal item, ask the
|
|
6
7
|
* ones that can answer cheaply whether they know it. The Add sheet runs it
|
|
7
8
|
* while the user is still in the form, the first sync runs it again after
|
|
8
9
|
* saving, and routing keeps retrying it while the filed carrier cannot track
|
package/dist/data/catalog.json
CHANGED
|
@@ -468,7 +468,8 @@
|
|
|
468
468
|
"timezone": "UTC",
|
|
469
469
|
"tracking": {
|
|
470
470
|
"mode": "automatic",
|
|
471
|
-
"adapter": "dhl-ecommerce"
|
|
471
|
+
"adapter": "dhl-ecommerce",
|
|
472
|
+
"recognitionRank": 23
|
|
472
473
|
},
|
|
473
474
|
"canaryUrl": "https://www.dhl.com/ch-en/home/tracking.html",
|
|
474
475
|
"trackingUrlTemplate": "https://www.dhl.com/ch-en/home/tracking.html?tracking-id={trackingNumber}&submit=1",
|
|
@@ -1973,7 +1974,8 @@
|
|
|
1973
1974
|
"timezone": "Europe/Helsinki",
|
|
1974
1975
|
"tracking": {
|
|
1975
1976
|
"mode": "automatic",
|
|
1976
|
-
"adapter": "posti"
|
|
1977
|
+
"adapter": "posti",
|
|
1978
|
+
"recognitionRank": 27
|
|
1977
1979
|
},
|
|
1978
1980
|
"canaryUrl": "https://www.posti.fi/en/tracking",
|
|
1979
1981
|
"trackingUrlTemplate": "https://www.posti.fi/en/tracking/{trackingNumber}",
|
|
@@ -1985,7 +1987,14 @@
|
|
|
1985
1987
|
"path": "^/(?:[a-z]{2}/)?tracking/([^/?#]+)(?:/[0-9]+)?/?$"
|
|
1986
1988
|
}
|
|
1987
1989
|
],
|
|
1988
|
-
"detectionRules": [
|
|
1990
|
+
"detectionRules": [
|
|
1991
|
+
{
|
|
1992
|
+
"pattern": "^[A-Z]{2}\\d{9}FI$",
|
|
1993
|
+
"confidence": "low",
|
|
1994
|
+
"checksum": "s10",
|
|
1995
|
+
"preferred": true
|
|
1996
|
+
}
|
|
1997
|
+
]
|
|
1989
1998
|
},
|
|
1990
1999
|
"correos-express": {
|
|
1991
2000
|
"displayName": "Correos Express",
|
package/dist/facade/index.js
CHANGED
|
@@ -129,7 +129,7 @@ export function createTracker(options = {}) {
|
|
|
129
129
|
if (typeof carrier !== 'string')
|
|
130
130
|
throw new TypeError('Invalid carrier');
|
|
131
131
|
carrierDefinition(carrier);
|
|
132
|
-
if (!input.carrier &&
|
|
132
|
+
if (!input.carrier && recognitionCandidates(number).length > 0) {
|
|
133
133
|
try {
|
|
134
134
|
const answer = await recognize(number, { signal, budgetMs: Math.min(10_000, ms) });
|
|
135
135
|
if (answer.carrier)
|
|
@@ -308,6 +308,7 @@ export declare const CARRIER_CATALOG: {
|
|
|
308
308
|
readonly tracking: {
|
|
309
309
|
readonly mode: "automatic";
|
|
310
310
|
readonly adapter: "dhl-ecommerce";
|
|
311
|
+
readonly recognitionRank: 23;
|
|
311
312
|
};
|
|
312
313
|
readonly canaryUrl: "https://www.dhl.com/ch-en/home/tracking.html";
|
|
313
314
|
readonly trackingUrlTemplate: "https://www.dhl.com/ch-en/home/tracking.html?tracking-id={trackingNumber}&submit=1";
|
|
@@ -1293,6 +1294,7 @@ export declare const CARRIER_CATALOG: {
|
|
|
1293
1294
|
readonly tracking: {
|
|
1294
1295
|
readonly mode: "automatic";
|
|
1295
1296
|
readonly adapter: "posti";
|
|
1297
|
+
readonly recognitionRank: 27;
|
|
1296
1298
|
};
|
|
1297
1299
|
readonly canaryUrl: "https://www.posti.fi/en/tracking";
|
|
1298
1300
|
readonly trackingUrlTemplate: "https://www.posti.fi/en/tracking/{trackingNumber}";
|
|
@@ -1300,7 +1302,12 @@ export declare const CARRIER_CATALOG: {
|
|
|
1300
1302
|
readonly domains: readonly ["posti.fi"];
|
|
1301
1303
|
readonly path: "^/(?:[a-z]{2}/)?tracking/([^/?#]+)(?:/[0-9]+)?/?$";
|
|
1302
1304
|
}];
|
|
1303
|
-
readonly detectionRules: readonly [
|
|
1305
|
+
readonly detectionRules: readonly [{
|
|
1306
|
+
readonly pattern: "^[A-Z]{2}\\d{9}FI$";
|
|
1307
|
+
readonly confidence: "low";
|
|
1308
|
+
readonly checksum: "s10";
|
|
1309
|
+
readonly preferred: true;
|
|
1310
|
+
}];
|
|
1304
1311
|
};
|
|
1305
1312
|
readonly "correos-express": {
|
|
1306
1313
|
readonly displayName: "Correos Express";
|
|
@@ -469,7 +469,8 @@ export const CARRIER_CATALOG = {
|
|
|
469
469
|
"timezone": "UTC",
|
|
470
470
|
"tracking": {
|
|
471
471
|
"mode": "automatic",
|
|
472
|
-
"adapter": "dhl-ecommerce"
|
|
472
|
+
"adapter": "dhl-ecommerce",
|
|
473
|
+
"recognitionRank": 23
|
|
473
474
|
},
|
|
474
475
|
"canaryUrl": "https://www.dhl.com/ch-en/home/tracking.html",
|
|
475
476
|
"trackingUrlTemplate": "https://www.dhl.com/ch-en/home/tracking.html?tracking-id={trackingNumber}&submit=1",
|
|
@@ -1974,7 +1975,8 @@ export const CARRIER_CATALOG = {
|
|
|
1974
1975
|
"timezone": "Europe/Helsinki",
|
|
1975
1976
|
"tracking": {
|
|
1976
1977
|
"mode": "automatic",
|
|
1977
|
-
"adapter": "posti"
|
|
1978
|
+
"adapter": "posti",
|
|
1979
|
+
"recognitionRank": 27
|
|
1978
1980
|
},
|
|
1979
1981
|
"canaryUrl": "https://www.posti.fi/en/tracking",
|
|
1980
1982
|
"trackingUrlTemplate": "https://www.posti.fi/en/tracking/{trackingNumber}",
|
|
@@ -1986,7 +1988,14 @@ export const CARRIER_CATALOG = {
|
|
|
1986
1988
|
"path": "^/(?:[a-z]{2}/)?tracking/([^/?#]+)(?:/[0-9]+)?/?$"
|
|
1987
1989
|
}
|
|
1988
1990
|
],
|
|
1989
|
-
"detectionRules": [
|
|
1991
|
+
"detectionRules": [
|
|
1992
|
+
{
|
|
1993
|
+
"pattern": "^[A-Z]{2}\\d{9}FI$",
|
|
1994
|
+
"confidence": "low",
|
|
1995
|
+
"checksum": "s10",
|
|
1996
|
+
"preferred": true
|
|
1997
|
+
}
|
|
1998
|
+
]
|
|
1990
1999
|
},
|
|
1991
2000
|
"correos-express": {
|
|
1992
2001
|
"displayName": "Correos Express",
|
|
@@ -11,6 +11,7 @@ export const CARRIER_RECOGNITION_RANKS = {
|
|
|
11
11
|
"colis-prive": 30,
|
|
12
12
|
"correos-express": 36,
|
|
13
13
|
"ctt-express": 35,
|
|
14
|
+
"dhl-ecommerce": 23,
|
|
14
15
|
"dpd": 70,
|
|
15
16
|
"gls-ch": 60,
|
|
16
17
|
"gls-de": 50,
|
|
@@ -21,6 +22,7 @@ export const CARRIER_RECOGNITION_RANKS = {
|
|
|
21
22
|
"ninja-van": 28,
|
|
22
23
|
"nz-post": 32,
|
|
23
24
|
"poczta-polska": 31,
|
|
25
|
+
"posti": 27,
|
|
24
26
|
"postlogistics": 26,
|
|
25
27
|
"relais-colis": 25,
|
|
26
28
|
"seur": 43,
|
package/dist/server/openapi.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-main.322",
|
|
4
4
|
"description": "Self-hosted parcel tracking: carrier detection, dedicated scrapers and optional universal providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|