scripter-x 1.0.31 → 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.31",
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
@@ -552,13 +552,12 @@ function saveBigbasketSession(args, number, sessionKeys) {
552
552
  }
553
553
 
554
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
- }
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);
562
561
 
563
562
  io.print(' ◉ BigBasket — local OTP → session JSON (Manual)');
564
563
  io.print(' ◉ press esc to cancel', 'accent');
@@ -617,6 +616,94 @@ export async function bigbasketCmd(io, api, args = {}) {
617
616
  io.print(` ✓ done — ${done} session(s) saved.`);
618
617
  }
619
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');
702
+ } else {
703
+ io.print(' ◉ no sessions extracted — nothing to save.');
704
+ }
705
+ }
706
+
620
707
  const TIER_LABEL = { rs100: '🟢 ₹100', rs75: '🟢 ₹75', rs50: '🟢 ₹50', normal: '⚪ normal', unchecked: '❔ unchecked' };
621
708
 
622
709
  // The directory the per-tier files go in for an auto run.
package/src/index.js CHANGED
@@ -55,8 +55,10 @@ const HELP = `${paint.bold('scripterx')} — local Flipkart session extractor
55
55
  --number <10-digit> --otp <code>
56
56
  (envelope keyed to ZeptoAuthManager cert; override with --cert or $ZAUTH_CERT_SHA)
57
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)
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
60
62
  scripterx recheck <file> re-validate Minutes status of a session JSON, locally (your IP)
61
63
  → re-splits into corrected -minutes-free / -minutes-used files + stats
62
64
  scripterx campaigns | export [name] | balance | creds | stop | delete | whoami | config
@@ -121,6 +123,7 @@ async function main() {
121
123
  mode: flags.email ? 'json_email' : flags.mode, // --email or --mode json_email
122
124
  target: flags.target, // run: 'flipkart' | 'zepto'
123
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)
124
127
  auto: flags.auto, manual: flags.manual,
125
128
  checkCoupon: flags['check-coupon'] === true ? true : (flags['check-coupon'] === false ? false : null),
126
129
  campaign: flags._[0], out: flags.out, action: flags._[0], value: flags._[1],
@@ -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/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;
@@ -759,3 +760,197 @@ export class ZeptoWorker {
759
760
  }
760
761
  }
761
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
+ }