retire the bunq integration
All checks were successful
Deploy / build-deploy (push) Successful in 3m32s
All checks were successful
Deploy / build-deploy (push) Successful in 3m32s
The webhook is deregistered at bunq and the callback endpoint, its parser, default-deny classifier, dedup ledger and signature check are removed; the code is in git history if a bank feed ever comes back. /financials keeps reading the hand-maintained aggregates file, and sales + shop donations stay live from the order ledger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4c49000d46
commit
c31797bd9a
13 changed files with 70 additions and 1694 deletions
|
|
@ -76,10 +76,10 @@ PaymentRails gRails;
|
|||
std::string gRedirectBase = "https://catcrafts.net";
|
||||
|
||||
// The bank-derived aggregates for /financials live in Catcrafts.Server-
|
||||
// Financials.cpp, which owns their file and the bunq callback that updates
|
||||
// them. They are read through CurrentFinancials() per request rather than
|
||||
// cached: unlike the content files they CAN change under a running process,
|
||||
// and live is the page's whole promise.
|
||||
// Financials.cpp, which owns their file. They are read through
|
||||
// CurrentFinancials() per request rather than cached: unlike the content
|
||||
// files they CAN change under a running process, and live is the page's
|
||||
// whole promise.
|
||||
|
||||
// The reconciler's sweep cadence: the shortest interval any configured rail
|
||||
// asks for. Each order is still paced by ITS OWN rail's interval inside the
|
||||
|
|
@ -98,12 +98,6 @@ std::chrono::seconds SweepInterval() {
|
|||
return out;
|
||||
}
|
||||
|
||||
// The callback URL's fixed prefix; everything after it is the shared secret.
|
||||
// Under /api because Caddy proxies that prefix straight through and the
|
||||
// analytics ingest censors it out of the public report (deploy/README.md) —
|
||||
// a URL carrying a secret must not end up on a page anyone can read.
|
||||
inline constexpr std::string_view kBunqCallbackPrefix = "/api/bunq/";
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& p) {
|
||||
std::ifstream in(p, std::ios::binary);
|
||||
if (!in) return {};
|
||||
|
|
@ -1132,48 +1126,6 @@ int Serve(std::uint16_t port) {
|
|||
};
|
||||
|
||||
auto fallback = [](const HTTPRequest& req) -> HTTPResponse {
|
||||
// The bunq mutation callback. Handled here rather than through
|
||||
// ParseRoute because the path carries a SECRET — the shared route
|
||||
// table is compiled into the wasm bundle that ships to every browser,
|
||||
// and a secret has no business being in it.
|
||||
//
|
||||
// Everything unauthorised answers 404, never 401: the endpoint should
|
||||
// not confirm its own existence to a prober, exactly as an unknown
|
||||
// order token does not confirm the shape of a real one.
|
||||
if (const std::string_view path = PathWithoutQueryHTTP(req.path);
|
||||
path.starts_with(kBunqCallbackPrefix)) {
|
||||
HTTPResponse res;
|
||||
res.headers["content-type"] = "text/plain; charset=utf-8";
|
||||
res.headers["cache-control"] = "no-store";
|
||||
res.headers["x-robots-tag"] = "noindex, nofollow";
|
||||
const std::string_view secret = path.substr(kBunqCallbackPrefix.size());
|
||||
if (!BunqCallbackConfigured() || req.method != "POST"
|
||||
|| req.body.size() > Form::kMaxBodyBytes) {
|
||||
res.status = "404";
|
||||
res.body = "Not found\n";
|
||||
return res;
|
||||
}
|
||||
std::string_view signature;
|
||||
if (const auto h = req.headers.find("x-bunq-server-signature");
|
||||
h != req.headers.end()) {
|
||||
signature = h->second;
|
||||
}
|
||||
if (!BunqCallbackAuthorised(secret, req.body, signature)) {
|
||||
res.status = "404";
|
||||
res.body = "Not found\n";
|
||||
return res;
|
||||
}
|
||||
// 200 for everything the endpoint understood, including a
|
||||
// withheld or duplicate mutation: those are correct outcomes, and
|
||||
// a non-2xx would make bunq redeliver a callback that was already
|
||||
// handled exactly as intended. Only a failed WRITE earns a 500,
|
||||
// because a retry of that genuinely could succeed.
|
||||
const BunqIngestResult result = IngestBunqNotification(req.body);
|
||||
res.status = result == BunqIngestResult::Failed ? "500" : "200";
|
||||
res.body = result == BunqIngestResult::Failed ? "Could not record\n" : "OK\n";
|
||||
return res;
|
||||
}
|
||||
|
||||
// A POST to a product page is a checkout submission.
|
||||
if (req.method == "POST") {
|
||||
const Route route = ParseRoute(PathWithoutQueryHTTP(req.path));
|
||||
|
|
|
|||
Loading…
Reference in a new issue