From b4f174dcffa934debe628bb39cb8b3b6c538c155 Mon Sep 17 00:00:00 2001 From: jeanGaston Date: Mon, 13 Apr 2026 10:34:23 +0200 Subject: [PATCH] feat: interactive setup script Prompts for EB app_id and RSA private key path, validates the key with jose, generates GC secret_id/secret_key, writes to data/store.json, and prints the values to enter in Actual Budget's GoCardless settings. Co-Authored-By: Claude Sonnet 4.6 --- src/setup.js | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/setup.js diff --git a/src/setup.js b/src/setup.js new file mode 100644 index 0000000..3bf812b --- /dev/null +++ b/src/setup.js @@ -0,0 +1,126 @@ +#!/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); +});