- Add pairing code auth with 515 reconnect handling (Baileys stream
error after pairing is now retried instead of failing)
- Use Browsers.macOS('Chrome') identifier for WhatsApp compatibility
- Fix LID-to-phone translation for DMs using signalRepository.getPNForLID
- Strip device suffix (:0) from resolved phone JIDs
- Update setup skill with three auth options (browser QR, pairing code,
terminal QR), DM channel type, and LID troubleshooting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
158 lines
5.3 KiB
TypeScript
158 lines
5.3 KiB
TypeScript
/**
|
|
* WhatsApp Authentication Script
|
|
*
|
|
* Run this during setup to authenticate with WhatsApp.
|
|
* Displays QR code, waits for scan, saves credentials, then exits.
|
|
*
|
|
* Usage: npx tsx src/whatsapp-auth.ts
|
|
*/
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import pino from 'pino';
|
|
import qrcode from 'qrcode-terminal';
|
|
import readline from 'readline';
|
|
|
|
import makeWASocket, {
|
|
Browsers,
|
|
DisconnectReason,
|
|
makeCacheableSignalKeyStore,
|
|
useMultiFileAuthState,
|
|
} from '@whiskeysockets/baileys';
|
|
|
|
const AUTH_DIR = './store/auth';
|
|
const QR_FILE = './store/qr-data.txt';
|
|
const STATUS_FILE = './store/auth-status.txt';
|
|
|
|
const logger = pino({
|
|
level: 'warn', // Quiet logging - only show errors
|
|
});
|
|
|
|
// Check for --pairing-code flag and phone number
|
|
const usePairingCode = process.argv.includes('--pairing-code');
|
|
const phoneArg = process.argv.find((_, i, arr) => arr[i - 1] === '--phone');
|
|
|
|
function askQuestion(prompt: string): Promise<string> {
|
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
return new Promise((resolve) => {
|
|
rl.question(prompt, (answer) => {
|
|
rl.close();
|
|
resolve(answer.trim());
|
|
});
|
|
});
|
|
}
|
|
|
|
async function connectSocket(phoneNumber?: string): Promise<void> {
|
|
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
|
|
|
|
if (state.creds.registered) {
|
|
fs.writeFileSync(STATUS_FILE, 'already_authenticated');
|
|
console.log('✓ Already authenticated with WhatsApp');
|
|
console.log(
|
|
' To re-authenticate, delete the store/auth folder and run again.',
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
const sock = makeWASocket({
|
|
auth: {
|
|
creds: state.creds,
|
|
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
|
},
|
|
printQRInTerminal: false,
|
|
logger,
|
|
browser: Browsers.macOS('Chrome'),
|
|
});
|
|
|
|
if (usePairingCode && phoneNumber && !state.creds.me) {
|
|
// Request pairing code after a short delay for connection to initialize
|
|
// Only on first connect (not reconnect after 515)
|
|
setTimeout(async () => {
|
|
try {
|
|
const code = await sock.requestPairingCode(phoneNumber!);
|
|
console.log(`\n🔗 Your pairing code: ${code}\n`);
|
|
console.log(' 1. Open WhatsApp on your phone');
|
|
console.log(' 2. Tap Settings → Linked Devices → Link a Device');
|
|
console.log(' 3. Tap "Link with phone number instead"');
|
|
console.log(` 4. Enter this code: ${code}\n`);
|
|
fs.writeFileSync(STATUS_FILE, `pairing_code:${code}`);
|
|
} catch (err: any) {
|
|
console.error('Failed to request pairing code:', err.message);
|
|
process.exit(1);
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
sock.ev.on('connection.update', (update) => {
|
|
const { connection, lastDisconnect, qr } = update;
|
|
|
|
if (qr) {
|
|
// Write raw QR data to file so the setup skill can render it
|
|
fs.writeFileSync(QR_FILE, qr);
|
|
console.log('Scan this QR code with WhatsApp:\n');
|
|
console.log(' 1. Open WhatsApp on your phone');
|
|
console.log(' 2. Tap Settings → Linked Devices → Link a Device');
|
|
console.log(' 3. Point your camera at the QR code below\n');
|
|
qrcode.generate(qr, { small: true });
|
|
}
|
|
|
|
if (connection === 'close') {
|
|
const reason = (lastDisconnect?.error as any)?.output?.statusCode;
|
|
|
|
if (reason === DisconnectReason.loggedOut) {
|
|
fs.writeFileSync(STATUS_FILE, 'failed:logged_out');
|
|
console.log('\n✗ Logged out. Delete store/auth and try again.');
|
|
process.exit(1);
|
|
} else if (reason === DisconnectReason.timedOut) {
|
|
fs.writeFileSync(STATUS_FILE, 'failed:qr_timeout');
|
|
console.log('\n✗ QR code timed out. Please try again.');
|
|
process.exit(1);
|
|
} else if (reason === 515) {
|
|
// 515 = stream error, often happens after pairing succeeds but before
|
|
// registration completes. Reconnect to finish the handshake.
|
|
console.log('\n⟳ Stream error (515) after pairing — reconnecting...');
|
|
connectSocket(phoneNumber);
|
|
} else {
|
|
fs.writeFileSync(STATUS_FILE, `failed:${reason || 'unknown'}`);
|
|
console.log('\n✗ Connection failed. Please try again.');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (connection === 'open') {
|
|
fs.writeFileSync(STATUS_FILE, 'authenticated');
|
|
// Clean up QR file now that we're connected
|
|
try { fs.unlinkSync(QR_FILE); } catch {}
|
|
console.log('\n✓ Successfully authenticated with WhatsApp!');
|
|
console.log(' Credentials saved to store/auth/');
|
|
console.log(' You can now start the NanoClaw service.\n');
|
|
|
|
// Give it a moment to save credentials, then exit
|
|
setTimeout(() => process.exit(0), 1000);
|
|
}
|
|
});
|
|
|
|
sock.ev.on('creds.update', saveCreds);
|
|
}
|
|
|
|
async function authenticate(): Promise<void> {
|
|
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
|
|
|
// Clean up any stale QR/status files from previous runs
|
|
try { fs.unlinkSync(QR_FILE); } catch {}
|
|
try { fs.unlinkSync(STATUS_FILE); } catch {}
|
|
|
|
let phoneNumber = phoneArg;
|
|
if (usePairingCode && !phoneNumber) {
|
|
phoneNumber = await askQuestion('Enter your phone number (with country code, no + or spaces, e.g. 14155551234): ');
|
|
}
|
|
|
|
console.log('Starting WhatsApp authentication...\n');
|
|
|
|
await connectSocket(phoneNumber);
|
|
}
|
|
|
|
authenticate().catch((err) => {
|
|
console.error('Authentication failed:', err.message);
|
|
process.exit(1);
|
|
});
|