scripter-x 1.0.32 → 1.0.33

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.32",
3
+ "version": "1.0.33",
4
4
  "description": "ScripterX — local Flipkart session extractor (runs on your residential IP, syncs to marthunt)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -113,6 +113,8 @@ export function newSession(number) {
113
113
  mId: null, // base64 member id (server-provided)
114
114
  visitorId: null, // base64 visitor id (server-provided)
115
115
  user: null,
116
+ isSignup: false, // true if verify created a brand-new account
117
+ activated: false, // true once the name/profile step succeeded (or wasn't needed)
116
118
  };
117
119
  }
118
120
 
@@ -146,18 +148,44 @@ export async function sendOtp(session) {
146
148
  const refId = r.json && (r.json.refId || r.json.ref_id);
147
149
  const ok = r.status === 200 && !!refId;
148
150
  if (ok) session.refId = refId;
149
- return { ok, status: r.status, msg: ok ? (r.json.message || 'OTP sent') : (r.json?.message || r.text.slice(0, 160)) };
151
+ // Surface BigBasket's error code so callers can react:
152
+ // HU4001 = account does not exist / inactive (often a recycled/dead number) → rotate
153
+ // HU4000 = bad request shape
154
+ const err = r.json?.errors?.[0];
155
+ const code = err?.code_str || null;
156
+ return {
157
+ ok, status: r.status, code,
158
+ msg: ok ? (r.json.message || 'OTP sent') : (err?.msg || err?.display_msg || r.json?.message || r.text.slice(0, 160)),
159
+ };
150
160
  }
151
161
 
162
+ // A default name for brand-new accounts. BigBasket requires a non-empty first_name to
163
+ // activate a fresh signup; override via $BB_SIGNUP_NAME.
164
+ const SIGNUP_NAME = (process.env.BB_SIGNUP_NAME || 'Rohan').trim();
165
+
152
166
  // Step 2 — verify the OTP. On success, fills session.token / mId / visitorId / user.
167
+ // Handles BOTH returning members AND brand-new signups: a new account comes back with
168
+ // is_signup:true and an empty name, then needs a profile/set-name call to activate
169
+ // (otherwise the account is half-created and the session is unusable). See the BigBasket
170
+ // signup flow: send(unified_login) → verify → [if is_signup] profile/set-name.
171
+ async function _doVerify(session, otp, referrer) {
172
+ const body = { mobile_no: session.mobile, mobile_no_otp: String(otp).trim(), refId: session.refId, source: 'BIGBASKET' };
173
+ if (referrer) body.referrer = referrer; // signup variant adds referrer:"login_signup"
174
+ await throttle.wait();
175
+ return req(session, 'POST', '/member-tdl/v3/member/unified-login/', body,
176
+ { 'X-Login-Origin': referrer || 'unified_login', 'X-tcp-client-id': 'BIGBASKET-ANDROID-APP' });
177
+ }
178
+
153
179
  export async function verifyOtp(session, otp) {
154
180
  if (!session.refId) return { ok: false, status: 0, error: 'no refId — call sendOtp first' };
155
- await throttle.wait();
156
- const r = await req(session, 'POST', '/member-tdl/v3/member/unified-login/',
157
- { mobile_no: session.mobile, mobile_no_otp: String(otp).trim(), refId: session.refId, source: 'BIGBASKET' },
158
- { 'X-Login-Origin': 'unified_login', 'X-tcp-client-id': 'BIGBASKET-ANDROID-APP' });
181
+ // First try the returning-member verify. If the number has no account yet (HU4001),
182
+ // retry with the signup referrer so a brand-new account is created.
183
+ let r = await _doVerify(session, otp, null);
184
+ if (r.status !== 200 && r.json?.errors?.[0]?.code_str === 'HU4001') {
185
+ r = await _doVerify(session, otp, 'login_signup');
186
+ }
159
187
 
160
- // Native unified-login returns { bb_token, m_id, visitor_id }.
188
+ // Native unified-login returns { bb_token, m_id, visitor_id, is_signup, first_name, ... }.
161
189
  const J = r.json || {};
162
190
  const token = J.bb_token || J.BBAUTHTOKEN || session.jar.BBAUTHTOKEN || J.token;
163
191
  if (r.status === 200 && token) {
@@ -165,11 +193,39 @@ export async function verifyOtp(session, otp) {
165
193
  session.mId = J.m_id || session.jar._bb_mid || null;
166
194
  session.visitorId = J.visitor_id || session.jar._bb_vid || null;
167
195
  session.user = J.member_details || J.user || null;
168
- return { ok: true, user: session.user };
196
+ session.isSignup = !!(J.is_signup || J.isSignup);
197
+ // A brand-new account (or one with no name) must be activated with a name, else the
198
+ // session is half-baked. Best-effort: failure here still returns the token we got.
199
+ const noName = !((J.first_name || J.firstName || '').trim());
200
+ if (session.isSignup || noName) {
201
+ session.activated = await setProfile(session, SIGNUP_NAME);
202
+ } else {
203
+ session.activated = true;
204
+ }
205
+ return { ok: true, user: session.user, isSignup: session.isSignup, activated: session.activated };
206
+ }
207
+ // Error codes seen at verify:
208
+ // HU4011 = wrong/expired OTP HU4001 = account does not exist/inactive HU4000 = bad request
209
+ const e = J.errors?.[0];
210
+ const code = e?.code_str || null;
211
+ const error = e?.msg || e?.display_msg || J.message || r.text.slice(0, 160);
212
+ return { ok: false, status: r.status, code, error };
213
+ }
214
+
215
+ // Step 3 (new users only) — set the name to activate the freshly-created account.
216
+ // POST /member-tdl/v3/member/profile/ with the BBAUTHTOKEN we just got. Returns true on
217
+ // success. Never throws — a name-set failure shouldn't discard the token.
218
+ export async function setProfile(session, firstName, lastName = '') {
219
+ try {
220
+ if (session.token) session.jar.BBAUTHTOKEN = session.token; // authenticate the call
221
+ await throttle.wait();
222
+ const r = await req(session, 'POST', '/member-tdl/v3/member/profile/',
223
+ { first_name: firstName, last_name: lastName, name: lastName ? `${firstName} ${lastName}` : firstName },
224
+ { 'X-Login-Origin': 'login_signup', 'X-tcp-client-id': 'BIGBASKET-ANDROID-APP' });
225
+ return r.status === 200;
226
+ } catch {
227
+ return false;
169
228
  }
170
- // HU4000 / wrong-otp surface here
171
- const err = J.errors?.[0]?.msg || J.message || r.text.slice(0, 160);
172
- return { ok: false, status: r.status, error: err };
173
229
  }
174
230
 
175
231
  // Build the persisted-session object — the requested {bbAuthToken, mId, bbVisitorId}
@@ -61,8 +61,22 @@ export class TempOTPProvider {
61
61
 
62
62
  startOtp(number) { return new TempStream(this.apiKey, number.txn); }
63
63
 
64
+ // Cancel + refund the rented number. Returns { ok, tooEarly, msg } so the worker's
65
+ // releaseNumber loop knows whether the cancel landed. TempOTP enforces a ~2-min
66
+ // no-cancel policy: a too-early attempt is reported back so the caller waits + retries
67
+ // (rather than treating it as a hard failure or a success).
64
68
  async cancel(number) {
65
- try { await getJson('/cancelNumber', { apikey: this.apiKey, id: number.txn }); } catch { /* */ }
69
+ let d;
70
+ try { d = await getJson('/cancelNumber', { apikey: this.apiKey, id: number.txn }); }
71
+ catch (e) { return { ok: false, tooEarly: false, msg: e.message }; }
72
+ // success: API returns status 200 (sometimes with a "cancelled"/"success" message)
73
+ const msg = (d && (d.message || d.msg)) || '';
74
+ if (d && (d.status === 200 || /cancel|success|refund/i.test(msg))) {
75
+ return { ok: true, tooEarly: false, msg: msg || 'cancelled' };
76
+ }
77
+ // the 2-min policy / "wait before cancel" → retryable, not a hard fail
78
+ const tooEarly = /2\s*min|two\s*min|120\s*sec|wait|too early|cannot cancel|not allowed yet|before/i.test(msg);
79
+ return { ok: false, tooEarly, msg: msg || `cancel rejected (status ${d?.status})` };
66
80
  }
67
81
  }
68
82
 
package/src/worker.js CHANGED
@@ -10,7 +10,11 @@ import * as bigbasket from './providers/bigbasket.js';
10
10
 
11
11
  const OTP_RESEND_AFTER = 60_000;
12
12
  const OTPCART_DEADLINE = 120_000;
13
- const TEMPOTP_DEADLINE = 90_000; // TempOTP: give up after 90s, cancel + buy a new number
13
+ // TempOTP now enforces a 2-min no-cancel policy on rented numbers (you can't cancel/refund
14
+ // a number before 120s). So wait the FULL 2 min for the OTP — there's no point bailing at
15
+ // 90s when the number can't be released until 120s anyway. The cancel-lock below (also
16
+ // 120s) then fires right at the unlock point, identical to OTPCart's behaviour.
17
+ const TEMPOTP_DEADLINE = 120_000;
14
18
  const WRONG_OTP_WAIT = 60_000;
15
19
  const NUMBER_CANCEL_LOCK = 120_000;
16
20
  const RENT_RETRY_DELAY = 3_000;
@@ -917,7 +921,12 @@ export class BigBasketWorker {
917
921
  // bigbasket.sendOtp does register/device → header → otp (all on our IP).
918
922
  this._emit(slot, { phase: 'send_otp', detail: 'sending OTP' });
919
923
  const sent = await bigbasket.sendOtp(session);
920
- if (!sent.ok) return fail(`send: ${sent.msg}`, 'send_blocked');
924
+ if (!sent.ok) {
925
+ // HU4001 = the rented number has no BigBasket account / is inactive (recycled).
926
+ // No SMS will ever come → don't wait; mark as a dead number and rotate to a new one.
927
+ const dead = sent.code === 'HU4001';
928
+ return fail(`send: ${sent.msg}`, dead ? 'no_account' : 'send_blocked');
929
+ }
921
930
 
922
931
  // Poll the rented number's SMS stream for the BigBasket OTP.
923
932
  const end = Date.now() + this.deadlineMs;