universal-parcel-scraper 0.2.0 → 0.3.0-main.298

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.
Files changed (210) hide show
  1. package/README.md +209 -71
  2. package/data/carrier.schema.json +6 -4
  3. package/data/catalog.json +6 -6
  4. package/dist/app.d.ts +11 -0
  5. package/dist/app.js +11 -0
  6. package/dist/browser/scraper.js +1386 -1568
  7. package/dist/carriers/aliexpress/adapter.d.ts +5 -3
  8. package/dist/carriers/aliexpress/adapter.js +28 -8
  9. package/dist/carriers/amazon-shipping/adapter.d.ts +3 -2
  10. package/dist/carriers/amazon-shipping/adapter.js +15 -10
  11. package/dist/carriers/amazon-shipping/eligibility.d.ts +2 -1
  12. package/dist/carriers/amazon-shipping/eligibility.js +2 -2
  13. package/dist/carriers/aramex/adapter.d.ts +2 -0
  14. package/dist/carriers/aramex/adapter.js +5 -3
  15. package/dist/carriers/aramex/parser.js +2 -2
  16. package/dist/carriers/asendia/adapter.d.ts +8 -0
  17. package/dist/carriers/asendia/adapter.js +66 -11
  18. package/dist/carriers/asendia/probe.d.ts +2 -0
  19. package/dist/carriers/asendia/probe.js +9 -7
  20. package/dist/carriers/australia-post/adapter.js +2 -2
  21. package/dist/carriers/austrian-post/adapter.js +2 -2
  22. package/dist/carriers/blue-dart/parser.js +2 -2
  23. package/dist/carriers/bpost/parser.js +2 -2
  24. package/dist/carriers/bring-posten/parser.js +2 -2
  25. package/dist/carriers/brt/parser.js +2 -2
  26. package/dist/carriers/c-chez-vous/adapter.d.ts +3 -2
  27. package/dist/carriers/c-chez-vous/adapter.js +15 -10
  28. package/dist/carriers/canada-post/adapter.d.ts +1 -0
  29. package/dist/carriers/canada-post/adapter.js +5 -3
  30. package/dist/carriers/canada-post/parser.js +2 -2
  31. package/dist/carriers/canpar/parser.js +2 -2
  32. package/dist/carriers/ciblex/parser.js +2 -2
  33. package/dist/carriers/colis-prive/adapter.d.ts +3 -2
  34. package/dist/carriers/colis-prive/adapter.js +0 -0
  35. package/dist/carriers/colisweb/adapter.d.ts +3 -2
  36. package/dist/carriers/colisweb/adapter.js +13 -8
  37. package/dist/carriers/correios-br/parser.js +2 -2
  38. package/dist/carriers/correos-chile/parser.js +3 -3
  39. package/dist/carriers/correos-express/parser.js +2 -2
  40. package/dist/carriers/correos-spain/adapter.d.ts +4 -2
  41. package/dist/carriers/correos-spain/adapter.js +16 -8
  42. package/dist/carriers/ctt/adapter.d.ts +2 -2
  43. package/dist/carriers/ctt/adapter.js +30 -25
  44. package/dist/carriers/ctt-express/parser.js +2 -2
  45. package/dist/carriers/dachser/adapter.d.ts +3 -2
  46. package/dist/carriers/dachser/adapter.js +34 -16
  47. package/dist/carriers/delhivery/parser.js +2 -2
  48. package/dist/carriers/dhl/adapter.d.ts +2 -2
  49. package/dist/carriers/dhl/adapter.js +50 -22
  50. package/dist/carriers/dhl-ecommerce/adapter.d.ts +3 -3
  51. package/dist/carriers/dhl-ecommerce/adapter.js +8 -8
  52. package/dist/carriers/dpd/adapter.d.ts +3 -3
  53. package/dist/carriers/dpd/adapter.js +113 -57
  54. package/dist/carriers/dpd/carrier.json +1 -1
  55. package/dist/carriers/dpd-fr/adapter.d.ts +2 -2
  56. package/dist/carriers/dpd-fr/adapter.js +29 -20
  57. package/dist/carriers/dtdc/parser.js +2 -2
  58. package/dist/carriers/ecoscooting/parser.js +2 -2
  59. package/dist/carriers/ems/adapter.js +2 -2
  60. package/dist/carriers/estafeta/parser.js +2 -2
  61. package/dist/carriers/evri/adapter.js +2 -2
  62. package/dist/carriers/fedex/adapter.d.ts +2 -2
  63. package/dist/carriers/fedex/adapter.js +14 -10
  64. package/dist/carriers/four-px/adapter.js +2 -2
  65. package/dist/carriers/geodis/adapter.d.ts +3 -2
  66. package/dist/carriers/geodis/adapter.js +13 -8
  67. package/dist/carriers/gls-ch/adapter.d.ts +4 -3
  68. package/dist/carriers/gls-ch/adapter.js +27 -19
  69. package/dist/carriers/gls-ch/carrier.json +1 -1
  70. package/dist/carriers/gls-de/adapter.d.ts +3 -3
  71. package/dist/carriers/gls-de/adapter.js +14 -9
  72. package/dist/carriers/gls-de/carrier.json +1 -1
  73. package/dist/carriers/gls-fr/adapter.d.ts +3 -2
  74. package/dist/carriers/gls-fr/adapter.js +16 -11
  75. package/dist/carriers/gofo/parser.js +2 -2
  76. package/dist/carriers/heppner/adapter.d.ts +4 -2
  77. package/dist/carriers/heppner/adapter.js +19 -14
  78. package/dist/carriers/heppner/carrier.json +1 -1
  79. package/dist/carriers/hermes/adapter.d.ts +3 -2
  80. package/dist/carriers/hermes/adapter.js +13 -8
  81. package/dist/carriers/hermes-de/adapter.d.ts +3 -2
  82. package/dist/carriers/hermes-de/adapter.js +16 -12
  83. package/dist/carriers/india-post/adapter.d.ts +2 -2
  84. package/dist/carriers/india-post/adapter.js +42 -21
  85. package/dist/carriers/inpost/adapter.d.ts +4 -2
  86. package/dist/carriers/inpost/adapter.js +17 -10
  87. package/dist/carriers/japan-post/adapter.js +2 -2
  88. package/dist/carriers/korea-post/adapter.js +2 -2
  89. package/dist/carriers/la-poste/adapter.d.ts +4 -2
  90. package/dist/carriers/la-poste/adapter.js +18 -13
  91. package/dist/carriers/landmark-global/parser.js +2 -2
  92. package/dist/carriers/mondial-relay/adapter.d.ts +2 -2
  93. package/dist/carriers/mondial-relay/adapter.js +26 -17
  94. package/dist/carriers/mondial-relay/carrier.json +1 -1
  95. package/dist/carriers/mrw/adapter.js +4 -2
  96. package/dist/carriers/mrw/parser.js +3 -3
  97. package/dist/carriers/nacex/parser.js +2 -2
  98. package/dist/carriers/ninja-van/parser.js +2 -2
  99. package/dist/carriers/nz-post/parser.js +2 -2
  100. package/dist/carriers/ontrac/parser.js +2 -2
  101. package/dist/carriers/paack/adapter.d.ts +4 -2
  102. package/dist/carriers/paack/adapter.js +24 -14
  103. package/dist/carriers/paack/carrier.json +1 -1
  104. package/dist/carriers/packeta/adapter.d.ts +4 -2
  105. package/dist/carriers/packeta/adapter.js +16 -8
  106. package/dist/carriers/planzer/adapter.d.ts +5 -3
  107. package/dist/carriers/planzer/adapter.js +35 -12
  108. package/dist/carriers/planzer/shared.d.ts +4 -1
  109. package/dist/carriers/planzer/shared.js +20 -6
  110. package/dist/carriers/poczta-polska/parser.js +2 -2
  111. package/dist/carriers/pos-malaysia/adapter.d.ts +2 -0
  112. package/dist/carriers/pos-malaysia/adapter.js +9 -5
  113. package/dist/carriers/poste-italiane/adapter.d.ts +4 -2
  114. package/dist/carriers/poste-italiane/adapter.js +16 -8
  115. package/dist/carriers/posti/adapter.js +2 -2
  116. package/dist/carriers/postlogistics/adapter.d.ts +6 -4
  117. package/dist/carriers/postlogistics/adapter.js +49 -17
  118. package/dist/carriers/postlogistics/status.d.ts +2 -0
  119. package/dist/carriers/postlogistics/status.js +2 -0
  120. package/dist/carriers/postnord/parser.js +2 -2
  121. package/dist/carriers/purolator/parser.js +2 -2
  122. package/dist/carriers/relais-colis/adapter.js +2 -2
  123. package/dist/carriers/royal-mail/adapter.d.ts +2 -2
  124. package/dist/carriers/royal-mail/adapter.js +14 -10
  125. package/dist/carriers/seur/parser.js +2 -2
  126. package/dist/carriers/sf-express/parser.js +2 -2
  127. package/dist/carriers/singapore-post/adapter.js +2 -2
  128. package/dist/carriers/spring-gds/adapter.d.ts +2 -0
  129. package/dist/carriers/spring-gds/adapter.js +16 -15
  130. package/dist/carriers/sunyou/adapter.d.ts +5 -3
  131. package/dist/carriers/sunyou/adapter.js +16 -9
  132. package/dist/carriers/swiss-post/adapter.d.ts +4 -4
  133. package/dist/carriers/swiss-post/adapter.js +33 -15
  134. package/dist/carriers/swiss-post-cargo/adapter.d.ts +3 -2
  135. package/dist/carriers/swiss-post-cargo/adapter.js +14 -9
  136. package/dist/carriers/the-courier-guy/number.js +3 -2
  137. package/dist/carriers/tipsa/parser.js +3 -3
  138. package/dist/carriers/tnt/adapter.js +3 -3
  139. package/dist/carriers/ukrposhta/adapter.js +20 -62
  140. package/dist/carriers/ukrposhta/parser.js +2 -2
  141. package/dist/carriers/uniuni/parser.js +3 -3
  142. package/dist/carriers/ups/adapter.d.ts +2 -2
  143. package/dist/carriers/ups/adapter.js +40 -32
  144. package/dist/carriers/usps/adapter.d.ts +2 -2
  145. package/dist/carriers/usps/adapter.js +14 -10
  146. package/dist/carriers/yamato/adapter.js +2 -2
  147. package/dist/carriers/yanwen/adapter.js +2 -2
  148. package/dist/carriers/yto/parser.js +2 -2
  149. package/dist/carriers/yunda/parser.js +2 -2
  150. package/dist/carriers/yunexpress/adapter.js +21 -39
  151. package/dist/cli/index.d.ts +2 -0
  152. package/dist/cli/index.js +49 -19
  153. package/dist/core/adapter/index.d.ts +18 -1
  154. package/dist/core/adapter/index.js +28 -4
  155. package/dist/core/adapter/track.js +21 -7
  156. package/dist/core/catalog/index.d.ts +1 -15
  157. package/dist/core/catalog/index.js +3 -106
  158. package/dist/core/catalog/inputs.d.ts +2 -2
  159. package/dist/core/catalog/inputs.js +4 -4
  160. package/dist/core/catalog/parcel.d.ts +53 -0
  161. package/dist/core/catalog/parcel.js +105 -0
  162. package/dist/core/catalog/types.d.ts +1 -29
  163. package/dist/core/catalog/urls.js +1 -0
  164. package/dist/core/errors/hint.d.ts +7 -0
  165. package/dist/core/errors/hint.js +11 -1
  166. package/dist/core/errors/index.d.ts +8 -1
  167. package/dist/core/errors/index.js +11 -0
  168. package/dist/core/runner/index.d.ts +23 -1
  169. package/dist/core/runner/index.js +71 -14
  170. package/dist/core/transport/boundedFetch.js +24 -19
  171. package/dist/core/transport/browser.d.ts +1 -0
  172. package/dist/core/transport/browser.js +76 -99
  173. package/dist/core/transport/index.d.ts +4 -1
  174. package/dist/core/transport/index.js +3 -1
  175. package/dist/core/transport/localBrowser.d.ts +24 -0
  176. package/dist/core/transport/localBrowser.js +87 -0
  177. package/dist/core/transport/trawl.d.ts +6 -0
  178. package/dist/core/transport/trawl.js +30 -9
  179. package/dist/core/transport/userAgent.d.ts +8 -0
  180. package/dist/core/transport/userAgent.js +15 -0
  181. package/dist/data/catalog.json +6 -6
  182. package/dist/facade/index.d.ts +3 -0
  183. package/dist/facade/index.js +29 -11
  184. package/dist/generated/catalog.d.ts +6 -6
  185. package/dist/generated/catalog.js +6 -6
  186. package/dist/index.d.ts +0 -4
  187. package/dist/index.js +0 -4
  188. package/dist/providers/parcelsapp/adapter.d.ts +1 -1
  189. package/dist/providers/parcelsapp/adapter.js +13 -10
  190. package/dist/providers/parcelsapp/http.d.ts +1 -1
  191. package/dist/providers/parcelsapp/http.js +2 -2
  192. package/dist/providers/postal-ninja/adapter.d.ts +2 -2
  193. package/dist/providers/postal-ninja/adapter.js +9 -8
  194. package/dist/providers/seventeentrack/adapter.d.ts +1 -1
  195. package/dist/providers/seventeentrack/adapter.js +7 -7
  196. package/dist/providers/shared/capture.d.ts +2 -0
  197. package/dist/providers/shared/capture.js +1 -1
  198. package/dist/providers/shared/result.js +4 -2
  199. package/dist/providers/ship24/adapter.d.ts +2 -2
  200. package/dist/providers/ship24/adapter.js +15 -11
  201. package/dist/providers/ship24/http.d.ts +1 -1
  202. package/dist/providers/ship24/http.js +4 -3
  203. package/dist/providers/universal.d.ts +3 -2
  204. package/dist/providers/universal.js +20 -2
  205. package/dist/providers/upu/adapter.js +2 -2
  206. package/dist/scripts/carrier-canary.js +2 -1
  207. package/dist/server/index.d.ts +15 -0
  208. package/dist/server/index.js +90 -12
  209. package/dist/server/openapi.json +399 -52
  210. package/package.json +13 -7
@@ -12,19 +12,21 @@
12
12
  * only when `mailNoSource` is `EXTERNAL`; otherwise Cainiao is still waiting
13
13
  * for the seller and the parcel is simply pending.
14
14
  */
15
- import type { AdapterFactory } from '../../core/adapter/index.js';
15
+ import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
16
16
  import type { CarrierResult } from '../../core/result/index.js';
17
17
  /** Projects one `detail.json` payload. Pure: the offline tests target this. */
18
18
  export declare function parseCainiaoTrackingResponse(value: unknown, trackingNumber: string): CarrierResult;
19
19
  export declare class CainiaoTracker {
20
20
  private readonly fetcher;
21
21
  private readonly timeoutMs;
22
+ private readonly userAgent;
22
23
  constructor(options?: {
23
24
  fetcher?: typeof fetch;
24
25
  timeoutMs?: number;
26
+ userAgent?: string;
25
27
  });
26
- fetch(trackingNumber: string): Promise<CarrierResult>;
28
+ fetch(trackingNumber: string, context?: TrackingContext): Promise<CarrierResult>;
27
29
  }
28
30
  /** Kept for the host's legacy dispatch chain until it is deleted. */
29
- export declare function fetchCainiao(trackingNumber: string): Promise<CarrierResult>;
31
+ export declare function fetchCainiao(trackingNumber: string, context?: TrackingContext): Promise<CarrierResult>;
30
32
  export declare const adapter: AdapterFactory;
@@ -1,7 +1,22 @@
1
+ /**
2
+ * AliExpress / Cainiao tracking.
3
+ *
4
+ * Cainiao's public shipment detail endpoint (`global.cainiao.com/global/
5
+ * detail.json`) is the same keyless JSON the consumer tracking page reads. One
6
+ * GET returns every requested `mailNo` as a module, so the adapter selects the
7
+ * module whose identifier equals the number that was asked for and refuses
8
+ * anything else: the endpoint happily echoes other shipments when the query is
9
+ * malformed, and showing a stranger's parcel would be worse than an error.
10
+ *
11
+ * A module with no status, no history and no latest trace is genuinely unknown
12
+ * only when `mailNoSource` is `EXTERNAL`; otherwise Cainiao is still waiting
13
+ * for the seller and the parcel is simply pending.
14
+ */
15
+ import { lookupBudget } from '../../core/adapter/index.js';
1
16
  import { normalizeTrackingNumber } from '../../core/detection/normalize.js';
2
17
  import { NotFoundError, SchemaError } from '../../core/errors/index.js';
3
18
  import { explicitOffsetTime } from '../../core/time/index.js';
4
- import { fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
19
+ import { fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
5
20
  import { isRecord } from '../../core/types.js';
6
21
  import { CAINIAO_ACTION_STATUS, CAINIAO_STATUS, cainiaoActionCode, cainiaoStageByStatus, } from './status.js';
7
22
  const PROVIDER = 'Cainiao';
@@ -12,7 +27,6 @@ const MAX_EVENTS_TO_RETURN = 20;
12
27
  const BASE_HEADERS = {
13
28
  Accept: 'application/json, text/plain, */*',
14
29
  'Accept-Language': 'en-US,en;q=0.9',
15
- 'User-Agent': 'Mozilla/5.0 (compatible; SwissDeliveryTracker/1.0)',
16
30
  };
17
31
  function record(value) {
18
32
  return isRecord(value) ? value : {};
@@ -140,25 +154,31 @@ export function parseCainiaoTrackingResponse(value, trackingNumber) {
140
154
  export class CainiaoTracker {
141
155
  fetcher;
142
156
  timeoutMs;
157
+ userAgent;
143
158
  constructor(options = {}) {
144
159
  this.fetcher = options.fetcher;
145
160
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
161
+ this.userAgent = userAgentOf(options.userAgent);
146
162
  }
147
- async fetch(trackingNumber) {
148
- const { bytes } = await fetchBounded(`${DETAIL_URL}?${new URLSearchParams({ mailNos: trackingNumber, lang: 'en-US' })}`, { headers: { ...BASE_HEADERS, Referer: 'https://www.aliexpress.com/' } }, { provider: UPSTREAM, timeoutMs: this.timeoutMs, fetcher: this.fetcher });
163
+ async fetch(trackingNumber, context = {}) {
164
+ const budget = lookupBudget(context, this.timeoutMs);
165
+ const { bytes } = await fetchBounded(`${DETAIL_URL}?${new URLSearchParams({ mailNos: trackingNumber, lang: 'en-US' })}`, {
166
+ signal: budget.signal,
167
+ headers: { ...BASE_HEADERS, 'User-Agent': this.userAgent, Referer: 'https://www.aliexpress.com/' },
168
+ }, { provider: UPSTREAM, timeoutMs: Math.min(this.timeoutMs, budget.remainingMs()), fetcher: this.fetcher });
149
169
  return parseCainiaoTrackingResponse(parseJsonBytes(bytes, UPSTREAM), trackingNumber);
150
170
  }
151
171
  }
152
172
  /** Kept for the host's legacy dispatch chain until it is deleted. */
153
- export async function fetchCainiao(trackingNumber) {
154
- return new CainiaoTracker().fetch(trackingNumber);
173
+ export async function fetchCainiao(trackingNumber, context = {}) {
174
+ return new CainiaoTracker().fetch(trackingNumber, context);
155
175
  }
156
176
  export const adapter = (environment) => {
157
- const tracker = new CainiaoTracker({ fetcher: environment.fetcher });
177
+ const tracker = new CainiaoTracker({ fetcher: environment.fetcher, userAgent: environment.userAgent });
158
178
  return {
159
179
  id: 'aliexpress',
160
180
  // One keyless GET; there is no second tier to fall back to.
161
181
  steps: ['direct'],
162
- track: (input) => tracker.fetch(input.number),
182
+ track: (input, context) => tracker.fetch(input.number, context),
163
183
  };
164
184
  };
@@ -1,4 +1,4 @@
1
- import type { AdapterFactory } from '../../core/adapter/index.js';
1
+ import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
2
2
  import { IndeterminateError, NotFoundError } from '../../core/errors/index.js';
3
3
  import type { CarrierResult } from '../../core/result/index.js';
4
4
  export { amazonShippingStatus } from './status.js';
@@ -6,6 +6,7 @@ export interface AmazonShippingOptions {
6
6
  timeoutMs?: number;
7
7
  /** Test seam; production uses the global fetch. */
8
8
  fetcher?: typeof fetch;
9
+ userAgent?: string;
9
10
  }
10
11
  /** A confirmed absence, with a public reason consumers can inspect. */
11
12
  export declare class AmazonShippingNotFoundError extends NotFoundError {
@@ -29,6 +30,6 @@ export declare class AmazonShippingTracker {
29
30
  #private;
30
31
  readonly timeoutMs: number;
31
32
  constructor(options?: number | AmazonShippingOptions);
32
- fetch(rawTrackingNumber: string): Promise<CarrierResult>;
33
+ fetch(rawTrackingNumber: string, context?: TrackingContext): Promise<CarrierResult>;
33
34
  }
34
35
  export declare const adapter: AdapterFactory;
@@ -1,8 +1,9 @@
1
1
  import { DateTime } from 'luxon';
2
+ import { lookupBudget } from '../../core/adapter/index.js';
2
3
  import { amazonShippingOrigin, amazonShippingUrl } from '../../core/catalog/index.js';
3
4
  import { isAmazonTrackingNumber } from '../../core/detection/index.js';
4
- import { IndeterminateError, InputRequiredError, NotFoundError, SchemaError } from '../../core/errors/index.js';
5
- import { cleanScalar, fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
5
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
6
+ import { cleanScalar, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
6
7
  import { isRecord } from '../../core/types.js';
7
8
  import { classifyStatus, statusKey } from './status.js';
8
9
  const PROVIDER = 'Amazon Shipping';
@@ -155,7 +156,7 @@ function metadataValue(metadata, field) {
155
156
  export function normalizeAmazonShippingTrackingNumber(raw) {
156
157
  const value = raw.trim().toLocaleUpperCase('en-US').replace(/[\s.-]/g, '');
157
158
  if (!isAmazonTrackingNumber(value)) {
158
- throw new InputRequiredError(PROVIDER, 'a European country prefix and 10 digits, or TBA and 12 digits', 'Amazon tracking numbers need a European country prefix and 10 digits, or TBA and 12 digits');
159
+ throw new InvalidInputError(PROVIDER, 'Amazon tracking numbers need a European country prefix and 10 digits, or TBA and 12 digits');
159
160
  }
160
161
  return value;
161
162
  }
@@ -254,28 +255,32 @@ export function amazonShippingTimezone(number) {
254
255
  export class AmazonShippingTracker {
255
256
  timeoutMs;
256
257
  #fetcher;
258
+ #userAgent;
257
259
  constructor(options = {}) {
258
- const { timeoutMs = DEFAULT_TIMEOUT_MS, fetcher } = typeof options === 'number'
259
- ? { timeoutMs: options, fetcher: undefined }
260
+ const { timeoutMs = DEFAULT_TIMEOUT_MS, fetcher, userAgent } = typeof options === 'number'
261
+ ? { timeoutMs: options, fetcher: undefined, userAgent: undefined }
260
262
  : options;
261
263
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
262
264
  throw new TypeError('Amazon Shipping timeout must be positive');
263
265
  }
264
266
  this.timeoutMs = timeoutMs;
265
267
  this.#fetcher = fetcher;
268
+ this.#userAgent = userAgentOf(userAgent);
266
269
  }
267
- async fetch(rawTrackingNumber) {
270
+ async fetch(rawTrackingNumber, context = {}) {
268
271
  const trackingNumber = normalizeAmazonShippingTrackingNumber(rawTrackingNumber);
272
+ const budget = lookupBudget(context, this.timeoutMs);
269
273
  const { bytes } = await fetchBounded(amazonShippingTrackingApiUrl(trackingNumber), {
274
+ signal: budget.signal,
270
275
  headers: {
271
276
  Accept: 'application/json',
272
277
  'Accept-Language': 'en-US,en;q=0.9',
273
278
  Referer: amazonShippingTrackingUrl(trackingNumber),
274
- 'User-Agent': 'Mozilla/5.0 (compatible; DeliveryTracker/1.0)',
279
+ 'User-Agent': this.#userAgent,
275
280
  },
276
281
  }, {
277
282
  provider: 'Amazon Shipping tracking',
278
- timeoutMs: this.timeoutMs,
283
+ timeoutMs: Math.min(this.timeoutMs, budget.remainingMs()),
279
284
  maxBytes: MAX_RESPONSE_BYTES,
280
285
  ...(this.#fetcher ? { fetcher: this.#fetcher } : {}),
281
286
  });
@@ -283,10 +288,10 @@ export class AmazonShippingTracker {
283
288
  }
284
289
  }
285
290
  export const adapter = (environment) => {
286
- const tracker = new AmazonShippingTracker({ fetcher: environment.fetcher });
291
+ const tracker = new AmazonShippingTracker({ fetcher: environment.fetcher, userAgent: environment.userAgent });
287
292
  return {
288
293
  id: 'amazon-shipping',
289
294
  steps: ['direct'],
290
- track: (input) => tracker.fetch(input.number),
295
+ track: (input, context) => tracker.fetch(input.number, context),
291
296
  };
292
297
  };
@@ -1,3 +1,4 @@
1
1
  import { type AmazonShippingOptions } from './adapter.js';
2
+ import type { TrackingContext } from '../../core/adapter/index.js';
2
3
  /** A public eligibility check; transport errors remain errors for the consumer to report. */
3
- export declare function amazonShippingEligibility(number: string, options?: AmazonShippingOptions): Promise<'available' | 'expired' | 'not-found'>;
4
+ export declare function amazonShippingEligibility(number: string, options?: AmazonShippingOptions, context?: TrackingContext): Promise<'available' | 'expired' | 'not-found'>;
@@ -1,9 +1,9 @@
1
1
  import { AmazonShippingTracker } from './adapter.js';
2
2
  import { CarrierError } from '../../core/errors/index.js';
3
3
  /** A public eligibility check; transport errors remain errors for the consumer to report. */
4
- export async function amazonShippingEligibility(number, options = {}) {
4
+ export async function amazonShippingEligibility(number, options = {}, context = {}) {
5
5
  try {
6
- await new AmazonShippingTracker({ ...options, timeoutMs: options.timeoutMs ?? 5_000 }).fetch(number);
6
+ await new AmazonShippingTracker({ ...options, timeoutMs: options.timeoutMs ?? 5_000 }).fetch(number, context);
7
7
  return 'available';
8
8
  }
9
9
  catch (error) {
@@ -2,9 +2,11 @@ import type { AdapterFactory, TrackingContext } from '../../core/adapter/index.j
2
2
  import { type StepRecorder } from '../../core/telemetry/index.js';
3
3
  export declare class AramexTracker {
4
4
  private readonly options;
5
+ private readonly userAgent;
5
6
  constructor(options?: {
6
7
  fetcher?: typeof fetch;
7
8
  recorder?: StepRecorder;
9
+ userAgent?: string;
8
10
  });
9
11
  fetch(raw: string, context?: TrackingContext): Promise<import("../../index.js").CarrierResult>;
10
12
  }
@@ -1,12 +1,14 @@
1
1
  import { BudgetExceededError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
2
2
  import { runSteps } from '../../core/runner/index.js';
3
3
  import { NOOP_RECORDER } from '../../core/telemetry/index.js';
4
- import { decodeText, fetchBounded } from '../../core/transport/index.js';
4
+ import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
5
5
  import { aramexDetailRedirect, aramexDetailUrl, normalizeAramexNumber, parseAramex } from './parser.js';
6
6
  export class AramexTracker {
7
7
  options;
8
+ userAgent;
8
9
  constructor(options = {}) {
9
10
  this.options = options;
11
+ this.userAgent = userAgentOf(options.userAgent);
10
12
  }
11
13
  fetch(raw, context = {}) {
12
14
  const number = normalizeAramexNumber(raw);
@@ -21,7 +23,7 @@ export class AramexTracker {
21
23
  try {
22
24
  // The edge rejects Node's default user agent on server networks.
23
25
  const { response, bytes } = await fetchBounded(url, { signal, headers: {
24
- Accept: 'text/html', 'User-Agent': 'SwissDeliveryTracker/1.0',
26
+ Accept: 'text/html', 'User-Agent': this.userAgent,
25
27
  } }, {
26
28
  provider: 'Aramex', timeoutMs: Math.max(1, Math.floor(left)), maxBytes: 1_500_000,
27
29
  fetcher: this.options.fetcher, ...(regionalRedirect ? { redirect: 'manual', allowHttpStatuses: [301, 302, 303, 307, 308] } : {}),
@@ -44,6 +46,6 @@ export class AramexTracker {
44
46
  }
45
47
  }
46
48
  export const adapter = environment => {
47
- const tracker = new AramexTracker({ fetcher: environment.fetcher, recorder: environment.recorder });
49
+ const tracker = new AramexTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
48
50
  return { id: 'aramex', recordsSteps: true, steps: ['direct'], track: (input, context) => tracker.fetch(input.number, context) };
49
51
  };
@@ -1,12 +1,12 @@
1
1
  import { load } from 'cheerio';
2
2
  import { DateTime } from 'luxon';
3
- import { IndeterminateError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
4
4
  import { clean } from '../../core/transport/index.js';
5
5
  import { classifyAramexStatus } from './status.js';
6
6
  export function normalizeAramexNumber(raw) {
7
7
  const number = raw.replace(/[\s.-]/g, '');
8
8
  if (!/^\d{11,12}$/.test(number))
9
- throw new TypeError('Aramex requires an eleven- or twelve-digit shipment number');
9
+ throw new InvalidInputError('Aramex', 'Aramex requires an eleven- or twelve-digit shipment number');
10
10
  return number;
11
11
  }
12
12
  export function aramexDetailUrl(html, number) {
@@ -61,11 +61,13 @@ export interface AsendiaA1TrackerOptions {
61
61
  /** Wall clock in milliseconds, for the configuration cache and budgets. */
62
62
  now?: () => number;
63
63
  budgetMs?: number;
64
+ userAgent?: string;
64
65
  }
65
66
  export declare class AsendiaA1Tracker {
66
67
  private readonly fetcher?;
67
68
  private readonly now;
68
69
  private readonly budgetMs;
70
+ private readonly userAgent;
69
71
  private cached;
70
72
  private loading;
71
73
  constructor(options?: AsendiaA1TrackerOptions);
@@ -73,10 +75,16 @@ export declare class AsendiaA1Tracker {
73
75
  private freshConfig;
74
76
  /** One page read at a time; concurrent lookups share it. */
75
77
  private loadConfig;
78
+ private startRead;
79
+ /** Waits for a read another lookup started, no longer than this lookup's own signal and budget allow. */
80
+ private whileLive;
81
+ /** Whether a failed read was ended by its own lookup's cancellation or budget rather than by Asendia. */
82
+ private cutShort;
76
83
  private readConfig;
77
84
  private keyAccepted;
78
85
  private tracking;
79
86
  private api;
80
87
  private requestTimeout;
88
+ private remaining;
81
89
  }
82
90
  export declare const adapter: AdapterFactory;
@@ -1,18 +1,19 @@
1
1
  import { carrierIdFromPartner } from '../../core/catalog/hints.js';
2
- import { BudgetExceededError, ChallengeError, IndeterminateError, NotFoundError, SchemaError, UpstreamHttpError, } from '../../core/errors/index.js';
2
+ import { BudgetExceededError, ChallengeError, IndeterminateError, InvalidInputError, NotFoundError, SchemaError, UpstreamHttpError, UpstreamNetworkError } from '../../core/errors/index.js';
3
3
  import { languageStageStatus, wordingStage } from '../../core/status/index.js';
4
4
  import { explicitOffsetTime } from '../../core/time/index.js';
5
- import { clean, cleanScalar, decodeText, fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
5
+ import { clean, cleanScalar, decodeText, fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
6
6
  import { isRecord } from '../../core/types.js';
7
7
  import { classifyAsendiaA1Event } from './status.js';
8
8
  const PROVIDER = 'Asendia';
9
9
  const PAGE_URL = 'https://a1.asendiausa.com/tracking/';
10
10
  const SCRIPT_URL = `${PAGE_URL}js/main.js`;
11
11
  const API_HOST_SUFFIXES = ['asendiaprod.com', 'asendia.io', 'asendia.com', 'asendiausa.com'];
12
- const USER_AGENT = 'Mozilla/5.0 (compatible; SwissDeliveryTracker/1.0)';
13
12
  const GUID = /[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/gi;
14
13
  const DEFAULT_BUDGET_MS = 25_000;
15
14
  const MAX_REQUEST_MS = 10_000;
15
+ /** The longest delay a timer holds; a longer one fires at once. */
16
+ const MAX_TIMER_MS = 2 ** 31 - 1;
16
17
  /** The public configuration is re-read at least this often, in case it rotates. */
17
18
  const CONFIG_TTL_MS = 6 * 60 * 60_000;
18
19
  /** A not-found is only trusted when the tracking key was accepted this recently. */
@@ -39,7 +40,7 @@ function comparable(value) {
39
40
  export function normalizeAsendiaA1TrackingNumber(raw) {
40
41
  const value = comparable(raw);
41
42
  if (!/^[A-Z0-9]{6,40}$/.test(value)) {
42
- throw new TypeError('Asendia tracking numbers must contain 6 to 40 ASCII letters and digits');
43
+ throw new InvalidInputError(PROVIDER, 'Asendia tracking numbers must contain 6 to 40 ASCII letters and digits');
43
44
  }
44
45
  return value;
45
46
  }
@@ -206,6 +207,7 @@ export class AsendiaA1Tracker {
206
207
  fetcher;
207
208
  now;
208
209
  budgetMs;
210
+ userAgent;
209
211
  cached = null;
210
212
  loading = null;
211
213
  constructor(options = {}) {
@@ -214,6 +216,7 @@ export class AsendiaA1Tracker {
214
216
  this.budgetMs = options.budgetMs ?? DEFAULT_BUDGET_MS;
215
217
  if (!Number.isFinite(this.budgetMs) || this.budgetMs <= 0)
216
218
  throw new TypeError('Asendia budget must be positive');
219
+ this.userAgent = userAgentOf(options.userAgent);
217
220
  }
218
221
  async fetch(rawTrackingNumber, context = {}) {
219
222
  const trackingNumber = normalizeAsendiaA1TrackingNumber(rawTrackingNumber);
@@ -256,13 +259,59 @@ export class AsendiaA1Tracker {
256
259
  return this.cached && this.now() - this.cached.loadedAt < CONFIG_TTL_MS ? this.cached : null;
257
260
  }
258
261
  /** One page read at a time; concurrent lookups share it. */
259
- loadConfig(request) {
260
- this.loading ??= this.readConfig(request).finally(() => { this.loading = null; });
261
- return this.loading;
262
+ async loadConfig(request) {
263
+ for (;;) {
264
+ const read = this.loading ??= this.startRead(request);
265
+ if (read.owner === request)
266
+ return read.promise;
267
+ const outcome = await this.whileLive(read.promise.then((config) => ({ config }), (error) => ({ error })), request);
268
+ if ('config' in outcome)
269
+ return outcome.config;
270
+ // A read cut short by its own lookup's cancellation or budget says
271
+ // nothing about Asendia: a lookup that shared it reads again.
272
+ if (!read.cut)
273
+ throw outcome.error;
274
+ }
275
+ }
276
+ startRead(owner) {
277
+ const read = {
278
+ owner,
279
+ cut: false,
280
+ promise: this.readConfig(owner).catch((error) => {
281
+ // Judged as the read fails: a host may cancel the lookup once it has settled.
282
+ read.cut = this.cutShort(owner, error);
283
+ throw error;
284
+ }).finally(() => { this.loading = null; }),
285
+ };
286
+ return read;
287
+ }
288
+ /** Waits for a read another lookup started, no longer than this lookup's own signal and budget allow. */
289
+ async whileLive(shared, request) {
290
+ let leave;
291
+ const gone = new Promise((_resolve, reject) => { leave = reject; });
292
+ const cancelled = () => leave(request.signal?.reason);
293
+ const spent = setTimeout(() => leave(new BudgetExceededError(PROVIDER, request.budgetMs)), Math.min(MAX_TIMER_MS, this.remaining(request)));
294
+ request.signal?.addEventListener('abort', cancelled, { once: true });
295
+ try {
296
+ return await Promise.race([shared, gone]);
297
+ }
298
+ finally {
299
+ clearTimeout(spent);
300
+ request.signal?.removeEventListener('abort', cancelled);
301
+ }
302
+ }
303
+ /** Whether a failed read was ended by its own lookup's cancellation or budget rather than by Asendia. */
304
+ cutShort(owner, error) {
305
+ if (owner.signal?.aborted || owner.deadline - this.now() < 1)
306
+ return true;
307
+ // A request timer can fire just before the clock reaches the deadline.
308
+ return owner.budgetBound === true && error instanceof UpstreamNetworkError
309
+ && isRecord(error.cause) && error.cause.name === 'TimeoutError';
262
310
  }
263
311
  async readConfig(request) {
264
312
  const { bytes } = await fetchBounded(SCRIPT_URL, {
265
- headers: { Accept: '*/*', Referer: PAGE_URL, 'User-Agent': USER_AGENT },
313
+ signal: request.signal,
314
+ headers: { Accept: '*/*', Referer: PAGE_URL, 'User-Agent': this.userAgent },
266
315
  }, {
267
316
  provider: 'Asendia tracking page', timeoutMs: this.requestTimeout(request), maxBytes: MAX_SCRIPT_BYTES, fetcher: this.fetcher,
268
317
  });
@@ -293,11 +342,12 @@ export class AsendiaA1Tracker {
293
342
  }
294
343
  async api(url, config, request, provider) {
295
344
  const { response, bytes } = await fetchBounded(url, {
345
+ signal: request.signal,
296
346
  headers: {
297
347
  Accept: 'application/json',
298
348
  Authorization: config.authorization,
299
349
  'X-AsendiaOne-ApiKey': config.apiKey,
300
- 'User-Agent': USER_AGENT,
350
+ 'User-Agent': this.userAgent,
301
351
  },
302
352
  }, {
303
353
  provider, timeoutMs: this.requestTimeout(request), maxBytes: MAX_RESPONSE_BYTES, fetcher: this.fetcher,
@@ -324,15 +374,20 @@ export class AsendiaA1Tracker {
324
374
  return payload;
325
375
  }
326
376
  requestTimeout(request) {
377
+ const remaining = this.remaining(request);
378
+ request.budgetBound = remaining <= MAX_REQUEST_MS;
379
+ return Math.min(MAX_REQUEST_MS, remaining);
380
+ }
381
+ remaining(request) {
327
382
  request.signal?.throwIfAborted();
328
383
  const remaining = Math.floor(request.deadline - this.now());
329
384
  if (remaining < 1)
330
385
  throw new BudgetExceededError(PROVIDER, request.budgetMs);
331
- return Math.min(MAX_REQUEST_MS, remaining);
386
+ return remaining;
332
387
  }
333
388
  }
334
389
  export const adapter = (environment) => {
335
- const tracker = new AsendiaA1Tracker({ fetcher: environment.fetcher });
390
+ const tracker = new AsendiaA1Tracker({ fetcher: environment.fetcher, userAgent: environment.userAgent });
336
391
  return {
337
392
  id: 'asendia',
338
393
  // Plain HTTP against the public page's own API; no second tier.
@@ -18,12 +18,14 @@ export interface AsendiaTrackerOptions {
18
18
  turnstileTokenProvider?: () => string | Promise<string>;
19
19
  /** Test seam; production uses the global fetch. */
20
20
  fetcher?: typeof fetch;
21
+ userAgent?: string;
21
22
  }
22
23
  export declare class AsendiaTracker {
23
24
  readonly timeoutMs: number;
24
25
  readonly now: () => Date;
25
26
  readonly turnstileTokenProvider: () => string | Promise<string>;
26
27
  readonly fetcher?: typeof fetch;
28
+ readonly userAgent: string;
27
29
  constructor(options?: AsendiaTrackerOptions);
28
30
  fetch(rawTrackingNumber: string): Promise<CarrierResult>;
29
31
  private requestText;
@@ -24,9 +24,9 @@
24
24
  import { createHash } from 'node:crypto';
25
25
  import { load } from 'cheerio';
26
26
  import { DateTime } from 'luxon';
27
- import { ChallengeError, NotFoundError, SchemaError } from '../../core/errors/index.js';
27
+ import { ChallengeError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
28
28
  import { isoTime, zonedTime } from '../../core/time/index.js';
29
- import { cleanScalar, decodeText, fetchBounded, parseJsonBytes, UpstreamHttpError, } from '../../core/transport/index.js';
29
+ import { cleanScalar, decodeText, fetchBounded, parseJsonBytes, UpstreamHttpError, userAgentOf, } from '../../core/transport/index.js';
30
30
  import { isRecord } from '../../core/types.js';
31
31
  import { classifyAsendiaStatus } from './status.js';
32
32
  const TRACKING_ORIGIN = 'https://track.asendia.com';
@@ -59,7 +59,7 @@ function plainText(value, maxLength = 500) {
59
59
  export function normalizeAsendiaTrackingNumber(raw) {
60
60
  const value = raw.toLocaleUpperCase('en-US').replace(/[\s-]/g, '');
61
61
  if (!/^[A-Z0-9]{8,40}$/.test(value) || !/\d/.test(value)) {
62
- throw new TypeError('Asendia tracking numbers must contain 8 to 40 ASCII letters and digits');
62
+ throw new InvalidInputError('Asendia', 'Asendia tracking numbers must contain 8 to 40 ASCII letters and digits');
63
63
  }
64
64
  return value;
65
65
  }
@@ -260,7 +260,7 @@ function challengeToken(value) {
260
260
  }
261
261
  return token;
262
262
  }
263
- function baseHeaders() {
263
+ function baseHeaders(userAgent) {
264
264
  return {
265
265
  Accept: 'application/json',
266
266
  'Accept-Language': 'en',
@@ -270,7 +270,7 @@ function baseHeaders() {
270
270
  'X-Timezone': ZONE,
271
271
  Origin: TRACKING_ORIGIN,
272
272
  Referer: `${TRACKING_PAGE}/`,
273
- 'User-Agent': 'Mozilla/5.0 (compatible; DeliveryTracker/1.0)',
273
+ 'User-Agent': userAgent,
274
274
  };
275
275
  }
276
276
  export class AsendiaTracker {
@@ -278,6 +278,7 @@ export class AsendiaTracker {
278
278
  now;
279
279
  turnstileTokenProvider;
280
280
  fetcher;
281
+ userAgent;
281
282
  constructor(options = {}) {
282
283
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
283
284
  this.now = options.now ?? (() => new Date());
@@ -287,13 +288,14 @@ export class AsendiaTracker {
287
288
  if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) {
288
289
  throw new TypeError('Asendia timeout must be positive');
289
290
  }
291
+ this.userAgent = userAgentOf(options.userAgent);
290
292
  }
291
293
  async fetch(rawTrackingNumber) {
292
294
  const trackingNumber = normalizeAsendiaTrackingNumber(rawTrackingNumber);
293
295
  const token = challengeToken(await this.turnstileTokenProvider());
294
296
  const [environmentScript, configPayload] = await Promise.all([
295
297
  this.requestText(ENVIRONMENT_SCRIPT, 'Asendia public environment'),
296
- this.requestJson(CONFIG_API, { headers: baseHeaders() }, 'Asendia tracking configuration'),
298
+ this.requestJson(CONFIG_API, { headers: baseHeaders(this.userAgent) }, 'Asendia tracking configuration'),
297
299
  ]);
298
300
  const publicHitKey = parseAsendiaPublicHitKey(environmentScript);
299
301
  const config = parseTenantConfig(configPayload);
@@ -301,7 +303,7 @@ export class AsendiaTracker {
301
303
  const hitToken = asendiaHitToken(trackingNumber, date, publicHitKey);
302
304
  const payload = await this.requestJson(asendiaTrackingApiUrl(), {
303
305
  method: 'POST',
304
- headers: { ...baseHeaders(), 'X-Hit-Token': hitToken },
306
+ headers: { ...baseHeaders(this.userAgent), 'X-Hit-Token': hitToken },
305
307
  body: JSON.stringify({
306
308
  ids: [trackingNumber],
307
309
  id: config.id,
@@ -1,4 +1,4 @@
1
- import { BudgetExceededError, ChallengeError, InputRequiredError, NotFoundError, RateLimitedError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
1
+ import { BudgetExceededError, ChallengeError, InvalidInputError, NotFoundError, RateLimitedError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
2
2
  import { runSteps } from '../../core/runner/index.js';
3
3
  import { explicitOffsetTime, epochMillisTime } from '../../core/time/index.js';
4
4
  import { clean } from '../../core/transport/index.js';
@@ -12,7 +12,7 @@ const TRANSPORT_ALLOWANCE_MS = 15_000;
12
12
  export function normalizeAustraliaPostNumber(raw) {
13
13
  const number = raw.toUpperCase().replace(/[\s.-]/g, '');
14
14
  if (!/^(?=.*\d)[A-Z0-9]{10,34}$/.test(number)) {
15
- throw new InputRequiredError(PROVIDER, 'number', 'Australia Post requires a 10-to-34-character tracking number');
15
+ throw new InvalidInputError(PROVIDER, 'Australia Post requires a 10-to-34-character tracking number');
16
16
  }
17
17
  return number;
18
18
  }
@@ -1,6 +1,6 @@
1
1
  import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
2
  import { isValidS10TrackingNumber, normalizeTrackingNumber } from '../../core/detection/index.js';
3
- import { IndeterminateError, NotFoundError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
3
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
4
4
  import { explicitOffsetTime } from '../../core/time/index.js';
5
5
  import { clean, fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
6
6
  import { isRecord } from '../../core/types.js';
@@ -12,7 +12,7 @@ const ENDPOINT = 'https://api.post.at/sendungen/sv/graphqlPublic';
12
12
  export function normalizeAustrianPostNumber(raw) {
13
13
  const number = normalizeTrackingNumber(raw);
14
14
  if (!/^\d{22}$/.test(number) && !(/^[A-Z]{2}\d{9}AT$/.test(number) && isValidS10TrackingNumber(number))) {
15
- throw new TypeError('Austrian Post requires a 22-digit or valid AT postal identifier');
15
+ throw new InvalidInputError(PROVIDER, 'Austrian Post requires a 22-digit or valid AT postal identifier');
16
16
  }
17
17
  return number;
18
18
  }
@@ -1,12 +1,12 @@
1
1
  import { load } from 'cheerio';
2
- import { IndeterminateError, NotFoundError, SchemaError } from '../../core/errors/index.js';
2
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
3
  import { zonedTime } from '../../core/time/index.js';
4
4
  import { clean } from '../../core/transport/index.js';
5
5
  import { classifyBlueDartStatus } from './status.js';
6
6
  export function normalizeBlueDartNumber(raw) {
7
7
  const number = raw.replace(/[\s.-]/g, '');
8
8
  if (!/^\d{11}$/.test(number))
9
- throw new TypeError('Blue Dart requires an eleven-digit waybill');
9
+ throw new InvalidInputError('Blue Dart', 'Blue Dart requires an eleven-digit waybill');
10
10
  return number;
11
11
  }
12
12
  export function parseBlueDart(html, number) {
@@ -1,13 +1,13 @@
1
1
  import { DateTime } from 'luxon';
2
2
  import { isValidS10TrackingNumber } from '../../core/detection/s10.js';
3
- import { IndeterminateError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
4
4
  import { clean } from '../../core/transport/index.js';
5
5
  import { isRecord } from '../../core/types.js';
6
6
  import { classifyBpostStatus } from './status.js';
7
7
  export function normalizeBpostNumber(raw) {
8
8
  const number = raw.toUpperCase().replace(/[\s.-]/g, '');
9
9
  if (!/^(?:\d{18}|\d{24}|\d{30})$/.test(number) && !isValidS10TrackingNumber(number)) {
10
- throw new TypeError('bpost requires a parcel barcode or postal tracking number');
10
+ throw new InvalidInputError('bpost', 'bpost requires a parcel barcode or postal tracking number');
11
11
  }
12
12
  return number;
13
13
  }
@@ -1,6 +1,6 @@
1
1
  import { DateTime } from 'luxon';
2
2
  import { isValidS10TrackingNumber, normalizeTrackingNumber } from '../../core/detection/index.js';
3
- import { IndeterminateError, SchemaError } from '../../core/errors/index.js';
3
+ import { IndeterminateError, InvalidInputError, SchemaError } from '../../core/errors/index.js';
4
4
  import { explicitOffsetTime } from '../../core/time/index.js';
5
5
  import { clean } from '../../core/transport/index.js';
6
6
  import { isRecord } from '../../core/types.js';
@@ -8,7 +8,7 @@ import { bringStatus, bringWording } from './status.js';
8
8
  export function normalizeBringNumber(raw) {
9
9
  const number = normalizeTrackingNumber(raw);
10
10
  if (!/^\d{17,18}$/.test(number) && !(/^[A-Z]{2}\d{9}NO$/.test(number) && isValidS10TrackingNumber(number))) {
11
- throw new TypeError('Bring requires a parcel or consignment tracking number');
11
+ throw new InvalidInputError('Bring', 'Bring requires a parcel or consignment tracking number');
12
12
  }
13
13
  return number;
14
14
  }
@@ -1,12 +1,12 @@
1
1
  import { load } from 'cheerio';
2
- import { IndeterminateError, NotFoundError, SchemaError } from '../../core/errors/index.js';
2
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
3
3
  import { calendarDay } from '../../core/time/index.js';
4
4
  import { clean } from '../../core/transport/index.js';
5
5
  import { classifyBrtStatus } from './status.js';
6
6
  export function normalizeBrtNumber(raw) {
7
7
  const number = raw.replace(/\s/g, '');
8
8
  if (!/^\d{14}$/.test(number))
9
- throw new TypeError('BRT requires a fourteen-digit BRTcode');
9
+ throw new InvalidInputError('BRT', 'BRT requires a fourteen-digit BRTcode');
10
10
  return number;
11
11
  }
12
12
  function scanClock(date, clock) {