actual-gocardless-proxy/src/setup.js
jeanGaston b4f174dcff 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 <noreply@anthropic.com>
2026-04-13 10:34:23 +02:00

127 lines
4.6 KiB
JavaScript

#!/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);
});