universal-parcel-scraper 0.43.0 → 0.44.0-main.411
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 +1 -1
- package/data/catalog.json +4 -4
- package/data/checksum-vectors.json +63 -0
- package/data/detection-golden.json +43 -0
- package/dist/browser/scraper.js +18 -13
- package/dist/carriers/chronopost/carrier.json +4 -4
- package/dist/carriers/chronopost/numbers.json +56 -0
- package/dist/carriers/chronopost/parser.js +29 -3
- package/dist/carriers/dpd/adapter.js +39 -2
- package/dist/carriers/intl-post/carrier.json +1 -1
- package/dist/carriers/intl-post/numbers.json +18 -0
- package/dist/carriers/la-poste/carrier.json +1 -1
- package/dist/carriers/paack/adapter.d.ts +2 -2
- package/dist/carriers/paack/adapter.js +88 -44
- package/dist/carriers/paack/status.d.ts +10 -0
- package/dist/carriers/paack/status.js +27 -14
- package/dist/carriers/paack/statuses.json +44 -4
- package/dist/carriers/spring-gds/carrier.json +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/core/adapter/index.d.ts +1 -1
- package/dist/data/catalog.json +4 -4
- package/dist/facade/index.d.ts +1 -1
- package/dist/generated/catalog.d.ts +4 -4
- package/dist/generated/catalog.js +4 -4
- package/dist/providers/parcelsapp/adapter.d.ts +1 -1
- package/dist/providers/parcelsapp/adapter.js +11 -16
- package/dist/providers/parcelsapp/http.d.ts +2 -4
- package/dist/providers/parcelsapp/http.js +3 -13
- package/dist/providers/plan.d.ts +2 -2
- package/dist/providers/plan.js +17 -5
- package/dist/providers/shared/hints.d.ts +9 -1
- package/dist/providers/shared/hints.js +18 -3
- package/dist/providers/shared/scans.js +2 -0
- package/dist/providers/universal.d.ts +9 -9
- package/dist/providers/universal.js +11 -5
- package/dist/server/openapi.json +3 -2
- package/package.json +1 -1
|
@@ -24,6 +24,24 @@
|
|
|
24
24
|
"confidence": "high"
|
|
25
25
|
},
|
|
26
26
|
"note": "Made-up number that follows the published format."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"number": "RR123456785TS",
|
|
30
|
+
"role": "negative",
|
|
31
|
+
"evidence": "synthetic",
|
|
32
|
+
"derivedFrom": "invented",
|
|
33
|
+
"shapeConfirmed": "2026-10-08",
|
|
34
|
+
"expect": {
|
|
35
|
+
"carrier": "unknown",
|
|
36
|
+
"confidence": "low",
|
|
37
|
+
"candidates": [
|
|
38
|
+
"chronopost"
|
|
39
|
+
],
|
|
40
|
+
"not": [
|
|
41
|
+
"intl-post"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"note": "Invented number whose S10 check digit passes but whose TS suffix is a Chronopost code rather than an issuing country, so it is not international mail."
|
|
27
45
|
}
|
|
28
46
|
]
|
|
29
47
|
}
|
|
@@ -3,8 +3,8 @@ import type { CarrierResult } from '../../core/result/index.js';
|
|
|
3
3
|
export declare function normalizePaackTrackingNumber(raw: string): string;
|
|
4
4
|
export declare function normalizePaackPostcode(raw: string): string;
|
|
5
5
|
export declare function paackTrackingUrl(rawTrackingNumber: string, rawPostcode: string): string;
|
|
6
|
-
export declare function parsePaackTrackingResponse(payload: unknown,
|
|
7
|
-
export declare function parsePaackTrackingHtml(html: string,
|
|
6
|
+
export declare function parsePaackTrackingResponse(payload: unknown, rawPostcode: string): CarrierResult;
|
|
7
|
+
export declare function parsePaackTrackingHtml(html: string, rawPostcode: string): CarrierResult;
|
|
8
8
|
export interface PaackTrackerOptions {
|
|
9
9
|
timeoutMs?: number;
|
|
10
10
|
/** Test seam; production uses the global fetch. */
|
|
@@ -5,17 +5,21 @@
|
|
|
5
5
|
* number and the delivery postcode returns server-rendered HTML whose
|
|
6
6
|
* `window.__remixContext` already holds the loader response for the tracking
|
|
7
7
|
* route. Reading that embedded payload avoids a second, undocumented API call
|
|
8
|
-
* and gives the same data the page itself renders.
|
|
9
|
-
* response echoes is verified before anything else is read.
|
|
8
|
+
* and gives the same data the page itself renders.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Paack answers the number and postcode as a pair: a wrong pair is redirected
|
|
11
|
+
* back to the form with `err=true`, or answered with an explicit "order not
|
|
12
|
+
* found" message, and both become a clean not-found. A tracking page is
|
|
13
|
+
* therefore the order for that pair. It does not echo the number when that is
|
|
14
|
+
* a label barcode (`external_id` is then the retailer's own reference), but it
|
|
15
|
+
* does echo the delivery postcode, which must agree with the requested one.
|
|
13
16
|
*
|
|
14
17
|
* Privacy: the loader payload carries the retailer, the recipient's name,
|
|
15
18
|
* e-mail, phone and address, and per-event `variables` that repeat them.
|
|
16
19
|
* `parse()` copies nothing from those objects: each event is rebuilt from its
|
|
17
20
|
* timestamp, our own description and the mapped stage, and the only order-level
|
|
18
|
-
* fields read are the
|
|
21
|
+
* fields read are the delivery postcode, compared and never copied, and the
|
|
22
|
+
* delivery window.
|
|
19
23
|
*/
|
|
20
24
|
import { load } from 'cheerio';
|
|
21
25
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
@@ -27,6 +31,8 @@ const TRACKING_ENDPOINT = 'https://mydeliveries.paack.app/tracking/order';
|
|
|
27
31
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
28
32
|
const MAX_RESPONSE_BYTES = 2_000_000;
|
|
29
33
|
const MAX_EVENTS = 100;
|
|
34
|
+
const MAX_REDIRECTS = 1;
|
|
35
|
+
const PAACK_HOST = /(?:^|\.)paack\.(?:app|co)$/;
|
|
30
36
|
const NOT_FOUND_PATTERN = /order not found|incorrect order number|commande introuvable|pedido no encontrado/i;
|
|
31
37
|
const NOT_FOUND_PAGE_PATTERN = /order not found|incorrect order number or postal code|commande introuvable|pedido no encontrado/i;
|
|
32
38
|
/**
|
|
@@ -122,16 +128,49 @@ export function normalizePaackPostcode(raw) {
|
|
|
122
128
|
}
|
|
123
129
|
return value.replace(/\s+/g, '');
|
|
124
130
|
}
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
const postcode = normalizePaackPostcode(rawPostcode);
|
|
128
|
-
const url = new URL(TRACKING_ENDPOINT);
|
|
131
|
+
function trackingOrderUrl(base, trackingNumber, postcode) {
|
|
132
|
+
const url = new URL(base);
|
|
129
133
|
url.searchParams.set('tracking_number', trackingNumber);
|
|
130
134
|
url.searchParams.set('postal_code', postcode);
|
|
131
135
|
return url.toString();
|
|
132
136
|
}
|
|
133
|
-
export function
|
|
134
|
-
|
|
137
|
+
export function paackTrackingUrl(rawTrackingNumber, rawPostcode) {
|
|
138
|
+
return trackingOrderUrl(TRACKING_ENDPOINT, normalizePaackTrackingNumber(rawTrackingNumber), normalizePaackPostcode(rawPostcode));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Where a redirect from the lookup leads. `err=true` is Paack's answer to a
|
|
142
|
+
* wrong pair. The same lookup on another Paack host is followed: the page is
|
|
143
|
+
* moving from mydeliveries.paack.app to paack.co, and the old host forwards
|
|
144
|
+
* some lookups there before checking the postcode. Anything else proves
|
|
145
|
+
* nothing about the parcel.
|
|
146
|
+
*/
|
|
147
|
+
function redirectTarget(location, from, trackingNumber, postcode) {
|
|
148
|
+
if (!location)
|
|
149
|
+
return null;
|
|
150
|
+
let target;
|
|
151
|
+
try {
|
|
152
|
+
target = new URL(location, from);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (target.protocol !== 'https:' || !PAACK_HOST.test(target.hostname))
|
|
158
|
+
return null;
|
|
159
|
+
if (target.searchParams.get('err') === 'true')
|
|
160
|
+
return { notFound: true };
|
|
161
|
+
const echoes = (name, value) => ((target.searchParams.get(name) ?? '').toLocaleUpperCase('en-US').replace(/\s/g, '') === value);
|
|
162
|
+
if (!target.pathname.endsWith('/tracking/order')
|
|
163
|
+
|| !echoes('tracking_number', trackingNumber)
|
|
164
|
+
|| !echoes('postal_code', postcode))
|
|
165
|
+
return null;
|
|
166
|
+
return { url: trackingOrderUrl(`${target.origin}${target.pathname}`, trackingNumber, postcode) };
|
|
167
|
+
}
|
|
168
|
+
/** Letters and digits only, so "SW1A 1AA" and "4445-027" compare by content. */
|
|
169
|
+
function postcodeKey(value) {
|
|
170
|
+
return value.toLocaleUpperCase('en-US').replace(/[^A-Z0-9]/g, '');
|
|
171
|
+
}
|
|
172
|
+
export function parsePaackTrackingResponse(payload, rawPostcode) {
|
|
173
|
+
const postcode = normalizePaackPostcode(rawPostcode);
|
|
135
174
|
if (NOT_FOUND_PATTERN.test(errorText(payload)))
|
|
136
175
|
throw new NotFoundError('Paack');
|
|
137
176
|
const route = routeData(payload);
|
|
@@ -140,17 +179,10 @@ export function parsePaackTrackingResponse(payload, rawTrackingNumber) {
|
|
|
140
179
|
const order = route.orderTrackData;
|
|
141
180
|
if (!isRecord(order))
|
|
142
181
|
throw new SchemaError('Paack', 'Paack returned incomplete tracking details');
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
responseNumber = normalizePaackTrackingNumber(order.external_id);
|
|
148
|
-
}
|
|
149
|
-
catch (cause) {
|
|
150
|
-
// The echoed id is the provider's value, not the caller's input.
|
|
151
|
-
throw new SchemaError('Paack', 'Paack returned an invalid shipment number', { cause });
|
|
152
|
-
}
|
|
153
|
-
if (responseNumber !== trackingNumber) {
|
|
182
|
+
// The number is not echoed when it is a label barcode; the postcode is.
|
|
183
|
+
const address = isRecord(order.delivery_address) ? order.delivery_address : null;
|
|
184
|
+
const echoedPostcode = typeof address?.post_code === 'string' ? postcodeKey(address.post_code) : '';
|
|
185
|
+
if (echoedPostcode && echoedPostcode !== postcodeKey(postcode)) {
|
|
154
186
|
throw new SchemaError('Paack', 'Paack returned a different shipment');
|
|
155
187
|
}
|
|
156
188
|
const rawEvents = Array.isArray(route.eventList) ? route.eventList : [];
|
|
@@ -203,13 +235,13 @@ export function parsePaackTrackingResponse(payload, rawTrackingNumber) {
|
|
|
203
235
|
events,
|
|
204
236
|
};
|
|
205
237
|
}
|
|
206
|
-
export function parsePaackTrackingHtml(html,
|
|
238
|
+
export function parsePaackTrackingHtml(html, rawPostcode) {
|
|
207
239
|
// An empty body proves nothing about the shipment, so it stays indeterminate.
|
|
208
240
|
if (!html.trim())
|
|
209
241
|
throw new IndeterminateError('Paack', 'Paack returned an empty tracking response');
|
|
210
242
|
if (NOT_FOUND_PAGE_PATTERN.test(html))
|
|
211
243
|
throw new NotFoundError('Paack');
|
|
212
|
-
return parsePaackTrackingResponse(remixContext(html),
|
|
244
|
+
return parsePaackTrackingResponse(remixContext(html), rawPostcode);
|
|
213
245
|
}
|
|
214
246
|
export class PaackTracker {
|
|
215
247
|
timeoutMs;
|
|
@@ -227,27 +259,39 @@ export class PaackTracker {
|
|
|
227
259
|
const trackingNumber = normalizePaackTrackingNumber(rawTrackingNumber);
|
|
228
260
|
const postcode = normalizePaackPostcode(rawPostcode);
|
|
229
261
|
const budget = lookupBudget(context, this.timeoutMs);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
262
|
+
let url = paackTrackingUrl(trackingNumber, postcode);
|
|
263
|
+
for (let redirects = 0;; redirects += 1) {
|
|
264
|
+
const { response, bytes } = await fetchBounded(url, {
|
|
265
|
+
signal: budget.signal,
|
|
266
|
+
headers: {
|
|
267
|
+
Accept: 'text/html,application/xhtml+xml',
|
|
268
|
+
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
|
|
269
|
+
'User-Agent': this.userAgent,
|
|
270
|
+
},
|
|
271
|
+
}, {
|
|
272
|
+
provider: 'Paack tracking',
|
|
273
|
+
timeoutMs: Math.min(this.timeoutMs, budget.remainingMs()),
|
|
274
|
+
maxBytes: MAX_RESPONSE_BYTES,
|
|
275
|
+
redirect: 'manual',
|
|
276
|
+
fetcher: this.fetcher,
|
|
277
|
+
allowHttpError: true,
|
|
278
|
+
});
|
|
279
|
+
if (response.status >= 300 && response.status < 400) {
|
|
280
|
+
const target = redirectTarget(response.headers.get('location'), url, trackingNumber, postcode);
|
|
281
|
+
if (target && 'notFound' in target)
|
|
282
|
+
throw new NotFoundError('Paack');
|
|
283
|
+
if (!target || redirects >= MAX_REDIRECTS) {
|
|
284
|
+
throw new IndeterminateError('Paack', 'Paack redirected the lookup without an answer');
|
|
285
|
+
}
|
|
286
|
+
url = target.url;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (response.status === 404)
|
|
290
|
+
throw new NotFoundError('Paack');
|
|
291
|
+
if (!response.ok)
|
|
292
|
+
throw new UpstreamHttpError('Paack tracking', response.status);
|
|
293
|
+
return parsePaackTrackingHtml(decodeText(bytes), postcode);
|
|
247
294
|
}
|
|
248
|
-
if (!response.ok)
|
|
249
|
-
throw new UpstreamHttpError('Paack tracking', response.status);
|
|
250
|
-
return parsePaackTrackingHtml(decodeText(bytes), trackingNumber);
|
|
251
295
|
}
|
|
252
296
|
}
|
|
253
297
|
export const adapter = (environment) => {
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
* delivery ones, so "notDelivered" can never match the "delivered" substring.
|
|
13
13
|
* The provider's own wording is never returned; each mapped entry supplies the
|
|
14
14
|
* English description we display.
|
|
15
|
+
*
|
|
16
|
+
* Aggregators relay the English text of Paack's timeline instead of its
|
|
17
|
+
* identifiers. `paackScan` maps those labels exactly, never by substring, and
|
|
18
|
+
* stores the same description as the direct lookup so both copies of a scan
|
|
19
|
+
* read alike.
|
|
15
20
|
*/
|
|
16
21
|
import type { CarrierStatus } from '../../core/result/index.js';
|
|
17
22
|
import type { Stage } from '../../generated/catalog.js';
|
|
@@ -23,4 +28,9 @@ export interface ClassifiedPaackStatus {
|
|
|
23
28
|
}
|
|
24
29
|
/** Reduce a provider identifier or label to lower-case letters and digits. */
|
|
25
30
|
export declare function statusKey(value: unknown): string;
|
|
31
|
+
/** The stage and stored wording of a Paack timeline label an aggregator relays. */
|
|
32
|
+
export declare function paackScan(label: string): {
|
|
33
|
+
stage: Stage;
|
|
34
|
+
wording: string;
|
|
35
|
+
} | undefined;
|
|
26
36
|
export declare function classifyPaackEvent(value: JsonObject): ClassifiedPaackStatus;
|
|
@@ -7,6 +7,26 @@ export function statusKey(value) {
|
|
|
7
7
|
function includesAny(value, candidates) {
|
|
8
8
|
return candidates.some((candidate) => value.includes(candidate));
|
|
9
9
|
}
|
|
10
|
+
const REGISTERED = { status: 'pending', stage: 'registered', description: 'Shipment registered' };
|
|
11
|
+
const ACCEPTED = { status: 'in_transit', stage: 'accepted', description: 'Shipment accepted' };
|
|
12
|
+
const OUT_FOR_DELIVERY = {
|
|
13
|
+
status: 'out_for_delivery',
|
|
14
|
+
stage: 'out_for_delivery',
|
|
15
|
+
description: 'Out for delivery',
|
|
16
|
+
};
|
|
17
|
+
const DELIVERED = { status: 'delivered', stage: 'delivered', description: 'Delivered' };
|
|
18
|
+
/** Paack's English timeline labels, keyed like `statusKey`, with the identifier each one renders. */
|
|
19
|
+
const RELAYED = new Map([
|
|
20
|
+
['orderdetailsreceived', REGISTERED], // manifested
|
|
21
|
+
['inpaacksdistributioncentre', ACCEPTED], // scannedAtOrigin
|
|
22
|
+
['outfordelivery', OUT_FOR_DELIVERY], // inDelivery
|
|
23
|
+
['delivered', DELIVERED], // delivered
|
|
24
|
+
]);
|
|
25
|
+
/** The stage and stored wording of a Paack timeline label an aggregator relays. */
|
|
26
|
+
export function paackScan(label) {
|
|
27
|
+
const entry = RELAYED.get(statusKey(label));
|
|
28
|
+
return entry && { stage: entry.stage, wording: entry.description };
|
|
29
|
+
}
|
|
10
30
|
export function classifyPaackEvent(value) {
|
|
11
31
|
const key = `${statusKey(value.label)} ${statusKey(value.id)}`;
|
|
12
32
|
if (includesAny(key, [
|
|
@@ -40,19 +60,13 @@ export function classifyPaackEvent(value) {
|
|
|
40
60
|
'undelivered',
|
|
41
61
|
]))
|
|
42
62
|
return { status: 'exception', stage: 'failed_attempt', description: 'Delivery issue' };
|
|
43
|
-
if (includesAny(key, ['delivered', 'deliverycompleted']))
|
|
44
|
-
return {
|
|
45
|
-
}
|
|
63
|
+
if (includesAny(key, ['delivered', 'deliverycompleted']))
|
|
64
|
+
return { ...DELIVERED };
|
|
46
65
|
if (includesAny(key, ['readyforpickup', 'atpickuppoint'])) {
|
|
47
66
|
return { status: 'out_for_delivery', stage: 'ready_for_pickup', description: 'Ready for pickup' };
|
|
48
67
|
}
|
|
49
|
-
if (includesAny(key, ['outfordelivery', 'driverassigned', 'inprogress']))
|
|
50
|
-
return {
|
|
51
|
-
status: 'out_for_delivery',
|
|
52
|
-
stage: 'out_for_delivery',
|
|
53
|
-
description: 'Out for delivery',
|
|
54
|
-
};
|
|
55
|
-
}
|
|
68
|
+
if (includesAny(key, ['outfordelivery', 'indelivery', 'driverassigned', 'inprogress']))
|
|
69
|
+
return { ...OUT_FOR_DELIVERY };
|
|
56
70
|
if (includesAny(key, [
|
|
57
71
|
'manifested',
|
|
58
72
|
'created',
|
|
@@ -63,10 +77,9 @@ export function classifyPaackEvent(value) {
|
|
|
63
77
|
'appointmentrescheduled',
|
|
64
78
|
'appointmentscheduled',
|
|
65
79
|
]))
|
|
66
|
-
return {
|
|
67
|
-
if (includesAny(key, ['scannedatorigin']))
|
|
68
|
-
return {
|
|
69
|
-
}
|
|
80
|
+
return { ...REGISTERED };
|
|
81
|
+
if (includesAny(key, ['scannedatorigin']))
|
|
82
|
+
return { ...ACCEPTED };
|
|
70
83
|
if (includesAny(key, [
|
|
71
84
|
'received',
|
|
72
85
|
'collected',
|
|
@@ -209,6 +209,14 @@
|
|
|
209
209
|
"firstSeen": "2026-09-12",
|
|
210
210
|
"note": "Identifier substring inherited from the pre-package adapter; source not recorded."
|
|
211
211
|
},
|
|
212
|
+
{
|
|
213
|
+
"code": "indelivery",
|
|
214
|
+
"language": "en",
|
|
215
|
+
"stage": "out_for_delivery",
|
|
216
|
+
"confirmedBy": "live",
|
|
217
|
+
"firstSeen": "2026-10-07",
|
|
218
|
+
"note": "Timeline step SP002, rendered as \"Out for delivery\"; listed without a timestamp before it happens."
|
|
219
|
+
},
|
|
212
220
|
{
|
|
213
221
|
"code": "driverassigned",
|
|
214
222
|
"language": "en",
|
|
@@ -229,9 +237,9 @@
|
|
|
229
237
|
"code": "manifested",
|
|
230
238
|
"language": "en",
|
|
231
239
|
"stage": "registered",
|
|
232
|
-
"confirmedBy": "
|
|
240
|
+
"confirmedBy": "live",
|
|
233
241
|
"firstSeen": "2026-09-12",
|
|
234
|
-
"note": "
|
|
242
|
+
"note": "Timeline step SM001: the retailer sent the order details."
|
|
235
243
|
},
|
|
236
244
|
{
|
|
237
245
|
"code": "created",
|
|
@@ -293,9 +301,9 @@
|
|
|
293
301
|
"code": "scannedatorigin",
|
|
294
302
|
"language": "en",
|
|
295
303
|
"stage": "accepted",
|
|
296
|
-
"confirmedBy": "
|
|
304
|
+
"confirmedBy": "live",
|
|
297
305
|
"firstSeen": "2026-09-12",
|
|
298
|
-
"note": "
|
|
306
|
+
"note": "Timeline step SM002, Paack's first scan; also reached as scannedAtOriginHeader."
|
|
299
307
|
},
|
|
300
308
|
{
|
|
301
309
|
"code": "received",
|
|
@@ -352,6 +360,38 @@
|
|
|
352
360
|
"confirmedBy": "fixture",
|
|
353
361
|
"firstSeen": "2026-09-12",
|
|
354
362
|
"note": "Exercised by the offline active-label table via pudoAssignedHeader."
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
"wording": "Order details received",
|
|
366
|
+
"language": "en",
|
|
367
|
+
"stage": "registered",
|
|
368
|
+
"confirmedBy": "live",
|
|
369
|
+
"firstSeen": "2026-10-07",
|
|
370
|
+
"note": "Paack's page label for manifested, relayed by ParcelsApp; stored as \"Shipment registered\"."
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"wording": "In Paack's distribution centre",
|
|
374
|
+
"language": "en",
|
|
375
|
+
"stage": "accepted",
|
|
376
|
+
"confirmedBy": "live",
|
|
377
|
+
"firstSeen": "2026-10-07",
|
|
378
|
+
"note": "Paack's page label for scannedAtOrigin, relayed by ParcelsApp; stored as \"Shipment accepted\"."
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"wording": "Out for delivery",
|
|
382
|
+
"language": "en",
|
|
383
|
+
"stage": "out_for_delivery",
|
|
384
|
+
"confirmedBy": "live",
|
|
385
|
+
"firstSeen": "2026-10-07",
|
|
386
|
+
"note": "Paack's page label for inDelivery."
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
"wording": "Delivered",
|
|
390
|
+
"language": "en",
|
|
391
|
+
"stage": "delivered",
|
|
392
|
+
"confirmedBy": "live",
|
|
393
|
+
"firstSeen": "2026-10-07",
|
|
394
|
+
"note": "Paack's page label for delivered."
|
|
355
395
|
}
|
|
356
396
|
]
|
|
357
397
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -9,7 +9,7 @@ const help = `Universal Parcel Scraper
|
|
|
9
9
|
|
|
10
10
|
parcel-scraper detect <number, link or text>
|
|
11
11
|
parcel-scraper recognize <number>
|
|
12
|
-
parcel-scraper track <number> [--carrier <id>] [--postcode <value>] [--tracking-url <url>]
|
|
12
|
+
parcel-scraper track <number> [--carrier <id>] [--postcode <value>] [--tracking-url <url>]
|
|
13
13
|
parcel-scraper carriers
|
|
14
14
|
parcel-scraper serve [--host <address>] [--port <port>]
|
|
15
15
|
|
|
@@ -8,7 +8,7 @@ export interface TrackingInput {
|
|
|
8
8
|
trackingUrl?: string | null;
|
|
9
9
|
/** The delivery postcode for carriers that need one; part of the tracking credential. */
|
|
10
10
|
postcode?: string | null;
|
|
11
|
-
/**
|
|
11
|
+
/** @deprecated Accepted for compatibility; no adapter or provider reads it. */
|
|
12
12
|
countryHint?: string | null;
|
|
13
13
|
/**
|
|
14
14
|
* The zone of the carrier the parcel is filed under, for universal providers
|
package/dist/data/catalog.json
CHANGED
|
@@ -331,7 +331,7 @@
|
|
|
331
331
|
],
|
|
332
332
|
"detectionRules": [
|
|
333
333
|
{
|
|
334
|
-
"pattern": "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}NL$",
|
|
334
|
+
"pattern": "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}NL$",
|
|
335
335
|
"confidence": "high",
|
|
336
336
|
"checksum": "s10"
|
|
337
337
|
},
|
|
@@ -1090,7 +1090,7 @@
|
|
|
1090
1090
|
"confidence": "low"
|
|
1091
1091
|
},
|
|
1092
1092
|
{
|
|
1093
|
-
"pattern": "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
|
|
1093
|
+
"pattern": "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
|
|
1094
1094
|
"confidence": "high",
|
|
1095
1095
|
"checksum": "s10"
|
|
1096
1096
|
}
|
|
@@ -1131,7 +1131,7 @@
|
|
|
1131
1131
|
],
|
|
1132
1132
|
"detectionRules": [
|
|
1133
1133
|
{
|
|
1134
|
-
"pattern": "^(?:PZ|XU|XW|XY)\\d{9}[A-Z]{2}$",
|
|
1134
|
+
"pattern": "^(?:PZ|XR|XT|XU|XW|XY)\\d{9}[A-Z]{2}$",
|
|
1135
1135
|
"confidence": "high",
|
|
1136
1136
|
"checksum": "s10"
|
|
1137
1137
|
},
|
|
@@ -1746,7 +1746,7 @@
|
|
|
1746
1746
|
"linkRules": [],
|
|
1747
1747
|
"detectionRules": [
|
|
1748
1748
|
{
|
|
1749
|
-
"pattern": "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|CL$)[A-Z]{2}$",
|
|
1749
|
+
"pattern": "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|TS$|JF$|CL$)[A-Z]{2}$",
|
|
1750
1750
|
"confidence": "high",
|
|
1751
1751
|
"checksum": "s10"
|
|
1752
1752
|
}
|
package/dist/facade/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface ParcelInput {
|
|
|
24
24
|
number: string;
|
|
25
25
|
carrier?: string;
|
|
26
26
|
postcode?: string | null;
|
|
27
|
-
/**
|
|
27
|
+
/** @deprecated Accepted for compatibility; it no longer affects a lookup. */
|
|
28
28
|
countryHint?: string | null;
|
|
29
29
|
trackingUrl?: string | null;
|
|
30
30
|
}
|
|
@@ -217,7 +217,7 @@ export declare const CARRIER_CATALOG: {
|
|
|
217
217
|
readonly path: "/details/([^/?#]+)";
|
|
218
218
|
}];
|
|
219
219
|
readonly detectionRules: readonly [{
|
|
220
|
-
readonly pattern: "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}NL$";
|
|
220
|
+
readonly pattern: "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}NL$";
|
|
221
221
|
readonly confidence: "high";
|
|
222
222
|
readonly checksum: "s10";
|
|
223
223
|
}, {
|
|
@@ -678,7 +678,7 @@ export declare const CARRIER_CATALOG: {
|
|
|
678
678
|
readonly pattern: "^[A-Z0-9]{2}\\d{11}$";
|
|
679
679
|
readonly confidence: "low";
|
|
680
680
|
}, {
|
|
681
|
-
readonly pattern: "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}FR$";
|
|
681
|
+
readonly pattern: "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}FR$";
|
|
682
682
|
readonly confidence: "high";
|
|
683
683
|
readonly checksum: "s10";
|
|
684
684
|
}];
|
|
@@ -703,7 +703,7 @@ export declare const CARRIER_CATALOG: {
|
|
|
703
703
|
readonly params: readonly ["listeNumerosLT", "listeNumeros", "numeroLT"];
|
|
704
704
|
}];
|
|
705
705
|
readonly detectionRules: readonly [{
|
|
706
|
-
readonly pattern: "^(?:PZ|XU|XW|XY)\\d{9}[A-Z]{2}$";
|
|
706
|
+
readonly pattern: "^(?:PZ|XR|XT|XU|XW|XY)\\d{9}[A-Z]{2}$";
|
|
707
707
|
readonly confidence: "high";
|
|
708
708
|
readonly checksum: "s10";
|
|
709
709
|
}, {
|
|
@@ -1123,7 +1123,7 @@ export declare const CARRIER_CATALOG: {
|
|
|
1123
1123
|
readonly trackingSiteName: "17TRACK";
|
|
1124
1124
|
readonly linkRules: readonly [];
|
|
1125
1125
|
readonly detectionRules: readonly [{
|
|
1126
|
-
readonly pattern: "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|CL$)[A-Z]{2}$";
|
|
1126
|
+
readonly pattern: "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|TS$|JF$|CL$)[A-Z]{2}$";
|
|
1127
1127
|
readonly confidence: "high";
|
|
1128
1128
|
readonly checksum: "s10";
|
|
1129
1129
|
}];
|
|
@@ -332,7 +332,7 @@ export const CARRIER_CATALOG = {
|
|
|
332
332
|
],
|
|
333
333
|
"detectionRules": [
|
|
334
334
|
{
|
|
335
|
-
"pattern": "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}NL$",
|
|
335
|
+
"pattern": "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}NL$",
|
|
336
336
|
"confidence": "high",
|
|
337
337
|
"checksum": "s10"
|
|
338
338
|
},
|
|
@@ -1091,7 +1091,7 @@ export const CARRIER_CATALOG = {
|
|
|
1091
1091
|
"confidence": "low"
|
|
1092
1092
|
},
|
|
1093
1093
|
{
|
|
1094
|
-
"pattern": "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
|
|
1094
|
+
"pattern": "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
|
|
1095
1095
|
"confidence": "high",
|
|
1096
1096
|
"checksum": "s10"
|
|
1097
1097
|
}
|
|
@@ -1132,7 +1132,7 @@ export const CARRIER_CATALOG = {
|
|
|
1132
1132
|
],
|
|
1133
1133
|
"detectionRules": [
|
|
1134
1134
|
{
|
|
1135
|
-
"pattern": "^(?:PZ|XU|XW|XY)\\d{9}[A-Z]{2}$",
|
|
1135
|
+
"pattern": "^(?:PZ|XR|XT|XU|XW|XY)\\d{9}[A-Z]{2}$",
|
|
1136
1136
|
"confidence": "high",
|
|
1137
1137
|
"checksum": "s10"
|
|
1138
1138
|
},
|
|
@@ -1747,7 +1747,7 @@ export const CARRIER_CATALOG = {
|
|
|
1747
1747
|
"linkRules": [],
|
|
1748
1748
|
"detectionRules": [
|
|
1749
1749
|
{
|
|
1750
|
-
"pattern": "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|CL$)[A-Z]{2}$",
|
|
1750
|
+
"pattern": "^(?![CL][A-Z]\\d{9}DE$)(?!AS\\d{9}US$)(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}(?!CA$|CH$|FR$|IN$|NL$|GB$|IE$|BE$|PT$|NZ$|SG$|JP$|KR$|TH$|HK$|MY$|CN$|NO$|BR$|ES$|SE$|DK$|AT$|YP$|TS$|JF$|CL$)[A-Z]{2}$",
|
|
1751
1751
|
"confidence": "high",
|
|
1752
1752
|
"checksum": "s10"
|
|
1753
1753
|
}
|
|
@@ -19,6 +19,6 @@ export declare class ParcelsAppTracker {
|
|
|
19
19
|
readonly options: ParcelsAppOptions;
|
|
20
20
|
private readonly http;
|
|
21
21
|
constructor(options?: ParcelsAppOptions);
|
|
22
|
-
fetch(trackingNumber: string, budgetMs?: number, postcode?: string | null, timezone?: string | null, signal?: AbortSignal
|
|
22
|
+
fetch(trackingNumber: string, budgetMs?: number, postcode?: string | null, timezone?: string | null, signal?: AbortSignal): Promise<CarrierResult>;
|
|
23
23
|
}
|
|
24
24
|
export declare const adapter: AdapterFactory;
|
|
@@ -21,7 +21,7 @@ import { facilityZone, LatestScan, relaysFacilityClock } from '../shared/facilit
|
|
|
21
21
|
import { universalCarrierHints } from '../shared/hints.js';
|
|
22
22
|
import { event, isNotice, localEvent, numberOf, result } from '../shared/result.js';
|
|
23
23
|
import { carrierScan, markReturnLeg } from '../shared/scans.js';
|
|
24
|
-
import { PARCELSAPP_API,
|
|
24
|
+
import { PARCELSAPP_API, ParcelsAppHttpClient } from './http.js';
|
|
25
25
|
const SOURCE = 'ParcelsApp';
|
|
26
26
|
const MAX_EVENTS = 1000;
|
|
27
27
|
export const PARCELSAPP_BUDGET_MS = 45_000;
|
|
@@ -351,14 +351,13 @@ export class ParcelsAppTracker {
|
|
|
351
351
|
this.options = options;
|
|
352
352
|
this.http = options.httpClient === undefined ? new ParcelsAppHttpClient(options.fetcher, options.userAgent) : options.httpClient;
|
|
353
353
|
}
|
|
354
|
-
async fetch(trackingNumber, budgetMs = this.options.timeoutMs ?? PARCELSAPP_BUDGET_MS, postcode, timezone = null, signal
|
|
354
|
+
async fetch(trackingNumber, budgetMs = this.options.timeoutMs ?? PARCELSAPP_BUDGET_MS, postcode, timezone = null, signal) {
|
|
355
355
|
const number = numberOf(trackingNumber);
|
|
356
356
|
if (!Number.isFinite(budgetMs) || budgetMs < 1)
|
|
357
357
|
throw new TypeError('ParcelsApp timeout must be positive');
|
|
358
358
|
const deadline = performance.now() + budgetMs;
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
const payload = await this.http.fetch(number, Math.max(1, Math.min(DIRECT_BUDGET_MS, Math.floor(remainingMs))), postcode, signal, manualCountry);
|
|
359
|
+
const request = async (remainingMs, signal) => {
|
|
360
|
+
const payload = await this.http.fetch(number, Math.max(1, Math.min(DIRECT_BUDGET_MS, Math.floor(remainingMs))), postcode, signal);
|
|
362
361
|
// This endpoint returns one shipment per POST, synchronously, with no
|
|
363
362
|
// shared session or polling handle. Each retry keeps its own request
|
|
364
363
|
// binding. Numberless browser captures never get this exemption.
|
|
@@ -375,17 +374,13 @@ export class ParcelsAppTracker {
|
|
|
375
374
|
id: 'retry',
|
|
376
375
|
enabled: this.http !== null,
|
|
377
376
|
// Uncached carrier aggregation can outlive a timed-out HTTP request.
|
|
378
|
-
// Retry a replayable network failure with the same input
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
run: async ({ signal, previousError }) => {
|
|
384
|
-
const empty = previousError instanceof NoHistoryError;
|
|
385
|
-
if (!empty)
|
|
386
|
-
await timers.setTimeout(RETRY_DELAY_MS, undefined, { signal });
|
|
377
|
+
// Retry a replayable network failure with the same input, within the
|
|
378
|
+
// original lookup budget.
|
|
379
|
+
recovers: (error) => error instanceof UpstreamNetworkError && deadline - performance.now() > RETRY_DELAY_MS + 1,
|
|
380
|
+
run: async ({ signal }) => {
|
|
381
|
+
await timers.setTimeout(RETRY_DELAY_MS, undefined, { signal });
|
|
387
382
|
signal.throwIfAborted();
|
|
388
|
-
return request(deadline - performance.now(), signal
|
|
383
|
+
return request(deadline - performance.now(), signal);
|
|
389
384
|
},
|
|
390
385
|
}, {
|
|
391
386
|
id: 'trawl',
|
|
@@ -418,6 +413,6 @@ export const adapter = (environment) => {
|
|
|
418
413
|
return {
|
|
419
414
|
id: SOURCE,
|
|
420
415
|
steps: ['direct', 'retry', 'trawl'],
|
|
421
|
-
track: (input, context) => tracker.fetch(input.number, context?.budgetMs, input.postcode, input.timezone ?? null, context?.signal
|
|
416
|
+
track: (input, context) => tracker.fetch(input.number, context?.budgetMs, input.postcode, input.timezone ?? null, context?.signal),
|
|
422
417
|
};
|
|
423
418
|
};
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
/** The country selector uses English names, not ISO codes. */
|
|
2
|
-
export declare function parcelsAppCountry(value: unknown): string | null;
|
|
3
1
|
export declare const PARCELSAPP_API = "https://parcelsapp.com/api/v2/parcels";
|
|
4
2
|
/** MurmurHash2 with the public website's seed, over ASCII protocol data. */
|
|
5
3
|
export declare function parcelsAppChecksum(text: string): number;
|
|
6
|
-
export declare function parcelsAppRequest(trackingNumber: string, postcode?: string | null
|
|
4
|
+
export declare function parcelsAppRequest(trackingNumber: string, postcode?: string | null): URLSearchParams;
|
|
7
5
|
export declare class ParcelsAppHttpClient {
|
|
8
6
|
#private;
|
|
9
7
|
readonly fetcher?: typeof fetch | undefined;
|
|
10
8
|
constructor(fetcher?: typeof fetch | undefined, userAgent?: string);
|
|
11
|
-
fetch(trackingNumber: string, timeoutMs: number, postcode?: string | null, signal?: AbortSignal
|
|
9
|
+
fetch(trackingNumber: string, timeoutMs: number, postcode?: string | null, signal?: AbortSignal): Promise<unknown>;
|
|
12
10
|
}
|