#!/usr/bin/env node /** * One-time setup script. * * Usage: * node src/setup.js * * What it does: * 1. Prompts for Enable Banking app_id and path to RSA private key (.pem) * 2. Validates the key can be loaded * 3. Generates a random GC secret_id / secret_key pair * 4. Writes everything to ./data/store.json * 5. Prints the secret_id/key to enter in Actual Budget */ import { createInterface } from 'readline'; import { readFileSync, mkdirSync, existsSync } from 'fs'; import { importPKCS8 } from 'jose'; import { v4 as uuidv4 } from 'uuid'; import { get as getStore, update, save } from './store.js'; const rl = createInterface({ input: process.stdin, output: process.stdout }); const ask = (q) => new Promise((res) => rl.question(q, res)); function randomToken() { return uuidv4().replace(/-/g, '') + uuidv4().replace(/-/g, ''); } async function main() { console.log('\n=== Actual Budget ↔ GoCardless Proxy — Setup ===\n'); const store = getStore(); // ── Enable Banking credentials ────────────────────────────────────────────── const defaultAppId = store.config?.eb_app_id ?? ''; const ebAppId = ( await ask(`Enable Banking App ID${defaultAppId ? ` [${defaultAppId}]` : ''}: `) ).trim() || defaultAppId; if (!ebAppId) { console.error('Error: App ID is required.'); process.exit(1); } let privateKeyPem = store.config?.eb_private_key ?? ''; const keyInput = (await ask('Path to RSA private key PEM file (leave blank to keep existing): ')).trim(); if (keyInput) { if (!existsSync(keyInput)) { console.error(`Error: File not found: ${keyInput}`); process.exit(1); } privateKeyPem = readFileSync(keyInput, 'utf8'); } if (!privateKeyPem) { console.error('Error: Private key is required.'); process.exit(1); } // Validate key try { await importPKCS8(privateKeyPem, 'RS256'); console.log('✓ Private key loaded and validated.'); } catch (e) { console.error(`Error: Could not parse private key: ${e.message}`); console.error('Make sure the PEM file contains a PKCS8-encoded RSA private key.'); console.error('To convert from a traditional RSA key: openssl pkcs8 -topk8 -nocrypt -in key.pem -out key_pkcs8.pem'); process.exit(1); } // ── GoCardless credentials (generated) ───────────────────────────────────── let gcSecretId = store.config?.gc_secret_id; let gcSecretKey = store.config?.gc_secret_key; if (gcSecretId && gcSecretKey) { const regen = (await ask('GoCardless credentials already exist. Regenerate? [y/N]: ')).trim().toLowerCase(); if (regen === 'y') { gcSecretId = uuidv4(); gcSecretKey = randomToken(); } } else { gcSecretId = uuidv4(); gcSecretKey = randomToken(); } // ── Proxy base URL (for OAuth callbacks) ─────────────────────────────────── const defaultProxyUrl = store.config?.proxy_base_url ?? 'http://gocardless-proxy:3456'; const proxyBaseUrl = ( await ask(`Proxy base URL (used in OAuth redirects) [${defaultProxyUrl}]: `) ).trim() || defaultProxyUrl; // ── Save ──────────────────────────────────────────────────────────────────── update((s) => { s.config = { eb_app_id: ebAppId, eb_private_key: privateKeyPem, gc_secret_id: gcSecretId, gc_secret_key: gcSecretKey, proxy_base_url: proxyBaseUrl, }; }); rl.close(); console.log('\n✓ Configuration saved to ./data/store.json\n'); console.log('──────────────────────────────────────────────────────────'); console.log('Enter these values in Actual Budget → Settings → GoCardless:'); console.log(''); console.log(` Secret ID: ${gcSecretId}`); console.log(` Secret Key: ${gcSecretKey}`); console.log(''); console.log('And set this environment variable on actual-server:'); console.log(''); console.log(` GOCARDLESS_BASE_URL=${proxyBaseUrl}`); console.log('──────────────────────────────────────────────────────────\n'); } main().catch((e) => { console.error(e); process.exit(1); });