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.
- package/README.md +209 -71
- package/data/carrier.schema.json +6 -4
- package/data/catalog.json +6 -6
- package/dist/app.d.ts +11 -0
- package/dist/app.js +11 -0
- package/dist/browser/scraper.js +1386 -1568
- package/dist/carriers/aliexpress/adapter.d.ts +5 -3
- package/dist/carriers/aliexpress/adapter.js +28 -8
- package/dist/carriers/amazon-shipping/adapter.d.ts +3 -2
- package/dist/carriers/amazon-shipping/adapter.js +15 -10
- package/dist/carriers/amazon-shipping/eligibility.d.ts +2 -1
- package/dist/carriers/amazon-shipping/eligibility.js +2 -2
- package/dist/carriers/aramex/adapter.d.ts +2 -0
- package/dist/carriers/aramex/adapter.js +5 -3
- package/dist/carriers/aramex/parser.js +2 -2
- package/dist/carriers/asendia/adapter.d.ts +8 -0
- package/dist/carriers/asendia/adapter.js +66 -11
- package/dist/carriers/asendia/probe.d.ts +2 -0
- package/dist/carriers/asendia/probe.js +9 -7
- package/dist/carriers/australia-post/adapter.js +2 -2
- package/dist/carriers/austrian-post/adapter.js +2 -2
- package/dist/carriers/blue-dart/parser.js +2 -2
- package/dist/carriers/bpost/parser.js +2 -2
- package/dist/carriers/bring-posten/parser.js +2 -2
- package/dist/carriers/brt/parser.js +2 -2
- package/dist/carriers/c-chez-vous/adapter.d.ts +3 -2
- package/dist/carriers/c-chez-vous/adapter.js +15 -10
- package/dist/carriers/canada-post/adapter.d.ts +1 -0
- package/dist/carriers/canada-post/adapter.js +5 -3
- package/dist/carriers/canada-post/parser.js +2 -2
- package/dist/carriers/canpar/parser.js +2 -2
- package/dist/carriers/ciblex/parser.js +2 -2
- package/dist/carriers/colis-prive/adapter.d.ts +3 -2
- package/dist/carriers/colis-prive/adapter.js +0 -0
- package/dist/carriers/colisweb/adapter.d.ts +3 -2
- package/dist/carriers/colisweb/adapter.js +13 -8
- package/dist/carriers/correios-br/parser.js +2 -2
- package/dist/carriers/correos-chile/parser.js +3 -3
- package/dist/carriers/correos-express/parser.js +2 -2
- package/dist/carriers/correos-spain/adapter.d.ts +4 -2
- package/dist/carriers/correos-spain/adapter.js +16 -8
- package/dist/carriers/ctt/adapter.d.ts +2 -2
- package/dist/carriers/ctt/adapter.js +30 -25
- package/dist/carriers/ctt-express/parser.js +2 -2
- package/dist/carriers/dachser/adapter.d.ts +3 -2
- package/dist/carriers/dachser/adapter.js +34 -16
- package/dist/carriers/delhivery/parser.js +2 -2
- package/dist/carriers/dhl/adapter.d.ts +2 -2
- package/dist/carriers/dhl/adapter.js +50 -22
- package/dist/carriers/dhl-ecommerce/adapter.d.ts +3 -3
- package/dist/carriers/dhl-ecommerce/adapter.js +8 -8
- package/dist/carriers/dpd/adapter.d.ts +3 -3
- package/dist/carriers/dpd/adapter.js +113 -57
- package/dist/carriers/dpd/carrier.json +1 -1
- package/dist/carriers/dpd-fr/adapter.d.ts +2 -2
- package/dist/carriers/dpd-fr/adapter.js +29 -20
- package/dist/carriers/dtdc/parser.js +2 -2
- package/dist/carriers/ecoscooting/parser.js +2 -2
- package/dist/carriers/ems/adapter.js +2 -2
- package/dist/carriers/estafeta/parser.js +2 -2
- package/dist/carriers/evri/adapter.js +2 -2
- package/dist/carriers/fedex/adapter.d.ts +2 -2
- package/dist/carriers/fedex/adapter.js +14 -10
- package/dist/carriers/four-px/adapter.js +2 -2
- package/dist/carriers/geodis/adapter.d.ts +3 -2
- package/dist/carriers/geodis/adapter.js +13 -8
- package/dist/carriers/gls-ch/adapter.d.ts +4 -3
- package/dist/carriers/gls-ch/adapter.js +27 -19
- package/dist/carriers/gls-ch/carrier.json +1 -1
- package/dist/carriers/gls-de/adapter.d.ts +3 -3
- package/dist/carriers/gls-de/adapter.js +14 -9
- package/dist/carriers/gls-de/carrier.json +1 -1
- package/dist/carriers/gls-fr/adapter.d.ts +3 -2
- package/dist/carriers/gls-fr/adapter.js +16 -11
- package/dist/carriers/gofo/parser.js +2 -2
- package/dist/carriers/heppner/adapter.d.ts +4 -2
- package/dist/carriers/heppner/adapter.js +19 -14
- package/dist/carriers/heppner/carrier.json +1 -1
- package/dist/carriers/hermes/adapter.d.ts +3 -2
- package/dist/carriers/hermes/adapter.js +13 -8
- package/dist/carriers/hermes-de/adapter.d.ts +3 -2
- package/dist/carriers/hermes-de/adapter.js +16 -12
- package/dist/carriers/india-post/adapter.d.ts +2 -2
- package/dist/carriers/india-post/adapter.js +42 -21
- package/dist/carriers/inpost/adapter.d.ts +4 -2
- package/dist/carriers/inpost/adapter.js +17 -10
- package/dist/carriers/japan-post/adapter.js +2 -2
- package/dist/carriers/korea-post/adapter.js +2 -2
- package/dist/carriers/la-poste/adapter.d.ts +4 -2
- package/dist/carriers/la-poste/adapter.js +18 -13
- package/dist/carriers/landmark-global/parser.js +2 -2
- package/dist/carriers/mondial-relay/adapter.d.ts +2 -2
- package/dist/carriers/mondial-relay/adapter.js +26 -17
- package/dist/carriers/mondial-relay/carrier.json +1 -1
- package/dist/carriers/mrw/adapter.js +4 -2
- package/dist/carriers/mrw/parser.js +3 -3
- package/dist/carriers/nacex/parser.js +2 -2
- package/dist/carriers/ninja-van/parser.js +2 -2
- package/dist/carriers/nz-post/parser.js +2 -2
- package/dist/carriers/ontrac/parser.js +2 -2
- package/dist/carriers/paack/adapter.d.ts +4 -2
- package/dist/carriers/paack/adapter.js +24 -14
- package/dist/carriers/paack/carrier.json +1 -1
- package/dist/carriers/packeta/adapter.d.ts +4 -2
- package/dist/carriers/packeta/adapter.js +16 -8
- package/dist/carriers/planzer/adapter.d.ts +5 -3
- package/dist/carriers/planzer/adapter.js +35 -12
- package/dist/carriers/planzer/shared.d.ts +4 -1
- package/dist/carriers/planzer/shared.js +20 -6
- package/dist/carriers/poczta-polska/parser.js +2 -2
- package/dist/carriers/pos-malaysia/adapter.d.ts +2 -0
- package/dist/carriers/pos-malaysia/adapter.js +9 -5
- package/dist/carriers/poste-italiane/adapter.d.ts +4 -2
- package/dist/carriers/poste-italiane/adapter.js +16 -8
- package/dist/carriers/posti/adapter.js +2 -2
- package/dist/carriers/postlogistics/adapter.d.ts +6 -4
- package/dist/carriers/postlogistics/adapter.js +49 -17
- package/dist/carriers/postlogistics/status.d.ts +2 -0
- package/dist/carriers/postlogistics/status.js +2 -0
- package/dist/carriers/postnord/parser.js +2 -2
- package/dist/carriers/purolator/parser.js +2 -2
- package/dist/carriers/relais-colis/adapter.js +2 -2
- package/dist/carriers/royal-mail/adapter.d.ts +2 -2
- package/dist/carriers/royal-mail/adapter.js +14 -10
- package/dist/carriers/seur/parser.js +2 -2
- package/dist/carriers/sf-express/parser.js +2 -2
- package/dist/carriers/singapore-post/adapter.js +2 -2
- package/dist/carriers/spring-gds/adapter.d.ts +2 -0
- package/dist/carriers/spring-gds/adapter.js +16 -15
- package/dist/carriers/sunyou/adapter.d.ts +5 -3
- package/dist/carriers/sunyou/adapter.js +16 -9
- package/dist/carriers/swiss-post/adapter.d.ts +4 -4
- package/dist/carriers/swiss-post/adapter.js +33 -15
- package/dist/carriers/swiss-post-cargo/adapter.d.ts +3 -2
- package/dist/carriers/swiss-post-cargo/adapter.js +14 -9
- package/dist/carriers/the-courier-guy/number.js +3 -2
- package/dist/carriers/tipsa/parser.js +3 -3
- package/dist/carriers/tnt/adapter.js +3 -3
- package/dist/carriers/ukrposhta/adapter.js +20 -62
- package/dist/carriers/ukrposhta/parser.js +2 -2
- package/dist/carriers/uniuni/parser.js +3 -3
- package/dist/carriers/ups/adapter.d.ts +2 -2
- package/dist/carriers/ups/adapter.js +40 -32
- package/dist/carriers/usps/adapter.d.ts +2 -2
- package/dist/carriers/usps/adapter.js +14 -10
- package/dist/carriers/yamato/adapter.js +2 -2
- package/dist/carriers/yanwen/adapter.js +2 -2
- package/dist/carriers/yto/parser.js +2 -2
- package/dist/carriers/yunda/parser.js +2 -2
- package/dist/carriers/yunexpress/adapter.js +21 -39
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +49 -19
- package/dist/core/adapter/index.d.ts +18 -1
- package/dist/core/adapter/index.js +28 -4
- package/dist/core/adapter/track.js +21 -7
- package/dist/core/catalog/index.d.ts +1 -15
- package/dist/core/catalog/index.js +3 -106
- package/dist/core/catalog/inputs.d.ts +2 -2
- package/dist/core/catalog/inputs.js +4 -4
- package/dist/core/catalog/parcel.d.ts +53 -0
- package/dist/core/catalog/parcel.js +105 -0
- package/dist/core/catalog/types.d.ts +1 -29
- package/dist/core/catalog/urls.js +1 -0
- package/dist/core/errors/hint.d.ts +7 -0
- package/dist/core/errors/hint.js +11 -1
- package/dist/core/errors/index.d.ts +8 -1
- package/dist/core/errors/index.js +11 -0
- package/dist/core/runner/index.d.ts +23 -1
- package/dist/core/runner/index.js +71 -14
- package/dist/core/transport/boundedFetch.js +24 -19
- package/dist/core/transport/browser.d.ts +1 -0
- package/dist/core/transport/browser.js +76 -99
- package/dist/core/transport/index.d.ts +4 -1
- package/dist/core/transport/index.js +3 -1
- package/dist/core/transport/localBrowser.d.ts +24 -0
- package/dist/core/transport/localBrowser.js +87 -0
- package/dist/core/transport/trawl.d.ts +6 -0
- package/dist/core/transport/trawl.js +30 -9
- package/dist/core/transport/userAgent.d.ts +8 -0
- package/dist/core/transport/userAgent.js +15 -0
- package/dist/data/catalog.json +6 -6
- package/dist/facade/index.d.ts +3 -0
- package/dist/facade/index.js +29 -11
- package/dist/generated/catalog.d.ts +6 -6
- package/dist/generated/catalog.js +6 -6
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -4
- package/dist/providers/parcelsapp/adapter.d.ts +1 -1
- package/dist/providers/parcelsapp/adapter.js +13 -10
- package/dist/providers/parcelsapp/http.d.ts +1 -1
- package/dist/providers/parcelsapp/http.js +2 -2
- package/dist/providers/postal-ninja/adapter.d.ts +2 -2
- package/dist/providers/postal-ninja/adapter.js +9 -8
- package/dist/providers/seventeentrack/adapter.d.ts +1 -1
- package/dist/providers/seventeentrack/adapter.js +7 -7
- package/dist/providers/shared/capture.d.ts +2 -0
- package/dist/providers/shared/capture.js +1 -1
- package/dist/providers/shared/result.js +4 -2
- package/dist/providers/ship24/adapter.d.ts +2 -2
- package/dist/providers/ship24/adapter.js +15 -11
- package/dist/providers/ship24/http.d.ts +1 -1
- package/dist/providers/ship24/http.js +4 -3
- package/dist/providers/universal.d.ts +3 -2
- package/dist/providers/universal.js +20 -2
- package/dist/providers/upu/adapter.js +2 -2
- package/dist/scripts/carrier-canary.js +2 -1
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +90 -12
- package/dist/server/openapi.json +399 -52
- package/package.json +13 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
|
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':
|
|
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
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
259
|
-
{
|
|
260
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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;
|