scripter-x 1.0.25 → 1.0.27

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.25",
3
+ "version": "1.0.27",
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/flipkart.js CHANGED
@@ -88,34 +88,68 @@ export class FlipkartLogin {
88
88
  this.isNewUser = false; // set by sendOtp: new number → verify as SIGNUP, not LOGIN
89
89
  }
90
90
 
91
- async sendOtp(mobile) {
92
- await throttle.wait();
91
+ // Send-OTP variants. The NATIVE app flow (okhttp UA, .net host, app body with
92
+ // addAppHash/phoneNumberFormat) is far less throttled than the browser/web flow —
93
+ // it mirrors exactly what the Flipkart app sends (captured). We try native first,
94
+ // and fall back to the web flow only if native yields no requestId.
95
+ async _sendNative(mobile) {
96
+ const body = { actionRequestContext: {
97
+ type: 'LOGIN_IDENTITY_VERIFY', loginId: mobile.slice(-10), loginIdPrefix: '+91',
98
+ phoneNumberFormat: 'E164', addAppHash: true, loginType: 'MOBILE',
99
+ verificationType: 'OTP', sourceContext: 'DEFAULT',
100
+ correlationId: null, snaFlowId: null, clientQueryParamMap: null } };
101
+ const res = await fetch(`${NATIVE_HOST}/1/action/view`, {
102
+ method: 'POST', body: JSON.stringify(body),
103
+ headers: {
104
+ 'User-Agent': 'okhttp/4.9.2', 'X-User-Agent': nativeUa(this.vid),
105
+ 'Content-Type': 'application/json; charset=UTF-8',
106
+ 'x-request-metaInfo': '{"actionType":"LOGIN_IDENTITY_VERIFY","pageUri":"questContext"}',
107
+ },
108
+ });
109
+ return res;
110
+ }
111
+
112
+ async _sendWeb(mobile) {
93
113
  const body = { actionRequestContext: {
94
114
  type: 'LOGIN_IDENTITY_VERIFY', loginIdPrefix: '+91', loginId: mobile.slice(-10),
95
115
  clientQueryParamMap: { ret: '/my-account', entryPage: 'DEFAULT' },
96
116
  loginType: 'MOBILE', verificationType: 'OTP', screenName: 'LOGIN_V4_MOBILE',
97
117
  triggerSna: false, sourceContext: 'DEFAULT' } };
98
- let res;
99
- try {
100
- res = await fetch(`${WEB_HOST}/1/action/view`, {
101
- method: 'POST', body: JSON.stringify(body),
102
- headers: {
103
- Accept: '*/*', 'Content-Type': 'application/json',
104
- 'X-User-Agent': FKUA, 'User-Agent': FKUA,
105
- Origin: 'https://www.flipkart.com', Referer: 'https://www.flipkart.com/',
106
- 'sec-ch-ua-mobile': '?1', 'sec-ch-ua-platform': '"iOS"', 'sec-fetch-site': 'same-site',
107
- 'x-request-metaInfo': '{"actionType":"LOGIN_IDENTITY_VERIFY","pageUri":"questContext"}',
108
- },
109
- });
110
- } catch (e) { throw new TransientError(`send network error: ${e.message}`); }
111
- // 529/5xx = Flipkart overloaded / blocking this IP — retryable.
112
- if (res.status === 429 || res.status === 529 || res.status >= 500) {
113
- throw new TransientError(`Flipkart busy on send (HTTP ${res.status})`);
118
+ return fetch(`${WEB_HOST}/1/action/view`, {
119
+ method: 'POST', body: JSON.stringify(body),
120
+ headers: {
121
+ Accept: '*/*', 'Content-Type': 'application/json',
122
+ 'X-User-Agent': FKUA, 'User-Agent': FKUA,
123
+ Origin: 'https://www.flipkart.com', Referer: 'https://www.flipkart.com/',
124
+ 'sec-ch-ua-mobile': '?1', 'sec-ch-ua-platform': '"iOS"', 'sec-fetch-site': 'same-site',
125
+ 'x-request-metaInfo': '{"actionType":"LOGIN_IDENTITY_VERIFY","pageUri":"questContext"}',
126
+ },
127
+ });
128
+ }
129
+
130
+ async sendOtp(mobile) {
131
+ await throttle.wait();
132
+ // The WEB flow establishes the SN session cookie that VERIFY needs, so it runs
133
+ // FIRST. If it throttles (200 but no requestId), retry on the NATIVE app flow —
134
+ // which is much less throttled and now reuses the SN web just set. Either flow
135
+ // that yields a requestId wins.
136
+ let res, data, rid;
137
+ for (const send of [this._sendWeb.bind(this), this._sendNative.bind(this)]) {
138
+ try {
139
+ res = await send(mobile);
140
+ } catch (e) { throw new TransientError(`send network error: ${e.message}`); }
141
+ // 529/5xx = Flipkart overloaded / blocking this IP — retryable.
142
+ if (res.status === 429 || res.status === 529 || res.status >= 500) {
143
+ throw new TransientError(`Flipkart busy on send (HTTP ${res.status})`);
144
+ }
145
+ parseCookies(res, this.jar);
146
+ // capture SN as soon as any flow provides it (web normally does)
147
+ if (this.jar.SN) { this.sn = this.jar.SN; this.vid = this.sn.split('.')[0]; }
148
+ data = await res.json().catch(() => ({}));
149
+ rid = data?.RESPONSE?.actionResponseContext?.requestId;
150
+ if (rid) break; // got it — done
114
151
  }
115
- parseCookies(res, this.jar);
116
- const data = await res.json().catch(() => ({}));
117
- const rid = data?.RESPONSE?.actionResponseContext?.requestId;
118
- // no requestId on a 200 = soft anti-bot throttle — also retryable (often clears on retry).
152
+ // no requestId after both flows = soft anti-bot throttle — retryable (often clears on retry).
119
153
  if (!rid) throw new TransientError('blocked while sending OTP (no requestId — throttled)');
120
154
  this.reqId = rid;
121
155
  // NEW vs EXISTING account: for an UNREGISTERED number FK still sends the OTP but
@@ -205,11 +239,15 @@ export class FlipkartLogin {
205
239
  parseCookies(res, fresh);
206
240
  const ud = fresh.ud || this.jar.ud || '';
207
241
  const vd = fresh.vd || '';
242
+ // secureToken: prefer the value FK returns in the SESSION (vid:vid); fall back to
243
+ // computing it from vid. (Was a free var in the old single-method verify — the
244
+ // SIGNUP/LOGIN split moved its definition into _verifyWith, hence recompute here.)
245
+ const secureToken = env.secureToken || (this.vid ? `${this.vid}:${this.vid}` : '');
208
246
  return {
209
247
  accountId: env.accountId || '', at: env.at || '', rt: env.rt || '', sn: env.sn || '',
210
248
  secureToken, secureCookie: this.jar.S || '', ud, vd,
211
249
  cookie_T: fresh.T || this.jar.T || '', // T cookie — needed for the email-attach calls
212
- visitId: (env.sn || '').split('.')[0], nsid: env.nsid || '',
250
+ visitId: env.vid || (env.sn || '').split('.')[0], nsid: env.nsid || '',
213
251
  mobileNo: mobile.slice(-10), isLoggedIn: true, rt_expires_at: rtExpiry(env.rt || ''),
214
252
  };
215
253
  }
package/src/ui/RunView.js CHANGED
@@ -30,14 +30,35 @@ function SlotRow({ slot, mobile, phase, detail, wait }) {
30
30
  );
31
31
  }
32
32
 
33
- function ResultRow({ status, mobile, detail, coupon, cost }) {
34
- const st = STATUS[status] || STATUS.pending;
33
+ // Map a result to a clean { glyph, label, color, note } for display. Driven by the
34
+ // worker's `category` so every outcome reads consistently (no "successed"/"success" mix).
35
+ function resultDisplay({ status, category, detail }) {
36
+ const C = COLORS;
37
+ switch (category) {
38
+ case 'fresh': return { glyph: '🆕', label: 'fresh account', color: C.accent, note: 'new account created' };
39
+ case 'coupon': return { glyph: '✓', label: 'success', color: C.success, note: '₹100 coupon available' };
40
+ case 'no_coupon': return { glyph: '✓', label: 'success', color: C.success, note: 'no ₹100 coupon' };
41
+ case 'extracted': return { glyph: '✓', label: 'success', color: C.success, note: 'extracted' };
42
+ case 'timeout': return { glyph: '⏱', label: 'timeout', color: C.warn, note: 'no OTP in 90s' };
43
+ case 'cancelled_user': return { glyph: '⊘', label: 'cancelled', color: C.warn, note: 'cancelled by user' };
44
+ case 'wrong_otp': return { glyph: '✗', label: 'failed', color: C.danger, note: 'wrong OTP code' };
45
+ case 'goal_reached': return { glyph: '•', label: 'skipped', color: C.muted, note: 'goal reached' };
46
+ case 'failed': return { glyph: '✗', label: 'failed', color: C.danger, note: detail || 'failed' };
47
+ default: {
48
+ const st = STATUS[status] || STATUS.pending;
49
+ const note = status === 'success' ? 'extracted' : (detail || '');
50
+ return { glyph: st.glyph, label: status, color: st.color, note };
51
+ }
52
+ }
53
+ }
54
+
55
+ function ResultRow({ status, category, mobile, detail, cost }) {
56
+ const d = resultDisplay({ status, category, detail });
35
57
  const mob = (mobile || '—').slice(-10) || '—';
36
- const text = status === 'success' ? (coupon ? '₹100 coupon' : 'extracted') : (detail || '');
37
58
  return h(Box, null,
38
59
  h(Box, { width: 13 }, h(Text, null, mob)),
39
- h(Box, { width: 13 }, h(Text, { color: st.color }, `${st.glyph} ${status}`)),
40
- h(Box, { flexGrow: 1 }, h(Text, { color: 'gray' }, text)),
60
+ h(Box, { width: 14 }, h(Text, { color: d.color }, `${d.glyph} ${d.label}`)),
61
+ h(Box, { flexGrow: 1 }, h(Text, { color: 'gray' }, d.note)),
41
62
  h(Box, { width: 7, justifyContent: 'flex-end' }, h(Text, { color: 'gray' }, cost ? `₹${cost}` : '')),
42
63
  );
43
64
  }
@@ -76,7 +97,7 @@ export function RunView({ controller }) {
76
97
  slotList.length ? slotList.map((sl) => h(SlotRow, { key: sl.slot, ...sl }))
77
98
  : h(Text, { color: 'gray' }, 'starting…')),
78
99
  h(Panel, { title: 'results' },
79
- lastRows.length ? lastRows.map((r, i) => h(ResultRow, { key: i, status: r.status, mobile: r.mobile, detail: r.detail, coupon: r.coupon_eligible, cost: r.cost }))
100
+ lastRows.length ? lastRows.map((r, i) => h(ResultRow, { key: i, status: r.status, category: r.category, mobile: r.mobile, detail: r.detail, cost: r.cost }))
80
101
  : h(Text, { color: 'gray' }, 'waiting for first result…')),
81
102
  h(Box, { borderStyle: 'round', borderColor: COLORS.accent, paddingX: 1 },
82
103
  h(Text, { color: COLORS.success }, `✓ ${stats.succeeded}`),
package/src/util.js CHANGED
@@ -61,36 +61,51 @@ function writeAccounts(accounts, { name, out, checkMinutes } = {}) {
61
61
  return dest;
62
62
  }
63
63
 
64
- // Partition by coupon_eligible when the Minutes check was requested.
65
- const minutesFree = []; // coupon_eligible=true no Minutes order, ₹100 coupon available
66
- const minutesUsed = []; // coupon_eligible=false already placed a Minutes order
67
- const unchecked = []; // coupon_eligible absent check wasn't done
64
+ // Partition into the THREE buckets the user wants:
65
+ // fresh brand-new account just created (signup)
66
+ // coupon → existing account with the ₹100 Minutes coupon available
67
+ // noCoupon existing account, coupon already used
68
+ // plus `unchecked` (Minutes wasn't checked). Categorisation prefers the explicit
69
+ // `category` set by the worker; falls back to is_new_account / coupon_eligible.
70
+ const fresh = [];
71
+ const coupon = [];
72
+ const noCoupon = [];
73
+ const unchecked = [];
68
74
 
69
75
  let hasChecked = false;
70
76
  for (const a of accounts) {
71
77
  const sess = a.session;
72
78
  if (!sess) continue;
79
+ const cat = a.category;
73
80
  const eligible = a.coupon_eligible;
74
- if (eligible !== undefined && eligible !== null) {
81
+ if (cat === 'fresh' || a.is_new_account) {
82
+ fresh.push(sess);
83
+ hasChecked = true;
84
+ } else if (cat === 'coupon' || eligible === true) {
85
+ coupon.push(sess);
86
+ hasChecked = true;
87
+ } else if (cat === 'no_coupon' || eligible === false) {
88
+ noCoupon.push(sess);
75
89
  hasChecked = true;
76
- (eligible ? minutesFree : minutesUsed).push(sess);
77
90
  } else {
78
91
  unchecked.push(sess);
79
92
  }
80
93
  }
81
94
 
82
95
  const results = [];
83
- if (hasChecked || (checkMinutes && (minutesFree.length || minutesUsed.length))) {
84
- const destFree = write('-minutes-free', minutesFree);
85
- if (destFree) results.push({ label: '🟢 minutes-free (₹100 coupon)', path: destFree, count: minutesFree.length });
86
- const destUsed = write('-minutes-used', minutesUsed);
87
- if (destUsed) results.push({ label: '🔴 minutes-used (coupon gone)', path: destUsed, count: minutesUsed.length });
96
+ if (hasChecked || (checkMinutes && (fresh.length || coupon.length || noCoupon.length))) {
97
+ const dFresh = write('-fresh-new-account', fresh);
98
+ if (dFresh) results.push({ label: '🆕 fresh new account', path: dFresh, count: fresh.length });
99
+ const dCoupon = write('-with-100-coupon', coupon);
100
+ if (dCoupon) results.push({ label: '🟢 ₹100 coupon available', path: dCoupon, count: coupon.length });
101
+ const dNo = write('-no-100-coupon', noCoupon);
102
+ if (dNo) results.push({ label: '🔴 no ₹100 coupon', path: dNo, count: noCoupon.length });
88
103
  if (unchecked.length) {
89
- const destUnk = write('-unchecked', unchecked);
90
- if (destUnk) results.push({ label: '⚪ unchecked', path: destUnk, count: unchecked.length });
104
+ const dUnk = write('-unchecked', unchecked);
105
+ if (dUnk) results.push({ label: '⚪ unchecked', path: dUnk, count: unchecked.length });
91
106
  }
92
107
  } else {
93
- const allSessions = [...minutesFree, ...minutesUsed, ...unchecked];
108
+ const allSessions = [...fresh, ...coupon, ...noCoupon, ...unchecked];
94
109
  const dest = write('', allSessions);
95
110
  if (dest) results.push({ label: 'combined', path: dest, count: allSessions.length });
96
111
  }
package/src/worker.js CHANGED
@@ -78,6 +78,8 @@ export class Worker {
78
78
  if (res.status === 'success' && res.session) {
79
79
  this.results.push({
80
80
  id_no: res.id_no, mobile: res.mobile || '', session: res.session,
81
+ category: res.category, // 'fresh' | 'coupon' | 'no_coupon' | 'extracted'
82
+ is_new_account: !!res.is_new_account,
81
83
  minutes_checked: !!res.minutes_checked,
82
84
  coupon_eligible: res.coupon_eligible ?? undefined,
83
85
  has_minutes_order: res.has_minutes_order ?? undefined,
@@ -152,18 +154,25 @@ export class Worker {
152
154
  this._emit(slot, { phase: 'done', detail: 'goal reached' });
153
155
  res.status = 'cancelled';
154
156
  res.detail = 'goal reached';
157
+ res.category = 'goal_reached';
155
158
  return res;
156
159
  }
157
160
 
158
161
  this._emit(slot, { mobile: '', phase: 'renting', detail: 'requesting a number', wait: 0 });
159
162
  const number = await this._rent(slot);
160
- if (!number) { res.status = 'cancelled'; res.detail = 'stopped while renting'; return res; }
163
+ if (!number) {
164
+ res.status = 'cancelled';
165
+ res.detail = this.stopped ? 'cancelled by user' : 'stopped while renting';
166
+ res.category = this.stopped ? 'cancelled_user' : 'goal_reached';
167
+ return res;
168
+ }
161
169
  if (this.stats.succeeded >= this.requested) {
162
170
  this.log(`goal reached by another slot; releasing ${number.mobile} immediately`);
163
171
  this._release(number, Date.now(), slot);
164
172
  this._emit(slot, { phase: 'done', detail: 'goal reached' });
165
173
  res.status = 'cancelled';
166
174
  res.detail = 'goal reached';
175
+ res.category = 'goal_reached';
167
176
  return res;
168
177
  }
169
178
  const rentedAt = Date.now();
@@ -194,11 +203,13 @@ export class Worker {
194
203
  this._release(number, rentedAt, slot);
195
204
  };
196
205
 
197
- const fail = (detail) => {
206
+ const fail = (detail, category) => {
198
207
  const charged = smsArrived.v;
199
208
  res.status = charged ? 'failed' : 'cancelled';
200
209
  res.cost = charged ? number.cost : 0;
201
210
  res.detail = detail;
211
+ // category: timeout (90s, no OTP), wrong_otp, send_blocked, or generic failed/cancelled
212
+ res.category = category || (res.status === 'failed' ? 'failed' : 'cancelled');
202
213
  this.log(`FAILED ${number.mobile}: ${detail} (charged: ${charged})`);
203
214
  releaseOnce(charged, 'failed');
204
215
  return res;
@@ -224,13 +235,14 @@ export class Worker {
224
235
  const deadline = this.provider.name === 'tempotp' ? TEMPOTP_DEADLINE : OTPCART_DEADLINE;
225
236
  const doResend = this.provider.name === 'otpcart';
226
237
  const otp = await this._awaitOtp(slot, stream, fk, number, deadline, doResend, smsArrived);
227
- if (!otp) return fail(`no OTP within ${deadline / 1000}s abandoned`);
238
+ if (!otp) return fail(`no OTP in ${deadline / 1000}s (timeout)`, 'timeout');
228
239
 
229
240
  // 3. VERIFY (already retries transients internally via _verify; re-verify on blockage)
230
241
  this._emit(slot, { phase: 'verify', detail: 'verifying OTP' });
231
242
  const { session, error: verifyErr } = await this._verify(slot, fk, stream, number, otp, smsArrived);
232
243
  if (!session) {
233
- const failed = fail(verifyErr || 'verify failed');
244
+ const isWrongOtp = /incorrect|wrong|invalid|expired|code/i.test(verifyErr || '');
245
+ const failed = fail(verifyErr || 'verify failed', isWrongOtp ? 'wrong_otp' : 'failed');
234
246
  if (this.onFailure && !this.stopped && !this._asking) {
235
247
  this._asking = true;
236
248
  try {
@@ -282,7 +294,17 @@ export class Worker {
282
294
  res.status = 'success';
283
295
  res.cost = number.cost;
284
296
  res.session = session;
285
- const doneDetail = res.email_linked ? `extracted + ${res.linked_email}` : 'extracted';
297
+ res.is_new_account = !!fk.isNewUser; // a freshly-created (signup) account
298
+ // Derive the result CATEGORY for display + file routing:
299
+ // fresh → brand-new account just created
300
+ // coupon → existing account, ₹100 Minutes coupon available
301
+ // no_coupon → existing account, coupon already used
302
+ // extracted → success but Minutes wasn't checked
303
+ res.category = res.is_new_account ? 'fresh'
304
+ : (res.minutes_checked ? (res.coupon_eligible ? 'coupon' : 'no_coupon') : 'extracted');
305
+ const doneDetail = res.is_new_account ? 'fresh account'
306
+ : res.minutes_checked ? (res.coupon_eligible ? '₹100 coupon' : 'no coupon')
307
+ : 'extracted';
286
308
  this._emit(slot, { phase: 'done', detail: doneDetail });
287
309
  releaseOnce(true, doneDetail, true); // success: OTP consumed → no cancel
288
310
  return res;