scripter-x 1.0.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripter-x",
3
- "version": "1.0.2",
3
+ "version": "1.0.7",
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();
@@ -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
- if (io.startRun) io.startRun(controller); // mount the live view (interactive)
113
- const onSigint = () => worker.stop();
114
- process.on('SIGINT', onSigint);
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', [{ 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 }) });
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
@@ -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,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
+ }