scripter-x 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands.js +7 -3
- package/src/flipkart.js +1 -1
- package/src/providers/otpcart.js +154 -25
- package/src/util.js +4 -2
- package/src/worker.js +7 -0
package/package.json
CHANGED
package/src/commands.js
CHANGED
|
@@ -122,14 +122,16 @@ async function buildProvider(io, name) {
|
|
|
122
122
|
io.print(' ✓ OTPCart connected');
|
|
123
123
|
if (freshlyEntered && await io.confirm('save these creds locally for next time?', true)) config.setMany({ otpcart_email: email, otpcart_password: password });
|
|
124
124
|
const deep = await io.confirm('deep number check?', false);
|
|
125
|
-
|
|
125
|
+
// Pass creds so the provider can transparently re-login if OTPCart's single active
|
|
126
|
+
// session is taken over by another login mid-run (otherwise the OTP WS goes silent).
|
|
127
|
+
return new oc.OTPCartProvider(jwt, deep, undefined, { email, password });
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
export async function run(io, api, args = {}) {
|
|
129
131
|
// TARGET: Flipkart (the backend campaign flow) or Zepto (local OTP → ZAUTH1 envelope).
|
|
130
132
|
const target = args.target || (args.number ? 'zepto' : (await io.select('what to extract?', [
|
|
131
133
|
{ label: 'Flipkart', value: 'flipkart', description: 'session JSON via OTP provider (campaign)' },
|
|
132
|
-
{ label: 'Zepto', value: 'zepto', description: '
|
|
134
|
+
{ label: 'Zepto', value: 'zepto', description: 'headless extraction (auto) OR local OTP login (manual)' }])).value);
|
|
133
135
|
if (target === 'zepto') return zeptoCmd(io, api, args);
|
|
134
136
|
|
|
135
137
|
api = api || await getApi(io);
|
|
@@ -337,7 +339,9 @@ export async function zeptoCmd(io, api, args = {}) {
|
|
|
337
339
|
if (!(await io.confirm(`start ${count} Zepto auto extractions at concurrency ${concurrency}?`, true))) return;
|
|
338
340
|
|
|
339
341
|
const ZEPTO_SERVICE_ID = '68b2cf55980e8cf480b28c96';
|
|
340
|
-
|
|
342
|
+
// Pass creds so the provider can transparently re-login if OTPCart invalidates the
|
|
343
|
+
// session (another login with the same account takes it over → WS stops pushing OTPs).
|
|
344
|
+
const provider = new oc.OTPCartProvider(jwt, false, ZEPTO_SERVICE_ID, { email, password });
|
|
341
345
|
|
|
342
346
|
const { ZeptoWorker } = await import('./worker.js');
|
|
343
347
|
const { RunController } = await import('./controller.js');
|
package/src/flipkart.js
CHANGED
|
@@ -159,7 +159,7 @@ export class FlipkartLogin {
|
|
|
159
159
|
},
|
|
160
160
|
});
|
|
161
161
|
const text = await res.text();
|
|
162
|
-
const count = (text.match(/
|
|
162
|
+
const count = (text.match(/OD\d{15,}/g) || []).length;
|
|
163
163
|
return count === 0; // no orders ⇒ coupon available
|
|
164
164
|
}
|
|
165
165
|
|
package/src/providers/otpcart.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
// OTPCart provider (api.otpcart.xyz) — email/password → JWT, WebSocket OTP push.
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ OTPCart allows only ONE active session per account: a fresh login (anyone,
|
|
4
|
+
// anywhere, with the same email/password) INVALIDATES the previous JWT. When that
|
|
5
|
+
// happens mid-run the symptom is a *silently dead* run — rent/cancel start returning
|
|
6
|
+
// 401 and, worst of all, the OTP WebSocket connects fine but never pushes any
|
|
7
|
+
// `otpMessage`, so OTPs simply never arrive.
|
|
8
|
+
//
|
|
9
|
+
// To survive that, the provider holds the credentials and can transparently
|
|
10
|
+
// re-login (de-duped, one at a time) to mint a fresh JWT, then everything —
|
|
11
|
+
// HTTP calls and NEW WebSocket connections — reads the *current* token. Callers
|
|
12
|
+
// that hit an auth failure trigger relogin() and retry.
|
|
2
13
|
import WebSocket from 'ws';
|
|
3
14
|
|
|
4
|
-
|
|
5
|
-
const
|
|
15
|
+
// Overridable for tests (defaults to the real hosts).
|
|
16
|
+
const BASE = process.env.OTPCART_BASE || 'https://api.otpcart.xyz';
|
|
17
|
+
const WS_URL = process.env.OTPCART_WS || 'wss://api.otpcart.xyz/check-otp';
|
|
6
18
|
const SERVICE_ID = '68b19097980e8cf480b1df04';
|
|
7
19
|
const OTP_RE = /\b(\d{6})\b/;
|
|
8
20
|
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
9
21
|
'(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36';
|
|
10
22
|
|
|
23
|
+
// Heuristic: does an OTPCart response (status + body message) look like the token
|
|
24
|
+
// was rejected / the session was taken over by another login?
|
|
25
|
+
function isAuthFailure(status, message) {
|
|
26
|
+
if (status === 401 || status === 403) return true;
|
|
27
|
+
const m = String(message || '').toLowerCase();
|
|
28
|
+
return /unauthor|jwt|token|please login|log\s?in again|session expired|invalid signature/.test(m);
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
export async function login(email, password) {
|
|
12
32
|
const res = await fetch(`${BASE}/users/login`, {
|
|
13
33
|
method: 'POST', headers: { 'Content-Type': 'application/json', Origin: 'https://www.otpcart.xyz', 'User-Agent': UA },
|
|
@@ -22,10 +42,19 @@ export async function balance() { return null; }
|
|
|
22
42
|
|
|
23
43
|
export class OTPCartProvider {
|
|
24
44
|
name = 'otpcart';
|
|
25
|
-
|
|
45
|
+
|
|
46
|
+
// Backward compatible: (jwt, deepCheck, serviceId). Pass creds via the 4th arg
|
|
47
|
+
// { email, password } to enable transparent re-login when the session is taken
|
|
48
|
+
// over by another login.
|
|
49
|
+
constructor(jwt, deepCheck = false, serviceId = SERVICE_ID, creds = null) {
|
|
26
50
|
this.jwt = jwt;
|
|
27
51
|
this.deepCheck = deepCheck;
|
|
28
52
|
this.serviceId = serviceId;
|
|
53
|
+
this.email = creds?.email || null;
|
|
54
|
+
this.password = creds?.password || null;
|
|
55
|
+
this._reloginInflight = null; // shared promise so concurrent slots relogin once
|
|
56
|
+
this._tokenEpoch = 0; // bumps on every successful relogin
|
|
57
|
+
this.onNotice = null; // optional (msg) => void for UI breadcrumbs
|
|
29
58
|
}
|
|
30
59
|
|
|
31
60
|
_hdrs() {
|
|
@@ -33,15 +62,62 @@ export class OTPCartProvider {
|
|
|
33
62
|
Origin: 'https://www.otpcart.xyz', 'User-Agent': UA };
|
|
34
63
|
}
|
|
35
64
|
|
|
65
|
+
get token() { return this.jwt; }
|
|
66
|
+
get tokenEpoch() { return this._tokenEpoch; }
|
|
67
|
+
get canRelogin() { return !!(this.email && this.password); }
|
|
68
|
+
|
|
69
|
+
// Re-login to mint a fresh JWT after the session was invalidated. De-duped:
|
|
70
|
+
// concurrent callers share ONE in-flight login so they don't invalidate each
|
|
71
|
+
// other in a relogin storm. `seenEpoch` lets a caller say "only relogin if the
|
|
72
|
+
// token I was using is still the current one" (avoids redundant relogins when
|
|
73
|
+
// another slot already refreshed it).
|
|
74
|
+
async relogin(seenEpoch = null) {
|
|
75
|
+
if (!this.canRelogin) throw new Error('OTPCart session invalidated and no saved credentials to re-login');
|
|
76
|
+
if (seenEpoch != null && seenEpoch !== this._tokenEpoch) {
|
|
77
|
+
// Someone already refreshed since this caller's token — use the new one.
|
|
78
|
+
return this.jwt;
|
|
79
|
+
}
|
|
80
|
+
if (this._reloginInflight) return this._reloginInflight;
|
|
81
|
+
this._reloginInflight = (async () => {
|
|
82
|
+
try {
|
|
83
|
+
if (this.onNotice) this.onNotice('OTPCart session was taken over — re-logging in…');
|
|
84
|
+
const tok = await login(this.email, this.password);
|
|
85
|
+
this.jwt = tok;
|
|
86
|
+
this._tokenEpoch++;
|
|
87
|
+
return tok;
|
|
88
|
+
} finally {
|
|
89
|
+
this._reloginInflight = null;
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
return this._reloginInflight;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// POST a JSON body to OTPCart, transparently re-logging-in + retrying ONCE on an
|
|
96
|
+
// auth failure. Returns { status, data }.
|
|
97
|
+
async _post(path, body, timeoutMs = 15000) {
|
|
98
|
+
const epoch = this._tokenEpoch;
|
|
99
|
+
const res = await fetch(`${BASE}${path}`, {
|
|
100
|
+
method: 'POST', headers: this._hdrs(), body: JSON.stringify(body),
|
|
101
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
102
|
+
});
|
|
103
|
+
const data = await res.json().catch(() => ({}));
|
|
104
|
+
if (isAuthFailure(res.status, data?.message) && this.canRelogin) {
|
|
105
|
+
await this.relogin(epoch);
|
|
106
|
+
const res2 = await fetch(`${BASE}${path}`, {
|
|
107
|
+
method: 'POST', headers: this._hdrs(), body: JSON.stringify(body),
|
|
108
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
109
|
+
});
|
|
110
|
+
const data2 = await res2.json().catch(() => ({}));
|
|
111
|
+
return { status: res2.status, data: data2 };
|
|
112
|
+
}
|
|
113
|
+
return { status: res.status, data };
|
|
114
|
+
}
|
|
115
|
+
|
|
36
116
|
async rentOnce() {
|
|
37
|
-
let d;
|
|
117
|
+
let status, d;
|
|
38
118
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
body: JSON.stringify({ serviceId: this.serviceId, isDeepCheck: this.deepCheck }),
|
|
42
|
-
signal: AbortSignal.timeout(15000),
|
|
43
|
-
});
|
|
44
|
-
d = await res.json();
|
|
119
|
+
({ status, data: d } = await this._post('/mobile/generate',
|
|
120
|
+
{ serviceId: this.serviceId, isDeepCheck: this.deepCheck }));
|
|
45
121
|
} catch (e) { return { number: null, msg: e.message, err: e }; }
|
|
46
122
|
if (d.isNumberGenerated && d.mobile?.mobileno) {
|
|
47
123
|
const m = d.mobile;
|
|
@@ -50,55 +126,103 @@ export class OTPCartProvider {
|
|
|
50
126
|
return { number: null, msg: d.message || 'no number available', err: 'err' };
|
|
51
127
|
}
|
|
52
128
|
|
|
53
|
-
startOtp(number) { return new OTPCartStream(this
|
|
129
|
+
startOtp(number) { return new OTPCartStream(this, number.txn, number.extra); }
|
|
54
130
|
|
|
55
131
|
async cancel(number) {
|
|
56
132
|
try {
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
body: JSON.stringify({ serialNumber: number.txn, mobileId: number.extra, serviceId: this.serviceId }),
|
|
60
|
-
signal: AbortSignal.timeout(12000),
|
|
61
|
-
});
|
|
133
|
+
await this._post('/otp/cancelOtp',
|
|
134
|
+
{ serialNumber: number.txn, mobileId: number.extra, serviceId: this.serviceId }, 12000);
|
|
62
135
|
} catch { /* */ }
|
|
63
136
|
}
|
|
64
137
|
}
|
|
65
138
|
|
|
139
|
+
// Grace period after the WS connects with no OTP push before we suspect the token
|
|
140
|
+
// is dead and reconnect with a freshly re-logged-in token. The real SMS usually
|
|
141
|
+
// lands well within this; a *dead-token* socket NEVER pushes, so this is what
|
|
142
|
+
// rescues an otherwise-silent run.
|
|
143
|
+
const WS_SILENT_RELOGIN_MS = 25000;
|
|
144
|
+
|
|
66
145
|
class OTPCartStream {
|
|
67
|
-
|
|
146
|
+
// Takes the PROVIDER (not a frozen jwt) so every (re)connect reads the current
|
|
147
|
+
// token and can ask the provider to re-login when the session was taken over.
|
|
148
|
+
constructor(provider, serial, mobileId) {
|
|
149
|
+
this.provider = provider;
|
|
68
150
|
this.serial = serial;
|
|
69
151
|
this.mobileId = mobileId;
|
|
70
152
|
this.otp = null;
|
|
71
153
|
this.arrived = false;
|
|
72
154
|
this.closed = false;
|
|
73
|
-
this.
|
|
74
|
-
this.
|
|
155
|
+
this._openP = null; // resolves when the WS is OPEN
|
|
156
|
+
this._reconnectTimer = null;
|
|
157
|
+
this._connectedAt = 0;
|
|
158
|
+
this._silentTimer = null; // fires if no OTP arrives → relogin + reconnect
|
|
159
|
+
this._reloggingIn = false;
|
|
160
|
+
this._silentReloginsLeft = 1; // recover a taken-over session once; don't churn a slow SMS
|
|
75
161
|
this._connect();
|
|
76
162
|
}
|
|
77
163
|
|
|
164
|
+
_currentToken() { return this.provider.token; }
|
|
165
|
+
|
|
166
|
+
_clearSilentTimer() {
|
|
167
|
+
if (this._silentTimer) { clearTimeout(this._silentTimer); this._silentTimer = null; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Schedule the "this socket has gone quiet — maybe the token is dead" check.
|
|
171
|
+
// Only while we still have a relogin budget (a genuinely slow SMS shouldn't keep
|
|
172
|
+
// rotating the shared token, which would force every other slot to reconnect).
|
|
173
|
+
_armSilentTimer() {
|
|
174
|
+
this._clearSilentTimer();
|
|
175
|
+
if (this.closed || this.otp || !this.provider.canRelogin || this._silentReloginsLeft <= 0) return;
|
|
176
|
+
this._silentTimer = setTimeout(() => this._handleSilent(), WS_SILENT_RELOGIN_MS);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async _handleSilent() {
|
|
180
|
+
if (this.closed || this.otp || this._reloggingIn || this._silentReloginsLeft <= 0) return;
|
|
181
|
+
this._silentReloginsLeft--;
|
|
182
|
+
this._reloggingIn = true;
|
|
183
|
+
try {
|
|
184
|
+
// Re-login (de-duped at the provider) to get a token that the WS will accept,
|
|
185
|
+
// then reconnect this socket with it. If the token was actually fine, this is
|
|
186
|
+
// a cheap no-op reconnect that costs nothing but a fresh socket.
|
|
187
|
+
const epoch = this.provider.tokenEpoch;
|
|
188
|
+
await this.provider.relogin(epoch);
|
|
189
|
+
} catch { /* keep the old socket; nothing better to do */ }
|
|
190
|
+
finally { this._reloggingIn = false; }
|
|
191
|
+
if (this.closed || this.otp) return;
|
|
192
|
+
try { if (this.ws) this.ws.terminate(); } catch { /* */ }
|
|
193
|
+
this._connect(); // reconnect with the (possibly refreshed) current token
|
|
194
|
+
}
|
|
195
|
+
|
|
78
196
|
_connect() {
|
|
79
|
-
|
|
197
|
+
const token = this._currentToken();
|
|
198
|
+
this.ws = new WebSocket(`${WS_URL}?token=${token}`);
|
|
80
199
|
this._openP = new Promise((resolve) => {
|
|
81
200
|
this.ws.once('open', resolve);
|
|
82
201
|
this.ws.once('error', resolve); // resolve anyway so ready() never hangs
|
|
83
202
|
});
|
|
203
|
+
this.ws.on('open', () => {
|
|
204
|
+
this._connectedAt = Date.now();
|
|
205
|
+
this._armSilentTimer(); // start the dead-token watchdog
|
|
206
|
+
});
|
|
84
207
|
this.ws.on('message', (raw) => {
|
|
85
208
|
let msg; try { msg = JSON.parse(raw.toString()); } catch { return; }
|
|
86
209
|
// OTP frame: {message, otpMessage}. Ack frame {serialNumber,mobileId,resend} is ignored.
|
|
87
210
|
if (msg.otpMessage) {
|
|
88
211
|
const m = String(msg.otpMessage).match(OTP_RE);
|
|
89
|
-
if (m) { this.otp = m[1]; this.arrived = true; }
|
|
212
|
+
if (m) { this.otp = m[1]; this.arrived = true; this._clearSilentTimer(); }
|
|
90
213
|
}
|
|
91
214
|
});
|
|
92
215
|
this.ws.on('error', () => { /* swallow — poll() returns nothing; we auto-reconnect */ });
|
|
93
216
|
// Auto-reconnect if the socket drops before the OTP arrives (so a dropped WS doesn't
|
|
94
217
|
// silently lose the push — this is the "purchased but never got OTP" symptom).
|
|
95
218
|
this.ws.on('close', () => {
|
|
96
|
-
|
|
97
|
-
|
|
219
|
+
this._clearSilentTimer();
|
|
220
|
+
if (this.closed || this.otp || this._reloggingIn) return;
|
|
221
|
+
this._reconnectTimer = setTimeout(() => { if (!this.closed && !this.otp) this._connect(); }, 1000);
|
|
98
222
|
});
|
|
99
223
|
}
|
|
100
224
|
|
|
101
|
-
// Wait until the WS is OPEN (so we never send the
|
|
225
|
+
// Wait until the WS is OPEN (so we never send the OTP before we can receive the push).
|
|
102
226
|
async ready(timeoutMs = 8000) {
|
|
103
227
|
await Promise.race([this._openP, new Promise((r) => setTimeout(r, timeoutMs))]);
|
|
104
228
|
return this.ws.readyState === WebSocket.OPEN;
|
|
@@ -122,5 +246,10 @@ class OTPCartStream {
|
|
|
122
246
|
return false;
|
|
123
247
|
}
|
|
124
248
|
|
|
125
|
-
close() {
|
|
249
|
+
close() {
|
|
250
|
+
this.closed = true;
|
|
251
|
+
this._clearSilentTimer();
|
|
252
|
+
if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; }
|
|
253
|
+
try { this.ws.close(); } catch { /* */ }
|
|
254
|
+
}
|
|
126
255
|
}
|
package/src/util.js
CHANGED
|
@@ -84,11 +84,13 @@ export async function saveSessions(api, cid, { campaignName, out } = {}) {
|
|
|
84
84
|
const minutesUsed = []; // coupon_eligible=false → already placed a Minutes order
|
|
85
85
|
const unchecked = []; // coupon_eligible absent → check wasn't done for this account
|
|
86
86
|
|
|
87
|
+
let hasChecked = false;
|
|
87
88
|
for (const a of accounts) {
|
|
88
89
|
const sess = a.session;
|
|
89
90
|
if (!sess) continue;
|
|
90
91
|
const eligible = a.coupon_eligible;
|
|
91
|
-
if (
|
|
92
|
+
if (eligible !== undefined && eligible !== null) {
|
|
93
|
+
hasChecked = true;
|
|
92
94
|
(eligible ? minutesFree : minutesUsed).push(sess);
|
|
93
95
|
} else {
|
|
94
96
|
unchecked.push(sess);
|
|
@@ -96,7 +98,7 @@ export async function saveSessions(api, cid, { campaignName, out } = {}) {
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
const results = [];
|
|
99
|
-
if (checkMinutes && (minutesFree.length || minutesUsed.length)) {
|
|
101
|
+
if (hasChecked || (checkMinutes && (minutesFree.length || minutesUsed.length))) {
|
|
100
102
|
const destFree = write('-minutes-free', minutesFree);
|
|
101
103
|
if (destFree) results.push({ label: '🟢 minutes-free (₹100 coupon)', path: destFree, count: minutesFree.length });
|
|
102
104
|
|
package/src/worker.js
CHANGED
|
@@ -28,6 +28,8 @@ export class Worker {
|
|
|
28
28
|
this.concurrency = Math.max(1, Math.min(concurrency, requested));
|
|
29
29
|
this.checkMinutes = checkMinutes;
|
|
30
30
|
this.onEvent = onEvent || (() => {});
|
|
31
|
+
// Surface OTPCart re-logins (session taken over by another login) in the UI.
|
|
32
|
+
if (provider && 'onNotice' in provider) provider.onNotice = (msg) => this.onEvent('warn', `⚠ ${msg}`);
|
|
31
33
|
// onFailure({mobile, reason}) → Promise<boolean>: true = keep going, false = stop campaign.
|
|
32
34
|
this.onFailure = onFailure || null;
|
|
33
35
|
this.emailMode = !!emailMode; // also attach a newaddr.com email to each account
|
|
@@ -242,6 +244,9 @@ export class Worker {
|
|
|
242
244
|
|
|
243
245
|
this._emit(slot, { phase: 'saving', detail: 'saving session' });
|
|
244
246
|
this.log(`SUCCESS ${number.mobile} — session extracted (₹${number.cost})${res.email_linked ? ` + email ${res.linked_email}` : ''}`);
|
|
247
|
+
res.status = 'success';
|
|
248
|
+
res.cost = number.cost;
|
|
249
|
+
res.session = session;
|
|
245
250
|
const doneDetail = res.email_linked ? `extracted + ${res.linked_email}` : 'extracted';
|
|
246
251
|
this._emit(slot, { phase: 'done', detail: doneDetail });
|
|
247
252
|
releaseOnce(true, doneDetail); // success still releases the number (OTP already consumed)
|
|
@@ -463,6 +468,8 @@ export class ZeptoWorker {
|
|
|
463
468
|
this.concurrency = Math.max(1, Math.min(concurrency, requested));
|
|
464
469
|
this.certSha = certSha;
|
|
465
470
|
this.onEvent = onEvent || (() => {});
|
|
471
|
+
// Surface OTPCart re-logins (session taken over by another login) in the UI.
|
|
472
|
+
if (provider && 'onNotice' in provider) provider.onNotice = (msg) => this.onEvent('warn', `⚠ ${msg}`);
|
|
466
473
|
this.stats = { total: requested, succeeded: 0, failed: 0, cancelled: 0, generated: 0, attempts: 0, charges: 0 };
|
|
467
474
|
this.slots = {};
|
|
468
475
|
this.seq = 0;
|