scripter-x 1.0.1 → 1.0.7
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/api.js +3 -1
- package/src/commands.js +135 -12
- package/src/config.js +2 -0
- package/src/flipkart.js +79 -2
- package/src/index.js +6 -1
- package/src/logger.js +52 -0
- package/src/providers/kuku.js +238 -0
- package/src/providers/otpcart.js +31 -4
- package/src/providers/zauth.js +155 -0
- package/src/providers/zepto.js +131 -0
- package/src/ui/App.js +66 -4
- package/src/ui/Shell.js +4 -3
- package/src/ui/fullscreen.js +10 -2
- package/src/worker.js +191 -17
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -76,11 +76,13 @@ export class ApiClient {
|
|
|
76
76
|
async deleteCampaign(cid) { return this._req('DELETE', `/scriptx/campaigns/${cid}`); }
|
|
77
77
|
|
|
78
78
|
// ── server-side creds ──
|
|
79
|
-
async saveCreds(provider, { email, password, apiKey } = {}) {
|
|
79
|
+
async saveCreds(provider, { email, password, apiKey, masterNumber, masterPassword } = {}) {
|
|
80
80
|
const body = { provider };
|
|
81
81
|
if (email != null) body.email = email;
|
|
82
82
|
if (password != null) body.password = password;
|
|
83
83
|
if (apiKey != null) body.api_key = apiKey;
|
|
84
|
+
if (masterNumber != null) body.master_number = masterNumber;
|
|
85
|
+
if (masterPassword != null) body.master_password = masterPassword;
|
|
84
86
|
return this._req('POST', '/scriptx/otpcart-creds', { body });
|
|
85
87
|
}
|
|
86
88
|
async testCreds(provider) { return this._req('POST', '/scriptx/otpcart-creds/test', { body: { provider } }); }
|
package/src/commands.js
CHANGED
|
@@ -6,6 +6,11 @@ import { ApiClient, AuthError } from './api.js';
|
|
|
6
6
|
import { saveSessions } from './util.js';
|
|
7
7
|
import * as tp from './providers/tempotp.js';
|
|
8
8
|
import * as oc from './providers/otpcart.js';
|
|
9
|
+
import * as kuku from './providers/kuku.js';
|
|
10
|
+
import * as zepto from './providers/zepto.js';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { join, dirname } from 'node:path';
|
|
13
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
9
14
|
import { Worker } from './worker.js';
|
|
10
15
|
import { RunController } from './controller.js';
|
|
11
16
|
import { STATUS } from './theme.js';
|
|
@@ -38,6 +43,39 @@ function maskSecret(s) {
|
|
|
38
43
|
return s.slice(0, 4) + '…' + s.slice(-4);
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
// Resolve the kuku premium-MASTER creds for email-linking (use-saved / add-new, like the
|
|
47
|
+
// providers). Validates by logging in + checking premium. Saves locally AND to the server.
|
|
48
|
+
// Returns { number, password, masterSession } — masterSession is the logged-in premium master.
|
|
49
|
+
async function resolveKukuMaster(io, api) {
|
|
50
|
+
const cfg = config.load();
|
|
51
|
+
let number = cfg.kuku_master_number, password = cfg.kuku_master_password;
|
|
52
|
+
if (number && password) {
|
|
53
|
+
const choice = await io.select(`saved kuku master: ${maskSecret(number)}`, [
|
|
54
|
+
{ label: 'use saved', value: 'use', description: maskSecret(number) },
|
|
55
|
+
{ label: 'add new', value: 'new', description: 'enter a different premium master' }]);
|
|
56
|
+
if ((choice.value || choice) === 'new') { number = null; password = null; }
|
|
57
|
+
}
|
|
58
|
+
let fresh = false;
|
|
59
|
+
if (!number || !password) {
|
|
60
|
+
number = (await io.ask('kuku premium master account id')).trim();
|
|
61
|
+
password = (await io.ask('kuku master password', { mask: true })).trim();
|
|
62
|
+
fresh = true;
|
|
63
|
+
}
|
|
64
|
+
// validate: login + must be premium (else newaddr.com won't work)
|
|
65
|
+
let masterSession;
|
|
66
|
+
try { masterSession = await kuku.loginAccount(number, password); }
|
|
67
|
+
catch (e) { throw new Error(`kuku master login failed: ${e.message}`); }
|
|
68
|
+
if (!(await kuku.isPremium(masterSession))) {
|
|
69
|
+
throw new Error('kuku master account is NOT premium — needed for newaddr.com emails');
|
|
70
|
+
}
|
|
71
|
+
io.print(' ✓ kuku premium master verified');
|
|
72
|
+
if (fresh && await io.confirm('save these master creds (locally + your account)?', true)) {
|
|
73
|
+
config.setMany({ kuku_master_number: number, kuku_master_password: password });
|
|
74
|
+
try { await api.saveCreds('kuku', { masterNumber: number, masterPassword: password }); } catch { /* server save best-effort */ }
|
|
75
|
+
}
|
|
76
|
+
return { number, password, masterSession };
|
|
77
|
+
}
|
|
78
|
+
|
|
41
79
|
// ── provider setup (shared by run) ──
|
|
42
80
|
async function buildProvider(io, name) {
|
|
43
81
|
const cfg = config.load();
|
|
@@ -85,23 +123,46 @@ async function buildProvider(io, name) {
|
|
|
85
123
|
|
|
86
124
|
export async function run(io, api, args = {}) {
|
|
87
125
|
api = api || await getApi(io);
|
|
126
|
+
|
|
127
|
+
// MODE: extract JSON only, or also link a newaddr.com email to each account.
|
|
128
|
+
const mode = args.mode || (await io.select('what do you want?', [
|
|
129
|
+
{ label: 'JSON only', value: 'json', description: 'extract the session JSON' },
|
|
130
|
+
{ label: 'JSON + email link', value: 'json_email', description: 'also attach a newaddr.com email (premium kuku)' }])).value;
|
|
131
|
+
const emailMode = mode === 'json_email';
|
|
132
|
+
|
|
88
133
|
const pSel = args.provider || (await io.select('provider', [
|
|
89
134
|
{ label: 'otpcart', value: 'otpcart', description: 'email/password · WebSocket OTP' },
|
|
90
135
|
{ label: 'tempotp', value: 'tempotp', description: 'API key · HTTP poll' }])).value;
|
|
91
136
|
const provider = await buildProvider(io, pSel);
|
|
137
|
+
|
|
138
|
+
// In email mode: resolve the premium master + create ONE premium kuku account for the
|
|
139
|
+
// whole campaign (all emails come from it — no re-creating premium per email).
|
|
140
|
+
let kukuAccount = null;
|
|
141
|
+
if (emailMode) {
|
|
142
|
+
const { masterSession } = await resolveKukuMaster(io, api);
|
|
143
|
+
io.print(' ◉ preparing a premium kuku account for this campaign…');
|
|
144
|
+
const acc = await kuku.createAccount();
|
|
145
|
+
const linkedHash = await kuku.linkPremium(masterSession, acc.accountId, acc.password);
|
|
146
|
+
kuku.applyLinkedHash(acc, linkedHash);
|
|
147
|
+
if (!(await kuku.isPremium(acc))) throw new Error('could not premium-link the kuku account');
|
|
148
|
+
kukuAccount = acc;
|
|
149
|
+
io.print(' ✓ premium kuku account ready (newaddr.com emails enabled)');
|
|
150
|
+
}
|
|
151
|
+
|
|
92
152
|
const count = args.count || Number(await io.ask('how many JSONs?', { dflt: '5' }));
|
|
93
153
|
const concurrency = args.concurrency || Number(await io.ask('concurrency (keep low — one IP)', { dflt: String(config.get('default_concurrency', 2)) }));
|
|
94
154
|
const checkMinutes = args.checkMinutes != null ? args.checkMinutes : await io.confirm('check Minutes (₹100 coupon)?', false);
|
|
95
155
|
const name = args.name || `ScripterX-${Math.floor(Date.now() / 1000)}`;
|
|
96
156
|
|
|
97
|
-
if (!(await io.confirm(`start ${count} extraction(s) at concurrency ${concurrency}?`, true))) return;
|
|
157
|
+
if (!(await io.confirm(`start ${count} extraction(s) at concurrency ${concurrency}${emailMode ? ' + email link' : ''}?`, true))) return;
|
|
98
158
|
|
|
99
159
|
const cid = await api.createCampaign({ name, provider: pSel, count, concurrency, checkMinutes,
|
|
100
160
|
tempotpServiceId: provider.serviceId || '940', deepCheck: provider.deepCheck || false });
|
|
101
161
|
io.print(' ✓ campaign created · syncing to your dashboard');
|
|
102
162
|
|
|
103
163
|
const controller = new RunController({ name, provider: pSel, requested: count });
|
|
104
|
-
const worker = new Worker(api, provider, { campaignId: cid, requested: count, concurrency, checkMinutes,
|
|
164
|
+
const worker = new Worker(api, provider, { campaignId: cid, requested: count, concurrency, checkMinutes, name,
|
|
165
|
+
emailMode, kukuAccount,
|
|
105
166
|
onEvent: controller.handleEvent,
|
|
106
167
|
// on a failed/rejected OTP: pause, show the real reason, ask continue-or-stop
|
|
107
168
|
onFailure: async ({ mobile, reason }) => {
|
|
@@ -109,11 +170,25 @@ export async function run(io, api, args = {}) {
|
|
|
109
170
|
return io.confirm('OTP failed. Buy a new number and keep going?', true);
|
|
110
171
|
} });
|
|
111
172
|
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
173
|
+
controller.stop = () => worker.stop(); // let the App's double-Ctrl+C stop this run
|
|
174
|
+
const interactive = !!io.startRun;
|
|
175
|
+
if (interactive) io.startRun(controller); // mount the live view; App owns Ctrl+C
|
|
176
|
+
// One-shot mode (no App): double-Ctrl+C to exit — first press stops the campaign + warns,
|
|
177
|
+
// second within 2s exits. Interactive mode handles this at the App level.
|
|
178
|
+
let onSigint = null;
|
|
179
|
+
if (!interactive) {
|
|
180
|
+
let lastSig = 0;
|
|
181
|
+
onSigint = () => {
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
if (now - lastSig < 2000) { worker.stop(); process.exit(0); }
|
|
184
|
+
lastSig = now;
|
|
185
|
+
worker.stop();
|
|
186
|
+
io.print(' ⚠ stopping campaign — press Ctrl+C again to exit');
|
|
187
|
+
};
|
|
188
|
+
process.on('SIGINT', onSigint);
|
|
189
|
+
}
|
|
115
190
|
const stats = await worker.run();
|
|
116
|
-
process.off('SIGINT', onSigint);
|
|
191
|
+
if (onSigint) process.off('SIGINT', onSigint);
|
|
117
192
|
if (io.endRun) io.endRun();
|
|
118
193
|
|
|
119
194
|
io.print(` ✓ done — ${stats.succeeded} succeeded · ${stats.failed} failed · ${stats.cancelled} cancelled · ₹${stats.charges} spent`);
|
|
@@ -126,6 +201,47 @@ export async function run(io, api, args = {}) {
|
|
|
126
201
|
if (saved) { io.print(` ✓ saved ${saved.count} session(s) to:`); io.print(` ${saved.path}`, 'accent'); }
|
|
127
202
|
else io.print(` ! couldn't auto-save${lastErr ? ` (${lastErr.message})` : ''} — run \`export ${name}\` to retry`);
|
|
128
203
|
} else io.print(' ◉ no sessions extracted — nothing to save.');
|
|
204
|
+
if (worker.logPath) io.print(` ◉ log saved to: ${worker.logPath}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── zepto: local OTP login → extract session to {phone}-{timestamp}.json ──
|
|
208
|
+
// Dead-simple, no backend: enter a number → we SMS an OTP locally → enter the
|
|
209
|
+
// code → we verify and write the session JSON. Runs entirely on your IP.
|
|
210
|
+
export async function zeptoCmd(io, _api, args = {}) {
|
|
211
|
+
const number = (args.number || await io.ask('zepto phone number')).replace(/\D/g, '').slice(-10);
|
|
212
|
+
if (number.length !== 10) throw new Error('enter a 10-digit number');
|
|
213
|
+
|
|
214
|
+
const session = zepto.newSession(number);
|
|
215
|
+
io.print(` ◉ sending OTP to ${number} …`);
|
|
216
|
+
const s = await zepto.sendOtp(session);
|
|
217
|
+
if (!s.ok) throw new Error(`could not send OTP: ${s.msg || s.status}`);
|
|
218
|
+
io.print(' ✓ OTP sent');
|
|
219
|
+
|
|
220
|
+
const otp = (args.otp || await io.ask('enter the OTP')).trim();
|
|
221
|
+
const v = await zepto.verifyOtp(session, otp);
|
|
222
|
+
if (!v.ok) throw new Error(`OTP verify failed: ${v.error || v.status}`);
|
|
223
|
+
io.print(` ✓ logged in${v.user?.fullName ? ` as ${v.user.fullName}` : ''}`);
|
|
224
|
+
|
|
225
|
+
// Build the ZAUTH1 envelope — the format the ZeptoAuthManager tool imports.
|
|
226
|
+
const envelope = zepto.encodeEnvelope(zepto.toSessionKeys(session), args.cert);
|
|
227
|
+
|
|
228
|
+
// {phone}-{timestamp}.txt in the chosen dir (default ~/Downloads/scripterx/zepto)
|
|
229
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
230
|
+
const fname = `${number}-${stamp}.txt`;
|
|
231
|
+
let dest;
|
|
232
|
+
if (args.out) {
|
|
233
|
+
const expanded = args.out.startsWith('~') ? join(homedir(), args.out.slice(1)) : args.out;
|
|
234
|
+
dest = /\.(txt|json)$/i.test(expanded) ? expanded : join(expanded, fname);
|
|
235
|
+
} else {
|
|
236
|
+
const downloads = join(homedir(), 'Downloads');
|
|
237
|
+
dest = join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'zepto', fname);
|
|
238
|
+
}
|
|
239
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
240
|
+
writeFileSync(dest, envelope + '\n');
|
|
241
|
+
io.print(' ✓ envelope saved to:');
|
|
242
|
+
io.print(` ${dest}`, 'accent');
|
|
243
|
+
io.print(' ◉ paste into ZeptoAuthManager → Import:');
|
|
244
|
+
io.print(` ${envelope}`, 'accent');
|
|
129
245
|
}
|
|
130
246
|
|
|
131
247
|
export async function campaigns(io, api) {
|
|
@@ -183,11 +299,18 @@ export async function creds(io, api, args = {}) {
|
|
|
183
299
|
{ label: 'test', value: 'test', description: 'verify creds' }])).value;
|
|
184
300
|
if (action === 'show') {
|
|
185
301
|
const r = await api.creds();
|
|
186
|
-
for (const p of ['otpcart', 'tempotp']) io.print(` ◉ ${p}: ${r[p]?.has_creds ? '✓ saved' : '— not set'}`);
|
|
302
|
+
for (const p of ['otpcart', 'tempotp', 'kuku']) io.print(` ◉ ${p}: ${r[p]?.has_creds ? '✓ saved' : '— not set'}`);
|
|
187
303
|
} else if (action === 'save') {
|
|
188
|
-
const provider = (await io.select('provider', [
|
|
189
|
-
|
|
190
|
-
|
|
304
|
+
const provider = (await io.select('provider', [
|
|
305
|
+
{ label: 'otpcart', value: 'otpcart' }, { label: 'tempotp', value: 'tempotp' },
|
|
306
|
+
{ label: 'kuku', value: 'kuku', description: 'premium master (email linking)' }])).value;
|
|
307
|
+
if (provider === 'tempotp') await api.saveCreds('tempotp', { apiKey: (await io.ask('TempOTP API key')).trim() });
|
|
308
|
+
else if (provider === 'kuku') {
|
|
309
|
+
const number = (await io.ask('kuku premium master account id')).trim();
|
|
310
|
+
const password = (await io.ask('kuku master password', { mask: true })).trim();
|
|
311
|
+
config.setMany({ kuku_master_number: number, kuku_master_password: password });
|
|
312
|
+
await api.saveCreds('kuku', { masterNumber: number, masterPassword: password });
|
|
313
|
+
} else await api.saveCreds('otpcart', { email: (await io.ask('OTPCart email')).trim(), password: (await io.ask('OTPCart password', { mask: true })).trim() });
|
|
191
314
|
io.print(` ✓ ${provider} credentials saved.`);
|
|
192
315
|
} else {
|
|
193
316
|
const provider = (await io.select('provider', [{ label: 'otpcart', value: 'otpcart' }, { label: 'tempotp', value: 'tempotp' }])).value;
|
|
@@ -237,7 +360,7 @@ export async function configCmd(io, _api, args = {}) {
|
|
|
237
360
|
}
|
|
238
361
|
|
|
239
362
|
export function help(io) {
|
|
240
|
-
io.print(' commands: run · campaigns · export · balance · creds · stop · delete · whoami · config · update · logout · exit');
|
|
363
|
+
io.print(' commands: run · zepto · campaigns · export · balance · creds · stop · delete · whoami · config · update · logout · exit');
|
|
241
364
|
}
|
|
242
365
|
|
|
243
366
|
export async function update(io) {
|
|
@@ -250,6 +373,6 @@ export async function update(io) {
|
|
|
250
373
|
|
|
251
374
|
// dispatch table used by the interactive shell
|
|
252
375
|
export const REGISTRY = {
|
|
253
|
-
run, campaigns, export: exportCmd, balance, creds, stop, delete: del,
|
|
376
|
+
run, zepto: zeptoCmd, campaigns, export: exportCmd, balance, creds, stop, delete: del,
|
|
254
377
|
whoami, login, logout, config: configCmd, update, help,
|
|
255
378
|
};
|
package/src/config.js
CHANGED
package/src/flipkart.js
CHANGED
|
@@ -9,6 +9,7 @@ const FKUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit
|
|
|
9
9
|
'(KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 FKUA/msite/0.0.3/msite/Mobile channelType/brw';
|
|
10
10
|
const WEB_HOST = 'https://1.rome.api.flipkart.com';
|
|
11
11
|
const NATIVE_HOST = 'https://2.rome.api.flipkart.net';
|
|
12
|
+
const EMAIL_HOST = 'https://2.rome.api.flipkart.com'; // FK-5/6/7 email-attach host (.com)
|
|
12
13
|
|
|
13
14
|
export class BlockedError extends Error {}
|
|
14
15
|
export class WrongOTP extends Error {} // genuinely wrong/expired OTP code
|
|
@@ -71,11 +72,16 @@ export class FlipkartLogin {
|
|
|
71
72
|
'x-request-metaInfo': '{"actionType":"LOGIN_IDENTITY_VERIFY","pageUri":"questContext"}',
|
|
72
73
|
},
|
|
73
74
|
});
|
|
74
|
-
} catch (e) { throw new
|
|
75
|
+
} catch (e) { throw new TransientError(`send network error: ${e.message}`); }
|
|
76
|
+
// 529/5xx = Flipkart overloaded / blocking this IP — retryable.
|
|
77
|
+
if (res.status === 429 || res.status === 529 || res.status >= 500) {
|
|
78
|
+
throw new TransientError(`Flipkart busy on send (HTTP ${res.status})`);
|
|
79
|
+
}
|
|
75
80
|
parseCookies(res, this.jar);
|
|
76
81
|
const data = await res.json().catch(() => ({}));
|
|
77
82
|
const rid = data?.RESPONSE?.actionResponseContext?.requestId;
|
|
78
|
-
|
|
83
|
+
// no requestId on a 200 = soft anti-bot throttle — also retryable (often clears on retry).
|
|
84
|
+
if (!rid) throw new TransientError('blocked while sending OTP (no requestId — throttled)');
|
|
79
85
|
this.reqId = rid;
|
|
80
86
|
this.sn = this.jar.SN || '';
|
|
81
87
|
this.vid = this.sn ? this.sn.split('.')[0] : '';
|
|
@@ -126,6 +132,7 @@ export class FlipkartLogin {
|
|
|
126
132
|
return {
|
|
127
133
|
accountId: env.accountId || '', at: env.at || '', rt: env.rt || '', sn: env.sn || '',
|
|
128
134
|
secureToken, secureCookie: this.jar.S || '', ud, vd,
|
|
135
|
+
cookie_T: fresh.T || this.jar.T || '', // T cookie — needed for the email-attach calls
|
|
129
136
|
visitId: (env.sn || '').split('.')[0], nsid: env.nsid || '',
|
|
130
137
|
mobileNo: mobile.slice(-10), isLoggedIn: true, rt_expires_at: rtExpiry(env.rt || ''),
|
|
131
138
|
};
|
|
@@ -155,6 +162,76 @@ export class FlipkartLogin {
|
|
|
155
162
|
const count = (text.match(/"orderId"\s*:\s*"(OD\w+)"/g) || []).length;
|
|
156
163
|
return count === 0; // no orders ⇒ coupon available
|
|
157
164
|
}
|
|
165
|
+
|
|
166
|
+
// ─── Email attach (FK-5/6/7) — on the EMAIL host (2.rome.api.flipkart.com) ──────
|
|
167
|
+
// Shared headers for the email-attach calls (uses the logged-in session's tokens).
|
|
168
|
+
_emailHeaders(session) {
|
|
169
|
+
return {
|
|
170
|
+
'User-Agent': 'okhttp/4.9.2',
|
|
171
|
+
'X-User-Agent': nativeUa(session.visitId || this.vid),
|
|
172
|
+
'Content-Type': 'application/json',
|
|
173
|
+
at: session.at || '', sn: session.sn || '',
|
|
174
|
+
sc: session.secureCookie || '', securecookie: session.secureCookie || '',
|
|
175
|
+
flipkart_secure: 'true',
|
|
176
|
+
Origin: 'https://www.flipkart.com', Referer: 'https://www.flipkart.com/',
|
|
177
|
+
'X-Requested-With': 'com.flipkart.android',
|
|
178
|
+
Cookie: `T=${session.cookie_T || ''}; vd=${session.vd || ''}; ud=${session.ud || ''}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// FK-5: is an email already attached? Returns { emailAlready, email }.
|
|
183
|
+
async profileInfo(session) {
|
|
184
|
+
await throttle.wait();
|
|
185
|
+
const res = await fetch(`${EMAIL_HOST}/1/user/profile/info?v=${Date.now()}`, { headers: this._emailHeaders(session) });
|
|
186
|
+
if (res.status === 429 || res.status === 529 || res.status >= 500) throw new TransientError(`profile-info busy (HTTP ${res.status})`);
|
|
187
|
+
const data = await res.json().catch(() => ({}));
|
|
188
|
+
const r = data.RESPONSE || {};
|
|
189
|
+
// emailVerificationFlow:false = no email yet. If an email exists it shows in RESPONSE.
|
|
190
|
+
const existing = r.email || r.emailId || null;
|
|
191
|
+
return { emailAlready: r.emailVerificationFlow === true || !!existing, email: existing };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// FK-6: send OTP to BOTH the new email AND the registered phone. Returns the two requestIds.
|
|
195
|
+
async sendEmailOtp(session, email) {
|
|
196
|
+
await throttle.wait();
|
|
197
|
+
const res = await fetch(`${EMAIL_HOST}/8/user/otp/generate`, {
|
|
198
|
+
method: 'POST', headers: this._emailHeaders(session),
|
|
199
|
+
body: JSON.stringify({ flowType: 'ADD_UPDATE_IDENTIFIER', newLoginId: email }),
|
|
200
|
+
});
|
|
201
|
+
if (res.status === 429 || res.status === 529 || res.status >= 500) throw new TransientError(`email-otp-gen busy (HTTP ${res.status})`);
|
|
202
|
+
const data = await res.json().catch(() => ({}));
|
|
203
|
+
const info = data?.RESPONSE?.otpIdentifierInfo || [];
|
|
204
|
+
let emailReqId = null, phoneReqId = null, phoneUserId = null;
|
|
205
|
+
for (const it of info) {
|
|
206
|
+
if (String(it.userId || '').includes('@')) emailReqId = it.requestId;
|
|
207
|
+
else { phoneReqId = it.requestId; phoneUserId = it.userId; } // "91-<10digit>"
|
|
208
|
+
}
|
|
209
|
+
if (!emailReqId || !phoneReqId) {
|
|
210
|
+
const msg = flipkartErrorMessage(data, JSON.stringify(data));
|
|
211
|
+
throw new TransientError(msg || 'email-otp-gen: missing requestIds');
|
|
212
|
+
}
|
|
213
|
+
return { emailReqId, phoneReqId, phoneUserId };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// FK-7: submit BOTH OTPs (email + phone) to link the email. Returns { ok, message }.
|
|
217
|
+
async verifyEmailOtp(session, { email, emailOtp, emailReqId, phoneUserId, phoneOtp, phoneReqId }) {
|
|
218
|
+
await throttle.wait();
|
|
219
|
+
const res = await fetch(`${EMAIL_HOST}/6/user/identity`, {
|
|
220
|
+
method: 'POST', headers: this._emailHeaders(session),
|
|
221
|
+
body: JSON.stringify({ otpInfo: [
|
|
222
|
+
{ otp: emailOtp, userId: email, requestId: emailReqId },
|
|
223
|
+
{ otp: phoneOtp, userId: phoneUserId, requestId: phoneReqId },
|
|
224
|
+
] }),
|
|
225
|
+
});
|
|
226
|
+
if (res.status === 429 || res.status === 529 || res.status >= 500) throw new TransientError(`identity-verify busy (HTTP ${res.status})`);
|
|
227
|
+
const text = await res.text();
|
|
228
|
+
let data; try { data = JSON.parse(text); } catch { throw new TransientError('identity-verify: non-JSON'); }
|
|
229
|
+
const msg = data?.RESPONSE?.message || '';
|
|
230
|
+
if (/successfully linked|linked to account/i.test(msg)) return { ok: true, message: msg };
|
|
231
|
+
// wrong/expired OTP or other failure → surface the real reason (not retryable as transient)
|
|
232
|
+
const err = flipkartErrorMessage(data, text) || msg || `link failed (HTTP ${res.status})`;
|
|
233
|
+
throw new WrongOTP(err);
|
|
234
|
+
}
|
|
158
235
|
}
|
|
159
236
|
|
|
160
237
|
function rtExpiry(rt) {
|
package/src/index.js
CHANGED
|
@@ -43,6 +43,9 @@ const HELP = `${paint.bold('scripterx')} — local Flipkart session extractor
|
|
|
43
43
|
scripterx interactive shell (recommended)
|
|
44
44
|
scripterx run [flags] run an extraction directly
|
|
45
45
|
--provider otpcart|tempotp --count N --concurrency N --name <n> --check-minutes
|
|
46
|
+
scripterx zepto [flags] local Zepto OTP login → {phone}-{timestamp}.txt (ZAUTH1 envelope)
|
|
47
|
+
--number <10-digit> --otp <code> --out <file|dir> --cert <64hex>
|
|
48
|
+
(envelope keyed to ZeptoAuthManager cert; override with --cert or $ZAUTH_CERT_SHA)
|
|
46
49
|
scripterx campaigns | export [name] | balance | creds | stop | delete | whoami | config
|
|
47
50
|
scripterx --version`;
|
|
48
51
|
|
|
@@ -93,7 +96,7 @@ async function main() {
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
// ── one-shot mode ──
|
|
96
|
-
const map = { run: 'run', campaigns: 'campaigns', export: 'export', balance: 'balance',
|
|
99
|
+
const map = { run: 'run', zepto: 'zepto', campaigns: 'campaigns', export: 'export', balance: 'balance',
|
|
97
100
|
creds: 'creds', stop: 'stop', delete: 'delete', whoami: 'whoami', login: 'login',
|
|
98
101
|
logout: 'logout', config: 'config', update: 'update', help: 'help' };
|
|
99
102
|
const fnName = map[cmd];
|
|
@@ -102,6 +105,8 @@ async function main() {
|
|
|
102
105
|
provider: flags.provider, count: flags.count ? +flags.count : null,
|
|
103
106
|
concurrency: flags.concurrency ? +flags.concurrency : null, name: flags.name,
|
|
104
107
|
checkMinutes: flags['check-minutes'] === true ? true : (flags['check-minutes'] === false ? false : null),
|
|
108
|
+
mode: flags.email ? 'json_email' : flags.mode, // --email or --mode json_email
|
|
109
|
+
number: flags.number, otp: flags.otp, cert: flags.cert, // zepto: local OTP login → ZAUTH1 envelope
|
|
105
110
|
campaign: flags._[0], out: flags.out, action: flags._[0], value: flags._[1],
|
|
106
111
|
};
|
|
107
112
|
try { await REGISTRY[fnName](cliIo, null, args); }
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Per-campaign logging → ~/Downloads/scripterx/temp/logs/<campaign>-<id>.log
|
|
2
|
+
// Keeps a clean, timestamped, append-only log of everything that happened in a run, so
|
|
3
|
+
// failures/charges can be audited after the fact. Flushed to disk at campaign end.
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { mkdirSync, appendFileSync, existsSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
function logDir() {
|
|
9
|
+
const dir = join(homedir(), 'Downloads', 'scripterx', 'temp', 'logs');
|
|
10
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
11
|
+
return dir;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function stamp() {
|
|
15
|
+
const d = new Date();
|
|
16
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
17
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class CampaignLogger {
|
|
21
|
+
constructor({ campaignId, name, provider }) {
|
|
22
|
+
const safe = (name || campaignId).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || campaignId.slice(0, 8);
|
|
23
|
+
this.path = join(logDir(), `${safe}-${campaignId.slice(0, 8)}.log`);
|
|
24
|
+
this.buffer = [];
|
|
25
|
+
this.header(name, provider, campaignId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
header(name, provider, cid) {
|
|
29
|
+
const d = new Date();
|
|
30
|
+
this.write(`──────────────────────────────────────────────`);
|
|
31
|
+
this.write(`ScripterX campaign log`);
|
|
32
|
+
this.write(` name: ${name}`);
|
|
33
|
+
this.write(` provider: ${provider}`);
|
|
34
|
+
this.write(` id: ${cid}`);
|
|
35
|
+
this.write(` started: ${d.toISOString()}`);
|
|
36
|
+
this.write(`──────────────────────────────────────────────`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// append one clean, timestamped line (buffered; written immediately too for crash safety)
|
|
40
|
+
write(line) {
|
|
41
|
+
const entry = `[${stamp()}] ${line}\n`;
|
|
42
|
+
this.buffer.push(entry);
|
|
43
|
+
try { appendFileSync(this.path, entry); } catch { /* logging must never crash the run */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
summary(stats) {
|
|
47
|
+
this.write(`──────────────────────────────────────────────`);
|
|
48
|
+
this.write(`SUMMARY ✓ ${stats.succeeded} ✗ ${stats.failed} ⊘ ${stats.cancelled} · ${stats.generated} numbers · ₹${stats.charges} spent`);
|
|
49
|
+
this.write(`finished: ${new Date().toISOString()}`);
|
|
50
|
+
this.write(`──────────────────────────────────────────────`);
|
|
51
|
+
}
|
|
52
|
+
}
|