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 +1 -1
- package/src/commands.js +56 -3
- package/src/flipkart.js +81 -14
- package/src/index.js +3 -1
- package/src/worker.js +14 -2
package/package.json
CHANGED
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
|
-
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
return
|
|
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
|
|
220
|
-
|
|
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
|
|