// TEARust — FCM Register // Standalone script — no npm install required. Just: node fcm-register.cjs // // Usage: // node fcm-register.cjs // node fcm-register.cjs --browser chrome // node fcm-register.cjs --browser brave // node fcm-register.cjs --browser edge // node fcm-register.cjs --browser /full/path/to/browser // node fcm-register.cjs --config-file /path/to/rustplus.config.json 'use strict'; const path = require('path'); const fs = require('fs'); const crypto = require('crypto'); const https = require('https'); const { execSync } = require('child_process'); const axios = require('axios'); const express = require('express'); const { v4: uuidv4 } = require('uuid'); const ChromeLauncher = require('chrome-launcher'); const protobuf = require('protobufjs'); const Long = require('long'); // ── arg parsing ────────────────────────────────────────────────────────────── const args = process.argv.slice(2); let browserArg = null; let configFile = path.join(process.cwd(), 'rustplus.config.json'); for (let i = 0; i < args.length; i++) { if ((args[i] === '--browser' || args[i] === '-b') && args[i + 1]) { browserArg = args[++i]; continue; } if (args[i] === '--config-file' && args[i + 1]) { configFile = args[++i]; continue; } if (args[i] === '--help' || args[i] === '-h') { printHelp(); process.exit(0); } } function printHelp() { console.log(` TEARust — FCM Register Usage: node fcm-register.cjs [options] Options: --browser Browser to open for Steam login. Names: arc, chrome, brave, edge, opera, operagx, vivaldi, chromium Or a full path to the browser executable. Default: auto-detect system default, then scan installed browsers. --config-file Output config file. Default: ./rustplus.config.json --help Show this help. Note: Only Chromium-based browsers work. Firefox and Safari cannot handle the cross-origin Steam login flow and will be rejected. `); } // ── browser registry ───────────────────────────────────────────────────────── const BROWSERS = [ { key: 'arc', chromium: true, mac: '/Applications/Arc.app/Contents/MacOS/Arc', win: [(process.env.LOCALAPPDATA||'')+'\\Programs\\Arc\\Arc.exe'], linux: [], winProgIds: ['Arc', 'ArcHTM'], macAppName: 'Arc', }, { key: 'chrome', chromium: true, mac: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', win: [ (process.env.LOCALAPPDATA||'')+'\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', ], linux: ['google-chrome-stable', 'google-chrome'], winProgIds: ['ChromeHTML', 'ChromiumHTM'], macAppName: 'Google Chrome', }, { key: 'brave', chromium: true, mac: '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser', win: [ (process.env.LOCALAPPDATA||'')+'\\BraveSoftware\\Brave-Browser\\Application\\brave.exe', 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe', ], linux: ['brave-browser', 'brave'], winProgIds: ['BraveHTML'], macAppName: 'Brave Browser', }, { key: 'edge', chromium: true, mac: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', win: [ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', ], linux: ['microsoft-edge-stable', 'microsoft-edge'], winProgIds: ['MSEdgeHTM', 'MSEdgeHTMS'], macAppName: 'Microsoft Edge', }, { key: 'opera', chromium: true, mac: '/Applications/Opera.app/Contents/MacOS/Opera', win: [ (process.env.LOCALAPPDATA||'')+'\\Programs\\Opera\\opera.exe', 'C:\\Program Files\\Opera\\opera.exe', ], linux: ['opera-stable', 'opera'], winProgIds: ['OperaStable'], macAppName: 'Opera', }, { key: 'operagx', chromium: true, mac: '/Applications/Opera GX.app/Contents/MacOS/Opera GX', win: [(process.env.LOCALAPPDATA||'')+'\\Programs\\Opera GX\\opera.exe'], linux: [], winProgIds: ['OperaGXStable'], macAppName: 'Opera GX', }, { key: 'vivaldi', chromium: true, mac: '/Applications/Vivaldi.app/Contents/MacOS/Vivaldi', win: [(process.env.LOCALAPPDATA||'')+'\\Vivaldi\\Application\\vivaldi.exe'], linux: ['vivaldi-stable', 'vivaldi'], winProgIds: ['VivaldiHTM'], macAppName: 'Vivaldi', }, { key: 'chromium', chromium: true, mac: '/Applications/Chromium.app/Contents/MacOS/Chromium', win: [ 'C:\\Program Files\\Chromium\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Chromium\\Application\\chrome.exe', ], linux: ['chromium-browser', 'chromium'], winProgIds: [], macAppName: 'Chromium', }, { key: 'firefox', chromium: false, mac: '', win: [], linux: ['firefox'], winProgIds: ['FirefoxURL', 'FirefoxHTML'], macAppName: 'Firefox' }, { key: 'safari', chromium: false, mac: '', win: [], linux: [], winProgIds: [], macAppName: 'Safari' }, ]; const NAME_ALIASES = { 'googlechrome': 'chrome', 'microsoftedge': 'edge', 'msedge': 'edge', 'operagx': 'operagx', 'opera gx': 'operagx', 'thebrowsercompany': 'arc', }; function which(cmd) { try { return execSync(`which "${cmd}" 2>/dev/null`, { stdio: 'pipe' }).toString().trim() || null; } catch { return null; } } function browserExePath(b) { if (process.platform === 'win32') { for (const p of b.win) { if (fs.existsSync(p)) return p; } return null; } if (process.platform === 'darwin') { return (b.mac && fs.existsSync(b.mac)) ? b.mac : null; } for (const cmd of b.linux) { const f = which(cmd); if (f) return f; } return null; } const NON_CHROMIUM_APPS = new Set(['firefox', 'safari', 'tor browser', 'waterfox', 'librewolf']); function getMacDefaultBrowserPath() { try { const appPath = execSync( `osascript -e 'POSIX path of (path to default web browser)'`, { encoding: 'utf8', timeout: 4000 }, ).trim().replace(/\/$/, ''); const appName = path.basename(appPath, '.app'); const norm = appName.toLowerCase().replace(/[\s-]/g, ''); console.log(`System default browser: ${appName} (${appPath})`); if (NON_CHROMIUM_APPS.has(norm) || NON_CHROMIUM_APPS.has(appName.toLowerCase())) { console.warn(`⚠ ${appName} is not Chromium-based — cannot use it. Scanning for Chromium browser...`); return null; } const binaryPath = path.join(appPath, 'Contents', 'MacOS', appName); if (fs.existsSync(binaryPath)) return binaryPath; const macosDir = path.join(appPath, 'Contents', 'MacOS'); if (fs.existsSync(macosDir)) { const bins = fs.readdirSync(macosDir).filter(f => !f.startsWith('.') && !f.endsWith('.dylib')); if (bins.length >= 1) { const best = bins.find(f => f.toLowerCase() === appName.toLowerCase()) || bins[0]; const p = path.join(macosDir, best); if (fs.existsSync(p)) return p; } } console.warn(` Could not find binary inside ${appPath}`); } catch (e) { console.log(` osascript query failed: ${e.message}`); } return null; } function getWinDefaultKey() { try { const out = execSync( `reg query "HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\https\\UserChoice" /v ProgId`, { encoding: 'utf8', timeout: 3000 }, ); const m = out.match(/ProgId\s+REG_SZ\s+(\S+)/i); const progId = m?.[1] || ''; console.log(`Registry ProgId: ${progId}`); return BROWSERS.find(b => b.winProgIds.some(id => progId.toLowerCase().startsWith(id.toLowerCase())))?.key || null; } catch (e) { console.log(` Registry query failed: ${e.message}`); return null; } } function getLinuxDefaultKey() { try { const desktop = execSync(`xdg-settings get default-web-browser`, { encoding: 'utf8', timeout: 3000 }) .trim().replace(/\.desktop$/i, ''); console.log(`xdg default: ${desktop}`); const norm = desktop.toLowerCase().replace(/[\s_-]/g, ''); return BROWSERS.find(b => b.linux.some(cmd => cmd.replace(/[\s_-]/g,'') === norm))?.key || null; } catch (e) { console.log(` xdg-settings failed: ${e.message}`); return null; } } function findBrowserByName(name) { const norm = name.toLowerCase().replace(/[\s-]/g, ''); const key = NAME_ALIASES[norm] || norm; const b = BROWSERS.find(x => x.key === key); if (!b) return null; return browserExePath(b); } function autoDetect() { console.log('Detecting browser...'); if (process.platform === 'darwin') { const p = getMacDefaultBrowserPath(); if (p) { console.log(`Using: ${p}\n`); return p; } } else if (process.platform === 'win32') { const key = getWinDefaultKey(); if (key) { const b = BROWSERS.find(x => x.key === key); if (b && !b.chromium) { console.warn(`⚠ Default browser (${b.key}) is not Chromium-based. Scanning...`); } else if (b) { const p = browserExePath(b); if (p) { console.log(`Using system default: ${b.key} → ${p}\n`); return p; } console.log(` ${b.key} detected but binary not found at expected path. Scanning...`); } } } else { const key = getLinuxDefaultKey(); if (key) { const b = BROWSERS.find(x => x.key === key); if (b && !b.chromium) { console.warn(`⚠ Default browser (${b.key}) is not Chromium-based. Scanning...`); } else if (b) { const p = browserExePath(b); if (p) { console.log(`Using system default: ${b.key} → ${p}\n`); return p; } } } } console.log('Scanning for installed Chromium browsers...'); for (const b of BROWSERS.filter(x => x.chromium)) { const p = browserExePath(b); if (p) { console.log(`Found: ${b.key} → ${p}\n`); return p; } } console.log('No browser found — letting chrome-launcher try its own detection.\n'); return null; } function resolveBrowser() { if (!browserArg) return autoDetect(); if (browserArg.includes('/') || browserArg.includes('\\')) { if (!fs.existsSync(browserArg)) { console.error(`Browser not found: ${browserArg}`); process.exit(1); } return browserArg; } const norm = browserArg.toLowerCase().replace(/[\s-]/g, ''); const key = NAME_ALIASES[norm] || norm; const b = BROWSERS.find(x => x.key === key); if (b && !b.chromium) { console.error(`✗ ${b.key} is not Chromium-based and cannot be used.`); console.error(' Supported: arc, chrome, brave, edge, opera, vivaldi, chromium'); process.exit(1); } const found = findBrowserByName(browserArg); if (!found) { console.error(`Cannot locate browser "${browserArg}". Try --browser /full/path/to/browser`); process.exit(1); } return found; } // ── config helpers ──────────────────────────────────────────────────────────── function readConfig() { try { return JSON.parse(fs.readFileSync(configFile, 'utf8')); } catch { return {}; } } function saveConfig(data) { fs.writeFileSync(configFile, JSON.stringify({ ...readConfig(), ...data }, null, 2), 'utf8'); } // ── inline GCM checkin (no file-system proto loading) ───────────────────────── // Combined checkin.proto + android_checkin.proto as inline string const CHECKIN_PROTO = ` syntax = "proto2"; option optimize_for = LITE_RUNTIME; package checkin_proto; message ChromeBuildProto { enum Platform { PLATFORM_WIN = 1; PLATFORM_MAC = 2; PLATFORM_LINUX = 3; PLATFORM_CROS = 4; PLATFORM_IOS = 5; PLATFORM_ANDROID = 6; } enum Channel { CHANNEL_STABLE = 1; CHANNEL_BETA = 2; CHANNEL_DEV = 3; CHANNEL_CANARY = 4; CHANNEL_UNKNOWN = 5; } optional Platform platform = 1; optional string chrome_version = 2; optional Channel channel = 3; } message AndroidCheckinProto { optional int64 last_checkin_msec = 2; optional string cell_operator = 6; optional string sim_operator = 7; optional string roaming = 8; optional int32 user_number = 9; optional DeviceType type = 12 [default = DEVICE_ANDROID_OS]; optional ChromeBuildProto chrome_build = 13; } enum DeviceType { DEVICE_ANDROID_OS = 1; DEVICE_IOS_OS = 2; DEVICE_CHROME_BROWSER = 3; DEVICE_CHROME_OS = 4; } message GservicesSetting { required bytes name = 1; required bytes value = 2; } message AndroidCheckinRequest { optional string imei = 1; optional string meid = 10; repeated string mac_addr = 9; repeated string mac_addr_type = 19; optional string serial_number = 16; optional string esn = 17; optional int64 id = 2; optional int64 logging_id = 7; optional string digest = 3; optional string locale = 6; required AndroidCheckinProto checkin = 4; optional string desired_build = 5; optional string market_checkin = 8; repeated string account_cookie = 11; optional string time_zone = 12; optional fixed64 security_token = 13; optional int32 version = 14; repeated string ota_cert = 15; optional int32 fragment = 20; optional string user_name = 21; optional int32 user_serial_number = 22; } message AndroidCheckinResponse { required bool stats_ok = 1; optional int64 time_msec = 3; optional string digest = 4; optional bool settings_diff = 9; repeated string delete_setting = 10; repeated GservicesSetting setting = 5; optional bool market_ok = 6; optional fixed64 android_id = 7; optional fixed64 security_token = 8; optional string version_info = 11; } `; // GCM server key (public — same key used by all Chromium-based FCM clients) const GCM_SERVER_KEY = Buffer.from([ 0x04,0x33,0x94,0xf7,0xdf,0xa1,0xeb,0xb1,0xdc,0x03,0xa2,0x5e,0x15,0x71,0xdb,0x48, 0xd3,0x2e,0xed,0xed,0xb2,0x34,0xdb,0xb7,0x47,0x3a,0x0c,0x8f,0xc4,0xcc,0xe1,0x6f, 0x3c,0x8c,0x84,0xdf,0xab,0xb6,0x66,0x3e,0xf2,0x0c,0xd4,0x8b,0xfe,0xe3,0xf9,0x76, 0x2f,0x14,0x1c,0x63,0x08,0x6a,0x6f,0x2d,0xb1,0x1a,0x95,0xb0,0xce,0x37,0xc0,0x9c,0x6e, ]).toString('base64').replace(/=/g,'').replace(/\+/g,'-').replace(/\//g,'_'); let _checkinRoot = null; async function gcmCheckIn() { if (!_checkinRoot) { protobuf.util.Long = Long; protobuf.configure(); _checkinRoot = protobuf.parse(CHECKIN_PROTO).root; } const AndroidCheckinRequest = _checkinRoot.lookupType('checkin_proto.AndroidCheckinRequest'); const AndroidCheckinResponse = _checkinRoot.lookupType('checkin_proto.AndroidCheckinResponse'); const payload = { userSerialNumber: 0, checkin: { type: 3, chromeBuild: { platform: 2, chromeVersion: '63.0.3234.0', channel: 1 }, }, version: 3, }; const errMsg = AndroidCheckinRequest.verify(payload); if (errMsg) throw new Error(errMsg); const buffer = AndroidCheckinRequest.encode(AndroidCheckinRequest.create(payload)).finish(); const response = await axios.post('https://android.clients.google.com/checkin', buffer, { headers: { 'Content-Type': 'application/x-protobuf' }, responseType: 'arraybuffer', }); const msg = AndroidCheckinResponse.decode(new Uint8Array(response.data)); const obj = AndroidCheckinResponse.toObject(msg, { longs: String, enums: String, bytes: String }); return obj; } async function gcmRegister(androidId, securityToken, appId) { const body = new URLSearchParams({ app: 'org.chromium.linux', 'X-subtype': appId, device: androidId, sender: GCM_SERVER_KEY, }); let lastResponse; for (let retry = 0; retry <= 5; retry++) { const r = await axios.post('https://android.clients.google.com/c2dm/register3', body.toString(), { headers: { Authorization: `AidLogin ${androidId}:${securityToken}`, 'Content-Type': 'application/x-www-form-urlencoded', }, }); lastResponse = r.data; if (!String(lastResponse).includes('Error')) break; if (retry >= 5) throw new Error('GCM register failed after 5 retries'); console.warn(`GCM register retry ${retry + 1}...`); await new Promise(r => setTimeout(r, 2000)); } return String(lastResponse).split('=')[1]; } // ── inline FCM registration (replaces @liamcottle/push-receiver) ───────────── function generateFirebaseFID() { const buf = crypto.randomBytes(17); buf[0] = 0b01110000 | (buf[0] & 0b00001111); return buf.toString('base64').replace(/=/g, ''); } async function fcmInstallRequest(apiKey, projectId, gmsAppId, androidPackage, androidCert) { const response = await axios.post( `https://firebaseinstallations.googleapis.com/v1/projects/${projectId}/installations`, { fid: generateFirebaseFID(), appId: gmsAppId, authVersion: 'FIS_v2', sdkVersion: 'a:17.0.0', }, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Android-Package': androidPackage, 'X-Android-Cert': androidCert, 'x-firebase-client': 'android-min-sdk/23 fire-core/20.0.0 device-name/a21snnxx device-brand/samsung device-model/a21s android-installer/com.android.vending fire-android/30 fire-installations/17.0.0 fire-fcm/22.0.0 android-platform/ kotlin/1.9.23 android-target-sdk/34', 'x-firebase-client-log-type': '3', 'x-goog-api-key': apiKey, 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 11; SM-A217F Build/RP1A.200720.012)', }, }, ); if (!response.data.authToken?.token) throw new Error(`Firebase install failed: ${JSON.stringify(response.data)}`); return response.data.authToken.token; } async function fcmRegisterGcm(androidId, securityToken, installationAuthToken, apiKey, gcmSenderId, gmsAppId, androidPackageName, androidPackageCert) { const body = new URLSearchParams({ device: String(androidId), app: androidPackageName, cert: androidPackageCert, app_ver: '1', 'X-subtype': gcmSenderId, 'X-app_ver': '1', 'X-osv': '29', 'X-cliv': 'fiid-21.1.1', 'X-gmsv': '220217001', 'X-scope': '*', 'X-Goog-Firebase-Installations-Auth': installationAuthToken, 'X-gms_app_id': gmsAppId, 'X-Firebase-Client': 'android-min-sdk/23 fire-core/20.0.0 device-name/a21snnxx device-brand/samsung device-model/a21s android-installer/com.android.vending fire-android/30 fire-installations/17.0.0 fire-fcm/22.0.0 android-platform/ kotlin/1.9.23 android-target-sdk/34', 'X-Firebase-Client-Log-Type': '1', 'X-app_ver_name': '1', target_ver: '31', sender: gcmSenderId, }); for (let retry = 0; retry <= 5; retry++) { const r = await axios.post('https://android.clients.google.com/c2dm/register3', body.toString(), { headers: { Authorization: `AidLogin ${androidId}:${securityToken}`, 'Content-Type': 'application/x-www-form-urlencoded', }, }); const data = String(r.data); if (!data.includes('Error')) return data.split('=')[1]; if (retry >= 5) throw new Error('GCM register failed'); console.warn(`GCM register retry ${retry + 1}...`); await new Promise(r => setTimeout(r, 1000)); } } async function registerFCM(apiKey, projectId, gcmSenderId, gmsAppId, androidPackageName, androidPackageCert) { const installationAuthToken = await fcmInstallRequest(apiKey, projectId, gmsAppId, androidPackageName, androidPackageCert); const checkInResponse = await gcmCheckIn(); const fcmToken = await fcmRegisterGcm( checkInResponse.androidId, checkInResponse.securityToken, installationAuthToken, apiKey, gcmSenderId, gmsAppId, androidPackageName, androidPackageCert, ); return { gcm: { androidId: checkInResponse.androidId, securityToken: checkInResponse.securityToken }, fcm: { token: fcmToken }, }; } // ── pair HTML (inline) ──────────────────────────────────────────────────────── const PAIR_HTML = ` TEARust — Steam Login

TEARust — Link Steam with Rust+

A Steam login popup should open. Log in and click Allow on the Rust+ page.

If no popup appeared, check your browser's address bar for a blocked popup notification and allow it, then refresh this page.

`; const SUCCESS_HTML = ` Done

✓ Steam linked!

You can close this window and go back to the console.

`; // ── main flow ───────────────────────────────────────────────────────────────── let server; async function linkSteamWithRustPlus(chromePath) { return new Promise((resolve, reject) => { const app = express(); app.get('/', (_req, res) => res.send(PAIR_HTML)); app.get('/callback', async (req, res) => { await ChromeLauncher.killAll(); const token = req.query.token; if (token) { res.send(SUCCESS_HTML); resolve(token); } else { res.status(400).send('Token missing from request!'); reject(new Error('Token missing from callback')); } server.close(); }); const port = 3000; server = app.listen(port, async () => { const browserLabel = chromePath ? path.basename(chromePath) : 'auto-detected browser'; console.log(`Launching ${browserLabel} for Steam login...`); const opts = { startingUrl: `http://localhost:${port}`, chromeFlags: [ '--disable-web-security', '--disable-popup-blocking', '--disable-site-isolation-trials', '--user-data-dir=/tmp/tea-rustplus-pair-profile', ], handleSIGINT: false, }; if (chromePath) opts.chromePath = chromePath; await ChromeLauncher.launch(opts).catch((err) => { console.error('\nFailed to launch browser:', err.message); console.error('Try specifying one with: node fcm-register.cjs --browser brave'); console.error('Supported names: chrome, brave, chromium, edge'); process.exit(1); }); }); }); } async function main() { console.log('TEARust FCM Register\n'); const chromePath = resolveBrowser(); console.log('Registering with FCM...'); const fcmCreds = await registerFCM( 'AIzaSyB5y2y-Tzqb4-I4Qnlsh_9naYv_TD8pCvY', 'rust-companion-app', '976529667804', '1:976529667804:android:d6f1ddeb4403b338fea619', 'com.facepunch.rust.companion', 'E28D05345FB78A7A1A63D70F4A302DBF426CA5AD', ).catch(err => { console.error('FCM registration failed:', err.message); process.exit(1); }); console.log('Fetching Expo Push Token...'); const expoPushToken = await axios.post('https://exp.host/--/api/v2/push/getExpoPushToken', { type: 'fcm', deviceId: uuidv4(), development: false, appId: 'com.facepunch.rust.companion', deviceToken: fcmCreds.fcm.token, projectId: '49451aca-a822-41e6-ad59-955718d0ff9c', }).then(r => r.data.data.expoPushToken) .catch(err => { console.error('Expo token failed:', err.message); process.exit(1); }); console.log(`Expo Push Token: ${expoPushToken}`); const authToken = await linkSteamWithRustPlus(chromePath); console.log(`\nRust+ Auth Token: ${authToken}`); console.log('Registering with Rust Companion API...'); await axios.post('https://companion-rust.facepunch.com:443/api/push/register', { AuthToken: authToken, DeviceId: 'tearust', PushKind: 3, PushToken: expoPushToken, }).catch(err => { console.error('Companion API registration failed:', err.message); process.exit(1); }); saveConfig({ fcm_credentials: fcmCreds, expo_push_token: expoPushToken, rustplus_auth_token: authToken }); console.log(`\nConfig saved: ${configFile}`); console.log('Done! Drag rustplus.config.json into TEARust to finish pairing.'); } process.on('SIGTERM', async () => { await ChromeLauncher.killAll(); server?.close(); }); process.on('SIGINT', async () => { await ChromeLauncher.killAll(); server?.close(); process.exit(0); }); main().catch(err => { console.error(err); process.exit(1); });