scripter-x 1.0.24 → 1.0.26

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/flipkart.js +111 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripter-x",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
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
@@ -85,69 +85,145 @@ export class FlipkartLogin {
85
85
  this.reqId = null;
86
86
  this.sn = '';
87
87
  this.vid = '';
88
+ this.isNewUser = false; // set by sendOtp: new number → verify as SIGNUP, not LOGIN
88
89
  }
89
90
 
90
- async sendOtp(mobile) {
91
- 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) {
92
113
  const body = { actionRequestContext: {
93
114
  type: 'LOGIN_IDENTITY_VERIFY', loginIdPrefix: '+91', loginId: mobile.slice(-10),
94
115
  clientQueryParamMap: { ret: '/my-account', entryPage: 'DEFAULT' },
95
116
  loginType: 'MOBILE', verificationType: 'OTP', screenName: 'LOGIN_V4_MOBILE',
96
117
  triggerSna: false, sourceContext: 'DEFAULT' } };
97
- let res;
98
- try {
99
- res = await fetch(`${WEB_HOST}/1/action/view`, {
100
- method: 'POST', body: JSON.stringify(body),
101
- headers: {
102
- Accept: '*/*', 'Content-Type': 'application/json',
103
- 'X-User-Agent': FKUA, 'User-Agent': FKUA,
104
- Origin: 'https://www.flipkart.com', Referer: 'https://www.flipkart.com/',
105
- 'sec-ch-ua-mobile': '?1', 'sec-ch-ua-platform': '"iOS"', 'sec-fetch-site': 'same-site',
106
- 'x-request-metaInfo': '{"actionType":"LOGIN_IDENTITY_VERIFY","pageUri":"questContext"}',
107
- },
108
- });
109
- } catch (e) { throw new TransientError(`send network error: ${e.message}`); }
110
- // 529/5xx = Flipkart overloaded / blocking this IP — retryable.
111
- if (res.status === 429 || res.status === 529 || res.status >= 500) {
112
- 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
113
151
  }
114
- parseCookies(res, this.jar);
115
- const data = await res.json().catch(() => ({}));
116
- const rid = data?.RESPONSE?.actionResponseContext?.requestId;
117
- // 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).
118
153
  if (!rid) throw new TransientError('blocked while sending OTP (no requestId — throttled)');
119
154
  this.reqId = rid;
155
+ // NEW vs EXISTING account: for an UNREGISTERED number FK still sends the OTP but
156
+ // routes the flow to signup. The reliable signals (verified against captures):
157
+ // • landingPageAction.url starts with "/signup" (existing accounts: no such url)
158
+ // • eVar79 tracking is "New|…|Sign-Up|…" (existing: "Existing|…|Sign-In|…")
159
+ // NOTE: a bare "isNewUser":true appears even for EXISTING accounts (unrelated page
160
+ // flag) — do NOT use it. verifyOtp uses type:SIGNUP when isNewUser, else LOGIN.
161
+ const landingUrl = data?.RESPONSE?.actionResponseContext?.landingPageAction?.url || '';
162
+ const eVar79 = data?.RESPONSE?.pageResponse?.pageData?.trackingContext?.tracking?.eVar79 || '';
163
+ this.isNewUser = /^\/signup\b/.test(landingUrl)
164
+ || /\bSign-?Up\b/i.test(eVar79)
165
+ || /^New\b/i.test(eVar79);
120
166
  this.sn = this.jar.SN || '';
121
167
  this.vid = this.sn ? this.sn.split('.')[0] : '';
122
168
  return rid;
123
169
  }
124
170
 
125
- async verifyOtp(mobile, otp) {
171
+ // Low-level OTP verify with an explicit action type ("LOGIN" for existing
172
+ // accounts, "SIGNUP" for brand-new numbers). Returns { res, text, data }.
173
+ async _verifyWith(type, mobile, otp) {
126
174
  await throttle.wait();
127
175
  const secureToken = this.vid ? `${this.vid}:${this.vid}` : '';
128
176
  const body = { actionRequestContext: {
129
- type: 'LOGIN', loginIdPrefix: '+91', loginId: mobile.slice(-10), password: null,
177
+ type, loginIdPrefix: '+91', loginId: mobile.slice(-10), password: null,
130
178
  otp, otpRequestId: this.reqId, remainingAttempts: 5, phoneNumberFormat: 'E164',
131
- loginType: 'MOBILE', verificationType: 'OTP', sourceContext: 'GO_LOGIN', churned: false,
132
- otpRegex: null, data: null, clientQueryParamMap: null } };
179
+ loginType: 'MOBILE', verificationType: 'OTP', sourceContext: type === 'SIGNUP' ? 'DEFAULT' : 'GO_LOGIN',
180
+ churned: false, otpRegex: null, data: null, clientQueryParamMap: null } };
133
181
  const res = await fetch(`${NATIVE_HOST}/1/action/view`, {
134
182
  method: 'POST', body: JSON.stringify(body),
135
183
  headers: {
136
184
  'User-Agent': 'okhttp/4.9.2', 'X-User-Agent': nativeUa(this.vid),
137
185
  'Content-Type': 'application/json; charset=UTF-8',
138
- 'x-request-metaInfo': '{"actionType":"LOGIN","pageUri":"questContext"}',
186
+ 'x-request-metaInfo': `{"actionType":"${type}","pageUri":"questContext"}`,
139
187
  at: this.jar.at || '', sn: this.sn, secureToken, secureCookie: this.jar.S || '',
140
188
  Cookie: cookieHeader(this.jar),
141
189
  },
142
190
  });
143
191
  const text = await res.text();
192
+ let data = null;
193
+ try { data = JSON.parse(text); } catch { /* caller handles */ }
194
+ return { res, text, data };
195
+ }
196
+
197
+ async verifyOtp(mobile, otp) {
198
+ // New number → SIGNUP (FK rejects LOGIN with "account does not exist"); existing → LOGIN.
199
+ const primary = this.isNewUser ? 'SIGNUP' : 'LOGIN';
200
+ const fallback = this.isNewUser ? 'LOGIN' : 'SIGNUP';
201
+
202
+ let { res, text, data } = await this._verifyWith(primary, mobile, otp);
144
203
  // 529/5xx = Flipkart overloaded / soft-blocking this IP — NOT the OTP's fault.
145
204
  if (res.status === 429 || res.status === 529 || res.status >= 500) {
146
205
  throw new TransientError(`Flipkart busy (HTTP ${res.status}) — will retry`);
147
206
  }
148
- let data;
149
- try { data = JSON.parse(text); } catch { throw new TransientError('Flipkart returned a non-JSON response — will retry'); }
150
- const env = data.SESSION;
207
+ if (!data) throw new TransientError('Flipkart returned a non-JSON response — will retry');
208
+
209
+ let env = data.SESSION;
210
+ // If the primary type didn't log in AND the failure looks like a wrong account-type
211
+ // (not a wrong OTP), retry once with the other type. This auto-creates a new account
212
+ // when a number we thought existed doesn't (and vice-versa) — same OTP, no extra SMS.
213
+ if ((!env || !env.isLoggedIn)) {
214
+ const fkMsg0 = flipkartErrorMessage(data, text);
215
+ const low0 = (fkMsg0 + ' ' + text).toLowerCase();
216
+ const wrongType = /does not exist|not registered|no account|sign\s*up|signup|already (registered|exists)|please login/.test(low0);
217
+ const wrongOtp = /incorrect|wrong|invalid|expired|otp.*not.*match|verification code/.test(low0);
218
+ if (wrongType && !wrongOtp) {
219
+ const r2 = await this._verifyWith(fallback, mobile, otp);
220
+ if (r2.data && r2.data.SESSION && r2.data.SESSION.isLoggedIn) {
221
+ ({ res, text, data } = r2);
222
+ env = data.SESSION;
223
+ }
224
+ }
225
+ }
226
+
151
227
  if (!env || !env.isLoggedIn) {
152
228
  // pull Flipkart's own human message if present (the real reason)
153
229
  const fkMsg = flipkartErrorMessage(data, text);
@@ -163,11 +239,15 @@ export class FlipkartLogin {
163
239
  parseCookies(res, fresh);
164
240
  const ud = fresh.ud || this.jar.ud || '';
165
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}` : '');
166
246
  return {
167
247
  accountId: env.accountId || '', at: env.at || '', rt: env.rt || '', sn: env.sn || '',
168
248
  secureToken, secureCookie: this.jar.S || '', ud, vd,
169
249
  cookie_T: fresh.T || this.jar.T || '', // T cookie — needed for the email-attach calls
170
- visitId: (env.sn || '').split('.')[0], nsid: env.nsid || '',
250
+ visitId: env.vid || (env.sn || '').split('.')[0], nsid: env.nsid || '',
171
251
  mobileNo: mobile.slice(-10), isLoggedIn: true, rt_expires_at: rtExpiry(env.rt || ''),
172
252
  };
173
253
  }