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
@@ -1,4 +1,5 @@
1
- import { IndeterminateError, MaintenanceError, NotFoundError, SchemaError } from '../../core/errors/index.js';
1
+ import { lookupBudget } from '../../core/adapter/index.js';
2
+ import { IndeterminateError, InvalidInputError, MaintenanceError, NotFoundError, SchemaError } from '../../core/errors/index.js';
2
3
  import { isValidS10TrackingNumber } from '../../core/detection/index.js';
3
4
  import { explicitOffsetTime } from '../../core/time/index.js';
4
5
  import { clean, decodeText, escapeRegExp, fetchBounded, parseJsonBytes, UpstreamHttpError, } from '../../core/transport/index.js';
@@ -54,7 +55,7 @@ export function normalizeCttTrackingNumber(raw) {
54
55
  // the generic postal fallback — those routes were never sampled here.
55
56
  const value = raw.toLocaleUpperCase('en-US').replace(/[\s.-]/g, '');
56
57
  if (!/^[A-Z]{2}\d{9}PT$/.test(value) || !isValidS10TrackingNumber(value)) {
57
- throw new TypeError('CTT tracking requires a valid 13-character S10 number ending in PT');
58
+ throw new InvalidInputError('CTT', 'CTT tracking requires a valid 13-character S10 number ending in PT');
58
59
  }
59
60
  return value;
60
61
  }
@@ -167,31 +168,32 @@ export class CttTracker {
167
168
  throw new TypeError('CTT timeout must be positive');
168
169
  }
169
170
  }
170
- async fetch(rawTrackingNumber) {
171
+ async fetch(rawTrackingNumber, context = {}) {
171
172
  const trackingNumber = normalizeCttTrackingNumber(rawTrackingNumber);
173
+ const budget = lookupBudget(context, this.timeoutMs);
172
174
  const payload = await this.callAction(TRACK_ACTION, {
173
175
  ObjectCodeInput: trackingNumber,
174
176
  SearchInput: trackingNumber,
175
177
  IsFromPublicArea: true,
176
178
  IPClient: '',
177
- }, null, true, true);
179
+ }, null, true, true, budget);
178
180
  const record = isRecord(payload.data) ? payload.data.ObjectEventsFromQuery : undefined;
179
181
  if (!isRecord(record) || !record.Found) {
180
182
  // Found:false is both genuine unknown and backend outage: a found parcel
181
183
  // already proves health, so the sibling check runs solely on negatives.
182
- if (await this.isMaintenance())
184
+ if (await this.isMaintenance(budget))
183
185
  throw new CttMaintenanceError();
184
186
  throw new NotFoundError('CTT');
185
187
  }
186
188
  return parseCttTrackingResponse(payload, trackingNumber);
187
189
  }
188
- async isMaintenance() {
189
- const payload = await this.callAction(MAINTENANCE_ACTION, {}, null, true, true);
190
+ async isMaintenance(budget) {
191
+ const payload = await this.callAction(MAINTENANCE_ACTION, {}, null, true, true, budget);
190
192
  const data = isRecord(payload.data) ? payload.data : {};
191
193
  return data.IsMaintenance === true;
192
194
  }
193
- async callAction(action, variables, session, retrySession, retryVersion) {
194
- const apiVersion = await this.ensureApiVersion(action);
195
+ async callAction(action, variables, session, retrySession, retryVersion, budget) {
196
+ const apiVersion = await this.ensureApiVersion(action, budget);
195
197
  const headers = {
196
198
  Accept: 'application/json',
197
199
  'Content-Type': 'application/json; charset=UTF-8',
@@ -203,6 +205,7 @@ export class CttTracker {
203
205
  }
204
206
  const { response, bytes } = await fetchBounded(`${BASE_URL}${SCREEN_PATH}/${action}`, {
205
207
  method: 'POST',
208
+ signal: budget.signal,
206
209
  headers,
207
210
  body: JSON.stringify({
208
211
  versionInfo: { moduleVersion: this.moduleVersion, apiVersion },
@@ -211,7 +214,7 @@ export class CttTracker {
211
214
  }),
212
215
  }, {
213
216
  provider: 'CTT tracking',
214
- timeoutMs: this.timeoutMs,
217
+ timeoutMs: Math.min(this.timeoutMs, budget.remainingMs()),
215
218
  maxBytes: MAX_RESPONSE_BYTES,
216
219
  retryTransient: true,
217
220
  allowHttpError: true,
@@ -220,7 +223,7 @@ export class CttTracker {
220
223
  if (response.status === 403) {
221
224
  const bootstrapped = sessionFromSetCookie(response);
222
225
  if (retrySession && bootstrapped) {
223
- return this.callAction(action, variables, bootstrapped, false, retryVersion);
226
+ return this.callAction(action, variables, bootstrapped, false, retryVersion, budget);
224
227
  }
225
228
  throw new CttApiError('anonymous session bootstrap failed');
226
229
  }
@@ -234,30 +237,31 @@ export class CttTracker {
234
237
  this.moduleVersion = null;
235
238
  this.screenScript = null;
236
239
  this.apiVersions.delete(action);
237
- return this.callAction(action, variables, session, retrySession, false);
240
+ return this.callAction(action, variables, session, retrySession, false, budget);
238
241
  }
239
242
  return payload;
240
243
  }
241
- async ensureApiVersion(action) {
244
+ async ensureApiVersion(action, budget) {
242
245
  const cached = this.apiVersions.get(action);
243
246
  if (cached !== undefined)
244
247
  return cached;
245
- await this.ensureModuleVersion();
246
- const script = await this.ensureScreenScript();
248
+ await this.ensureModuleVersion(budget);
249
+ const script = await this.ensureScreenScript(budget);
247
250
  const version = new RegExp(`callDataAction\\("${escapeRegExp(action)}",\\s*"[^"]*",\\s*"([^"]+)"`).exec(script)?.[1];
248
251
  if (!version)
249
252
  throw new CttApiError(`could not derive apiVersion for ${action}`);
250
253
  this.apiVersions.set(action, version);
251
254
  return version;
252
255
  }
253
- async ensureModuleVersion() {
256
+ async ensureModuleVersion(budget) {
254
257
  if (this.moduleVersion !== null)
255
258
  return;
256
259
  const { response, bytes } = await fetchBounded(MODULE_VERSION_URL, {
260
+ signal: budget.signal,
257
261
  headers: { Accept: 'application/json', 'User-Agent': BROWSER_USER_AGENT },
258
262
  }, {
259
263
  provider: 'CTT tracking',
260
- timeoutMs: this.timeoutMs,
264
+ timeoutMs: Math.min(this.timeoutMs, budget.remainingMs()),
261
265
  maxBytes: MAX_RESPONSE_BYTES,
262
266
  retryTransient: true,
263
267
  allowHttpError: true,
@@ -271,13 +275,13 @@ export class CttTracker {
271
275
  throw new CttApiError('moduleversioninfo returned no versionToken');
272
276
  this.moduleVersion = token;
273
277
  }
274
- async ensureScreenScript() {
278
+ async ensureScreenScript(budget) {
275
279
  if (this.screenScript !== null)
276
280
  return this.screenScript;
277
281
  // The version token carries +/= characters: it must travel percent-encoded
278
282
  // or the manifest endpoint answers an unrelated shape.
279
283
  const manifestUrl = `${BASE_URL}/CustomerArea/moduleservices/moduleinfo?${encodeURIComponent(this.moduleVersion ?? '')}`;
280
- const manifest = await this.fetchJson(manifestUrl);
284
+ const manifest = await this.fetchJson(manifestUrl, budget);
281
285
  const versions = isRecord(manifest) && isRecord(manifest.manifest) && isRecord(manifest.manifest.urlVersions)
282
286
  ? manifest.manifest.urlVersions
283
287
  : {};
@@ -285,19 +289,20 @@ export class CttTracker {
285
289
  if (typeof scriptToken !== 'string' || !scriptToken) {
286
290
  throw new CttApiError("module manifest missing the screen script's version token");
287
291
  }
288
- const script = decodeText((await this.fetchBytes(`${BASE_URL}${SCREEN_SCRIPT_PATH}?${scriptToken}`)).bytes);
292
+ const script = decodeText((await this.fetchBytes(`${BASE_URL}${SCREEN_SCRIPT_PATH}?${scriptToken}`, budget)).bytes);
289
293
  this.screenScript = script;
290
294
  return script;
291
295
  }
292
- async fetchJson(url) {
293
- return parseJsonBytes((await this.fetchBytes(url)).bytes, 'CTT tracking');
296
+ async fetchJson(url, budget) {
297
+ return parseJsonBytes((await this.fetchBytes(url, budget)).bytes, 'CTT tracking');
294
298
  }
295
- async fetchBytes(url) {
299
+ async fetchBytes(url, budget) {
296
300
  const { response, bytes } = await fetchBounded(url, {
301
+ signal: budget.signal,
297
302
  headers: { Accept: 'application/json, text/plain, */*', 'User-Agent': BROWSER_USER_AGENT },
298
303
  }, {
299
304
  provider: 'CTT tracking',
300
- timeoutMs: this.timeoutMs,
305
+ timeoutMs: Math.min(this.timeoutMs, budget.remainingMs()),
301
306
  maxBytes: MAX_SCRIPT_BYTES,
302
307
  retryTransient: true,
303
308
  allowHttpError: true,
@@ -315,6 +320,6 @@ export const adapter = (environment) => {
315
320
  // The OutSystems session bootstrap and the version-token refresh are part
316
321
  // of the direct call, not separate tiers: they never change the transport.
317
322
  steps: ['direct'],
318
- track: (input) => tracker.fetch(input.number),
323
+ track: (input, context) => tracker.fetch(input.number, context),
319
324
  };
320
325
  };
@@ -1,5 +1,5 @@
1
1
  import { DateTime } from 'luxon';
2
- import { IndeterminateError, SchemaError } from '../../core/errors/index.js';
2
+ import { IndeterminateError, InvalidInputError, SchemaError } from '../../core/errors/index.js';
3
3
  import { calendarDay, explicitOffsetTime } from '../../core/time/index.js';
4
4
  import { clean } from '../../core/transport/index.js';
5
5
  import { isRecord } from '../../core/types.js';
@@ -7,7 +7,7 @@ import { classifyCttExpressStatus } from './status.js';
7
7
  export function normalizeCttExpressNumber(raw) {
8
8
  const number = raw.replace(/[\s.-]/g, '');
9
9
  if (!/^00\d{20}$/.test(number))
10
- throw new TypeError('CTT Express requires a Spanish shipment tracking number');
10
+ throw new InvalidInputError('CTT Express', 'CTT Express requires a Spanish shipment tracking number');
11
11
  return number;
12
12
  }
13
13
  function scanClock(value) {
@@ -1,10 +1,11 @@
1
1
  export { validateDachserTrackingUrl } from '../../core/catalog/urls.js';
2
- import type { AdapterFactory } from '../../core/adapter/index.js';
2
+ import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
3
3
  import type { CarrierResult } from '../../core/result/index.js';
4
4
  export interface DachserOptions {
5
5
  timeoutMs?: number;
6
6
  /** Test seam; production uses the global fetch. */
7
7
  fetcher?: typeof fetch;
8
+ userAgent?: string;
8
9
  }
9
10
  /**
10
11
  * Validate and canonicalize a pasted Dachser capability URL.
@@ -20,6 +21,6 @@ export declare class DachserTracker {
20
21
  #private;
21
22
  readonly timeoutMs: number;
22
23
  constructor(options?: number | DachserOptions);
23
- fetch(trackingNumber: string, trackingUrl: string): Promise<CarrierResult>;
24
+ fetch(trackingNumber: string, trackingUrl: string, context?: TrackingContext): Promise<CarrierResult>;
24
25
  }
25
26
  export declare const adapter: AdapterFactory;
@@ -1,8 +1,9 @@
1
1
  import { validateDachserTrackingUrl } from '../../core/catalog/urls.js';
2
2
  export { validateDachserTrackingUrl } from '../../core/catalog/urls.js';
3
3
  import { DateTime } from 'luxon';
4
- import { InputRequiredError, NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
5
- import { fetchBounded, parseJsonBytes } from '../../core/transport/index.js';
4
+ import { lookupBudget } from '../../core/adapter/index.js';
5
+ import { InputRequiredError, InvalidInputError, NotFoundError, SchemaError, UpstreamHttpError } from '../../core/errors/index.js';
6
+ import { fetchBounded, parseJsonBytes, userAgentOf } from '../../core/transport/index.js';
6
7
  import { isRecord } from '../../core/types.js';
7
8
  import { eventLabel, plainText, shipmentStatus } from './status.js';
8
9
  const DACHSER_HOST = 'customeriberia.dachser.com';
@@ -30,7 +31,17 @@ function normalizeTrackingNumber(raw) {
30
31
  * like a password.
31
32
  */
32
33
  export function dachserApiUrl(trackingUrl, trackingNumber) {
33
- const url = new URL(validateDachserTrackingUrl(trackingUrl, trackingNumber));
34
+ let validated;
35
+ try {
36
+ validated = validateDachserTrackingUrl(trackingUrl, trackingNumber);
37
+ }
38
+ catch (error) {
39
+ // A link that is not this shipment's is the caller's credential, rejected before any request.
40
+ if (error instanceof TypeError)
41
+ throw new InvalidInputError(PROVIDER, error.message, { cause: error });
42
+ throw error;
43
+ }
44
+ const url = new URL(validated);
34
45
  url.pathname = DACHSER_API_PATH;
35
46
  return url.toString();
36
47
  }
@@ -105,24 +116,29 @@ export function parseDachserTrackingResponse(payload, trackingNumber) {
105
116
  export class DachserTracker {
106
117
  timeoutMs;
107
118
  #fetcher;
119
+ #userAgent;
108
120
  constructor(options = {}) {
109
- const { timeoutMs = DEFAULT_TIMEOUT_MS, fetcher } = typeof options === 'number'
110
- ? { timeoutMs: options, fetcher: undefined }
121
+ const { timeoutMs = DEFAULT_TIMEOUT_MS, fetcher, userAgent } = typeof options === 'number'
122
+ ? { timeoutMs: options, fetcher: undefined, userAgent: undefined }
111
123
  : options;
112
124
  this.timeoutMs = timeoutMs;
113
125
  this.#fetcher = fetcher;
126
+ this.#userAgent = userAgentOf(userAgent);
114
127
  }
115
- async fetch(trackingNumber, trackingUrl) {
116
- const { bytes, response } = await fetchBounded(dachserApiUrl(trackingUrl, trackingNumber), {
128
+ async fetch(trackingNumber, trackingUrl, context = {}) {
129
+ const url = dachserApiUrl(trackingUrl, trackingNumber);
130
+ const budget = lookupBudget(context, this.timeoutMs);
131
+ const { bytes, response } = await fetchBounded(url, {
132
+ signal: budget.signal,
117
133
  headers: {
118
134
  Accept: 'application/json',
119
135
  'Accept-Language': 'en',
120
136
  Referer: `https://${DACHSER_HOST}${DACHSER_PAGE_PATH}`,
121
- 'User-Agent': 'SwissDeliveryTracker/1.0',
137
+ 'User-Agent': this.#userAgent,
122
138
  },
123
139
  }, {
124
140
  provider: 'Dachser tracking',
125
- timeoutMs: this.timeoutMs,
141
+ timeoutMs: Math.min(this.timeoutMs, budget.remainingMs()),
126
142
  allowHttpError: true,
127
143
  ...(this.#fetcher ? { fetcher: this.#fetcher } : {}),
128
144
  });
@@ -136,10 +152,12 @@ export class DachserTracker {
136
152
  catch {
137
153
  throw new UpstreamHttpError('Dachser tracking', response.status);
138
154
  }
139
- // An unknown shipment/access tuple currently reaches a null result in
140
- // Dachser's public endpoint and is surfaced as this stable JSON 500.
141
- // Match the public error signature narrowly so unrelated 500s remain
142
- // operational failures rather than false not-found results.
155
+ // An unknown shipment/access tuple reaches a null result in Dachser's
156
+ // public endpoint and surfaces as a JSON 500 whose message names it.
157
+ // ERR_APP_500 is the endpoint's catch-all for any unhandled exception and
158
+ // the message is the only field that says which one, so the `message: null`
159
+ // reply the same tuple also gets stays an operational failure, like every
160
+ // unrelated 500, rather than a false not-found.
143
161
  const errorMessage = plainText(isRecord(errorPayload) ? errorPayload.message ?? '' : '');
144
162
  if (response.status === 500
145
163
  && isRecord(errorPayload)
@@ -154,15 +172,15 @@ export class DachserTracker {
154
172
  }
155
173
  }
156
174
  export const adapter = (environment) => {
157
- const tracker = new DachserTracker({ fetcher: environment.fetcher });
175
+ const tracker = new DachserTracker({ fetcher: environment.fetcher, userAgent: environment.userAgent });
158
176
  return {
159
177
  id: 'dachser',
160
178
  steps: ['direct'],
161
- track: async (input) => {
179
+ track: async (input, context) => {
162
180
  if (!input.trackingUrl) {
163
181
  throw new InputRequiredError(PROVIDER, 'its complete tracking URL', 'Dachser tracking requires its complete tracking URL');
164
182
  }
165
- return tracker.fetch(input.number, input.trackingUrl);
183
+ return tracker.fetch(input.number, input.trackingUrl, context);
166
184
  },
167
185
  };
168
186
  };
@@ -1,4 +1,4 @@
1
- import { IndeterminateError, NotFoundError, SchemaError } from '../../core/errors/index.js';
1
+ import { IndeterminateError, InvalidInputError, NotFoundError, SchemaError } from '../../core/errors/index.js';
2
2
  import { isoTime } from '../../core/time/index.js';
3
3
  import { clean } from '../../core/transport/index.js';
4
4
  import { isRecord } from '../../core/types.js';
@@ -13,7 +13,7 @@ function scanTime(value) {
13
13
  export function normalizeDelhiveryNumber(raw) {
14
14
  const number = raw.replace(/[\s.-]/g, '');
15
15
  if (!/^\d{13,14}$/.test(number))
16
- throw new TypeError('Delhivery requires a thirteen- or fourteen-digit waybill');
16
+ throw new InvalidInputError('Delhivery', 'Delhivery requires a thirteen- or fourteen-digit waybill');
17
17
  return number;
18
18
  }
19
19
  export function parseDelhivery(payload, number) {
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * DHL's business API credentials are not needed for this public flow.
15
15
  */
16
- import type { AdapterFactory } from '../../core/adapter/index.js';
16
+ import type { AdapterFactory, TrackingContext } from '../../core/adapter/index.js';
17
17
  import { ChallengeError, type CarrierErrorOptions } from '../../core/errors/index.js';
18
18
  import type { CarrierResult } from '../../core/result/index.js';
19
19
  import { type StepRecorder } from '../../core/telemetry/index.js';
@@ -60,7 +60,7 @@ export declare class DHLTracker {
60
60
  constructor(options?: DHLTrackerOptions);
61
61
  private open;
62
62
  /** Lookups share one session, so two parcels never renew it at the same time. */
63
- fetch(trackingNumber: string): Promise<CarrierResult>;
63
+ fetch(trackingNumber: string, context?: TrackingContext): Promise<CarrierResult>;
64
64
  private lookup;
65
65
  /** The direct session, renewing a stale one or an interrupted read once. */
66
66
  private direct;
@@ -15,9 +15,9 @@
15
15
  */
16
16
  import makeFetchCookie from 'fetch-cookie';
17
17
  import { Cookie, CookieJar } from 'tough-cookie';
18
- import { ChallengeError, IndeterminateError, InputRequiredError, RateLimitedError, SchemaError, } from '../../core/errors/index.js';
18
+ import { ChallengeError, IndeterminateError, InputRequiredError, InvalidInputError, RateLimitedError, SchemaError, } from '../../core/errors/index.js';
19
19
  import { carrierIdFromPartnerLinks } from '../../core/catalog/hints.js';
20
- import { runSteps, singleFlight } from '../../core/runner/index.js';
20
+ import { runSteps, singleFlight, takeTurn } from '../../core/runner/index.js';
21
21
  import { NOOP_RECORDER } from '../../core/telemetry/index.js';
22
22
  import { isoTime } from '../../core/time/index.js';
23
23
  import { clean as cleanText, fetchBounded, parseJsonBytes, TrawlClient, UpstreamHttpError, UpstreamNetworkError, } from '../../core/transport/index.js';
@@ -42,7 +42,7 @@ function clean(value, limit = 500) {
42
42
  export function normalizeDHLTrackingNumber(raw) {
43
43
  const value = raw.toUpperCase().replace(/[\s.-]/g, '');
44
44
  if (!/^[A-Z0-9]{5,40}$/.test(value))
45
- throw new TypeError('DHL tracking number is invalid');
45
+ throw new InvalidInputError(PROVIDER, 'DHL tracking number is invalid');
46
46
  return value;
47
47
  }
48
48
  export function dhlTrackingUrl(number) {
@@ -152,6 +152,14 @@ const SESSION_MAX_AGE_MS = 100 * 60_000;
152
152
  function renewable(error) {
153
153
  return error instanceof DHLSessionError || error instanceof UpstreamNetworkError;
154
154
  }
155
+ /** A request's own timeout, shortened to what is left of the lookup budget. */
156
+ function capped(timeoutMs, { deadline }) {
157
+ return Math.max(1, Math.floor(Math.min(timeoutMs, deadline - performance.now())));
158
+ }
159
+ /** A lookup that was cancelled or ran out of time starts nothing more: no renewal, no browser. */
160
+ function over(signal, deadline) {
161
+ return signal?.aborted === true || deadline - performance.now() < 1;
162
+ }
155
163
  class DHLSession {
156
164
  timeoutMs;
157
165
  jar = new CookieJar();
@@ -182,9 +190,9 @@ class DHLSession {
182
190
  }), `https://${domain}${path}`);
183
191
  }
184
192
  }
185
- async fetch(number) {
193
+ async fetch(number, limits) {
186
194
  if (!this.token) {
187
- const config = await this.request(CONFIG_URL);
195
+ const config = await this.request(CONFIG_URL, limits);
188
196
  if (typeof config.verfolgenCsrfToken !== 'string' || !config.verfolgenCsrfToken)
189
197
  throw new DHLSessionError();
190
198
  this.token = config.verfolgenCsrfToken;
@@ -192,16 +200,16 @@ class DHLSession {
192
200
  }
193
201
  const url = new URL(`${ORIGIN}${DATA_PATH}/search`);
194
202
  url.search = new URLSearchParams({ piececode: number, noRedirect: 'true', language: 'en' }).toString();
195
- return parseDHLTrackingResponse(await this.request(url), number);
203
+ return parseDHLTrackingResponse(await this.request(url, limits), number);
196
204
  }
197
- async request(url) {
198
- const { response, bytes } = await fetchBounded(url, { headers: {
205
+ async request(url, limits) {
206
+ const { response, bytes } = await fetchBounded(url, { signal: limits.signal, headers: {
199
207
  Accept: 'application/json, text/plain, */*', 'Accept-Language': 'en-US,en;q=0.9',
200
208
  'User-Agent': this.userAgent, Referer: TRACKING_PAGE,
201
209
  'Sec-Fetch-Site': 'same-origin', 'Sec-Fetch-Mode': 'cors', 'Sec-Fetch-Dest': 'empty',
202
210
  ...(this.token ? { 'verfolgen-CSRF-token': this.token, 'verfolgen-wg': this.wg } : {}),
203
211
  } }, {
204
- provider: PROVIDER, timeoutMs: this.timeoutMs, maxBytes: MAX_BYTES,
212
+ provider: PROVIDER, timeoutMs: capped(this.timeoutMs, limits), maxBytes: MAX_BYTES,
205
213
  fetcher: this.fetcher, redirect: 'manual', allowHttpError: true,
206
214
  });
207
215
  if ([301, 302, 303, 307, 308, 401, 403, 419].includes(response.status))
@@ -249,15 +257,32 @@ export class DHLTracker {
249
257
  return session;
250
258
  }
251
259
  /** Lookups share one session, so two parcels never renew it at the same time. */
252
- async fetch(trackingNumber) {
260
+ async fetch(trackingNumber, context = {}) {
253
261
  const number = normalizeDHLTrackingNumber(trackingNumber);
254
- return this.serialize(() => this.lookup(number));
262
+ return takeTurn(this.serialize, 'dhl', context, (left) => this.lookup(number, left));
255
263
  }
256
- async lookup(number) {
264
+ async lookup(number, context) {
265
+ const { signal } = context;
266
+ // The caller may have left while the lookup waited for its turn.
267
+ signal?.throwIfAborted();
268
+ const budgetMs = context.budgetMs ?? this.budgetMs;
269
+ const deadline = performance.now() + budgetMs;
270
+ // The direct step's own signal: once it has aborted, the lookup is over and no browser follows.
271
+ let ended;
257
272
  try {
258
- return await runSteps({ carrier: 'dhl', budgetMs: this.budgetMs, recorder: this.recorder }, [
259
- { id: 'direct', run: () => this.direct(number) },
260
- { id: 'trawl', enabled: this.trawl !== null || this.trawlUrl !== '', recovers: renewable, run: () => this.browser(number) },
273
+ return await runSteps({ carrier: 'dhl', budgetMs, signal, recorder: this.recorder }, [
274
+ {
275
+ id: 'direct',
276
+ run: (step) => {
277
+ ended = step.signal;
278
+ return this.direct(number, { signal: step.signal, deadline });
279
+ },
280
+ },
281
+ {
282
+ id: 'trawl', enabled: this.trawl !== null || this.trawlUrl !== '',
283
+ recovers: (error) => renewable(error) && !over(ended, deadline),
284
+ run: (step) => this.browser(number, { signal: step.signal, deadline }),
285
+ },
261
286
  ]);
262
287
  }
263
288
  catch (error) {
@@ -268,30 +293,33 @@ export class DHLTracker {
268
293
  }
269
294
  }
270
295
  /** The direct session, renewing a stale one or an interrupted read once. */
271
- async direct(number) {
296
+ async direct(number, limits) {
272
297
  if (this.session !== null && this.now() - this.sessionOpenedAt >= SESSION_MAX_AGE_MS)
273
298
  this.session = null;
274
299
  const cached = this.session !== null;
275
300
  const session = this.session ?? this.open(new DHLSession(this.directTimeoutMs, this.fetcher));
276
301
  try {
277
- return await session.fetch(number);
302
+ return await session.fetch(number, limits);
278
303
  }
279
304
  catch (error) {
280
305
  // A fresh session that was rejected outright needs a browser, not a
281
306
  // second identical attempt; a cached one may simply have expired.
282
307
  if (!renewable(error) || (!cached && !(error instanceof UpstreamNetworkError)))
283
308
  throw error;
284
- return await this.open(new DHLSession(this.directTimeoutMs, this.fetcher)).fetch(number);
309
+ if (over(limits.signal, limits.deadline))
310
+ throw error;
311
+ return await this.open(new DHLSession(this.directTimeoutMs, this.fetcher)).fetch(number, limits);
285
312
  }
286
313
  }
287
314
  /** A solved browser session, replayed over HTTP with its cookies and user agent. */
288
- async browser(number) {
315
+ async browser(number, limits) {
289
316
  this.session = null;
290
317
  const trawl = this.trawl ?? new TrawlClient(this.trawlUrl, this.fetcher);
291
- const solved = await trawl.scrape({ url: dhlTrackingUrl(number), skipHttp: true, maxTier: 3, maxTimeout: this.timeoutMs }, { provider: `TRAWL while fetching ${PROVIDER}`, timeoutMs: this.timeoutMs, maxBytes: 10_000_000, fetcher: this.fetcher });
318
+ const timeoutMs = capped(this.timeoutMs, limits);
319
+ const solved = await trawl.scrape({ url: dhlTrackingUrl(number), skipHttp: true, maxTier: 3, maxTimeout: timeoutMs }, { provider: `TRAWL while fetching ${PROVIDER}`, timeoutMs, maxBytes: 10_000_000, fetcher: this.fetcher, signal: limits.signal });
292
320
  const session = new DHLSession(this.directTimeoutMs, this.fetcher);
293
321
  await session.seed(solved.cookies, solved.userAgent);
294
- const result = await session.fetch(number);
322
+ const result = await session.fetch(number, limits);
295
323
  this.open(session);
296
324
  return result;
297
325
  }
@@ -303,6 +331,6 @@ export const adapter = (environment) => {
303
331
  return {
304
332
  id: 'dhl',
305
333
  steps: ['direct', 'trawl'],
306
- track: (input) => tracker.fetch(input.number),
334
+ track: (input, context) => tracker.fetch(input.number, context),
307
335
  };
308
336
  };
@@ -14,7 +14,7 @@
14
14
  * alias, so only one eCommerce shipment from that exact request URL is
15
15
  * accepted, and the id itself is never retained.
16
16
  */
17
- import type { AdapterFactory } from '../../core/adapter/index.js';
17
+ import type { AdapterFactory, TrackingContext } from '../../core/adapter/index.js';
18
18
  import { ChallengeError, type CarrierErrorOptions } from '../../core/errors/index.js';
19
19
  import type { CarrierResult } from '../../core/result/index.js';
20
20
  import { type StepRecorder } from '../../core/telemetry/index.js';
@@ -34,7 +34,7 @@ export declare class DHLEcommerceSessionError extends ChallengeError {
34
34
  readonly status: number;
35
35
  constructor(status: number, options?: CarrierErrorOptions);
36
36
  }
37
- export interface DHLEcommerceTrackerOptions extends UniversalBrowserOptions {
37
+ export interface DHLEcommerceTrackerOptions extends Omit<UniversalBrowserOptions, 'signal'> {
38
38
  budgetMs?: number;
39
39
  recorder?: StepRecorder;
40
40
  }
@@ -44,7 +44,7 @@ export declare class DHLEcommerceTracker {
44
44
  private readonly serialize;
45
45
  constructor(options?: DHLEcommerceTrackerOptions);
46
46
  /** One browser at a time per instance: a batch must not spawn a Chromium per parcel. */
47
- fetch(trackingNumber: string): Promise<CarrierResult>;
47
+ fetch(trackingNumber: string, context?: TrackingContext): Promise<CarrierResult>;
48
48
  private browser;
49
49
  }
50
50
  export declare const adapter: AdapterFactory;
@@ -15,8 +15,8 @@
15
15
  * accepted, and the id itself is never retained.
16
16
  */
17
17
  import { DateTime } from 'luxon';
18
- import { ChallengeError, SchemaError } from '../../core/errors/index.js';
19
- import { runSteps, singleFlight } from '../../core/runner/index.js';
18
+ import { ChallengeError, InvalidInputError, SchemaError } from '../../core/errors/index.js';
19
+ import { runSteps, singleFlight, takeTurn } from '../../core/runner/index.js';
20
20
  import { countryTimeZone } from '../../core/time/index.js';
21
21
  import { NOOP_RECORDER } from '../../core/telemetry/index.js';
22
22
  import { clean as cleanText, UpstreamHttpError } from '../../core/transport/index.js';
@@ -38,7 +38,7 @@ function clean(value, limit = 500) {
38
38
  export function normalizeDHLEcommerceNumber(raw) {
39
39
  const number = raw.toUpperCase().replace(/[\s.-]/g, '');
40
40
  if (!/^(?=.*\d)[A-Z0-9]{5,40}$/.test(number))
41
- throw new TypeError('DHL eCommerce tracking number is invalid');
41
+ throw new InvalidInputError(PROVIDER, 'DHL eCommerce tracking number is invalid');
42
42
  return number;
43
43
  }
44
44
  export function dhlEcommerceTrackingUrl(number) {
@@ -146,14 +146,14 @@ export class DHLEcommerceTracker {
146
146
  this.recorder = options.recorder ?? NOOP_RECORDER;
147
147
  }
148
148
  /** One browser at a time per instance: a batch must not spawn a Chromium per parcel. */
149
- async fetch(trackingNumber) {
149
+ async fetch(trackingNumber, context = {}) {
150
150
  const number = normalizeDHLEcommerceNumber(trackingNumber);
151
151
  const timeoutMs = this.options.timeoutMs ?? 45_000;
152
- return this.serialize(() => runSteps({ carrier: 'dhl-ecommerce', budgetMs: this.options.budgetMs ?? timeoutMs + 15_000, recorder: this.recorder }, [{ id: 'browser', run: () => this.browser(number, timeoutMs) }]));
152
+ return takeTurn(this.serialize, PROVIDER, context, ({ signal, budgetMs }) => runSteps({ carrier: 'dhl-ecommerce', budgetMs: budgetMs ?? this.options.budgetMs ?? timeoutMs + 15_000, signal, recorder: this.recorder }, [{ id: 'browser', run: (step) => this.browser(number, Math.max(1, Math.floor(Math.min(timeoutMs, step.remainingMs))), step.signal) }]));
153
153
  }
154
- async browser(number, timeoutMs) {
154
+ async browser(number, timeoutMs, signal) {
155
155
  try {
156
- return await scrapeUniversalPage({ executablePath: this.options.executablePath, timeoutMs }, {
156
+ return await scrapeUniversalPage({ executablePath: this.options.executablePath, timeoutMs, signal }, {
157
157
  name: PROVIDER, url: dhlEcommerceTrackingUrl(number), responseUrl: trackingApiUrl(number),
158
158
  }, parseDHLEcommerceResponse);
159
159
  }
@@ -174,6 +174,6 @@ export const adapter = (environment) => {
174
174
  return {
175
175
  id: 'dhl-ecommerce',
176
176
  steps: ['browser'],
177
- track: (input) => tracker.fetch(input.number),
177
+ track: (input, context) => tracker.fetch(input.number, context),
178
178
  };
179
179
  };
@@ -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 { ChallengeError, IndeterminateError, NotFoundError, type CarrierErrorOptions } from '../../core/errors/index.js';
3
3
  import type { CarrierResult } from '../../core/result/index.js';
4
4
  import type { StepRecorder } from '../../core/telemetry/index.js';
@@ -45,7 +45,7 @@ export declare class DPDTracker {
45
45
  constructor(options?: DPDTrackerOptions);
46
46
  /** The browser service, resolved late so a malformed URL fails the page step, not construction. */
47
47
  private browserService;
48
- fetch(trackingNumber: string, postcode?: string): Promise<CarrierResult>;
48
+ fetch(trackingNumber: string, postcode?: string, context?: TrackingContext): Promise<CarrierResult>;
49
49
  /**
50
50
  * Whether DPD knows a 14-digit number, for the host's carrier-detection
51
51
  * route, which promotes the number to `dpd` only when this returns true.
@@ -54,7 +54,7 @@ export declare class DPDTracker {
54
54
  * parcels) are false; any other failure, a 400 from a token step included,
55
55
  * stays a failure.
56
56
  */
57
- recognizes(trackingNumber: string): Promise<boolean>;
57
+ recognizes(trackingNumber: string, context?: TrackingContext): Promise<boolean>;
58
58
  private apiFetch;
59
59
  private detailsWithFreshToken;
60
60
  private parcelDetails;