scripter-x 1.0.20 → 1.0.21

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.20",
3
+ "version": "1.0.21",
4
4
  "description": "ScripterX — local Flipkart session extractor (runs on your residential IP, syncs to marthunt)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -128,11 +128,30 @@ export class OTPCartProvider {
128
128
 
129
129
  startOtp(number) { return new OTPCartStream(this, number.txn, number.extra); }
130
130
 
131
+ // Cancel (release) a rented number. Returns a structured outcome instead of
132
+ // swallowing everything, so the worker can tell apart:
133
+ // { ok: true } → number released
134
+ // { ok: false, tooEarly: true, ... } → still inside OTPCart's ~2min cancel lock; retry later
135
+ // { ok: false, ... } → other error; retry
136
+ // OTPCart refuses to cancel a number rented < ~2 min ago; the worker must wait
137
+ // out that lock and keep retrying, otherwise the number stays rented (charged).
131
138
  async cancel(number) {
139
+ let status, d;
132
140
  try {
133
- await this._post('/otp/cancelOtp',
134
- { serialNumber: number.txn, mobileId: number.extra, serviceId: this.serviceId }, 12000);
135
- } catch { /* */ }
141
+ ({ status, data: d } = await this._post('/otp/cancelOtp',
142
+ { serialNumber: number.txn, mobileId: number.extra, serviceId: this.serviceId }, 12000));
143
+ } catch (e) {
144
+ return { ok: false, tooEarly: false, msg: e.message };
145
+ }
146
+ const msg = String(d?.message || '');
147
+ const low = msg.toLowerCase();
148
+ // success: doc says "Otp canceled successfully!!!"
149
+ if (status >= 200 && status < 300 && /cancel|success|refund/.test(low)) {
150
+ return { ok: true, msg };
151
+ }
152
+ // the cancel-lock window: messages like "you can cancel after 2 minutes" / "wait".
153
+ const tooEarly = /(\d+\s*min|after|wait|too soon|not allowed yet|before)/.test(low);
154
+ return { ok: false, tooEarly, msg: msg || `cancel failed (status ${status})` };
136
155
  }
137
156
  }
138
157
 
@@ -141,6 +160,8 @@ export class OTPCartProvider {
141
160
  // lands well within this; a *dead-token* socket NEVER pushes, so this is what
142
161
  // rescues an otherwise-silent run.
143
162
  const WS_SILENT_RELOGIN_MS = 25000;
163
+ const WS_PING_MS = 20000; // keep-alive so an idle socket isn't dropped before the SMS lands
164
+ const WS_DEBUG = process.env.OTPCART_WS_DEBUG === '1';
144
165
 
145
166
  class OTPCartStream {
146
167
  // Takes the PROVIDER (not a frozen jwt) so every (re)connect reads the current
@@ -156,16 +177,22 @@ class OTPCartStream {
156
177
  this._reconnectTimer = null;
157
178
  this._connectedAt = 0;
158
179
  this._silentTimer = null; // fires if no OTP arrives → relogin + reconnect
180
+ this._pingTimer = null; // keep-alive
159
181
  this._reloggingIn = false;
160
182
  this._silentReloginsLeft = 1; // recover a taken-over session once; don't churn a slow SMS
183
+ this._gotServerAck = false; // server sent the {serialNumber,mobileId} connect frame
161
184
  this._connect();
162
185
  }
163
186
 
164
187
  _currentToken() { return this.provider.token; }
188
+ _dbg(...a) { if (WS_DEBUG) { try { console.error('[otpcart-ws]', ...a); } catch { /* */ } } }
165
189
 
166
190
  _clearSilentTimer() {
167
191
  if (this._silentTimer) { clearTimeout(this._silentTimer); this._silentTimer = null; }
168
192
  }
193
+ _clearPingTimer() {
194
+ if (this._pingTimer) { clearInterval(this._pingTimer); this._pingTimer = null; }
195
+ }
169
196
 
170
197
  // Schedule the "this socket has gone quiet — maybe the token is dead" check.
171
198
  // Only while we still have a relogin budget (a genuinely slow SMS shouldn't keep
@@ -180,6 +207,7 @@ class OTPCartStream {
180
207
  if (this.closed || this.otp || this._reloggingIn || this._silentReloginsLeft <= 0) return;
181
208
  this._silentReloginsLeft--;
182
209
  this._reloggingIn = true;
210
+ this._dbg('silent watchdog fired — relogin + reconnect');
183
211
  try {
184
212
  // Re-login (de-duped at the provider) to get a token that the WS will accept,
185
213
  // then reconnect this socket with it. If the token was actually fine, this is
@@ -193,30 +221,77 @@ class OTPCartStream {
193
221
  this._connect(); // reconnect with the (possibly refreshed) current token
194
222
  }
195
223
 
224
+ // The browser sends an Origin (and a UA) on the WS handshake. The `ws` library
225
+ // sends NEITHER by default — and OTPCart's server appears to only attach the OTP
226
+ // listener for browser-origin sockets, so a header-less socket connects but never
227
+ // receives a push. Mirroring the browser handshake is the actual fix for the
228
+ // "site gets the OTP, the script doesn't" symptom.
229
+ _wsOptions() {
230
+ return {
231
+ headers: {
232
+ Origin: 'https://www.otpcart.xyz',
233
+ 'User-Agent': UA,
234
+ },
235
+ // also keeps proxies from dropping us; ws sends its own pongs automatically
236
+ handshakeTimeout: 15000,
237
+ };
238
+ }
239
+
240
+ _subscribe() {
241
+ // Echo the ack frame the browser sends back to bind this socket to our number.
242
+ // Harmless if the server doesn't require it; necessary if it does.
243
+ try {
244
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
245
+ this.ws.send(JSON.stringify({ serialNumber: this.serial, mobileId: this.mobileId, resend: false }));
246
+ this._dbg('sent subscribe', this.serial, this.mobileId);
247
+ }
248
+ } catch { /* */ }
249
+ }
250
+
196
251
  _connect() {
197
252
  const token = this._currentToken();
198
- this.ws = new WebSocket(`${WS_URL}?token=${token}`);
253
+ this._dbg('connecting', WS_URL, 'token', String(token).slice(0, 10) + '…');
254
+ this.ws = new WebSocket(`${WS_URL}?token=${token}`, this._wsOptions());
255
+ this._gotServerAck = false;
199
256
  this._openP = new Promise((resolve) => {
200
257
  this.ws.once('open', resolve);
201
258
  this.ws.once('error', resolve); // resolve anyway so ready() never hangs
202
259
  });
203
260
  this.ws.on('open', () => {
204
261
  this._connectedAt = Date.now();
262
+ this._dbg('open');
263
+ this._subscribe(); // bind this socket to our number (browser parity)
205
264
  this._armSilentTimer(); // start the dead-token watchdog
265
+ this._clearPingTimer();
266
+ this._pingTimer = setInterval(() => { try { this.ws.ping(); } catch { /* */ } }, WS_PING_MS);
206
267
  });
207
268
  this.ws.on('message', (raw) => {
208
- let msg; try { msg = JSON.parse(raw.toString()); } catch { return; }
209
- // OTP frame: {message, otpMessage}. Ack frame {serialNumber,mobileId,resend} is ignored.
269
+ const text = raw.toString();
270
+ this._dbg('recv', text.slice(0, 160));
271
+ let msg; try { msg = JSON.parse(text); } catch { return; }
272
+ // OTP frame: {message, otpMessage}.
210
273
  if (msg.otpMessage) {
211
274
  const m = String(msg.otpMessage).match(OTP_RE);
212
- if (m) { this.otp = m[1]; this.arrived = true; this._clearSilentTimer(); }
275
+ if (m) { this.otp = m[1]; this.arrived = true; this._clearSilentTimer(); this._dbg('OTP', m[1]); }
276
+ return;
277
+ }
278
+ // Ack frame: {serialNumber, mobileId, resend}. Confirms the socket is bound.
279
+ // Adopt the server's serial/mobileId in case they differ from ours.
280
+ if (msg.serialNumber || msg.mobileId) {
281
+ this._gotServerAck = true;
282
+ if (msg.serialNumber) this.serial = msg.serialNumber;
283
+ if (msg.mobileId) this.mobileId = msg.mobileId;
284
+ this._dbg('server ack', this.serial, this.mobileId);
213
285
  }
214
286
  });
215
- this.ws.on('error', () => { /* swallow — poll() returns nothing; we auto-reconnect */ });
287
+ this.ws.on('pong', () => this._dbg('pong'));
288
+ this.ws.on('error', (e) => { this._dbg('error', e?.message); /* swallow — poll() returns nothing; we auto-reconnect */ });
216
289
  // Auto-reconnect if the socket drops before the OTP arrives (so a dropped WS doesn't
217
290
  // silently lose the push — this is the "purchased but never got OTP" symptom).
218
- this.ws.on('close', () => {
291
+ this.ws.on('close', (code) => {
292
+ this._dbg('close', code);
219
293
  this._clearSilentTimer();
294
+ this._clearPingTimer();
220
295
  if (this.closed || this.otp || this._reloggingIn) return;
221
296
  this._reconnectTimer = setTimeout(() => { if (!this.closed && !this.otp) this._connect(); }, 1000);
222
297
  });
@@ -240,6 +315,7 @@ class OTPCartStream {
240
315
  try {
241
316
  if (this.ws.readyState === WebSocket.OPEN) {
242
317
  this.ws.send(JSON.stringify({ serialNumber: this.serial, mobileId: this.mobileId, resend: true }));
318
+ this._dbg('sent resend');
243
319
  return true;
244
320
  }
245
321
  } catch { /* */ }
@@ -249,6 +325,7 @@ class OTPCartStream {
249
325
  close() {
250
326
  this.closed = true;
251
327
  this._clearSilentTimer();
328
+ this._clearPingTimer();
252
329
  if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; }
253
330
  try { this.ws.close(); } catch { /* */ }
254
331
  }
@@ -3,9 +3,9 @@ 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 };
6
+ export const SERVICES = { '1040': 10.0, '940': 16.0, '2451': 12.5, '2484': 18.0 };
7
7
  // optional friendly names shown in the service picker
8
- export const SERVICE_NAMES = { '1040': 'Flipkart', '940': 'Flipkart', '2451': 'Shopsy/Flipkart 1 (SERVER 16)' };
8
+ export const SERVICE_NAMES = { '1040': 'Flipkart', '940': 'Flipkart', '2451': 'Shopsy/Flipkart 1 (SERVER 16)', '2484': 'ALL 6 DIGIT (SERVER 17 [ALL])' };
9
9
  export const DEFAULT_SERVICE = '940';
10
10
  const COUNTRY = '22';
11
11
 
package/src/worker.js CHANGED
@@ -141,7 +141,7 @@ export class Worker {
141
141
  if (!number) { res.status = 'cancelled'; res.detail = 'stopped while renting'; return res; }
142
142
  if (this.stats.succeeded >= this.requested) {
143
143
  this.log(`goal reached by another slot; releasing ${number.mobile} immediately`);
144
- this._release(number, Date.now());
144
+ this._release(number, Date.now(), slot);
145
145
  this._emit(slot, { phase: 'done', detail: 'goal reached' });
146
146
  res.status = 'cancelled';
147
147
  res.detail = 'goal reached';
@@ -160,15 +160,19 @@ export class Worker {
160
160
  // releaseOnce — the single chokepoint that schedules the provider cancel. Called from
161
161
  // every terminal path (fail, success, error). Without this a blocked/errored number
162
162
  // could stay rented → we'd be charged.
163
- const releaseOnce = (charged = false, doneDetail = 'done') => {
163
+ const releaseOnce = (charged = false, doneDetail = 'done', consumed = false) => {
164
164
  if (released) return;
165
165
  released = true;
166
- if (charged) {
166
+ // `consumed` = the OTP was actually used (success) → cancelling is pointless.
167
+ // A charged-but-not-consumed number (SMS arrived, login failed) normally isn't
168
+ // worth cancelling either (no refund) — EXCEPT on an explicit stop, where we
169
+ // ALWAYS attempt the cancel so no rented number is left active when the user bails.
170
+ if (consumed || (charged && !this.stopped)) {
167
171
  this._emit(slot, { phase: 'done', detail: doneDetail });
168
172
  return;
169
173
  }
170
174
  this._emit(slot, { phase: 'cancelling', detail: 'releasing number' });
171
- this._release(number, rentedAt);
175
+ this._release(number, rentedAt, slot);
172
176
  };
173
177
 
174
178
  const fail = (detail) => {
@@ -261,7 +265,7 @@ export class Worker {
261
265
  res.session = session;
262
266
  const doneDetail = res.email_linked ? `extracted + ${res.linked_email}` : 'extracted';
263
267
  this._emit(slot, { phase: 'done', detail: doneDetail });
264
- releaseOnce(true, doneDetail); // success still releases the number (OTP already consumed)
268
+ releaseOnce(true, doneDetail, true); // success: OTP consumed no cancel
265
269
  return res;
266
270
  } catch (e) {
267
271
  // ANY unexpected error → still release the number so we're never charged for a leak
@@ -444,35 +448,71 @@ export class Worker {
444
448
  }
445
449
 
446
450
  // _release — GUARANTEE the rented number is cancelled so we're never charged for a
447
- // number that didn't succeed. TempOTP can't be cancelled before its 2-min lock, so we
448
- // wait that out first. The cancel is RETRIED up to 5× until it confirms; the outcome is
449
- // logged. The returned promise is tracked so the campaign waits for it before finishing.
450
- _release(number, rentedAt) {
451
+ // number that didn't succeed. Both TempOTP and OTPCart enforce a ~2-min cancel lock,
452
+ // so we wait that out, then RETRY until the provider confirms the cancel. The returned
453
+ // promise is tracked so the campaign waits for it before finishing (incl. on ESC/stop).
454
+ _release(number, rentedAt, slot) {
451
455
  if (!this._releases) this._releases = [];
452
- const p = (async () => {
453
- if (this.provider.name === 'tempotp') {
454
- const wait = NUMBER_CANCEL_LOCK - (Date.now() - rentedAt);
455
- if (wait > 0) await sleep(wait);
456
- }
457
- for (let attempt = 1; attempt <= 5; attempt++) {
458
- try {
459
- await this.provider.cancel(number);
460
- this.log(`cancelled ${number.mobile} (txn ${number.txn}) — refunded`);
461
- return;
462
- } catch (e) {
463
- this.log(`cancel ${number.mobile} attempt ${attempt}/5 failed: ${e.message}`);
464
- await sleep(3000);
465
- }
466
- }
467
- // Could NOT cancel after 5 tries — flag it loudly so the user knows a number may be charged.
468
- this.log(`⚠ COULD NOT CANCEL ${number.mobile} (txn ${number.txn}) after 5 tries — may be charged! cancel manually on the provider.`);
469
- this.onEvent('warn', `⚠ couldn't cancel ${number.mobile.slice(-10)} — cancel it on the provider to avoid a charge`);
470
- })();
456
+ const p = releaseNumber(this.provider, number, rentedAt, {
457
+ log: (m) => this.log(m),
458
+ warn: (m) => this.onEvent('warn', m),
459
+ onWait: slot ? (sec) => this._emit(slot, { phase: 'cancelling', detail: 'releasing (cancel-lock)', wait: sec }) : undefined,
460
+ });
471
461
  this._releases.push(p);
472
462
  return p;
473
463
  }
474
464
  }
475
465
 
466
+ // Shared release routine for both workers. Waits out the provider's cancel lock,
467
+ // then retries until the cancel is CONFIRMED (the provider returns ok), honouring a
468
+ // `tooEarly` response by waiting and trying again rather than giving up. Never throws.
469
+ async function releaseNumber(provider, number, rentedAt, { log = () => {}, warn = () => {}, onWait = () => {} } = {}) {
470
+ // OTPCart AND TempOTP both refuse to cancel a number rented < ~2 min ago.
471
+ const sinceRent = Date.now() - (rentedAt || Date.now());
472
+ const initialWait = NUMBER_CANCEL_LOCK - sinceRent;
473
+ if (initialWait > 0) {
474
+ log(`waiting ${Math.ceil(initialWait / 1000)}s for ${number.mobile} cancel-lock to expire…`);
475
+ // Tick down a visible countdown so ESC/stop doesn't look frozen during the wait.
476
+ const until = Date.now() + initialWait;
477
+ while (Date.now() < until) {
478
+ onWait(Math.ceil((until - Date.now()) / 1000));
479
+ await sleep(1000);
480
+ }
481
+ }
482
+ // Retry generously — we MUST land the cancel or the number stays charged. ~3 min of
483
+ // retries past the lock covers transient errors and a slightly-longer server lock.
484
+ const deadline = Date.now() + 180_000;
485
+ let attempt = 0;
486
+ while (Date.now() < deadline) {
487
+ attempt++;
488
+ let r;
489
+ try {
490
+ r = await provider.cancel(number);
491
+ } catch (e) {
492
+ r = { ok: false, tooEarly: false, msg: e.message };
493
+ }
494
+ if (r && r.ok) {
495
+ log(`cancelled ${number.mobile} (txn ${number.txn}) — refunded`);
496
+ return true;
497
+ }
498
+ const why = r?.msg || 'unknown error';
499
+ if (r?.tooEarly) {
500
+ log(`cancel ${number.mobile}: still locked (${why}) — retrying in 15s`);
501
+ await sleep(15_000);
502
+ } else {
503
+ log(`cancel ${number.mobile} attempt ${attempt} failed: ${why} — retrying in 5s`);
504
+ await sleep(5_000);
505
+ }
506
+ }
507
+ // Could NOT cancel — flag it loudly so the user knows a number may be charged.
508
+ log(`⚠ COULD NOT CANCEL ${number.mobile} (txn ${number.txn}) — may be charged! cancel manually on the provider.`);
509
+ warn(`⚠ couldn't cancel ${number.mobile.slice(-10)} — cancel it on the provider to avoid a charge`);
510
+ return false;
511
+ }
512
+
513
+ // Exposed for tests only.
514
+ export const __releaseNumberForTest = releaseNumber;
515
+
476
516
  export class ZeptoWorker {
477
517
  constructor(provider, { requested, concurrency, certSha, onEvent }) {
478
518
  this.provider = provider;
@@ -518,25 +558,25 @@ export class ZeptoWorker {
518
558
  async run() {
519
559
  const loops = Array.from({ length: this.concurrency }, (_, i) => this._loop(i + 1));
520
560
  await Promise.all(loops);
561
+ // Don't report "done" until EVERY rented number is actually cancelled — including
562
+ // numbers still inside their 2-min cancel lock. This is what makes ESC/stop wait
563
+ // for the last taken number to be released properly.
521
564
  if (this._releases.length) {
565
+ this.onEvent('warn', `finishing up — releasing ${this._releases.length} number(s)…`);
522
566
  await Promise.allSettled(this._releases);
523
567
  }
524
568
  this.onEvent('done', this.stats);
525
569
  return this.stats;
526
570
  }
527
571
 
528
- _release(number) {
529
- const p = (async () => {
530
- for (let attempt = 1; attempt <= 5; attempt++) {
531
- try {
532
- await this.provider.cancel(number);
533
- return;
534
- } catch {
535
- await sleep(3000);
536
- }
537
- }
538
- })();
572
+ _release(number, rentedAt, slot) {
573
+ const p = releaseNumber(this.provider, number, rentedAt, {
574
+ log: () => {},
575
+ warn: (m) => this.onEvent('warn', m),
576
+ onWait: slot ? (sec) => this._emit(slot, { phase: 'cancelling', detail: 'releasing (cancel-lock)', wait: sec }) : undefined,
577
+ });
539
578
  this._releases.push(p);
579
+ return p;
540
580
  }
541
581
 
542
582
  async _rent(slot) {
@@ -581,29 +621,32 @@ export class ZeptoWorker {
581
621
  this._emit(slot, { mobile: '', phase: 'renting', detail: 'requesting a number', wait: 0 });
582
622
  const number = await this._rent(slot);
583
623
  if (!number) { res.status = 'cancelled'; res.detail = 'stopped while renting'; return res; }
624
+ const rentedAt = Date.now();
584
625
  if (this.stats.succeeded >= this.requested) {
585
- this._release(number);
626
+ this._release(number, rentedAt, slot);
586
627
  this._emit(slot, { phase: 'done', detail: 'goal reached' });
587
628
  res.status = 'cancelled';
588
629
  res.detail = 'goal reached';
589
630
  return res;
590
631
  }
591
- const rentedAt = Date.now();
592
632
  res.mobile = number.mobile;
593
633
  this._emit(slot, { mobile: number.mobile, phase: 'rented', detail: 'got a number' });
594
634
 
595
635
  let stream = null;
596
636
  let released = false;
597
637
 
598
- const releaseOnce = (charged = false, doneDetail = 'done') => {
638
+ const releaseOnce = (charged = false, doneDetail = 'done', consumed = false) => {
599
639
  if (released) return;
600
640
  released = true;
601
- if (charged) {
641
+ // `consumed` = OTP used (success) → no point cancelling. On an explicit stop we
642
+ // ALWAYS attempt the cancel for a non-consumed number (even if charged) so none
643
+ // is left active when the user bails.
644
+ if (consumed || (charged && !this.stopped)) {
602
645
  this._emit(slot, { phase: 'done', detail: doneDetail });
603
646
  return;
604
647
  }
605
648
  this._emit(slot, { phase: 'cancelling', detail: 'releasing number' });
606
- this._release(number);
649
+ this._release(number, rentedAt, slot);
607
650
  };
608
651
 
609
652
  const fail = (detail) => {
@@ -645,12 +688,14 @@ export class ZeptoWorker {
645
688
  const envelope = zepto.encodeEnvelope(zepto.toSessionKeys(session), this.certSha);
646
689
  this._emit(slot, { phase: 'saving', detail: 'encoding envelope' });
647
690
  res.status = 'success'; res.cost = number.cost; res.session = zepto.toSessionKeys(session); res.envelope = envelope;
648
- releaseOnce(true, 'extracted');
691
+ releaseOnce(true, 'extracted', true); // success: OTP consumed → no cancel
649
692
  return res;
650
693
  } catch (e) {
651
694
  return fail(`unexpected: ${e.message}`);
652
695
  } finally {
653
- releaseOnce(true);
696
+ // Catch-all: if neither success nor fail released it (shouldn't happen), make sure
697
+ // the number is cancelled rather than left rented.
698
+ releaseOnce(false);
654
699
  if (stream) try { stream.close(); } catch { /* */ }
655
700
  }
656
701
  }