106 lines
3.3 KiB
JavaScript
106 lines
3.3 KiB
JavaScript
import crypto from 'crypto';
|
|
import fetch from 'node-fetch';
|
|
import { DEVICE, APP, UA_NATIVE } from './config.js';
|
|
import { computeTokenSnMac, computeXSign } from './crypto.js';
|
|
|
|
// ─── Utilities ───
|
|
|
|
export const generateUUID = () => crypto.randomUUID().toUpperCase();
|
|
|
|
export const nowISO = () => {
|
|
const d = new Date();
|
|
const off = -d.getTimezoneOffset();
|
|
const sign = off >= 0 ? '+' : '-';
|
|
const hh = String(Math.floor(Math.abs(off) / 60)).padStart(2, '0');
|
|
const mm = String(Math.abs(off) % 60).padStart(2, '0');
|
|
return (
|
|
d
|
|
.toISOString()
|
|
.replace('Z', '')
|
|
.replace(/\.\d{3}/, `.${String(d.getMilliseconds()).padStart(3, '0')}`) +
|
|
sign +
|
|
hh +
|
|
mm
|
|
);
|
|
};
|
|
|
|
// ─── Cookie builder ───
|
|
|
|
export const entranceCookie = (extraUserToken) => {
|
|
let c = `deviceId=${DEVICE.deviceId}; installId=${DEVICE.installId}; is_mobile_app=true; locale=${APP.locale}; ma_bld=${APP.build}; ma_platform_type=${APP.platform}; ma_platform_ver=${APP.platformVer}; ma_ver=${APP.version}; pk=${DEVICE.pk}; pkTag=${DEVICE.pkTag}; xs=R:0|E:0|RH:0|N:0`;
|
|
if (extraUserToken) c += `; user_token=${extraUserToken}`;
|
|
return c;
|
|
};
|
|
|
|
// ─── Extract user_token from set-cookie ───
|
|
|
|
export const extractUserToken = (resp) => {
|
|
const raw = resp.headers.raw()['set-cookie'] || [];
|
|
for (const c of raw) {
|
|
const m = c.match(/user_token=([^;]+)/);
|
|
if (m) return m[1];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// ─── Logged fetch wrapper ───
|
|
|
|
export const loggedFetch = async (url, options = {}) => {
|
|
const method = (options.method || 'GET').toUpperCase();
|
|
console.log(`\n>>> ${method} ${url}`);
|
|
if (options.headers) console.log('>>> Headers:', JSON.stringify(options.headers, null, 2));
|
|
if (options.body) {
|
|
try {
|
|
console.log('>>> Body:', JSON.parse(options.body));
|
|
} catch {
|
|
console.log('>>> Body:', options.body);
|
|
}
|
|
}
|
|
|
|
const resp = await fetch(url, options);
|
|
const cloned = resp.clone();
|
|
let body;
|
|
try {
|
|
body = await cloned.json();
|
|
} catch {
|
|
try {
|
|
body = await cloned.text();
|
|
} catch {
|
|
body = '[unreadable]';
|
|
}
|
|
}
|
|
console.log(`<<< ${resp.status} ${resp.statusText}`);
|
|
console.log('<<< Response:', typeof body === 'object' ? JSON.stringify(body, null, 2) : body);
|
|
return resp;
|
|
};
|
|
|
|
// ─── Signed QR-pay headers (session passed as parameter) ───
|
|
|
|
export const signedQrPayHeaders = (url, session) => {
|
|
const xsh =
|
|
'url,X-Request-ID,X-Device-ID,X-Platform-Ver,X-App-Bld,X-Time,X-Kb-TokenSn,X-App-Ver,X-Kb-TokenSnMac,X-Call,X-PI,X-Install-ID,X-Platform-Type,X-Locale,X-SV';
|
|
const headers = {
|
|
'X-Kb-TokenSn': session.tokenSN,
|
|
'X-Kb-TokenSnMac': computeTokenSnMac(session.tokenSN, session.decryptedSecret),
|
|
'X-PI': session.profileId != null ? String(session.profileId) : '',
|
|
'X-Install-ID': DEVICE.installId,
|
|
'X-Device-ID': DEVICE.deviceId,
|
|
'X-App-Ver': APP.version,
|
|
'X-App-Bld': APP.build,
|
|
'X-Platform-Type': APP.platform,
|
|
'X-Platform-Ver': APP.platformVer,
|
|
'X-Locale': APP.locale,
|
|
'X-Time': nowISO(),
|
|
'X-Request-ID': generateUUID(),
|
|
'X-Call': 'notConnected',
|
|
'X-SV': '2',
|
|
'X-SH': xsh,
|
|
'User-Agent': UA_NATIVE,
|
|
Accept: '*/*',
|
|
'Accept-Language': 'ru',
|
|
'Accept-Encoding': 'gzip, deflate, br',
|
|
};
|
|
headers['X-Sign'] = computeXSign(url, headers, xsh);
|
|
return headers;
|
|
};
|