Task — engineering-spec@1

"Wise webhook HTTP host — POST /v1/webhooks/wise/{profile_id} + 24h key cache + WAL processor"

doneTASK-INV-012
module inv · class product · priority p0 · created 2026-07-26 · shipped null
depends on TASK-INV-004 · blocks none

TASK-INV-012: Wise webhook HTTP host

Summary

Wire the shipped cyberos-inv Wise library into an axum host: POST /v1/webhooks/wise/{profile_id} with 24h PEM cache + one rotation re-fetch on verify failure, in-memory WAL push for fast 200, and a background processor that accepts events into an in-memory append log (cash-app stubbed until TASK-INV-006).

Problem

TASK-INV-004 landed verify + parse + migrations as a library only. No HTTP route, no key cache, no WAL processor — Wise cannot deliver webhooks.

Proposed Solution

  1. public_key.rs: PublicKeySource trait + CachedPublicKeys (24h TTL); on verify fail → force_refresh once → retry verify (DEC-848).
  2. wal.rs: bounded queue of WiseWalItem { profile_id, event_id, event_type, body }.
  3. handler.rs: extract raw body + X-Signature-SHA256 → verify (with rotation) → parse → reject unknown type / stale / profile mismatch (200 for stale per DEC-844) → WAL push → empty 200.
  4. processor.rs: pop WAL → idempotent in-memory receipt set → state received; cash-app call is a documented stub (CashAppStub).
  5. Binary cyberos-inv: axum router, WISE_PUBLIC_KEY_PEM / fetch URL env for key source in dev; listen INV_LISTEN_ADDR default :7710.
  6. Tests: cache TTL/refresh, rotation retry accepts new key, handler happy path with generated RSA keypair, stale → 200 without WAL growth.

Alternatives Considered

Success Metrics

Scope

In scope

Out of scope / Non-Goals

Dependencies

AI Authorship Disclosure

Generated then reviewed against as-built inv crate + INV-004 deferred host list (2026-07-26).

Acceptance Criteria

  1. POST route exists at /v1/webhooks/wise/{profile_id}.
  2. Valid signature → 200 empty body; event recorded once.
  3. Invalid signature (no refresh help) → 401.
  4. Rotation retry — stale cache PEM fails, refreshed PEM succeeds → 200.
  5. Stale event → 200 without processor receipt.
  6. Unknown event_type → not processed as receipt (dead-letter or drop with test).
  7. Cash-app stub — processor does not call INV-006; documents stub.
  8. 24h cache — second fetch not called within TTL for same profile.

Verification

cd services
cargo test -p cyberos-inv -- --test-threads=1
bash .cyberos/cuo/gates/run-gates.sh

Failure Modes

FailureDetectionOutcomeRecovery
Bad sigverify err401Operator check key
Key fetch failsource err401 after retryRetry later
WAL overflowpush err503 or 200+dropRaise capacity
Staleis_stale200 no process
Dup eventHashSet200 no reprocess
Unknown typeparse Nonedead-letterOps
Profile mismatchcheck401/200 policySpec: treat as bad
Cash-app missingstubreceived onlyINV-006
Body too largelimit 1MiB413Client
Panic in processorcatch/logevent retriedDLQ later

End of TASK-INV-012.

Audit

§1 — Verdict summary

Wise HTTP host residual over INV-004 library. 8 ACs, 10 failure modes; cash-app and Postgres persist honestly Out of scope.

§2 — Findings (all resolved)

ISS-001 — Network in CI

Resolved: PublicKeySource trait + static PEM tests.

ISS-002 — Cash-app blocked on INV-006

Resolved: CashAppStub; AC #7.

ISS-003 — Stale must 200

Resolved: AC #5 + DEC-844.

ISS-004 — Rotation once only

Resolved: AC #4 force_refresh once.

ISS-005 — JWT on webhook

Resolved: no JWT; signature auth only.

ISS-006 — Persist vs in-memory

Resolved: Out of scope Postgres; in-memory receipt set for host-c.

§3 — Resolution

All 6 mechanical concerns addressed. Score = 10/10.


End of TASK-INV-012 audit.