scripter-x 1.0.29 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripter-x",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
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,10 +8,11 @@ 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';
14
- import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
15
+ import { mkdirSync, writeFileSync, appendFileSync, readFileSync, existsSync } from 'node:fs';
15
16
  import { Worker } from './worker.js';
16
17
  import { RunController } from './controller.js';
17
18
  import { STATUS } from './theme.js';
@@ -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
 
@@ -289,29 +292,32 @@ export async function recheck(io, _api, args = {}) {
289
292
  // code → we verify, build the ZAUTH1 envelope, and write {phone}-{timestamp}.txt.
290
293
  // LOOPS: after each one it asks for the next number — keep going until the user
291
294
  // presses Esc (or double Ctrl+C). Runs entirely on your IP.
292
- // Resolve the dir + tier-file path for a Zepto envelope. tier ∈ rs50|rs75|rs100|normal|unchecked|''.
293
- function zeptoOutPath(args, number, tier) {
294
- const stamp = new Date().toISOString().replace(/[:.]/g, '-');
295
- const suffix = tier ? `-${tier}` : '';
296
- let baseDir;
295
+ // Resolve the base dir for Zepto envelope output.
296
+ function zeptoBaseDir(args) {
297
297
  if (args.out) {
298
298
  const expanded = args.out.startsWith('~') ? join(homedir(), args.out.slice(1)) : args.out;
299
- // if --out is an explicit file, honor it (no tier split); else treat as dir
300
- if (/\.(txt|json)$/i.test(expanded) && !tier) return expanded;
301
- baseDir = /\.(txt|json)$/i.test(expanded) ? dirname(expanded) : expanded;
302
- } else {
303
- const downloads = join(homedir(), 'Downloads');
304
- baseDir = join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'zepto');
299
+ return /\.(txt|json)$/i.test(expanded) ? dirname(expanded) : expanded;
305
300
  }
306
- // tier accounts go into per-tier subfolders so the 4 types are cleanly separated
307
- const dir = tier ? join(baseDir, tier) : baseDir;
308
- mkdirSync(dir, { recursive: true });
309
- return join(dir, `${number}-${stamp}.txt`);
301
+ const downloads = join(homedir(), 'Downloads');
302
+ return join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'zepto');
310
303
  }
311
304
 
312
- // Append one envelope to the right tier file/dir and return the path written.
305
+ // Save one envelope. With a tier APPEND to {tier}.txt (one envelope per block,
306
+ // blank-line separated, so the file is readable + paste-friendly). Without a tier →
307
+ // a single per-number timestamped file (legacy behaviour). Returns the path written.
313
308
  function saveZeptoEnvelope(args, number, envelope, tier) {
314
- const dest = zeptoOutPath(args, number, tier);
309
+ const baseDir = zeptoBaseDir(args);
310
+ if (tier) {
311
+ mkdirSync(baseDir, { recursive: true });
312
+ const dest = join(baseDir, `${tier}.txt`);
313
+ // append with a blank-line separator if the file already has content
314
+ const block = (existsSync(dest) ? '\n' : '') + envelope + '\n';
315
+ appendFileSync(dest, block);
316
+ return dest;
317
+ }
318
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
319
+ const dest = join(baseDir, `${number}-${stamp}.txt`);
320
+ mkdirSync(baseDir, { recursive: true });
315
321
  writeFileSync(dest, envelope + '\n');
316
322
  return dest;
317
323
  }
@@ -438,12 +444,29 @@ export async function zeptoCmd(io, api, args = {}) {
438
444
  const { ZeptoWorker } = await import('./worker.js');
439
445
  const { RunController } = await import('./controller.js');
440
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
+
441
463
  const worker = new ZeptoWorker(provider, {
442
464
  requested: count,
443
465
  concurrency,
444
466
  certSha: cert,
445
467
  checkCoupon,
446
468
  onEvent: controller.handleEvent,
469
+ onResult,
447
470
  });
448
471
  controller.stop = () => worker.stop();
449
472
 
@@ -475,41 +498,125 @@ export async function zeptoCmd(io, api, args = {}) {
475
498
 
476
499
  io.print(` ✓ done — ${worker.stats.succeeded} succeeded · ${worker.stats.failed} failed · ${worker.stats.cancelled} cancelled · ₹${worker.stats.charges} spent`);
477
500
 
478
- 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)
479
507
  const saved = await saveZeptoSessions(results, name, args.out);
480
508
  if (saved) {
481
509
  io.print(` ✓ saved ${saved.count} session(s) to 2 files:`);
482
510
  io.print(` full → ${saved.fullPath}`, 'accent');
483
511
  io.print(` zauth → ${saved.zauthPath}`, 'accent');
484
512
  }
485
- // If coupon-tier was checked, ALSO split the envelopes into per-tier files.
486
513
  if (checkCoupon) {
487
- const tiers = {};
488
- for (const r of results) {
489
- if (r.status !== 'success') continue;
490
- const tier = r.coupon_tier || 'unchecked';
491
- (tiers[tier] = tiers[tier] || []).push(r.envelope);
492
- }
493
- 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}):`);
494
516
  for (const tier of ['rs100', 'rs75', 'rs50', 'normal', 'unchecked']) {
495
- if (!tiers[tier]?.length) continue;
496
- const dir = zeptoTierDir(args, name);
497
- const dest = join(dir, `${tier}.txt`);
498
- mkdirSync(dir, { recursive: true });
499
- writeFileSync(dest, tiers[tier].join('\n') + '\n');
500
- 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');
501
519
  }
502
520
  } else {
503
521
  io.print(' ◉ ZAUTH1 envelopes:');
504
- for (const r of results) {
505
- if (r.status === 'success') io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
506
- }
522
+ for (const r of succeeded) io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
507
523
  }
508
524
  } else {
509
525
  io.print(' ◉ no sessions extracted — nothing to save.');
510
526
  }
511
527
  }
512
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
+
513
620
  const TIER_LABEL = { rs100: '🟢 ₹100', rs75: '🟢 ₹75', rs50: '🟢 ₹50', normal: '⚪ normal', unchecked: '❔ unchecked' };
514
621
 
515
622
  // The directory the per-tier files go in for an auto run.
@@ -691,7 +798,7 @@ export async function configCmd(io, _api, args = {}) {
691
798
  }
692
799
 
693
800
  export function help(io) {
694
- 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');
695
802
  }
696
803
 
697
804
  export async function update(io) {
@@ -717,6 +824,6 @@ export async function update(io) {
717
824
 
718
825
  // dispatch table used by the interactive shell
719
826
  export const REGISTRY = {
720
- 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,
721
828
  whoami, login, logout, config: configCmd, update, help,
722
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() {