scripter-x 1.0.32 → 1.0.34
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/providers/bigbasket.js +66 -10
- package/src/providers/tempotp.js +15 -1
- package/src/update.js +51 -9
- package/src/worker.js +11 -2
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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}
|
package/src/providers/tempotp.js
CHANGED
|
@@ -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
|
-
|
|
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/update.js
CHANGED
|
@@ -48,44 +48,86 @@ export async function runUpdate() {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Hard-reset the terminal so the freshly-spawned child inherits a CLEAN, cooked TTY.
|
|
52
|
+
//
|
|
53
|
+
// THE BUG this fixes: after the old ink app unmounts, the terminal could still be left in
|
|
54
|
+
// raw mode WITH SGR mouse tracking enabled (`\x1b[?1000h…`). When the restarted process
|
|
55
|
+
// took over that same TTY, every keypress/click emitted raw escape/coordinate codes that
|
|
56
|
+
// got echoed as garbage ("random shit on every keypress") until the user killed + relaunched
|
|
57
|
+
// manually. ink's alt-screen restore only ran on `process.on('exit')` AND the old + new
|
|
58
|
+
// processes briefly shared the TTY, so the disable sequences raced the child's re-enable.
|
|
59
|
+
//
|
|
60
|
+
// Fix: synchronously (1) disable ALL mouse modes + restore the screen, (2) take stdin out of
|
|
61
|
+
// raw mode, pause it, and drop every listener — BEFORE the child is spawned — so there is no
|
|
62
|
+
// window where mouse reporting or a stray raw-mode listener can corrupt the child's input.
|
|
63
|
+
function resetTerminal() {
|
|
64
|
+
try {
|
|
65
|
+
if (process.stdout.isTTY) {
|
|
66
|
+
// disable button/motion/SGR mouse modes (the source of the coordinate-code garbage)
|
|
67
|
+
process.stdout.write('\x1b[?1000l\x1b[?1003l\x1b[?1006l');
|
|
68
|
+
process.stdout.write('\x1b[?25h'); // show cursor
|
|
69
|
+
process.stdout.write('\x1b[?1049l'); // leave alt-screen → normal buffer
|
|
70
|
+
}
|
|
71
|
+
} catch { /* */ }
|
|
72
|
+
try {
|
|
73
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
74
|
+
process.stdin.setRawMode(false); // back to cooked/canonical mode
|
|
75
|
+
}
|
|
76
|
+
// drop every data/keypress listener the old ink app (or our mouse hook) attached, then
|
|
77
|
+
// pause so nothing in THIS process consumes input meant for the child.
|
|
78
|
+
process.stdin.removeAllListeners('data');
|
|
79
|
+
process.stdin.removeAllListeners('keypress');
|
|
80
|
+
process.stdin.pause();
|
|
81
|
+
} catch { /* */ }
|
|
82
|
+
}
|
|
83
|
+
|
|
51
84
|
// Re-exec the CLI so the just-installed version takes over. Spawns a fresh, DETACHED
|
|
52
85
|
// process that inherits this terminal, then exits the current one. The caller MUST have
|
|
53
|
-
// already torn down the ink/alt-screen UI
|
|
54
|
-
// this — otherwise the child renders over a dirty screen.
|
|
86
|
+
// already torn down the ink/alt-screen UI before calling this.
|
|
55
87
|
//
|
|
56
88
|
// We re-run the CURRENT entry file with node (`process.argv[1]`): after `npm install -g`,
|
|
57
89
|
// the global bin symlink points at the new version, so this loads the updated code. If
|
|
58
90
|
// that entry path is somehow missing, we fall back to the `scripterx` bin on PATH.
|
|
59
91
|
//
|
|
92
|
+
// ORDERING is what makes the restart clean: we resetTerminal() FIRST (cooked TTY, mouse off,
|
|
93
|
+
// stdin drained), THEN spawn the child, THEN exit immediately on the next tick. The child
|
|
94
|
+
// only sets up raw mode / mouse tracking after it boots — by which point the parent is gone,
|
|
95
|
+
// so the two never fight over the TTY. No more leaked keystrokes.
|
|
96
|
+
//
|
|
60
97
|
// NOTE: on success it exits the process and never returns. Returns false only if BOTH
|
|
61
98
|
// spawn attempts throw synchronously, so the caller can show "restart manually".
|
|
62
|
-
export function restartCli({ binName = 'scripterx'
|
|
99
|
+
export function restartCli({ binName = 'scripterx' } = {}) {
|
|
63
100
|
// Re-run with the SAME args the user launched with (drop node + the script path), so
|
|
64
101
|
// `scripterx` → interactive shell, `scripterx zepto` → zepto, etc.
|
|
65
102
|
const argv = process.argv.slice(2);
|
|
66
103
|
const entry = process.argv[1];
|
|
67
104
|
|
|
105
|
+
// Clean the terminal BEFORE spawning so the child never inherits raw mode / mouse mode.
|
|
106
|
+
resetTerminal();
|
|
107
|
+
|
|
68
108
|
const trySpawn = (cmd, args) => {
|
|
69
109
|
const child = spawn(cmd, args, { stdio: 'inherit', detached: true, shell: false });
|
|
70
110
|
child.unref();
|
|
71
111
|
return child;
|
|
72
112
|
};
|
|
73
113
|
|
|
114
|
+
const exitSoon = () => {
|
|
115
|
+
// Exit on the next tick (not after a long timer) so we don't linger on the TTY. The
|
|
116
|
+
// detached child has already inherited the (now clean) fds and takes over.
|
|
117
|
+
setImmediate(() => process.exit(0));
|
|
118
|
+
};
|
|
119
|
+
|
|
74
120
|
try {
|
|
75
121
|
const primary = trySpawn(process.execPath, [entry, ...argv]);
|
|
76
|
-
// If the entry re-exec fails to even start, fall back to the named bin on PATH.
|
|
77
122
|
primary.on('error', () => {
|
|
78
123
|
try { trySpawn(binName, argv); } catch { /* nothing more we can do */ }
|
|
79
124
|
});
|
|
80
|
-
|
|
81
|
-
// detached child takes over the same TTY running the new version.
|
|
82
|
-
setTimeout(() => process.exit(0), delayMs);
|
|
125
|
+
exitSoon();
|
|
83
126
|
return true;
|
|
84
127
|
} catch {
|
|
85
|
-
// process.execPath spawn threw synchronously — try the named bin directly.
|
|
86
128
|
try {
|
|
87
129
|
trySpawn(binName, argv);
|
|
88
|
-
|
|
130
|
+
exitSoon();
|
|
89
131
|
return true;
|
|
90
132
|
} catch {
|
|
91
133
|
return false;
|
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
|
-
|
|
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)
|
|
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;
|