universal-parcel-scraper 0.413.0-main.2132 → 0.415.0-main.2136

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 CHANGED
@@ -13,7 +13,7 @@ The engine behind [Peek](https://peektracker.com) ([GitHub](https://github.com/p
13
13
  [![License: Apache-2.0](https://img.shields.io/badge/core-Apache--2.0-blue.svg)](LICENSE)
14
14
 
15
15
  <!-- GENERATED:summary -->
16
- **3,500+ carriers** through **448 dedicated adapters** and **5 universal fallbacks**
16
+ **3,500+ carriers** through **450 dedicated adapters** and **5 universal fallbacks**
17
17
 
18
18
  <sub>760 catalog entries · 134 countries represented</sub>
19
19
  <!-- /GENERATED:summary -->
@@ -155,7 +155,7 @@ createTracker({ providers: ['ParcelsApp', 'Ship24', '17TRACK', 'Postal Ninja', '
155
155
  <!-- GENERATED:stages -->
156
156
  <img src="docs/assets/stages.svg" alt="DHL: Die Sendung wurde in das Zustellfahrzeug geladen.; Mondial Relay: En cours de livraison; Correios Brazil: Objeto saiu para entrega ao destinatário; Correos Express: EN REPARTO; Yamato Transport: 配達中; La Poste / Colissimo: DISTOU. All are filed under out_for_delivery." width="760">
157
157
 
158
- The carrier folders record 14,283 such statuses, each filed under one stage.
158
+ The carrier folders record 14,300 such statuses, each filed under one stage.
159
159
  <!-- /GENERATED:stages -->
160
160
 
161
161
  Wording nobody recorded yet goes through a classifier that reads seven European languages.
package/data/catalog.json CHANGED
@@ -18174,7 +18174,9 @@
18174
18174
  "timezone": "Asia/Riyadh",
18175
18175
  "tracking": {
18176
18176
  "mode": "automatic",
18177
- "adapter": "universal"
18177
+ "adapter": "labaih",
18178
+ "recognitionRank": 788,
18179
+ "localClocks": true
18178
18180
  },
18179
18181
  "canaryUrl": "https://mylabaih.com/",
18180
18182
  "trackingUrlTemplate": "https://track.mylabaih.com/track/?cn={trackingNumber}",
@@ -18184,7 +18186,7 @@
18184
18186
  "domains": [
18185
18187
  "track.mylabaih.com"
18186
18188
  ],
18187
- "pathPattern": "^/track/?$",
18189
+ "pathPattern": "^/track(?:/en)?/?$",
18188
18190
  "params": [
18189
18191
  "cn"
18190
18192
  ]
@@ -25350,7 +25352,8 @@
25350
25352
  "timezone": "Africa/Cairo",
25351
25353
  "tracking": {
25352
25354
  "mode": "automatic",
25353
- "adapter": "universal"
25355
+ "adapter": "shipblu",
25356
+ "recognitionRank": 789
25354
25357
  },
25355
25358
  "canaryUrl": "https://go.shipblu.com/en/",
25356
25359
  "trackingUrlTemplate": "https://go.shipblu.com/en/{trackingNumber}",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "catalog": 760,
3
- "activeAdapters": 448,
3
+ "activeAdapters": 450,
4
4
  "countries": 134,
5
5
  "referenceCarriers": 100,
6
6
  "comparison": {
@@ -13879,14 +13879,16 @@ var Re = {
13879
13879
  timezone: "Asia/Riyadh",
13880
13880
  tracking: {
13881
13881
  mode: "automatic",
13882
- adapter: "universal"
13882
+ adapter: "labaih",
13883
+ recognitionRank: 788,
13884
+ localClocks: !0
13883
13885
  },
13884
13886
  canaryUrl: "https://mylabaih.com/",
13885
13887
  trackingUrlTemplate: "https://track.mylabaih.com/track/?cn={trackingNumber}",
13886
13888
  trackingSearchUrl: "https://mylabaih.com/",
13887
13889
  linkRules: [{
13888
13890
  domains: ["track.mylabaih.com"],
13889
- pathPattern: "^/track/?$",
13891
+ pathPattern: "^/track(?:/en)?/?$",
13890
13892
  params: ["cn"]
13891
13893
  }],
13892
13894
  detectionRules: []
@@ -19162,7 +19164,8 @@ var Re = {
19162
19164
  timezone: "Africa/Cairo",
19163
19165
  tracking: {
19164
19166
  mode: "automatic",
19165
- adapter: "universal"
19167
+ adapter: "shipblu",
19168
+ recognitionRank: 789
19166
19169
  },
19167
19170
  canaryUrl: "https://go.shipblu.com/en/",
19168
19171
  trackingUrlTemplate: "https://go.shipblu.com/en/{trackingNumber}",
@@ -29141,6 +29144,7 @@ var _l = {
29141
29144
  "kuehne-nagel": 435,
29142
29145
  "l-post": 808,
29143
29146
  "la-poste": 1e3,
29147
+ labaih: 788,
29144
29148
  "latvijas-pasts": 100,
29145
29149
  "leopards-courier": 965,
29146
29150
  "lietuvos-pastas": 895,
@@ -29226,6 +29230,7 @@ var _l = {
29226
29230
  seur: 870,
29227
29231
  sfc: 955,
29228
29232
  shadowfax: 595,
29233
+ shipblu: 789,
29229
29234
  shipentegra: 833,
29230
29235
  "shree-maruti": 822,
29231
29236
  "skroutz-last-mile": 913,
@@ -0,0 +1,15 @@
1
+ import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
2
+ import type { CarrierResult } from '../../core/result/index.js';
3
+ import { type StepRecorder } from '../../core/telemetry/index.js';
4
+ /** The tracker's English page; the Arabic one that links open leaves some scans out. */
5
+ export declare const LABAIH_TRACKING = "https://track.mylabaih.com/track/en/?cn=";
6
+ export declare class LabaihTracker {
7
+ private readonly options;
8
+ constructor(options?: {
9
+ fetcher?: typeof fetch;
10
+ recorder?: StepRecorder;
11
+ userAgent?: string;
12
+ });
13
+ fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
14
+ }
15
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,59 @@
1
+ import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
+ import { NotFoundError, TransportError } from '../../core/errors/index.js';
3
+ import { runSteps } from '../../core/runner/index.js';
4
+ import { firstAttemptMs, networkRetryStep } from '../../core/runner/networkRetry.js';
5
+ import { NOOP_RECORDER } from '../../core/telemetry/index.js';
6
+ import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
7
+ import { LABAIH_MAX_BYTES, normalizeLabaihNumber, parseLabaih, PROVIDER } from './parser.js';
8
+ /** The tracker's English page; the Arabic one that links open leaves some scans out. */
9
+ export const LABAIH_TRACKING = 'https://track.mylabaih.com/track/en/?cn=';
10
+ const REDIRECTS = [301, 302, 303, 307, 308];
11
+ /** The tracker's redirect for a number it does not have, to its "Shipment(s) Not Found" page. */
12
+ function missing(response, url) {
13
+ const location = response.headers.get('location');
14
+ if (!location)
15
+ return false;
16
+ try {
17
+ const target = new URL(location, url);
18
+ return target.hostname === 'track.mylabaih.com' && /^\/errors?\/404\/?$/.test(target.pathname);
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ export class LabaihTracker {
25
+ options;
26
+ constructor(options = {}) {
27
+ this.options = options;
28
+ }
29
+ async fetch(raw, context = {}) {
30
+ const number = normalizeLabaihNumber(raw);
31
+ const url = `${LABAIH_TRACKING}${number}`;
32
+ const read = async (signal, timeoutMs) => {
33
+ const { response, bytes } = await fetchBounded(url, { signal, headers: {
34
+ Accept: 'text/html', 'User-Agent': userAgentOf(this.options.userAgent),
35
+ } }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: LABAIH_MAX_BYTES, redirect: 'manual',
36
+ fetcher: this.options.fetcher, allowHttpStatuses: [...REDIRECTS, 404, 410] });
37
+ if (REDIRECTS.includes(response.status) && missing(response, url))
38
+ throw new NotFoundError(PROVIDER);
39
+ // Any other redirect, or a 404, is a moved page.
40
+ if (response.status !== 200)
41
+ throw new TransportError(PROVIDER, 'LaBaih tracking page moved', { status: response.status });
42
+ return parseLabaih(decodeText(bytes), number);
43
+ };
44
+ // A read that fails to reach the site, or hangs, gets one more: the first gets half of the budget.
45
+ return runSteps({ carrier: 'labaih', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
46
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [
47
+ { id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
48
+ networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
49
+ ]);
50
+ }
51
+ }
52
+ export const adapter = (environment) => {
53
+ const tracker = new LabaihTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
54
+ return {
55
+ id: 'labaih', recordsSteps: true, steps: ['direct', 'retry'],
56
+ track: (input, context) => tracker.fetch(input.number, context),
57
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeLabaihNumber(number))),
58
+ };
59
+ };
@@ -17,25 +17,65 @@
17
17
  "timezone": "Asia/Riyadh",
18
18
  "tracking": {
19
19
  "mode": "automatic",
20
- "adapter": "universal",
21
- "steps": []
20
+ "adapter": "labaih",
21
+ "steps": [
22
+ "direct",
23
+ "retry"
24
+ ],
25
+ "recognition": {
26
+ "rank": 788
27
+ },
28
+ "localClocks": true
22
29
  },
23
- "capabilities": [],
30
+ "capabilities": [
31
+ "history",
32
+ "location"
33
+ ],
24
34
  "portal": {
25
35
  "url": "https://track.mylabaih.com/track/?cn={trackingNumber}",
26
36
  "searchUrl": "https://mylabaih.com/",
27
37
  "canaryUrl": "https://mylabaih.com/",
28
- "shows": [],
29
- "scraped": [],
30
- "discarded": [],
31
- "unavailable": []
38
+ "shows": [
39
+ "status",
40
+ "progress_rail",
41
+ "history",
42
+ "location",
43
+ "sender_name",
44
+ "order_contents",
45
+ "customer_reference",
46
+ "recipient_address"
47
+ ],
48
+ "scraped": [
49
+ "status",
50
+ "history",
51
+ "location"
52
+ ],
53
+ "discarded": [
54
+ "progress_rail",
55
+ "sender_name",
56
+ "order_contents",
57
+ "customer_reference",
58
+ "recipient_address"
59
+ ],
60
+ "unavailable": [
61
+ "eta",
62
+ "weight",
63
+ "service_name",
64
+ "provider_code",
65
+ "recipient_name",
66
+ "proof_of_delivery",
67
+ "delivered_at",
68
+ "offset_timestamps",
69
+ "delivery_partner",
70
+ "delivery_tracking_number"
71
+ ]
32
72
  },
33
73
  "links": [
34
74
  {
35
75
  "domains": [
36
76
  "track.mylabaih.com"
37
77
  ],
38
- "pathPattern": "^/track/?$",
78
+ "pathPattern": "^/track(?:/en)?/?$",
39
79
  "params": [
40
80
  "cn"
41
81
  ]
@@ -0,0 +1,23 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "LaBaih";
3
+ export declare const LABAIH_MAX_BYTES = 1000000;
4
+ /** A LaBaih number: fourteen digits, which begin with the date and time it was created. */
5
+ export declare function normalizeLabaihNumber(raw: string): string;
6
+ /** A challenge or block page in place of the tracker. */
7
+ export declare function assertNotChallenged(html: string): void;
8
+ /**
9
+ * A scan's wording. A failed attempt's reason follows a colon and can go on,
10
+ * after another colon, with the recipient's district or city as given:
11
+ * "Unsuccessful Delivery: Wrong Customer District: …". Only the reason is
12
+ * kept, and only when it is words; phone numbers are dropped.
13
+ */
14
+ export declare function labaihWording(text: string): string;
15
+ /**
16
+ * The tracker's English page for one number, `track.mylabaih.com/track/en/?cn=`,
17
+ * which the site renders on its server: a heading with the number and the shop's
18
+ * reference, the shop, a progress rail, then the history, newest first. Each
19
+ * scan has a wording, a wall clock without a zone and, for an arrival at a
20
+ * distribution centre, its city. An unknown number is a redirect the adapter
21
+ * reads before this. The shop, its reference and the rail are not kept.
22
+ */
23
+ export declare function parseLabaih(html: string, raw: string): CarrierResult;
@@ -0,0 +1,127 @@
1
+ import { load } from 'cheerio';
2
+ import { normalizeTrackingNumber } from '../../core/detection/index.js';
3
+ import { ChallengeError, IndeterminateError, InvalidInputError, NoHistoryError, SchemaError } from '../../core/errors/index.js';
4
+ import { languageStageStatus } from '../../core/status/index.js';
5
+ import { calendarDay } from '../../core/time/index.js';
6
+ import { clean } from '../../core/transport/index.js';
7
+ import { labaihStage } from './status.js';
8
+ export const PROVIDER = 'LaBaih';
9
+ // A page is about 110 kB, most of it inline icons.
10
+ export const LABAIH_MAX_BYTES = 1_000_000;
11
+ const NUMBER = /^\d{14}$/;
12
+ // The heading over the shipment: the number, then the shop's own reference.
13
+ const TITLE = /^Shipment Tracking - (\d{14})(?:\s+-.*)?$/;
14
+ // A scan's wall clock, without a zone: "2026-01-02 13:45:06".
15
+ const CLOCK = /^(\d{4})-(\d{2})-(\d{2}) ([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$/;
16
+ // Bot checks a CDN or firewall could put in place of the page.
17
+ const CHALLENGE = /_cf_chl_opt|cf-turnstile|\/cdn-cgi\/challenge-platform\/|<title>\s*(?:Just a moment|Attention Required!|Access Denied)/i;
18
+ // A run of nine or more digits, with spaces, brackets, dots or hyphens between them, as phone numbers are written.
19
+ const PHONE = /(?<![A-Z\d])\+?\d(?:[\s().-]{0,2}\d){8,}(?![A-Z\d])/gi;
20
+ // A failed attempt's reason as the page words it, from the courier's list: letters only.
21
+ const REASON = /^[\p{L}][\p{L}\s'’/&().,-]{0,79}$/u;
22
+ const MAX_ROWS = 300;
23
+ /** A LaBaih number: fourteen digits, which begin with the date and time it was created. */
24
+ export function normalizeLabaihNumber(raw) {
25
+ const number = raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
26
+ if (!NUMBER.test(number))
27
+ throw new InvalidInputError(PROVIDER, 'LaBaih numbers are fourteen digits');
28
+ return number;
29
+ }
30
+ /** A challenge or block page in place of the tracker. */
31
+ export function assertNotChallenged(html) {
32
+ if (CHALLENGE.test(html.slice(0, 64_000)))
33
+ throw new ChallengeError(PROVIDER, 'LaBaih answered with a browser check');
34
+ }
35
+ /**
36
+ * A scan's wording. A failed attempt's reason follows a colon and can go on,
37
+ * after another colon, with the recipient's district or city as given:
38
+ * "Unsuccessful Delivery: Wrong Customer District: …". Only the reason is
39
+ * kept, and only when it is words; phone numbers are dropped.
40
+ */
41
+ export function labaihWording(text) {
42
+ const [head = '', reason = ''] = clean(text, 500).split(':').map((part) => clean(part, 500));
43
+ const label = clean(head.replace(PHONE, ''), 160).replace(/[\s,;\-–]+$/, '');
44
+ return REASON.test(reason) ? `${label}: ${reason}` : label;
45
+ }
46
+ /** A scan's wall clock in `local_time` form, checked against the calendar. */
47
+ function wallClock(text) {
48
+ const match = CLOCK.exec(text);
49
+ const day = match ? calendarDay(Number(match[1]), Number(match[2]), Number(match[3])) : null;
50
+ if (!match || !day || Number(match[1]) < 2015)
51
+ throw new SchemaError(PROVIDER, 'LaBaih returned an invalid scan time');
52
+ return `${day}T${match[4]}:${match[5]}:${match[6]}`;
53
+ }
54
+ /**
55
+ * The tracker's English page for one number, `track.mylabaih.com/track/en/?cn=`,
56
+ * which the site renders on its server: a heading with the number and the shop's
57
+ * reference, the shop, a progress rail, then the history, newest first. Each
58
+ * scan has a wording, a wall clock without a zone and, for an arrival at a
59
+ * distribution centre, its city. An unknown number is a redirect the adapter
60
+ * reads before this. The shop, its reference and the rail are not kept.
61
+ */
62
+ export function parseLabaih(html, raw) {
63
+ const number = normalizeLabaihNumber(raw);
64
+ assertNotChallenged(html);
65
+ const $ = load(html);
66
+ const titles = $('.shipper-details h1');
67
+ if (titles.length === 0)
68
+ throw new SchemaError(PROVIDER, 'LaBaih returned a page without a shipment');
69
+ if (titles.length > 1)
70
+ throw new SchemaError(PROVIDER, 'LaBaih returned several shipments');
71
+ const shown = TITLE.exec(clean(titles.text(), 200))?.[1];
72
+ if (!shown)
73
+ throw new SchemaError(PROVIDER, 'LaBaih returned a shipment without its number');
74
+ if (shown !== number)
75
+ throw new IndeterminateError(PROVIDER, 'LaBaih answered for another shipment');
76
+ const lists = $('.shipment-details .collapsible');
77
+ if (lists.length !== 1)
78
+ throw new SchemaError(PROVIDER, 'LaBaih returned a page without its history');
79
+ const rows = lists.find('.list-update').toArray();
80
+ if (rows.length > MAX_ROWS)
81
+ throw new SchemaError(PROVIDER, 'LaBaih returned an oversized history');
82
+ const seen = new Set();
83
+ const scans = [];
84
+ rows.forEach((row, index) => {
85
+ const headings = $(row).find('h3');
86
+ const metas = $(row).find('.up-meta');
87
+ if (headings.length !== 1 || metas.length !== 1)
88
+ throw new SchemaError(PROVIDER, 'LaBaih returned an invalid scan');
89
+ const description = labaihWording(headings.text());
90
+ if (!description)
91
+ throw new SchemaError(PROVIDER, 'LaBaih returned a scan without its wording');
92
+ const location = clean(metas.find('strong').text(), 80);
93
+ const meta = metas.clone();
94
+ meta.find('strong').remove();
95
+ const clock = wallClock(clean(meta.text(), 60));
96
+ const staged = labaihStage(description);
97
+ const event = {
98
+ local_time: clock,
99
+ ...(location ? { location } : {}),
100
+ description,
101
+ ...(staged ? { stage: staged.stage, stage_source: staged.source } : {}),
102
+ };
103
+ // A scan the page lists twice is kept once.
104
+ const identity = JSON.stringify(event);
105
+ if (seen.has(identity))
106
+ return;
107
+ seen.add(identity);
108
+ scans.push({ event, clock, index });
109
+ });
110
+ if (!scans.length)
111
+ throw new NoHistoryError(PROVIDER);
112
+ // Newest first; scans at the same second keep the page's order.
113
+ const events = scans.sort((left, right) => (left.clock < right.clock ? 1 : left.clock > right.clock ? -1 : left.index - right.index))
114
+ .map(({ event }) => event);
115
+ const newest = events[0];
116
+ const current = events.find((event) => event.stage);
117
+ const stage = current?.stage;
118
+ return {
119
+ status: stage ? languageStageStatus(stage) : 'unknown',
120
+ ...(stage ? { current_stage: stage, current_stage_source: current.stage_source } : {}),
121
+ last_status_text: newest.description,
122
+ last_update: null,
123
+ last_update_local: newest.local_time,
124
+ expected_delivery: null,
125
+ events,
126
+ };
127
+ }
@@ -0,0 +1,8 @@
1
+ import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
2
+ import { type CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The stage LaBaih's wordings give a scan, in `normalizeStatusWording` form or as shown. */
4
+ export declare function labaihMappedStage(wording: string): Stage | undefined;
5
+ /** A scan's stage: LaBaih's wordings, else the shared wording rules, else nothing. */
6
+ export declare function labaihStage(wording: string): ClassifiedWording | null;
7
+ /** What the map says about one scan, by its wording: the tracker shows no codes. */
8
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,54 @@
1
+ import { classifyWording } from '../../core/status/index.js';
2
+ import { normalizeStatusWording } from '../../core/status/statusMap.js';
3
+ const key = normalizeStatusWording;
4
+ /**
5
+ * LaBaih's scan wordings, in English as the adapter reads them and in Arabic
6
+ * as the default page shows them. The tracker shows no codes. A parcel is
7
+ * collected from the online shop that sends it, arrives at a city's
8
+ * distribution centre and goes out to the customer from there.
9
+ */
10
+ const WORDINGS = new Map([
11
+ [key('Shipment created'), 'registered'],
12
+ [key('تم إنشاء الشحنة'), 'registered'],
13
+ [key('Shipment Collected From Partner'), 'accepted'],
14
+ [key('تم استلام الشحنة من الشريك'), 'accepted'],
15
+ // Back at the distribution centre after an attempt; the Arabic page words it as an arrival there.
16
+ [key('Returned To Facility'), 'in_transit'],
17
+ [key('وصلت الشحنة إلى مركز التوزيع'), 'in_transit'],
18
+ [key('In-Transit to Customer'), 'out_for_delivery'],
19
+ [key('الشحنة في طريقها إلى العميل'), 'out_for_delivery'],
20
+ [key('Unsuccessful Delivery'), 'failed_attempt'],
21
+ [key('محاولة توصيل غير ناجحة'), 'failed_attempt'],
22
+ [key('Shipment Delivered'), 'delivered'],
23
+ [key('تم توصيل الشحنة'), 'delivered'],
24
+ ]);
25
+ // An arrival at a city's distribution centre: "Arrived at Riyadh Facility", or in Arabic with "بـ" and the city.
26
+ const ARRIVED = /^(?:arrived at \S.* facility|وصلت الشحنة إلى مركز التوزيع بـ\S.*)$/u;
27
+ // A failed attempt keeps the reason the page gives after a colon: "Unsuccessful Delivery: Waiting time exceed".
28
+ const FAILED = /^(?:unsuccessful delivery|محاولة توصيل غير ناجحة):/u;
29
+ // Messages about the delivery location, which move nothing.
30
+ const NOTICES = [
31
+ 'SMS sent requesting location to customer',
32
+ 'Customer sent location',
33
+ 'العميل قام بإرسال إحداثيات التوصيل',
34
+ ].map(key);
35
+ /** The stage LaBaih's wordings give a scan, in `normalizeStatusWording` form or as shown. */
36
+ export function labaihMappedStage(wording) {
37
+ const normalized = key(wording);
38
+ return WORDINGS.get(normalized) ?? (ARRIVED.test(normalized) ? 'in_transit' : FAILED.test(normalized) ? 'failed_attempt' : undefined);
39
+ }
40
+ /** A scan's stage: LaBaih's wordings, else the shared wording rules, else nothing. */
41
+ export function labaihStage(wording) {
42
+ const mapped = labaihMappedStage(wording);
43
+ if (mapped)
44
+ return { stage: mapped, source: 'carrier_map' };
45
+ if (NOTICES.includes(key(wording)))
46
+ return null;
47
+ const worded = classifyWording(wording, 'in_transit');
48
+ return worded.source === 'none' ? null : worded;
49
+ }
50
+ /** What the map says about one scan, by its wording: the tracker shows no codes. */
51
+ export const statusMap = {
52
+ stage: (code, wording) => (code ? undefined : labaihMappedStage(wording)),
53
+ gaps: NOTICES.map((wording) => ({ wording, note: 'A message asking for, or giving, the delivery location; the parcel does not move.' })),
54
+ };
@@ -4,11 +4,23 @@
4
4
  {"wording": "تم إنشاء الشحنة", "stage": "registered", "language": "ar", "confirmedBy": "portal", "note": "Shipment created; the first event of every captured history."},
5
5
  {"wording": "تم استلام الشحنة من الشريك", "stage": "accepted", "language": "ar", "confirmedBy": "portal", "note": "Shipment collected from the partner, the online shop that sends it."},
6
6
  {"wording": "العميل قام بإرسال إحداثيات التوصيل", "language": "ar", "confirmedBy": "portal", "note": "The recipient sent the delivery location; not a parcel movement."},
7
- {"wording": "وصلت الشحنة إلى مركز التوزيع", "stage": "in_transit", "language": "ar", "confirmedBy": "portal", "note": "Shipment arrived at the distribution centre."},
7
+ {"wording": "وصلت الشحنة إلى مركز التوزيع", "stage": "in_transit", "language": "ar", "confirmedBy": "portal", "note": "Shipment arrived at the distribution centre. The Arabic page also words a return to the facility after an attempt this way."},
8
8
  {"wording": "وصلت الشحنة إلى مركز التوزيع بـالرياض", "stage": "in_transit", "language": "ar", "confirmedBy": "portal", "note": "The same event with the city appended; Jeddah appears the same way."},
9
9
  {"wording": "الشحنة في طريقها إلى العميل", "stage": "out_for_delivery", "language": "ar", "confirmedBy": "portal", "note": "Shipment on its way to the customer."},
10
10
  {"wording": "محاولة توصيل غير ناجحة", "stage": "failed_attempt", "language": "ar", "confirmedBy": "portal", "note": "Unsuccessful delivery attempt."},
11
+ {"wording": "تم توصيل الشحنة", "stage": "delivered", "language": "ar", "confirmedBy": "live", "note": "Shipment delivered."},
11
12
  {"wording": "تم استلام الشحنة", "stage": "accepted", "language": "ar", "confirmedBy": "portal", "note": "Step label in the tracker's progress bar."},
12
- {"wording": "تم التوصيل", "stage": "delivered", "language": "ar", "confirmedBy": "portal", "note": "Step label in the tracker's progress bar."}
13
+ {"wording": "تم التوصيل", "stage": "delivered", "language": "ar", "confirmedBy": "portal", "note": "Step label in the tracker's progress bar."},
14
+ {"wording": "Shipment created", "stage": "registered", "language": "en", "confirmedBy": "live", "note": "The first event of every history."},
15
+ {"wording": "Shipment Collected From Partner", "stage": "accepted", "language": "en", "confirmedBy": "live", "note": "Collected from the online shop that sends the parcel."},
16
+ {"wording": "SMS sent requesting location to customer", "language": "en", "confirmedBy": "live", "note": "A text asking the recipient for the delivery location; only the English page lists it, and the parcel does not move."},
17
+ {"wording": "Customer sent location", "language": "en", "confirmedBy": "live", "note": "The recipient sent the delivery location; the parcel does not move."},
18
+ {"wording": "Arrived at Riyadh Facility", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Arrival at a city's distribution centre, whose city the scan also gives as its place; Jeddah appears the same way."},
19
+ {"wording": "In-Transit to Customer", "stage": "out_for_delivery", "language": "en", "confirmedBy": "live", "note": "On its way to the customer."},
20
+ {"wording": "Unsuccessful Delivery: Waiting time exceed", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "A failed attempt with the courier's reason after the colon."},
21
+ {"wording": "Unsuccessful Delivery: Wrong Customer District", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "The page adds the district as the recipient gave it after another colon; the adapter drops it."},
22
+ {"wording": "Unsuccessful Delivery: Wrong Customer City", "stage": "failed_attempt", "language": "en", "confirmedBy": "live", "note": "The page adds the city as the recipient gave it after another colon; the adapter drops it."},
23
+ {"wording": "Returned To Facility", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Back at the distribution centre after a failed attempt, before going out again; the Arabic page words it as an arrival there."},
24
+ {"wording": "Shipment Delivered", "stage": "delivered", "language": "en", "confirmedBy": "live", "note": "Delivered."}
13
25
  ]
14
26
  }
@@ -0,0 +1,15 @@
1
+ import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
2
+ import type { CarrierResult } from '../../core/result/index.js';
3
+ import { type StepRecorder } from '../../core/telemetry/index.js';
4
+ /** The API the tracking page on go.shipblu.com looks the number in its address up in. */
5
+ export declare const SHIPBLU_API = "https://api.shipblu.com/api/v1/";
6
+ export declare class ShipbluTracker {
7
+ private readonly options;
8
+ constructor(options?: {
9
+ fetcher?: typeof fetch;
10
+ recorder?: StepRecorder;
11
+ userAgent?: string;
12
+ });
13
+ fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
14
+ }
15
+ export declare const adapter: AdapterFactory;
@@ -0,0 +1,76 @@
1
+ import { accepted, recognizeFromLookup } from '../../core/adapter/index.js';
2
+ import { ChallengeError, NotFoundError, SchemaError, TransportError } from '../../core/errors/index.js';
3
+ import { runSteps } from '../../core/runner/index.js';
4
+ import { firstAttemptMs, networkRetryStep } from '../../core/runner/networkRetry.js';
5
+ import { NOOP_RECORDER } from '../../core/telemetry/index.js';
6
+ import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
7
+ import { isRecord } from '../../core/types.js';
8
+ import { normalizeShipbluNumber, parseShipblu, PROVIDER, SHIPBLU_MAX_BYTES, shipbluOrderType } from './parser.js';
9
+ /** The API the tracking page on go.shipblu.com looks the number in its address up in. */
10
+ export const SHIPBLU_API = 'https://api.shipblu.com/api/v1/';
11
+ const REDIRECTS = [301, 302, 303, 307, 308];
12
+ // Bot checks a CDN or firewall could put in place of the reply.
13
+ const CHALLENGE = /_cf_chl_opt|cf-turnstile|challenge-platform|<title>\s*(?:Just a moment|Attention Required)|AwsWafIntegration|awswaf\.com/i;
14
+ function json(body) {
15
+ try {
16
+ return JSON.parse(body);
17
+ }
18
+ catch (cause) {
19
+ if (CHALLENGE.test(body.slice(0, 64_000)))
20
+ throw new ChallengeError(PROVIDER, 'ShipBlu answered with a browser check', { cause });
21
+ throw new SchemaError(PROVIDER, 'ShipBlu returned a reply that is not JSON', { cause });
22
+ }
23
+ }
24
+ /** The service's own answer to a number it does not know: `{"detail":"Not found."}`. */
25
+ function unknownNumber(body) {
26
+ try {
27
+ const reply = JSON.parse(body);
28
+ return isRecord(reply) && typeof reply.detail === 'string' && /^not found\.?$/i.test(reply.detail.trim());
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ export class ShipbluTracker {
35
+ options;
36
+ constructor(options = {}) {
37
+ this.options = options;
38
+ }
39
+ async fetch(raw, context = {}) {
40
+ const number = normalizeShipbluNumber(raw);
41
+ const url = `${SHIPBLU_API}${shipbluOrderType(number)}/${number}/?pin=`;
42
+ const read = async (signal, timeoutMs) => {
43
+ // The service words its errors in the language asked for; English keeps the not-found answer readable.
44
+ const { response, bytes } = await fetchBounded(url, { method: 'GET', signal, headers: {
45
+ Accept: 'application/json', 'Accept-Language': 'en', 'User-Agent': userAgentOf(this.options.userAgent),
46
+ } }, { provider: PROVIDER, timeoutMs: Math.max(1, Math.floor(timeoutMs)), maxBytes: SHIPBLU_MAX_BYTES, redirect: 'manual',
47
+ fetcher: this.options.fetcher, allowHttpStatuses: [...REDIRECTS, 404, 410] });
48
+ if (REDIRECTS.includes(response.status))
49
+ throw new TransportError(PROVIDER, 'ShipBlu redirected its tracking lookup');
50
+ const body = decodeText(bytes);
51
+ if (response.status === 404 && unknownNumber(body))
52
+ throw new NotFoundError(PROVIDER);
53
+ if (response.status === 404 || response.status === 410) {
54
+ if (CHALLENGE.test(body.slice(0, 64_000)))
55
+ throw new ChallengeError(PROVIDER, 'ShipBlu answered with a browser check', { status: response.status });
56
+ throw new TransportError(PROVIDER, 'ShipBlu tracking lookup is unavailable', { status: response.status });
57
+ }
58
+ return parseShipblu(json(body), number);
59
+ };
60
+ // A request that fails to reach the service, or hangs, gets one more try:
61
+ // the first gets half of the budget, so the retry fits in the rest.
62
+ return runSteps({ carrier: 'shipblu', budgetMs: context.budgetMs ?? 15_000, signal: context.signal,
63
+ recorder: this.options.recorder ?? NOOP_RECORDER }, [
64
+ { id: 'direct', run: ({ signal, remainingMs }) => read(signal, firstAttemptMs(remainingMs)) },
65
+ networkRetryStep(({ signal, remainingMs }) => read(signal, remainingMs)),
66
+ ]);
67
+ }
68
+ }
69
+ export const adapter = (environment) => {
70
+ const tracker = new ShipbluTracker({ fetcher: environment.fetcher, recorder: environment.recorder, userAgent: environment.userAgent });
71
+ return {
72
+ id: 'shipblu', recordsSteps: true, steps: ['direct', 'retry'],
73
+ track: (input, context) => tracker.fetch(input.number, context),
74
+ recognize: (number, context) => recognizeFromLookup(() => tracker.fetch(number, context), () => accepted(() => normalizeShipbluNumber(number))),
75
+ };
76
+ };
@@ -17,18 +17,69 @@
17
17
  "timezone": "Africa/Cairo",
18
18
  "tracking": {
19
19
  "mode": "automatic",
20
- "adapter": "universal",
21
- "steps": []
20
+ "adapter": "shipblu",
21
+ "steps": [
22
+ "direct",
23
+ "retry"
24
+ ],
25
+ "recognition": {
26
+ "rank": 789
27
+ }
22
28
  },
23
- "capabilities": [],
29
+ "capabilities": [
30
+ "history",
31
+ "provider_code",
32
+ "offset_timestamps",
33
+ "eta",
34
+ "delivered_at"
35
+ ],
24
36
  "portal": {
25
37
  "url": "https://go.shipblu.com/en/{trackingNumber}",
26
38
  "searchUrl": "https://go.shipblu.com/en/",
27
39
  "canaryUrl": "https://go.shipblu.com/en/",
28
- "shows": [],
29
- "scraped": [],
30
- "discarded": [],
31
- "unavailable": []
40
+ "shows": [
41
+ "status",
42
+ "progress_rail",
43
+ "eta",
44
+ "delivered_at",
45
+ "failed_delivery_reason",
46
+ "sender_name",
47
+ "recipient_address",
48
+ "parcel_count",
49
+ "qr",
50
+ "payment_details",
51
+ "delivery_options"
52
+ ],
53
+ "scraped": [
54
+ "status",
55
+ "history",
56
+ "provider_code",
57
+ "offset_timestamps",
58
+ "eta",
59
+ "delivered_at",
60
+ "failed_delivery_reason"
61
+ ],
62
+ "discarded": [
63
+ "sender_name",
64
+ "recipient_name",
65
+ "recipient_address",
66
+ "contact_details",
67
+ "coordinates",
68
+ "parcel_count",
69
+ "qr",
70
+ "payment_details",
71
+ "delivery_options",
72
+ "remarks"
73
+ ],
74
+ "unavailable": [
75
+ "location",
76
+ "weight",
77
+ "service_name",
78
+ "proof_of_delivery",
79
+ "destination_country",
80
+ "delivery_partner",
81
+ "delivery_tracking_number"
82
+ ]
32
83
  },
33
84
  "links": [
34
85
  {
@@ -0,0 +1,19 @@
1
+ import type { CarrierResult } from '../../core/result/index.js';
2
+ export declare const PROVIDER = "ShipBlu";
3
+ export declare const SHIPBLU_MAX_BYTES: number;
4
+ export type ShipbluOrderType = 'delivery-order' | 'return';
5
+ /**
6
+ * The tracker's list for a number, by its leading digits as the page routes
7
+ * it: 1 and 31 are delivery orders, 2 and 32 returns.
8
+ */
9
+ export declare function shipbluOrderType(number: string): ShipbluOrderType | undefined;
10
+ /** A ShipBlu number for a parcel, as its digits; anything else is not one ShipBlu issues for a parcel. */
11
+ export declare function normalizeShipbluNumber(raw: string): string;
12
+ /**
13
+ * The reply of the tracking page's `GET /api/v1/{delivery-order|return}/{number}/`
14
+ * for one number. An unknown number is HTTP 404, which the adapter reads before
15
+ * this. The reply must name the number asked. It also carries the merchant, the
16
+ * recipient's name, phones and address and the cash to collect, which are never
17
+ * read.
18
+ */
19
+ export declare function parseShipblu(payload: unknown, raw: string): CarrierResult;
@@ -0,0 +1,174 @@
1
+ import { DateTime } from 'luxon';
2
+ import { normalizeTrackingNumber } from '../../core/detection/index.js';
3
+ import { IndeterminateError, InvalidInputError, NoHistoryError, SchemaError } from '../../core/errors/index.js';
4
+ import { languageStageStatus } from '../../core/status/index.js';
5
+ import { explicitOffsetTime } from '../../core/time/index.js';
6
+ import { clean } from '../../core/transport/index.js';
7
+ import { isRecord } from '../../core/types.js';
8
+ import { shipbluMappedStage, shipbluStage, shipbluWording } from './status.js';
9
+ export const PROVIDER = 'ShipBlu';
10
+ // An order with its recipient and a long history is a few kilobytes.
11
+ export const SHIPBLU_MAX_BYTES = 256 * 1024;
12
+ // The site's tracking box takes seven or more digits starting with 1 to 4.
13
+ const NUMBER = /^[1-4]\d{6,39}$/;
14
+ const CODE = /^[a-z][a-z_]{0,39}$/;
15
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
16
+ // An instant as the service writes it: up to microseconds, and an offset.
17
+ const INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:\d{2})$/;
18
+ // The scans whose comment the page shows: the reason a courier gives for a missed visit.
19
+ const ATTEMPTS = new Set(['delivery_attempted', 'return_attempted']);
20
+ // Before pickup the page promises no day: "To be determined after picking up your order".
21
+ const BEFORE_PICKUP = new Set(['created', 'pickup_requested', 'pickup_rescheduled']);
22
+ // The statuses the page's timeline places on a step of their own; on any
23
+ // other, an order on hold shows as such.
24
+ const OWN_STEP = new Set(['created', 'pickup_requested', 'pickup_rescheduled', 'out_for_pickup', 'return_requested',
25
+ 'picked_up', 'cancelled', 'out_for_delivery', 'out_for_return', 'delivered', 'returned']);
26
+ const FINAL = new Set(['delivered', 'returned', 'exception']);
27
+ // A run of eight or more digits, with spaces, brackets, dots or hyphens
28
+ // between them, as phone numbers are written; a date stays.
29
+ const PHONE = /(?<![A-Z\d])(?!\d{4}-\d{2}-\d{2}(?!\d))\+?\d(?:[\s().-]{0,2}\d){7,}(?![A-Z\d])/gi;
30
+ const MAX_ROWS = 500;
31
+ const MAX_EVENTS = 100;
32
+ /**
33
+ * The tracker's list for a number, by its leading digits as the page routes
34
+ * it: 1 and 31 are delivery orders, 2 and 32 returns.
35
+ */
36
+ export function shipbluOrderType(number) {
37
+ if (/^(?:1|31)/.test(number))
38
+ return 'delivery-order';
39
+ if (/^(?:2|32)/.test(number))
40
+ return 'return';
41
+ return undefined;
42
+ }
43
+ /** A ShipBlu number for a parcel, as its digits; anything else is not one ShipBlu issues for a parcel. */
44
+ export function normalizeShipbluNumber(raw) {
45
+ const number = raw.length <= 64 ? normalizeTrackingNumber(raw) : '';
46
+ if (!NUMBER.test(number))
47
+ throw new InvalidInputError(PROVIDER, 'ShipBlu numbers are seven or more digits starting with 1 to 4');
48
+ // The page looks numbers starting with 4 up as cash collections, which carry no parcel,
49
+ // and has no list for one starting with 3 then another digit than 1 or 2.
50
+ if (!shipbluOrderType(number))
51
+ throw new InvalidInputError(PROVIDER, 'ShipBlu tracks parcels under numbers starting with 1, 2, 31 or 32');
52
+ return number;
53
+ }
54
+ /** An instant the reply states with its offset. */
55
+ function instant(value) {
56
+ return typeof value === 'string' && INSTANT.test(value.trim()) ? explicitOffsetTime(value) : null;
57
+ }
58
+ /** A calendar day, or nothing. */
59
+ function day(value) {
60
+ if (typeof value !== 'string' || !DAY.test(value.trim()))
61
+ return null;
62
+ return DateTime.fromISO(value.trim(), { zone: 'UTC' }).isValid ? value.trim() : null;
63
+ }
64
+ /** A status code, in the lower case the service writes. */
65
+ function statusCode(row) {
66
+ const code = typeof row.status === 'string' ? row.status.trim().toLowerCase() : '';
67
+ if (!CODE.test(code))
68
+ throw new SchemaError(PROVIDER, 'ShipBlu returned an invalid status');
69
+ return code;
70
+ }
71
+ /** A missed visit's reason, without phone numbers. */
72
+ function reason(row) {
73
+ const value = row.comment;
74
+ if (value === null || value === undefined)
75
+ return '';
76
+ if (typeof value !== 'string')
77
+ throw new SchemaError(PROVIDER, 'ShipBlu returned an invalid comment');
78
+ return clean(value.replace(PHONE, ''), 200).replace(/^[\s,;:.\-–]+|[\s,;:\-–]+$/g, '');
79
+ }
80
+ function projectRow(row) {
81
+ const code = statusCode(row);
82
+ const time = instant(row.created);
83
+ if (!time)
84
+ throw new SchemaError(PROVIDER, 'ShipBlu returned an invalid scan time');
85
+ const why = ATTEMPTS.has(code) ? reason(row) : '';
86
+ const description = why ? `${shipbluWording(code)}: ${why}` : shipbluWording(code);
87
+ const staged = shipbluStage(code);
88
+ const event = {
89
+ time: time.iso,
90
+ description,
91
+ provider_code: code,
92
+ ...(staged ? { stage: staged.stage, stage_source: staged.source } : {}),
93
+ };
94
+ return { key: JSON.stringify([time.timestamp, code, why]), scan: { event, timestamp: time.timestamp } };
95
+ }
96
+ /**
97
+ * The day the page promises: none before pickup or once the order ended, else
98
+ * the day the customer chose, else for a return the service's estimate. The
99
+ * page asks another endpoint for a delivery order's estimate, which this
100
+ * does not read.
101
+ */
102
+ function estimate(payload, type, current, stage) {
103
+ if (BEFORE_PICKUP.has(current) || (stage && FINAL.has(stage)))
104
+ return null;
105
+ return day(payload.preferred_date) ?? (type === 'return' ? day(payload.estimated_date) : null);
106
+ }
107
+ /**
108
+ * The reply of the tracking page's `GET /api/v1/{delivery-order|return}/{number}/`
109
+ * for one number. An unknown number is HTTP 404, which the adapter reads before
110
+ * this. The reply must name the number asked. It also carries the merchant, the
111
+ * recipient's name, phones and address and the cash to collect, which are never
112
+ * read.
113
+ */
114
+ export function parseShipblu(payload, raw) {
115
+ const number = normalizeShipbluNumber(raw);
116
+ const type = shipbluOrderType(number);
117
+ if (!isRecord(payload))
118
+ throw new SchemaError(PROVIDER, 'ShipBlu returned an invalid reply');
119
+ if (typeof payload.tracking_number !== 'string')
120
+ throw new SchemaError(PROVIDER, 'ShipBlu returned an order without its number');
121
+ if (payload.tracking_number.trim() !== number)
122
+ throw new IndeterminateError(PROVIDER, 'ShipBlu answered for another order');
123
+ const current = statusCode(payload);
124
+ const rows = payload.tracking_events ?? [];
125
+ if (!Array.isArray(rows))
126
+ throw new SchemaError(PROVIDER, 'ShipBlu returned an order with invalid scans');
127
+ if (rows.length > MAX_ROWS)
128
+ throw new SchemaError(PROVIDER, 'ShipBlu returned an oversized history');
129
+ const seen = new Set();
130
+ const scans = [];
131
+ for (const row of rows) {
132
+ if (!isRecord(row))
133
+ throw new SchemaError(PROVIDER, 'ShipBlu returned an invalid scan');
134
+ const { key, scan } = projectRow(row);
135
+ if (seen.has(key))
136
+ continue;
137
+ seen.add(key);
138
+ scans.push(scan);
139
+ }
140
+ // Newest first; scans at the same instant keep the reverse of the order they were recorded in.
141
+ scans.reverse().sort((a, b) => b.timestamp - a.timestamp);
142
+ const events = scans.map((scan) => scan.event);
143
+ // The page shows an order held for the merchant on the step it waits at.
144
+ const onHold = payload.on_hold !== null && payload.on_hold !== undefined && !OWN_STEP.has(current);
145
+ const summary = onHold ? 'exception' : shipbluMappedStage(current);
146
+ const scanned = events.find((event) => event.stage);
147
+ const stage = summary ?? scanned?.stage;
148
+ const source = summary ? 'carrier_map' : scanned?.stage_source;
149
+ const delivered = stage === 'delivered'
150
+ ? instant(payload.delivered_date)?.iso ?? events.find((event) => event.provider_code === 'delivered')?.time : undefined;
151
+ const facts = {
152
+ expected_delivery: estimate(payload, type, current, stage),
153
+ ...(delivered ? { delivered_at: delivered } : {}),
154
+ };
155
+ if (!events.length) {
156
+ if (!summary)
157
+ throw new NoHistoryError(PROVIDER);
158
+ return {
159
+ status: languageStageStatus(summary), current_stage: summary, current_stage_source: 'carrier_map',
160
+ last_status_text: onHold ? 'On hold' : shipbluWording(current), provider_code: current,
161
+ last_update: delivered ?? null, ...facts, summary_only: true, events: [],
162
+ };
163
+ }
164
+ const newest = events[0];
165
+ return {
166
+ status: stage ? languageStageStatus(stage) : 'unknown',
167
+ ...(stage ? { current_stage: stage, current_stage_source: source ?? 'carrier_map' } : {}),
168
+ last_status_text: onHold ? 'On hold' : newest.description,
169
+ last_update: newest.time,
170
+ ...facts,
171
+ events: events.slice(0, MAX_EVENTS),
172
+ ...(events.length > MAX_EVENTS ? { history_truncated: true } : {}),
173
+ };
174
+ }
@@ -0,0 +1,9 @@
1
+ import { type ClassifiedWording, type Stage } from '../../core/status/index.js';
2
+ import type { CarrierStatusMap } from '../../core/status/statusMap.js';
3
+ /** The stage ShipBlu's map gives a status code. */
4
+ export declare function shipbluMappedStage(code: string): Stage | undefined;
5
+ /** The wording of a status code, as `out_for_delivery` reads "Out for delivery". */
6
+ export declare function shipbluWording(code: string): string;
7
+ /** A scan's stage: the map, else the shared wording rules for a code the map does not know, else nothing. */
8
+ export declare function shipbluStage(code: string): ClassifiedWording | null;
9
+ export declare const statusMap: CarrierStatusMap;
@@ -0,0 +1,52 @@
1
+ import { classifyWording } from '../../core/status/index.js';
2
+ /**
3
+ * ShipBlu's order statuses, as its API documentation lists them. A return
4
+ * order travels from the customer to the merchant, so its arrival there is
5
+ * `returned`, as on an order sent back.
6
+ */
7
+ const CODES = new Map([
8
+ ['created', 'registered'],
9
+ ['pickup_requested', 'registered'], // The merchant asked ShipBlu to collect the order.
10
+ ['pickup_rescheduled', 'registered'],
11
+ ['out_for_pickup', 'registered'], // A courier on the way to the merchant.
12
+ ['return_requested', 'registered'], // The merchant asked ShipBlu to collect a return from the customer.
13
+ ['picked_up', 'accepted'],
14
+ ['in_transit', 'in_transit'], // At a ShipBlu warehouse.
15
+ ['en_route', 'in_transit'], // Between two warehouses.
16
+ ['out_for_delivery', 'out_for_delivery'],
17
+ ['delivery_attempted', 'failed_attempt'],
18
+ ['delivered', 'delivered'],
19
+ ['return_to_origin', 'returned'], // Refused, badly packed or out of zone: it goes back to the merchant.
20
+ ['out_for_return', 'returned'], // A courier taking it to the merchant.
21
+ ['return_postponed', 'returned'], // The merchant moved the return to the next day.
22
+ ['returned', 'returned'],
23
+ ['return_refused', 'exception'], // The merchant refused to take it back.
24
+ ['cancelled', 'exception'], // Cancelled by the merchant before pickup.
25
+ ]);
26
+ /** Codes the map leaves without a stage on purpose. */
27
+ const GAPS = new Map([
28
+ ['return_attempted', 'A courier could not collect a return from the customer; the parcel is not in ShipBlu\'s hands yet.'],
29
+ ]);
30
+ /** The stage ShipBlu's map gives a status code. */
31
+ export function shipbluMappedStage(code) {
32
+ return CODES.get(code);
33
+ }
34
+ /** The wording of a status code, as `out_for_delivery` reads "Out for delivery". */
35
+ export function shipbluWording(code) {
36
+ const words = code.replace(/_+/g, ' ').trim();
37
+ return words.charAt(0).toUpperCase() + words.slice(1);
38
+ }
39
+ /** A scan's stage: the map, else the shared wording rules for a code the map does not know, else nothing. */
40
+ export function shipbluStage(code) {
41
+ const mapped = shipbluMappedStage(code);
42
+ if (mapped)
43
+ return { stage: mapped, source: 'carrier_map' };
44
+ if (GAPS.has(code))
45
+ return null;
46
+ const classified = classifyWording(shipbluWording(code), 'in_transit');
47
+ return classified.source === 'none' ? null : classified;
48
+ }
49
+ export const statusMap = {
50
+ stage: (code) => (code === null ? undefined : shipbluMappedStage(code)),
51
+ gaps: [...GAPS].map(([code, note]) => ({ code, note })),
52
+ };
@@ -1,26 +1,31 @@
1
1
  {
2
2
  "carrier": "shipblu",
3
3
  "entries": [
4
- {"code": "created", "wording": "Created", "stage": "registered", "language": "en", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
5
- {"wording": "Pickup Requested", "stage": "registered", "language": "en", "confirmedBy": "official-doc", "note": "From ShipBlu's help article on the order timeline."},
6
- {"wording": "Out for Pickup", "stage": "registered", "language": "en", "confirmedBy": "official-doc", "note": "From ShipBlu's help article on the order timeline. A courier is on the way to collect the order from the merchant."},
7
- {"wording": "Pickup Rescheduled", "stage": "registered", "language": "en", "confirmedBy": "official-doc", "note": "From ShipBlu's help article on the order timeline. The pickup moved to the next business day."},
8
- {"code": "picked_up", "wording": "Picked up", "stage": "accepted", "language": "en", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
9
- {"code": "picked_up", "wording": "تم استلام الطلب", "stage": "accepted", "language": "ar", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
10
- {"code": "in_transit", "wording": "En Route", "stage": "in_transit", "language": "en", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
11
- {"code": "in_transit", "wording": "في مركز التوزيع", "stage": "in_transit", "language": "ar", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
12
- {"wording": "In Transit", "stage": "in_transit", "language": "en", "confirmedBy": "official-doc", "note": "From ShipBlu's help article on the order timeline. The article groups it with En Route: the order is on its way to the hub serving the customer."},
13
- {"code": "out_for_delivery", "wording": "Out for Delivery", "stage": "out_for_delivery", "language": "en", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
14
- {"code": "out_for_delivery", "wording": "طلبك في طريقه إليك", "stage": "out_for_delivery", "language": "ar", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
15
- {"code": "delivery_attempted", "wording": "Delivery Attempted", "stage": "failed_attempt", "language": "en", "confirmedBy": "official-doc", "note": "From ShipBlu's help article on the order timeline."},
16
- {"code": "delivered", "wording": "Delivered", "stage": "delivered", "language": "en", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
17
- {"code": "delivered", "wording": "تم تسليم الطلب", "stage": "delivered", "language": "ar", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
18
- {"wording": "On Hold", "stage": "exception", "language": "en", "confirmedBy": "official-doc", "note": "From ShipBlu's help article on the order timeline. The merchant's balance does not cover the shipping fee."},
19
- {"wording": "Return to Origin", "stage": "returned", "language": "en", "confirmedBy": "official-doc", "note": "From ShipBlu's help article on the order timeline."},
20
- {"code": "out_for_return", "wording": "Out for Return", "stage": "returned", "language": "en", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
21
- {"code": "out_for_return", "wording": "طلبك فى طريقه للتاجر", "stage": "returned", "language": "ar", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history. The order is on its way back to the merchant."},
22
- {"code": "returned", "wording": "Returned", "stage": "returned", "language": "en", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
23
- {"code": "returned", "wording": "تم ارجاع الطلب", "stage": "returned", "language": "ar", "confirmedBy": "official-doc", "note": "Label in the go.shipblu.com tracker's script; not seen in a captured history."},
24
- {"wording": "Canceled", "stage": "exception", "language": "en", "confirmedBy": "official-doc", "note": "From ShipBlu's help article on the order timeline. The order was canceled before pickup."}
4
+ {"code": "created", "wording": "Created", "stage": "registered", "language": "en", "confirmedBy": "live", "note": "The merchant created the order."},
5
+ {"code": "pickup_requested", "wording": "Pickup requested", "stage": "registered", "language": "en", "confirmedBy": "live", "note": "The merchant asked ShipBlu to collect the order."},
6
+ {"code": "out_for_pickup", "wording": "Out for pickup", "stage": "registered", "language": "en", "confirmedBy": "live", "note": "A courier is on the way to collect the order from the merchant."},
7
+ {"code": "picked_up", "wording": "Picked up", "stage": "accepted", "language": "en", "confirmedBy": "live"},
8
+ {"code": "in_transit", "wording": "In transit", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "At a ShipBlu warehouse, as the API documentation defines the status; it alternates with en_route along the way."},
9
+ {"code": "en_route", "wording": "En route", "stage": "in_transit", "language": "en", "confirmedBy": "live", "note": "Moving from one warehouse to another, as the API documentation defines the status."},
10
+ {"code": "out_for_delivery", "wording": "Out for delivery", "stage": "out_for_delivery", "language": "en", "confirmedBy": "live"},
11
+ {"code": "delivered", "wording": "Delivered", "stage": "delivered", "language": "en", "confirmedBy": "live"},
12
+ {"code": "pickup_rescheduled", "wording": "Pickup rescheduled", "stage": "registered", "language": "en", "confirmedBy": "official-doc", "note": "Status in ShipBlu's API documentation: the merchant moved the pickup."},
13
+ {"code": "return_requested", "wording": "Return requested", "stage": "registered", "language": "en", "confirmedBy": "official-doc", "note": "Status in ShipBlu's API documentation: the merchant asked ShipBlu to collect a return from the customer."},
14
+ {"code": "return_attempted", "wording": "Return attempted", "language": "en", "confirmedBy": "official-doc", "unmapped": true, "note": "Status in ShipBlu's API documentation: a courier could not collect a return from the customer, so the parcel is not in ShipBlu's hands yet."},
15
+ {"code": "delivery_attempted", "wording": "Delivery attempted", "stage": "failed_attempt", "language": "en", "confirmedBy": "official-doc", "note": "Status in ShipBlu's API documentation; the courier gives the reason in the scan's comment."},
16
+ {"code": "return_to_origin", "wording": "Return to origin", "stage": "returned", "language": "en", "confirmedBy": "official-doc", "note": "Status in ShipBlu's API documentation: the customer refused the order, its packaging broke ShipBlu's rules or the address is out of zone."},
17
+ {"code": "out_for_return", "wording": "Out for return", "stage": "returned", "language": "en", "confirmedBy": "official-doc", "note": "Status in ShipBlu's API documentation: a courier is taking the order back to the merchant."},
18
+ {"code": "return_postponed", "wording": "Return postponed", "stage": "returned", "language": "en", "confirmedBy": "official-doc", "note": "Status in ShipBlu's API documentation: the merchant moved the return to the next day."},
19
+ {"code": "return_refused", "wording": "Return refused", "stage": "exception", "language": "en", "confirmedBy": "official-doc", "note": "Status in ShipBlu's API documentation: the merchant refused to take the order back."},
20
+ {"code": "returned", "wording": "Returned", "stage": "returned", "language": "en", "confirmedBy": "official-doc", "note": "Status in ShipBlu's API documentation: the order reached the merchant."},
21
+ {"code": "cancelled", "wording": "Cancelled", "stage": "exception", "language": "en", "confirmedBy": "official-doc", "note": "Status in ShipBlu's API documentation: the merchant cancelled the order before pickup."},
22
+ {"code": "picked_up", "wording": "تم استلام الطلب", "stage": "accepted", "language": "ar", "confirmedBy": "bundle", "note": "Arabic label of the step in the go.shipblu.com tracker's timeline."},
23
+ {"code": "in_transit", "wording": "En Route", "stage": "in_transit", "language": "en", "confirmedBy": "bundle", "note": "Label of the step in the go.shipblu.com tracker's timeline."},
24
+ {"code": "in_transit", "wording": "في مركز التوزيع", "stage": "in_transit", "language": "ar", "confirmedBy": "bundle", "note": "Arabic label of the step in the go.shipblu.com tracker's timeline: at the distribution centre."},
25
+ {"code": "out_for_delivery", "wording": "طلبك في طريقه إليك", "stage": "out_for_delivery", "language": "ar", "confirmedBy": "bundle", "note": "Arabic label of the step in the go.shipblu.com tracker's timeline."},
26
+ {"code": "delivered", "wording": "تم تسليم الطلب", "stage": "delivered", "language": "ar", "confirmedBy": "bundle", "note": "Arabic label of the step in the go.shipblu.com tracker's timeline."},
27
+ {"code": "out_for_return", "wording": "طلبك فى طريقه للتاجر", "stage": "returned", "language": "ar", "confirmedBy": "bundle", "note": "Arabic label of the step in the go.shipblu.com tracker's timeline: the order is on its way back to the merchant."},
28
+ {"code": "returned", "wording": "تم ارجاع الطلب", "stage": "returned", "language": "ar", "confirmedBy": "bundle", "note": "Arabic label of the step in the go.shipblu.com tracker's timeline."},
29
+ {"wording": "On Hold", "stage": "exception", "language": "en", "confirmedBy": "official-doc", "note": "From ShipBlu's help article on the order timeline: the merchant's balance does not cover the shipping fee. The tracker shows it from the order's hold flag, not as a status."}
25
30
  ]
26
31
  }
@@ -202,6 +202,7 @@ import { statusMap as kyoungdong } from '../../carriers/kyoungdong/status.js';
202
202
  import { statusMap as lPost } from '../../carriers/l-post/status.js';
203
203
  import { statusMap as laPoste } from '../../carriers/la-poste/status.js';
204
204
  import { statusMap as laPosteSenegal } from '../../carriers/la-poste-senegal/status.js';
205
+ import { statusMap as labaih } from '../../carriers/labaih/status.js';
205
206
  import { statusMap as latvijasPasts } from '../../carriers/latvijas-pasts/status.js';
206
207
  import { statusMap as leopardsCourier } from '../../carriers/leopards-courier/status.js';
207
208
  import { statusMap as liccardi } from '../../carriers/liccardi/status.js';
@@ -295,6 +296,7 @@ import { statusMap as servientrega } from '../../carriers/servientrega/status.js
295
296
  import { statusMap as sfc } from '../../carriers/sfc/status.js';
296
297
  import { statusMap as shadowfax } from '../../carriers/shadowfax/status.js';
297
298
  import { statusMap as shipa } from '../../carriers/shipa/status.js';
299
+ import { statusMap as shipblu } from '../../carriers/shipblu/status.js';
298
300
  import { statusMap as shipentegra } from '../../carriers/shipentegra/status.js';
299
301
  import { statusMap as shreeMaruti } from '../../carriers/shree-maruti/status.js';
300
302
  import { statusMap as skroutzLastMile } from '../../carriers/skroutz-last-mile/status.js';
@@ -403,7 +405,7 @@ export const STATUS_MAPS = {
403
405
  'intex-paketdienst': intexPaketdienst, 'israel-post': israelPost, 'j-and-t': jAndT, 'j-and-t-cargo': jntCargo,
404
406
  'j-and-t-middle-east': jAndTMiddleEast, janio, jeebly, 'jersey-post': jerseyPost, 'jet-logistic': jetLogistic, jhgj, jitsu, jne, 'jordan-post': jordanPost,
405
407
  jrt, 'js-express': jsExpress, kazpost, kex, 'kuehne-nagel': kuehneNagel, kyoungdong, 'l-post': lPost, 'la-poste': laPoste,
406
- 'la-poste-senegal': laPosteSenegal, 'latvijas-pasts': latvijasPasts,
408
+ 'la-poste-senegal': laPosteSenegal, labaih, 'latvijas-pasts': latvijasPasts,
407
409
  'leopards-courier': leopardsCourier, liccardi, 'lietuvos-pastas': lietuvosPastas, linhan, 'lion-parcel': lionParcel, logen, loggi, logoix, 'loomis-express': loomisExpress,
408
410
  'lotte-global': lotteGlobal, 'm-one': mOne, 'magyar-posta': magyarPosta, mailamericas, mainfreight, maltapost, matkahuolto, mayiyinfu, meest, 'messenger-cz': messengerCz,
409
411
  'metropolitan-warehouse': metropolitanWarehouse, 'mng-kargo': mngKargo, mtscn, mylerz, naqel, nationex, 'next-level-delivery': nextLevelDelivery, nipost, 'nippon-express': nipponExpress,
@@ -418,7 +420,7 @@ export const STATUS_MAPS = {
418
420
  'pts-worldwide-express': ptsWorldwideExpress, ptt, 'quantium-solutions': quantiumSolutions, 'quicksilver-sa': quicksilverSa, raben, redpack, redur, redx, relaytech,
419
421
  'rl-carriers': rlCarriers,
420
422
  'rpx-indonesia': rpxIndonesia, sagawa, saia, sailpost, sameday, 'saudi-post': saudiPost, seino, seko, sending, serpost, servientrega, sfc,
421
- shadowfax, shipa, shipentegra, 'shree-maruti': shreeMaruti, 'skroutz-last-mile': skroutzLastMile, 'skynet-worldwide': skynetWorldwide,
423
+ shadowfax, shipa, shipblu, shipentegra, 'shree-maruti': shreeMaruti, 'skroutz-last-mile': skroutzLastMile, 'skynet-worldwide': skynetWorldwide,
422
424
  'slovak-parcel-service': slovakParcelService,
423
425
  'slovenska-posta': slovenskaPosta, 'smsa-express': smsaExpress, 'south-african-post-office': southAfricanPost,
424
426
  'southeastern-freight-lines': southeasternFreightLines, 'sp-express': spExpress, speedaf, speedex, speedx, speedy, 'spx-br': spxBr, 'spx-id': spxId, 'spx-my': spxMy,
@@ -18174,7 +18174,9 @@
18174
18174
  "timezone": "Asia/Riyadh",
18175
18175
  "tracking": {
18176
18176
  "mode": "automatic",
18177
- "adapter": "universal"
18177
+ "adapter": "labaih",
18178
+ "recognitionRank": 788,
18179
+ "localClocks": true
18178
18180
  },
18179
18181
  "canaryUrl": "https://mylabaih.com/",
18180
18182
  "trackingUrlTemplate": "https://track.mylabaih.com/track/?cn={trackingNumber}",
@@ -18184,7 +18186,7 @@
18184
18186
  "domains": [
18185
18187
  "track.mylabaih.com"
18186
18188
  ],
18187
- "pathPattern": "^/track/?$",
18189
+ "pathPattern": "^/track(?:/en)?/?$",
18188
18190
  "params": [
18189
18191
  "cn"
18190
18192
  ]
@@ -25350,7 +25352,8 @@
25350
25352
  "timezone": "Africa/Cairo",
25351
25353
  "tracking": {
25352
25354
  "mode": "automatic",
25353
- "adapter": "universal"
25355
+ "adapter": "shipblu",
25356
+ "recognitionRank": 789
25354
25357
  },
25355
25358
  "canaryUrl": "https://go.shipblu.com/en/",
25356
25359
  "trackingUrlTemplate": "https://go.shipblu.com/en/{trackingNumber}",
@@ -11687,14 +11687,16 @@ export declare const CARRIER_CATALOG: {
11687
11687
  readonly timezone: "Asia/Riyadh";
11688
11688
  readonly tracking: {
11689
11689
  readonly mode: "automatic";
11690
- readonly adapter: "universal";
11690
+ readonly adapter: "labaih";
11691
+ readonly recognitionRank: 788;
11692
+ readonly localClocks: true;
11691
11693
  };
11692
11694
  readonly canaryUrl: "https://mylabaih.com/";
11693
11695
  readonly trackingUrlTemplate: "https://track.mylabaih.com/track/?cn={trackingNumber}";
11694
11696
  readonly trackingSearchUrl: "https://mylabaih.com/";
11695
11697
  readonly linkRules: readonly [{
11696
11698
  readonly domains: readonly ["track.mylabaih.com"];
11697
- readonly pathPattern: "^/track/?$";
11699
+ readonly pathPattern: "^/track(?:/en)?/?$";
11698
11700
  readonly params: readonly ["cn"];
11699
11701
  }];
11700
11702
  readonly detectionRules: readonly [];
@@ -16185,7 +16187,8 @@ export declare const CARRIER_CATALOG: {
16185
16187
  readonly timezone: "Africa/Cairo";
16186
16188
  readonly tracking: {
16187
16189
  readonly mode: "automatic";
16188
- readonly adapter: "universal";
16190
+ readonly adapter: "shipblu";
16191
+ readonly recognitionRank: 789;
16189
16192
  };
16190
16193
  readonly canaryUrl: "https://go.shipblu.com/en/";
16191
16194
  readonly trackingUrlTemplate: "https://go.shipblu.com/en/{trackingNumber}";
@@ -18175,7 +18175,9 @@ export const CARRIER_CATALOG = {
18175
18175
  "timezone": "Asia/Riyadh",
18176
18176
  "tracking": {
18177
18177
  "mode": "automatic",
18178
- "adapter": "universal"
18178
+ "adapter": "labaih",
18179
+ "recognitionRank": 788,
18180
+ "localClocks": true
18179
18181
  },
18180
18182
  "canaryUrl": "https://mylabaih.com/",
18181
18183
  "trackingUrlTemplate": "https://track.mylabaih.com/track/?cn={trackingNumber}",
@@ -18185,7 +18187,7 @@ export const CARRIER_CATALOG = {
18185
18187
  "domains": [
18186
18188
  "track.mylabaih.com"
18187
18189
  ],
18188
- "pathPattern": "^/track/?$",
18190
+ "pathPattern": "^/track(?:/en)?/?$",
18189
18191
  "params": [
18190
18192
  "cn"
18191
18193
  ]
@@ -25351,7 +25353,8 @@ export const CARRIER_CATALOG = {
25351
25353
  "timezone": "Africa/Cairo",
25352
25354
  "tracking": {
25353
25355
  "mode": "automatic",
25354
- "adapter": "universal"
25356
+ "adapter": "shipblu",
25357
+ "recognitionRank": 789
25355
25358
  },
25356
25359
  "canaryUrl": "https://go.shipblu.com/en/",
25357
25360
  "trackingUrlTemplate": "https://go.shipblu.com/en/{trackingNumber}",
@@ -182,6 +182,7 @@ export const CARRIER_RECOGNITION_RANKS = {
182
182
  "kuehne-nagel": 435,
183
183
  "l-post": 808,
184
184
  "la-poste": 1000,
185
+ "labaih": 788,
185
186
  "latvijas-pasts": 100,
186
187
  "leopards-courier": 965,
187
188
  "lietuvos-pastas": 895,
@@ -267,6 +268,7 @@ export const CARRIER_RECOGNITION_RANKS = {
267
268
  "seur": 870,
268
269
  "sfc": 955,
269
270
  "shadowfax": 595,
271
+ "shipblu": 789,
270
272
  "shipentegra": 833,
271
273
  "shree-maruti": 822,
272
274
  "skroutz-last-mile": 913,
@@ -241,6 +241,7 @@ import { adapter as kyoungdong } from '../carriers/kyoungdong/adapter.js';
241
241
  import { adapter as lPost } from '../carriers/l-post/adapter.js';
242
242
  import { adapter as laPoste } from '../carriers/la-poste/adapter.js';
243
243
  import { adapter as laPosteSenegal } from '../carriers/la-poste-senegal/adapter.js';
244
+ import { adapter as labaih } from '../carriers/labaih/adapter.js';
244
245
  import { adapter as landmarkGlobal } from '../carriers/landmark-global/adapter.js';
245
246
  import { adapter as latvijasPasts } from '../carriers/latvijas-pasts/adapter.js';
246
247
  import { adapter as lbcExpress } from '../carriers/lbc-express/adapter.js';
@@ -356,6 +357,7 @@ import { adapter as sfExpress } from '../carriers/sf-express/adapter.js';
356
357
  import { adapter as sfc } from '../carriers/sfc/adapter.js';
357
358
  import { adapter as shadowfax } from '../carriers/shadowfax/adapter.js';
358
359
  import { adapter as shipa } from '../carriers/shipa/adapter.js';
360
+ import { adapter as shipblu } from '../carriers/shipblu/adapter.js';
359
361
  import { adapter as shipentegra } from '../carriers/shipentegra/adapter.js';
360
362
  import { adapter as shreeMaruti } from '../carriers/shree-maruti/adapter.js';
361
363
  import { adapter as singaporePost } from '../carriers/singapore-post/adapter.js';
@@ -691,6 +693,7 @@ export const REGISTRY = {
691
693
  "l-post": lPost,
692
694
  "la-poste": laPoste,
693
695
  "la-poste-senegal": laPosteSenegal,
696
+ "labaih": labaih,
694
697
  "landmark-global": landmarkGlobal,
695
698
  "latvijas-pasts": latvijasPasts,
696
699
  "lbc-express": lbcExpress,
@@ -806,6 +809,7 @@ export const REGISTRY = {
806
809
  "sfc": sfc,
807
810
  "shadowfax": shadowfax,
808
811
  "shipa": shipa,
812
+ "shipblu": shipblu,
809
813
  "shipentegra": shipentegra,
810
814
  "shree-maruti": shreeMaruti,
811
815
  "singapore-post": singaporePost,
@@ -1296,7 +1300,7 @@ export const REGISTRY = {
1296
1300
  "la-poste-senegal": "la-poste-senegal",
1297
1301
  "la-poste-tunisienne": "universal",
1298
1302
  "laar-courier": "universal",
1299
- "labaih": "universal",
1303
+ "labaih": "labaih",
1300
1304
  "landmark-global": "landmark-global",
1301
1305
  "lao-post": "universal",
1302
1306
  "last-one-mile-coop": "universal",
@@ -1507,7 +1511,7 @@ export const REGISTRY = {
1507
1511
  "sfc": "sfc",
1508
1512
  "shadowfax": "shadowfax",
1509
1513
  "shipa": "shipa",
1510
- "shipblu": "universal",
1514
+ "shipblu": "shipblu",
1511
1515
  "shipentegra": "shipentegra",
1512
1516
  "shipup": "universal",
1513
1517
  "shree-anjani": "universal",
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Universal Parcel Scraper",
5
- "version": "0.413.0"
5
+ "version": "0.415.0"
6
6
  },
7
7
  "paths": {
8
8
  "/health": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-parcel-scraper",
3
- "version": "0.413.0-main.2132",
3
+ "version": "0.415.0-main.2136",
4
4
  "description": "Self-hosted parcel tracking: carrier detection, dedicated scrapers and optional universal providers.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",