scripter-x 1.0.2 → 1.0.8

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.2",
3
+ "version": "1.0.8",
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/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();
@@ -84,24 +122,53 @@ async function buildProvider(io, name) {
84
122
  }
85
123
 
86
124
  export async function run(io, api, args = {}) {
125
+ // TARGET: Flipkart (the backend campaign flow) or Zepto (local OTP → ZAUTH1 envelope).
126
+ const target = args.target || (args.number ? 'zepto' : (await io.select('what to extract?', [
127
+ { label: 'Flipkart', value: 'flipkart', description: 'session JSON via OTP provider (campaign)' },
128
+ { label: 'Zepto', value: 'zepto', description: 'local OTP login → ZAUTH1 envelope for AuthManager' }])).value);
129
+ if (target === 'zepto') return zeptoCmd(io, api, args);
130
+
87
131
  api = api || await getApi(io);
132
+
133
+ // MODE: extract JSON only, or also link a newaddr.com email to each account.
134
+ const mode = args.mode || (await io.select('what do you want?', [
135
+ { label: 'JSON only', value: 'json', description: 'extract the session JSON' },
136
+ { label: 'JSON + email link', value: 'json_email', description: 'also attach a newaddr.com email (premium kuku)' }])).value;
137
+ const emailMode = mode === 'json_email';
138
+
88
139
  const pSel = args.provider || (await io.select('provider', [
89
140
  { label: 'otpcart', value: 'otpcart', description: 'email/password · WebSocket OTP' },
90
141
  { label: 'tempotp', value: 'tempotp', description: 'API key · HTTP poll' }])).value;
91
142
  const provider = await buildProvider(io, pSel);
143
+
144
+ // In email mode: resolve the premium master + create ONE premium kuku account for the
145
+ // whole campaign (all emails come from it — no re-creating premium per email).
146
+ let kukuAccount = null;
147
+ if (emailMode) {
148
+ const { masterSession } = await resolveKukuMaster(io, api);
149
+ io.print(' ◉ preparing a premium kuku account for this campaign…');
150
+ const acc = await kuku.createAccount();
151
+ const linkedHash = await kuku.linkPremium(masterSession, acc.accountId, acc.password);
152
+ kuku.applyLinkedHash(acc, linkedHash);
153
+ if (!(await kuku.isPremium(acc))) throw new Error('could not premium-link the kuku account');
154
+ kukuAccount = acc;
155
+ io.print(' ✓ premium kuku account ready (newaddr.com emails enabled)');
156
+ }
157
+
92
158
  const count = args.count || Number(await io.ask('how many JSONs?', { dflt: '5' }));
93
159
  const concurrency = args.concurrency || Number(await io.ask('concurrency (keep low — one IP)', { dflt: String(config.get('default_concurrency', 2)) }));
94
160
  const checkMinutes = args.checkMinutes != null ? args.checkMinutes : await io.confirm('check Minutes (₹100 coupon)?', false);
95
161
  const name = args.name || `ScripterX-${Math.floor(Date.now() / 1000)}`;
96
162
 
97
- if (!(await io.confirm(`start ${count} extraction(s) at concurrency ${concurrency}?`, true))) return;
163
+ if (!(await io.confirm(`start ${count} extraction(s) at concurrency ${concurrency}${emailMode ? ' + email link' : ''}?`, true))) return;
98
164
 
99
165
  const cid = await api.createCampaign({ name, provider: pSel, count, concurrency, checkMinutes,
100
166
  tempotpServiceId: provider.serviceId || '940', deepCheck: provider.deepCheck || false });
101
167
  io.print(' ✓ campaign created · syncing to your dashboard');
102
168
 
103
169
  const controller = new RunController({ name, provider: pSel, requested: count });
104
- const worker = new Worker(api, provider, { campaignId: cid, requested: count, concurrency, checkMinutes,
170
+ const worker = new Worker(api, provider, { campaignId: cid, requested: count, concurrency, checkMinutes, name,
171
+ emailMode, kukuAccount,
105
172
  onEvent: controller.handleEvent,
106
173
  // on a failed/rejected OTP: pause, show the real reason, ask continue-or-stop
107
174
  onFailure: async ({ mobile, reason }) => {
@@ -109,11 +176,25 @@ export async function run(io, api, args = {}) {
109
176
  return io.confirm('OTP failed. Buy a new number and keep going?', true);
110
177
  } });
111
178
 
112
- if (io.startRun) io.startRun(controller); // mount the live view (interactive)
113
- const onSigint = () => worker.stop();
114
- process.on('SIGINT', onSigint);
179
+ controller.stop = () => worker.stop(); // let the App's double-Ctrl+C stop this run
180
+ const interactive = !!io.startRun;
181
+ if (interactive) io.startRun(controller); // mount the live view; App owns Ctrl+C
182
+ // One-shot mode (no App): double-Ctrl+C to exit — first press stops the campaign + warns,
183
+ // second within 2s exits. Interactive mode handles this at the App level.
184
+ let onSigint = null;
185
+ if (!interactive) {
186
+ let lastSig = 0;
187
+ onSigint = () => {
188
+ const now = Date.now();
189
+ if (now - lastSig < 2000) { worker.stop(); process.exit(0); }
190
+ lastSig = now;
191
+ worker.stop();
192
+ io.print(' ⚠ stopping campaign — press Ctrl+C again to exit');
193
+ };
194
+ process.on('SIGINT', onSigint);
195
+ }
115
196
  const stats = await worker.run();
116
- process.off('SIGINT', onSigint);
197
+ if (onSigint) process.off('SIGINT', onSigint);
117
198
  if (io.endRun) io.endRun();
118
199
 
119
200
  io.print(` ✓ done — ${stats.succeeded} succeeded · ${stats.failed} failed · ${stats.cancelled} cancelled · ₹${stats.charges} spent`);
@@ -126,6 +207,47 @@ export async function run(io, api, args = {}) {
126
207
  if (saved) { io.print(` ✓ saved ${saved.count} session(s) to:`); io.print(` ${saved.path}`, 'accent'); }
127
208
  else io.print(` ! couldn't auto-save${lastErr ? ` (${lastErr.message})` : ''} — run \`export ${name}\` to retry`);
128
209
  } else io.print(' ◉ no sessions extracted — nothing to save.');
210
+ if (worker.logPath) io.print(` ◉ log saved to: ${worker.logPath}`);
211
+ }
212
+
213
+ // ── zepto: local OTP login → extract session to {phone}-{timestamp}.json ──
214
+ // Dead-simple, no backend: enter a number → we SMS an OTP locally → enter the
215
+ // code → we verify and write the session JSON. Runs entirely on your IP.
216
+ export async function zeptoCmd(io, _api, args = {}) {
217
+ const number = (args.number || await io.ask('zepto phone number')).replace(/\D/g, '').slice(-10);
218
+ if (number.length !== 10) throw new Error('enter a 10-digit number');
219
+
220
+ const session = zepto.newSession(number);
221
+ io.print(` ◉ sending OTP to ${number} …`);
222
+ const s = await zepto.sendOtp(session);
223
+ if (!s.ok) throw new Error(`could not send OTP: ${s.msg || s.status}`);
224
+ io.print(' ✓ OTP sent');
225
+
226
+ const otp = (args.otp || await io.ask('enter the OTP')).trim();
227
+ const v = await zepto.verifyOtp(session, otp);
228
+ if (!v.ok) throw new Error(`OTP verify failed: ${v.error || v.status}`);
229
+ io.print(` ✓ logged in${v.user?.fullName ? ` as ${v.user.fullName}` : ''}`);
230
+
231
+ // Build the ZAUTH1 envelope — the format the ZeptoAuthManager tool imports.
232
+ const envelope = zepto.encodeEnvelope(zepto.toSessionKeys(session), args.cert);
233
+
234
+ // {phone}-{timestamp}.txt in the chosen dir (default ~/Downloads/scripterx/zepto)
235
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
236
+ const fname = `${number}-${stamp}.txt`;
237
+ let dest;
238
+ if (args.out) {
239
+ const expanded = args.out.startsWith('~') ? join(homedir(), args.out.slice(1)) : args.out;
240
+ dest = /\.(txt|json)$/i.test(expanded) ? expanded : join(expanded, fname);
241
+ } else {
242
+ const downloads = join(homedir(), 'Downloads');
243
+ dest = join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'zepto', fname);
244
+ }
245
+ mkdirSync(dirname(dest), { recursive: true });
246
+ writeFileSync(dest, envelope + '\n');
247
+ io.print(' ✓ envelope saved to:');
248
+ io.print(` ${dest}`, 'accent');
249
+ io.print(' ◉ paste into ZeptoAuthManager → Import:');
250
+ io.print(` ${envelope}`, 'accent');
129
251
  }
130
252
 
131
253
  export async function campaigns(io, api) {
@@ -183,11 +305,18 @@ export async function creds(io, api, args = {}) {
183
305
  { label: 'test', value: 'test', description: 'verify creds' }])).value;
184
306
  if (action === 'show') {
185
307
  const r = await api.creds();
186
- for (const p of ['otpcart', 'tempotp']) io.print(` ◉ ${p}: ${r[p]?.has_creds ? '✓ saved' : '— not set'}`);
308
+ for (const p of ['otpcart', 'tempotp', 'kuku']) io.print(` ◉ ${p}: ${r[p]?.has_creds ? '✓ saved' : '— not set'}`);
187
309
  } else if (action === 'save') {
188
- const provider = (await io.select('provider', [{ label: 'otpcart', value: 'otpcart' }, { label: 'tempotp', value: 'tempotp' }])).value;
189
- if (provider === 'tempotp') await api.saveCreds('tempotp', { apiKey: await io.ask('TempOTP API key') });
190
- else await api.saveCreds('otpcart', { email: await io.ask('OTPCart email'), password: await io.ask('OTPCart password', { mask: true }) });
310
+ const provider = (await io.select('provider', [
311
+ { label: 'otpcart', value: 'otpcart' }, { label: 'tempotp', value: 'tempotp' },
312
+ { label: 'kuku', value: 'kuku', description: 'premium master (email linking)' }])).value;
313
+ if (provider === 'tempotp') await api.saveCreds('tempotp', { apiKey: (await io.ask('TempOTP API key')).trim() });
314
+ else if (provider === 'kuku') {
315
+ const number = (await io.ask('kuku premium master account id')).trim();
316
+ const password = (await io.ask('kuku master password', { mask: true })).trim();
317
+ config.setMany({ kuku_master_number: number, kuku_master_password: password });
318
+ await api.saveCreds('kuku', { masterNumber: number, masterPassword: password });
319
+ } else await api.saveCreds('otpcart', { email: (await io.ask('OTPCart email')).trim(), password: (await io.ask('OTPCart password', { mask: true })).trim() });
191
320
  io.print(` ✓ ${provider} credentials saved.`);
192
321
  } else {
193
322
  const provider = (await io.select('provider', [{ label: 'otpcart', value: 'otpcart' }, { label: 'tempotp', value: 'tempotp' }])).value;
@@ -237,7 +366,7 @@ export async function configCmd(io, _api, args = {}) {
237
366
  }
238
367
 
239
368
  export function help(io) {
240
- io.print(' commands: run · campaigns · export · balance · creds · stop · delete · whoami · config · update · logout · exit');
369
+ io.print(' commands: run · zepto · campaigns · export · balance · creds · stop · delete · whoami · config · update · logout · exit');
241
370
  }
242
371
 
243
372
  export async function update(io) {
@@ -250,6 +379,6 @@ export async function update(io) {
250
379
 
251
380
  // dispatch table used by the interactive shell
252
381
  export const REGISTRY = {
253
- run, campaigns, export: exportCmd, balance, creds, stop, delete: del,
382
+ run, zepto: zeptoCmd, campaigns, export: exportCmd, balance, creds, stop, delete: del,
254
383
  whoami, login, logout, config: configCmd, update, help,
255
384
  };
package/src/config.js CHANGED
@@ -15,6 +15,8 @@ const DEFAULTS = {
15
15
  tempotp_api_key: null,
16
16
  otpcart_email: null,
17
17
  otpcart_password: null,
18
+ kuku_master_number: null, // kuku.lu premium master account id (for email linking)
19
+ kuku_master_password: null,
18
20
  default_concurrency: 2,
19
21
  };
20
22
 
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 BlockedError(`send failed: ${e.message}`); }
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
- if (!rid) throw new BlockedError('blocked while sending OTP (no requestId)');
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,9 @@ 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
+ target: flags.target, // run: 'flipkart' | 'zepto'
110
+ number: flags.number, otp: flags.otp, cert: flags.cert, // zepto: local OTP login → ZAUTH1 envelope
105
111
  campaign: flags._[0], out: flags.out, action: flags._[0], value: flags._[1],
106
112
  };
107
113
  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
+ }