universal-parcel-scraper 0.14.0-main.340 → 0.14.1-main.344
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.
|
@@ -6,12 +6,14 @@ export declare function trackingUrl(raw: string): string;
|
|
|
6
6
|
export declare function parseUnified(payload: unknown, raw: string): CarrierResult;
|
|
7
7
|
/** A waybill can be reused. Multiple or duplicate shipments need recipient disambiguation. */
|
|
8
8
|
export declare function parse(payload: unknown, raw: string): CarrierResult;
|
|
9
|
+
/** Guest mobile history has facility-local clocks and orders scans by counter. */
|
|
10
|
+
export declare function parseMobile(payload: unknown, raw: string): CarrierResult;
|
|
9
11
|
export declare class DhlExpressTracker {
|
|
10
12
|
private readonly environment;
|
|
13
|
+
private readonly mobileDevice;
|
|
11
14
|
constructor(environment: AdapterEnvironment);
|
|
12
15
|
direct(number: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
13
16
|
browser(number: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
14
|
-
private decode;
|
|
15
17
|
fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
16
18
|
}
|
|
17
19
|
export declare const adapter: AdapterFactory;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { DateTime } from 'luxon';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { accepted, lookupBudget, recognizeFromBrowserLookup, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
3
4
|
import { isValidDhlExpressWaybill } from '../../core/detection/numericChecksums.js';
|
|
4
5
|
import { BudgetExceededError, ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, RateLimitedError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
5
6
|
import { runSteps } from '../../core/runner/index.js';
|
|
6
7
|
import { classifyWording } from '../../core/status/index.js';
|
|
7
8
|
import { explicitOffsetTime } from '../../core/time/index.js';
|
|
8
|
-
import { clean,
|
|
9
|
+
import { clean, parseJsonBytes } from '../../core/transport/index.js';
|
|
9
10
|
import { isRecord } from '../../core/types.js';
|
|
11
|
+
import { mobileTracking } from './mobile.js';
|
|
10
12
|
const PROVIDER = 'DHL Express';
|
|
11
|
-
const API = 'https://mydhl.express.dhl/shipmentTracking';
|
|
12
13
|
const BROWSER_API = 'https://www.dhl.com/utapi';
|
|
13
14
|
const MAX_BYTES = 1_000_000;
|
|
14
15
|
// The scoped browser runner bounds context cleanup to 1.5 seconds. Leave room
|
|
@@ -24,10 +25,6 @@ export function normalizeNumber(raw) {
|
|
|
24
25
|
export function trackingUrl(raw) {
|
|
25
26
|
return `https://mydhl.express.dhl/gb/en/tracking.html#/results?id=${normalizeNumber(raw)}`;
|
|
26
27
|
}
|
|
27
|
-
function apiUrl(number) {
|
|
28
|
-
return `${API}?${new URLSearchParams({ AWB: number, clientApp: 'mydhlplus', countryCode: 'gb', languageCode: 'en',
|
|
29
|
-
requestAdditionalDetails: 'controlledAccessDataCodes,productCode,shipmentActivationDate,countryCodes' }).toString().replace(/%2C/g, ',')}`;
|
|
30
|
-
}
|
|
31
28
|
/** MyDHL clocks are local to each facility; they do not carry a UTC offset. */
|
|
32
29
|
function clock(date, time) {
|
|
33
30
|
if (typeof date !== 'string' || typeof time !== 'string' || !/^\d{2}:\d{2}$/.test(time))
|
|
@@ -131,19 +128,37 @@ export function parse(payload, raw) {
|
|
|
131
128
|
...(typeof shipment.consigneeCountryCode === 'string' && /^[A-Z]{2}$/.test(shipment.consigneeCountryCode)
|
|
132
129
|
? { destination_country: shipment.consigneeCountryCode } : {}) };
|
|
133
130
|
}
|
|
131
|
+
/** Guest mobile history has facility-local clocks and orders scans by counter. */
|
|
132
|
+
export function parseMobile(payload, raw) {
|
|
133
|
+
const number = normalizeNumber(raw);
|
|
134
|
+
if (!Array.isArray(payload) || payload.length > 20 || payload.some((shipment) => !isRecord(shipment)))
|
|
135
|
+
throw new SchemaError(PROVIDER, 'DHL mobile tracking returned invalid shipments');
|
|
136
|
+
if (!payload.length)
|
|
137
|
+
throw new NotFoundError(PROVIDER);
|
|
138
|
+
const shipments = payload.filter(isRecord).map((shipment) => {
|
|
139
|
+
if (!Array.isArray(shipment.checkpoints) || shipment.checkpoints.length > 1000)
|
|
140
|
+
throw new SchemaError(PROVIDER, 'DHL mobile tracking returned invalid checkpoints');
|
|
141
|
+
const counters = new Set();
|
|
142
|
+
const checkpoints = shipment.checkpoints.map((row) => {
|
|
143
|
+
if (!isRecord(row) || typeof row.counter !== 'number' || !Number.isSafeInteger(row.counter) || row.counter < 1 || counters.has(row.counter))
|
|
144
|
+
throw new SchemaError(PROVIDER, 'DHL mobile tracking returned an invalid checkpoint counter');
|
|
145
|
+
counters.add(row.counter);
|
|
146
|
+
return { ...row, counter: row.counter, date: typeof row.date_en === 'string' ? row.date_en : row.date };
|
|
147
|
+
}).sort((a, b) => b.counter - a.counter);
|
|
148
|
+
return { ...shipment, checkpoints };
|
|
149
|
+
});
|
|
150
|
+
return parse({ results: shipments }, number);
|
|
151
|
+
}
|
|
134
152
|
export class DhlExpressTracker {
|
|
135
153
|
environment;
|
|
154
|
+
mobileDevice = randomUUID();
|
|
136
155
|
constructor(environment) {
|
|
137
156
|
this.environment = environment;
|
|
138
157
|
}
|
|
139
158
|
async direct(number, context) {
|
|
159
|
+
const normalized = normalizeNumber(number);
|
|
140
160
|
const budget = lookupBudget(context, 10_000, PROVIDER);
|
|
141
|
-
|
|
142
|
-
headers: { accept: 'application/json', 'user-agent': this.environment.userAgent ?? 'Mozilla/5.0' }, signal: budget.signal,
|
|
143
|
-
}, { provider: PROVIDER, timeoutMs: budget.remainingMs(), maxBytes: MAX_BYTES, fetcher: this.environment.fetcher });
|
|
144
|
-
if (/^\s*</.test(decodeText(bytes)))
|
|
145
|
-
throw new ChallengeError(PROVIDER, 'DHL Express rejected the tracking request');
|
|
146
|
-
return this.decode(bytes, number);
|
|
161
|
+
return parseMobile(await mobileTracking(normalized, budget, this.environment, this.mobileDevice), normalized);
|
|
147
162
|
}
|
|
148
163
|
async browser(number, context) {
|
|
149
164
|
const budget = lookupBudget(context, 45_000, PROVIDER);
|
|
@@ -192,16 +207,6 @@ export class DhlExpressTracker {
|
|
|
192
207
|
throw new SchemaError(PROVIDER, 'DHL Express returned invalid browser data');
|
|
193
208
|
return parseUnified(parseJsonBytes(new TextEncoder().encode(response.body), PROVIDER), number);
|
|
194
209
|
}
|
|
195
|
-
decode(bytes, number) {
|
|
196
|
-
let payload;
|
|
197
|
-
try {
|
|
198
|
-
payload = parseJsonBytes(bytes, PROVIDER);
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
throw new SchemaError(PROVIDER, 'DHL Express returned invalid tracking JSON', { cause: error });
|
|
202
|
-
}
|
|
203
|
-
return parse(payload, number);
|
|
204
|
-
}
|
|
205
210
|
fetch(raw, context) {
|
|
206
211
|
const number = normalizeNumber(raw);
|
|
207
212
|
return runSteps({ carrier: 'dhl-express', budgetMs: context?.budgetMs ?? 45_000, signal: context?.signal, recorder: this.environment.recorder }, [
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { AdapterEnvironment, LookupBudget } from '../../core/adapter/index.js';
|
|
2
|
+
/** Verify current application configuration before treating an empty tracking list as missing. */
|
|
3
|
+
export declare function mobileTracking(number: string, budget: LookupBudget, environment: AdapterEnvironment, device: string): Promise<unknown>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { ChallengeError, RateLimitedError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
3
|
+
import { decodeText, fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
|
|
4
|
+
import { isRecord } from '../../core/types.js';
|
|
5
|
+
const PROVIDER = 'DHL Express';
|
|
6
|
+
const APP = 'com.dhl.exp.dhlmobile';
|
|
7
|
+
const VERSION = '6.1.0';
|
|
8
|
+
const API = `https://dhle.dhl.com/access/access/${APP}`;
|
|
9
|
+
// The shared guest application bearer is deliberately distributed with this adapter.
|
|
10
|
+
const APPLICATION_BEARER = "Bearer MtX6uKNcGM295BXaJGa83GFhTHCL9a";
|
|
11
|
+
/** Mobile errors can arrive as JSON strings or envelopes with HTTP 200. */
|
|
12
|
+
function checkReply(payload, httpStatus) {
|
|
13
|
+
const root = isRecord(payload) ? payload : {};
|
|
14
|
+
const nested = isRecord(root.error) ? root.error : {};
|
|
15
|
+
const codes = [payload, root.error, root.statusCode, root.status, root.code, root.message,
|
|
16
|
+
nested.error, nested.statusCode, nested.status, nested.code, nested.message]
|
|
17
|
+
.filter((value) => typeof value === 'string' || typeof value === 'number')
|
|
18
|
+
.map((value) => String(value).trim());
|
|
19
|
+
if (codes.some((code) => ['429', 'temporary_blocked', 'max_attempts_reached'].includes(code)))
|
|
20
|
+
throw new RateLimitedError(PROVIDER);
|
|
21
|
+
if (codes.some((code) => ['401', '403', '428', 'DRG10012', 'DRG10013'].includes(code)))
|
|
22
|
+
throw new ChallengeError(PROVIDER, 'DHL mobile tracking requires verification', httpStatus === 200 ? undefined : { status: httpStatus });
|
|
23
|
+
const serverError = codes.find((code) => /^5\d{2}$/.test(code));
|
|
24
|
+
if (serverError)
|
|
25
|
+
throw new UpstreamHttpError(PROVIDER, Number(serverError));
|
|
26
|
+
}
|
|
27
|
+
/** Verify current application configuration before treating an empty tracking list as missing. */
|
|
28
|
+
export async function mobileTracking(number, budget, environment, device) {
|
|
29
|
+
const request = async (method, service, extra) => {
|
|
30
|
+
budget.signal.throwIfAborted();
|
|
31
|
+
const { response, bytes } = await fetchBounded(`${API}?${new URLSearchParams({ appVersion: VERSION, service: `${service}-${method}` })}`, {
|
|
32
|
+
method: 'POST', signal: budget.signal,
|
|
33
|
+
headers: { accept: 'application/json', 'content-type': 'application/json', authorization: APPLICATION_BEARER,
|
|
34
|
+
'cache-control': 'no-cache', transactionId: randomUUID(), 'user-agent': environment.userAgent ?? 'Mozilla/5.0' },
|
|
35
|
+
body: JSON.stringify({ method, service, authentication: { provider: 'DEMP.RS1', token: '', login: '' },
|
|
36
|
+
data: { parameters: {}, timezoneOffset: '+00:00', appVersion: VERSION, UIClient: 'Android',
|
|
37
|
+
device_info: { device_unique_id: device, deviceId: device, device_language: 'en', app_id: APP,
|
|
38
|
+
platform: 'Android', appStore: 'PlayStore', gpsLoc: '', pushToken: '' }, ...extra } }),
|
|
39
|
+
}, { provider: PROVIDER, timeoutMs: budget.remainingMs(), maxBytes: 1_000_000, fetcher: environment.fetcher,
|
|
40
|
+
allowHttpStatuses: [400, 404, 410, 503] });
|
|
41
|
+
budget.signal.throwIfAborted();
|
|
42
|
+
if (response.status === 404 || response.status === 410)
|
|
43
|
+
throw new TransportError(PROVIDER, 'DHL mobile tracking endpoint is unavailable', { status: response.status });
|
|
44
|
+
if (/^\s*</.test(decodeText(bytes))) {
|
|
45
|
+
if (response.status !== 200)
|
|
46
|
+
throw new UpstreamHttpError(PROVIDER, response.status);
|
|
47
|
+
throw new ChallengeError(PROVIDER, 'DHL mobile tracking rejected the request');
|
|
48
|
+
}
|
|
49
|
+
let payload;
|
|
50
|
+
try {
|
|
51
|
+
payload = parseJsonBytes(bytes, PROVIDER);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (response.status !== 200)
|
|
55
|
+
throw new UpstreamHttpError(PROVIDER, response.status);
|
|
56
|
+
throw new SchemaError(PROVIDER, 'DHL mobile tracking returned invalid JSON', { cause: error });
|
|
57
|
+
}
|
|
58
|
+
checkReply(payload, response.status);
|
|
59
|
+
if (response.status !== 200)
|
|
60
|
+
throw new UpstreamHttpError(PROVIDER, response.status);
|
|
61
|
+
return payload;
|
|
62
|
+
};
|
|
63
|
+
const settings = await request('countrySettingsBySettingName', 'common', { countryCode: 'GB', settingNames: ['api_awb_encryption'] });
|
|
64
|
+
if (!Array.isArray(settings) || settings.length > 20 || settings.some((setting) => !isRecord(setting)))
|
|
65
|
+
throw new SchemaError(PROVIDER, 'DHL mobile tracking returned invalid settings');
|
|
66
|
+
const encryption = settings.filter(isRecord).filter((setting) => setting.name === 'api_awb_encryption');
|
|
67
|
+
if (encryption.length !== 1 || (encryption[0].value !== 'N' && encryption[0].value !== 'Y'))
|
|
68
|
+
throw new SchemaError(PROVIDER, 'DHL mobile tracking returned invalid encryption settings');
|
|
69
|
+
if (encryption[0].value === 'Y')
|
|
70
|
+
throw new ChallengeError(PROVIDER, 'DHL mobile tracking requires waybill encryption');
|
|
71
|
+
return request('tracking', 'shipments', { airWayBill: number, countryCode: 'GB', languageCd: 'en', addShipmentToODD: 'N',
|
|
72
|
+
moreDetails: 'Y', iv: Buffer.from(randomBytes(8).toString('hex')).toString('base64'), captchaVerificationData: {} });
|
|
73
|
+
}
|
package/dist/server/openapi.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1-main.344",
|
|
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",
|