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.
Files changed (37) hide show
  1. package/README.md +1 -1
  2. package/data/catalog.json +4 -4
  3. package/data/checksum-vectors.json +63 -0
  4. package/data/detection-golden.json +43 -0
  5. package/dist/browser/scraper.js +18 -13
  6. package/dist/carriers/chronopost/carrier.json +4 -4
  7. package/dist/carriers/chronopost/numbers.json +56 -0
  8. package/dist/carriers/chronopost/parser.js +29 -3
  9. package/dist/carriers/dpd/adapter.js +39 -2
  10. package/dist/carriers/intl-post/carrier.json +1 -1
  11. package/dist/carriers/intl-post/numbers.json +18 -0
  12. package/dist/carriers/la-poste/carrier.json +1 -1
  13. package/dist/carriers/paack/adapter.d.ts +2 -2
  14. package/dist/carriers/paack/adapter.js +88 -44
  15. package/dist/carriers/paack/status.d.ts +10 -0
  16. package/dist/carriers/paack/status.js +27 -14
  17. package/dist/carriers/paack/statuses.json +44 -4
  18. package/dist/carriers/spring-gds/carrier.json +1 -1
  19. package/dist/cli/index.js +1 -1
  20. package/dist/core/adapter/index.d.ts +1 -1
  21. package/dist/data/catalog.json +4 -4
  22. package/dist/facade/index.d.ts +1 -1
  23. package/dist/generated/catalog.d.ts +4 -4
  24. package/dist/generated/catalog.js +4 -4
  25. package/dist/providers/parcelsapp/adapter.d.ts +1 -1
  26. package/dist/providers/parcelsapp/adapter.js +11 -16
  27. package/dist/providers/parcelsapp/http.d.ts +2 -4
  28. package/dist/providers/parcelsapp/http.js +3 -13
  29. package/dist/providers/plan.d.ts +2 -2
  30. package/dist/providers/plan.js +17 -5
  31. package/dist/providers/shared/hints.d.ts +9 -1
  32. package/dist/providers/shared/hints.js +18 -3
  33. package/dist/providers/shared/scans.js +2 -0
  34. package/dist/providers/universal.d.ts +9 -9
  35. package/dist/providers/universal.js +11 -5
  36. package/dist/server/openapi.json +3 -2
  37. 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
  }
@@ -96,7 +96,7 @@
96
96
  },
97
97
  {
98
98
  "id": "la-poste-3",
99
- "pattern": "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
99
+ "pattern": "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}FR$",
100
100
  "confidence": "high",
101
101
  "checksum": "s10"
102
102
  }
@@ -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, rawTrackingNumber: string): CarrierResult;
7
- export declare function parsePaackTrackingHtml(html: string, rawTrackingNumber: string): CarrierResult;
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. The order number the
9
- * response echoes is verified before anything else is read.
8
+ * and gives the same data the page itself renders.
10
9
  *
11
- * A wrong number or postcode is answered with a redirect back to the form, or
12
- * with an explicit "order not found" message; both become a clean not-found.
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 echoed identifier and the delivery window.
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
- export function paackTrackingUrl(rawTrackingNumber, rawPostcode) {
126
- const trackingNumber = normalizePaackTrackingNumber(rawTrackingNumber);
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 parsePaackTrackingResponse(payload, rawTrackingNumber) {
134
- const trackingNumber = normalizePaackTrackingNumber(rawTrackingNumber);
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
- if (typeof order.external_id !== 'string')
144
- throw new SchemaError('Paack', 'Paack returned an invalid shipment number');
145
- let responseNumber;
146
- try {
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, rawTrackingNumber) {
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), rawTrackingNumber);
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
- const { response, bytes } = await fetchBounded(paackTrackingUrl(trackingNumber, postcode), {
231
- signal: budget.signal,
232
- headers: {
233
- Accept: 'text/html,application/xhtml+xml',
234
- 'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
235
- 'User-Agent': this.userAgent,
236
- },
237
- }, {
238
- provider: 'Paack tracking',
239
- timeoutMs: Math.min(this.timeoutMs, budget.remainingMs()),
240
- maxBytes: MAX_RESPONSE_BYTES,
241
- redirect: 'manual',
242
- fetcher: this.fetcher,
243
- allowHttpError: true,
244
- });
245
- if (response.status === 404 || (response.status >= 300 && response.status < 400)) {
246
- throw new NotFoundError('Paack');
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 { status: 'delivered', stage: 'delivered', description: 'Delivered' };
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 { status: 'pending', stage: 'registered', description: 'Shipment registered' };
67
- if (includesAny(key, ['scannedatorigin'])) {
68
- return { status: 'in_transit', stage: 'accepted', description: 'Shipment accepted' };
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": "prior-art",
240
+ "confirmedBy": "live",
233
241
  "firstSeen": "2026-09-12",
234
- "note": "Identifier substring inherited from the pre-package adapter; source not recorded."
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": "fixture",
304
+ "confirmedBy": "live",
297
305
  "firstSeen": "2026-09-12",
298
- "note": "Exercised by the offline active-label table via scannedAtOriginHeader."
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
  }
@@ -77,7 +77,7 @@
77
77
  "detection": [
78
78
  {
79
79
  "id": "spring-gds-1",
80
- "pattern": "^(?!(?:PZ|XU|XW|XY))[A-Z]{2}\\d{9}NL$",
80
+ "pattern": "^(?!(?:PZ|XR|XT|XU|XW|XY))[A-Z]{2}\\d{9}NL$",
81
81
  "confidence": "high",
82
82
  "checksum": "s10"
83
83
  },
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>] [--country-hint <code>]
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
- /** ISO country code or English name used only to retry an empty universal lookup; never shipment evidence. */
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
@@ -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
  }
@@ -24,7 +24,7 @@ export interface ParcelInput {
24
24
  number: string;
25
25
  carrier?: string;
26
26
  postcode?: string | null;
27
- /** A destination or visitor country hint for an empty universal lookup; never shipment evidence. */
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, countryHint?: string | null): Promise<CarrierResult>;
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, parcelsAppCountry, ParcelsAppHttpClient } from './http.js';
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, countryHint) {
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 country = parcelsAppCountry(countryHint);
360
- const request = async (remainingMs, signal, manualCountry) => {
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. An empty
379
- // answer can instead try the caller's country once, as the website's
380
- // selector does. Both share this step and the original lookup budget.
381
- recovers: (error) => (error instanceof UpstreamNetworkError && deadline - performance.now() > RETRY_DELAY_MS + 1)
382
- || (country !== null && error instanceof NoHistoryError && deadline - performance.now() > 1),
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, empty ? country : null);
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, input.countryHint),
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, countryHint?: string | null): URLSearchParams;
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, countryHint?: string | null): Promise<unknown>;
9
+ fetch(trackingNumber: string, timeoutMs: number, postcode?: string | null, signal?: AbortSignal): Promise<unknown>;
12
10
  }