universal-parcel-scraper 0.232.0-main.1493 → 0.233.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/carriers/wanbexpress/adapter.d.ts +9 -0
- package/dist/carriers/wanbexpress/adapter.js +13 -3
- package/dist/carriers/wanbexpress/browser.d.ts +22 -0
- package/dist/carriers/wanbexpress/browser.js +142 -0
- package/dist/carriers/wanbexpress/carrier.json +1 -0
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,15 @@ import type { CarrierResult } from '../../core/result/index.js';
|
|
|
3
3
|
export declare class WanbTracker {
|
|
4
4
|
private readonly environment;
|
|
5
5
|
constructor(environment: AdapterEnvironment);
|
|
6
|
+
private get endpoint();
|
|
7
|
+
browserSteps(number: string): {
|
|
8
|
+
id: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
run: ({ signal, remainingMs }: {
|
|
11
|
+
signal: AbortSignal;
|
|
12
|
+
remainingMs: number;
|
|
13
|
+
}) => Promise<CarrierResult>;
|
|
14
|
+
}[];
|
|
6
15
|
direct(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
7
16
|
private parse;
|
|
8
17
|
browser(raw: string, signal: AbortSignal, remainingMs: number): Promise<CarrierResult>;
|
|
@@ -3,11 +3,19 @@ import { BudgetExceededError, ChallengeError, RateLimitedError, SchemaError, Tra
|
|
|
3
3
|
import { runSteps } from '../../core/runner/index.js';
|
|
4
4
|
import { decodeText, fetchBounded, userAgentOf } from '../../core/transport/index.js';
|
|
5
5
|
import { API, MAX_BYTES, normalizeWanbNumber, ORIGIN, parseWanb, PROVIDER } from './parser.js';
|
|
6
|
+
import { fetchWanbInBrowser } from './browser.js';
|
|
6
7
|
export class WanbTracker {
|
|
7
8
|
environment;
|
|
8
9
|
constructor(environment) {
|
|
9
10
|
this.environment = environment;
|
|
10
11
|
}
|
|
12
|
+
get endpoint() { return this.environment.env.WANBEXPRESS_BROWSER_WS_ENDPOINT?.trim() || null; }
|
|
13
|
+
browserSteps(number) {
|
|
14
|
+
return [
|
|
15
|
+
{ id: 'browser', enabled: !!this.endpoint, run: ({ signal, remainingMs }) => fetchWanbInBrowser(number, this.endpoint, signal, Math.max(1, Math.min(20_000, remainingMs - 2000))) },
|
|
16
|
+
{ id: 'trawl', enabled: !!this.environment.trawl, run: ({ signal, remainingMs }) => this.browser(number, signal, remainingMs) },
|
|
17
|
+
];
|
|
18
|
+
}
|
|
11
19
|
async direct(raw, context = {}) {
|
|
12
20
|
const number = normalizeWanbNumber(raw);
|
|
13
21
|
const { bytes } = await fetchBounded(`${API}?TrackingNumber=${number}`, { signal: context.signal,
|
|
@@ -65,20 +73,22 @@ export class WanbTracker {
|
|
|
65
73
|
const number = normalizeWanbNumber(raw);
|
|
66
74
|
return runSteps({ carrier: 'wanbexpress', budgetMs: context.budgetMs ?? 45000, signal: context.signal, recorder: this.environment.recorder }, [
|
|
67
75
|
{ id: 'direct', run: ({ signal, remainingMs }) => this.direct(number, { signal, budgetMs: Math.min(5000, remainingMs) }) },
|
|
68
|
-
|
|
76
|
+
...this.browserSteps(number),
|
|
69
77
|
]);
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
export const adapter = environment => {
|
|
73
81
|
const tracker = new WanbTracker(environment);
|
|
74
|
-
return { id: 'wanbexpress', recordsSteps: true, steps: ['direct', 'trawl'], track: (input, context) => tracker.fetch(input.number, context),
|
|
82
|
+
return { id: 'wanbexpress', recordsSteps: true, steps: ['direct', ...(environment.env.WANBEXPRESS_BROWSER_WS_ENDPOINT?.trim() ? ['browser'] : []), 'trawl'], track: (input, context) => tracker.fetch(input.number, context),
|
|
75
83
|
recognize: (number, context) => recognizeFromLookup(() => tracker.direct(number, context), () => accepted(() => normalizeWanbNumber(number))),
|
|
76
84
|
recognizeWithBrowser: (number, context, previous) => {
|
|
77
85
|
if (previous instanceof SchemaError || previous instanceof RateLimitedError)
|
|
78
86
|
return Promise.reject(previous);
|
|
87
|
+
if (!environment.env.WANBEXPRESS_BROWSER_WS_ENDPOINT?.trim() && !environment.trawl)
|
|
88
|
+
return Promise.reject(new ChallengeError(PROVIDER, 'WanbExpress requires a browser tracking service'));
|
|
79
89
|
return recognizeFromBrowserLookup(() => runSteps({ carrier: 'wanbexpress', budgetMs: context?.budgetMs ?? 45000,
|
|
80
90
|
signal: context?.signal, recorder: environment.recorder }, [
|
|
81
|
-
|
|
91
|
+
...tracker.browserSteps(normalizeWanbNumber(number)),
|
|
82
92
|
]));
|
|
83
93
|
} };
|
|
84
94
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface Reply {
|
|
2
|
+
url: string;
|
|
3
|
+
status: number;
|
|
4
|
+
retryAfter: string | null;
|
|
5
|
+
body: string | null;
|
|
6
|
+
truncated: boolean;
|
|
7
|
+
}
|
|
8
|
+
interface GuestResult {
|
|
9
|
+
replies: Reply[];
|
|
10
|
+
verification?: {
|
|
11
|
+
status: number;
|
|
12
|
+
retryAfter: string | null;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Runs in the carrier page. CAPTCHA credentials never leave this function. */
|
|
16
|
+
export declare function wanbGuestFlow({ number, timeoutMs, maxBytes }: {
|
|
17
|
+
number: string;
|
|
18
|
+
timeoutMs: number;
|
|
19
|
+
maxBytes: number;
|
|
20
|
+
}): Promise<GuestResult>;
|
|
21
|
+
export declare function fetchWanbInBrowser(number: string, endpoint: string, signal: AbortSignal, timeoutMs: number): Promise<import("../../index.js").CarrierResult>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { ChallengeError, RateLimitedError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
2
|
+
import { withRemoteBrowser } from '../../core/transport/localBrowser.js';
|
|
3
|
+
import { API, MAX_BYTES, ORIGIN, parseWanb, PROVIDER } from './parser.js';
|
|
4
|
+
/** Runs in the carrier page. CAPTCHA credentials never leave this function. */
|
|
5
|
+
export async function wanbGuestFlow({ number, timeoutMs, maxBytes }) {
|
|
6
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
7
|
+
const result = { replies: [] };
|
|
8
|
+
const body = async (response) => {
|
|
9
|
+
if (Number(response.headers.get('content-length')) > maxBytes) {
|
|
10
|
+
void response.body?.cancel().catch(() => { });
|
|
11
|
+
return { body: null, truncated: true };
|
|
12
|
+
}
|
|
13
|
+
const reader = response.body?.getReader();
|
|
14
|
+
if (!reader)
|
|
15
|
+
return { body: '', truncated: false };
|
|
16
|
+
const chunks = [];
|
|
17
|
+
let size = 0;
|
|
18
|
+
try {
|
|
19
|
+
for (;;) {
|
|
20
|
+
const part = await reader.read();
|
|
21
|
+
if (part.done)
|
|
22
|
+
break;
|
|
23
|
+
size += part.value.length;
|
|
24
|
+
if (size > maxBytes) {
|
|
25
|
+
void reader.cancel().catch(() => { });
|
|
26
|
+
return { body: null, truncated: true };
|
|
27
|
+
}
|
|
28
|
+
chunks.push(part.value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
reader.releaseLock();
|
|
33
|
+
}
|
|
34
|
+
const bytes = new Uint8Array(size);
|
|
35
|
+
let offset = 0;
|
|
36
|
+
for (const chunk of chunks) {
|
|
37
|
+
bytes.set(chunk, offset);
|
|
38
|
+
offset += chunk.length;
|
|
39
|
+
}
|
|
40
|
+
return { body: new TextDecoder().decode(bytes), truncated: false };
|
|
41
|
+
};
|
|
42
|
+
const track = async () => {
|
|
43
|
+
const response = await fetch('/TrackPoints/Track?TrackingNumber=' + encodeURIComponent(number), { credentials: 'same-origin', signal });
|
|
44
|
+
const reply = { url: response.url, status: response.status, retryAfter: response.headers.get('retry-after'), body: null, truncated: false };
|
|
45
|
+
result.replies.push(reply);
|
|
46
|
+
if (response.status === 200)
|
|
47
|
+
Object.assign(reply, await body(response));
|
|
48
|
+
else
|
|
49
|
+
void response.body?.cancel().catch(() => { });
|
|
50
|
+
return reply;
|
|
51
|
+
};
|
|
52
|
+
const first = await track();
|
|
53
|
+
if (first.status !== 200 || !first.body || first.truncated)
|
|
54
|
+
return result;
|
|
55
|
+
const record = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
56
|
+
let initial;
|
|
57
|
+
try {
|
|
58
|
+
initial = JSON.parse(first.body);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
if (!record(initial) || initial.Succeeded !== false || !record(initial.Error) || initial.Error.Code !== '0x110012')
|
|
64
|
+
return result;
|
|
65
|
+
const version = document.querySelector('#showCaptchaVersion')?.value;
|
|
66
|
+
const key = document.querySelector('#sitekey')?.value;
|
|
67
|
+
if (version !== 'V3' || !/^[A-Za-z0-9_-]{30,100}$/.test(key ?? ''))
|
|
68
|
+
return result;
|
|
69
|
+
const win = window;
|
|
70
|
+
try {
|
|
71
|
+
delete win.grecaptcha;
|
|
72
|
+
delete win.___grecaptcha_cfg;
|
|
73
|
+
await new Promise((resolve, reject) => {
|
|
74
|
+
const script = document.createElement('script');
|
|
75
|
+
script.src = 'https://www.recaptcha.net/recaptcha/api.js?render=' + encodeURIComponent(key);
|
|
76
|
+
script.onload = () => resolve();
|
|
77
|
+
script.onerror = () => reject(new Error('Verification script unavailable'));
|
|
78
|
+
document.head.appendChild(script);
|
|
79
|
+
});
|
|
80
|
+
await new Promise(resolve => win.grecaptcha.ready(resolve));
|
|
81
|
+
const token = await win.grecaptcha.execute(key, { action: 'submit' });
|
|
82
|
+
signal.throwIfAborted();
|
|
83
|
+
const response = await fetch('/Home/CaptchaVerify?token=' + encodeURIComponent(token) + '&version=V3', { credentials: 'same-origin', signal });
|
|
84
|
+
result.verification = { status: response.status, retryAfter: response.headers.get('retry-after') };
|
|
85
|
+
if (response.status !== 200) {
|
|
86
|
+
void response.body?.cancel().catch(() => { });
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
const verification = await body(response);
|
|
90
|
+
if (!verification.truncated && verification.body) {
|
|
91
|
+
const accepted = JSON.parse(verification.body);
|
|
92
|
+
if (record(accepted) && accepted.Succeeded === true)
|
|
93
|
+
await track();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch { /* The initial rejection remains authoritative when verification cannot complete. */ }
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
function checkStatus(status, retryAfter) {
|
|
100
|
+
if ([401, 403, 428].includes(status))
|
|
101
|
+
throw new ChallengeError(PROVIDER);
|
|
102
|
+
if (status === 429) {
|
|
103
|
+
const delay = retryAfter && /^\d+$/.test(retryAfter) ? Number(retryAfter) * 1000 : retryAfter ? Date.parse(retryAfter) - Date.now() : NaN;
|
|
104
|
+
throw new RateLimitedError(PROVIDER, Number.isFinite(delay) ? Math.max(0, delay) : undefined);
|
|
105
|
+
}
|
|
106
|
+
if (status >= 500)
|
|
107
|
+
throw new UpstreamHttpError(PROVIDER, status);
|
|
108
|
+
if (status >= 400)
|
|
109
|
+
throw new TransportError(PROVIDER, 'WanbExpress browser endpoint is unavailable', { status });
|
|
110
|
+
}
|
|
111
|
+
export function fetchWanbInBrowser(number, endpoint, signal, timeoutMs) {
|
|
112
|
+
return withRemoteBrowser({ provider: PROVIDER, endpoint, signal, timeoutMs }, async ({ browser, signal: stopped, remainingMs }) => {
|
|
113
|
+
const context = await browser.newContext({ acceptDownloads: false });
|
|
114
|
+
const page = await context.newPage();
|
|
115
|
+
const navigation = await page.goto(`${ORIGIN}/`, { waitUntil: 'domcontentloaded', timeout: Math.min(10_000, remainingMs()) });
|
|
116
|
+
if (!navigation)
|
|
117
|
+
throw new TransportError(PROVIDER, 'WanbExpress tracking page is unavailable');
|
|
118
|
+
checkStatus(navigation.status(), navigation.headers()['retry-after'] ?? null);
|
|
119
|
+
if (navigation.status() !== 200 || new URL(navigation.url()).origin !== ORIGIN)
|
|
120
|
+
throw new SchemaError(PROVIDER, 'WanbExpress returned an unexpected tracking page');
|
|
121
|
+
const result = await page.evaluate(wanbGuestFlow, { number, timeoutMs: remainingMs(), maxBytes: MAX_BYTES });
|
|
122
|
+
stopped.throwIfAborted();
|
|
123
|
+
if (result.verification)
|
|
124
|
+
checkStatus(result.verification.status, result.verification.retryAfter);
|
|
125
|
+
const reply = result.replies.at(-1);
|
|
126
|
+
if (!reply || result.replies.length > 2 || reply.url !== `${API}?TrackingNumber=${encodeURIComponent(number)}`)
|
|
127
|
+
throw new SchemaError(PROVIDER, 'WanbExpress returned an unrelated browser response');
|
|
128
|
+
checkStatus(reply.status, reply.retryAfter);
|
|
129
|
+
if (reply.truncated || reply.body === null)
|
|
130
|
+
throw new TransportError(PROVIDER, 'WanbExpress returned an incomplete browser response');
|
|
131
|
+
if (reply.status !== 200)
|
|
132
|
+
throw new SchemaError(PROVIDER);
|
|
133
|
+
let payload;
|
|
134
|
+
try {
|
|
135
|
+
payload = JSON.parse(reply.body);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new SchemaError(PROVIDER, 'WanbExpress returned invalid browser JSON');
|
|
139
|
+
}
|
|
140
|
+
return parseWanb(payload, number);
|
|
141
|
+
});
|
|
142
|
+
}
|
package/dist/server/openapi.json
CHANGED
package/package.json
CHANGED