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>
This commit is contained in:
parent
57a808c054
commit
b4f174dcff
126
src/setup.js
Normal file
126
src/setup.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user