scripter-x 1.0.30 → 1.0.31
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/package.json +1 -1
- package/src/commands.js +127 -24
- package/src/index.js +4 -1
- package/src/providers/bigbasket.js +184 -0
- package/src/ui/RunView.js +23 -5
- package/src/ui/Shell.js +1 -0
- package/src/worker.js +7 -1
package/package.json
CHANGED
package/src/commands.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as tp from './providers/tempotp.js';
|
|
|
8
8
|
import * as oc from './providers/otpcart.js';
|
|
9
9
|
import * as kuku from './providers/kuku.js';
|
|
10
10
|
import * as zepto from './providers/zepto.js';
|
|
11
|
+
import * as bigbasket from './providers/bigbasket.js';
|
|
11
12
|
import { checkCouponTier } from './providers/zeptoCoupon.js';
|
|
12
13
|
import { homedir } from 'node:os';
|
|
13
14
|
import { join, dirname } from 'node:path';
|
|
@@ -129,11 +130,13 @@ async function buildProvider(io, name) {
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
export async function run(io, api, args = {}) {
|
|
132
|
-
// TARGET: Flipkart (
|
|
133
|
-
const target = args.target || (
|
|
133
|
+
// TARGET: Flipkart (backend campaign), Zepto, or BigBasket (local OTP → session JSON).
|
|
134
|
+
const target = args.target || (await io.select('what to extract?', [
|
|
134
135
|
{ label: 'Flipkart', value: 'flipkart', description: 'session JSON via OTP provider (campaign)' },
|
|
135
|
-
{ label: 'Zepto', value: 'zepto', description: 'headless extraction (auto) OR local OTP login (manual)' }
|
|
136
|
+
{ label: 'Zepto', value: 'zepto', description: 'headless extraction (auto) OR local OTP login (manual)' },
|
|
137
|
+
{ label: 'BigBasket', value: 'bigbasket', description: 'local OTP login → session JSON (manual; auto soon)' }])).value;
|
|
136
138
|
if (target === 'zepto') return zeptoCmd(io, api, args);
|
|
139
|
+
if (target === 'bigbasket') return bigbasketCmd(io, api, args);
|
|
137
140
|
|
|
138
141
|
api = api || await getApi(io);
|
|
139
142
|
|
|
@@ -441,12 +444,29 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
441
444
|
const { ZeptoWorker } = await import('./worker.js');
|
|
442
445
|
const { RunController } = await import('./controller.js');
|
|
443
446
|
const controller = new RunController({ name, provider: 'otpcart-zepto', requested: count });
|
|
447
|
+
|
|
448
|
+
// INCREMENTAL SAVE: append every extracted envelope to its tier file the instant it
|
|
449
|
+
// succeeds — so a mid-campaign stop / double-Ctrl+C / crash never loses an account.
|
|
450
|
+
const tierDir = checkCoupon ? zeptoTierDir(args, name) : null;
|
|
451
|
+
const savedTiers = {}; // tier -> count, for the final summary
|
|
452
|
+
const onResult = (res) => {
|
|
453
|
+
if (res.status !== 'success' || !res.envelope) return;
|
|
454
|
+
if (checkCoupon) {
|
|
455
|
+
const tier = res.coupon_tier || 'unchecked';
|
|
456
|
+
mkdirSync(tierDir, { recursive: true });
|
|
457
|
+
const dest = join(tierDir, `${tier}.txt`);
|
|
458
|
+
appendFileSync(dest, (existsSync(dest) ? '\n' : '') + res.envelope + '\n');
|
|
459
|
+
savedTiers[tier] = (savedTiers[tier] || 0) + 1;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
444
463
|
const worker = new ZeptoWorker(provider, {
|
|
445
464
|
requested: count,
|
|
446
465
|
concurrency,
|
|
447
466
|
certSha: cert,
|
|
448
467
|
checkCoupon,
|
|
449
468
|
onEvent: controller.handleEvent,
|
|
469
|
+
onResult,
|
|
450
470
|
});
|
|
451
471
|
controller.stop = () => worker.stop();
|
|
452
472
|
|
|
@@ -478,42 +498,125 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
478
498
|
|
|
479
499
|
io.print(` ✓ done — ${worker.stats.succeeded} succeeded · ${worker.stats.failed} failed · ${worker.stats.cancelled} cancelled · ₹${worker.stats.charges} spent`);
|
|
480
500
|
|
|
481
|
-
|
|
501
|
+
// Note "stopped" so the summary is honest about a mid-run stop.
|
|
502
|
+
if (worker.stopped) io.print(' ⚠ campaign stopped — saving everything extracted so far', 'warn');
|
|
503
|
+
|
|
504
|
+
const succeeded = results.filter((r) => r.status === 'success' && r.envelope);
|
|
505
|
+
if (succeeded.length > 0) {
|
|
506
|
+
// combined full/zauth files (all successes, for convenience)
|
|
482
507
|
const saved = await saveZeptoSessions(results, name, args.out);
|
|
483
508
|
if (saved) {
|
|
484
509
|
io.print(` ✓ saved ${saved.count} session(s) to 2 files:`);
|
|
485
510
|
io.print(` full → ${saved.fullPath}`, 'accent');
|
|
486
511
|
io.print(` zauth → ${saved.zauthPath}`, 'accent');
|
|
487
512
|
}
|
|
488
|
-
// If coupon-tier was checked, ALSO split the envelopes into per-tier files.
|
|
489
513
|
if (checkCoupon) {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
if (r.status !== 'success') continue;
|
|
493
|
-
const tier = r.coupon_tier || 'unchecked';
|
|
494
|
-
(tiers[tier] = tiers[tier] || []).push(r.envelope);
|
|
495
|
-
}
|
|
496
|
-
io.print(' ◉ coupon tiers:');
|
|
514
|
+
// tier files were written INCREMENTALLY during the run (survives any exit).
|
|
515
|
+
io.print(` ◉ coupon tiers (saved live in ${tierDir}):`);
|
|
497
516
|
for (const tier of ['rs100', 'rs75', 'rs50', 'normal', 'unchecked']) {
|
|
498
|
-
if (!
|
|
499
|
-
|
|
500
|
-
const dest = join(dir, `${tier}.txt`);
|
|
501
|
-
mkdirSync(dir, { recursive: true });
|
|
502
|
-
// one envelope per block, separated by a blank line — readable + paste-friendly
|
|
503
|
-
writeFileSync(dest, tiers[tier].join('\n\n') + '\n');
|
|
504
|
-
io.print(` ${TIER_LABEL[tier]} (${tiers[tier].length}) → ${dest}`, 'accent');
|
|
517
|
+
if (!savedTiers[tier]) continue;
|
|
518
|
+
io.print(` ${TIER_LABEL[tier]} (${savedTiers[tier]}) → ${join(tierDir, `${tier}.txt`)}`, 'accent');
|
|
505
519
|
}
|
|
506
520
|
} else {
|
|
507
521
|
io.print(' ◉ ZAUTH1 envelopes:');
|
|
508
|
-
for (const r of
|
|
509
|
-
if (r.status === 'success') io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
|
|
510
|
-
}
|
|
522
|
+
for (const r of succeeded) io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
|
|
511
523
|
}
|
|
512
524
|
} else {
|
|
513
525
|
io.print(' ◉ no sessions extracted — nothing to save.');
|
|
514
526
|
}
|
|
515
527
|
}
|
|
516
528
|
|
|
529
|
+
// ── BigBasket: local OTP login → session JSON ────────────────────────────────
|
|
530
|
+
// Manual mode (built now): enter a number → SMS an OTP locally → type the code →
|
|
531
|
+
// verify → write {phone}-{timestamp}.json with { phone, bbAuthToken, mId, bbVisitorId }.
|
|
532
|
+
// LOOPS until Esc. Runs entirely on your IP. Auto mode lands once the server is wired.
|
|
533
|
+
|
|
534
|
+
// Resolve the base dir for BigBasket session output.
|
|
535
|
+
function bigbasketBaseDir(args) {
|
|
536
|
+
if (args.out) {
|
|
537
|
+
const expanded = args.out.startsWith('~') ? join(homedir(), args.out.slice(1)) : args.out;
|
|
538
|
+
return /\.(json|txt)$/i.test(expanded) ? dirname(expanded) : expanded;
|
|
539
|
+
}
|
|
540
|
+
const downloads = join(homedir(), 'Downloads');
|
|
541
|
+
return join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'bigbasket');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Save one session as a per-number JSON. Returns the path written.
|
|
545
|
+
function saveBigbasketSession(args, number, sessionKeys) {
|
|
546
|
+
const baseDir = bigbasketBaseDir(args);
|
|
547
|
+
mkdirSync(baseDir, { recursive: true });
|
|
548
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
549
|
+
const dest = join(baseDir, `${number}-${stamp}.json`);
|
|
550
|
+
writeFileSync(dest, JSON.stringify(sessionKeys, null, 2) + '\n');
|
|
551
|
+
return dest;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export async function bigbasketCmd(io, api, args = {}) {
|
|
555
|
+
// Only manual exists today; auto is wired after the server contract is known.
|
|
556
|
+
const mode = args.auto ? 'auto' : 'manual';
|
|
557
|
+
if (mode === 'auto') {
|
|
558
|
+
io.print(' ! BigBasket auto mode is not wired yet — coming once the server is set.', 'danger');
|
|
559
|
+
io.print(' ◉ use manual for now: scripterx bigbasket --manual', 'accent');
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
io.print(' ◉ BigBasket — local OTP → session JSON (Manual)');
|
|
564
|
+
io.print(' ◉ press esc to cancel', 'accent');
|
|
565
|
+
|
|
566
|
+
let done = 0;
|
|
567
|
+
let lastSig = 0, stopping = false;
|
|
568
|
+
const onSigint = () => {
|
|
569
|
+
const now = Date.now();
|
|
570
|
+
if (now - lastSig < 2000) { stopping = true; process.exit(0); }
|
|
571
|
+
lastSig = now;
|
|
572
|
+
io.print(' ⚠ press Ctrl+C again to exit');
|
|
573
|
+
};
|
|
574
|
+
const interactive = !!io.startRun; // ink App owns its own Ctrl+C
|
|
575
|
+
if (!interactive) process.on('SIGINT', onSigint);
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
for (;;) {
|
|
579
|
+
// number entry — Esc stops the whole loop
|
|
580
|
+
const raw = args.number || await io.ask('bigbasket phone number', { escapable: true });
|
|
581
|
+
if (raw === CANCEL) break;
|
|
582
|
+
const number = String(raw).replace(/\D/g, '').slice(-10);
|
|
583
|
+
if (number.length !== 10) { io.print(' ! enter a 10-digit number', 'danger'); if (args.number) break; continue; }
|
|
584
|
+
|
|
585
|
+
try {
|
|
586
|
+
const session = bigbasket.newSession(number);
|
|
587
|
+
io.print(` ◉ sending OTP to ${number} …`);
|
|
588
|
+
const s = await bigbasket.sendOtp(session);
|
|
589
|
+
if (!s.ok) { io.print(` ✗ could not send OTP: ${s.msg || s.status}`, 'danger'); if (args.number) break; continue; }
|
|
590
|
+
io.print(' ✓ OTP sent');
|
|
591
|
+
|
|
592
|
+
// OTP entry — Esc abandons this number and loops back
|
|
593
|
+
const otpRaw = args.otp || await io.ask('enter the OTP', { escapable: true });
|
|
594
|
+
if (otpRaw === CANCEL) break;
|
|
595
|
+
const v = await bigbasket.verifyOtp(session, String(otpRaw).trim());
|
|
596
|
+
if (!v.ok) { io.print(` ✗ OTP verify failed: ${v.error || v.status}`, 'danger'); if (args.number) break; continue; }
|
|
597
|
+
io.print(` ✓ logged in${v.user?.first_name ? ` as ${v.user.first_name}` : ''}`);
|
|
598
|
+
|
|
599
|
+
const keys = bigbasket.toSessionKeys(session);
|
|
600
|
+
const dest = saveBigbasketSession(args, number, keys);
|
|
601
|
+
done++;
|
|
602
|
+
io.print(' ✓ session saved to:');
|
|
603
|
+
io.print(` ${dest}`, 'accent');
|
|
604
|
+
io.print(` bbAuthToken=${(keys.bbAuthToken || '').slice(0, 24)}… mId=${keys.mId} bbVisitorId=${keys.bbVisitorId}`, 'accent');
|
|
605
|
+
} catch (e) {
|
|
606
|
+
io.print(` ✗ ${number}: ${e.message}`, 'danger');
|
|
607
|
+
if (args.number) break;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (args.number) break; // one-shot: do exactly one and stop
|
|
612
|
+
io.print(' ◉ next number (esc to finish)', 'accent');
|
|
613
|
+
}
|
|
614
|
+
} finally {
|
|
615
|
+
if (!interactive && !stopping) process.off('SIGINT', onSigint);
|
|
616
|
+
}
|
|
617
|
+
io.print(` ✓ done — ${done} session(s) saved.`);
|
|
618
|
+
}
|
|
619
|
+
|
|
517
620
|
const TIER_LABEL = { rs100: '🟢 ₹100', rs75: '🟢 ₹75', rs50: '🟢 ₹50', normal: '⚪ normal', unchecked: '❔ unchecked' };
|
|
518
621
|
|
|
519
622
|
// The directory the per-tier files go in for an auto run.
|
|
@@ -695,7 +798,7 @@ export async function configCmd(io, _api, args = {}) {
|
|
|
695
798
|
}
|
|
696
799
|
|
|
697
800
|
export function help(io) {
|
|
698
|
-
io.print(' commands: run · zepto · recheck · campaigns · export · balance · creds · stop · delete · whoami · config · update · logout · exit');
|
|
801
|
+
io.print(' commands: run · zepto · bigbasket · recheck · campaigns · export · balance · creds · stop · delete · whoami · config · update · logout · exit');
|
|
699
802
|
}
|
|
700
803
|
|
|
701
804
|
export async function update(io) {
|
|
@@ -721,6 +824,6 @@ export async function update(io) {
|
|
|
721
824
|
|
|
722
825
|
// dispatch table used by the interactive shell
|
|
723
826
|
export const REGISTRY = {
|
|
724
|
-
run, zepto: zeptoCmd, recheck, campaigns, export: exportCmd, balance, creds, stop, delete: del,
|
|
827
|
+
run, zepto: zeptoCmd, bigbasket: bigbasketCmd, recheck, campaigns, export: exportCmd, balance, creds, stop, delete: del,
|
|
725
828
|
whoami, login, logout, config: configCmd, update, help,
|
|
726
829
|
};
|
package/src/index.js
CHANGED
|
@@ -54,6 +54,9 @@ const HELP = `${paint.bold('scripterx')} — local Flipkart session extractor
|
|
|
54
54
|
--auto | --manual --count N --concurrency N --out <file|dir> --cert <64hex>
|
|
55
55
|
--number <10-digit> --otp <code>
|
|
56
56
|
(envelope keyed to ZeptoAuthManager cert; override with --cert or $ZAUTH_CERT_SHA)
|
|
57
|
+
scripterx bigbasket [flags] local BigBasket OTP login → session JSON (alias: bb)
|
|
58
|
+
--manual --number <10-digit> --otp <code> --out <file|dir>
|
|
59
|
+
→ writes {phone, bbAuthToken, mId, bbVisitorId} per number (auto mode soon)
|
|
57
60
|
scripterx recheck <file> re-validate Minutes status of a session JSON, locally (your IP)
|
|
58
61
|
→ re-splits into corrected -minutes-free / -minutes-used files + stats
|
|
59
62
|
scripterx campaigns | export [name] | balance | creds | stop | delete | whoami | config
|
|
@@ -106,7 +109,7 @@ async function main() {
|
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
// ── one-shot mode ──
|
|
109
|
-
const map = { run: 'run', zepto: 'zepto', recheck: 'recheck', campaigns: 'campaigns', export: 'export', balance: 'balance',
|
|
112
|
+
const map = { run: 'run', zepto: 'zepto', bigbasket: 'bigbasket', bb: 'bigbasket', recheck: 'recheck', campaigns: 'campaigns', export: 'export', balance: 'balance',
|
|
110
113
|
creds: 'creds', stop: 'stop', delete: 'delete', whoami: 'whoami', login: 'login',
|
|
111
114
|
logout: 'logout', config: 'config', update: 'update', help: 'help' };
|
|
112
115
|
const fnName = map[cmd];
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// BigBasket local login provider — send an OTP to a phone number and verify it,
|
|
2
|
+
// straight against BigBasket's native API (runs on YOUR IP, no emulator, no backend).
|
|
3
|
+
//
|
|
4
|
+
// Flow (reverse-engineered from the native app; the login screen is Flutter, so its
|
|
5
|
+
// OTP calls don't show in a proxy — but they're plain native endpoints we can call):
|
|
6
|
+
// 1. sendOtp(session) -> register device + harvest csrf/csurf, then SMS an OTP.
|
|
7
|
+
// 2. verifyOtp(session, otp) -> unified-login returns { bb_token, m_id, visitor_id }.
|
|
8
|
+
//
|
|
9
|
+
// IMPORTANT: the native /member-tdl/v3/member/otp/ endpoint needs **no reCAPTCHA**.
|
|
10
|
+
// Bootstrap (register/device + ui-svc/header) is what mints the csrftoken + csurftoken
|
|
11
|
+
// the OTP/verify POSTs require. Verify body uses mobile_no + mobile_no_otp + refId +
|
|
12
|
+
// source:"BIGBASKET" (NOT identifier/referrer — that 400s with HU4000).
|
|
13
|
+
//
|
|
14
|
+
// Same shape as providers/zepto.js so the CLI command can mirror zeptoCmd exactly.
|
|
15
|
+
import https from 'node:https';
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
import { hostname } from 'node:os';
|
|
18
|
+
import * as throttle from '../throttle.js';
|
|
19
|
+
|
|
20
|
+
const HOST = 'www.bigbasket.com';
|
|
21
|
+
const APP_VERSION = '8.13.0';
|
|
22
|
+
const BUILD_CODE = '24105210';
|
|
23
|
+
const OS_RELEASE = '13';
|
|
24
|
+
|
|
25
|
+
const uuid = () => crypto.randomUUID();
|
|
26
|
+
// stable per-machine device id (the app keeps one forever in SharedPreferences)
|
|
27
|
+
function deviceId() {
|
|
28
|
+
const seed = hostname() + '|scripterx-bb-android';
|
|
29
|
+
return crypto.createHash('sha1').update(seed).digest('hex').slice(0, 16);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── cookie jar (the app quotes some identity cookies) ─────────────────────────
|
|
33
|
+
const QUOTED = new Set(['_bb_source', '_bb_vid', 'BBAUTHTOKEN', '_bb_mid']);
|
|
34
|
+
function cookieHeader(jar) {
|
|
35
|
+
jar._bb_source = 'app'; // marks the session as the Android app
|
|
36
|
+
return Object.entries(jar)
|
|
37
|
+
.filter(([, v]) => v != null && v !== '')
|
|
38
|
+
.map(([k, v]) => (QUOTED.has(k) ? `${k}="${v}"` : `${k}=${v}`))
|
|
39
|
+
.join('; ');
|
|
40
|
+
}
|
|
41
|
+
function absorb(jar, setCookie) {
|
|
42
|
+
(setCookie || []).forEach((c) => {
|
|
43
|
+
const kv = c.split(';')[0];
|
|
44
|
+
const i = kv.indexOf('=');
|
|
45
|
+
if (i > 0) {
|
|
46
|
+
const name = kv.slice(0, i).trim();
|
|
47
|
+
const val = kv.slice(i + 1).trim().replace(/^"(.*)"$/, '$1');
|
|
48
|
+
if (val && val.toLowerCase() !== 'deleted') jar[name] = val;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── native header builder (mirrors the two OkHttp interceptors) ───────────────
|
|
54
|
+
function headers(session, method, extra = {}) {
|
|
55
|
+
const h = {
|
|
56
|
+
Accept: 'application/json, text/plain, */*',
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
'User-Agent': `BB Android/v${APP_VERSION}/os ${OS_RELEASE}`,
|
|
59
|
+
'X-Channel': 'BB-Android',
|
|
60
|
+
'X-Caller': 'Monster-SVC',
|
|
61
|
+
'X-Tracker': uuid(),
|
|
62
|
+
'X-Tcp-Platform': 'native',
|
|
63
|
+
'X-Tcp-Device-Version': `android_${APP_VERSION}_${BUILD_CODE}`,
|
|
64
|
+
'common-client-static-version': '101',
|
|
65
|
+
'X-Entry-Context': 'bbnow',
|
|
66
|
+
'X-Entry-Context-Id': '10',
|
|
67
|
+
'x-device-id': session.deviceId,
|
|
68
|
+
'x-device-model': 'Google sdk_gphone64_arm64',
|
|
69
|
+
'x-is-debug': 'false',
|
|
70
|
+
'X-Pharma': 'true',
|
|
71
|
+
Cookie: cookieHeader(session.jar),
|
|
72
|
+
};
|
|
73
|
+
// csurftoken/csrftoken only on mutating requests (ApiCallInterceptor.addCsrfToken)
|
|
74
|
+
const m = method.toUpperCase();
|
|
75
|
+
if (m === 'POST' || m === 'PUT' || m === 'DELETE' || m === 'PATCH') {
|
|
76
|
+
if (session.jar.csurftoken) h['x-csurftoken'] = session.jar.csurftoken;
|
|
77
|
+
if (session.jar.csrftoken) h['x-csrftoken'] = session.jar.csrftoken;
|
|
78
|
+
}
|
|
79
|
+
return { ...h, ...extra };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function req(session, method, path, body, extra = {}) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const payload = body == null ? '' : typeof body === 'string' ? body : JSON.stringify(body);
|
|
85
|
+
const h = { ...headers(session, method, extra), host: HOST };
|
|
86
|
+
if (payload) h['Content-Length'] = Buffer.byteLength(payload);
|
|
87
|
+
const r = https.request({ hostname: HOST, port: 443, method, path, headers: h, timeout: 20000 }, (res) => {
|
|
88
|
+
const ch = [];
|
|
89
|
+
res.on('data', (c) => ch.push(c));
|
|
90
|
+
res.on('end', () => {
|
|
91
|
+
absorb(session.jar, res.headers['set-cookie']);
|
|
92
|
+
const text = Buffer.concat(ch).toString('utf8');
|
|
93
|
+
let json = null;
|
|
94
|
+
try { json = JSON.parse(text); } catch { /* not json */ }
|
|
95
|
+
resolve({ status: res.statusCode, text, json });
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
r.on('error', (e) => resolve({ status: 0, text: 'err:' + e.message, json: null }));
|
|
99
|
+
r.on('timeout', () => { r.destroy(); resolve({ status: 0, text: 'timeout', json: null }); });
|
|
100
|
+
if (payload) r.write(payload);
|
|
101
|
+
r.end();
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// A fresh session = a new cookie jar + a stable device identity.
|
|
106
|
+
export function newSession(number) {
|
|
107
|
+
return {
|
|
108
|
+
jar: {},
|
|
109
|
+
deviceId: deviceId(),
|
|
110
|
+
refId: null,
|
|
111
|
+
mobile: String(number).replace(/\D/g, '').slice(-10),
|
|
112
|
+
token: null, // bb_token (BBAUTHTOKEN JWT)
|
|
113
|
+
mId: null, // base64 member id (server-provided)
|
|
114
|
+
visitorId: null, // base64 visitor id (server-provided)
|
|
115
|
+
user: null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Step 1 — bootstrap (register device + header) then request the OTP SMS.
|
|
120
|
+
// Returns { ok, status, msg }. Stores session.refId on success.
|
|
121
|
+
export async function sendOtp(session) {
|
|
122
|
+
// (a) register device → csrftoken + _bb_vid
|
|
123
|
+
const form =
|
|
124
|
+
'imei=02%3A00%3A00%3A00%3A00%3A00&device_id=' + session.deviceId + '&city_id=1&properties=' +
|
|
125
|
+
encodeURIComponent(JSON.stringify({
|
|
126
|
+
platform: 'java', os_name: 'android', os_version: OS_RELEASE, app_version: APP_VERSION,
|
|
127
|
+
device_make: 'Google', device_model: 'sdk_gphone64_arm64', screen_resolution: '1080X2201', screen_dpi: 420,
|
|
128
|
+
}));
|
|
129
|
+
await throttle.wait();
|
|
130
|
+
await req(session, 'POST', '/mapi/v4.2.0/register/device/', form, { 'Content-Type': 'application/x-www-form-urlencoded' });
|
|
131
|
+
|
|
132
|
+
// (b) header → csurftoken
|
|
133
|
+
await throttle.wait();
|
|
134
|
+
await req(session, 'GET', '/ui-svc/v2/header/?send_door_info=true');
|
|
135
|
+
|
|
136
|
+
if (!session.jar.csurftoken) {
|
|
137
|
+
return { ok: false, status: 0, msg: 'no csurftoken after bootstrap (blocked?)' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// (c) send OTP
|
|
141
|
+
await throttle.wait();
|
|
142
|
+
const r = await req(session, 'POST', '/member-tdl/v3/member/otp/',
|
|
143
|
+
{ identifier: session.mobile, referrer: 'unified_login' },
|
|
144
|
+
{ 'X-Login-Origin': 'unified_login' });
|
|
145
|
+
|
|
146
|
+
const refId = r.json && (r.json.refId || r.json.ref_id);
|
|
147
|
+
const ok = r.status === 200 && !!refId;
|
|
148
|
+
if (ok) session.refId = refId;
|
|
149
|
+
return { ok, status: r.status, msg: ok ? (r.json.message || 'OTP sent') : (r.json?.message || r.text.slice(0, 160)) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Step 2 — verify the OTP. On success, fills session.token / mId / visitorId / user.
|
|
153
|
+
export async function verifyOtp(session, otp) {
|
|
154
|
+
if (!session.refId) return { ok: false, status: 0, error: 'no refId — call sendOtp first' };
|
|
155
|
+
await throttle.wait();
|
|
156
|
+
const r = await req(session, 'POST', '/member-tdl/v3/member/unified-login/',
|
|
157
|
+
{ mobile_no: session.mobile, mobile_no_otp: String(otp).trim(), refId: session.refId, source: 'BIGBASKET' },
|
|
158
|
+
{ 'X-Login-Origin': 'unified_login', 'X-tcp-client-id': 'BIGBASKET-ANDROID-APP' });
|
|
159
|
+
|
|
160
|
+
// Native unified-login returns { bb_token, m_id, visitor_id }.
|
|
161
|
+
const J = r.json || {};
|
|
162
|
+
const token = J.bb_token || J.BBAUTHTOKEN || session.jar.BBAUTHTOKEN || J.token;
|
|
163
|
+
if (r.status === 200 && token) {
|
|
164
|
+
session.token = token;
|
|
165
|
+
session.mId = J.m_id || session.jar._bb_mid || null;
|
|
166
|
+
session.visitorId = J.visitor_id || session.jar._bb_vid || null;
|
|
167
|
+
session.user = J.member_details || J.user || null;
|
|
168
|
+
return { ok: true, user: session.user };
|
|
169
|
+
}
|
|
170
|
+
// HU4000 / wrong-otp surface here
|
|
171
|
+
const err = J.errors?.[0]?.msg || J.message || r.text.slice(0, 160);
|
|
172
|
+
return { ok: false, status: r.status, error: err };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Build the persisted-session object — the requested {bbAuthToken, mId, bbVisitorId}
|
|
176
|
+
// shape, plus phone for traceability.
|
|
177
|
+
export function toSessionKeys(session) {
|
|
178
|
+
return {
|
|
179
|
+
phone: session.mobile,
|
|
180
|
+
bbAuthToken: session.token || '',
|
|
181
|
+
mId: session.mId || '',
|
|
182
|
+
bbVisitorId: session.visitorId || '',
|
|
183
|
+
};
|
|
184
|
+
}
|
package/src/ui/RunView.js
CHANGED
|
@@ -30,10 +30,28 @@ function SlotRow({ slot, mobile, phase, detail, wait }) {
|
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Per Zepto coupon tier → a colored note for the live results row.
|
|
34
|
+
function couponNote(tier, label) {
|
|
35
|
+
const C = COLORS;
|
|
36
|
+
switch (tier) {
|
|
37
|
+
case 'rs100': return { note: '₹100 coupon', color: C.success };
|
|
38
|
+
case 'rs75': return { note: '₹75 coupon', color: C.success };
|
|
39
|
+
case 'rs50': return { note: '₹50 coupon', color: C.success };
|
|
40
|
+
case 'normal': return { note: 'no coupon', color: C.muted };
|
|
41
|
+
case 'unchecked': return { note: 'coupon unchecked', color: C.warn };
|
|
42
|
+
default: return { note: label || 'extracted', color: 'gray' };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
// Map a result to a clean { glyph, label, color, note } for display. Driven by the
|
|
34
47
|
// worker's `category` so every outcome reads consistently (no "successed"/"success" mix).
|
|
35
|
-
function resultDisplay({ status, category, detail }) {
|
|
48
|
+
function resultDisplay({ status, category, detail, couponTier, couponLabel }) {
|
|
36
49
|
const C = COLORS;
|
|
50
|
+
// Zepto coupon-tier success rows: show the EXACT coupon the account has.
|
|
51
|
+
if (status === 'success' && couponTier) {
|
|
52
|
+
const cn = couponNote(couponTier, couponLabel);
|
|
53
|
+
return { glyph: '✓', label: 'success', color: C.success, note: cn.note, noteColor: cn.color };
|
|
54
|
+
}
|
|
37
55
|
switch (category) {
|
|
38
56
|
case 'fresh': return { glyph: '🆕', label: 'fresh account', color: C.accent, note: 'new account created' };
|
|
39
57
|
case 'coupon': return { glyph: '✓', label: 'success', color: C.success, note: '₹100 coupon available' };
|
|
@@ -52,13 +70,13 @@ function resultDisplay({ status, category, detail }) {
|
|
|
52
70
|
}
|
|
53
71
|
}
|
|
54
72
|
|
|
55
|
-
function ResultRow({ status, category, mobile, detail, cost }) {
|
|
56
|
-
const d = resultDisplay({ status, category, detail });
|
|
73
|
+
function ResultRow({ status, category, mobile, detail, cost, couponTier, couponLabel }) {
|
|
74
|
+
const d = resultDisplay({ status, category, detail, couponTier, couponLabel });
|
|
57
75
|
const mob = (mobile || '—').slice(-10) || '—';
|
|
58
76
|
return h(Box, null,
|
|
59
77
|
h(Box, { width: 13 }, h(Text, null, mob)),
|
|
60
78
|
h(Box, { width: 14 }, h(Text, { color: d.color }, `${d.glyph} ${d.label}`)),
|
|
61
|
-
h(Box, { flexGrow: 1 }, h(Text, { color: 'gray' }, d.note)),
|
|
79
|
+
h(Box, { flexGrow: 1 }, h(Text, { color: d.noteColor || 'gray' }, d.note)),
|
|
62
80
|
h(Box, { width: 7, justifyContent: 'flex-end' }, h(Text, { color: 'gray' }, cost ? `₹${cost}` : '')),
|
|
63
81
|
);
|
|
64
82
|
}
|
|
@@ -97,7 +115,7 @@ export function RunView({ controller }) {
|
|
|
97
115
|
slotList.length ? slotList.map((sl) => h(SlotRow, { key: sl.slot, ...sl }))
|
|
98
116
|
: h(Text, { color: 'gray' }, 'starting…')),
|
|
99
117
|
h(Panel, { title: 'results' },
|
|
100
|
-
lastRows.length ? lastRows.map((r, i) => h(ResultRow, { key: i, status: r.status, category: r.category, mobile: r.mobile, detail: r.detail, cost: r.cost }))
|
|
118
|
+
lastRows.length ? lastRows.map((r, i) => h(ResultRow, { key: i, status: r.status, category: r.category, mobile: r.mobile, detail: r.detail, cost: r.cost, couponTier: r.coupon_tier, couponLabel: r.coupon_label }))
|
|
101
119
|
: h(Text, { color: 'gray' }, 'waiting for first result…')),
|
|
102
120
|
h(Box, { borderStyle: 'round', borderColor: COLORS.accent, paddingX: 1 },
|
|
103
121
|
h(Text, { color: COLORS.success }, `✓ ${stats.succeeded}`),
|
package/src/ui/Shell.js
CHANGED
|
@@ -13,6 +13,7 @@ const h = React.createElement;
|
|
|
13
13
|
|
|
14
14
|
export const COMMANDS = [
|
|
15
15
|
{ name: 'run', desc: 'run an extraction' },
|
|
16
|
+
{ name: 'bigbasket', desc: 'local BigBasket OTP login → session JSON' },
|
|
16
17
|
{ name: 'campaigns', desc: 'list your campaigns' },
|
|
17
18
|
{ name: 'export', desc: "download a campaign's sessions" },
|
|
18
19
|
{ name: 'balance', desc: 'check provider balance' },
|
package/src/worker.js
CHANGED
|
@@ -555,13 +555,14 @@ async function releaseNumber(provider, number, rentedAt, { log = () => {}, warn
|
|
|
555
555
|
export const __releaseNumberForTest = releaseNumber;
|
|
556
556
|
|
|
557
557
|
export class ZeptoWorker {
|
|
558
|
-
constructor(provider, { requested, concurrency, certSha, onEvent, checkCoupon }) {
|
|
558
|
+
constructor(provider, { requested, concurrency, certSha, onEvent, onResult, checkCoupon }) {
|
|
559
559
|
this.provider = provider;
|
|
560
560
|
this.requested = requested;
|
|
561
561
|
this.concurrency = Math.max(1, Math.min(concurrency, requested));
|
|
562
562
|
this.certSha = certSha;
|
|
563
563
|
this.checkCoupon = !!checkCoupon; // also classify the ₹50/₹75/₹100 coupon tier
|
|
564
564
|
this.onEvent = onEvent || (() => {});
|
|
565
|
+
this.onResult = onResult || null; // called per successful result for incremental save
|
|
565
566
|
// Surface OTPCart re-logins (session taken over by another login) in the UI.
|
|
566
567
|
if (provider && 'onNotice' in provider) provider.onNotice = (msg) => this.onEvent('warn', `⚠ ${msg}`);
|
|
567
568
|
this.stats = { total: requested, succeeded: 0, failed: 0, cancelled: 0, generated: 0, attempts: 0, charges: 0 };
|
|
@@ -595,6 +596,11 @@ export class ZeptoWorker {
|
|
|
595
596
|
else { this.stats.failed++; this.stats.charges += res.cost || 0; }
|
|
596
597
|
this.onEvent('progress', this.stats);
|
|
597
598
|
this.onEvent('row', res);
|
|
599
|
+
// Persist each successful envelope IMMEDIATELY (incremental save) so a mid-run
|
|
600
|
+
// stop / double-Ctrl+C / crash never loses an already-extracted account.
|
|
601
|
+
if (res.status === 'success' && res.envelope && this.onResult) {
|
|
602
|
+
try { this.onResult(res); } catch { /* best-effort; never break the loop */ }
|
|
603
|
+
}
|
|
598
604
|
}
|
|
599
605
|
|
|
600
606
|
async run() {
|