codex-auth-automation 0.1.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.
@@ -0,0 +1,129 @@
1
+ import { GmailReader, GmailReaderError } from './gmail-reader.js';
2
+ import { OAuthFlowError, fullBrowserFlow } from './oauth-flow.js';
3
+ import { TokenStore, TokenStoreError } from './token-store.js';
4
+
5
+ export interface AutomationConfig {
6
+ gmailAddr: string;
7
+ gmailAppPassword: string;
8
+ storePath?: string;
9
+ headless?: boolean;
10
+ }
11
+
12
+ export class CodexAuthAutomation {
13
+ private gmail: GmailReader;
14
+ private store: TokenStore;
15
+ private headless: boolean;
16
+ private aliasCounter = 0;
17
+
18
+ constructor(config: AutomationConfig) {
19
+ this.gmail = new GmailReader({ email: config.gmailAddr, appPassword: config.gmailAppPassword });
20
+ this.store = new TokenStore(config.storePath);
21
+ this.headless = config.headless ?? true;
22
+ }
23
+
24
+ private nextAlias(): string {
25
+ this.aliasCounter += 1;
26
+ const [base, domain] = this.gmail.email.split('@');
27
+ return `${base}+codex${this.aliasCounter}@${domain}`;
28
+ }
29
+
30
+ async createAccount(alias?: string): Promise<{ email: string; index: number; tokens: Record<string, unknown> } | null> {
31
+ const targetAlias = alias || this.nextAlias();
32
+
33
+ console.log(`[Create] Using alias: ${targetAlias}`);
34
+ console.log('[Create] Waiting for OpenAI verification email (up to 90s)...');
35
+
36
+ let otpCode: string | null;
37
+ try {
38
+ otpCode = await this.gmail.searchForCode(targetAlias, 90000, 3000);
39
+ } catch (err) {
40
+ console.log(`[Create] Gmail error: ${err instanceof Error ? err.message : String(err)}`);
41
+ return null;
42
+ }
43
+
44
+ if (!otpCode) {
45
+ console.log('[Create] No verification code found in Gmail');
46
+ return null;
47
+ }
48
+
49
+ console.log(`[Create] Got OTP code: ${otpCode}`);
50
+ console.log('[Create] Starting browser OAuth flow...');
51
+
52
+ let tokens: Record<string, unknown>;
53
+ try {
54
+ tokens = await fullBrowserFlow(targetAlias, otpCode, this.headless);
55
+ } catch (err) {
56
+ console.log(`[Create] OAuth flow failed: ${err instanceof Error ? err.message : String(err)}`);
57
+ return null;
58
+ }
59
+
60
+ let idx: number;
61
+ try {
62
+ idx = this.store.addAccount(tokens, targetAlias);
63
+ console.log(`[Create] Account saved at index ${idx}: ${targetAlias}`);
64
+ } catch (err) {
65
+ console.log(`[Create] Store error: ${err instanceof Error ? err.message : String(err)}`);
66
+ return null;
67
+ }
68
+
69
+ return { email: targetAlias, index: idx, tokens };
70
+ }
71
+
72
+ status(): void {
73
+ const accounts = this.store.listAccounts();
74
+ if (!accounts.length) {
75
+ console.log('No accounts in store.');
76
+ return;
77
+ }
78
+
79
+ console.log('\n' + '='.repeat(60));
80
+ console.log(` Accounts (${accounts.length} total)`);
81
+ console.log('='.repeat(60));
82
+
83
+ const nowMs = Date.now();
84
+ const activeIdx = this.store.getActiveAccount()?.index ?? 0;
85
+
86
+ for (const acc of accounts) {
87
+ const marker = acc.index === activeIdx ? '>>>' : ' ';
88
+ let status = 'OK';
89
+ if (acc.authInvalid) status = 'AUTH INVALID';
90
+ else if (!acc.enabled) status = 'DISABLED';
91
+ else if (acc.expiresAt < nowMs) status = 'EXPIRED';
92
+
93
+ let expiryStr = '';
94
+ if (acc.expiresAt) {
95
+ const expDt = new Date(acc.expiresAt);
96
+ expiryStr = ` (exp: ${expDt.toISOString().replace('T', ' ').slice(0, 16)} UTC)`;
97
+ }
98
+
99
+ console.log(` ${marker} [${acc.index}] ${acc.email}`);
100
+ console.log(` Status: ${status}${expiryStr} | Usage: ${acc.usageCount}`);
101
+ }
102
+
103
+ console.log('='.repeat(60) + '\n');
104
+ }
105
+
106
+ rotate(): void {
107
+ const accounts = this.store.listAccounts();
108
+ if (!accounts.length) {
109
+ console.log('No accounts to rotate to.');
110
+ return;
111
+ }
112
+
113
+ const current = this.store.getActiveAccount()?.index ?? 0;
114
+ const nextIdx = (current + 1) % accounts.length;
115
+
116
+ this.store.setActive(nextIdx);
117
+ const acc = this.store.getAccount(nextIdx);
118
+ if (acc) {
119
+ console.log(`Rotated to account #${nextIdx}: ${acc.email}`);
120
+ } else {
121
+ console.log(`Rotated to account #${nextIdx}`);
122
+ }
123
+ }
124
+
125
+ clean(): void {
126
+ const removed = this.store.cleanInvalid();
127
+ console.log(`Removed ${removed} invalid/expired accounts.`);
128
+ }
129
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import { CodexAuthAutomation } from './automation.js';
8
+
9
+ const CONFIG_FILE = path.join(os.homedir(), '.config', 'codex-auth', 'config.json');
10
+
11
+ interface Config {
12
+ gmail: string;
13
+ gmail_app_password: string;
14
+ headless: boolean;
15
+ }
16
+
17
+ function loadConfig(): Config {
18
+ if (fs.existsSync(CONFIG_FILE)) {
19
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
20
+ }
21
+ return { gmail: '', gmail_app_password: '', headless: true };
22
+ }
23
+
24
+ function saveConfig(config: Config): void {
25
+ fs.mkdirSync(path.dirname(CONFIG_FILE), { recursive: true });
26
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
27
+ }
28
+
29
+ async function cmdCreate(config: Config, alias?: string) {
30
+ if (!config.gmail || !config.gmail_app_password) {
31
+ console.error('Error: Gmail credentials not configured.');
32
+ console.error('Run: codex-auth config --set gmail your@email.com');
33
+ console.error('Run: codex-auth config --set gmail_app_password YOUR_APP_PASSWORD');
34
+ process.exit(1);
35
+ }
36
+
37
+ const app = new CodexAuthAutomation({
38
+ gmailAddr: config.gmail,
39
+ gmailAppPassword: config.gmail_app_password,
40
+ headless: config.headless,
41
+ });
42
+
43
+ const result = await app.createAccount(alias);
44
+ if (result) {
45
+ console.log(`\nSuccess! Account created: ${result.email}`);
46
+ } else {
47
+ console.error('\nFailed to create account.');
48
+ process.exit(1);
49
+ }
50
+ }
51
+
52
+ function cmdStatus(config: Config) {
53
+ const app = new CodexAuthAutomation({
54
+ gmailAddr: config.gmail,
55
+ gmailAppPassword: config.gmail_app_password,
56
+ });
57
+ app.status();
58
+ }
59
+
60
+ function cmdRotate(config: Config) {
61
+ const app = new CodexAuthAutomation({
62
+ gmailAddr: config.gmail,
63
+ gmailAppPassword: config.gmail_app_password,
64
+ });
65
+ app.rotate();
66
+ }
67
+
68
+ function cmdClean(config: Config) {
69
+ const app = new CodexAuthAutomation({
70
+ gmailAddr: config.gmail,
71
+ gmailAppPassword: config.gmail_app_password,
72
+ });
73
+ app.clean();
74
+ }
75
+
76
+ function cmdSetup(config: Config) {
77
+ const readline = require('readline').createInterface({
78
+ input: process.stdin,
79
+ output: process.stdout,
80
+ });
81
+
82
+ const question = (prompt: string): Promise<string> =>
83
+ new Promise((resolve) => readline.question(prompt, resolve));
84
+
85
+ (async () => {
86
+ console.log('codex-auth setup');
87
+ console.log('='.repeat(40));
88
+
89
+ const gmail = await question('Gmail address: ');
90
+ const appPw = await question('Gmail App Password: ');
91
+ const headlessInput = await question('Headless mode? (Y/n): ');
92
+ const headless = headlessInput.trim().toLowerCase() !== 'n';
93
+
94
+ config.gmail = gmail.trim();
95
+ config.gmail_app_password = appPw.trim();
96
+ config.headless = headless;
97
+ saveConfig(config);
98
+
99
+ console.log(`\nConfig saved to ${CONFIG_FILE}`);
100
+ console.log("Run 'codex-auth create' to create your first account.");
101
+ readline.close();
102
+ })();
103
+ }
104
+
105
+ function cmdConfig(config: Config, key?: string, value?: string) {
106
+ if (key && value !== undefined) {
107
+ let parsed: string | boolean = value;
108
+ if (key === 'headless') {
109
+ parsed = ['true', '1', 'yes'].includes(value.toLowerCase());
110
+ }
111
+ (config as unknown as Record<string, unknown>)[key] = parsed;
112
+ saveConfig(config);
113
+ console.log(`Set ${key} = ${parsed}`);
114
+ } else {
115
+ console.log(JSON.stringify(config, null, 2));
116
+ }
117
+ }
118
+
119
+ const program = new Command();
120
+
121
+ program
122
+ .name('codex-auth')
123
+ .description('Automated OpenAI/Codex account creation with Gmail aliases')
124
+ .version('0.1.0');
125
+
126
+ program
127
+ .command('create')
128
+ .description('Create a new Codex account')
129
+ .option('--alias <alias>', 'Specific alias (e.g. you+custom@gmail.com)')
130
+ .action((opts) => {
131
+ const config = loadConfig();
132
+ cmdCreate(config, opts.alias);
133
+ });
134
+
135
+ program
136
+ .command('status')
137
+ .description('Show all accounts')
138
+ .action(() => {
139
+ const config = loadConfig();
140
+ cmdStatus(config);
141
+ });
142
+
143
+ program
144
+ .command('rotate')
145
+ .description('Rotate to next account')
146
+ .action(() => {
147
+ const config = loadConfig();
148
+ cmdRotate(config);
149
+ });
150
+
151
+ program
152
+ .command('clean')
153
+ .description('Remove invalid/expired accounts')
154
+ .action(() => {
155
+ const config = loadConfig();
156
+ cmdClean(config);
157
+ });
158
+
159
+ program
160
+ .command('setup')
161
+ .description('Interactive setup wizard')
162
+ .action(() => {
163
+ const config = loadConfig();
164
+ cmdSetup(config);
165
+ });
166
+
167
+ program
168
+ .command('config')
169
+ .description('View or set config')
170
+ .option('--set <key> <value>', 'Set config value')
171
+ .action((opts) => {
172
+ const config = loadConfig();
173
+ if (opts.set) {
174
+ const [key, value] = opts.set.split(' ');
175
+ cmdConfig(config, key, value);
176
+ } else {
177
+ cmdConfig(config);
178
+ }
179
+ });
180
+
181
+ program.parse();
@@ -0,0 +1,205 @@
1
+ import Imap from 'imap';
2
+ import { simpleParser } from 'mailparser';
3
+ import { Readable } from 'stream';
4
+
5
+ export class GmailReaderError extends Error {
6
+ constructor(message: string) {
7
+ super(message);
8
+ this.name = 'GmailReaderError';
9
+ }
10
+ }
11
+
12
+ export interface GmailConfig {
13
+ email: string;
14
+ appPassword: string;
15
+ }
16
+
17
+ export class GmailReader {
18
+ readonly email: string;
19
+ private appPassword: string;
20
+ private conn: Imap | null = null;
21
+
22
+ private static CODE_PATTERNS = [
23
+ /verification code[:\s]+(\d{6})/i,
24
+ /your code is[:\s]*(\d{6})/i,
25
+ /enter this code[:\s]+(\d{6})/i,
26
+ /your verification code is (\d{6})/i,
27
+ /code[:\s]+(\d{6})/i,
28
+ /\b(\d{6})\b/,
29
+ ];
30
+
31
+ constructor(config: GmailConfig) {
32
+ this.email = config.email;
33
+ this.appPassword = config.appPassword;
34
+ }
35
+
36
+ connect(): Promise<Imap> {
37
+ return new Promise((resolve, reject) => {
38
+ const conn = new Imap({
39
+ user: this.email,
40
+ password: this.appPassword.replace(/\s/g, ''),
41
+ host: 'imap.gmail.com',
42
+ port: 993,
43
+ tls: true,
44
+ tlsOptions: { rejectUnauthorized: true },
45
+ });
46
+
47
+ conn.once('ready', () => {
48
+ this.conn = conn;
49
+ resolve(conn);
50
+ });
51
+
52
+ conn.once('error', (err) => {
53
+ reject(new GmailReaderError(`IMAP login failed: ${err.message}`));
54
+ });
55
+
56
+ conn.connect();
57
+ });
58
+ }
59
+
60
+ private async ensureConnected(): Promise<Imap> {
61
+ if (!this.conn) {
62
+ return this.connect();
63
+ }
64
+ return this.conn;
65
+ }
66
+
67
+ async searchForCode(
68
+ alias: string,
69
+ timeout = 60000,
70
+ pollInterval = 3000,
71
+ ): Promise<string | null> {
72
+ const conn = await this.ensureConnected();
73
+ const deadline = Date.now() + timeout;
74
+ const seenUids = new Set<string>();
75
+
76
+ while (Date.now() < deadline) {
77
+ try {
78
+ const messages = await this.searchImap(conn, seenUids);
79
+ for (const msg of messages) {
80
+ if (seenUids.has(msg.uid)) continue;
81
+ seenUids.add(msg.uid);
82
+
83
+ const body = await this.fetchMessageBody(conn, msg.uid);
84
+ const code = this.extractCode(body);
85
+ if (code) return code;
86
+ }
87
+ } catch {
88
+ // transient error, keep polling
89
+ }
90
+
91
+ const remaining = deadline - Date.now();
92
+ if (remaining <= 0) break;
93
+ await this.sleep(Math.min(pollInterval, remaining));
94
+ }
95
+
96
+ return null;
97
+ }
98
+
99
+ async getLatestCode(): Promise<{ code: string; recipient: string } | null> {
100
+ const conn = await this.ensureConnected();
101
+
102
+ return new Promise((resolve, reject) => {
103
+ conn.openBox('INBOX', true, (err) => {
104
+ if (err) return reject(new GmailReaderError(`Failed to open inbox: ${err.message}`));
105
+
106
+ conn.search(['OR', ['FROM', 'auth.openai.com'], ['FROM', 'openai.com']], (err, results) => {
107
+ if (err || !results || results.length === 0) return resolve(null);
108
+
109
+ const uids = results.slice(-10);
110
+ const fetch = conn.fetch(uids, { bodies: '' });
111
+
112
+ fetch.on('message', (msg) => {
113
+ msg.on('body', (stream) => {
114
+ simpleParser(Readable.from(stream))
115
+ .then((parsed) => {
116
+ const body = parsed.text || parsed.html || '';
117
+ const code = this.extractCode(String(body));
118
+ if (code) {
119
+ const toText = parsed.to
120
+ ? (Array.isArray(parsed.to) ? parsed.to[0]?.text : parsed.to.text)
121
+ : '';
122
+ resolve({ code, recipient: String(toText || '') });
123
+ }
124
+ })
125
+ .catch(() => {});
126
+ });
127
+ });
128
+
129
+ fetch.once('error', reject);
130
+ fetch.once('end', () => resolve(null));
131
+ });
132
+ });
133
+ });
134
+ }
135
+
136
+ close(): void {
137
+ if (this.conn) {
138
+ try { this.conn.end(); } catch { }
139
+ this.conn = null;
140
+ }
141
+ }
142
+
143
+ private searchImap(
144
+ conn: Imap,
145
+ seenUids: Set<string>,
146
+ ): Promise<{ uid: string }[]> {
147
+ return new Promise((resolve, reject) => {
148
+ conn.openBox('INBOX', true, (err) => {
149
+ if (err) return reject(new GmailReaderError(`Failed to open inbox: ${err.message}`));
150
+
151
+ conn.search(['OR', ['FROM', 'auth.openai.com'], ['FROM', 'openai.com']], (err, results) => {
152
+ if (err) return reject(new GmailReaderError(`Search failed: ${err.message}`));
153
+ if (!results || results.length === 0) return resolve([]);
154
+
155
+ resolve(
156
+ results.map((uid) => ({ uid: String(uid) })).filter((m) => !seenUids.has(m.uid)),
157
+ );
158
+ });
159
+ });
160
+ });
161
+ }
162
+
163
+ private fetchMessageBody(conn: Imap, uid: string): Promise<string> {
164
+ return new Promise((resolve, reject) => {
165
+ const fetch = conn.fetch(uid, { bodies: '' });
166
+
167
+ fetch.on('message', (msg) => {
168
+ msg.on('body', (stream) => {
169
+ simpleParser(Readable.from(stream))
170
+ .then((parsed) => {
171
+ resolve(String(parsed.text || parsed.html || ''));
172
+ })
173
+ .catch(reject);
174
+ });
175
+ msg.once('error', reject);
176
+ });
177
+
178
+ fetch.once('error', reject);
179
+ fetch.once('end', () => resolve(''));
180
+ });
181
+ }
182
+
183
+ private extractCode(content: string): string | null {
184
+ if (!content) return null;
185
+
186
+ for (const pattern of GmailReader.CODE_PATTERNS) {
187
+ const match = pattern.exec(content);
188
+ if (match && !match[1].startsWith('20')) {
189
+ return match[1];
190
+ }
191
+ }
192
+
193
+ const allCodes = content.match(/\b(\d{6})\b/g);
194
+ if (allCodes) {
195
+ const valid = allCodes.filter((c) => !c.startsWith('20'));
196
+ if (valid.length > 0) return valid[valid.length - 1];
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ private sleep(ms: number): Promise<void> {
203
+ return new Promise((resolve) => setTimeout(resolve, ms));
204
+ }
205
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { GmailReader, GmailReaderError } from './gmail-reader.js';
2
+ export { OAuthFlowError, fullBrowserFlow, browserSignup } from './oauth-flow.js';
3
+ export { TokenStore, TokenStoreError } from './token-store.js';
4
+ export { CodexAuthAutomation } from './automation.js';