grok-builder-cli 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/.env.example +11 -0
- package/README.md +69 -0
- package/package.json +42 -0
- package/src/index.js +214 -0
- package/src/mail.js +99 -0
- package/src/router9.js +118 -0
- package/src/signup.js +165 -0
- package/turnstilePatch/manifest.json +18 -0
- package/turnstilePatch/script.js +12 -0
package/.env.example
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# ── Grok Builder CLI ──────────────────────────────────
|
|
2
|
+
# Password for Grok accounts (min 16 chars, mix cases+digits+symbols)
|
|
3
|
+
PASSWORD=YourSecurePassword123!@
|
|
4
|
+
|
|
5
|
+
# ── Temp Mail (CloakMail / glx.web.id) ───────────────
|
|
6
|
+
MAIL_URL=https://glx.web.id
|
|
7
|
+
MAIL_DOMAIN=glx.web.id
|
|
8
|
+
|
|
9
|
+
# ── 9router Gateway ───────────────────────────────────
|
|
10
|
+
ROUTER9_URL=http://localhost:20128
|
|
11
|
+
ROUTER9_PASS=123456
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# 🚀 Grok Builder CLI
|
|
2
|
+
|
|
3
|
+
**Auto-register Grok (x.ai) accounts — multi-threaded, with 9router integration**
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @gelglx/grok-builder-cli
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- ✨ **Auto-register** Grok (x.ai) accounts via Playwright
|
|
12
|
+
- 📧 **Temp mail** via CloakMail (glx.web.id) — no external API needed
|
|
13
|
+
- 🛡️ **Turnstile bypass** — Chrome extension patches Cloudflare detection
|
|
14
|
+
- 🔗 **9router OAuth** — auto-add accounts as Grok CLI providers
|
|
15
|
+
- ⚡ **Multi-threaded** — parallel workers for batch registration
|
|
16
|
+
- 🎨 **Beautiful CLI** — progress bars, spinners, live stats
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Set password (min 16 chars)
|
|
22
|
+
export PASSWORD='MySecurePass123!@'
|
|
23
|
+
|
|
24
|
+
# Run interactive CLI
|
|
25
|
+
npx @gelglx/grok-builder-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install globally:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g @gelglx/grok-builder-cli
|
|
32
|
+
grok-builder
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Environment Variables
|
|
36
|
+
|
|
37
|
+
| Variable | Default | Description |
|
|
38
|
+
|----------|---------|-------------|
|
|
39
|
+
| `PASSWORD` | *(required)* | Password for Grok accounts (min 16 chars) |
|
|
40
|
+
| `MAIL_URL` | `https://glx.web.id` | Temp mail base URL |
|
|
41
|
+
| `MAIL_DOMAIN` | `glx.web.id` | Temp mail domain |
|
|
42
|
+
| `ROUTER9_URL` | `http://localhost:20128` | 9router gateway URL |
|
|
43
|
+
| `ROUTER9_PASS` | `123456` | 9router dashboard password |
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Node.js 18+
|
|
48
|
+
- Xvfb (Linux) — for headless browser display
|
|
49
|
+
- Google Chrome or Chromium installed
|
|
50
|
+
|
|
51
|
+
## How It Works
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
┌──────────────────────────────────────────┐
|
|
55
|
+
│ 1. Create temp email (glx.web.id) │
|
|
56
|
+
│ 2. Open accounts.x.ai/sign-up │
|
|
57
|
+
│ 3. Enter email → wait for OTP │
|
|
58
|
+
│ 4. Extract code from mail inbox │
|
|
59
|
+
│ 5. Fill name + password │
|
|
60
|
+
│ 6. Turnstile auto-solve │
|
|
61
|
+
│ 7. Submit → redirect to grok.com │
|
|
62
|
+
│ 8. Save SSO cookies │
|
|
63
|
+
│ 9. (optional) OAuth device flow → 9R │
|
|
64
|
+
└──────────────────────────────────────────┘
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "grok-builder-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Auto-register Grok (x.ai) accounts with temp mail (CloakMail/glx.web.id) and auto-integrate into 9router — multi-threaded CLI",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"grok-builder": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/",
|
|
11
|
+
"turnstilePatch/",
|
|
12
|
+
".env.example",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/index.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"grok",
|
|
20
|
+
"xai",
|
|
21
|
+
"x.ai",
|
|
22
|
+
"signup",
|
|
23
|
+
"registration",
|
|
24
|
+
"9router",
|
|
25
|
+
"temp-mail",
|
|
26
|
+
"cloakmail",
|
|
27
|
+
"playwright"
|
|
28
|
+
],
|
|
29
|
+
"author": "gelglx",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"playwright": "^1.60.0",
|
|
33
|
+
"chalk": "^5.4.0",
|
|
34
|
+
"ora": "^8.1.0",
|
|
35
|
+
"cli-progress": "^3.12.0",
|
|
36
|
+
"uuid": "^11.0.0"
|
|
37
|
+
},
|
|
38
|
+
"type": "module",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// GrokBuilderCLI — Auto-register Grok (x.ai) accounts + 9router integration
|
|
4
|
+
// Multi-threaded, interactive CLI
|
|
5
|
+
|
|
6
|
+
import { chromium } from 'playwright';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import cliProgress from 'cli-progress';
|
|
10
|
+
import * as readline from 'node:readline/promises';
|
|
11
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
12
|
+
|
|
13
|
+
import { runOneSignup, getTurnstilePath } from './signup.js';
|
|
14
|
+
import { login as loginRouter9 } from './router9.js';
|
|
15
|
+
|
|
16
|
+
// ── ANSI helpers ──────────────────────────────────────────────
|
|
17
|
+
const { bold, dim, green, red, yellow, cyan, magenta } = chalk;
|
|
18
|
+
const BAR_W = 74;
|
|
19
|
+
|
|
20
|
+
function barLine(color = cyan) {
|
|
21
|
+
console.log(color('─'.repeat(BAR_W)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function header(text) {
|
|
25
|
+
console.log(`\n${cyan(` ─── [ ${text} ] `)}${cyan('─'.repeat(Math.max(0, BAR_W - text.length - 8)))}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function row(num, email, status, elapsed, extra = '') {
|
|
29
|
+
const e = (email || '—').padEnd(36).slice(0, 36);
|
|
30
|
+
const s = status.padEnd(10);
|
|
31
|
+
const t = elapsed ? dim(`${elapsed}s`) : '';
|
|
32
|
+
console.log(` ${dim(String(num).padStart(3))} ${cyan('│')} ${e} ${s} ${t} ${extra}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Interactive prompt ───────────────────────────────────────
|
|
36
|
+
const rl = readline.createInterface({ input, output });
|
|
37
|
+
|
|
38
|
+
async function ask(q, defaultVal) {
|
|
39
|
+
const answer = await rl.question(`${cyan('?')} ${bold(q)} ${defaultVal ? dim(`(${defaultVal}) `) : ''}`);
|
|
40
|
+
return answer.trim() || defaultVal;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function closePrompt() {
|
|
44
|
+
rl.close();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Main ──────────────────────────────────────────────────────
|
|
48
|
+
async function main() {
|
|
49
|
+
console.clear();
|
|
50
|
+
console.log(`\n${cyan('╔' + '═'.repeat(BAR_W - 2) + '╗')}`);
|
|
51
|
+
console.log(`${cyan('║')} ${bold('🚀 Grok Builder CLI')} ${dim('v1.0.0')}${' '.repeat(Math.max(0, BAR_W - 37))}${cyan('║')}`);
|
|
52
|
+
console.log(`${cyan('║')} ${dim('Auto-register Grok (x.ai) + 9router integration')}${' '.repeat(10)}${cyan('║')}`);
|
|
53
|
+
console.log(`${cyan('╚' + '═'.repeat(BAR_W - 2) + '╝')}\n`);
|
|
54
|
+
|
|
55
|
+
// ── Config checks ───────────────────────────────────────────
|
|
56
|
+
const checks = [
|
|
57
|
+
{ label: 'PASSWORD ', ok: !!process.env.PASSWORD, msg: 'env PASSWORD set' },
|
|
58
|
+
{ label: 'MAIL_URL ', ok: true, msg: process.env.MAIL_URL || 'https://glx.web.id (default)' },
|
|
59
|
+
{ label: 'ROUTER9_URL', ok: true, msg: process.env.ROUTER9_URL || 'http://localhost:20128 (default)' },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
console.log(` ${dim('Config:')}`);
|
|
63
|
+
for (const c of checks) {
|
|
64
|
+
const icon = c.ok ? green('✓') : red('✗');
|
|
65
|
+
console.log(` ${icon} ${dim(c.label)} ${c.msg}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!process.env.PASSWORD) {
|
|
69
|
+
console.log(`\n ${yellow('⚠')} ${dim('Set PASSWORD in environment or .env file')}`);
|
|
70
|
+
console.log(` ${dim(' Example:')} PASSWORD='MySecurePass123!@' grok-builder`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Interactive questions ───────────────────────────────────
|
|
74
|
+
console.log();
|
|
75
|
+
const countStr = await ask('How many accounts to create?', '5');
|
|
76
|
+
const count = parseInt(countStr) || 5;
|
|
77
|
+
|
|
78
|
+
const threadsStr = await ask('How many parallel threads?', '2');
|
|
79
|
+
const threads = parseInt(threadsStr) || 2;
|
|
80
|
+
|
|
81
|
+
const addRouter = (await ask('Auto-add to 9router?', 'y')).toLowerCase() === 'y';
|
|
82
|
+
|
|
83
|
+
let routerOk = false;
|
|
84
|
+
if (addRouter) {
|
|
85
|
+
try {
|
|
86
|
+
await loginRouter9();
|
|
87
|
+
routerOk = true;
|
|
88
|
+
console.log(` ${green('✓')} ${dim('9router login OK')}`);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.log(` ${red('✗')} ${dim(`9router login: ${err.message}`)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
closePrompt();
|
|
95
|
+
|
|
96
|
+
// ── Start ───────────────────────────────────────────────────
|
|
97
|
+
console.log();
|
|
98
|
+
header(`GROK BUILDER | ${count} accounts × ${threads} threads`);
|
|
99
|
+
barLine();
|
|
100
|
+
|
|
101
|
+
const tStart = Date.now();
|
|
102
|
+
const results = [];
|
|
103
|
+
let succeeded = 0, failed = 0;
|
|
104
|
+
let active = 0; // track concurrent
|
|
105
|
+
|
|
106
|
+
// Multi-progress bar
|
|
107
|
+
const multibar = new cliProgress.MultiBar({
|
|
108
|
+
clearOnComplete: false,
|
|
109
|
+
hideCursor: true,
|
|
110
|
+
format: ` ${cyan('│')} {bar} | {percentage}% | {value}/{total} accounts | {duration_formatted}`,
|
|
111
|
+
barCompleteChar: '■',
|
|
112
|
+
barIncompleteChar: '□',
|
|
113
|
+
barsize: 32,
|
|
114
|
+
}, cliProgress.Presets.shades_classic);
|
|
115
|
+
|
|
116
|
+
const progressBar = multibar.create(count, 0);
|
|
117
|
+
|
|
118
|
+
// ── Launch persistent browser with turnstile patch ──────────
|
|
119
|
+
const extPath = getTurnstilePath();
|
|
120
|
+
console.log(` ${dim('Turnstile patch:')} ${extPath}\n`);
|
|
121
|
+
|
|
122
|
+
// We'll use one shared browser for all contexts
|
|
123
|
+
const browser = await chromium.launch({
|
|
124
|
+
headless: false,
|
|
125
|
+
args: [
|
|
126
|
+
'--no-sandbox',
|
|
127
|
+
'--disable-dev-shm-usage',
|
|
128
|
+
'--disable-blink-features=AutomationControlled',
|
|
129
|
+
`--disable-extensions-except=${extPath}`,
|
|
130
|
+
`--load-extension=${extPath}`,
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── Worker pool ─────────────────────────────────────────────
|
|
135
|
+
async function worker(taskIndex) {
|
|
136
|
+
const context = await browser.newContext({
|
|
137
|
+
viewport: { width: 1280, height: 1024 },
|
|
138
|
+
ignoreHTTPSErrors: true,
|
|
139
|
+
});
|
|
140
|
+
try {
|
|
141
|
+
const account = await runOneSignup({
|
|
142
|
+
context,
|
|
143
|
+
addToRouter: addRouter && routerOk,
|
|
144
|
+
});
|
|
145
|
+
results.push(account);
|
|
146
|
+
succeeded++;
|
|
147
|
+
const elapsed = account.elapsed || ((Date.now() - tStart) / 1000).toFixed(1);
|
|
148
|
+
row(succeeded, account.email, green('SUCCESS'), elapsed);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
failed++;
|
|
151
|
+
row(count - failed + 1, '—failed—', red('FAILED'), '', dim(err.message));
|
|
152
|
+
} finally {
|
|
153
|
+
await context.close();
|
|
154
|
+
progressBar.increment();
|
|
155
|
+
active--;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Queue processor ─────────────────────────────────────────
|
|
160
|
+
let idx = 0;
|
|
161
|
+
async function processQueue() {
|
|
162
|
+
while (idx < count) {
|
|
163
|
+
if (active < threads) {
|
|
164
|
+
active++;
|
|
165
|
+
const taskIdx = idx++;
|
|
166
|
+
worker(taskIdx); // fire and forget — we track via active counter
|
|
167
|
+
}
|
|
168
|
+
await sleep(100); // small tick
|
|
169
|
+
}
|
|
170
|
+
// Wait for remaining workers
|
|
171
|
+
while (active > 0) {
|
|
172
|
+
await sleep(200);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await processQueue();
|
|
177
|
+
multibar.stop();
|
|
178
|
+
|
|
179
|
+
// ── Final report ────────────────────────────────────────────
|
|
180
|
+
console.log();
|
|
181
|
+
header('SYSTEM LOGS');
|
|
182
|
+
for (const r of results) {
|
|
183
|
+
const icon = r.router9 === 'added' ? green('→9R') : r.router9 ? red(`→${r.router9}`) : dim('—');
|
|
184
|
+
console.log(` ${green('DONE')} ${dim(`[${r.email}]`)} ${green('✓')} ${r.elapsed}s ${icon}`);
|
|
185
|
+
}
|
|
186
|
+
barLine();
|
|
187
|
+
|
|
188
|
+
const tElapsed = ((Date.now() - tStart) / 1000).toFixed(1);
|
|
189
|
+
const rate = succeeded / (tElapsed / 60);
|
|
190
|
+
const health = count > 0 ? (succeeded / count * 100).toFixed(1) : '0';
|
|
191
|
+
|
|
192
|
+
console.log(`\n ${dim('[Summary]')}`);
|
|
193
|
+
console.log(` ${green('✓')} ${succeeded} success ${red('✗')} ${failed} failed ${dim(`(${tElapsed}s, ${rate.toFixed(1)}/min)`)}`);
|
|
194
|
+
console.log(` Health: ${health}% | Speed: ${rate.toFixed(1)}/m`);
|
|
195
|
+
console.log();
|
|
196
|
+
|
|
197
|
+
// ── Save results ────────────────────────────────────────────
|
|
198
|
+
const fs = await import('fs');
|
|
199
|
+
const outPath = 'grok-accounts.json';
|
|
200
|
+
fs.writeFileSync(outPath, JSON.stringify(results, null, 2));
|
|
201
|
+
console.log(` ${dim('Saved:')} ${outPath} (${results.length} accounts)`);
|
|
202
|
+
|
|
203
|
+
await browser.close();
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function sleep(ms) {
|
|
208
|
+
return new Promise(r => setTimeout(r, ms));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
main().catch(err => {
|
|
212
|
+
console.error(`\n${red('FATAL:')} ${err.message}`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
});
|
package/src/mail.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// glx.web.id (CloakMail) temp email client
|
|
2
|
+
// API: SvelteKit __data.json + SSR page parsing
|
|
3
|
+
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
5
|
+
|
|
6
|
+
const MAIL_BASE = process.env.MAIL_URL || 'https://glx.web.id';
|
|
7
|
+
const MAIL_DOMAIN = process.env.MAIL_DOMAIN || 'glx.web.id';
|
|
8
|
+
|
|
9
|
+
function randomLocal() {
|
|
10
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz';
|
|
11
|
+
let s = '';
|
|
12
|
+
for (let i = 0; i < 8; i++) s += chars[randomBytes(1)[0] % 26];
|
|
13
|
+
const num = String(Date.now()).slice(-6);
|
|
14
|
+
return s + num;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function createTempEmail() {
|
|
18
|
+
const local = randomLocal();
|
|
19
|
+
const addr = `${local}@${MAIL_DOMAIN}`;
|
|
20
|
+
const url = `${MAIL_BASE}/confirm/${encodeURIComponent(addr)}/__data.json?x-sveltekit-invalidated=01`;
|
|
21
|
+
const res = await fetch(url);
|
|
22
|
+
if (!res.ok) throw new Error(`Mail confirm failed: ${res.status}`);
|
|
23
|
+
return addr;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fetch emails from glx.web.id inbox using __data.json endpoint
|
|
28
|
+
* Returns array of {id, to, from, subject, text, html, receivedAt}
|
|
29
|
+
*/
|
|
30
|
+
export async function fetchEmails(addr) {
|
|
31
|
+
const url = `${MAIL_BASE}/inbox/${encodeURIComponent(addr)}/__data.json?x-sveltekit-invalidated=01`;
|
|
32
|
+
const res = await fetch(url, {
|
|
33
|
+
headers: { Accept: 'application/json' }
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) throw new Error(`Inbox fetch failed: ${res.status}`);
|
|
36
|
+
const json = await res.json();
|
|
37
|
+
|
|
38
|
+
// Parse SvelteKit __data.json format
|
|
39
|
+
for (const node of json.nodes || []) {
|
|
40
|
+
if (node?.type === 'data') {
|
|
41
|
+
const data = node.data;
|
|
42
|
+
// data = [address_string, emails_array, pagination_object]
|
|
43
|
+
if (Array.isArray(data)) {
|
|
44
|
+
if (data.length >= 2 && Array.isArray(data[1])) return data[1];
|
|
45
|
+
if (data.length >= 3 && Array.isArray(data[2])) return data[2];
|
|
46
|
+
}
|
|
47
|
+
// alternative: data = {address, emails, pagination}
|
|
48
|
+
if (data?.emails && Array.isArray(data.emails)) return data.emails;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback: try parsing from page data at top level
|
|
53
|
+
if (json?.data?.emails) return json.data.emails;
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract OTP code from email subject/text/html
|
|
59
|
+
* Returns 6-char code (without hyphen)
|
|
60
|
+
*/
|
|
61
|
+
export function extractOTP(subject, text, html) {
|
|
62
|
+
const sources = [subject || '', text || '', html || ''];
|
|
63
|
+
for (const src of sources) {
|
|
64
|
+
// Pattern 1: "confirmation code: XXX-XXX"
|
|
65
|
+
let m = src.match(/confirmation code:\s*([A-Z0-9]{3}-[A-Z0-9]{3})/i);
|
|
66
|
+
if (m) return m[1].replace('-', '');
|
|
67
|
+
// Pattern 2: "code: XXX-XXX" or "code: XXXXXX"
|
|
68
|
+
m = src.match(/code:\s*([A-Z0-9]{3}-[A-Z0-9]{3})/i);
|
|
69
|
+
if (m) return m[1].replace('-', '');
|
|
70
|
+
m = src.match(/code[:\s]\s*([A-Z0-9]{6})/i);
|
|
71
|
+
if (m) return m[1];
|
|
72
|
+
// Pattern 3: inline HTML like >DQC-UR9<
|
|
73
|
+
m = src.match(/>([A-Z0-9]{3}-[A-Z0-9]{3})</);
|
|
74
|
+
if (m) return m[1].replace('-', '');
|
|
75
|
+
m = src.match(/>([A-Z0-9]{6})</);
|
|
76
|
+
if (m) return m[1];
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Poll mailbox for OTP code (poll every 3s, timeout ms)
|
|
83
|
+
*/
|
|
84
|
+
export async function waitForOTP(addr, timeout = 120000) {
|
|
85
|
+
const started = Date.now();
|
|
86
|
+
while (Date.now() - started < timeout) {
|
|
87
|
+
const emails = await fetchEmails(addr);
|
|
88
|
+
for (const email of emails) {
|
|
89
|
+
const code = extractOTP(email.subject, email.text, email.html);
|
|
90
|
+
if (code) return { code, email };
|
|
91
|
+
}
|
|
92
|
+
await sleep(3000);
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`OTP timeout after ${timeout / 1000}s`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sleep(ms) {
|
|
98
|
+
return new Promise(r => setTimeout(r, ms));
|
|
99
|
+
}
|
package/src/router9.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// 9router OAuth Device Flow integration
|
|
2
|
+
import { randomBytes } from 'crypto';
|
|
3
|
+
|
|
4
|
+
const ROUTER9_URL = process.env.ROUTER9_URL || 'http://localhost:20128';
|
|
5
|
+
const ROUTER9_PASS = process.env.ROUTER9_PASS || '123456';
|
|
6
|
+
|
|
7
|
+
let _authToken = null;
|
|
8
|
+
|
|
9
|
+
async function api(method, path, body) {
|
|
10
|
+
const headers = { 'Content-Type': 'application/json', Accept: 'application/json' };
|
|
11
|
+
const opts = { method, headers };
|
|
12
|
+
if (_authToken) headers.Cookie = `auth_token=${_authToken}`;
|
|
13
|
+
if (body) opts.body = JSON.stringify(body);
|
|
14
|
+
|
|
15
|
+
const res = await fetch(`${ROUTER9_URL}${path}`, opts);
|
|
16
|
+
|
|
17
|
+
// Handle Set-Cookie for auth
|
|
18
|
+
const setCookie = res.headers.get('set-cookie');
|
|
19
|
+
if (setCookie) {
|
|
20
|
+
const m = setCookie.match(/auth_token=([^;]+)/);
|
|
21
|
+
if (m) _authToken = m[1];
|
|
22
|
+
}
|
|
23
|
+
return res.json();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function login() {
|
|
27
|
+
const r = await api('POST', '/api/auth/login', { password: ROUTER9_PASS });
|
|
28
|
+
if (!r.success) throw new Error(`9router login failed: ${r.error || 'unknown'}`);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function requestDeviceCode() {
|
|
33
|
+
const r = await api('GET', '/api/oauth/grok-cli/device-code');
|
|
34
|
+
if (r.error) throw new Error(`Device code failed: ${r.error}`);
|
|
35
|
+
return r;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function pollDevice(deviceCode, codeVerifier) {
|
|
39
|
+
const r = await api('POST', '/api/oauth/grok-cli/poll', {
|
|
40
|
+
deviceCode,
|
|
41
|
+
codeVerifier
|
|
42
|
+
});
|
|
43
|
+
return r;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function listGrokProviders() {
|
|
47
|
+
const r = await api('GET', '/api/providers');
|
|
48
|
+
const conns = r.connections || [];
|
|
49
|
+
return conns.filter(c => c.provider === 'grok-cli');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Complete OAuth device flow via browser context (Playwright page)
|
|
54
|
+
* Returns {success, connection} after polling
|
|
55
|
+
*/
|
|
56
|
+
export async function doDeviceFlow(page, cookies, { deviceCode, codeVerifier, userCode, verificationUriComplete }) {
|
|
57
|
+
// Inject SSO cookies
|
|
58
|
+
if (cookies && cookies.length > 0) {
|
|
59
|
+
const pwCookies = cookies.map(c => ({
|
|
60
|
+
...c,
|
|
61
|
+
sameSite: ['Strict', 'Lax', 'None'].includes(c.sameSite) ? c.sameSite : 'Lax'
|
|
62
|
+
}));
|
|
63
|
+
try { await page.context().clearCookies(); } catch {}
|
|
64
|
+
await page.context().addCookies(pwCookies);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Open verification URL
|
|
68
|
+
await page.goto(verificationUriComplete, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
|
69
|
+
await sleep(3000);
|
|
70
|
+
|
|
71
|
+
// Check if SSO expired (login page shown)
|
|
72
|
+
const hasLoginInput = await page.evaluate(
|
|
73
|
+
() => !!document.querySelector('input[type=email], input[type=password]')
|
|
74
|
+
);
|
|
75
|
+
if (hasLoginInput) {
|
|
76
|
+
throw new Error('SSO expired — login required');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Click "Continue" button
|
|
80
|
+
try {
|
|
81
|
+
await page.getByRole('button', { name: /continue/i, exact: false }).click({ timeout: 5000 });
|
|
82
|
+
await sleep(2500);
|
|
83
|
+
} catch {
|
|
84
|
+
// May already be on consent page
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Click "Allow" / "Allow All" on consent page
|
|
88
|
+
try {
|
|
89
|
+
await page.getByRole('button', { name: /^Allow$/i, exact: true }).click({ timeout: 8000 });
|
|
90
|
+
} catch {
|
|
91
|
+
try {
|
|
92
|
+
await page.getByRole('button', { name: /Allow All/i, exact: true }).click({ timeout: 3000 });
|
|
93
|
+
} catch {
|
|
94
|
+
try {
|
|
95
|
+
await page.getByRole('button', { name: /allow/i, exact: false }).click({ timeout: 3000 });
|
|
96
|
+
} catch {
|
|
97
|
+
throw new Error('Allow button not found on consent page');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await sleep(3000);
|
|
103
|
+
|
|
104
|
+
// Poll until success (max 60 tries = 5 min)
|
|
105
|
+
for (let i = 0; i < 60; i++) {
|
|
106
|
+
const res = await pollDevice(deviceCode, codeVerifier);
|
|
107
|
+
if (res.success) return res;
|
|
108
|
+
if (res.pending !== true) {
|
|
109
|
+
throw new Error(`Poll error: ${res.error || 'unknown'}`);
|
|
110
|
+
}
|
|
111
|
+
await sleep(5000);
|
|
112
|
+
}
|
|
113
|
+
throw new Error('Poll timeout after 5 minutes');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sleep(ms) {
|
|
117
|
+
return new Promise(r => setTimeout(r, ms));
|
|
118
|
+
}
|
package/src/signup.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// x.ai signup flow — Playwright automation
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { createTempEmail, waitForOTP } from './mail.js';
|
|
5
|
+
import * as r9 from './router9.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const TS_EXT = path.resolve(__dirname, '..', 'turnstilePatch');
|
|
9
|
+
const SIGNUP_URL = 'https://accounts.x.ai/sign-up?redirect=grok-com';
|
|
10
|
+
|
|
11
|
+
const PASSWORD = process.env.PASSWORD || 'GrokBuilder2024!Secure';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run one signup flow + optional 9router addition
|
|
15
|
+
* @param {object} opts
|
|
16
|
+
* @param {import('playwright').BrowserContext} opts.context
|
|
17
|
+
* @param {boolean} opts.addToRouter
|
|
18
|
+
* @returns {Promise<object>} account data
|
|
19
|
+
*/
|
|
20
|
+
export async function runOneSignup({ context, addToRouter = false }) {
|
|
21
|
+
const page = await context.newPage();
|
|
22
|
+
const startTime = Date.now();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Set extra headers
|
|
26
|
+
await page.setExtraHTTPHeaders({
|
|
27
|
+
'sec-ch-ua': '"Chromium";v="128", "Google Chrome";v="128", "Not-A.Brand";v="99"',
|
|
28
|
+
'sec-ch-ua-mobile': '?0',
|
|
29
|
+
'sec-ch-ua-platform': '"Linux"',
|
|
30
|
+
'accept-language': 'en-US,en;q=0.9',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Step 1: Open x.ai signup
|
|
34
|
+
await page.goto(SIGNUP_URL, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
|
35
|
+
await sleep(3000);
|
|
36
|
+
try { await page.getByRole('button', { name: /Accept All Cookies/i }).click({ timeout: 3000 }); } catch {}
|
|
37
|
+
await sleep(500);
|
|
38
|
+
|
|
39
|
+
// Step 2: Click "Sign up with email"
|
|
40
|
+
await page.getByText('Sign up with email').click({ timeout: 8000 });
|
|
41
|
+
await page.waitForSelector('input[type=email]', { timeout: 8000 });
|
|
42
|
+
|
|
43
|
+
// Step 3: Create temp email
|
|
44
|
+
const addr = await createTempEmail();
|
|
45
|
+
await page.locator('input[type=email]').fill(addr);
|
|
46
|
+
await page.locator('input[type=email]').press('Enter');
|
|
47
|
+
|
|
48
|
+
// Wait for code input to appear
|
|
49
|
+
try {
|
|
50
|
+
await page.waitForSelector('input[name=code]', { timeout: 20000 });
|
|
51
|
+
} catch {
|
|
52
|
+
try {
|
|
53
|
+
await page.getByRole('button', { name: /Sign up/i }).click({ timeout: 3000 });
|
|
54
|
+
await page.waitForSelector('input[name=code]', { timeout: 15000 });
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error('Could not reach OTP input');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 4: Wait for OTP
|
|
61
|
+
const { code } = await waitForOTP(addr, 120000);
|
|
62
|
+
|
|
63
|
+
// Step 5: Submit OTP
|
|
64
|
+
await page.locator('input[name=code]').first().fill(code, { timeout: 15000 });
|
|
65
|
+
await sleep(500);
|
|
66
|
+
await page.keyboard.press('Enter');
|
|
67
|
+
await page.waitForSelector('input[name=givenName]', { timeout: 20000 });
|
|
68
|
+
|
|
69
|
+
// Step 6: Fill name & password
|
|
70
|
+
const local = addr.split('@')[0];
|
|
71
|
+
const parts = local.split(/[._\-]/);
|
|
72
|
+
const givenName = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
|
|
73
|
+
const familyName = (parts[1] || 'Xyz').charAt(0).toUpperCase() + (parts[1] || 'Xyz').slice(1);
|
|
74
|
+
|
|
75
|
+
await page.locator('input[name=givenName]').fill(givenName);
|
|
76
|
+
await page.locator('input[name=familyName]').fill(familyName);
|
|
77
|
+
await page.locator('input[name=password]').fill(PASSWORD);
|
|
78
|
+
|
|
79
|
+
// Step 7: Solve turnstile & submit
|
|
80
|
+
let tok = '';
|
|
81
|
+
for (let i = 0; i < 40; i++) {
|
|
82
|
+
tok = await page.evaluate(
|
|
83
|
+
() => document.querySelector('input[name=cf-turnstile-response]')?.value || ''
|
|
84
|
+
);
|
|
85
|
+
if (tok) break;
|
|
86
|
+
await sleep(1000);
|
|
87
|
+
}
|
|
88
|
+
if (!tok) throw new Error('Turnstile timeout 40s');
|
|
89
|
+
|
|
90
|
+
await page.getByRole('button', { name: /Complete sign up/i }).click();
|
|
91
|
+
await sleep(1000);
|
|
92
|
+
|
|
93
|
+
// Step 8: Wait for redirect to grok.com
|
|
94
|
+
let redirected = false;
|
|
95
|
+
for (let i = 0; i < 30; i++) {
|
|
96
|
+
await sleep(2000);
|
|
97
|
+
try {
|
|
98
|
+
const url = page.url();
|
|
99
|
+
if (url.includes('grok.com')) { redirected = true; break; }
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!redirected) {
|
|
104
|
+
// Check if we have SSO cookies anyway
|
|
105
|
+
const cookies = await page.context().cookies();
|
|
106
|
+
const sso = cookies.filter(c => c.name?.toLowerCase().includes('sso'));
|
|
107
|
+
if (sso.length === 0) throw new Error(`No redirect to grok.com (last URL: ${page.url()})`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Step 9: Save credentials
|
|
111
|
+
const allCookies = await page.context().cookies();
|
|
112
|
+
const accountData = {
|
|
113
|
+
email: addr,
|
|
114
|
+
password: PASSWORD,
|
|
115
|
+
code,
|
|
116
|
+
sso_cookies: allCookies,
|
|
117
|
+
final_url: page.url(),
|
|
118
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
119
|
+
elapsed: ((Date.now() - startTime) / 1000).toFixed(1),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Step 10: Add to 9router if requested
|
|
123
|
+
if (addToRouter) {
|
|
124
|
+
try {
|
|
125
|
+
await addToRouter9(page, accountData);
|
|
126
|
+
accountData.router9 = 'added';
|
|
127
|
+
} catch (err) {
|
|
128
|
+
accountData.router9 = `failed: ${err.message}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return accountData;
|
|
133
|
+
} finally {
|
|
134
|
+
await page.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function addToRouter9(page, account) {
|
|
139
|
+
// Check if already exists
|
|
140
|
+
const existing = await r9.listGrokProviders();
|
|
141
|
+
if (existing.some(c => c.email === account.email)) {
|
|
142
|
+
throw new Error('Already exists in 9router');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const dc = await r9.requestDeviceCode();
|
|
146
|
+
const result = await r9.doDeviceFlow(page, account.sso_cookies, {
|
|
147
|
+
deviceCode: dc.device_code,
|
|
148
|
+
codeVerifier: dc.codeVerifier,
|
|
149
|
+
userCode: dc.user_code,
|
|
150
|
+
verificationUriComplete: dc.verification_uri_complete
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get turnstile patch extension path
|
|
158
|
+
*/
|
|
159
|
+
export function getTurnstilePath() {
|
|
160
|
+
return TS_EXT;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function sleep(ms) {
|
|
164
|
+
return new Promise(r => setTimeout(r, ms));
|
|
165
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "Turnstile Patcher",
|
|
4
|
+
"version": "2.1",
|
|
5
|
+
"content_scripts": [
|
|
6
|
+
{
|
|
7
|
+
"js": [
|
|
8
|
+
"./script.js"
|
|
9
|
+
],
|
|
10
|
+
"matches": [
|
|
11
|
+
"<all_urls>"
|
|
12
|
+
],
|
|
13
|
+
"run_at": "document_start",
|
|
14
|
+
"all_frames": true,
|
|
15
|
+
"world": "MAIN"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function getRandomInt(min, max) {
|
|
2
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
// old method wouldn't work on 4k screens
|
|
6
|
+
|
|
7
|
+
let screenX = getRandomInt(800, 1200);
|
|
8
|
+
let screenY = getRandomInt(400, 600);
|
|
9
|
+
|
|
10
|
+
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX });
|
|
11
|
+
|
|
12
|
+
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY });
|