671c00d0d8
Build Kvitt images / build (push) Failing after 8m17s
Contract (SPEC §14.8): migration 0004_card_purchases.sql (server schema 4, byte-identical contract and server copies), E-36 to E-44 in contracts/openapi.yaml with the four new error codes and a 502 response, examples for every card body and error code, the match_card_purchase vectors (17 cases) with their Python reference in tools/contracts-check.py (157 checks), web/src/api/schema.d.ts regenerated; web/src/lib/vectors.test.ts lists match_card_purchase as not ported until K3 ports it (orchestrator's allowance). Server: src/cards/ with the provider registry (CP-1), the CardSource trait and its Enable Banking adapter (JWT RS256 kid = application id, /aspsps, /auth, /sessions, transactions with PSU headers for user-present fetches, CP-14 error mapping, CP-7 mapping), the windowed fetch (30-day windows newest first, split at the 100-row cap down to a day, de-duplication by provider_ref, continuation keys, WRONG_TRANSACTIONS_PERIOD ends the walk), CP-8 matching with the secondary rule, the background poller (06:45, 11:45, 16:45, 21:45, 4 per 24 hours, 2-hour skip, no backlog), the bilingual callback page (E-39), E-36 to E-38 and E-40 to E-44, link clearing in sync push step 7, attempt pruning at 04:00, the card_fetch log line with aggregates only, KVITT_EB_* configuration. Tests against a local stub (tests/cards_api/, 34 tests); the real API is never called. Infra: Pi compose mounts secrets/cards read-only and passes the two application ids; .env.example keys; dev stack stages infra/secrets/cards/; NPM block with the /cards/callback gate line and unlogged location (kvitt.conf and README step 4); README "Card purchases". Pre-commit hook bypassed (--no-verify, orchestrator's decision): the Android fast tests fail in :domain VectorFilesTest (and the full check in :data ExampleRoundTripTest) because they pin the contract's vector and example sets; matchCardPurchase and the card DTOs are K2's by design (contract-first landing; K2 lands next). Server, contracts-check and web gates are green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
74 lines
4.6 KiB
Plaintext
74 lines
4.6 KiB
Plaintext
# Kvitt (SPEC §10.5.2, D-120, D-126, D-130). NPM, Proxy Host <KVITT_PUBLIC_HOST>, Advanced tab, Custom Nginx Configuration.
|
|
# App gate (SPEC §8.5, D-126): outside the home LAN only a request carrying the key built into the app passes;
|
|
# everything else gets the connection closed (444: nothing sent, nothing to fingerprint). Put the key from
|
|
# android/local.properties (kvitt.appKey) in place of KVITT_APP_KEY. To rotate, add a second if-line with the new key.
|
|
# The LAN check reads $realip_remote_addr, the address that really connected: NPM replaces $remote_addr with the
|
|
# request's own X-Real-IP header for private peers, so $remote_addr could be forged. Let's Encrypt's renewal passes.
|
|
set $kvitt_gate 0;
|
|
if ($http_x_kvitt_app = "KVITT_APP_KEY") { set $kvitt_gate 1; }
|
|
if ($realip_remote_addr ~ "^192\.168\.1\.") { set $kvitt_gate 1; }
|
|
if ($uri ~ "^/\.well-known/acme-challenge/") { set $kvitt_gate 1; }
|
|
# Web client (SPEC §15, D-130): its files under /app/, the invite accept it logs in with, and any request carrying a
|
|
# bearer token (the server checks the token; unknown tokens are rate-limited per IP), except /health, /setup/, /i/
|
|
# and /apk/, which stay LAN-or-key only.
|
|
if ($uri ~ "^/app(/|$)") { set $kvitt_gate 1; }
|
|
if ($uri = "/invites/accept") { set $kvitt_gate 1; }
|
|
# Card connection (SPEC §9.12, D-163): the bank redirects the user's browser here, without the app key.
|
|
if ($uri = "/cards/callback") { set $kvitt_gate 1; }
|
|
set $kvitt_bearer 0;
|
|
if ($http_authorization ~ "^Bearer ") { set $kvitt_bearer 1; }
|
|
if ($uri ~ "^/(health|setup/|i/|apk/)") { set $kvitt_bearer 0; }
|
|
if ($kvitt_bearer = 1) { set $kvitt_gate 1; }
|
|
if ($kvitt_gate = 0) { return 444; }
|
|
# Server body: 1 MiB, PUT /me/picture 5 MiB (SPEC §8.6); the server answers anything larger with its own 413.
|
|
client_max_body_size 6m;
|
|
# No long-poll in the API (SPEC §14.3); 60 s between two reads from the server.
|
|
proxy_read_timeout 60s;
|
|
# Response headers (D-117). They live at server level, so no location below may use add_header (nginx would then
|
|
# drop all of these in it); NPM's HSTS line (SSL tab) sits at the same level. The server's own copy is hidden, so
|
|
# each header appears once (the landing page sends Referrer-Policy itself).
|
|
proxy_hide_header Content-Security-Policy;
|
|
proxy_hide_header X-Content-Type-Options;
|
|
proxy_hide_header X-Frame-Options;
|
|
proxy_hide_header Referrer-Policy;
|
|
# The web client under /app/ (D-132) needs its own scripts, styles, images and API calls: one variable, still one
|
|
# server-level add_header.
|
|
set $kvitt_csp "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
|
|
if ($uri ~ "^/app(/|$)") { set $kvitt_csp "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:; font-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"; }
|
|
add_header Content-Security-Policy $kvitt_csp always;
|
|
add_header X-Content-Type-Options nosniff always;
|
|
add_header X-Frame-Options DENY always;
|
|
add_header Referrer-Policy no-referrer always;
|
|
# Replaces NPM's own "location /" (NPM leaves it out when this field has one), whose add_header X-Served-By would
|
|
# drop the headers above. $forward_scheme, $server and $port are NPM's Details tab values.
|
|
location / {
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_pass $forward_scheme://$server:$port$request_uri;
|
|
}
|
|
# Invite links /i/<code>?from=<name>: neither the code nor the inviter's name may reach a log (SPEC §8.7). NPM's
|
|
# access log format writes the whole $request_uri, query string included, so this path is not logged at all, and
|
|
# its error log (which would carry the request line on a 502) goes nowhere.
|
|
location ^~ /i/ {
|
|
access_log off;
|
|
error_log /dev/null crit;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_pass $forward_scheme://$server:$port$request_uri;
|
|
}
|
|
# Card connection callback /cards/callback?code=…&state=… (SPEC §9.12, D-163): the query carries a one-time
|
|
# authorisation code and the attempt's state, so this path is not logged at all, a 502 included.
|
|
location = /cards/callback {
|
|
access_log off;
|
|
error_log /dev/null crit;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_pass $forward_scheme://$server:$port$request_uri;
|
|
}
|