scripter-x 1.0.30 → 1.0.32

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripter-x",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "ScripterX — local Flipkart session extractor (runs on your residential IP, syncs to marthunt)",
5
5
  "type": "module",
6
6
  "bin": {
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 (the backend campaign flow) or Zepto (local OTP → ZAUTH1 envelope).
133
- const target = args.target || (args.number ? 'zepto' : (await io.select('what to extract?', [
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)' }])).value);
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,37 +498,207 @@ 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
- if (worker.stats.succeeded > 0) {
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
- const tiers = {};
491
- for (const r of results) {
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 (!tiers[tier]?.length) continue;
499
- const dir = zeptoTierDir(args, name);
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 results) {
509
- if (r.status === 'success') io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
522
+ for (const r of succeeded) io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
523
+ }
524
+ } else {
525
+ io.print(' ◉ no sessions extracted — nothing to save.');
526
+ }
527
+ }
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
+ const mode = args.manual ? 'manual' : (args.auto ? 'auto' : (args.number ? 'manual' : (await io.select('Choose BigBasket login flow', [
556
+ { label: 'Auto', value: 'auto', description: 'Auto-rent numbers via TempOTP (hands-off)' },
557
+ { label: 'Manual', value: 'manual', description: 'Enter phone + type OTP yourself' },
558
+ ])).value || 'auto'));
559
+
560
+ if (mode === 'auto') return bigbasketAuto(io, args);
561
+
562
+ io.print(' ◉ BigBasket — local OTP → session JSON (Manual)');
563
+ io.print(' ◉ press esc to cancel', 'accent');
564
+
565
+ let done = 0;
566
+ let lastSig = 0, stopping = false;
567
+ const onSigint = () => {
568
+ const now = Date.now();
569
+ if (now - lastSig < 2000) { stopping = true; process.exit(0); }
570
+ lastSig = now;
571
+ io.print(' ⚠ press Ctrl+C again to exit');
572
+ };
573
+ const interactive = !!io.startRun; // ink App owns its own Ctrl+C
574
+ if (!interactive) process.on('SIGINT', onSigint);
575
+
576
+ try {
577
+ for (;;) {
578
+ // number entry — Esc stops the whole loop
579
+ const raw = args.number || await io.ask('bigbasket phone number', { escapable: true });
580
+ if (raw === CANCEL) break;
581
+ const number = String(raw).replace(/\D/g, '').slice(-10);
582
+ if (number.length !== 10) { io.print(' ! enter a 10-digit number', 'danger'); if (args.number) break; continue; }
583
+
584
+ try {
585
+ const session = bigbasket.newSession(number);
586
+ io.print(` ◉ sending OTP to ${number} …`);
587
+ const s = await bigbasket.sendOtp(session);
588
+ if (!s.ok) { io.print(` ✗ could not send OTP: ${s.msg || s.status}`, 'danger'); if (args.number) break; continue; }
589
+ io.print(' ✓ OTP sent');
590
+
591
+ // OTP entry — Esc abandons this number and loops back
592
+ const otpRaw = args.otp || await io.ask('enter the OTP', { escapable: true });
593
+ if (otpRaw === CANCEL) break;
594
+ const v = await bigbasket.verifyOtp(session, String(otpRaw).trim());
595
+ if (!v.ok) { io.print(` ✗ OTP verify failed: ${v.error || v.status}`, 'danger'); if (args.number) break; continue; }
596
+ io.print(` ✓ logged in${v.user?.first_name ? ` as ${v.user.first_name}` : ''}`);
597
+
598
+ const keys = bigbasket.toSessionKeys(session);
599
+ const dest = saveBigbasketSession(args, number, keys);
600
+ done++;
601
+ io.print(' ✓ session saved to:');
602
+ io.print(` ${dest}`, 'accent');
603
+ io.print(` bbAuthToken=${(keys.bbAuthToken || '').slice(0, 24)}… mId=${keys.mId} bbVisitorId=${keys.bbVisitorId}`, 'accent');
604
+ } catch (e) {
605
+ io.print(` ✗ ${number}: ${e.message}`, 'danger');
606
+ if (args.number) break;
607
+ continue;
510
608
  }
609
+
610
+ if (args.number) break; // one-shot: do exactly one and stop
611
+ io.print(' ◉ next number (esc to finish)', 'accent');
511
612
  }
613
+ } finally {
614
+ if (!interactive && !stopping) process.off('SIGINT', onSigint);
615
+ }
616
+ io.print(` ✓ done — ${done} session(s) saved.`);
617
+ }
618
+
619
+ // ── BigBasket auto: rent numbers via TempOTP, poll SMS, verify headlessly ─────
620
+ // Mirrors the Zepto auto flow. Local-first: every success is appended to a combined
621
+ // file the instant it lands (so a mid-run stop never loses an account).
622
+ async function bigbasketAuto(io, args = {}) {
623
+ io.print(' ◉ BigBasket — auto extraction via TempOTP');
624
+ const cfg = config.load();
625
+
626
+ // TempOTP key (reuse the saved-key / add-new pattern from buildProvider).
627
+ let key = cfg.tempotp_api_key;
628
+ if (key) {
629
+ const choice = await io.select(`saved TempOTP key: ${maskSecret(key)}`, [
630
+ { label: 'use saved', value: 'use', description: maskSecret(key) },
631
+ { label: 'add new', value: 'new', description: 'enter a different key' }]);
632
+ if ((choice.value || choice) === 'new') key = null;
633
+ }
634
+ let freshlyEntered = false;
635
+ if (!key) { key = await io.ask('TempOTP API key'); freshlyEntered = true; }
636
+ const bal = await tp.balance(key);
637
+ if (bal == null) { io.print(' ✗ TempOTP key invalid or unreachable', 'danger'); return; }
638
+ io.print(` ✓ TempOTP balance: ₹${bal}`);
639
+ if (freshlyEntered && await io.confirm('save this key locally for next time?', true)) config.setMany({ tempotp_api_key: key });
640
+
641
+ // BigBasket service picker (only the BB service ids).
642
+ const svc = args.service || (await io.select('BigBasket service', tp.BIGBASKET_SERVICES.map((id) => ({
643
+ label: `${id} · ${tp.SERVICE_NAMES[id]}`, value: id, description: `₹${tp.SERVICES[id]}`,
644
+ })))).value;
645
+ const provider = new tp.TempOTPProvider(key, svc);
646
+
647
+ const count = args.count || Number(await io.ask('how many sessions?', { dflt: '1' }));
648
+ const concurrency = args.concurrency || Number(await io.ask('concurrency (keep low — 1 recommended)', { dflt: '1' }));
649
+ const name = args.name || `BigBasket-${Math.floor(Date.now() / 1000)}`;
650
+ if (!(await io.confirm(`start ${count} BigBasket extraction(s) at concurrency ${concurrency} on service ${svc}?`, true))) return;
651
+
652
+ const { BigBasketWorker } = await import('./worker.js');
653
+ const { RunController } = await import('./controller.js');
654
+ const controller = new RunController({ name, provider: `tempotp-bigbasket`, requested: count });
655
+
656
+ // INCREMENTAL SAVE: append every extracted session to one JSONL the instant it lands.
657
+ const baseDir = bigbasketBaseDir(args);
658
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
659
+ const liveFile = join(baseDir, `${name}-${stamp}.jsonl`);
660
+ const onResult = (res) => {
661
+ if (res.status !== 'success' || !res.session) return;
662
+ mkdirSync(baseDir, { recursive: true });
663
+ appendFileSync(liveFile, JSON.stringify(res.session) + '\n');
664
+ };
665
+
666
+ const worker = new BigBasketWorker(provider, { requested: count, concurrency, onEvent: controller.handleEvent, onResult });
667
+ controller.stop = () => worker.stop();
668
+
669
+ const results = [];
670
+ const origRecord = worker._record.bind(worker);
671
+ worker._record = (res) => { results.push(res); origRecord(res); };
672
+
673
+ const interactive = !!io.startRun;
674
+ if (interactive) io.startRun(controller);
675
+ let onSigint = null;
676
+ if (!interactive) {
677
+ let lastSig = 0;
678
+ onSigint = () => {
679
+ const now = Date.now();
680
+ if (now - lastSig < 2000) { worker.stop(); process.exit(0); }
681
+ lastSig = now; worker.stop();
682
+ io.print(' ⚠ stopping — press Ctrl+C again to exit');
683
+ };
684
+ process.on('SIGINT', onSigint);
685
+ }
686
+
687
+ await worker.run();
688
+ if (onSigint) process.off('SIGINT', onSigint);
689
+ if (io.endRun) io.endRun();
690
+
691
+ io.print(` ✓ done — ${worker.stats.succeeded} succeeded · ${worker.stats.failed} failed · ${worker.stats.cancelled} cancelled · ₹${worker.stats.charges} spent`);
692
+
693
+ // Also write a combined pretty JSON array of all successes for convenience.
694
+ const succeeded = results.filter((r) => r.status === 'success' && r.session).map((r) => r.session);
695
+ if (succeeded.length) {
696
+ mkdirSync(baseDir, { recursive: true });
697
+ const combined = join(baseDir, `${name}-${stamp}.json`);
698
+ writeFileSync(combined, JSON.stringify(succeeded, null, 2) + '\n');
699
+ io.print(` ✓ saved ${succeeded.length} session(s):`);
700
+ io.print(` ${combined}`, 'accent');
701
+ io.print(` (live JSONL: ${liveFile})`, 'accent');
512
702
  } else {
513
703
  io.print(' ◉ no sessions extracted — nothing to save.');
514
704
  }
@@ -695,7 +885,7 @@ export async function configCmd(io, _api, args = {}) {
695
885
  }
696
886
 
697
887
  export function help(io) {
698
- io.print(' commands: run · zepto · recheck · campaigns · export · balance · creds · stop · delete · whoami · config · update · logout · exit');
888
+ io.print(' commands: run · zepto · bigbasket · recheck · campaigns · export · balance · creds · stop · delete · whoami · config · update · logout · exit');
699
889
  }
700
890
 
701
891
  export async function update(io) {
@@ -721,6 +911,6 @@ export async function update(io) {
721
911
 
722
912
  // dispatch table used by the interactive shell
723
913
  export const REGISTRY = {
724
- run, zepto: zeptoCmd, recheck, campaigns, export: exportCmd, balance, creds, stop, delete: del,
914
+ run, zepto: zeptoCmd, bigbasket: bigbasketCmd, recheck, campaigns, export: exportCmd, balance, creds, stop, delete: del,
725
915
  whoami, login, logout, config: configCmd, update, help,
726
916
  };
package/src/index.js CHANGED
@@ -54,6 +54,11 @@ 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
+ --auto | --manual --count N --concurrency N --out <file|dir>
59
+ --service <1819|1874|1919|2259> (auto: TempOTP BigBasket service id)
60
+ --number <10-digit> --otp <code> (manual one-shot)
61
+ → writes {phone, bbAuthToken, mId, bbVisitorId} per session
57
62
  scripterx recheck <file> re-validate Minutes status of a session JSON, locally (your IP)
58
63
  → re-splits into corrected -minutes-free / -minutes-used files + stats
59
64
  scripterx campaigns | export [name] | balance | creds | stop | delete | whoami | config
@@ -106,7 +111,7 @@ async function main() {
106
111
  }
107
112
 
108
113
  // ── one-shot mode ──
109
- const map = { run: 'run', zepto: 'zepto', recheck: 'recheck', campaigns: 'campaigns', export: 'export', balance: 'balance',
114
+ const map = { run: 'run', zepto: 'zepto', bigbasket: 'bigbasket', bb: 'bigbasket', recheck: 'recheck', campaigns: 'campaigns', export: 'export', balance: 'balance',
110
115
  creds: 'creds', stop: 'stop', delete: 'delete', whoami: 'whoami', login: 'login',
111
116
  logout: 'logout', config: 'config', update: 'update', help: 'help' };
112
117
  const fnName = map[cmd];
@@ -118,6 +123,7 @@ async function main() {
118
123
  mode: flags.email ? 'json_email' : flags.mode, // --email or --mode json_email
119
124
  target: flags.target, // run: 'flipkart' | 'zepto'
120
125
  number: flags.number, otp: flags.otp, cert: flags.cert, // zepto: local OTP login → ZAUTH1 envelope
126
+ service: flags.service, // bigbasket auto: TempOTP service id (1819/1874/1919/2259)
121
127
  auto: flags.auto, manual: flags.manual,
122
128
  checkCoupon: flags['check-coupon'] === true ? true : (flags['check-coupon'] === false ? false : null),
123
129
  campaign: flags._[0], out: flags.out, action: flags._[0], value: flags._[1],
@@ -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
+ }
@@ -3,9 +3,19 @@ import { extractOtp } from '../flipkart.js';
3
3
 
4
4
  const BASE = 'https://api.tempotp.online';
5
5
  // serviceId -> cost (₹). Keep ids as strings (they go straight into the GET params).
6
- export const SERVICES = { '1040': 10.0, '940': 16.0, '2451': 12.5, '2452': 12.5, '2453': 12.0, '2484': 18.0 };
6
+ export const SERVICES = {
7
+ '1040': 10.0, '940': 16.0, '2451': 12.5, '2452': 12.5, '2453': 12.0, '2484': 18.0,
8
+ // BigBasket (SERVER 16 [best-sv-multi])
9
+ '1819': 12.0, '1874': 12.0, '1919': 14.0, '2259': 14.0,
10
+ };
7
11
  // optional friendly names shown in the service picker
8
- export const SERVICE_NAMES = { '1040': 'Flipkart', '940': 'Flipkart', '2451': 'Shopsy/Flipkart 1 (SERVER 16)', '2452': 'Shopsy/Flipkart 2 (SERVER 16)', '2453': 'Shopsy/Flipkart 3 (SERVER 16)', '2484': 'ALL 6 DIGIT (SERVER 17 [ALL])' };
12
+ export const SERVICE_NAMES = {
13
+ '1040': 'Flipkart', '940': 'Flipkart', '2451': 'Shopsy/Flipkart 1 (SERVER 16)', '2452': 'Shopsy/Flipkart 2 (SERVER 16)', '2453': 'Shopsy/Flipkart 3 (SERVER 16)', '2484': 'ALL 6 DIGIT (SERVER 17 [ALL])',
14
+ '1819': 'BigBasket 1.0 (SERVER 16)', '1874': 'BigBasket 2.0 (SERVER 16)', '1919': 'BigBasket 3.0 (SERVER 16)', '2259': 'BigBasket 4.0 (SERVER 16)',
15
+ };
16
+ // BigBasket service ids (for the bigbasket auto picker)
17
+ export const BIGBASKET_SERVICES = ['1819', '1874', '1919', '2259'];
18
+ export const DEFAULT_BIGBASKET_SERVICE = '1819';
9
19
  export const DEFAULT_SERVICE = '940';
10
20
  const COUNTRY = '22';
11
21
 
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
@@ -6,6 +6,7 @@ import { FlipkartLogin, WrongOTP, BlockedError, TransientError } from './flipkar
6
6
  import { CampaignLogger } from './logger.js';
7
7
  import * as kuku from './providers/kuku.js';
8
8
  import * as zepto from './providers/zepto.js';
9
+ import * as bigbasket from './providers/bigbasket.js';
9
10
 
10
11
  const OTP_RESEND_AFTER = 60_000;
11
12
  const OTPCART_DEADLINE = 120_000;
@@ -555,13 +556,14 @@ async function releaseNumber(provider, number, rentedAt, { log = () => {}, warn
555
556
  export const __releaseNumberForTest = releaseNumber;
556
557
 
557
558
  export class ZeptoWorker {
558
- constructor(provider, { requested, concurrency, certSha, onEvent, checkCoupon }) {
559
+ constructor(provider, { requested, concurrency, certSha, onEvent, onResult, checkCoupon }) {
559
560
  this.provider = provider;
560
561
  this.requested = requested;
561
562
  this.concurrency = Math.max(1, Math.min(concurrency, requested));
562
563
  this.certSha = certSha;
563
564
  this.checkCoupon = !!checkCoupon; // also classify the ₹50/₹75/₹100 coupon tier
564
565
  this.onEvent = onEvent || (() => {});
566
+ this.onResult = onResult || null; // called per successful result for incremental save
565
567
  // Surface OTPCart re-logins (session taken over by another login) in the UI.
566
568
  if (provider && 'onNotice' in provider) provider.onNotice = (msg) => this.onEvent('warn', `⚠ ${msg}`);
567
569
  this.stats = { total: requested, succeeded: 0, failed: 0, cancelled: 0, generated: 0, attempts: 0, charges: 0 };
@@ -595,6 +597,11 @@ export class ZeptoWorker {
595
597
  else { this.stats.failed++; this.stats.charges += res.cost || 0; }
596
598
  this.onEvent('progress', this.stats);
597
599
  this.onEvent('row', res);
600
+ // Persist each successful envelope IMMEDIATELY (incremental save) so a mid-run
601
+ // stop / double-Ctrl+C / crash never loses an already-extracted account.
602
+ if (res.status === 'success' && res.envelope && this.onResult) {
603
+ try { this.onResult(res); } catch { /* best-effort; never break the loop */ }
604
+ }
598
605
  }
599
606
 
600
607
  async run() {
@@ -753,3 +760,197 @@ export class ZeptoWorker {
753
760
  }
754
761
  }
755
762
  }
763
+
764
+ // ── BigBasket auto worker ─────────────────────────────────────────────────────
765
+ // Identical orchestration to ZeptoWorker (rent → SMS poll → verify → save), but the
766
+ // merchant is BigBasket: bigbasket.sendOtp() does its own 3-call bootstrap+send, we
767
+ // poll the OTP provider's SMS stream, then bigbasket.verifyOtp() → session JSON.
768
+ // Designed for TempOTP (poll, no resend) but works with any provider exposing
769
+ // rentOnce/startOtp(poll)/cancel. Local-first: emits res.session for incremental save;
770
+ // server push is layered on top by the caller (onResult) when a backend is configured.
771
+ export class BigBasketWorker {
772
+ constructor(provider, { requested, concurrency, deadlineMs, onEvent, onResult }) {
773
+ this.provider = provider;
774
+ this.requested = requested;
775
+ this.concurrency = Math.max(1, Math.min(concurrency, requested));
776
+ this.deadlineMs = deadlineMs || (provider?.name === 'tempotp' ? TEMPOTP_DEADLINE : OTPCART_DEADLINE);
777
+ this.onEvent = onEvent || (() => {});
778
+ this.onResult = onResult || null;
779
+ if (provider && 'onNotice' in provider) provider.onNotice = (msg) => this.onEvent('warn', `⚠ ${msg}`);
780
+ this.stats = { total: requested, succeeded: 0, failed: 0, cancelled: 0, generated: 0, attempts: 0, charges: 0 };
781
+ this.slots = {};
782
+ this.seq = 0;
783
+ this.stopped = false;
784
+ this._releases = [];
785
+ }
786
+
787
+ stop() { this.stopped = true; }
788
+ _nextSeq() { return ++this.seq; }
789
+
790
+ _claim() {
791
+ if (this.stopped) return 0;
792
+ if (this.stats.succeeded >= this.requested) return 0;
793
+ const active = this.stats.attempts - (this.stats.succeeded + this.stats.failed + this.stats.cancelled);
794
+ if (this.stats.succeeded + active >= this.requested) return 0;
795
+ if (this.stats.attempts >= this.requested * ATTEMPT_CAP_MULT) return 0;
796
+ return ++this.stats.attempts;
797
+ }
798
+
799
+ _emit(slot, kv) {
800
+ this.slots[slot] = { slot, ...(this.slots[slot] || {}), ...kv };
801
+ this.onEvent('slot', this.slots[slot]);
802
+ }
803
+
804
+ _record(res) {
805
+ if (res.mobile) this.stats.generated++;
806
+ if (res.status === 'success') { this.stats.succeeded++; this.stats.charges += res.cost || 0; }
807
+ else if (res.status === 'cancelled') this.stats.cancelled++;
808
+ else { this.stats.failed++; this.stats.charges += res.cost || 0; }
809
+ this.onEvent('progress', this.stats);
810
+ this.onEvent('row', res);
811
+ if (res.status === 'success' && res.session && this.onResult) {
812
+ try { this.onResult(res); } catch { /* best-effort; never break the loop */ }
813
+ }
814
+ }
815
+
816
+ async run() {
817
+ const loops = Array.from({ length: this.concurrency }, (_, i) => this._loop(i + 1));
818
+ await Promise.all(loops);
819
+ if (this._releases.length) {
820
+ this.onEvent('warn', `finishing up — releasing ${this._releases.length} number(s)…`);
821
+ await Promise.allSettled(this._releases);
822
+ }
823
+ this.onEvent('done', this.stats);
824
+ return this.stats;
825
+ }
826
+
827
+ _release(number, rentedAt, slot) {
828
+ const p = releaseNumber(this.provider, number, rentedAt, {
829
+ log: () => {},
830
+ warn: (m) => this.onEvent('warn', m),
831
+ onWait: slot ? (sec) => this._emit(slot, { phase: 'cancelling', detail: 'releasing (cancel-lock)', wait: sec }) : undefined,
832
+ });
833
+ this._releases.push(p);
834
+ return p;
835
+ }
836
+
837
+ async _rent(slot) {
838
+ let attempt = 0;
839
+ while (!this.stopped) {
840
+ attempt++;
841
+ this._emit(slot, { phase: 'renting', detail: `requesting a number (try ${attempt})` });
842
+ const { number } = await this.provider.rentOnce();
843
+ if (number) return number;
844
+ if (attempt % RENT_COOLDOWN_EVERY === 0) {
845
+ for (let rem = RENT_COOLDOWN_SECS; rem > 0; rem--) {
846
+ if (this.stopped) return null;
847
+ this._emit(slot, { phase: 'rent_wait', detail: 'no numbers — waiting', wait: rem });
848
+ await sleep(1000);
849
+ }
850
+ } else {
851
+ await sleep(RENT_RETRY_DELAY);
852
+ }
853
+ }
854
+ return null;
855
+ }
856
+
857
+ async _loop(slot) {
858
+ for (;;) {
859
+ if (this._claim() === 0) return;
860
+ const res = await this._process(slot);
861
+ await this._record(res);
862
+ }
863
+ }
864
+
865
+ async _process(slot) {
866
+ const idNo = this._nextSeq();
867
+ const res = { id_no: idNo, status: 'failed', cost: 0, detail: '' };
868
+
869
+ if (this.stats.succeeded >= this.requested) {
870
+ this._emit(slot, { phase: 'done', detail: 'goal reached' });
871
+ res.status = 'cancelled'; res.detail = 'goal reached';
872
+ return res;
873
+ }
874
+
875
+ this._emit(slot, { mobile: '', phase: 'renting', detail: 'requesting a number', wait: 0 });
876
+ const number = await this._rent(slot);
877
+ if (!number) { res.status = 'cancelled'; res.detail = 'stopped while renting'; return res; }
878
+ const rentedAt = Date.now();
879
+ if (this.stats.succeeded >= this.requested) {
880
+ this._release(number, rentedAt, slot);
881
+ this._emit(slot, { phase: 'done', detail: 'goal reached' });
882
+ res.status = 'cancelled'; res.detail = 'goal reached';
883
+ return res;
884
+ }
885
+ res.mobile = number.mobile;
886
+ this._emit(slot, { mobile: number.mobile, phase: 'rented', detail: 'got a number' });
887
+
888
+ let stream = null;
889
+ let released = false;
890
+ const smsArrived = { v: false };
891
+
892
+ const releaseOnce = (charged = false, doneDetail = 'done', consumed = false) => {
893
+ if (released) return;
894
+ released = true;
895
+ if (consumed || (charged && !this.stopped)) { this._emit(slot, { phase: 'done', detail: doneDetail }); return; }
896
+ this._emit(slot, { phase: 'cancelling', detail: 'releasing number' });
897
+ this._release(number, rentedAt, slot);
898
+ };
899
+
900
+ const fail = (detail, category) => {
901
+ const charged = smsArrived.v;
902
+ res.status = charged ? 'failed' : 'cancelled';
903
+ res.cost = charged ? number.cost : 0;
904
+ res.detail = detail;
905
+ res.category = category || (res.status === 'failed' ? 'failed' : 'cancelled');
906
+ releaseOnce(charged, 'failed');
907
+ return res;
908
+ };
909
+
910
+ const session = bigbasket.newSession(number.mobile);
911
+
912
+ try {
913
+ this._emit(slot, { phase: 'ws', detail: 'opening OTP channel' });
914
+ stream = this.provider.startOtp(number);
915
+ if (typeof stream.ready === 'function') await stream.ready();
916
+
917
+ // bigbasket.sendOtp does register/device → header → otp (all on our IP).
918
+ this._emit(slot, { phase: 'send_otp', detail: 'sending OTP' });
919
+ const sent = await bigbasket.sendOtp(session);
920
+ if (!sent.ok) return fail(`send: ${sent.msg}`, 'send_blocked');
921
+
922
+ // Poll the rented number's SMS stream for the BigBasket OTP.
923
+ const end = Date.now() + this.deadlineMs;
924
+ let otp = null;
925
+ while (Date.now() < end && !this.stopped) {
926
+ const { otp: o, arrived } = await stream.poll();
927
+ if (arrived) smsArrived.v = true;
928
+ if (o) { otp = o; break; }
929
+ this._emit(slot, { phase: 'await_otp', detail: 'waiting for OTP', wait: Math.floor((end - Date.now()) / 1000) });
930
+ await sleep(1000);
931
+ }
932
+ if (!otp) return fail(`no OTP within ${Math.round(this.deadlineMs / 1000)}s — abandoned`, 'timeout');
933
+
934
+ this._emit(slot, { phase: 'verify', detail: 'verifying OTP' });
935
+ const v = await bigbasket.verifyOtp(session, otp);
936
+ if (!v.ok) {
937
+ const wrong = /valid otp|incorrect|invalid|expired/i.test(v.error || '');
938
+ return fail(`verify: ${v.error}`, wrong ? 'wrong_otp' : 'failed');
939
+ }
940
+
941
+ res.status = 'success';
942
+ res.cost = number.cost;
943
+ res.session = bigbasket.toSessionKeys(session); // { phone, bbAuthToken, mId, bbVisitorId }
944
+ res.category = 'extracted';
945
+
946
+ this._emit(slot, { phase: 'saving', detail: 'saving session' });
947
+ releaseOnce(true, 'extracted', true); // success: OTP consumed → no cancel
948
+ return res;
949
+ } catch (e) {
950
+ return fail(`unexpected: ${e.message}`);
951
+ } finally {
952
+ releaseOnce(false);
953
+ if (stream) try { stream.close(); } catch { /* */ }
954
+ }
955
+ }
956
+ }