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.
@@ -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 && String(identity).toUpperCase() !== trackingNumber) {
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} .'\/-]{1,70}) (\d{2,3}) \(\2\)$/u.test(value) ? value : '';
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 == null) {
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 [...history.events].reverse()) {
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 String(raw ?? '').replace(/[\s.-]/g, '').toUpperCase();
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 = String(raw ?? '').trim();
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 });
@@ -1,6 +1,6 @@
1
1
  /** Lower-cased, accent-free text, for language-independent matching. */
2
2
  export function plainText(raw) {
3
- return String(raw ?? '')
3
+ return (typeof raw === 'string' ? raw : '')
4
4
  .toLocaleLowerCase()
5
5
  .normalize('NFKD')
6
6
  .replace(/\p{M}/gu, '');
@@ -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(String(event.description ?? ''));
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.type === 'text').map((_, node) => clean($(node).text(), 100)).get().filter(Boolean);
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 }) : 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(), String(owner?.code ?? ''), '4,5'), budget);
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 String(raw ?? '').replace(/[\s.-]/g, '').toUpperCase();
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.filter((event) => (String(event.sendungsstatus ?? '').trim()
47
- && String(event.sendungsstatusBuchungszeitpunkt ?? '').trim()));
48
- meaningfulEvents.sort((left, right) => String(right.sendungsstatusBuchungszeitpunkt ?? '')
49
- .localeCompare(String(left.sendungsstatusBuchungszeitpunkt ?? '')));
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: String(event.sendungsstatusBuchungszeitpunkt ?? ''),
56
+ time: event.time,
52
57
  location: '',
53
- description: String(event.sendungsstatus),
54
- stage: hermesEventStage(event.sendungsstatusId, event.sendungsstatus),
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].sendungsstatusId, meaningfulEvents[0].sendungsstatus)
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 String(raw ?? '').toLocaleLowerCase('de-DE').normalize('NFKD').replace(/\p{M}/gu, '');
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.some((entry) => !isRecord(entry))) {
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(String(cached.source_synced_at ?? ''));
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.type === 'text').map((_, node) => $(node).text()).get().join(' '), 300);
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 (String(payload.stato ?? '') === '5') {
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 = String(item.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 String(value ?? '').trim().slice(0, limit);
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 String(value ?? '').replace(/[\s.-]/g, '').toUpperCase();
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 shipment.custom_tracking_reference === 'string' && /^DD-[A-Z0-9]{6}$/.test(shipment.custom_tracking_reference)
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 === `${shipment.custom_tracking_reference}/1`)))
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 [...item.spath_list].reverse()) {
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 (String(payload.statusCode ?? '') !== '200') {
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 [...item.waybillProcessInfo].reverse()) {
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 { clean } from './text.js';
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, clean(String(value.error), 200) || 'The browser service could not fetch the page');
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)
@@ -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
- for await (const chunk of request) {
57
- length += chunk.length;
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(Buffer.from(chunk));
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
- const server = createServer(async (request, response) => {
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.312",
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 --pass-on-unpruned-suppressions",
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",