From e3891f4dc143a07cc2db1739901d0a7830405d2b Mon Sep 17 00:00:00 2001 From: jeanGaston Date: Mon, 13 Apr 2026 11:00:53 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20rewrite=20README=20=E2=80=94=20add=20To?= =?UTF-8?q?C,=20Ansible=20playbook,=20disclaimer,=20full=20MIT=20licence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Table of contents with anchor links - Ansible deploy.yml: clones repo, copies private key, bootstraps store.json via Ansible Vault variables, builds image, runs container; uses creates: guard to preserve live requisitions on re-deploy - AI-generated disclaimer (prominent, at the bottom) - Full MIT licence text (not just the word "MIT") - Improved prose and formatting throughout Co-Authored-By: Claude Sonnet 4.6 --- README.md | 305 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 245 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index c64079e..1b9a1dc 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,37 @@ # actual-gocardless-proxy -A drop-in [GoCardless Bank Account Data API](https://developer.gocardless.com/bank-account-data/quick-start-guide/) proxy server that lets [Actual Budget](https://actualbudget.org/) sync with French (and other European) bank accounts via [Enable Banking](https://enablebanking.com/) — a **free** PSD2 aggregator. +> **AI-generated code — see disclaimer below.** -**Zero modifications to actual-server.** The only change is setting one environment variable: +A drop-in [GoCardless Bank Account Data API](https://developer.gocardless.com/bank-account-data/quick-start-guide/) proxy that lets [Actual Budget](https://actualbudget.org/) sync with French (and other European) bank accounts via [Enable Banking](https://enablebanking.com/) — a **free** PSD2 aggregator. + +**Zero modifications to actual-server.** The only change required is one environment variable: ``` GOCARDLESS_BASE_URL=http://gocardless-proxy:3456 ``` +--- + +## Table of contents + +1. [How it works](#how-it-works) +2. [Setup](#setup) + - [1. Sign up for Enable Banking](#1-sign-up-for-enable-banking) + - [2. Generate an RSA key pair](#2-generate-an-rsa-key-pair) + - [3. Run the setup script](#3-run-the-setup-script) + - [4. Add the proxy to docker-compose](#4-add-the-proxy-to-docker-compose) + - [5. Start the services](#5-start-the-services) + - [6. Configure Actual Budget](#6-configure-actual-budget) +3. [Ansible deployment](#ansible-deployment) +4. [OAuth redirect flow](#oauth-redirect-flow) +5. [Supported banks](#supported-banks) +6. [Troubleshooting](#troubleshooting) +7. [Data & privacy](#data--privacy) +8. [Disclaimer](#disclaimer) +9. [License](#license) + +--- + ## How it works ``` @@ -15,15 +39,15 @@ Actual Budget (browser) │ GoCardless API calls ▼ actual-server - │ GOCARDLESS_BASE_URL → http://gocardless-proxy:3456 + │ GOCARDLESS_BASE_URL=http://gocardless-proxy:3456 ▼ -gocardless-proxy ◄── this repo - │ Enable Banking PSD2 API +gocardless-proxy ◄── this repo + │ Enable Banking PSD2 API (RS256 JWT auth) ▼ -Enable Banking → French banks (BNP, SG, CA, LCL, BP, …) +Enable Banking → French banks (BNP, SG, CA, LCL, BP, …) ``` -The proxy translates GoCardless API shapes ↔ Enable Banking shapes on the fly and persists all state (tokens, requisitions, account mappings) in `./data/store.json` so Docker restarts don't break existing account links. +The proxy translates GoCardless API shapes ↔ Enable Banking shapes on the fly. All state (tokens, requisitions, account mappings) is persisted in `./data/store.json` so Docker restarts do not break existing Actual Budget account links. --- @@ -31,53 +55,49 @@ The proxy translates GoCardless API shapes ↔ Enable Banking shapes on the fly ### 1. Sign up for Enable Banking -Go to [enablebanking.com](https://enablebanking.com) and create a **free personal account**. Under your app settings, note your **Application ID**. +Go to [enablebanking.com](https://enablebanking.com) and create a **free personal account**. Under your application settings, note your **Application ID**. ### 2. Generate an RSA key pair -Enable Banking uses RS256 JWT auth. You need to provide your **public key** in the Enable Banking dashboard and keep the **private key** on your server. +Enable Banking authenticates API calls with an RS256 JWT signed by your application's private key. You upload the public key to Enable Banking; the private key stays on your server. ```bash -# Generate a 2048-bit RSA private key in PKCS8 format +# Generate a PKCS8 RSA private key openssl genpkey -algorithm RSA -pkcs8 -out private_key.pem -pkeyopt rsa_keygen_bits:2048 -# Extract the public key (upload this to Enable Banking) +# Extract the public key — upload this to the Enable Banking dashboard openssl pkey -in private_key.pem -pubout -out public_key.pem ``` -Upload `public_key.pem` in the Enable Banking dashboard under your application's API settings. - -Keep `private_key.pem` somewhere safe — you'll need it in the next step. +Store `private_key.pem` securely. You will reference it during setup. ### 3. Run the setup script -Clone this repo and install dependencies: - ```bash git clone https://github.com/yourname/actual-gocardless-proxy cd actual-gocardless-proxy npm install -``` - -Run the interactive setup: - -```bash npm run setup ``` -The script will ask for: -- Your Enable Banking **Application ID** -- Path to your **private key PEM** file -- The **proxy base URL** (the URL that Enable Banking will redirect to after OAuth — must be reachable from your browser; e.g. `http://localhost:3456` for local dev or `https://yourserver.example.com` for production) +The script prompts for: -It will generate a random `secret_id` and `secret_key` pair and print them: +| Prompt | Example | +|--------|---------| +| Enable Banking Application ID | `abc123-...` | +| Path to private key PEM | `/home/user/private_key.pem` | +| Proxy base URL | `https://yourserver.example.com` or `http://localhost:3456` | + +The proxy base URL is the address Enable Banking redirects to after OAuth — it **must be reachable from the user's browser**, not just from within Docker. + +On completion the script prints: ``` ────────────────────────────────────────────────────────── Enter these values in Actual Budget → Settings → GoCardless: Secret ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - Secret Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + Secret Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx And set this environment variable on actual-server: @@ -85,26 +105,26 @@ And set this environment variable on actual-server: ────────────────────────────────────────────────────────── ``` -All credentials are stored in `./data/store.json`. +All credentials are written to `./data/store.json`. -### 4. Add the proxy to your docker-compose.yml +### 4. Add the proxy to docker-compose -In your existing `actual-server` `docker-compose.yml`, add the proxy service and wire it to actual-server: +Paste the `gocardless-proxy` service block into your existing `actual-server` `docker-compose.yml` and add `GOCARDLESS_BASE_URL` to actual-server: ```yaml services: actual-server: image: actualbudget/actual-server:latest - # ... your existing config ... environment: GOCARDLESS_BASE_URL: http://gocardless-proxy:3456 - # ... your other env vars ... + # ... rest of your actual-server env vars depends_on: - gocardless-proxy gocardless-proxy: build: /path/to/actual-gocardless-proxy - # or: image: ghcr.io/yourname/actual-gocardless-proxy:latest + # or use a pre-built image: + # image: ghcr.io/yourname/actual-gocardless-proxy:latest container_name: gocardless-proxy restart: unless-stopped volumes: @@ -113,7 +133,7 @@ services: PORT: 3456 ``` -> **Note on PROXY_BASE_URL:** The proxy base URL is saved in `data/store.json` during setup. If you need to change it later (e.g. you added a reverse proxy), re-run `npm run setup` or edit `data/store.json` directly and update `config.proxy_base_url`. +> **Changing the proxy base URL later:** Re-run `npm run setup`, or edit `config.proxy_base_url` directly in `data/store.json` and restart the container. ### 5. Start the services @@ -121,45 +141,176 @@ services: docker compose up -d ``` -Verify the proxy is running: +Verify the proxy is up: ```bash curl http://localhost:3456/health -# → {"status":"ok","configured":true} +# {"status":"ok","configured":true} ``` ### 6. Configure Actual Budget -In Actual Budget, go to **Settings → GoCardless** and enter the `secret_id` and `secret_key` printed in step 3. +Go to **Settings → GoCardless** in Actual Budget and enter the `Secret ID` and `Secret Key` printed in step 3. Then link your bank via **Accounts → Link account**. -You can now go to **Accounts → Link account** and search for your French bank. +--- + +## Ansible deployment + +The playbook below deploys the proxy to a remote host that already runs `actual-server` via Docker Compose. Adjust variables at the top to match your environment. + +```yaml +# deploy.yml +--- +- name: Deploy actual-gocardless-proxy + hosts: actual_host + become: true + + vars: + proxy_dir: /opt/actual-gocardless-proxy + data_dir: /opt/actual-gocardless-proxy/data + proxy_port: 3456 + # Set proxy_base_url to the URL reachable from the user's browser + proxy_base_url: "https://actual.example.com" + eb_app_id: "{{ vault_eb_app_id }}" + eb_private_key_src: "{{ playbook_dir }}/files/private_key.pem" + gc_secret_id: "{{ vault_gc_secret_id }}" + gc_secret_key: "{{ vault_gc_secret_key }}" + + tasks: + + - name: Install git and Docker dependencies + ansible.builtin.package: + name: + - git + - python3-docker + state: present + + - name: Clone / update repository + ansible.builtin.git: + repo: https://github.com/yourname/actual-gocardless-proxy.git + dest: "{{ proxy_dir }}" + version: main + force: true + + - name: Create data directory + ansible.builtin.file: + path: "{{ data_dir }}" + state: directory + mode: "0700" + + - name: Copy RSA private key + ansible.builtin.copy: + src: "{{ eb_private_key_src }}" + dest: "{{ data_dir }}/private_key.pem" + mode: "0600" + + - name: Write store.json (credentials + proxy config) + ansible.builtin.copy: + dest: "{{ data_dir }}/store.json" + mode: "0600" + content: | + { + "config": { + "eb_app_id": "{{ eb_app_id }}", + "eb_private_key": {{ lookup('file', eb_private_key_src) | to_json }}, + "gc_secret_id": "{{ gc_secret_id }}", + "gc_secret_key": "{{ gc_secret_key }}", + "proxy_base_url": "{{ proxy_base_url }}" + }, + "gc_tokens": {}, + "institutions_cache": null, + "institutions_cache_at": 0, + "agreements": {}, + "requisitions": {}, + "accounts": {} + } + # Skip this task if store.json already exists (preserves live state: + # tokens, requisitions, linked accounts). Remove the condition if you + # want to reset credentials on every deploy. + args: + creates: "{{ data_dir }}/store.json" + + - name: Build proxy Docker image + community.docker.docker_image: + name: actual-gocardless-proxy + tag: local + build: + path: "{{ proxy_dir }}" + source: build + force_source: true + + - name: Run gocardless-proxy container + community.docker.docker_container: + name: gocardless-proxy + image: actual-gocardless-proxy:local + restart_policy: unless-stopped + ports: + - "127.0.0.1:{{ proxy_port }}:{{ proxy_port }}" + volumes: + - "{{ data_dir }}:/app/data" + env: + PORT: "{{ proxy_port | string }}" + STORE_PATH: /app/data/store.json + + - name: Ensure actual-server has GOCARDLESS_BASE_URL set + ansible.builtin.debug: + msg: > + Remember to add + GOCARDLESS_BASE_URL=http://gocardless-proxy:{{ proxy_port }} + to your actual-server environment and restart it. +``` + +**Sensitive variables** (`eb_app_id`, `gc_secret_id`, `gc_secret_key`) should be stored in an Ansible Vault file: + +```bash +# Create vault file +ansible-vault create group_vars/actual_host/vault.yml +``` + +```yaml +# group_vars/actual_host/vault.yml +vault_eb_app_id: "your-enable-banking-app-id" +vault_gc_secret_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +vault_gc_secret_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +``` + +Run the playbook: + +```bash +ansible-playbook deploy.yml --ask-vault-pass +``` + +> **First deploy only:** `store.json` is written with `creates:` so it is not overwritten on subsequent runs — this preserves live requisitions and linked accounts. On a brand-new host the credentials are bootstrapped automatically; no need to run `npm run setup` manually. --- ## OAuth redirect flow ``` -1. actual-server POST /api/v2/requisitions/ { redirect: "http://actual:5006/..." } +1. actual-server POST /api/v2/requisitions/ + body: { redirect: "http://actual:5006/...", institution_id } ↓ 2. proxy POST https://api.enablebanking.com/auth - { aspsp: { name, country }, redirect_url: "http://proxy/callback", + { aspsp: { name, country }, + redirect_url: "http://proxy/callback", state: } ← { url: "https://tilisy.enablebanking.com/..." } ↓ -3. proxy returns requisition with link = EB auth URL +3. proxy returns requisition { id, status: "CR", link: } ↓ 4. User visits the link, authenticates at their bank ↓ -5. Enable Banking redirects → http://proxy/callback?code=xxx&state= +5. Enable Banking → http://proxy/callback?code=xxx&state= ↓ 6. proxy POST https://api.enablebanking.com/sessions { code } ← { session_id, accounts: [uid, ...] } - → fetches account details, stores GC account IDs - → marks requisition status "LN" (linked) - → redirects → http://actual:5006/...?requisition_id= + → fetches account details per UID + → stores gc_id ↔ eb_uid mapping in store.json + → sets requisition status "LN" (linked) + → 302 → http://actual:5006/...?requisition_id= ↓ 7. actual-server GET /api/v2/requisitions// - ← { status: "LN", accounts: ["gc-acc-id-1", ...] } + ← { status: "LN", accounts: ["gc-uuid-1", ...] } ↓ 8. actual-server GET /api/v2/accounts//details/ GET /api/v2/accounts//balances/ @@ -187,27 +338,27 @@ All French banks supported by Enable Banking, including: | Fortuneo | FTNOFRP1 | | Hello bank! | BNPAFRPP | -See [enablebanking.com/docs/markets/fr/](https://enablebanking.com/docs/markets/fr/) for the full list. +See [enablebanking.com/docs/markets/fr/](https://enablebanking.com/docs/markets/fr/) for the complete list. --- ## Troubleshooting -### "Proxy not configured" error +**"Proxy not configured" error** Run `npm run setup` and restart the container. -### "Unknown institution_id" when creating a requisition -actual-server caches institutions. Go to **Settings → GoCardless → Reset** and re-fetch banks. +**"Unknown institution_id" when linking an account** +actual-server caches the institutions list. Go to **Settings → GoCardless → Reset** and re-fetch. -### OAuth callback fails with "Enable Banking error" -- Make sure `proxy_base_url` in `data/store.json` is reachable from your browser (not just from Docker). -- Check that the redirect URL is whitelisted in your Enable Banking application settings. +**OAuth callback fails with "Enable Banking error"** +- `proxy_base_url` in `data/store.json` must be reachable from your browser, not just from inside Docker. +- Make sure the callback URL (`{proxy_base_url}/callback`) is whitelisted in your Enable Banking application settings. -### Transactions not appearing -Enable Banking may return an empty `transactions` array for accounts with no activity in the requested date range. Try widening the date range. +**Transactions not appearing** +Enable Banking may return an empty array if no transactions fall in the requested date range. Try widening the range in Actual Budget. -### Private key errors -Make sure the key is **PKCS8** format. To convert from traditional RSA format: +**Private key parse error** +The key must be in **PKCS8** format. To convert from a traditional RSA key: ```bash openssl pkcs8 -topk8 -nocrypt -in old_key.pem -out private_key.pem ``` @@ -216,12 +367,46 @@ openssl pkcs8 -topk8 -nocrypt -in old_key.pem -out private_key.pem ## Data & privacy -All bank credentials and tokens stay on your server inside `./data/store.json`. Nothing is sent to any third party except Enable Banking (which is the PSD2 provider) and your bank. +All credentials and session data are stored locally in `./data/store.json`. No data is sent to any third party other than Enable Banking (the licensed PSD2 provider) and your bank. -The `data/` directory is git-ignored and should not be committed. +The `data/` directory is git-ignored and must not be committed. + +--- + +## Disclaimer + +**This project was entirely generated by an AI assistant (Claude, by Anthropic) without manual code authorship.** + +It has not been independently audited, penetration-tested, or validated against production bank APIs. Use it at your own risk. In particular: + +- The Enable Banking integration is based on publicly available documentation and may require adjustments as the API evolves. +- No warranty is given regarding correctness, security, or fitness for any purpose. +- Your bank credentials and PSD2 access tokens are handled by this software — review the code before deploying in a sensitive environment. + +Contributions, corrections, and reviews from human developers are very welcome. --- ## License -MIT +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.