This commit is contained in:
parent
94a995d814
commit
aaa7a8ce99
10 changed files with 543 additions and 8 deletions
|
|
@ -351,6 +351,38 @@ BUNQ_API_KEY=… TRANSFER_IBAN=NL.. \
|
|||
# -> pulled 2 new credit(s) into /var/lib/catcrafts/orders.jsonl.transfer-credits.jsonl
|
||||
```
|
||||
|
||||
**The key is IP-restricted, and that decides the shape.** Verified the hard way
|
||||
on 2026-08-20: the bunq API key permits one address (the owner's home
|
||||
connection), and **bunq enforces that on every request, not only at device
|
||||
registration**. Registering the server's address in the device's
|
||||
`permitted_ips` does not help — the device registered fine from home with both
|
||||
addresses listed, and every read from the server still came back
|
||||
`"Incorrect API key or IP address"`. So `BUNQ_API_KEY` on the web host cannot
|
||||
work unless the key's allowlist is widened in the bunq app.
|
||||
|
||||
That constraint happens to enforce the right design, so it is now the supported
|
||||
one: **`tools/pull-and-ship-credits.sh`**, run by a systemd USER timer on the
|
||||
machine bunq permits (`deploy/catcrafts-credits.{service,timer}`, install
|
||||
instructions in the service file). It pulls, keeps an accumulating local copy,
|
||||
and ships **incoming credits only** to the file the rail reads. Outgoing lines
|
||||
are supplier payments and card spending: the matcher ignores negative amounts,
|
||||
so shipping them would put the business's outgoing payment history on a
|
||||
public-facing host for no settlement benefit. The script refuses to overwrite
|
||||
the remote with an empty file, so an upstream parse failure leaves the server
|
||||
settling from the last good copy rather than from nothing.
|
||||
|
||||
Latency, so nobody wonders: five minutes for the timer plus the reconciler's own
|
||||
60 s cadence, so a fresh order confirms within about six minutes of the money
|
||||
landing. Orders more than two hours old back off to a ten-minute poll, so an
|
||||
older one can take that long.
|
||||
|
||||
Two operational notes. The timer only runs while that machine is awake, so
|
||||
`Persistent=true` makes it pull once on waking rather than silently skipping
|
||||
every window it missed. And if the home address changes, both the key's
|
||||
allowlist (in the bunq app) and settlement stop working until it is updated —
|
||||
the failure mode is silent from the shop's side, so the credits file's mtime is
|
||||
worth glancing at.
|
||||
|
||||
**Read this before deciding where to put the key.** A bunq API key can
|
||||
**initiate payments**, and bunq offers no read-only scope, so there is no such
|
||||
thing as a key that can only read. That gives two deployment shapes, and they
|
||||
|
|
|
|||
30
deploy/catcrafts-credits.service
Normal file
30
deploy/catcrafts-credits.service
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Read the bank and hand the incoming credits to the server.
|
||||
#
|
||||
# A USER unit, on the machine whose IP the bunq API key permits — NOT on the
|
||||
# web host. The web host cannot call bunq at all (the key is IP-restricted at
|
||||
# the key level and bunq enforces it on every request), and it should not be
|
||||
# able to: a bunq key can initiate payments, because bunq offers no read-only
|
||||
# scope. So the key stays here and the server only ever reads a file.
|
||||
#
|
||||
# Install:
|
||||
# mkdir -p ~/.config/systemd/user
|
||||
# cp deploy/catcrafts-credits.{service,timer} ~/.config/systemd/user/
|
||||
# systemctl --user daemon-reload
|
||||
# systemctl --user enable --now catcrafts-credits.timer
|
||||
# loginctl enable-linger "$USER" # so it runs when nobody is logged in
|
||||
#
|
||||
# Watch: journalctl --user -u catcrafts-credits -f
|
||||
[Unit]
|
||||
Description=Pull bunq credits and ship them to catcrafts.net
|
||||
Documentation=file:deploy/README.md
|
||||
# Pointless without a route to the bank or to the server.
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=%h/repos/catcrafts.net
|
||||
ExecStart=%h/repos/catcrafts.net/tools/pull-and-ship-credits.sh
|
||||
# A failed pull must not look like a successful one. The script already refuses
|
||||
# to overwrite the remote with an empty file, so a failure here leaves the
|
||||
# server settling from the last good copy rather than from nothing.
|
||||
SuccessExitStatus=0
|
||||
23
deploy/catcrafts-credits.timer
Normal file
23
deploy/catcrafts-credits.timer
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Every five minutes. Two things set the floor and the ceiling:
|
||||
#
|
||||
# * bunq rate-limits reads to roughly 3 GET per 3 seconds per method, so one
|
||||
# call per five minutes is nowhere near it. The cost of going faster is
|
||||
# nothing technical; it is just noise.
|
||||
# * a donor watching the order page wants confirmation while they still care.
|
||||
# Five minutes here plus the reconciler's own 60 s cadence means a fresh
|
||||
# order confirms within about six minutes of the money landing.
|
||||
#
|
||||
# Persistent=true so a laptop that was asleep pulls once on waking instead of
|
||||
# silently skipping every window it missed — which is exactly when a donation
|
||||
# would otherwise sit unacknowledged overnight.
|
||||
[Unit]
|
||||
Description=Pull bunq credits for catcrafts.net every 5 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=5min
|
||||
Persistent=true
|
||||
AccuracySec=30s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Loading…
Reference in a new issue