universal-parcel-scraper 0.3.1-main.312 → 0.3.1-main.313
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/dist/carriers/amazon-shipping/adapter.js +1 -1
- package/dist/carriers/ciblex/parser.js +1 -1
- package/dist/carriers/correos-spain/adapter.js +2 -1
- package/dist/carriers/ctt-express/parser.js +2 -1
- package/dist/carriers/dachser/adapter.js +3 -3
- package/dist/carriers/dachser/status.js +1 -1
- package/dist/carriers/dhl-ecommerce/status.js +1 -1
- package/dist/carriers/dpd/adapter.js +1 -1
- package/dist/carriers/estafeta/parser.js +1 -1
- package/dist/carriers/fedex/adapter.js +2 -1
- package/dist/carriers/gls-de/adapter.js +2 -2
- package/dist/carriers/heppner/adapter.js +1 -1
- package/dist/carriers/hermes/adapter.js +15 -10
- package/dist/carriers/hermes/status.js +1 -1
- package/dist/carriers/hermes-de/adapter.js +1 -1
- package/dist/carriers/india-post/adapter.js +1 -1
- package/dist/carriers/mondial-relay/adapter.js +1 -1
- package/dist/carriers/nacex/parser.js +1 -1
- package/dist/carriers/poste-italiane/adapter.js +1 -1
- package/dist/carriers/sunyou/adapter.js +2 -2
- package/dist/carriers/swiss-post/adapter.js +3 -2
- package/dist/carriers/the-courier-guy/parser.js +3 -2
- package/dist/carriers/ukrposhta/parser.js +1 -1
- package/dist/carriers/uniuni/parser.js +2 -1
- package/dist/carriers/ups/adapter.js +2 -2
- package/dist/carriers/yto/parser.js +2 -1
- package/dist/carriers/yunda/adapter.js +2 -2
- package/dist/core/transport/trawl.js +2 -2
- package/dist/server/index.js +11 -4
- package/package.json +2 -2
|
@@ -181,7 +181,7 @@ export function parseAmazonShippingTrackingResponse(payload, zone = 'Europe/Pari
|
|
|
181
181
|
const identitySummary = isRecord(progress.summary) ? progress.summary : {};
|
|
182
182
|
const identityMetadata = isRecord(identitySummary.metadata) ? identitySummary.metadata : {};
|
|
183
183
|
for (const identity of [payload.trackingId, payload.trackingID, progress.trackingId, metadataValue(identityMetadata, 'trackingId')]) {
|
|
184
|
-
if (trackingNumber && identity != null &&
|
|
184
|
+
if (trackingNumber && identity != null && (typeof identity !== 'string' || identity.toUpperCase() !== trackingNumber)) {
|
|
185
185
|
throw new SchemaError(PROVIDER, 'Amazon Shipping returned a different tracking number');
|
|
186
186
|
}
|
|
187
187
|
}
|
|
@@ -33,7 +33,7 @@ function safeLocation(raw) {
|
|
|
33
33
|
const value = clean(raw, 100);
|
|
34
34
|
// Free-form places can contain the recipient address. Retain only the
|
|
35
35
|
// established operational-depot label with a repeated department code.
|
|
36
|
-
return /^([\p{Letter}\p{Mark} .'
|
|
36
|
+
return /^([\p{Letter}\p{Mark} .'/-]{1,70}) (\d{2,3}) \(\2\)$/u.test(value) ? value : '';
|
|
37
37
|
}
|
|
38
38
|
export function parseCiblexTrackingHtml(html, raw) {
|
|
39
39
|
const number = normalizeCiblexTrackingNumber(raw);
|
|
@@ -58,7 +58,8 @@ export function parseCorreosSpainTrackingResponse(payload, trackingNumber) {
|
|
|
58
58
|
if (returned !== requested)
|
|
59
59
|
throw new SchemaError('Correos', 'Correos returned a different shipment');
|
|
60
60
|
const error = envelope.error;
|
|
61
|
-
if (!isRecord(error) || error.codError
|
|
61
|
+
if (!isRecord(error) || (typeof error.codError !== 'string'
|
|
62
|
+
&& (typeof error.codError !== 'number' || !Number.isFinite(error.codError)))) {
|
|
62
63
|
throw new SchemaError('Correos', 'Correos returned a response without a result envelope');
|
|
63
64
|
}
|
|
64
65
|
// Any non-zero codError (e.g. "3" Sin Trazabilidad) means unknown or
|
|
@@ -49,9 +49,10 @@ export function parseCttExpress(payload, number) {
|
|
|
49
49
|
if (history.item_code !== `${requested}001`)
|
|
50
50
|
throw new SchemaError('CTT Express', 'CTT Express returned a different package');
|
|
51
51
|
const events = [];
|
|
52
|
+
const rows = history.events;
|
|
52
53
|
// Newest provider positions win equal-clock ties. Preserve duplicates until
|
|
53
54
|
// their chronological leg is known so an earlier return start is not lost.
|
|
54
|
-
for (const row of [...
|
|
55
|
+
for (const row of [...rows].reverse()) {
|
|
55
56
|
if (!isRecord(row) || row.type !== 'STATUS' || !['ITEM_STATUS_V2', 'ITEM_STATUS_CHANGE_V1'].includes(clean(row.source, 64))) {
|
|
56
57
|
throw new SchemaError('CTT Express', 'CTT Express returned an unsupported scan source');
|
|
57
58
|
}
|
|
@@ -3,7 +3,7 @@ export { validateDachserTrackingUrl } from '../../core/catalog/urls.js';
|
|
|
3
3
|
import { DateTime } from 'luxon';
|
|
4
4
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
5
5
|
import { InputRequiredError, InvalidInputError, NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
6
|
-
import { fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
6
|
+
import { cleanScalar, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
7
7
|
import { isRecord } from '../../core/types.js';
|
|
8
8
|
import { eventLabel, plainText, shipmentStatus } from './status.js';
|
|
9
9
|
const DACHSER_HOST = 'customeriberia.dachser.com';
|
|
@@ -20,7 +20,7 @@ const DATE_FORMATS = [
|
|
|
20
20
|
'yyyy-MM-dd',
|
|
21
21
|
];
|
|
22
22
|
function normalizeTrackingNumber(raw) {
|
|
23
|
-
return
|
|
23
|
+
return cleanScalar(raw, 64).replace(/[\s.-]/g, '').toUpperCase();
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Validate and canonicalize a pasted Dachser capability URL.
|
|
@@ -51,7 +51,7 @@ export function dachserApiUrl(trackingUrl, trackingNumber) {
|
|
|
51
51
|
* that do carry an offset keep it; everything else is read in Europe/Madrid.
|
|
52
52
|
*/
|
|
53
53
|
function parseDateTime(raw) {
|
|
54
|
-
const value =
|
|
54
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
55
55
|
if (!value)
|
|
56
56
|
return null;
|
|
57
57
|
let parsed = DateTime.fromISO(value, { zone: 'Europe/Madrid', setZone: true });
|
|
@@ -34,7 +34,7 @@ export function stageFor(event) {
|
|
|
34
34
|
// A terminal provider code outranks an intuitive translated label.
|
|
35
35
|
if (event.statusCode === 'delivered')
|
|
36
36
|
return 'delivered';
|
|
37
|
-
const translated = trackingLanguageStage(
|
|
37
|
+
const translated = trackingLanguageStage(typeof event.description === 'string' ? event.description : '');
|
|
38
38
|
if (translated)
|
|
39
39
|
return translated;
|
|
40
40
|
switch (event.statusCode) {
|
|
@@ -442,7 +442,7 @@ export function parseDPDTrackingHtml(html, trackingNumber) {
|
|
|
442
442
|
};
|
|
443
443
|
}
|
|
444
444
|
function durationSeconds(value, fallback) {
|
|
445
|
-
const match = /^(\d+)s?$/.exec(String(value
|
|
445
|
+
const match = /^(\d+)s?$/.exec(typeof value === 'string' || typeof value === 'number' ? String(value) : '');
|
|
446
446
|
return match ? Number(match[1]) : fallback;
|
|
447
447
|
}
|
|
448
448
|
/** `shared`, for as long as the lookup waiting on it is live: its own signal ends the wait, not the shared work. */
|
|
@@ -50,7 +50,7 @@ export function parseEstafetaLookup(html, rawNumber) {
|
|
|
50
50
|
throw new SchemaError('Estafeta', 'Estafeta returned a different history target');
|
|
51
51
|
const state = cards.find('.stateDescription.fontColorCurrentProcess');
|
|
52
52
|
const messages = cards.find('.fontColorCurrentProcessMessage');
|
|
53
|
-
const parts = messages.contents().filter((_, node) => node.
|
|
53
|
+
const parts = messages.contents().filter((_, node) => node.nodeType === 3).map((_, node) => clean($(node).text(), 100)).get().filter(Boolean);
|
|
54
54
|
if (state.length !== 1 || messages.length !== 1 || parts.length < 2)
|
|
55
55
|
throw new SchemaError('Estafeta', 'Estafeta returned incomplete latest activity');
|
|
56
56
|
return { number, guide, state: clean(state.text(), 200), latestClock: parts[0], latestDate: parts[1] };
|
|
@@ -279,7 +279,8 @@ export class FedExTracker {
|
|
|
279
279
|
throw error;
|
|
280
280
|
// An unreadable or unrelated reply; the rendered page may still name a challenge.
|
|
281
281
|
captureError ??= error instanceof SyntaxError
|
|
282
|
-
? new SchemaError('FedEx', 'FedEx returned unreadable tracking JSON', { cause: error })
|
|
282
|
+
? new SchemaError('FedEx', 'FedEx returned unreadable tracking JSON', { cause: error })
|
|
283
|
+
: error instanceof Error ? error : new SchemaError('FedEx', 'FedEx returned invalid tracking data', { cause: error });
|
|
283
284
|
}
|
|
284
285
|
}
|
|
285
286
|
if (parseFedExTrackingHtml(page.html) === 'challenged') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
2
2
|
import { NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
3
|
-
import { fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
|
|
3
|
+
import { cleanScalar, fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
|
|
4
4
|
import { isRecord } from '../../core/types.js';
|
|
5
5
|
import { glsSwitzerlandDetailApiUrl, glsDeliveryReference, glsSwitzerlandOverviewApiUrl, GLSSwitzerlandTrackingError, normalizeGLSSwitzerlandPostcode, normalizeGLSSwitzerlandTrackingNumber, parseGLSSwitzerlandTrackingResponse, selectGLSParcel, } from '../gls-ch/adapter.js';
|
|
6
6
|
// GLS's GROUP recipient service covers German and Swiss parcels. Keep the
|
|
@@ -53,7 +53,7 @@ export class GLSGermanyTracker {
|
|
|
53
53
|
}
|
|
54
54
|
const owners = Array.isArray(parcel.owners) ? parcel.owners.filter(isRecord) : [];
|
|
55
55
|
const owner = owners.find((row) => row.type === 'REQUEST');
|
|
56
|
-
const detail = await this.request(glsSwitzerlandDetailApiUrl(String(parcel.tuNo), postcode, this.now(),
|
|
56
|
+
const detail = await this.request(glsSwitzerlandDetailApiUrl(String(parcel.tuNo), postcode, this.now(), cleanScalar(owner?.code, 64), '4,5'), budget);
|
|
57
57
|
try {
|
|
58
58
|
const result = parseGLSSwitzerlandTrackingResponse(detail, String(parcel.tuNo));
|
|
59
59
|
// Both services use CET/CEST; expose the regional timezone to clients.
|
|
@@ -80,7 +80,7 @@ export function parseHeppnerCapability(rawCapability, rawTrackingNumber, rawPost
|
|
|
80
80
|
|| !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(capability)) {
|
|
81
81
|
throw new SchemaError('Heppner', 'Heppner returned an invalid tracking capability');
|
|
82
82
|
}
|
|
83
|
-
let decoded
|
|
83
|
+
let decoded;
|
|
84
84
|
try {
|
|
85
85
|
decoded = Buffer.from(capability, 'base64').toString('utf8');
|
|
86
86
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
2
2
|
import { NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
3
|
-
import { fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
3
|
+
import { cleanScalar, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
|
|
4
4
|
import { isRecord } from '../../core/types.js';
|
|
5
5
|
import { hermesEventStage, hermesStatus } from './status.js';
|
|
6
6
|
export { hermesStatus };
|
|
@@ -11,7 +11,7 @@ const HERMES_API = 'https://myhes.de/api/request/auftragsdaten';
|
|
|
11
11
|
const PROVIDER = 'Hermes';
|
|
12
12
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
13
13
|
function normalizeHermesTrackingNumber(raw) {
|
|
14
|
-
return
|
|
14
|
+
return cleanScalar(raw, 64).replace(/[\s.-]/g, '').toUpperCase();
|
|
15
15
|
}
|
|
16
16
|
export function parseHermesTrackingResponse(payload, trackingNumber) {
|
|
17
17
|
if (!isRecord(payload))
|
|
@@ -43,19 +43,24 @@ export function parseHermesTrackingResponse(payload, trackingNumber) {
|
|
|
43
43
|
&& order.auftragsart == null
|
|
44
44
|
&& order.statusjourneyDto == null)
|
|
45
45
|
throw new NotFoundError(PROVIDER);
|
|
46
|
-
const meaningfulEvents = rawEvents.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const meaningfulEvents = rawEvents.flatMap((event) => {
|
|
47
|
+
const description = event.sendungsstatus;
|
|
48
|
+
const time = event.sendungsstatusBuchungszeitpunkt;
|
|
49
|
+
if ((description != null && typeof description !== 'string') || (time != null && typeof time !== 'string')) {
|
|
50
|
+
throw new SchemaError(PROVIDER, 'Hermes returned invalid tracking history');
|
|
51
|
+
}
|
|
52
|
+
return description?.trim() && time?.trim() ? [{ description, time, statusId: event.sendungsstatusId }] : [];
|
|
53
|
+
});
|
|
54
|
+
meaningfulEvents.sort((left, right) => right.time.localeCompare(left.time));
|
|
50
55
|
const events = meaningfulEvents.map((event) => ({
|
|
51
|
-
time:
|
|
56
|
+
time: event.time,
|
|
52
57
|
location: '',
|
|
53
|
-
description:
|
|
54
|
-
stage: hermesEventStage(event.
|
|
58
|
+
description: event.description,
|
|
59
|
+
stage: hermesEventStage(event.statusId, event.description),
|
|
55
60
|
}));
|
|
56
61
|
return {
|
|
57
62
|
status: meaningfulEvents[0]
|
|
58
|
-
? hermesStatus(meaningfulEvents[0].
|
|
63
|
+
? hermesStatus(meaningfulEvents[0].statusId, meaningfulEvents[0].description)
|
|
59
64
|
: 'pending',
|
|
60
65
|
last_status_text: events[0]?.description ?? '',
|
|
61
66
|
last_update: events[0]?.time || null,
|
|
@@ -25,7 +25,7 @@ export const HERMES_STATUS = new Map([
|
|
|
25
25
|
[430, 'out_for_delivery'],
|
|
26
26
|
]);
|
|
27
27
|
function normalizedText(raw) {
|
|
28
|
-
return
|
|
28
|
+
return (typeof raw === 'string' ? raw : '').toLocaleLowerCase('de-DE').normalize('NFKD').replace(/\p{M}/gu, '');
|
|
29
29
|
}
|
|
30
30
|
export function hermesStatus(rawStatusId, rawDescription = '') {
|
|
31
31
|
if (!['string', 'number'].includes(typeof rawStatusId))
|
|
@@ -42,7 +42,7 @@ export function normalizeHermesGermanyNumber(raw) {
|
|
|
42
42
|
}
|
|
43
43
|
export function parseHermesGermanyResponse(payload, trackingNumber) {
|
|
44
44
|
const requested = normalizeHermesGermanyNumber(trackingNumber);
|
|
45
|
-
if (!Array.isArray(payload) || payload.
|
|
45
|
+
if (!Array.isArray(payload) || !payload.every(isRecord)) {
|
|
46
46
|
throw new SchemaError(CARRIER, 'Hermes Germany returned an invalid tracking response');
|
|
47
47
|
}
|
|
48
48
|
if (!payload.length)
|
|
@@ -417,7 +417,7 @@ export class IndiaPostTracker {
|
|
|
417
417
|
const initial = initialTrackComponent(page.html, normalized);
|
|
418
418
|
if (initial.status === 'Completed') {
|
|
419
419
|
const cached = parseIndiaPostTrackingHtml(page.html, normalized);
|
|
420
|
-
const syncedAt = Date.parse(
|
|
420
|
+
const syncedAt = typeof cached.source_synced_at === 'string' ? Date.parse(cached.source_synced_at) : NaN;
|
|
421
421
|
if (Number.isFinite(syncedAt) && this.now().getTime() - syncedAt < REFRESH_AFTER_MS)
|
|
422
422
|
return cached;
|
|
423
423
|
// What the page's Refresh button sends. A failed refresh still leaves
|
|
@@ -227,7 +227,7 @@ function trawlJson(response) {
|
|
|
227
227
|
const $ = load(response.html);
|
|
228
228
|
candidates.push($('pre').first().text(), $('body').text(), response.html);
|
|
229
229
|
for (const candidate of candidates) {
|
|
230
|
-
const cleaned = candidate.trim().replace(
|
|
230
|
+
const cleaned = candidate.trim().replace(/^\uFEFF/, '');
|
|
231
231
|
if (!cleaned)
|
|
232
232
|
continue;
|
|
233
233
|
try {
|
|
@@ -96,7 +96,7 @@ export function parseNacex(html, raw) {
|
|
|
96
96
|
}
|
|
97
97
|
const mapped = classifyNacexStatus(label);
|
|
98
98
|
const movement = mapped && ['accepted', 'in_transit', 'out_for_delivery', 'ready_for_pickup'].includes(mapped.stage);
|
|
99
|
-
const trailing = clean(cell.contents().filter((_, node) => node.
|
|
99
|
+
const trailing = clean(cell.contents().filter((_, node) => node.nodeType === 3).map((_, node) => $(node).text()).get().join(' '), 300);
|
|
100
100
|
// The same trailing column holds a recipient after delivery. Only the
|
|
101
101
|
// observed depot/locality shape of a mapped movement scan is a location.
|
|
102
102
|
const locality = movement && /^\d{4}(?:-\d{1,3})?\s*-\s*([\p{L}][\p{L}\s.()-]{1,99})$/u.exec(trailing)?.[1];
|
|
@@ -124,7 +124,7 @@ export function parsePosteItalianeTrackingResponse(payload, trackingNumber) {
|
|
|
124
124
|
const events = parsed.slice(0, MAX_EVENTS_TO_RETURN).map(({ event }) => event);
|
|
125
125
|
// Envelope stato "5" forces delivered; otherwise the newest mapped event wins
|
|
126
126
|
// and unmapped wording stays unknown with its raw text preserved.
|
|
127
|
-
if (
|
|
127
|
+
if (payload.stato === '5' || payload.stato === 5) {
|
|
128
128
|
return {
|
|
129
129
|
status: 'delivered',
|
|
130
130
|
current_stage: 'delivered',
|
|
@@ -14,7 +14,7 @@ import { randomInt } from 'node:crypto';
|
|
|
14
14
|
import { DateTime } from 'luxon';
|
|
15
15
|
import { lookupBudget } from '../../core/adapter/index.js';
|
|
16
16
|
import { NotFoundError, SchemaError } from '../../core/errors/index.js';
|
|
17
|
-
import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
|
|
17
|
+
import { cleanScalar, decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
|
|
18
18
|
import { isRecord } from '../../core/types.js';
|
|
19
19
|
import { sunYouStatus } from './status.js';
|
|
20
20
|
const PROVIDER = 'SunYou';
|
|
@@ -99,7 +99,7 @@ export function parseSunYouTrackingResponse(value, trackingNumber) {
|
|
|
99
99
|
const item = identified.find((candidate) => comparableIdentifier(candidate.orderNo) === requested);
|
|
100
100
|
if (!item)
|
|
101
101
|
throw new SchemaError(PROVIDER, 'SunYou returned a different shipment');
|
|
102
|
-
const displayStatus =
|
|
102
|
+
const displayStatus = cleanScalar(item.displayStatus, 16);
|
|
103
103
|
if (displayStatus === '0')
|
|
104
104
|
throw new NotFoundError(PROVIDER);
|
|
105
105
|
if (item.has !== true)
|
|
@@ -29,14 +29,15 @@ const HEADERS = {
|
|
|
29
29
|
* deliberately not used here.
|
|
30
30
|
*/
|
|
31
31
|
function text(value, limit = 500) {
|
|
32
|
-
return
|
|
32
|
+
return typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))
|
|
33
|
+
? String(value).trim().slice(0, limit) : '';
|
|
33
34
|
}
|
|
34
35
|
/** Whether the lookup was cancelled or has spent its budget, rather than a request failing on its own. */
|
|
35
36
|
function ended(budget) {
|
|
36
37
|
return budget.signal.aborted || budget.remainingMs() <= BUDGET_SLACK_MS;
|
|
37
38
|
}
|
|
38
39
|
function comparableShipmentNumber(value) {
|
|
39
|
-
return
|
|
40
|
+
return text(value, 64).replace(/[\s.-]/g, '').toUpperCase();
|
|
40
41
|
}
|
|
41
42
|
function datePart(value) {
|
|
42
43
|
return /(?<!\d)(\d{4}-\d{2}-\d{2})(?!\d)/.exec(text(value))?.[1] ?? null;
|
|
@@ -43,8 +43,9 @@ export function parseCourierGuy(payload, rawNumber) {
|
|
|
43
43
|
throw new SchemaError('The Courier Guy');
|
|
44
44
|
// Cancelled, never-collected DD bookings omit their count. This bounded
|
|
45
45
|
// precollection timeline cannot complete a delivered or moving shipment.
|
|
46
|
+
const customReference = shipment.custom_tracking_reference;
|
|
46
47
|
const cancelledCollection = shipment.parcel_count == null && shipment.status === 'cancelled'
|
|
47
|
-
&& typeof
|
|
48
|
+
&& typeof customReference === 'string' && /^DD-[A-Z0-9]{6}$/.test(customReference)
|
|
48
49
|
&& Array.isArray(shipment.parcel_tracking_references)
|
|
49
50
|
&& shipment.parcel_tracking_references.length === 1
|
|
50
51
|
&& shipment.tracking_events.every(row => isRecord(row) && row.parcel_id === 0
|
|
@@ -53,7 +54,7 @@ export function parseCourierGuy(payload, rawNumber) {
|
|
|
53
54
|
|| !shipment.parcel_tracking_references.length
|
|
54
55
|
|| shipment.parcel_tracking_references.length > 500
|
|
55
56
|
|| shipment.parcel_tracking_references.some(ref => typeof ref !== 'string'
|
|
56
|
-
|| (!/^[A-Z0-9]{5,40}$/.test(ref) && !(cancelledCollection && ref === `${
|
|
57
|
+
|| (!/^[A-Z0-9]{5,40}$/.test(ref) && !(cancelledCollection && ref === `${customReference}/1`)))
|
|
57
58
|
|| new Set(shipment.parcel_tracking_references).size !== shipment.parcel_tracking_references.length)
|
|
58
59
|
throw new SchemaError('The Courier Guy', 'Invalid shipment pieces');
|
|
59
60
|
if (!cancelledCollection && (!Number.isSafeInteger(shipment.parcel_count) || shipment.parcel_count < 1
|
|
@@ -102,7 +102,7 @@ export function parseUkrposhtaHistory(payload, overview) {
|
|
|
102
102
|
// Both native histories are current-first. Keep that order across foreign
|
|
103
103
|
// wall clocks or missing dates instead of sorting them as UTC instants.
|
|
104
104
|
let returnLeg = false;
|
|
105
|
-
const events = Array(rows.length);
|
|
105
|
+
const events = new Array(rows.length);
|
|
106
106
|
for (let index = rows.length - 1; index >= 0; index--) {
|
|
107
107
|
const row = rows[index];
|
|
108
108
|
if (ukrposhtaReturnCue(row.label))
|
|
@@ -82,9 +82,10 @@ export function parseUniuni(payload, rawNumber) {
|
|
|
82
82
|
throw new SchemaError(PROVIDER);
|
|
83
83
|
const events = [];
|
|
84
84
|
const seen = new Set();
|
|
85
|
+
const scans = item.spath_list;
|
|
85
86
|
// The public client displays this oldest-first list in reverse. Keep that
|
|
86
87
|
// sequence when the newest clock is incomplete, including equal-time scans.
|
|
87
|
-
for (const scan of [...
|
|
88
|
+
for (const scan of [...scans].reverse()) {
|
|
88
89
|
if (!isRecord(scan))
|
|
89
90
|
throw new SchemaError(PROVIDER, 'UniUni returned an incomplete scan');
|
|
90
91
|
const description = clean(scan.description_en, 500);
|
|
@@ -245,7 +245,7 @@ function expectedDelivery(detail, today) {
|
|
|
245
245
|
export function parseUPSTrackingResponse(payload, trackingNumber, today = new Date()) {
|
|
246
246
|
if (!isRecord(payload))
|
|
247
247
|
throw new SchemaError('UPS');
|
|
248
|
-
if (
|
|
248
|
+
if (payload.statusCode !== '200' && payload.statusCode !== 200) {
|
|
249
249
|
throw new IndeterminateError('UPS', clean(payload.statusText) || 'UPS tracking is unavailable');
|
|
250
250
|
}
|
|
251
251
|
if (!Array.isArray(payload.trackDetails) || payload.trackDetails.length === 0)
|
|
@@ -437,7 +437,7 @@ export class UPSTracker {
|
|
|
437
437
|
}
|
|
438
438
|
catch (error) {
|
|
439
439
|
// An unreadable or unrelated reply; the rendered page may still answer.
|
|
440
|
-
captureError = error;
|
|
440
|
+
captureError = error instanceof Error ? error : new SchemaError('UPS', 'UPS returned invalid tracking data', { cause: error });
|
|
441
441
|
}
|
|
442
442
|
}
|
|
443
443
|
try {
|
|
@@ -34,10 +34,11 @@ export function parseYto(payload, trackingNumber) {
|
|
|
34
34
|
if (!item.waybillProcessInfo.length)
|
|
35
35
|
throw new IndeterminateError(PROVIDER, 'YTO returned no shipment history');
|
|
36
36
|
const events = [];
|
|
37
|
+
const history = item.waybillProcessInfo;
|
|
37
38
|
let returning = false;
|
|
38
39
|
// The official client uses the first row as current. Read oldest first to
|
|
39
40
|
// preserve a return leg when later scans use ordinary movement/signature codes.
|
|
40
|
-
for (const raw of [...
|
|
41
|
+
for (const raw of [...history].reverse()) {
|
|
41
42
|
if (!isRecord(raw) || normalizeTrackingNumber(clean(raw.waybillNo, 64)) !== number) {
|
|
42
43
|
throw new SchemaError(PROVIDER, 'YTO returned mixed or incomplete shipment history');
|
|
43
44
|
}
|
|
@@ -42,10 +42,10 @@ export class YundaTracker {
|
|
|
42
42
|
throw error;
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
-
const type = yundaEnvelope(await request('/index.php/api/order.record/captcha_type?' + params));
|
|
45
|
+
const type = yundaEnvelope(await request('/index.php/api/order.record/captcha_type?' + params.toString()));
|
|
46
46
|
if (type.data !== 1)
|
|
47
47
|
throw new ChallengeError('Yunda Express', 'Yunda requires an unsupported verification type');
|
|
48
|
-
const captcha = yundaEnvelope(await request('/index.php/api/order.record/captcha?' + params));
|
|
48
|
+
const captcha = yundaEnvelope(await request('/index.php/api/order.record/captcha?' + params.toString()));
|
|
49
49
|
if (!isRecord(captcha.data))
|
|
50
50
|
throw new SchemaError('Yunda Express');
|
|
51
51
|
const coordinate = await solveYundaSlider(captcha.data, signal);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { CarrierError, UpstreamHttpError } from '../errors/index.js';
|
|
12
12
|
import { decodeText, fetchBounded, parseJsonBytes } from './boundedFetch.js';
|
|
13
|
-
import {
|
|
13
|
+
import { cleanScalar } from './text.js';
|
|
14
14
|
import { isRecord } from '../types.js';
|
|
15
15
|
export class TrawlError extends CarrierError {
|
|
16
16
|
constructor(provider, message, options) {
|
|
@@ -186,7 +186,7 @@ export class TrawlClient {
|
|
|
186
186
|
if (!isRecord(value))
|
|
187
187
|
throw new TrawlError(options.provider, 'The browser service returned an invalid response');
|
|
188
188
|
if (value.error) {
|
|
189
|
-
throw new TrawlError(options.provider,
|
|
189
|
+
throw new TrawlError(options.provider, cleanScalar(value.error, 200) || 'The browser service could not fetch the page');
|
|
190
190
|
}
|
|
191
191
|
const statusCode = Number(value.statusCode);
|
|
192
192
|
if (Number.isInteger(statusCode) && statusCode >= 400)
|
package/dist/server/index.js
CHANGED
|
@@ -53,11 +53,15 @@ async function body(request) {
|
|
|
53
53
|
throw new HttpError(413, 'Request body is too large');
|
|
54
54
|
const chunks = [];
|
|
55
55
|
let length = 0;
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const stream = request;
|
|
57
|
+
for await (const chunk of stream) {
|
|
58
|
+
if (typeof chunk !== 'string' && !Buffer.isBuffer(chunk))
|
|
59
|
+
throw new HttpError(400, 'Invalid request body');
|
|
60
|
+
const bytes = Buffer.from(chunk);
|
|
61
|
+
length += bytes.length;
|
|
58
62
|
if (length > limit)
|
|
59
63
|
throw new HttpError(413, 'Request body is too large');
|
|
60
|
-
chunks.push(
|
|
64
|
+
chunks.push(bytes);
|
|
61
65
|
}
|
|
62
66
|
let parsed;
|
|
63
67
|
try {
|
|
@@ -204,7 +208,7 @@ export function createTrackingServer(options = {}) {
|
|
|
204
208
|
pending.delete(key);
|
|
205
209
|
}
|
|
206
210
|
}
|
|
207
|
-
|
|
211
|
+
async function handleRequest(request, response) {
|
|
208
212
|
const started = performance.now();
|
|
209
213
|
let route = 'unknown';
|
|
210
214
|
response.on('finish', () => {
|
|
@@ -297,6 +301,9 @@ export function createTrackingServer(options = {}) {
|
|
|
297
301
|
json(response, 502, { error: 'Tracking is temporarily unavailable' });
|
|
298
302
|
}
|
|
299
303
|
}
|
|
304
|
+
}
|
|
305
|
+
const server = createServer((request, response) => {
|
|
306
|
+
void handleRequest(request, response).catch(() => response.destroy());
|
|
300
307
|
});
|
|
301
308
|
server.requestTimeout = 15_000;
|
|
302
309
|
server.headersTimeout = 10_000;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-parcel-scraper",
|
|
3
|
-
"version": "0.3.1-main.
|
|
3
|
+
"version": "0.3.1-main.313",
|
|
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",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"build": "node scripts/build.mjs",
|
|
59
59
|
"prepare": "npm run build",
|
|
60
60
|
"typecheck": "tsc --noEmit",
|
|
61
|
-
"lint": "eslint . --max-warnings=0
|
|
61
|
+
"lint": "eslint . --max-warnings=0",
|
|
62
62
|
"test": "vitest run",
|
|
63
63
|
"test:scripts": "node --test trawl/*.test.mjs scripts/*.test.mjs",
|
|
64
64
|
"generate": "node scripts/generate-catalog.mjs && node scripts/generate-registry.mjs --strict && node scripts/generate-region-towns.mjs && node scripts/coverage-tables.mjs && node scripts/generate-readme.mjs",
|