universal-parcel-scraper 0.3.4-main.317 → 0.4.0-main.319
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/dist/carriers/postlogistics/adapter.js +3 -1
- package/dist/carriers/postlogistics/status.d.ts +6 -8
- package/dist/carriers/postlogistics/status.js +15 -2
- package/dist/carriers/postlogistics/statuses.json +15 -1
- package/dist/cli/index.js +3 -3
- package/dist/core/adapter/index.d.ts +2 -0
- package/dist/core/adapter/track.js +1 -1
- package/dist/facade/index.d.ts +2 -0
- package/dist/facade/index.js +3 -1
- package/dist/providers/parcelsapp/adapter.d.ts +1 -1
- package/dist/providers/parcelsapp/adapter.js +16 -11
- package/dist/providers/parcelsapp/http.d.ts +4 -2
- package/dist/providers/parcelsapp/http.js +13 -3
- package/dist/providers/universal.d.ts +3 -2
- package/dist/providers/universal.js +5 -4
- package/dist/server/index.js +3 -3
- package/dist/server/openapi.json +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,7 +149,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
|
|
|
149
149
|
<!-- GENERATED:stages -->
|
|
150
150
|
<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">
|
|
151
151
|
|
|
152
|
-
The carrier folders record 1,
|
|
152
|
+
The carrier folders record 1,675 such statuses, each filed under one stage.
|
|
153
153
|
<!-- /GENERATED:stages -->
|
|
154
154
|
|
|
155
155
|
Wording nobody recorded yet goes through a classifier that reads seven European languages.
|
|
@@ -25,7 +25,7 @@ import { fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/
|
|
|
25
25
|
import { isRecord } from '../../core/types.js';
|
|
26
26
|
import { referenceConsignment } from '../swiss-post-cargo/reference.js';
|
|
27
27
|
import { postlogisticsIdentifier } from './number.js';
|
|
28
|
-
import { POSTLOGISTICS_IMAGE_CODE, postlogisticsStatus } from './status.js';
|
|
28
|
+
import { POSTLOGISTICS_IMAGE_CODE, postlogisticsStage, postlogisticsStatus } from './status.js';
|
|
29
29
|
const PROVIDER = 'PostLogistics';
|
|
30
30
|
const UPSTREAM = 'PostLogistics tracking';
|
|
31
31
|
const TRACK_URL = 'https://eosapi.postlogistics.ch/api/trackandtrace/public?culture=fr-FR';
|
|
@@ -108,6 +108,7 @@ export function parsePostlogisticsTrackingResponse(value, trackingNumber, now =
|
|
|
108
108
|
time: text(event.TimeStamp),
|
|
109
109
|
location: text(event.City),
|
|
110
110
|
description: text(event.Description),
|
|
111
|
+
stage: postlogisticsStage(text(event.Status)),
|
|
111
112
|
}));
|
|
112
113
|
const latest = orderedHistory[0]?.event ?? {};
|
|
113
114
|
const latestStatus = text(latest.Status);
|
|
@@ -116,6 +117,7 @@ export function parsePostlogisticsTrackingResponse(value, trackingNumber, now =
|
|
|
116
117
|
.find(Boolean) ?? '';
|
|
117
118
|
return {
|
|
118
119
|
status: postlogisticsStatus(latestStatus),
|
|
120
|
+
current_stage: postlogisticsStage(latestStatus),
|
|
119
121
|
last_status_text: text(latest.Description) || latestStatus,
|
|
120
122
|
last_update: text(latest.TimeStamp) || null,
|
|
121
123
|
expected_delivery: eta ? eta.slice(0, 10) : null,
|
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
* PostLogistics status vocabulary.
|
|
3
3
|
*
|
|
4
4
|
* Every history entry carries a three-letter `Status` code next to its
|
|
5
|
-
* free-text `Description`.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* "delivered" is worse than a missing nuance.
|
|
9
|
-
*
|
|
10
|
-
* The adapter classifies the shipment, not each event: the endpoint gives one
|
|
11
|
-
* code per scan but no stage vocabulary, so events are returned without an
|
|
12
|
-
* explicit stage and the sync records them for review.
|
|
5
|
+
* free-text `Description`. Confirmed codes classify each scan and the newest
|
|
6
|
+
* scan's summary. Unknown codes leave the stage to the wording classifier.
|
|
7
|
+
* An announcement must not become movement when its wording is unrecognized.
|
|
13
8
|
*/
|
|
14
9
|
import type { CarrierStatus } from '../../core/result/index.js';
|
|
10
|
+
import type { Stage } from '../../generated/catalog.js';
|
|
15
11
|
/** Codes that mean the parcel reached its recipient. */
|
|
16
12
|
export declare const POSTLOGISTICS_DELIVERED_CODES: readonly ["DEL", "DLV", "POD", "SIG"];
|
|
17
13
|
/** The code that means the shipment is announced but not yet moving. */
|
|
18
14
|
export declare const POSTLOGISTICS_NOTIFIED_CODE = "NTF";
|
|
19
15
|
/** The code of an entry that records a picture, not a movement. */
|
|
20
16
|
export declare const POSTLOGISTICS_IMAGE_CODE = "IMG";
|
|
17
|
+
/** The milestone of a scan whose code has a confirmed meaning. */
|
|
18
|
+
export declare function postlogisticsStage(code: string): Stage | undefined;
|
|
21
19
|
/** The shipment status the newest history code implies. */
|
|
22
20
|
export declare function postlogisticsStatus(code: string): CarrierStatus;
|
|
@@ -4,11 +4,24 @@ export const POSTLOGISTICS_DELIVERED_CODES = ['DEL', 'DLV', 'POD', 'SIG'];
|
|
|
4
4
|
export const POSTLOGISTICS_NOTIFIED_CODE = 'NTF';
|
|
5
5
|
/** The code of an entry that records a picture, not a movement. */
|
|
6
6
|
export const POSTLOGISTICS_IMAGE_CODE = 'IMG';
|
|
7
|
-
/** The
|
|
8
|
-
export function
|
|
7
|
+
/** The milestone of a scan whose code has a confirmed meaning. */
|
|
8
|
+
export function postlogisticsStage(code) {
|
|
9
9
|
if (POSTLOGISTICS_DELIVERED_CODES.includes(code))
|
|
10
10
|
return 'delivered';
|
|
11
11
|
if (code === POSTLOGISTICS_NOTIFIED_CODE)
|
|
12
|
+
return 'registered';
|
|
13
|
+
if (code === 'RFS')
|
|
14
|
+
return 'accepted';
|
|
15
|
+
if (code === 'SCA')
|
|
16
|
+
return 'out_for_delivery';
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
/** The shipment status the newest history code implies. */
|
|
20
|
+
export function postlogisticsStatus(code) {
|
|
21
|
+
const stage = postlogisticsStage(code);
|
|
22
|
+
if (stage === 'delivered' || stage === 'out_for_delivery')
|
|
23
|
+
return stage;
|
|
24
|
+
if (stage === 'registered')
|
|
12
25
|
return 'pending';
|
|
13
26
|
return 'in_transit';
|
|
14
27
|
}
|
|
@@ -34,7 +34,21 @@
|
|
|
34
34
|
"stage": "registered",
|
|
35
35
|
"confirmedBy": "fixture",
|
|
36
36
|
"firstSeen": "2026-09-12",
|
|
37
|
-
"note": "Shipment announced
|
|
37
|
+
"note": "Shipment announced; the summary remains pending and the scan is registered."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"code": "RFS",
|
|
41
|
+
"stage": "accepted",
|
|
42
|
+
"confirmedBy": "fixture",
|
|
43
|
+
"firstSeen": "2026-10-04",
|
|
44
|
+
"note": "Goods received by the carrier."
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"code": "SCA",
|
|
48
|
+
"stage": "out_for_delivery",
|
|
49
|
+
"confirmedBy": "fixture",
|
|
50
|
+
"firstSeen": "2026-10-04",
|
|
51
|
+
"note": "Loaded for delivery."
|
|
38
52
|
}
|
|
39
53
|
]
|
|
40
54
|
}
|
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>] [--country-hint <code>]
|
|
13
13
|
parcel-scraper carriers
|
|
14
14
|
parcel-scraper serve [--host <address>] [--port <port>]
|
|
15
15
|
|
|
@@ -38,7 +38,7 @@ export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
38
38
|
throw new TypeError('Unknown command; use --help');
|
|
39
39
|
const values = {};
|
|
40
40
|
const positional = [];
|
|
41
|
-
const allowed = command === 'track' ? ['carrier', 'postcode', 'tracking-url'] : command === 'serve' ? ['host', 'port'] : [];
|
|
41
|
+
const allowed = command === 'track' ? ['carrier', 'postcode', 'tracking-url', 'country-hint'] : command === 'serve' ? ['host', 'port'] : [];
|
|
42
42
|
for (let i = 0; i < args.length; i++) {
|
|
43
43
|
const argument = args[i];
|
|
44
44
|
if (!argument.startsWith('--')) {
|
|
@@ -91,7 +91,7 @@ export async function main(argv = process.argv.slice(2), env = process.env) {
|
|
|
91
91
|
if (command === 'recognize')
|
|
92
92
|
print(await tracker.recognize(input));
|
|
93
93
|
else
|
|
94
|
-
print(await tracker.track({ number: input, carrier: values.carrier, postcode: values.postcode, trackingUrl: values['tracking-url'] }));
|
|
94
|
+
print(await tracker.track({ number: input, carrier: values.carrier, postcode: values.postcode, trackingUrl: values['tracking-url'], countryHint: values['country-hint'] }));
|
|
95
95
|
return 0;
|
|
96
96
|
}
|
|
97
97
|
/** What a failed command prints: a rejected input, setting or listener explains itself; anything else stays generic. */
|
|
@@ -8,6 +8,8 @@ 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. */
|
|
12
|
+
countryHint?: string | null;
|
|
11
13
|
/**
|
|
12
14
|
* The zone of the carrier the parcel is filed under, for universal providers
|
|
13
15
|
* whose scan times name no zone they can be trusted with. When that
|
|
@@ -28,7 +28,7 @@ export async function trackCarrier(carrier, input, options) {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
if (options.registry.adapterIdFor(carrier) === 'universal') {
|
|
31
|
-
return normalizeCarrierResult(await options.universal.fetch(input.number, input.postcode, { signal: options.signal, budgetMs: options.budgetMs }));
|
|
31
|
+
return normalizeCarrierResult(await options.universal.fetch(input.number, input.postcode, { signal: options.signal, budgetMs: options.budgetMs }, ...(input.countryHint === undefined ? [] : [input.countryHint])));
|
|
32
32
|
}
|
|
33
33
|
throw new RangeError(`No tracking adapter is registered for ${carrier}`);
|
|
34
34
|
}
|
package/dist/facade/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ 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. */
|
|
28
|
+
countryHint?: string | null;
|
|
27
29
|
trackingUrl?: string | null;
|
|
28
30
|
}
|
|
29
31
|
export interface TrackingAttempt {
|
package/dist/facade/index.js
CHANGED
|
@@ -147,6 +147,8 @@ export function createTracker(options = {}) {
|
|
|
147
147
|
throw new TrackingError([], failureHint(new BudgetExceededError('Tracking', ms)));
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
+
if (input.countryHint != null && typeof input.countryHint !== 'string')
|
|
151
|
+
throw new TypeError('Invalid country hint');
|
|
150
152
|
if (input.postcode != null && typeof input.postcode !== 'string')
|
|
151
153
|
throw new TypeError('Invalid postcode');
|
|
152
154
|
if (input.trackingUrl != null && typeof input.trackingUrl !== 'string')
|
|
@@ -188,7 +190,7 @@ export function createTracker(options = {}) {
|
|
|
188
190
|
for (const candidate of plan.sources.filter(source => enabled.includes(source))) {
|
|
189
191
|
if (signal.aborted)
|
|
190
192
|
break;
|
|
191
|
-
result = await attempt(candidate, () => provider(candidate, async () => resolveResult(await universal.fetchSource(candidate, number, Math.min(remaining() + DEADLINE_SLACK_MS, universalSourceBudget(candidate)), fields.postcode, carrierTimezone(carrier) === 'UTC' ? null : carrierTimezone(carrier), signal)), signal));
|
|
193
|
+
result = await attempt(candidate, () => provider(candidate, async () => resolveResult(await universal.fetchSource(candidate, number, Math.min(remaining() + DEADLINE_SLACK_MS, universalSourceBudget(candidate)), fields.postcode, carrierTimezone(carrier) === 'UTC' ? null : carrierTimezone(carrier), signal, input.countryHint)), signal));
|
|
192
194
|
if (result) {
|
|
193
195
|
source = candidate;
|
|
194
196
|
break;
|
|
@@ -18,6 +18,6 @@ export declare class ParcelsAppTracker {
|
|
|
18
18
|
readonly options: ParcelsAppOptions;
|
|
19
19
|
private readonly http;
|
|
20
20
|
constructor(options?: ParcelsAppOptions);
|
|
21
|
-
fetch(trackingNumber: string, budgetMs?: number, postcode?: string | null, timezone?: string | null, signal?: AbortSignal): Promise<CarrierResult>;
|
|
21
|
+
fetch(trackingNumber: string, budgetMs?: number, postcode?: string | null, timezone?: string | null, signal?: AbortSignal, countryHint?: string | null): Promise<CarrierResult>;
|
|
22
22
|
}
|
|
23
23
|
export declare const adapter: AdapterFactory;
|
|
@@ -20,7 +20,7 @@ import { capturedBodies, loadCapture } from '../shared/capture.js';
|
|
|
20
20
|
import { universalCarrierHints } from '../shared/hints.js';
|
|
21
21
|
import { event, isNotice, localEvent, numberOf, result } from '../shared/result.js';
|
|
22
22
|
import { carrierScan, markReturnLeg } from '../shared/scans.js';
|
|
23
|
-
import { PARCELSAPP_API, ParcelsAppHttpClient } from './http.js';
|
|
23
|
+
import { PARCELSAPP_API, parcelsAppCountry, ParcelsAppHttpClient } from './http.js';
|
|
24
24
|
const SOURCE = 'ParcelsApp';
|
|
25
25
|
const MAX_EVENTS = 1000;
|
|
26
26
|
export const PARCELSAPP_BUDGET_MS = 45_000;
|
|
@@ -327,13 +327,14 @@ export class ParcelsAppTracker {
|
|
|
327
327
|
this.options = options;
|
|
328
328
|
this.http = options.httpClient === undefined ? new ParcelsAppHttpClient(options.fetcher) : options.httpClient;
|
|
329
329
|
}
|
|
330
|
-
async fetch(trackingNumber, budgetMs = this.options.timeoutMs ?? PARCELSAPP_BUDGET_MS, postcode, timezone = null, signal) {
|
|
330
|
+
async fetch(trackingNumber, budgetMs = this.options.timeoutMs ?? PARCELSAPP_BUDGET_MS, postcode, timezone = null, signal, countryHint) {
|
|
331
331
|
const number = numberOf(trackingNumber);
|
|
332
332
|
if (!Number.isFinite(budgetMs) || budgetMs < 1)
|
|
333
333
|
throw new TypeError('ParcelsApp timeout must be positive');
|
|
334
334
|
const deadline = performance.now() + budgetMs;
|
|
335
|
-
const
|
|
336
|
-
|
|
335
|
+
const country = parcelsAppCountry(countryHint);
|
|
336
|
+
const request = async (remainingMs, signal, manualCountry) => {
|
|
337
|
+
const payload = await this.http.fetch(number, Math.max(1, Math.min(DIRECT_BUDGET_MS, Math.floor(remainingMs))), postcode, signal, manualCountry);
|
|
337
338
|
// This endpoint returns one shipment per POST, synchronously, with no
|
|
338
339
|
// shared session or polling handle. Each retry keeps its own request
|
|
339
340
|
// binding. Numberless browser captures never get this exemption.
|
|
@@ -350,13 +351,17 @@ export class ParcelsAppTracker {
|
|
|
350
351
|
id: 'retry',
|
|
351
352
|
enabled: this.http !== null,
|
|
352
353
|
// Uncached carrier aggregation can outlive a timed-out HTTP request.
|
|
353
|
-
// Retry
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
354
|
+
// Retry a replayable network failure with the same input. An empty
|
|
355
|
+
// answer can instead try the caller's country once, as the website's
|
|
356
|
+
// selector does. Both share this step and the original lookup budget.
|
|
357
|
+
recovers: (error) => (error instanceof UpstreamNetworkError && deadline - performance.now() > RETRY_DELAY_MS + 1)
|
|
358
|
+
|| (country !== null && error instanceof NoHistoryError && deadline - performance.now() > 1),
|
|
359
|
+
run: async ({ signal, previousError }) => {
|
|
360
|
+
const empty = previousError instanceof NoHistoryError;
|
|
361
|
+
if (!empty)
|
|
362
|
+
await timers.setTimeout(RETRY_DELAY_MS, undefined, { signal });
|
|
358
363
|
signal.throwIfAborted();
|
|
359
|
-
return request(deadline - performance.now(), signal);
|
|
364
|
+
return request(deadline - performance.now(), signal, empty ? country : null);
|
|
360
365
|
},
|
|
361
366
|
}, {
|
|
362
367
|
id: 'trawl',
|
|
@@ -389,6 +394,6 @@ export const adapter = (environment) => {
|
|
|
389
394
|
return {
|
|
390
395
|
id: SOURCE,
|
|
391
396
|
steps: ['direct', 'retry', 'trawl'],
|
|
392
|
-
track: (input, context) => tracker.fetch(input.number, context?.budgetMs, input.postcode, input.timezone ?? null, context?.signal),
|
|
397
|
+
track: (input, context) => tracker.fetch(input.number, context?.budgetMs, input.postcode, input.timezone ?? null, context?.signal, input.countryHint),
|
|
393
398
|
};
|
|
394
399
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
/** The country selector uses English names, not ISO codes. */
|
|
2
|
+
export declare function parcelsAppCountry(value: unknown): string | null;
|
|
1
3
|
export declare const PARCELSAPP_API = "https://parcelsapp.com/api/v2/parcels";
|
|
2
4
|
/** MurmurHash2 with the public website's seed, over ASCII protocol data. */
|
|
3
5
|
export declare function parcelsAppChecksum(text: string): number;
|
|
4
|
-
export declare function parcelsAppRequest(trackingNumber: string, postcode?: string | null): URLSearchParams;
|
|
6
|
+
export declare function parcelsAppRequest(trackingNumber: string, postcode?: string | null, countryHint?: string | null): URLSearchParams;
|
|
5
7
|
export declare class ParcelsAppHttpClient {
|
|
6
8
|
readonly fetcher?: typeof fetch | undefined;
|
|
7
9
|
constructor(fetcher?: typeof fetch | undefined);
|
|
8
|
-
fetch(trackingNumber: string, timeoutMs: number, postcode?: string | null, signal?: AbortSignal): Promise<unknown>;
|
|
10
|
+
fetch(trackingNumber: string, timeoutMs: number, postcode?: string | null, signal?: AbortSignal, countryHint?: string | null): Promise<unknown>;
|
|
9
11
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
|
|
2
2
|
import { numberOf } from '../shared/result.js';
|
|
3
|
+
import { countryCode } from '../../core/time/index.js';
|
|
4
|
+
const COUNTRY_NAMES = new Intl.DisplayNames(['en'], { type: 'region' });
|
|
5
|
+
/** The country selector uses English names, not ISO codes. */
|
|
6
|
+
export function parcelsAppCountry(value) {
|
|
7
|
+
const code = countryCode(value);
|
|
8
|
+
return code && code !== 'ZZ' ? COUNTRY_NAMES.of(code) ?? null : null;
|
|
9
|
+
}
|
|
3
10
|
export const PARCELSAPP_API = 'https://parcelsapp.com/api/v2/parcels';
|
|
4
11
|
// Public frontend protocol, verified 2026-09-12. No session or issued secret:
|
|
5
12
|
// packs/js/application-aa19cda6a00923f7e330.js on dvow0vltefbxy.cloudfront.net.
|
|
@@ -29,7 +36,7 @@ export function parcelsAppChecksum(text) {
|
|
|
29
36
|
hash = Math.imul(hash, 0x5bd1e995);
|
|
30
37
|
return (hash ^ hash >>> 15) >>> 0;
|
|
31
38
|
}
|
|
32
|
-
export function parcelsAppRequest(trackingNumber, postcode) {
|
|
39
|
+
export function parcelsAppRequest(trackingNumber, postcode, countryHint) {
|
|
33
40
|
const number = numberOf(trackingNumber);
|
|
34
41
|
// The bundle shifts ASCII by 2 * sum([1,2,8,4,5,6,7,5]) modulo 126,
|
|
35
42
|
// URI-encodes it, then lets jQuery form-encode it a second time.
|
|
@@ -42,6 +49,9 @@ export function parcelsAppRequest(trackingNumber, postcode) {
|
|
|
42
49
|
const zipcode = postcode?.trim();
|
|
43
50
|
if (zipcode)
|
|
44
51
|
params.set('extra[zipcode]', zipcode);
|
|
52
|
+
const country = parcelsAppCountry(countryHint);
|
|
53
|
+
if (country)
|
|
54
|
+
params.set('extra[manualCountry]', country);
|
|
45
55
|
return params;
|
|
46
56
|
}
|
|
47
57
|
export class ParcelsAppHttpClient {
|
|
@@ -49,7 +59,7 @@ export class ParcelsAppHttpClient {
|
|
|
49
59
|
constructor(fetcher) {
|
|
50
60
|
this.fetcher = fetcher;
|
|
51
61
|
}
|
|
52
|
-
async fetch(trackingNumber, timeoutMs, postcode, signal) {
|
|
62
|
+
async fetch(trackingNumber, timeoutMs, postcode, signal, countryHint) {
|
|
53
63
|
const number = numberOf(trackingNumber);
|
|
54
64
|
if (!Number.isFinite(timeoutMs) || timeoutMs < 1)
|
|
55
65
|
throw new TypeError('ParcelsApp HTTP timeout must be positive');
|
|
@@ -60,7 +70,7 @@ export class ParcelsAppHttpClient {
|
|
|
60
70
|
Origin: 'https://parcelsapp.com', Referer: `https://parcelsapp.com/en/tracking/${number}`,
|
|
61
71
|
'X-Requested-With': 'XMLHttpRequest',
|
|
62
72
|
},
|
|
63
|
-
body: parcelsAppRequest(number, postcode),
|
|
73
|
+
body: parcelsAppRequest(number, postcode, countryHint),
|
|
64
74
|
}, { provider: 'ParcelsApp', fetcher: this.fetcher, timeoutMs: Math.floor(timeoutMs), maxBytes: 2_000_000 });
|
|
65
75
|
return parseJsonBytes(bytes, 'ParcelsApp');
|
|
66
76
|
}
|
|
@@ -56,10 +56,11 @@ export declare class UniversalTracker {
|
|
|
56
56
|
* stored delivery postcode, if the user supplied one: it is forwarded into
|
|
57
57
|
* every provider's track input. ParcelsApp submits it as extra[zipcode] on
|
|
58
58
|
* its direct API request; the other providers currently do not consume it.
|
|
59
|
+
* `countryHint` lets ParcelsApp retry an empty answer with that country.
|
|
59
60
|
* The caller's budget covers the whole chain and its signal ends it.
|
|
60
61
|
*/
|
|
61
|
-
fetch(trackingNumber: string, postcode?: string | null, context?: TrackingContext): Promise<CarrierResult>;
|
|
62
|
-
fetchSource(source: Source, trackingNumber: string, timeoutMs?: number, postcode?: string | null, timezone?: string | null, signal?: AbortSignal): Promise<CarrierResult>;
|
|
62
|
+
fetch(trackingNumber: string, postcode?: string | null, context?: TrackingContext, countryHint?: string | null): Promise<CarrierResult>;
|
|
63
|
+
fetchSource(source: Source, trackingNumber: string, timeoutMs?: number, postcode?: string | null, timezone?: string | null, signal?: AbortSignal, countryHint?: string | null): Promise<CarrierResult>;
|
|
63
64
|
/** One provider adapter, built from this tracker's environment. */
|
|
64
65
|
private provider;
|
|
65
66
|
private environment;
|
|
@@ -49,9 +49,10 @@ export class UniversalTracker {
|
|
|
49
49
|
* stored delivery postcode, if the user supplied one: it is forwarded into
|
|
50
50
|
* every provider's track input. ParcelsApp submits it as extra[zipcode] on
|
|
51
51
|
* its direct API request; the other providers currently do not consume it.
|
|
52
|
+
* `countryHint` lets ParcelsApp retry an empty answer with that country.
|
|
52
53
|
* The caller's budget covers the whole chain and its signal ends it.
|
|
53
54
|
*/
|
|
54
|
-
async fetch(trackingNumber, postcode, context = {}) {
|
|
55
|
+
async fetch(trackingNumber, postcode, context = {}, countryHint) {
|
|
55
56
|
numberOf(trackingNumber);
|
|
56
57
|
context.signal?.throwIfAborted();
|
|
57
58
|
const deadline = context.budgetMs === undefined ? Infinity : performance.now() + context.budgetMs;
|
|
@@ -64,7 +65,7 @@ export class UniversalTracker {
|
|
|
64
65
|
continue;
|
|
65
66
|
}
|
|
66
67
|
try {
|
|
67
|
-
return await this.fetchSource(source, trackingNumber, Math.min(remaining, this.options.timeoutMs ?? universalSourceBudget(source)), postcode, null, context.signal);
|
|
68
|
+
return await this.fetchSource(source, trackingNumber, Math.min(remaining, this.options.timeoutMs ?? universalSourceBudget(source)), postcode, null, context.signal, countryHint);
|
|
68
69
|
}
|
|
69
70
|
catch (error) {
|
|
70
71
|
if (context.signal?.aborted)
|
|
@@ -74,12 +75,12 @@ export class UniversalTracker {
|
|
|
74
75
|
}
|
|
75
76
|
throw new UniversalTrackingError(failures);
|
|
76
77
|
}
|
|
77
|
-
async fetchSource(source, trackingNumber, timeoutMs = this.options.timeoutMs ?? universalSourceBudget(source), postcode, timezone, signal) {
|
|
78
|
+
async fetchSource(source, trackingNumber, timeoutMs = this.options.timeoutMs ?? universalSourceBudget(source), postcode, timezone, signal, countryHint) {
|
|
78
79
|
const number = numberOf(trackingNumber);
|
|
79
80
|
if (this.options.browserLookup && (source === 'Postal Ninja' || source === 'Ship24')) {
|
|
80
81
|
return await this.options.browserLookup(source, number);
|
|
81
82
|
}
|
|
82
|
-
return await this.provider(source).track({ number, postcode: postcode ?? null, timezone: timezone ?? null }, { budgetMs: timeoutMs, signal });
|
|
83
|
+
return await this.provider(source).track({ number, postcode: postcode ?? null, timezone: timezone ?? null, countryHint: countryHint ?? null }, { budgetMs: timeoutMs, signal });
|
|
83
84
|
}
|
|
84
85
|
/** One provider adapter, built from this tracker's environment. */
|
|
85
86
|
provider(source) {
|
package/dist/server/index.js
CHANGED
|
@@ -142,7 +142,7 @@ export function createTrackingServer(options = {}) {
|
|
|
142
142
|
const number = normalizeTrackingNumber(input.number);
|
|
143
143
|
const detected = detectCarrierMatch(number);
|
|
144
144
|
input = { ...input, number, carrier: input.carrier ?? (detected.confidence === 'high' ? detected.carrier : undefined) };
|
|
145
|
-
const key = createHash('sha256').update(JSON.stringify([input.number, input.carrier, input.postcode, input.trackingUrl])).digest('hex');
|
|
145
|
+
const key = createHash('sha256').update(JSON.stringify([input.number, input.carrier, input.postcode, input.trackingUrl, input.countryHint])).digest('hex');
|
|
146
146
|
const previous = cache.get(key);
|
|
147
147
|
if (previous && previous.until > now()) {
|
|
148
148
|
if (previous.error)
|
|
@@ -261,7 +261,7 @@ export function createTrackingServer(options = {}) {
|
|
|
261
261
|
}
|
|
262
262
|
const input = await body(request);
|
|
263
263
|
const allowed = path === '/v1/detect' ? ['text'] : path === '/v1/recognize' ? ['number', 'budgetMs']
|
|
264
|
-
: ['number', 'carrier', 'postcode', 'trackingUrl', 'budgetMs'];
|
|
264
|
+
: ['number', 'carrier', 'postcode', 'trackingUrl', 'countryHint', 'budgetMs'];
|
|
265
265
|
if (Object.keys(input).some(key => !allowed.includes(key)))
|
|
266
266
|
throw new HttpError(400, 'Unknown request field');
|
|
267
267
|
if (path === '/v1/detect') {
|
|
@@ -275,7 +275,7 @@ export function createTrackingServer(options = {}) {
|
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
277
|
const result = await tracked({ number: input.number, carrier: input.carrier,
|
|
278
|
-
postcode: input.postcode, trackingUrl: input.trackingUrl }, input.budgetMs);
|
|
278
|
+
postcode: input.postcode, trackingUrl: input.trackingUrl, countryHint: input.countryHint }, input.budgetMs);
|
|
279
279
|
json(response, 200, result);
|
|
280
280
|
}
|
|
281
281
|
catch (error) {
|
package/dist/server/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Universal Parcel Scraper",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.0"
|
|
6
6
|
},
|
|
7
7
|
"paths": {
|
|
8
8
|
"/health": {
|
|
@@ -539,6 +539,13 @@
|
|
|
539
539
|
"type": "integer",
|
|
540
540
|
"minimum": 1,
|
|
541
541
|
"maximum": 120000
|
|
542
|
+
},
|
|
543
|
+
"countryHint": {
|
|
544
|
+
"type": [
|
|
545
|
+
"string",
|
|
546
|
+
"null"
|
|
547
|
+
],
|
|
548
|
+
"description": "Country code or English name used only to retry an empty universal lookup."
|
|
542
549
|
}
|
|
543
550
|
},
|
|
544
551
|
"required": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-main.319",
|
|
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",
|