@accounter/modern-poalim-scraper
Version:
Modern scraper for Israeli banks (Hapoalim, Isracard, Max)
147 lines • 5.61 kB
JavaScript
import { v4 as uuidv4 } from 'uuid';
export async function fetchPostWithinPage(page, url, data, extraHeaders = {}) {
return page.evaluate((url, data, extraHeaders) => {
return new Promise((resolve, reject) => {
fetch(url, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'include',
headers: new Headers(Object.assign({
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
}, extraHeaders)),
})
.then(result => {
if (result.status === 204) {
// No content response
resolve(null);
}
else {
resolve(result.json());
}
})
.catch(e => {
reject(e);
});
});
}, url, data, extraHeaders);
}
export async function fetchGetWithinPage(page, url) {
return page.evaluate(url => {
return new Promise((resolve, reject) => {
fetch(url, { credentials: 'include' })
.then(result => {
if (result.status === 204) {
resolve(null);
}
else {
resolve(result.json());
}
})
.catch(e => {
reject(e);
});
});
}, url);
}
/** Matches a hostname against a cookie's domain, honouring the leading-dot wildcard form. */
function cookieDomainMatches(hostname, cookieDomain) {
const domain = cookieDomain.startsWith('.') ? cookieDomain.slice(1) : cookieDomain;
return hostname === domain || hostname.endsWith(`.${domain}`);
}
/**
* Reads the XSRF token for the page's own host.
*
* Cookies are read from the browser context rather than the page (the
* page-level cookie API is deprecated), but every scraper shares the default
* context — so the result must be filtered by domain, otherwise another bank's
* `XSRF-TOKEN` could be picked up.
*/
export async function getXsrfToken(page) {
const { hostname } = new URL(page.url());
const cookies = await page.browserContext().cookies();
return cookies.find(cookie => cookie.name === 'XSRF-TOKEN' && cookieDomainMatches(hostname, cookie.domain))?.value;
}
/**
* Captures the `session` / `csession` headers the mytrade SPA sends on its own
* API calls, by listening to the page's outgoing requests.
*
* The session key is minted server-side when the SPA boots — the API rejects
* anything we invent with `InvalidSessionException`, echoing the bad key back.
* Rather than guessing where the SPA stashes it, let it authenticate and reuse
* the headers off the wire.
*
* Attach this *before* navigating, then await it after `goto` resolves.
*/
export function captureMytradeSession(page, timeoutMs = 30_000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
page.off('request', onRequest);
reject(new Error('Timed out waiting for the mytrade SPA to issue an API request; no session header captured.'));
}, timeoutMs);
function onRequest(request) {
if (!request.url().includes('/mytrade/api/'))
return;
const headers = request.headers();
const session = headers['session'];
if (!session)
return;
clearTimeout(timer);
page.off('request', onRequest);
resolve({ session, csession: headers['csession'] });
}
page.on('request', onRequest);
});
}
/**
* The "mytrade" portfolio API sits behind a different gateway than the rest of
* the Poalim REST surface: it wants the XSRF token like everything else, plus
* the SPA's server-issued `session` key, and is called with an empty body
* rather than `{}`.
*
* The method is per-endpoint: `account/view` is a POST, while the order
* executions history is a GET (sending it as POST returns nothing).
*/
export async function fetchPoalimMytradeWithinPage(page, url, mytradeSession, method = 'POST') {
const xsrfToken = await getXsrfToken(page);
const headers = {};
if (xsrfToken != null) {
headers['X-XSRF-TOKEN'] = xsrfToken;
}
headers['session'] = mytradeSession.session;
if (mytradeSession.csession != null) {
headers['csession'] = mytradeSession.csession;
}
headers['Content-Type'] = 'application/json; charset=utf-8';
return page.evaluate((url, headers, method) => {
return new Promise((resolve, reject) => {
fetch(url, {
method,
credentials: 'include',
headers: new Headers(headers),
})
.then(result => {
if (result.status === 204) {
resolve(null);
}
else {
resolve(result.json());
}
})
.catch(e => {
reject(e);
});
});
}, url, headers, method);
}
export async function fetchPoalimXSRFWithinPage(page, url, pageUuid) {
const xsrfToken = await getXsrfToken(page);
const headers = {};
if (xsrfToken != null) {
headers['X-XSRF-TOKEN'] = xsrfToken;
}
headers['pageUuid'] = pageUuid;
headers['uuid'] = uuidv4();
headers['Content-Type'] = 'application/json;charset=UTF-8';
return fetchPostWithinPage(page, url, {}, headers);
}
//# sourceMappingURL=fetch.js.map