scripter-x 1.0.15 → 1.0.16
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 +192 -68
- package/src/index.js +4 -2
- package/src/providers/otpcart.js +7 -3
- package/src/worker.js +182 -0
package/package.json
CHANGED
package/src/commands.js
CHANGED
|
@@ -224,80 +224,204 @@ export async function run(io, api, args = {}) {
|
|
|
224
224
|
// code → we verify, build the ZAUTH1 envelope, and write {phone}-{timestamp}.txt.
|
|
225
225
|
// LOOPS: after each one it asks for the next number — keep going until the user
|
|
226
226
|
// presses Esc (or double Ctrl+C). Runs entirely on your IP.
|
|
227
|
-
export async function zeptoCmd(io,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
dest
|
|
227
|
+
export async function zeptoCmd(io, api, args = {}) {
|
|
228
|
+
const mode = args.manual ? 'manual' : (args.auto ? 'auto' : (await io.select('Choose Zepto login flow', [
|
|
229
|
+
{ label: 'Auto', value: 'auto', description: 'Headless extraction via OTPCart (Recommended)' },
|
|
230
|
+
{ label: 'Manual', value: 'manual', description: 'Enter phone + type OTP manually' }
|
|
231
|
+
])).value || 'auto');
|
|
232
|
+
|
|
233
|
+
if (mode === 'manual') {
|
|
234
|
+
io.print(' ◉ Zepto — local OTP → ZAUTH1 envelope (Manual)');
|
|
235
|
+
io.print(' ◉ press esc to cancel', 'accent');
|
|
236
|
+
|
|
237
|
+
let done = 0;
|
|
238
|
+
// double-Ctrl+C to exit (only meaningful in one-shot mode; the ink App owns its own Ctrl+C)
|
|
239
|
+
let lastSig = 0, stopping = false;
|
|
240
|
+
const onSigint = () => {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
if (now - lastSig < 2000) { stopping = true; process.exit(0); }
|
|
243
|
+
lastSig = now;
|
|
244
|
+
io.print(' ⚠ press Ctrl+C again to exit');
|
|
245
|
+
};
|
|
246
|
+
const interactive = !!io.startRun; // ink App present → don't grab SIGINT
|
|
247
|
+
if (!interactive) process.on('SIGINT', onSigint);
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
for (;;) {
|
|
251
|
+
// number entry — escapable: Esc stops the whole loop
|
|
252
|
+
const raw = args.number || await io.ask('zepto phone number', { escapable: true });
|
|
253
|
+
if (raw === CANCEL) break;
|
|
254
|
+
const number = String(raw).replace(/\D/g, '').slice(-10);
|
|
255
|
+
if (number.length !== 10) { io.print(' ! enter a 10-digit number', 'danger'); if (args.number) break; continue; }
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const session = zepto.newSession(number);
|
|
259
|
+
io.print(` ◉ sending OTP to ${number} …`);
|
|
260
|
+
const s = await zepto.sendOtp(session);
|
|
261
|
+
if (!s.ok) { io.print(` ✗ could not send OTP: ${s.msg || s.status}`, 'danger'); if (args.number) break; continue; }
|
|
262
|
+
io.print(' ✓ OTP sent');
|
|
263
|
+
|
|
264
|
+
// OTP entry — escapable too (Esc abandons this number and loops back)
|
|
265
|
+
const otpRaw = args.otp || await io.ask('enter the OTP', { escapable: true });
|
|
266
|
+
if (otpRaw === CANCEL) break;
|
|
267
|
+
const v = await zepto.verifyOtp(session, String(otpRaw).trim());
|
|
268
|
+
if (!v.ok) { io.print(` ✗ OTP verify failed: ${v.error || v.status}`, 'danger'); if (args.number) break; continue; }
|
|
269
|
+
io.print(` ✓ logged in${v.user?.fullName ? ` as ${v.user.fullName}` : ''}`);
|
|
270
|
+
|
|
271
|
+
// Build the ZAUTH1 envelope — the format the ZeptoAuthManager tool imports.
|
|
272
|
+
const envelope = zepto.encodeEnvelope(zepto.toSessionKeys(session), args.cert);
|
|
273
|
+
|
|
274
|
+
// {phone}-{timestamp}.txt in the chosen dir (default ~/Downloads/scripterx/zepto)
|
|
275
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
276
|
+
const fname = `${number}-${stamp}.txt`;
|
|
277
|
+
let dest;
|
|
278
|
+
if (args.out) {
|
|
279
|
+
const expanded = args.out.startsWith('~') ? join(homedir(), args.out.slice(1)) : args.out;
|
|
280
|
+
dest = /\.(txt|json)$/i.test(expanded) ? expanded : join(expanded, fname);
|
|
281
|
+
} else {
|
|
282
|
+
const downloads = join(homedir(), 'Downloads');
|
|
283
|
+
dest = join(existsSync(downloads) ? downloads : homedir(), 'scripterx', 'zepto', fname);
|
|
284
|
+
}
|
|
285
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
286
|
+
writeFileSync(dest, envelope + '\n');
|
|
287
|
+
done++;
|
|
288
|
+
io.print(' ✓ envelope saved to:');
|
|
289
|
+
io.print(` ${dest}`, 'accent');
|
|
290
|
+
io.print(' ◉ paste into ZeptoAuthManager → Import:');
|
|
291
|
+
io.print(` ${envelope}`, 'accent');
|
|
292
|
+
} catch (e) {
|
|
293
|
+
// never let one bad number kill the loop — surface the real reason and keep going
|
|
294
|
+
io.print(` ✗ ${number}: ${e.message}`, 'danger');
|
|
295
|
+
if (args.number) break;
|
|
296
|
+
continue;
|
|
278
297
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
done++;
|
|
282
|
-
io.print(' ✓ envelope saved to:');
|
|
283
|
-
io.print(` ${dest}`, 'accent');
|
|
284
|
-
io.print(' ◉ paste into ZeptoAuthManager → Import:');
|
|
285
|
-
io.print(` ${envelope}`, 'accent');
|
|
286
|
-
} catch (e) {
|
|
287
|
-
// never let one bad number kill the loop — surface the real reason and keep going
|
|
288
|
-
io.print(` ✗ ${number}: ${e.message}`, 'danger');
|
|
298
|
+
|
|
299
|
+
// one-shot (a number was passed on the CLI): do exactly one and stop
|
|
289
300
|
if (args.number) break;
|
|
290
|
-
|
|
301
|
+
io.print(' ◉ next number (esc to finish)', 'accent');
|
|
291
302
|
}
|
|
303
|
+
} finally {
|
|
304
|
+
if (!interactive && !stopping) process.off('SIGINT', onSigint);
|
|
305
|
+
}
|
|
306
|
+
io.print(` ✓ done — ${done} envelope(s) saved.`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Auto Mode ─────────────────────────────────────────────────────────────
|
|
311
|
+
io.print(' ◉ Zepto — local OTP → ZAUTH1 envelope (Auto)');
|
|
312
|
+
const cfg = config.load();
|
|
313
|
+
let email = cfg.otpcart_email, password = cfg.otpcart_password;
|
|
314
|
+
if (email && password) {
|
|
315
|
+
const choice = await io.select(`saved OTPCart: ${email}`, [
|
|
316
|
+
{ label: 'use saved', value: 'use', description: email },
|
|
317
|
+
{ label: 'add new', value: 'new', description: 'enter different creds' }]);
|
|
318
|
+
if ((choice.value || choice) === 'new') { email = null; password = null; }
|
|
319
|
+
}
|
|
320
|
+
let freshlyEntered = false;
|
|
321
|
+
if (!email || !password) {
|
|
322
|
+
email = await io.ask('OTPCart email');
|
|
323
|
+
password = await io.ask('OTPCart password', { mask: true });
|
|
324
|
+
freshlyEntered = true;
|
|
325
|
+
}
|
|
326
|
+
const jwt = await oc.login(email, password);
|
|
327
|
+
io.print(' ✓ OTPCart connected');
|
|
328
|
+
if (freshlyEntered && await io.confirm('save these creds locally for next time?', true)) {
|
|
329
|
+
config.setMany({ otpcart_email: email, otpcart_password: password });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const count = args.count || Number(await io.ask('how many sessions?', { dflt: '1' }));
|
|
333
|
+
const concurrency = args.concurrency || Number(await io.ask('concurrency (keep low — 1 recommended)', { dflt: '1' }));
|
|
334
|
+
const cert = args.cert || zepto.DEFAULT_CERT_SHA;
|
|
335
|
+
const name = args.name || `Zepto-${Math.floor(Date.now() / 1000)}`;
|
|
336
|
+
|
|
337
|
+
if (!(await io.confirm(`start ${count} Zepto auto extractions at concurrency ${concurrency}?`, true))) return;
|
|
292
338
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
339
|
+
const ZEPTO_SERVICE_ID = '68b2cf55980e8cf480b28c96';
|
|
340
|
+
const provider = new oc.OTPCartProvider(jwt, false, ZEPTO_SERVICE_ID);
|
|
341
|
+
|
|
342
|
+
const { ZeptoWorker } = await import('./worker.js');
|
|
343
|
+
const { RunController } = await import('./controller.js');
|
|
344
|
+
const controller = new RunController({ name, provider: 'otpcart-zepto', requested: count });
|
|
345
|
+
const worker = new ZeptoWorker(provider, {
|
|
346
|
+
requested: count,
|
|
347
|
+
concurrency,
|
|
348
|
+
certSha: cert,
|
|
349
|
+
onEvent: controller.handleEvent,
|
|
350
|
+
});
|
|
351
|
+
controller.stop = () => worker.stop();
|
|
352
|
+
|
|
353
|
+
const results = [];
|
|
354
|
+
const origRecord = worker._record.bind(worker);
|
|
355
|
+
worker._record = (res) => {
|
|
356
|
+
results.push(res);
|
|
357
|
+
origRecord(res);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const interactive = !!io.startRun;
|
|
361
|
+
if (interactive) io.startRun(controller);
|
|
362
|
+
let onSigint = null;
|
|
363
|
+
if (!interactive) {
|
|
364
|
+
let lastSig = 0;
|
|
365
|
+
onSigint = () => {
|
|
366
|
+
const now = Date.now();
|
|
367
|
+
if (now - lastSig < 2000) { worker.stop(); process.exit(0); }
|
|
368
|
+
lastSig = now;
|
|
369
|
+
worker.stop();
|
|
370
|
+
io.print(' ⚠ stopping campaign — press Ctrl+C again to exit');
|
|
371
|
+
};
|
|
372
|
+
process.on('SIGINT', onSigint);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
await worker.run();
|
|
376
|
+
if (onSigint) process.off('SIGINT', onSigint);
|
|
377
|
+
if (io.endRun) io.endRun();
|
|
378
|
+
|
|
379
|
+
io.print(` ✓ done — ${worker.stats.succeeded} succeeded · ${worker.stats.failed} failed · ${worker.stats.cancelled} cancelled · ₹${worker.stats.charges} spent`);
|
|
380
|
+
|
|
381
|
+
if (worker.stats.succeeded > 0) {
|
|
382
|
+
const saved = await saveZeptoSessions(results, name, args.out);
|
|
383
|
+
if (saved) {
|
|
384
|
+
io.print(` ✓ saved ${saved.count} session(s) to:`);
|
|
385
|
+
io.print(` ${saved.path}`, 'accent');
|
|
386
|
+
io.print(' ◉ ZAUTH1 envelopes:');
|
|
387
|
+
for (const r of results) {
|
|
388
|
+
if (r.status === 'success') {
|
|
389
|
+
io.print(` +91${r.mobile} → ${r.envelope}`, 'accent');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
296
392
|
}
|
|
297
|
-
}
|
|
298
|
-
|
|
393
|
+
} else {
|
|
394
|
+
io.print(' ◉ no sessions extracted — nothing to save.');
|
|
299
395
|
}
|
|
300
|
-
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function saveZeptoSessions(results, name, out) {
|
|
399
|
+
const { statSync } = await import('node:fs');
|
|
400
|
+
const ok = results.filter((r) => r.status === 'success');
|
|
401
|
+
if (!ok.length) return null;
|
|
402
|
+
|
|
403
|
+
const safeName = name.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'Zepto';
|
|
404
|
+
const fname = `Zepto-${safeName}-${Math.floor(Date.now() / 1000)}.json`;
|
|
405
|
+
let dest;
|
|
406
|
+
if (out) {
|
|
407
|
+
const expanded = out.startsWith('~') ? join(homedir(), out.slice(1)) : out;
|
|
408
|
+
const isDir = (existsSync(expanded) && statSync(expanded).isDirectory()) || /[/\\]$/.test(out);
|
|
409
|
+
dest = isDir ? join(expanded, fname) : expanded;
|
|
410
|
+
} else {
|
|
411
|
+
const downloads = join(homedir(), 'Downloads');
|
|
412
|
+
const base = existsSync(downloads) ? downloads : homedir();
|
|
413
|
+
dest = join(base, 'scripterx', fname);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const payload = ok.map((r) => ({
|
|
417
|
+
mobileNo: r.mobile,
|
|
418
|
+
zauth1_envelope: r.envelope,
|
|
419
|
+
session: r.session,
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
423
|
+
writeFileSync(dest, JSON.stringify(payload, null, 2));
|
|
424
|
+
return { path: dest, count: payload.length };
|
|
301
425
|
}
|
|
302
426
|
|
|
303
427
|
export async function campaigns(io, api) {
|
package/src/index.js
CHANGED
|
@@ -50,8 +50,9 @@ const HELP = `${paint.bold('scripterx')} — local Flipkart session extractor
|
|
|
50
50
|
scripterx interactive shell (recommended)
|
|
51
51
|
scripterx run [flags] run an extraction directly
|
|
52
52
|
--provider otpcart|tempotp --count N --concurrency N --name <n> --check-minutes
|
|
53
|
-
scripterx zepto [flags] local Zepto OTP login →
|
|
54
|
-
--
|
|
53
|
+
scripterx zepto [flags] local Zepto OTP login → ZAUTH1 envelope (JSON/txt)
|
|
54
|
+
--auto | --manual --count N --concurrency N --out <file|dir> --cert <64hex>
|
|
55
|
+
--number <10-digit> --otp <code>
|
|
55
56
|
(envelope keyed to ZeptoAuthManager cert; override with --cert or $ZAUTH_CERT_SHA)
|
|
56
57
|
scripterx campaigns | export [name] | balance | creds | stop | delete | whoami | config
|
|
57
58
|
scripterx --version`;
|
|
@@ -115,6 +116,7 @@ async function main() {
|
|
|
115
116
|
mode: flags.email ? 'json_email' : flags.mode, // --email or --mode json_email
|
|
116
117
|
target: flags.target, // run: 'flipkart' | 'zepto'
|
|
117
118
|
number: flags.number, otp: flags.otp, cert: flags.cert, // zepto: local OTP login → ZAUTH1 envelope
|
|
119
|
+
auto: flags.auto, manual: flags.manual,
|
|
118
120
|
campaign: flags._[0], out: flags.out, action: flags._[0], value: flags._[1],
|
|
119
121
|
};
|
|
120
122
|
try { await REGISTRY[fnName](cliIo, null, args); }
|
package/src/providers/otpcart.js
CHANGED
|
@@ -22,7 +22,11 @@ export async function balance() { return null; }
|
|
|
22
22
|
|
|
23
23
|
export class OTPCartProvider {
|
|
24
24
|
name = 'otpcart';
|
|
25
|
-
constructor(jwt, deepCheck = false
|
|
25
|
+
constructor(jwt, deepCheck = false, serviceId = SERVICE_ID) {
|
|
26
|
+
this.jwt = jwt;
|
|
27
|
+
this.deepCheck = deepCheck;
|
|
28
|
+
this.serviceId = serviceId;
|
|
29
|
+
}
|
|
26
30
|
|
|
27
31
|
_hdrs() {
|
|
28
32
|
return { 'Content-Type': 'application/json', Authorization: `Bearer ${this.jwt}`,
|
|
@@ -34,7 +38,7 @@ export class OTPCartProvider {
|
|
|
34
38
|
try {
|
|
35
39
|
const res = await fetch(`${BASE}/mobile/generate`, {
|
|
36
40
|
method: 'POST', headers: this._hdrs(),
|
|
37
|
-
body: JSON.stringify({ serviceId:
|
|
41
|
+
body: JSON.stringify({ serviceId: this.serviceId, isDeepCheck: this.deepCheck }),
|
|
38
42
|
signal: AbortSignal.timeout(15000),
|
|
39
43
|
});
|
|
40
44
|
d = await res.json();
|
|
@@ -52,7 +56,7 @@ export class OTPCartProvider {
|
|
|
52
56
|
try {
|
|
53
57
|
await fetch(`${BASE}/otp/cancelOtp`, {
|
|
54
58
|
method: 'POST', headers: this._hdrs(),
|
|
55
|
-
body: JSON.stringify({ serialNumber: number.txn, mobileId: number.extra, serviceId:
|
|
59
|
+
body: JSON.stringify({ serialNumber: number.txn, mobileId: number.extra, serviceId: this.serviceId }),
|
|
56
60
|
signal: AbortSignal.timeout(12000),
|
|
57
61
|
});
|
|
58
62
|
} catch { /* */ }
|
package/src/worker.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { FlipkartLogin, WrongOTP, BlockedError, TransientError } from './flipkart.js';
|
|
6
6
|
import { CampaignLogger } from './logger.js';
|
|
7
7
|
import * as kuku from './providers/kuku.js';
|
|
8
|
+
import * as zepto from './providers/zepto.js';
|
|
8
9
|
|
|
9
10
|
const OTP_RESEND_AFTER = 60_000;
|
|
10
11
|
const OTPCART_DEADLINE = 120_000;
|
|
@@ -454,3 +455,184 @@ export class Worker {
|
|
|
454
455
|
return p;
|
|
455
456
|
}
|
|
456
457
|
}
|
|
458
|
+
|
|
459
|
+
export class ZeptoWorker {
|
|
460
|
+
constructor(provider, { requested, concurrency, certSha, onEvent }) {
|
|
461
|
+
this.provider = provider;
|
|
462
|
+
this.requested = requested;
|
|
463
|
+
this.concurrency = Math.max(1, Math.min(concurrency, requested));
|
|
464
|
+
this.certSha = certSha;
|
|
465
|
+
this.onEvent = onEvent || (() => {});
|
|
466
|
+
this.stats = { total: requested, succeeded: 0, failed: 0, cancelled: 0, generated: 0, attempts: 0, charges: 0 };
|
|
467
|
+
this.slots = {};
|
|
468
|
+
this.seq = 0;
|
|
469
|
+
this.stopped = false;
|
|
470
|
+
this._releases = [];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
stop() { this.stopped = true; }
|
|
474
|
+
_nextSeq() { return ++this.seq; }
|
|
475
|
+
|
|
476
|
+
_claim() {
|
|
477
|
+
if (this.stopped) return 0;
|
|
478
|
+
if (this.stats.succeeded >= this.requested) return 0;
|
|
479
|
+
const active = this.stats.attempts - (this.stats.succeeded + this.stats.failed + this.stats.cancelled);
|
|
480
|
+
if (this.stats.succeeded + active >= this.requested) return 0;
|
|
481
|
+
if (this.stats.attempts >= this.requested * ATTEMPT_CAP_MULT) return 0;
|
|
482
|
+
return ++this.stats.attempts;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
_emit(slot, kv) {
|
|
486
|
+
this.slots[slot] = { slot, ...(this.slots[slot] || {}), ...kv };
|
|
487
|
+
this.onEvent('slot', this.slots[slot]);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
_record(res) {
|
|
491
|
+
if (res.mobile) this.stats.generated++;
|
|
492
|
+
if (res.status === 'success') { this.stats.succeeded++; this.stats.charges += res.cost || 0; }
|
|
493
|
+
else if (res.status === 'cancelled') this.stats.cancelled++;
|
|
494
|
+
else { this.stats.failed++; this.stats.charges += res.cost || 0; }
|
|
495
|
+
this.onEvent('progress', this.stats);
|
|
496
|
+
this.onEvent('row', res);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async run() {
|
|
500
|
+
const loops = Array.from({ length: this.concurrency }, (_, i) => this._loop(i + 1));
|
|
501
|
+
await Promise.all(loops);
|
|
502
|
+
if (this._releases.length) {
|
|
503
|
+
await Promise.allSettled(this._releases);
|
|
504
|
+
}
|
|
505
|
+
this.onEvent('done', this.stats);
|
|
506
|
+
return this.stats;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
_release(number) {
|
|
510
|
+
const p = (async () => {
|
|
511
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
512
|
+
try {
|
|
513
|
+
await this.provider.cancel(number);
|
|
514
|
+
return;
|
|
515
|
+
} catch {
|
|
516
|
+
await sleep(3000);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
})();
|
|
520
|
+
this._releases.push(p);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async _rent(slot) {
|
|
524
|
+
let attempt = 0;
|
|
525
|
+
while (!this.stopped) {
|
|
526
|
+
attempt++;
|
|
527
|
+
this._emit(slot, { phase: 'renting', detail: `requesting a number (try ${attempt})` });
|
|
528
|
+
const { number } = await this.provider.rentOnce();
|
|
529
|
+
if (number) return number;
|
|
530
|
+
if (attempt % RENT_COOLDOWN_EVERY === 0) {
|
|
531
|
+
for (let rem = RENT_COOLDOWN_SECS; rem > 0; rem--) {
|
|
532
|
+
if (this.stopped) return null;
|
|
533
|
+
this._emit(slot, { phase: 'rent_wait', detail: 'no numbers — waiting', wait: rem });
|
|
534
|
+
await sleep(1000);
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
await sleep(RENT_RETRY_DELAY);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async _loop(slot) {
|
|
544
|
+
for (;;) {
|
|
545
|
+
if (this._claim() === 0) return;
|
|
546
|
+
const res = await this._process(slot);
|
|
547
|
+
await this._record(res);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async _process(slot) {
|
|
552
|
+
const idNo = this._nextSeq();
|
|
553
|
+
const res = { id_no: idNo, status: 'failed', cost: 0, detail: '' };
|
|
554
|
+
|
|
555
|
+
if (this.stats.succeeded >= this.requested) {
|
|
556
|
+
this._emit(slot, { phase: 'done', detail: 'goal reached' });
|
|
557
|
+
res.status = 'cancelled';
|
|
558
|
+
res.detail = 'goal reached';
|
|
559
|
+
return res;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
this._emit(slot, { mobile: '', phase: 'renting', detail: 'requesting a number', wait: 0 });
|
|
563
|
+
const number = await this._rent(slot);
|
|
564
|
+
if (!number) { res.status = 'cancelled'; res.detail = 'stopped while renting'; return res; }
|
|
565
|
+
if (this.stats.succeeded >= this.requested) {
|
|
566
|
+
this._release(number);
|
|
567
|
+
this._emit(slot, { phase: 'done', detail: 'goal reached' });
|
|
568
|
+
res.status = 'cancelled';
|
|
569
|
+
res.detail = 'goal reached';
|
|
570
|
+
return res;
|
|
571
|
+
}
|
|
572
|
+
const rentedAt = Date.now();
|
|
573
|
+
res.mobile = number.mobile;
|
|
574
|
+
this._emit(slot, { mobile: number.mobile, phase: 'rented', detail: 'got a number' });
|
|
575
|
+
|
|
576
|
+
let stream = null;
|
|
577
|
+
let released = false;
|
|
578
|
+
|
|
579
|
+
const releaseOnce = (charged = false, doneDetail = 'done') => {
|
|
580
|
+
if (released) return;
|
|
581
|
+
released = true;
|
|
582
|
+
if (charged) {
|
|
583
|
+
this._emit(slot, { phase: 'done', detail: doneDetail });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
this._emit(slot, { phase: 'cancelling', detail: 'releasing number' });
|
|
587
|
+
this._release(number);
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const fail = (detail) => {
|
|
591
|
+
const charged = smsArrived.v;
|
|
592
|
+
res.status = charged ? 'failed' : 'cancelled';
|
|
593
|
+
res.cost = charged ? number.cost : 0;
|
|
594
|
+
res.detail = detail;
|
|
595
|
+
releaseOnce(charged, 'failed');
|
|
596
|
+
return res;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const smsArrived = { v: false };
|
|
600
|
+
const session = zepto.newSession(number.mobile);
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
this._emit(slot, { phase: 'ws', detail: 'opening OTP channel' });
|
|
604
|
+
stream = this.provider.startOtp(number);
|
|
605
|
+
if (typeof stream.ready === 'function') await stream.ready();
|
|
606
|
+
|
|
607
|
+
this._emit(slot, { phase: 'send_otp', detail: 'sending OTP' });
|
|
608
|
+
const sent = await zepto.sendOtp(session);
|
|
609
|
+
if (!sent.ok) return fail(`send: ${sent.msg}`);
|
|
610
|
+
|
|
611
|
+
const end = Date.now() + 120000;
|
|
612
|
+
let otp = null;
|
|
613
|
+
while (Date.now() < end && !this.stopped) {
|
|
614
|
+
const { otp: o, arrived } = await stream.poll();
|
|
615
|
+
if (arrived) smsArrived.v = true;
|
|
616
|
+
if (o) { otp = o; break; }
|
|
617
|
+
this._emit(slot, { phase: 'await_otp', detail: 'waiting for OTP', wait: Math.floor((end - Date.now()) / 1000) });
|
|
618
|
+
await sleep(1000);
|
|
619
|
+
}
|
|
620
|
+
if (!otp) return fail('no OTP within 120s — abandoned');
|
|
621
|
+
|
|
622
|
+
this._emit(slot, { phase: 'verify', detail: 'verifying OTP' });
|
|
623
|
+
const v = await zepto.verifyOtp(session, otp);
|
|
624
|
+
if (!v.ok) return fail(`verify: ${v.error}`);
|
|
625
|
+
|
|
626
|
+
const envelope = zepto.encodeEnvelope(zepto.toSessionKeys(session), this.certSha);
|
|
627
|
+
this._emit(slot, { phase: 'saving', detail: 'encoding envelope' });
|
|
628
|
+
res.status = 'success'; res.cost = number.cost; res.session = zepto.toSessionKeys(session); res.envelope = envelope;
|
|
629
|
+
releaseOnce(true, 'extracted');
|
|
630
|
+
return res;
|
|
631
|
+
} catch (e) {
|
|
632
|
+
return fail(`unexpected: ${e.message}`);
|
|
633
|
+
} finally {
|
|
634
|
+
releaseOnce(true);
|
|
635
|
+
if (stream) try { stream.close(); } catch { /* */ }
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|