scripter-x 1.0.18 → 1.0.20

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.18",
3
+ "version": "1.0.20",
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/commands.js CHANGED
@@ -10,7 +10,7 @@ import * as kuku from './providers/kuku.js';
10
10
  import * as zepto from './providers/zepto.js';
11
11
  import { homedir } from 'node:os';
12
12
  import { join, dirname } from 'node:path';
13
- import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
13
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
14
14
  import { Worker } from './worker.js';
15
15
  import { RunController } from './controller.js';
16
16
  import { STATUS } from './theme.js';
@@ -221,6 +221,59 @@ export async function run(io, api, args = {}) {
221
221
  if (worker.logPath) io.print(` ◉ log saved to: ${worker.logPath}`);
222
222
  }
223
223
 
224
+ // ── recheck: re-validate Minutes status of an EXISTING session JSON, locally ──
225
+ // FK blocks server IPs, so the Minutes check must run here (residential IP). Takes
226
+ // a file (array of sessions, OR our {session:{...}} export shape), re-checks each
227
+ // account's live Minutes orders, and re-splits into correct -minutes-free /
228
+ // -minutes-used files with real stats + the actual order ids.
229
+ export async function recheck(io, _api, args = {}) {
230
+ // one-shot puts the positional path in action/campaign; interactive may pass file/_
231
+ const src = args.file || args._?.[0] || args.action || args.campaign;
232
+ if (!src || !existsSync(src)) throw new Error('usage: recheck <sessions.json>');
233
+ const raw = JSON.parse(readFileSync(src, 'utf8'));
234
+ const list = Array.isArray(raw) ? raw : (raw.accounts || raw.sessions || [raw]);
235
+ // normalize each entry to a flat FK session object
236
+ const sessions = list.map((e) => (e && e.session && typeof e.session === 'object' ? e.session : e)).filter(Boolean);
237
+ if (!sessions.length) throw new Error('no sessions found in file');
238
+
239
+ const { FlipkartLogin } = await import('./flipkart.js');
240
+ const fk = new FlipkartLogin();
241
+
242
+ io.print(` ◉ re-checking ${sessions.length} account(s) locally (your IP) …`);
243
+ const free = [], used = [], errored = [];
244
+ for (let i = 0; i < sessions.length; i++) {
245
+ const s = sessions[i];
246
+ const mob = s.mobileNo || s.mobile || `#${i + 1}`;
247
+ try {
248
+ const m = await fk.checkMinutes(s);
249
+ if (m.eligible) { free.push(s); io.print(` 🟢 ${mob} — minutes-free (₹100 coupon)`); }
250
+ else {
251
+ used.push(s);
252
+ io.print(` 🔴 ${mob} — ${m.count} minutes order(s): ${m.orders.map((o) => o.orderId).join(', ')}`, 'danger');
253
+ }
254
+ } catch (e) { errored.push(s); io.print(` ! ${mob} — check failed: ${e.message}`, 'danger'); }
255
+ }
256
+
257
+ // write corrected files next to the source
258
+ const dir = dirname(src);
259
+ const stem = src.split(/[/\\]/).pop().replace(/\.json$/i, '').replace(/-minutes-(free|used|unchecked)$/i, '');
260
+ const write = (suffix, arr) => {
261
+ if (!arr.length) return null;
262
+ const dest = join(dir, `${stem}-rechecked${suffix}.json`);
263
+ writeFileSync(dest, JSON.stringify(arr, null, 2));
264
+ return dest;
265
+ };
266
+ const fFree = write('-minutes-free', free);
267
+ const fUsed = write('-minutes-used', used);
268
+ const fErr = write('-errors', errored);
269
+
270
+ io.print('');
271
+ io.print(` ✓ stats: 🟢 ${free.length} minutes-free · 🔴 ${used.length} minutes-used · ! ${errored.length} errors`);
272
+ if (fFree) io.print(` 🟢 minutes-free → ${fFree}`, 'accent');
273
+ if (fUsed) io.print(` 🔴 minutes-used → ${fUsed}`, 'accent');
274
+ if (fErr) io.print(` ! errors → ${fErr}`, 'accent');
275
+ }
276
+
224
277
  // ── zepto: local OTP login → extract session to a ZAUTH1 envelope file ──
225
278
  // Dead-simple, no backend: enter a number → we SMS an OTP locally → enter the
226
279
  // code → we verify, build the ZAUTH1 envelope, and write {phone}-{timestamp}.txt.
@@ -549,7 +602,7 @@ export async function configCmd(io, _api, args = {}) {
549
602
  }
550
603
 
551
604
  export function help(io) {
552
- io.print(' commands: run · zepto · campaigns · export · balance · creds · stop · delete · whoami · config · update · logout · exit');
605
+ io.print(' commands: run · zepto · recheck · campaigns · export · balance · creds · stop · delete · whoami · config · update · logout · exit');
553
606
  }
554
607
 
555
608
  export async function update(io) {
@@ -562,6 +615,6 @@ export async function update(io) {
562
615
 
563
616
  // dispatch table used by the interactive shell
564
617
  export const REGISTRY = {
565
- run, zepto: zeptoCmd, campaigns, export: exportCmd, balance, creds, stop, delete: del,
618
+ run, zepto: zeptoCmd, recheck, campaigns, export: exportCmd, balance, creds, stop, delete: del,
566
619
  whoami, login, logout, config: configCmd, update, help,
567
620
  };
package/src/flipkart.js CHANGED
@@ -29,6 +29,40 @@ const nativeUa = (vid) =>
29
29
  `Mozilla/5.0 (Linux; Android 13; sdk_gphone64_arm64 Build/TE1A.240213.009) ` +
30
30
  `FKUA/Retail/3130902/Android/Mobile (Google/sdk_gphone64_arm64/${(vid || '').toLowerCase()})`;
31
31
 
32
+ // Parse a MY_ORDER_PAGE response and return ONLY genuine Flipkart-Minutes orders.
33
+ // An order is a slot whose widget is an ORDER CARD (SUPER_WIDGET +
34
+ // ORDER_CARD_SUPER_WIDGET_TRANSFORMER); its id is widget.params.trackingParams.orderId
35
+ // (exactly one per card). It's a MINUTES order iff the card's redirect carries
36
+ // queryParam.hyperlocal === "true" — Grocery/Kilos/memberships are NOT hyperlocal,
37
+ // so they're excluded even in an unfiltered response. (Verified against captured
38
+ // ground-truth: 0-Minutes accounts → 0, 1/2-Minutes accounts → exact match.)
39
+ // Returns [{ orderId, title, status }].
40
+ export function parseMinutesOrders(text) {
41
+ let env;
42
+ try { env = JSON.parse(text); } catch { return []; }
43
+ const resp = (env && env.RESPONSE) || env || {};
44
+ const slots = Array.isArray(resp.slots) ? resp.slots : [];
45
+ const seen = new Set();
46
+ const out = [];
47
+ for (const s of slots) {
48
+ const w = (s && s.widget) || {};
49
+ if (w.type !== 'SUPER_WIDGET' || w.transformerProvider !== 'ORDER_CARD_SUPER_WIDGET_TRANSFORMER') continue;
50
+ const tp = (w.params && w.params.trackingParams) || {};
51
+ const orderId = tp.orderId;
52
+ if (!orderId || seen.has(orderId)) continue;
53
+ let isMinutes = false, title = '';
54
+ try {
55
+ const rc = w.data.subWidgets[0].data.buttons.renderableComponents[0];
56
+ isMinutes = rc.action.nonWidgetizeRedirection.queryParam.hyperlocal === 'true';
57
+ title = (rc.value && rc.value.subText) || '';
58
+ } catch { /* malformed card → not a counted Minutes order */ }
59
+ if (!isMinutes) continue;
60
+ seen.add(orderId);
61
+ out.push({ orderId, title, status: tp.orderStatus || '' });
62
+ }
63
+ return out;
64
+ }
65
+
32
66
  // Parse a fetch Response's set-cookie headers into a {name:value} map.
33
67
  function parseCookies(res, jar) {
34
68
  // Node fetch exposes combined set-cookie via getSetCookie() (undici).
@@ -138,7 +172,9 @@ export class FlipkartLogin {
138
172
  };
139
173
  }
140
174
 
141
- async checkMinutes(session) {
175
+ // One MY_ORDER_PAGE / HYPERLOCAL fetch on a given DC. `withRT` attaches the refresh
176
+ // token so a 206 "AT expired" can be recovered in-place. Returns { status, text }.
177
+ async _fetchOrderPage(session, dc, withRT) {
142
178
  await throttle.wait();
143
179
  const body = {
144
180
  requestContext: { type: 'MY_ORDER_PAGE', pageView: '', queryTime: '', cxTenant: 'cs',
@@ -148,19 +184,50 @@ export class FlipkartLogin {
148
184
  paginatedFetch: false, pageNumber: 1, fetchAllPages: false, networkSpeed: 0,
149
185
  trackingContext: null, fetchSeoData: false },
150
186
  locationContext: { pincode: '' } };
151
- const res = await fetch(`${NATIVE_HOST}/4/page/fetch`, {
152
- method: 'POST', body: JSON.stringify(body),
153
- headers: {
154
- 'User-Agent': 'okhttp/4.9.2', 'X-User-Agent': nativeUa(this.vid),
155
- 'Content-Type': 'application/json; charset=UTF-8',
156
- 'x-request-metaInfo': '{"actionType":"LOGIN","pageUri":"questContext"}',
157
- at: session.at || '', sn: session.sn || '',
158
- secureToken: session.secureToken || '', secureCookie: session.secureCookie || '',
159
- },
160
- });
161
- const text = await res.text();
162
- const count = (text.match(/OD\d{15,}/g) || []).length;
163
- return count === 0; // no orders ⇒ coupon available
187
+ const host = `https://${dc}.rome.api.flipkart.net`;
188
+ const at = session.at || '';
189
+ const headers = {
190
+ 'User-Agent': 'okhttp/4.9.2', 'X-User-Agent': nativeUa(session.visitId || this.vid),
191
+ 'Content-Type': 'application/json; charset=UTF-8',
192
+ 'x-request-metaInfo': '{"actionType":"LOGIN","pageUri":"questContext"}',
193
+ at, sn: session.sn || '',
194
+ secureToken: session.secureToken || '', secureCookie: session.secureCookie || '',
195
+ Cookie: `ud=${session.ud || ''}`,
196
+ };
197
+ if (withRT && session.rt) headers.rt = session.rt;
198
+ const res = await fetch(`${host}/4/page/fetch`, { method: 'POST', body: JSON.stringify(body), headers });
199
+ return { status: res.status, text: await res.text() };
200
+ }
201
+
202
+ // Robust FK Minutes check. Handles 206 (AT-expired → resend with rt) and 406
203
+ // (DC change → retry on home DC), then parses ORDER CARDS (not a blind regex)
204
+ // and gates each on its per-card `hyperlocal` flag — so non-Minutes orders
205
+ // (Grocery/Kilos/memberships) and page chrome never count.
206
+ // Returns { eligible, count, orders:[{orderId,title,status}] }.
207
+ async checkMinutes(session) {
208
+ let dc = 2;
209
+ const triedDC = { 2: true };
210
+ let r = await this._fetchOrderPage(session, dc, false);
211
+
212
+ // 206 "AT expired" → resend with rt on the same DC
213
+ if (r.status === 206 || /AT expired/i.test(r.text)) {
214
+ r = await this._fetchOrderPage(session, dc, true);
215
+ // adopt the refreshed token for any follow-up DC retry
216
+ try { const e = JSON.parse(r.text); if (e?.SESSION?.at) session = { ...session, at: e.SESSION.at }; } catch { /* */ }
217
+ }
218
+ // 406 DC change → {RESPONSE:{id:"<dc>"}} → retry on the home DC
219
+ if (r.status === 406) {
220
+ let id = null;
221
+ try { const e = JSON.parse(r.text); id = parseInt(e?.RESPONSE?.id || e?.id, 10); } catch { /* */ }
222
+ if (id >= 1 && !triedDC[id]) {
223
+ dc = id; triedDC[id] = true;
224
+ r = await this._fetchOrderPage(session, dc, false);
225
+ if (r.status === 206 || /AT expired/i.test(r.text)) r = await this._fetchOrderPage(session, dc, true);
226
+ }
227
+ }
228
+
229
+ const orders = parseMinutesOrders(r.text);
230
+ return { eligible: orders.length === 0, count: orders.length, orders };
164
231
  }
165
232
 
166
233
  // ─── Email attach (FK-5/6/7) — on the EMAIL host (2.rome.api.flipkart.com) ──────
package/src/index.js CHANGED
@@ -54,6 +54,8 @@ const HELP = `${paint.bold('scripterx')} — local Flipkart session extractor
54
54
  --auto | --manual --count N --concurrency N --out <file|dir> --cert <64hex>
55
55
  --number <10-digit> --otp <code>
56
56
  (envelope keyed to ZeptoAuthManager cert; override with --cert or $ZAUTH_CERT_SHA)
57
+ scripterx recheck <file> re-validate Minutes status of a session JSON, locally (your IP)
58
+ → re-splits into corrected -minutes-free / -minutes-used files + stats
57
59
  scripterx campaigns | export [name] | balance | creds | stop | delete | whoami | config
58
60
  scripterx --version`;
59
61
 
@@ -104,7 +106,7 @@ async function main() {
104
106
  }
105
107
 
106
108
  // ── one-shot mode ──
107
- const map = { run: 'run', zepto: 'zepto', campaigns: 'campaigns', export: 'export', balance: 'balance',
109
+ const map = { run: 'run', zepto: 'zepto', recheck: 'recheck', campaigns: 'campaigns', export: 'export', balance: 'balance',
108
110
  creds: 'creds', stop: 'stop', delete: 'delete', whoami: 'whoami', login: 'login',
109
111
  logout: 'logout', config: 'config', update: 'update', help: 'help' };
110
112
  const fnName = map[cmd];
package/src/worker.js CHANGED
@@ -75,6 +75,11 @@ export class Worker {
75
75
  fail_reason: res.detail || '', session: res.session,
76
76
  minutes_checked: !!res.minutes_checked, coupon_eligible: res.coupon_eligible ?? undefined,
77
77
  has_minutes_order: res.has_minutes_order ?? undefined,
78
+ // pack the local Minutes order count + ids into minutes_note (free-text the
79
+ // server already stores) so stats/export survive the round-trip.
80
+ minutes_note: res.minutes_checked
81
+ ? (res.minutes_order_count ? `${res.minutes_order_count} minutes order(s): ${(res.minutes_orders || []).map((o) => o.orderId).join(',')}` : 'no minutes orders')
82
+ : undefined,
78
83
  linked_email: res.linked_email ?? undefined,
79
84
  email_linked: res.email_linked ?? undefined,
80
85
  email_reason: res.email_reason ?? undefined,
@@ -216,8 +221,15 @@ export class Worker {
216
221
  if (this.checkMinutes) {
217
222
  this._emit(slot, { phase: 'minutes', detail: 'checking Minutes' });
218
223
  try {
219
- const avail = await fk.checkMinutes(session);
220
- res.minutes_checked = true; res.coupon_eligible = avail; res.has_minutes_order = !avail;
224
+ const m = await fk.checkMinutes(session);
225
+ // checkMinutes now returns { eligible, count, orders }; tolerate an old boolean too.
226
+ const eligible = typeof m === 'boolean' ? m : m.eligible;
227
+ res.minutes_checked = true;
228
+ res.coupon_eligible = eligible;
229
+ res.has_minutes_order = !eligible;
230
+ res.minutes_order_count = typeof m === 'boolean' ? (eligible ? 0 : 1) : m.count;
231
+ res.minutes_orders = typeof m === 'boolean' ? [] : (m.orders || []);
232
+ this._emit(slot, { phase: 'minutes', detail: eligible ? 'minutes-free (₹100 coupon)' : `has ${res.minutes_order_count} minutes order(s)` });
221
233
  } catch { /* non-fatal */ }
222
234
  }
223
235