universal-parcel-scraper 0.776.0-main.4145 → 0.776.2-main.4151

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -161,7 +161,7 @@ the command line's `track --source ParcelsApp` does the same.
161
161
  <!-- GENERATED:stages -->
162
162
  <img src="docs/assets/stages.svg" alt="DHL: Die Sendung wurde in das Zustellfahrzeug geladen.; Mondial Relay: En cours de livraison; Correios Brazil: Objeto saiu para entrega ao destinatário; Correos Express: EN REPARTO; Yamato Transport: 配達中; La Poste / Colissimo: DISTOU. All are filed under out_for_delivery." width="760">
163
163
 
164
- The carrier folders record 17,127 such statuses, each filed under one stage.
164
+ The carrier folders record 17,129 such statuses, each filed under one stage.
165
165
  <!-- /GENERATED:stages -->
166
166
 
167
167
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
@@ -30,6 +30,8 @@ const codes = {
30
30
  'PAR/21': { status: 'in_transit', stage: 'customs' },
31
31
  // The import waits for the recipient's tax number before customs can clear it.
32
32
  'FC/16': { status: 'in_transit', stage: 'customs' },
33
+ // The import waits for customs to settle the product's classification code.
34
+ 'FC/40': { status: 'in_transit', stage: 'customs' },
33
35
  'PAR/24': { status: 'exception', stage: 'returned' },
34
36
  };
35
37
  export function classifyCorreiosStatus(code) {
@@ -14,6 +14,7 @@
14
14
  { "code": "PAR/07", "wording": "Análise concluída - importação autorizada", "language": "pt", "stage": "in_transit", "confirmedBy": "live" },
15
15
  { "code": "PAR/21", "wording": "Informações enviadas para análise da autoridade aduaneira/órgãos anuentes", "language": "pt", "stage": "customs", "confirmedBy": "live" },
16
16
  { "code": "FC/16", "wording": "Necessidade de informar CPF/CNPJ do destinatário", "language": "pt", "stage": "customs", "confirmedBy": "live", "note": "The import waits for the recipient's tax number (CPF or CNPJ) before customs can clear it." },
17
+ { "code": "FC/40", "wording": "Objeto em processo de identificação do código de classificação do produto", "language": "pt", "stage": "customs", "confirmedBy": "live", "note": "The import waits for customs to settle the product's classification code, after Correios received the item." },
17
18
  { "code": "BDI/01", "wording": "Objeto entregue ao destinatário", "language": "pt", "stage": "delivered", "confirmedBy": "live" },
18
19
  { "code": "BDE/10", "wording": "Objeto não entregue - cliente mudou-se", "language": "pt", "stage": "failed_attempt", "confirmedBy": "live" },
19
20
  { "code": "BDE/20", "wording": "Objeto não entregue - carteiro não atendido", "language": "pt", "stage": "failed_attempt", "confirmedBy": "live" },
@@ -1,11 +1,17 @@
1
1
  import { dpdUkWording, withoutDriverName } from './wording.js';
2
- const wording = (scan) => dpdUkWording(withoutDriverName(scan.description));
2
+ // DPD UK rewords a depot scan as the parcel moves on: it has arrived at the depot, it is there
3
+ // ready for delivery, then it is at the depot. The three are one scan.
4
+ const DEPOT = /^your parcel (?:has arrived at|is at) our depot(?: ready for delivery)?$/;
5
+ function wording(scan) {
6
+ const text = dpdUkWording(withoutDriverName(scan.description));
7
+ return DEPOT.test(text) ? 'your parcel is at our depot' : text;
8
+ }
3
9
  /** One scan, by its wording and place. DPD UK's scans have no code. */
4
10
  function sameScan(incoming, stored) {
5
11
  return wording(incoming) !== '' && wording(incoming) === wording(stored) && incoming.location.trim() === stored.location.trim();
6
12
  }
7
- // The round's notice drops the hour it gave for the delivery once the round is over, and
8
- // earlier releases kept the driver's name a scan gives. The scan keeps its row either way.
13
+ // The round's notice drops the hour it gave for the delivery once the round is over, a depot
14
+ // scan is reworded, and earlier releases kept the driver's name a scan gives. The scan keeps its row.
9
15
  export const sameInstantIdentityPolicy = {
10
16
  sourceCarrierId: 'dpd-uk', storedSources: ['dpd-uk'], requireProviderCode: false, matchEachScan: true, matches: sameScan,
11
17
  };
@@ -57,6 +57,7 @@ const VERIFIED_WORDING = {
57
57
  'the parcel is on the vehicle for delivery': 'out_for_delivery',
58
58
  'your parcel is at our depot': 'in_transit',
59
59
  'your parcel has arrived at our depot': 'in_transit',
60
+ 'your parcel is at our depot ready for delivery': 'in_transit',
60
61
  "your parcel has left our sortation facility and is on it's onwards journey": 'in_transit',
61
62
  'the parcel has arrived at the delivery depot': 'in_transit',
62
63
  'the parcel is in transit to its final destination': 'in_transit',
@@ -8,6 +8,7 @@
8
8
  { "wording": "We have your parcel and it's on its way to you", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-08" },
9
9
  { "wording": "Your parcel is at our depot", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-08" },
10
10
  { "wording": "Your parcel has arrived at our depot", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-08" },
11
+ { "wording": "Your parcel is at our depot ready for delivery", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-28", "note": "A depot scan as DPD rewords it once the parcel is ready for its round; later it reads \"Your parcel is at our depot\"." },
11
12
  { "wording": "Your parcel has left our sortation facility and is on it's onwards journey", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-08", "note": "On a parcel leaving the UK." },
12
13
  { "wording": "The parcel is in transit to its final destination", "language": "en", "stage": "in_transit", "confirmedBy": "live", "firstSeen": "2026-10-08", "note": "The partner's wording on a parcel delivered abroad, as are the other entries starting with \"The parcel\"." },
13
14
  { "wording": "The parcel is undergoing customs clearance", "language": "en", "stage": "customs", "confirmedBy": "live", "firstSeen": "2026-10-08" },
@@ -20,7 +20,7 @@ export declare function magyarPostaBrowser(trawl: TrawlClient, raw: string, sign
20
20
  * the number, bound to the whole number. The reply is the item, as later reads
21
21
  * of the id return it.
22
22
  */
23
- export declare function magyarPostaAppBrowser(trawl: TrawlClient, raw: string, applicationId: string, signal: AbortSignal, remainingMs: number): Promise<CarrierResult>;
23
+ export declare function magyarPostaAppBrowser(trawl: TrawlClient, raw: string, applicationId: string, signal: AbortSignal, remainingMs: number, rememberApplication?: () => void, supportsRegistrationCache?: () => void): Promise<CarrierResult>;
24
24
  export declare class MagyarPostaTracker {
25
25
  #private;
26
26
  private readonly options;
@@ -82,7 +82,7 @@ export async function magyarPostaBrowser(trawl, raw, signal, remainingMs) {
82
82
  * the number, bound to the whole number. The reply is the item, as later reads
83
83
  * of the id return it.
84
84
  */
85
- export async function magyarPostaAppBrowser(trawl, raw, applicationId, signal, remainingMs) {
85
+ export async function magyarPostaAppBrowser(trawl, raw, applicationId, signal, remainingMs, rememberApplication, supportsRegistrationCache) {
86
86
  const number = normalizeMagyarPostaNumber(raw);
87
87
  const timeoutMs = Math.floor(remainingMs - 2000);
88
88
  if (timeoutMs < 1)
@@ -90,6 +90,10 @@ export async function magyarPostaAppBrowser(trawl, raw, applicationId, signal, r
90
90
  const page = await trawl.scrape({ url: magyarPostaAppTrackingUrl(applicationId, number), skipHttp: true, maxTier: 3,
91
91
  captureResponses: [MAGYAR_POSTA_APP_TRACKING], maxTimeout: timeoutMs, settleTimeout: timeoutMs }, { provider: PROVIDER, timeoutMs, signal, requireSolved: false, captureWindow: 'last', maxBytes: 2 * MAGYAR_POSTA_MAX_BYTES });
92
92
  signal.throwIfAborted();
93
+ if (page.capturedResponses.some(row => row.headers['x-magyar-posta-registration-cache'] === 'memory'
94
+ && ((row.url === MAGYAR_POSTA_APP_PAGE && row.error === REFUSED)
95
+ || (row.url === MAGYAR_POSTA_APP_TRACKING && row.trackingNumberSubmitted === number))))
96
+ supportsRegistrationCache?.();
93
97
  if (page.capturedResponses.some(row => row.url === MAGYAR_POSTA_APP_PAGE && row.error === REFUSED)) {
94
98
  throw new ChallengeError(PROVIDER, 'Google refused Magyar Posta\'s audio challenge', { reason: 'verification_refused' });
95
99
  }
@@ -100,10 +104,13 @@ export async function magyarPostaAppBrowser(trawl, raw, applicationId, signal, r
100
104
  // No addition left the widget page: its page failed, or the widget issued no token.
101
105
  if (!reply)
102
106
  throw httpFailure(page.statusCode) ?? new ChallengeError(PROVIDER, 'Magyar Posta\'s app widget issued no token');
107
+ if (reply.error === 'Magyar Posta returned no bound cached history')
108
+ throw new SchemaError(PROVIDER, 'Magyar Posta returned unbound cached history');
103
109
  if (reply.error || reply.truncated || reply.base64Encoded)
104
110
  throw new TransportError(PROVIDER, 'Magyar Posta returned an incomplete app reply');
105
111
  const payload = reply.body === null ? null : magyarPostaAppJson(reply.body);
106
- if (reply.status === 502 && typeof payload === 'object' && payload !== null && payload.message === UNADDED) {
112
+ if (reply.headers['x-magyar-posta-registration'] !== 'retained' && reply.status === 502
113
+ && typeof payload === 'object' && payload !== null && payload.message === UNADDED) {
107
114
  throw new UnaddedError(PROVIDER, 'Magyar Posta\'s app did not add the number');
108
115
  }
109
116
  const failure = magyarPostaAppFailure(reply.status, reply.headers['retry-after']);
@@ -112,12 +119,18 @@ export async function magyarPostaAppBrowser(trawl, raw, applicationId, signal, r
112
119
  const result = parseMagyarPostaApp(payload, number);
113
120
  if (!result)
114
121
  throw new SchemaError(PROVIDER, 'Magyar Posta\'s app added the number without returning it');
122
+ // A retained registration belongs to the helper, not to this new app id.
123
+ // Its capability stays in the helper's memory rather than entering results.
124
+ if (reply.headers['x-magyar-posta-registration'] !== 'retained')
125
+ rememberApplication?.();
115
126
  return result;
116
127
  }
117
128
  export class MagyarPostaTracker {
118
129
  options;
119
130
  /** Until when Google's refusal holds, for the app's widget and for the tracker's checkbox, which have their own keys. */
120
131
  #refusedUntil = { app: 0, web: 0 };
132
+ /** A helper with retained registrations checks those before its own widget cooldown. */
133
+ #registrationCache = false;
121
134
  /** The application id each added number is read under, oldest first. */
122
135
  #added = new Map();
123
136
  /** The id new numbers go to, generated when first needed, and how many it took. */
@@ -178,7 +191,7 @@ export class MagyarPostaTracker {
178
191
  const id = await this.#applicationId(signal, remainingMs);
179
192
  const route = id ? 'app' : 'web';
180
193
  // Another solve soon after a refusal would hold a browser for the same answer.
181
- if (Date.now() < this.#refusedUntil[route]) {
194
+ if (Date.now() < this.#refusedUntil[route] && !(route === 'app' && this.#registrationCache)) {
182
195
  throw new ChallengeError(PROVIDER, 'Google refused Magyar Posta\'s audio challenge recently', { reason: 'verification_refused' });
183
196
  }
184
197
  if (id && (this.#unadded.get(number) ?? 0) > Date.now()) {
@@ -190,10 +203,13 @@ export class MagyarPostaTracker {
190
203
  if (!id)
191
204
  return await magyarPostaBrowser(trawl, number, signal, left);
192
205
  const current = this.#current;
193
- const result = await magyarPostaAppBrowser(trawl, number, id, signal, left);
194
- if (current && this.#current === current)
195
- current.count++;
196
- this.#remember(number, id);
206
+ let added = false;
207
+ const result = await magyarPostaAppBrowser(trawl, number, id, signal, left, () => { added = true; }, () => { this.#registrationCache = true; });
208
+ if (added) {
209
+ if (current && this.#current === current)
210
+ current.count++;
211
+ this.#remember(number, id);
212
+ }
197
213
  return result;
198
214
  }
199
215
  catch (error) {
@@ -246,6 +246,12 @@ function providerWordingStage(description) {
246
246
  return 'registered';
247
247
  if (/^export declaration in progress[.!]?$/i.test(value))
248
248
  return 'customs';
249
+ // KTD's steps as ParcelsApp relays them: the first carrier's local warehouse taking the parcel in and
250
+ // sending it on to KTD's warehouse, the parcel leaving that, its export cleared and its flight taking off.
251
+ if (/^package received and scanned at the carrier['’]s local warehouse[.!]?$/i.test(value))
252
+ return 'accepted';
253
+ if (/^package leaves the carrier['’]s local warehouse[.!]?$|^your item has left the [a-z ]{1,40} warehouse[.!]?$|^export declaration completed[.!]?$|^take ?off[.!]?$/i.test(value))
254
+ return 'in_transit';
249
255
  // MRW's own wording for a parcel left at one of its pickup points, relayed by a provider,
250
256
  // and for its pickup from the sender still to come, as MRW's own map reads them.
251
257
  if (/^depositado en punto mrw[.!]?$/i.test(value))
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.776.0-main.4145"
5
+ "version": "0.776.2-main.4151"
6
6
  },
7
7
  "paths": {
8
8
  "/health": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.776.0-main.4145",
3
+ "version": "0.776.2-main.4151",
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",