scripter-x 1.0.0
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/README.md +55 -0
- package/package.json +33 -0
- package/src/api.js +89 -0
- package/src/commands.js +247 -0
- package/src/config.js +67 -0
- package/src/controller.js +27 -0
- package/src/flipkart.js +186 -0
- package/src/index.js +105 -0
- package/src/prompt.js +60 -0
- package/src/providers/otpcart.js +95 -0
- package/src/providers/tempotp.js +83 -0
- package/src/theme.js +59 -0
- package/src/throttle.js +23 -0
- package/src/ui/App.js +101 -0
- package/src/ui/RunView.js +91 -0
- package/src/ui/Shell.js +114 -0
- package/src/ui/components.js +84 -0
- package/src/ui/mouse.js +95 -0
- package/src/util.js +54 -0
- package/src/worker.js +261 -0
package/src/flipkart.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// The proven headless Flipkart login flow — runs on the user's residential IP.
|
|
2
|
+
// SEND 1.rome.api.flipkart.com/1/action/view (msite UA) → guest cookies + requestId
|
|
3
|
+
// VERIFY 2.rome.api.flipkart.net/1/action/view (native UA) → SESSION.isLoggedIn + at/rt
|
|
4
|
+
// MINUTES 2.rome.api.flipkart.net/4/page/fetch (HYPERLOCAL) → any orderId ⇒ coupon used
|
|
5
|
+
// Every outbound Flipkart call passes through the global 2s throttle.
|
|
6
|
+
import * as throttle from './throttle.js';
|
|
7
|
+
|
|
8
|
+
const FKUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 ' +
|
|
9
|
+
'(KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1 FKUA/msite/0.0.3/msite/Mobile channelType/brw';
|
|
10
|
+
const WEB_HOST = 'https://1.rome.api.flipkart.com';
|
|
11
|
+
const NATIVE_HOST = 'https://2.rome.api.flipkart.net';
|
|
12
|
+
|
|
13
|
+
export class BlockedError extends Error {}
|
|
14
|
+
export class WrongOTP extends Error {} // genuinely wrong/expired OTP code
|
|
15
|
+
export class TransientError extends Error {} // 529 / 5xx / network — retryable, NOT the OTP's fault
|
|
16
|
+
|
|
17
|
+
const OTP_RES = [/(?:otp|code)\D*(\d{6})\b/i, /\b(\d{6})\D*(?:otp|code)/i, /\b(\d{6})\b/];
|
|
18
|
+
|
|
19
|
+
export function extractOtp(text) {
|
|
20
|
+
if (!text) return null;
|
|
21
|
+
const t = text.trim();
|
|
22
|
+
if (t.length === 6 && /^\d+$/.test(t)) return t;
|
|
23
|
+
for (const rx of OTP_RES) { const m = text.match(rx); if (m) return m[1]; }
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const nativeUa = (vid) =>
|
|
28
|
+
`Mozilla/5.0 (Linux; Android 13; sdk_gphone64_arm64 Build/TE1A.240213.009) ` +
|
|
29
|
+
`FKUA/Retail/3130902/Android/Mobile (Google/sdk_gphone64_arm64/${(vid || '').toLowerCase()})`;
|
|
30
|
+
|
|
31
|
+
// Parse a fetch Response's set-cookie headers into a {name:value} map.
|
|
32
|
+
function parseCookies(res, jar) {
|
|
33
|
+
// Node fetch exposes combined set-cookie via getSetCookie() (undici).
|
|
34
|
+
const raw = typeof res.headers.getSetCookie === 'function'
|
|
35
|
+
? res.headers.getSetCookie()
|
|
36
|
+
: (res.headers.get('set-cookie') ? [res.headers.get('set-cookie')] : []);
|
|
37
|
+
for (const line of raw) {
|
|
38
|
+
const m = line.match(/^([^=]+)=([^;]+)/);
|
|
39
|
+
if (m && m[2] && m[2] !== 'deleted') jar[m[1].trim()] = m[2].trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cookieHeader(jar) {
|
|
44
|
+
return Object.entries(jar).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class FlipkartLogin {
|
|
48
|
+
constructor() {
|
|
49
|
+
this.jar = {};
|
|
50
|
+
this.reqId = null;
|
|
51
|
+
this.sn = '';
|
|
52
|
+
this.vid = '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async sendOtp(mobile) {
|
|
56
|
+
await throttle.wait();
|
|
57
|
+
const body = { actionRequestContext: {
|
|
58
|
+
type: 'LOGIN_IDENTITY_VERIFY', loginIdPrefix: '+91', loginId: mobile.slice(-10),
|
|
59
|
+
clientQueryParamMap: { ret: '/my-account', entryPage: 'DEFAULT' },
|
|
60
|
+
loginType: 'MOBILE', verificationType: 'OTP', screenName: 'LOGIN_V4_MOBILE',
|
|
61
|
+
triggerSna: false, sourceContext: 'DEFAULT' } };
|
|
62
|
+
let res;
|
|
63
|
+
try {
|
|
64
|
+
res = await fetch(`${WEB_HOST}/1/action/view`, {
|
|
65
|
+
method: 'POST', body: JSON.stringify(body),
|
|
66
|
+
headers: {
|
|
67
|
+
Accept: '*/*', 'Content-Type': 'application/json',
|
|
68
|
+
'X-User-Agent': FKUA, 'User-Agent': FKUA,
|
|
69
|
+
Origin: 'https://www.flipkart.com', Referer: 'https://www.flipkart.com/',
|
|
70
|
+
'sec-ch-ua-mobile': '?1', 'sec-ch-ua-platform': '"iOS"', 'sec-fetch-site': 'same-site',
|
|
71
|
+
'x-request-metaInfo': '{"actionType":"LOGIN_IDENTITY_VERIFY","pageUri":"questContext"}',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
} catch (e) { throw new BlockedError(`send failed: ${e.message}`); }
|
|
75
|
+
parseCookies(res, this.jar);
|
|
76
|
+
const data = await res.json().catch(() => ({}));
|
|
77
|
+
const rid = data?.RESPONSE?.actionResponseContext?.requestId;
|
|
78
|
+
if (!rid) throw new BlockedError('blocked while sending OTP (no requestId)');
|
|
79
|
+
this.reqId = rid;
|
|
80
|
+
this.sn = this.jar.SN || '';
|
|
81
|
+
this.vid = this.sn ? this.sn.split('.')[0] : '';
|
|
82
|
+
return rid;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async verifyOtp(mobile, otp) {
|
|
86
|
+
await throttle.wait();
|
|
87
|
+
const secureToken = this.vid ? `${this.vid}:${this.vid}` : '';
|
|
88
|
+
const body = { actionRequestContext: {
|
|
89
|
+
type: 'LOGIN', loginIdPrefix: '+91', loginId: mobile.slice(-10), password: null,
|
|
90
|
+
otp, otpRequestId: this.reqId, remainingAttempts: 5, phoneNumberFormat: 'E164',
|
|
91
|
+
loginType: 'MOBILE', verificationType: 'OTP', sourceContext: 'GO_LOGIN', churned: false,
|
|
92
|
+
otpRegex: null, data: null, clientQueryParamMap: null } };
|
|
93
|
+
const res = await fetch(`${NATIVE_HOST}/1/action/view`, {
|
|
94
|
+
method: 'POST', body: JSON.stringify(body),
|
|
95
|
+
headers: {
|
|
96
|
+
'User-Agent': 'okhttp/4.9.2', 'X-User-Agent': nativeUa(this.vid),
|
|
97
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
98
|
+
'x-request-metaInfo': '{"actionType":"LOGIN","pageUri":"questContext"}',
|
|
99
|
+
at: this.jar.at || '', sn: this.sn, secureToken, secureCookie: this.jar.S || '',
|
|
100
|
+
Cookie: cookieHeader(this.jar),
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const text = await res.text();
|
|
104
|
+
// 529/5xx = Flipkart overloaded / soft-blocking this IP — NOT the OTP's fault.
|
|
105
|
+
if (res.status === 429 || res.status === 529 || res.status >= 500) {
|
|
106
|
+
throw new TransientError(`Flipkart busy (HTTP ${res.status}) — will retry`);
|
|
107
|
+
}
|
|
108
|
+
let data;
|
|
109
|
+
try { data = JSON.parse(text); } catch { throw new TransientError('Flipkart returned a non-JSON response — will retry'); }
|
|
110
|
+
const env = data.SESSION;
|
|
111
|
+
if (!env || !env.isLoggedIn) {
|
|
112
|
+
// pull Flipkart's own human message if present (the real reason)
|
|
113
|
+
const fkMsg = flipkartErrorMessage(data, text);
|
|
114
|
+
const low = (fkMsg + ' ' + text).toLowerCase();
|
|
115
|
+
if (/incorrect|wrong|invalid|expired|otp.*not.*match|verification code/.test(low)) {
|
|
116
|
+
throw new WrongOTP(fkMsg || 'OTP code rejected by Flipkart');
|
|
117
|
+
}
|
|
118
|
+
// not a clear wrong-OTP and not a 5xx → treat as transient (session/IP hiccup), retry
|
|
119
|
+
throw new TransientError(fkMsg || `verify did not complete (HTTP ${res.status})`);
|
|
120
|
+
}
|
|
121
|
+
// vd / refreshed ud from Set-Cookie
|
|
122
|
+
const fresh = {};
|
|
123
|
+
parseCookies(res, fresh);
|
|
124
|
+
const ud = fresh.ud || this.jar.ud || '';
|
|
125
|
+
const vd = fresh.vd || '';
|
|
126
|
+
return {
|
|
127
|
+
accountId: env.accountId || '', at: env.at || '', rt: env.rt || '', sn: env.sn || '',
|
|
128
|
+
secureToken, secureCookie: this.jar.S || '', ud, vd,
|
|
129
|
+
visitId: (env.sn || '').split('.')[0], nsid: env.nsid || '',
|
|
130
|
+
mobileNo: mobile.slice(-10), isLoggedIn: true, rt_expires_at: rtExpiry(env.rt || ''),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async checkMinutes(session) {
|
|
135
|
+
await throttle.wait();
|
|
136
|
+
const body = {
|
|
137
|
+
requestContext: { type: 'MY_ORDER_PAGE', pageView: '', queryTime: '', cxTenant: 'cs',
|
|
138
|
+
pageName: '', pageNumber: 1, marketplaceFilter: 'HYPERLOCAL', salesAppFilter: 'FLIPKART,SLAP' },
|
|
139
|
+
pageType: 'MY_ORDER_PAGE', pageUri: '/cx/my_orders',
|
|
140
|
+
pageContext: { pageHashKey: null, slotContextMap: null, paginationContextMap: null,
|
|
141
|
+
paginatedFetch: false, pageNumber: 1, fetchAllPages: false, networkSpeed: 0,
|
|
142
|
+
trackingContext: null, fetchSeoData: false },
|
|
143
|
+
locationContext: { pincode: '' } };
|
|
144
|
+
const res = await fetch(`${NATIVE_HOST}/4/page/fetch`, {
|
|
145
|
+
method: 'POST', body: JSON.stringify(body),
|
|
146
|
+
headers: {
|
|
147
|
+
'User-Agent': 'okhttp/4.9.2', 'X-User-Agent': nativeUa(this.vid),
|
|
148
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
149
|
+
'x-request-metaInfo': '{"actionType":"LOGIN","pageUri":"questContext"}',
|
|
150
|
+
at: session.at || '', sn: session.sn || '',
|
|
151
|
+
secureToken: session.secureToken || '', secureCookie: session.secureCookie || '',
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const text = await res.text();
|
|
155
|
+
const count = (text.match(/"orderId"\s*:\s*"(OD\w+)"/g) || []).length;
|
|
156
|
+
return count === 0; // no orders ⇒ coupon available
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function rtExpiry(rt) {
|
|
161
|
+
try {
|
|
162
|
+
if ((rt.match(/\./g) || []).length >= 1) {
|
|
163
|
+
const part = rt.split('.')[1];
|
|
164
|
+
const json = JSON.parse(Buffer.from(part, 'base64').toString('utf8'));
|
|
165
|
+
if (json.exp) return new Date(json.exp * 1000).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
166
|
+
}
|
|
167
|
+
} catch { /* */ }
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Dig Flipkart's own human-readable error out of a verify response. Flipkart surfaces it
|
|
172
|
+
// in a few places (errorMessage.message inside a widget, ERROR_MESSAGE, serverErrorMessage).
|
|
173
|
+
function flipkartErrorMessage(data, rawText) {
|
|
174
|
+
try {
|
|
175
|
+
if (data) {
|
|
176
|
+
if (typeof data.ERROR_MESSAGE === 'string' && data.ERROR_MESSAGE) return data.ERROR_MESSAGE;
|
|
177
|
+
if (typeof data.serverErrorMessage === 'string' && data.serverErrorMessage) return data.serverErrorMessage;
|
|
178
|
+
}
|
|
179
|
+
} catch { /* */ }
|
|
180
|
+
// the OTP-input widget carries: "errorMessage":{"message":{"text":"This Code is incorrect"}}
|
|
181
|
+
const m = (rawText || '').match(/"errorMessage"\s*:\s*\{[^}]*"text"\s*:\s*"([^"]+)"/);
|
|
182
|
+
if (m) return m[1];
|
|
183
|
+
const m2 = (rawText || '').match(/"message"\s*:\s*\{\s*"type"\s*:\s*"RichTextValue"\s*,\s*"text"\s*:\s*"([^"]+)"/);
|
|
184
|
+
if (m2) return m2[1];
|
|
185
|
+
return '';
|
|
186
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ScripterX CLI (Node). Two modes:
|
|
3
|
+
// • no subcommand → the interactive Claude-Code-style shell (ink App)
|
|
4
|
+
// • a subcommand → one-shot mode (plain stdio prompts), e.g. `scripterx export foo`
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { render } from 'ink';
|
|
7
|
+
import * as config from './config.js';
|
|
8
|
+
import { ApiClient } from './api.js';
|
|
9
|
+
import { paint } from './util.js';
|
|
10
|
+
import { ask, askSecret, confirm, askChoice } from './prompt.js';
|
|
11
|
+
import { REGISTRY } from './commands.js';
|
|
12
|
+
import { App } from './ui/App.js';
|
|
13
|
+
|
|
14
|
+
const VERSION = '1.0.0';
|
|
15
|
+
const h = React.createElement;
|
|
16
|
+
|
|
17
|
+
// ── one-shot io: plain stdio, for `scripterx <cmd>` ──
|
|
18
|
+
const cliIo = {
|
|
19
|
+
print: (line, color) => {
|
|
20
|
+
if (color === 'accent') console.log(paint.accent(line));
|
|
21
|
+
else if (color === 'danger') console.log(paint.err(line));
|
|
22
|
+
else if (line.includes('✓')) console.log(line.replace('✓', paint.ok('✓')));
|
|
23
|
+
else if (line.includes('✗')) console.log(line.replace('✗', paint.err('✗')));
|
|
24
|
+
else if (line.trimStart().startsWith('!')) console.log(line.replace('!', paint.warn('!')));
|
|
25
|
+
else if (line.includes('◉')) console.log(line.replace('◉', paint.accent('◉')));
|
|
26
|
+
else console.log(line);
|
|
27
|
+
},
|
|
28
|
+
ask: (msg, opts = {}) => (opts.mask ? askSecret(msg) : ask(msg, { dflt: opts.dflt })),
|
|
29
|
+
confirm: (msg, dflt) => confirm(msg, { dflt }),
|
|
30
|
+
select: async (msg, items) => {
|
|
31
|
+
const norm = items.map((it) => (typeof it === 'string' ? { label: it, value: it } : it));
|
|
32
|
+
const choice = await askChoice(msg, norm.map((n) => n.label), { dflt: norm[0]?.label });
|
|
33
|
+
return norm.find((n) => n.label === choice) || norm[0];
|
|
34
|
+
},
|
|
35
|
+
setUser: () => {},
|
|
36
|
+
// one-shot run: no live ink view, just let the worker run (events ignored)
|
|
37
|
+
startRun: () => {}, endRun: () => {},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const HELP = `${paint.bold('scripterx')} — local Flipkart session extractor
|
|
41
|
+
|
|
42
|
+
scripterx interactive shell (recommended)
|
|
43
|
+
scripterx run [flags] run an extraction directly
|
|
44
|
+
--provider otpcart|tempotp --count N --concurrency N --name <n> --check-minutes
|
|
45
|
+
scripterx campaigns | export [name] | balance | creds | stop | delete | whoami | config
|
|
46
|
+
scripterx --version`;
|
|
47
|
+
|
|
48
|
+
function parseFlags(args) {
|
|
49
|
+
const out = { _: [] };
|
|
50
|
+
for (let i = 0; i < args.length; i++) {
|
|
51
|
+
const a = args[i];
|
|
52
|
+
if (a.startsWith('--')) {
|
|
53
|
+
const key = a.slice(2);
|
|
54
|
+
if (key.startsWith('no-')) out[key.slice(3)] = false;
|
|
55
|
+
else if (i + 1 < args.length && !args[i + 1].startsWith('--')) out[key] = args[++i];
|
|
56
|
+
else out[key] = true;
|
|
57
|
+
} else out._.push(a);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
const argv = process.argv.slice(2);
|
|
64
|
+
const cmd = argv[0];
|
|
65
|
+
const flags = parseFlags(argv.slice(1));
|
|
66
|
+
|
|
67
|
+
if (flags.version || cmd === '--version') { console.log(`ScripterX ${VERSION}`); return; }
|
|
68
|
+
if (cmd === '-h' || cmd === '--help') { console.log(HELP); return; }
|
|
69
|
+
|
|
70
|
+
// ── interactive shell (default) ──
|
|
71
|
+
if (!cmd) {
|
|
72
|
+
const cfg = config.load();
|
|
73
|
+
let username = cfg.username;
|
|
74
|
+
// validate token quietly so the banner shows the right user
|
|
75
|
+
if (cfg.jwt) { try { username = (await new ApiClient().me()).username; } catch { username = null; } }
|
|
76
|
+
const ctx = {
|
|
77
|
+
username, server: cfg.server_base_url,
|
|
78
|
+
dispatch: async (name, io) => {
|
|
79
|
+
const fn = REGISTRY[name] || REGISTRY[name.split(' ')[0]];
|
|
80
|
+
if (!fn) { io.print(` ✗ unknown command: ${name} (type 'help')`); return; }
|
|
81
|
+
await fn(io, null, {});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const { waitUntilExit } = render(h(App, { ctx }));
|
|
85
|
+
await waitUntilExit();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── one-shot mode ──
|
|
90
|
+
const map = { run: 'run', campaigns: 'campaigns', export: 'export', balance: 'balance',
|
|
91
|
+
creds: 'creds', stop: 'stop', delete: 'delete', whoami: 'whoami', login: 'login',
|
|
92
|
+
logout: 'logout', config: 'config', help: 'help' };
|
|
93
|
+
const fnName = map[cmd];
|
|
94
|
+
if (!fnName) { cliIo.print(` ✗ unknown command: ${cmd}`); console.log(HELP); process.exit(1); }
|
|
95
|
+
const args = {
|
|
96
|
+
provider: flags.provider, count: flags.count ? +flags.count : null,
|
|
97
|
+
concurrency: flags.concurrency ? +flags.concurrency : null, name: flags.name,
|
|
98
|
+
checkMinutes: flags['check-minutes'] === true ? true : (flags['check-minutes'] === false ? false : null),
|
|
99
|
+
campaign: flags._[0], out: flags.out, action: flags._[0], value: flags._[1],
|
|
100
|
+
};
|
|
101
|
+
try { await REGISTRY[fnName](cliIo, null, args); }
|
|
102
|
+
catch (e) { cliIo.print(` ✗ ${e.message}`); process.exit(1); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
main();
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Minimal interactive prompts (readline) for the non-ink screens (login, run setup).
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import { paint } from './util.js';
|
|
4
|
+
|
|
5
|
+
function makeRl() {
|
|
6
|
+
return readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ask(question, { dflt } = {}) {
|
|
10
|
+
const rl = makeRl();
|
|
11
|
+
const suffix = dflt != null ? paint.dim(` (${dflt})`) : '';
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
rl.question(` ${paint.bold(question)}${suffix}: `, (a) => {
|
|
14
|
+
rl.close();
|
|
15
|
+
resolve(a.trim() || (dflt != null ? String(dflt) : ''));
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function askChoice(question, choices, { dflt } = {}) {
|
|
21
|
+
return ask(`${question} ${paint.dim(`[${choices.join('/')}]`)}`, { dflt }).then((a) => {
|
|
22
|
+
const v = (a || dflt || '').toLowerCase();
|
|
23
|
+
return choices.includes(v) ? v : (dflt || choices[0]);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function askInt(question, { dflt } = {}) {
|
|
28
|
+
return ask(question, { dflt }).then((a) => {
|
|
29
|
+
const n = parseInt(a, 10);
|
|
30
|
+
return Number.isFinite(n) ? n : dflt;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function confirm(question, { dflt = true } = {}) {
|
|
35
|
+
return ask(`${question} ${paint.dim('[y/n]')}`, { dflt: dflt ? 'y' : 'n' })
|
|
36
|
+
.then((a) => /^y/i.test(a));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Hidden password input (no echo).
|
|
40
|
+
export function askSecret(question) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
const rl = makeRl();
|
|
43
|
+
const stdin = process.stdin;
|
|
44
|
+
const onData = (chBuf) => {
|
|
45
|
+
const ch = chBuf.toString();
|
|
46
|
+
if (ch === '\n' || ch === '\r' || ch === '') { stdin.removeListener('data', onData); }
|
|
47
|
+
};
|
|
48
|
+
rl._writeToOutput = (str) => {
|
|
49
|
+
// hide everything after the prompt
|
|
50
|
+
if (rl.line.length === 0) rl.output.write(str);
|
|
51
|
+
else rl.output.write('');
|
|
52
|
+
};
|
|
53
|
+
stdin.on('data', onData);
|
|
54
|
+
rl.question(` ${paint.bold(question)}: `, (a) => {
|
|
55
|
+
rl.close();
|
|
56
|
+
process.stdout.write('\n');
|
|
57
|
+
resolve(a.trim());
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// OTPCart provider (api.otpcart.xyz) — email/password → JWT, WebSocket OTP push.
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
|
|
4
|
+
const BASE = 'https://api.otpcart.xyz';
|
|
5
|
+
const WS_URL = 'wss://api.otpcart.xyz/check-otp';
|
|
6
|
+
const SERVICE_ID = '68b19097980e8cf480b1df04';
|
|
7
|
+
const OTP_RE = /\b(\d{6})\b/;
|
|
8
|
+
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
|
|
9
|
+
'(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36';
|
|
10
|
+
|
|
11
|
+
export async function login(email, password) {
|
|
12
|
+
const res = await fetch(`${BASE}/users/login`, {
|
|
13
|
+
method: 'POST', headers: { 'Content-Type': 'application/json', Origin: 'https://www.otpcart.xyz', 'User-Agent': UA },
|
|
14
|
+
body: JSON.stringify({ email, password }), signal: AbortSignal.timeout(15000),
|
|
15
|
+
});
|
|
16
|
+
const tok = (await res.json().catch(() => ({}))).token;
|
|
17
|
+
if (!tok) throw new Error('OTPCart login failed — check email/password');
|
|
18
|
+
return tok;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function balance() { return null; }
|
|
22
|
+
|
|
23
|
+
export class OTPCartProvider {
|
|
24
|
+
name = 'otpcart';
|
|
25
|
+
constructor(jwt, deepCheck = false) { this.jwt = jwt; this.deepCheck = deepCheck; }
|
|
26
|
+
|
|
27
|
+
_hdrs() {
|
|
28
|
+
return { 'Content-Type': 'application/json', Authorization: `Bearer ${this.jwt}`,
|
|
29
|
+
Origin: 'https://www.otpcart.xyz', 'User-Agent': UA };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async rentOnce() {
|
|
33
|
+
let d;
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${BASE}/mobile/generate`, {
|
|
36
|
+
method: 'POST', headers: this._hdrs(),
|
|
37
|
+
body: JSON.stringify({ serviceId: SERVICE_ID, isDeepCheck: this.deepCheck }),
|
|
38
|
+
signal: AbortSignal.timeout(15000),
|
|
39
|
+
});
|
|
40
|
+
d = await res.json();
|
|
41
|
+
} catch (e) { return { number: null, msg: e.message, err: e }; }
|
|
42
|
+
if (d.isNumberGenerated && d.mobile?.mobileno) {
|
|
43
|
+
const m = d.mobile;
|
|
44
|
+
return { number: { mobile: m.mobileno, txn: m.serialNumber || '', extra: m._id || '', cost: Number(m.price || 0) }, msg: 'ok', err: null };
|
|
45
|
+
}
|
|
46
|
+
return { number: null, msg: d.message || 'no number available', err: 'err' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
startOtp(number) { return new OTPCartStream(this.jwt, number.txn, number.extra); }
|
|
50
|
+
|
|
51
|
+
async cancel(number) {
|
|
52
|
+
try {
|
|
53
|
+
await fetch(`${BASE}/otp/cancelOtp`, {
|
|
54
|
+
method: 'POST', headers: this._hdrs(),
|
|
55
|
+
body: JSON.stringify({ serialNumber: number.txn, mobileId: number.extra, serviceId: SERVICE_ID }),
|
|
56
|
+
signal: AbortSignal.timeout(12000),
|
|
57
|
+
});
|
|
58
|
+
} catch { /* */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class OTPCartStream {
|
|
63
|
+
constructor(jwt, serial, mobileId) {
|
|
64
|
+
this.serial = serial;
|
|
65
|
+
this.mobileId = mobileId;
|
|
66
|
+
this.otp = null;
|
|
67
|
+
this.arrived = false;
|
|
68
|
+
this.ws = new WebSocket(`${WS_URL}?token=${jwt}`);
|
|
69
|
+
this.ws.on('message', (raw) => {
|
|
70
|
+
let msg; try { msg = JSON.parse(raw.toString()); } catch { return; }
|
|
71
|
+
if (msg.otpMessage) {
|
|
72
|
+
const m = String(msg.otpMessage).match(OTP_RE);
|
|
73
|
+
if (m) { this.otp = m[1]; this.arrived = true; }
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
this.ws.on('error', () => { /* swallow — poll() just returns nothing */ });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async poll() {
|
|
80
|
+
if (this.otp) { const o = this.otp; this.otp = null; return { otp: o, arrived: true }; }
|
|
81
|
+
return { otp: null, arrived: this.arrived };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resend() {
|
|
85
|
+
try {
|
|
86
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
87
|
+
this.ws.send(JSON.stringify({ serialNumber: this.serial, mobileId: this.mobileId, resend: true }));
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
} catch { /* */ }
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
close() { try { this.ws.close(); } catch { /* */ } }
|
|
95
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// TempOTP provider (api.tempotp.online) — GET-based, single API key, HTTP-poll OTP.
|
|
2
|
+
import { extractOtp } from '../flipkart.js';
|
|
3
|
+
|
|
4
|
+
const BASE = 'https://api.tempotp.online';
|
|
5
|
+
export const SERVICES = { '1040': 10.0, '940': 16.0 };
|
|
6
|
+
export const DEFAULT_SERVICE = '940';
|
|
7
|
+
const COUNTRY = '22';
|
|
8
|
+
|
|
9
|
+
async function getJson(path, params) {
|
|
10
|
+
const url = `${BASE}${path}?` + new URLSearchParams(params);
|
|
11
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(12000) });
|
|
12
|
+
return res.json();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function balance(apiKey) {
|
|
16
|
+
try {
|
|
17
|
+
const d = await getJson('/getBalance', { apikey: apiKey });
|
|
18
|
+
if (d.status === 200) return d.balance ?? 0;
|
|
19
|
+
} catch { /* */ }
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function bought(d) {
|
|
24
|
+
if (!d || typeof d !== 'object') return null;
|
|
25
|
+
const tid = d.tid || d.id || d.transaction_id || d.order_id;
|
|
26
|
+
const num = d.number || d.phone || d.mobile;
|
|
27
|
+
return tid && num ? { tid: String(tid), number: String(num) } : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class TempOTPProvider {
|
|
31
|
+
name = 'tempotp';
|
|
32
|
+
constructor(apiKey, serviceId = DEFAULT_SERVICE) {
|
|
33
|
+
this.apiKey = apiKey;
|
|
34
|
+
this.serviceId = SERVICES[serviceId] ? serviceId : DEFAULT_SERVICE;
|
|
35
|
+
this.cost = SERVICES[this.serviceId];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async rentOnce() {
|
|
39
|
+
let d;
|
|
40
|
+
try { d = await getJson('/buyNumber', { apikey: this.apiKey, id: this.serviceId, country: COUNTRY }); }
|
|
41
|
+
catch (e) { return { number: null, msg: e.message, err: e }; }
|
|
42
|
+
if (d.status === 400 || d.status === 401) return { number: null, msg: d.message, err: 'err' };
|
|
43
|
+
let got = bought(d) || (typeof d.data === 'object' && !Array.isArray(d.data) ? bought(d.data) : null);
|
|
44
|
+
if (!got && Array.isArray(d.data) && d.data.length) got = bought(d.data[0]);
|
|
45
|
+
if (got) return { number: { mobile: got.number, txn: got.tid, extra: '', cost: this.cost }, msg: 'ok', err: null };
|
|
46
|
+
return { number: null, msg: d.message || 'no number available', err: 'err' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
startOtp(number) { return new TempStream(this.apiKey, number.txn); }
|
|
50
|
+
|
|
51
|
+
async cancel(number) {
|
|
52
|
+
try { await getJson('/cancelNumber', { apikey: this.apiKey, id: number.txn }); } catch { /* */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class TempStream {
|
|
57
|
+
constructor(apiKey, txn) { this.apiKey = apiKey; this.txn = txn; this.last = 0; this.arrived = false; }
|
|
58
|
+
|
|
59
|
+
async poll() {
|
|
60
|
+
if (Date.now() - this.last < 2000) return { otp: null, arrived: this.arrived };
|
|
61
|
+
this.last = Date.now();
|
|
62
|
+
let d;
|
|
63
|
+
try { d = await getJson('/checkSms', { apikey: this.apiKey, id: this.txn }); }
|
|
64
|
+
catch { return { otp: null, arrived: this.arrived }; }
|
|
65
|
+
const arr = d.sms;
|
|
66
|
+
if (Array.isArray(arr) && arr.length) {
|
|
67
|
+
this.arrived = true;
|
|
68
|
+
for (const s of arr) {
|
|
69
|
+
const msg = (s && typeof s === 'object') ? s.msg : s;
|
|
70
|
+
if (typeof msg === 'string') { const otp = extractOtp(msg); if (otp) return { otp, arrived: true }; }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (d.status === 400 && d.message) {
|
|
74
|
+
this.arrived = true;
|
|
75
|
+
const otp = extractOtp(d.message);
|
|
76
|
+
if (otp) return { otp, arrived: true };
|
|
77
|
+
}
|
|
78
|
+
return { otp: null, arrived: this.arrived };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
resend() { return false; }
|
|
82
|
+
close() { /* no socket */ }
|
|
83
|
+
}
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Visual theme — Claude-Code / modern-editor aesthetic for the ink TUI.
|
|
2
|
+
export const COLORS = {
|
|
3
|
+
accent: '#7dd3fc', // cyan — primary
|
|
4
|
+
success: '#86efac', // green
|
|
5
|
+
warn: '#fcd34d', // amber
|
|
6
|
+
danger: '#fca5a5', // red
|
|
7
|
+
muted: 'gray',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const BRAND_GRAD = ['#7dd3fc', '#a5b4fc', '#c4b5fd']; // cyan→indigo→violet
|
|
11
|
+
|
|
12
|
+
export const STATUS = {
|
|
13
|
+
success: { glyph: '✓', color: COLORS.success },
|
|
14
|
+
failed: { glyph: '✗', color: COLORS.danger },
|
|
15
|
+
cancelled: { glyph: '⊘', color: COLORS.warn },
|
|
16
|
+
running: { glyph: '▸', color: COLORS.accent },
|
|
17
|
+
completed: { glyph: '•', color: COLORS.muted },
|
|
18
|
+
pending: { glyph: '•', color: COLORS.muted },
|
|
19
|
+
stopping: { glyph: '◐', color: COLORS.warn },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const PHASE_LABEL = {
|
|
23
|
+
renting: 'renting number',
|
|
24
|
+
rent_retry: 'retrying rent',
|
|
25
|
+
rent_wait: 'no numbers — waiting',
|
|
26
|
+
ws: 'opening OTP channel',
|
|
27
|
+
send_otp: 'sending OTP',
|
|
28
|
+
await_otp: 'waiting for OTP',
|
|
29
|
+
otp_resend: 'OTP resent',
|
|
30
|
+
verify: 'verifying OTP',
|
|
31
|
+
minutes: 'checking Minutes',
|
|
32
|
+
saving: 'saving session',
|
|
33
|
+
done: 'extracted',
|
|
34
|
+
failed: 'failed',
|
|
35
|
+
cancelling: 'releasing number',
|
|
36
|
+
cancelled: 'cancelled',
|
|
37
|
+
rented: 'got a number',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function hexRgb(h) {
|
|
41
|
+
h = h.replace('#', '');
|
|
42
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Return an array of {char, color} for a smooth gradient sweep across `colors`.
|
|
46
|
+
export function gradientChars(s, colors = BRAND_GRAD) {
|
|
47
|
+
const stops = colors.map(hexRgb);
|
|
48
|
+
const n = Math.max(s.length - 1, 1);
|
|
49
|
+
return [...s].map((ch, i) => {
|
|
50
|
+
const pos = (i / n) * (stops.length - 1);
|
|
51
|
+
const lo = Math.floor(pos);
|
|
52
|
+
const hi = Math.min(lo + 1, stops.length - 1);
|
|
53
|
+
const f = pos - lo;
|
|
54
|
+
const r = Math.round(stops[lo][0] + (stops[hi][0] - stops[lo][0]) * f);
|
|
55
|
+
const g = Math.round(stops[lo][1] + (stops[hi][1] - stops[lo][1]) * f);
|
|
56
|
+
const b = Math.round(stops[lo][2] + (stops[hi][2] - stops[lo][2]) * f);
|
|
57
|
+
return { char: ch, color: `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}` };
|
|
58
|
+
});
|
|
59
|
+
}
|
package/src/throttle.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Global 2-second throttle for Flipkart HTTP calls — serializes all FK requests
|
|
2
|
+
// (send/verify/resend/minutes) across workers so a single residential IP never bursts.
|
|
3
|
+
// Provider (OTPCart/TempOTP) and server calls are NOT throttled.
|
|
4
|
+
|
|
5
|
+
const MIN_GAP = 2000; // ms
|
|
6
|
+
let chain = Promise.resolve();
|
|
7
|
+
let last = 0;
|
|
8
|
+
|
|
9
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
|
+
|
|
11
|
+
// Acquire the throttle: returns a promise that resolves once it's this caller's turn
|
|
12
|
+
// AND at least MIN_GAP has passed since the previous FK call. Serializes via a chain.
|
|
13
|
+
export function wait() {
|
|
14
|
+
const turn = chain.then(async () => {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const gap = now - last;
|
|
17
|
+
if (gap < MIN_GAP) await sleep(MIN_GAP - gap);
|
|
18
|
+
last = Date.now();
|
|
19
|
+
});
|
|
20
|
+
// keep the chain going but swallow errors so one failure doesn't break the queue
|
|
21
|
+
chain = turn.catch(() => {});
|
|
22
|
+
return turn;
|
|
23
|
+
}
|