universal-parcel-scraper 0.35.1-main.386 → 0.36.0

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.
@@ -30,6 +30,8 @@ export interface TrawlCapturedResponse {
30
30
  truncated: boolean;
31
31
  base64Encoded: boolean;
32
32
  error: string | null;
33
+ /** 17TRACK's matching browser request included the supplied delivery postcode. */
34
+ postcodeSubmitted?: boolean;
33
35
  }
34
36
  export interface TrawlScrapeResponse {
35
37
  url: string;
@@ -70,6 +70,7 @@ function captured(value) {
70
70
  truncated: entry.truncated === true,
71
71
  base64Encoded: entry.base64Encoded === true,
72
72
  error: typeof entry.error === 'string' ? entry.error : entry.error ? 'error' : null,
73
+ ...(entry.postcodeSubmitted === true ? { postcodeSubmitted: true } : {}),
73
74
  }));
74
75
  }
75
76
  /**
@@ -25,7 +25,7 @@ export declare class SeventeenTrackVerificationError extends ChallengeError {
25
25
  }
26
26
  /** Structured replies without usable history; polling may still produce a later success. */
27
27
  export declare function isSeventeenTrackLookupError(error: unknown): error is SeventeenTrackLookupError | SeventeenTrackNoHistoryError | SeventeenTrackVerificationError;
28
- export declare function parse17TrackResponse(payload: unknown, trackingNumber: string): CarrierResult;
28
+ export declare function parse17TrackResponse(payload: unknown, trackingNumber: string, postcode?: string | null): CarrierResult;
29
29
  export interface SeventeenTrackOptions {
30
30
  trawl?: TrawlClient | null;
31
31
  fetcher?: typeof fetch;
@@ -35,6 +35,6 @@ export interface SeventeenTrackOptions {
35
35
  export declare class SeventeenTrackTracker {
36
36
  readonly options: SeventeenTrackOptions;
37
37
  constructor(options?: SeventeenTrackOptions);
38
- fetch(trackingNumber: string, budgetMs?: number, signal?: AbortSignal): Promise<CarrierResult>;
38
+ fetch(trackingNumber: string, budgetMs?: number, signal?: AbortSignal, postcode?: string | null): Promise<CarrierResult>;
39
39
  }
40
40
  export declare const adapter: AdapterFactory;
@@ -9,7 +9,7 @@
9
9
  * verification wall from an outage.
10
10
  */
11
11
  import { DateTime } from 'luxon';
12
- import { ChallengeError, NoHistoryError, NotFoundError, SchemaError, TransportError } from '../../core/errors/index.js';
12
+ import { ChallengeError, InputRequiredError, InvalidInputError, NoHistoryError, NotFoundError, SchemaError, TransportError } from '../../core/errors/index.js';
13
13
  import { runSteps } from '../../core/runner/index.js';
14
14
  import { isRecord } from '../../core/types.js';
15
15
  import { universalCarrierHints } from '../shared/hints.js';
@@ -75,7 +75,13 @@ function facilityWall(raw) {
75
75
  return clock && typeof clock.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(clock.date)
76
76
  && typeof clock.time === 'string' && /^\d{2}:\d{2}:\d{2}$/.test(clock.time) ? `${clock.date}T${clock.time}` : null;
77
77
  }
78
- export function parse17TrackResponse(payload, trackingNumber) {
78
+ function requestsPostcode(shipment) {
79
+ if (Array.isArray(shipment.params_v2))
80
+ return shipment.params_v2.some(p => isRecord(p) && p.key === 'postal_code');
81
+ return (isRecord(shipment.param) && shipment.param.type === 'PostalCode')
82
+ || (Array.isArray(shipment.params) && shipment.params.some(p => isRecord(p) && p.type === 'PostalCode'));
83
+ }
84
+ export function parse17TrackResponse(payload, trackingNumber, postcode = null) {
79
85
  const number = numberOf(trackingNumber);
80
86
  if (!isRecord(payload) || !isRecord(payload.meta) || !Number.isInteger(payload.meta.code)) {
81
87
  throw new SchemaError(SOURCE, '17TRACK lookup unavailable');
@@ -94,8 +100,11 @@ export function parse17TrackResponse(payload, trackingNumber) {
94
100
  const code = Number(matches[0].code);
95
101
  // Observed repeatedly for valid references whose other providers have history.
96
102
  // Scope this to the matching shipment; envelope errors and unknown shapes remain failures.
97
- if (code === 400 && matches[0].shipment === null)
103
+ if (code === 400 && matches[0].shipment === null) {
104
+ if (!postcode && requestsPostcode(matches[0]))
105
+ throw new InputRequiredError(SOURCE, 'postcode');
98
106
  throw new SeventeenTrackNoHistoryError();
107
+ }
99
108
  throw new SeventeenTrackLookupError(code === 100 ? 'lookup_pending' : 'lookup_unavailable', code);
100
109
  }
101
110
  if (matches.length !== 1 || !isRecord(matches[0]) || matches[0].code !== 200 || !isRecord(matches[0].shipment)) {
@@ -156,10 +165,14 @@ export class SeventeenTrackTracker {
156
165
  constructor(options = {}) {
157
166
  this.options = options;
158
167
  }
159
- async fetch(trackingNumber, budgetMs = this.options.timeoutMs ?? 30_000, signal) {
168
+ async fetch(trackingNumber, budgetMs = this.options.timeoutMs ?? 30_000, signal, postcode = null) {
160
169
  const number = numberOf(trackingNumber);
170
+ postcode = postcode?.trim().replace(/\s+/g, ' ').toUpperCase() || null;
171
+ if (postcode && (postcode.length < 2 || postcode.length > 15 || !/^[A-Z0-9]+(?:[ -][A-Z0-9]+)*$/.test(postcode))) {
172
+ throw new InvalidInputError(SOURCE, '17TRACK delivery postcode has an invalid format');
173
+ }
161
174
  const spec = (remainingMs, signal) => ({
162
- source: SOURCE, url: `https://t.17track.net/en#nums=${number}`, apiUrl: API_URL,
175
+ source: SOURCE, url: `https://t.17track.net/en#nums=${number}${postcode ? `&trawl-postcode=${encodeURIComponent(postcode)}` : ''}`, apiUrl: API_URL,
163
176
  budgetMs: remainingMs, fetcher: this.options.fetcher, signal,
164
177
  });
165
178
  return runSteps({ carrier: SOURCE, budgetMs, signal, recorder: this.options.recorder }, [{
@@ -170,12 +183,28 @@ export class SeventeenTrackTracker {
170
183
  let pending;
171
184
  for (const body of capturedBodies(page, capture)) {
172
185
  try {
173
- return parse17TrackResponse(JSON.parse(body), number);
186
+ const payload = JSON.parse(body);
187
+ // Old browser-service builds ignore the private input marker. Never
188
+ // report that a supplied postcode was tried without a bound request.
189
+ const submitted = page.capturedResponses.some(entry => entry.body === body && entry.postcodeSubmitted === true);
190
+ if (postcode && !submitted) {
191
+ try {
192
+ return parse17TrackResponse(payload, number);
193
+ }
194
+ catch (error) {
195
+ if (error instanceof InputRequiredError || error instanceof SeventeenTrackNoHistoryError) {
196
+ throw new TransportError(SOURCE, '17TRACK browser service did not submit the delivery postcode', { reason: 'postcode_not_submitted' });
197
+ }
198
+ throw error;
199
+ }
200
+ }
201
+ return parse17TrackResponse(payload, number, postcode);
174
202
  }
175
203
  catch (error) {
176
204
  // Continue past polling replies and unrelated/demo numbers, but retain
177
205
  // the latest structured failure if no matching history follows.
178
- if (isSeventeenTrackLookupError(error) || error instanceof NotFoundError || error instanceof SchemaError)
206
+ if (isSeventeenTrackLookupError(error) || error instanceof NotFoundError || error instanceof SchemaError
207
+ || error instanceof InputRequiredError || error instanceof TransportError)
179
208
  pending ??= error;
180
209
  }
181
210
  }
@@ -191,6 +220,6 @@ export const adapter = (environment) => {
191
220
  return {
192
221
  id: SOURCE,
193
222
  steps: ['trawl'],
194
- track: (input, context) => tracker.fetch(input.number, context?.budgetMs, context?.signal),
223
+ track: (input, context) => tracker.fetch(input.number, context?.budgetMs, context?.signal, input.postcode),
195
224
  };
196
225
  };
@@ -55,7 +55,7 @@ export declare class UniversalTracker {
55
55
  * Look up one number through the whole chain. `postcode` is the parcel's
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
- * its direct API request; the other providers currently do not consume it.
58
+ * its direct API request; 17TRACK submits it through the browser's postcode form.
59
59
  * `countryHint` lets ParcelsApp retry an empty answer with that country.
60
60
  * The caller's budget covers the whole chain and its signal ends it.
61
61
  */
@@ -48,7 +48,7 @@ export class UniversalTracker {
48
48
  * Look up one number through the whole chain. `postcode` is the parcel's
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
- * its direct API request; the other providers currently do not consume it.
51
+ * its direct API request; 17TRACK submits it through the browser's postcode form.
52
52
  * `countryHint` lets ParcelsApp retry an empty answer with that country.
53
53
  * The caller's budget covers the whole chain and its signal ends it.
54
54
  */
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.35.1"
5
+ "version": "0.36.0"
6
6
  },
7
7
  "paths": {
8
8
  "/health": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.35.1-main.386",
3
+ "version": "0.36.0",
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",