name: Payment domain, storage, platform and UI on: push: paths: - 'crates/nigig-pay-domain/**' - 'crates/nigig-pay-storage/**' - 'crates/nigig-pay-platform/**' - 'crates/robius-ussd/**' - 'crates/apps/nigig-pay-ui/**' - 'tools/makepad-native-libs.sh' - 'tools/test-rust-clean.sh' - 'tools/test-mpesa-store-clean.sh' - 'crates/nigig-core/src/persistence/mpesa/**' - 'rust-toolchain.toml' - 'deny.toml' - '.forgejo/workflows/pay-domain.yml' pull_request: paths: - 'crates/nigig-pay-domain/**' - 'crates/nigig-pay-storage/**' - 'crates/nigig-pay-platform/**' - 'crates/robius-ussd/**' - 'crates/apps/nigig-pay-ui/**' - 'tools/makepad-native-libs.sh' - 'tools/test-rust-clean.sh' - 'tools/test-mpesa-store-clean.sh' - 'crates/nigig-core/src/persistence/mpesa/**' - 'rust-toolchain.toml' - 'deny.toml' - '.forgejo/workflows/pay-domain.yml' jobs: isolated-payment-tests: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 # Each step installs its own throwaway toolchain at the version declared # in rust-toolchain.toml, runs tests with --locked against the checked-in # lockfile, then checks formatting, Clippy (-D warnings) and benchmarks. - name: Domain tests, fmt, clippy and benches run: TEST_TARGET=domain ./tools/test-rust-clean.sh # The storage target additionally exercises the opt-in SQLCipher # encryption-at-rest path. - name: Storage tests, fmt, clippy and SQLCipher run: TEST_TARGET=storage ./tools/test-rust-clean.sh # The platform crate is the seam between the domain and Android. This # target additionally compiles the optional USSD adapter for the host # and asserts that the mock gateway cannot be built into a release. - name: Platform gateway tests, fmt, clippy and mock-in-release guard run: TEST_TARGET=platform ./tools/test-rust-clean.sh # nigig-core is not a workspace member (it needs git-hosted Makepad and # Robius crates), but its parser/classifier/store logic is pure and can # be verified standalone. This covers defects B1 and B4. - name: M-Pesa parser, classifier and store tests run: ./tools/test-mpesa-store-clean.sh - name: Reject whitespace errors run: git diff --check # Build governance: lockfiles must be present and current. A lockfile # that changes during CI means the committed one was stale. - name: Lockfiles must be committed and unchanged run: | set -e for crate in nigig-pay-domain nigig-pay-storage nigig-pay-platform; do test -f "crates/$crate/Cargo.lock" \ || { echo "missing crates/$crate/Cargo.lock"; exit 1; } done git diff --exit-code -- '**/Cargo.lock' # Dependency audit, licence and source checks (review item 1.2). # Scoped to the crates whose dependency graph fully resolves standalone. - name: Dependency audit, licences, bans and sources run: | set -e version=0.18.6 url="https://github.com/EmbarkStudios/cargo-deny/releases/download" curl -sSLf -o /tmp/cargo-deny.tar.gz \ "$url/$version/cargo-deny-$version-x86_64-unknown-linux-musl.tar.gz" tar xzf /tmp/cargo-deny.tar.gz -C /tmp install -m 0755 \ "/tmp/cargo-deny-$version-x86_64-unknown-linux-musl/cargo-deny" \ /usr/local/bin/cargo-deny for crate in nigig-pay-domain nigig-pay-storage nigig-pay-platform; do echo "== cargo-deny: $crate ==" cargo-deny --manifest-path "crates/$crate/Cargo.toml" \ --config deny.toml check done # Boundary enforcement (review item 1.4): the payment domain and storage # crates must never depend on the UI framework. This checks real # dependencies and imports, not prose in doc comments. - name: Payment crates must not depend on Makepad run: | set -e for crate in nigig-pay-domain nigig-pay-storage nigig-pay-platform; do if grep -n -i 'makepad' "crates/$crate/Cargo.toml"; then echo "Makepad dependency declared in $crate" exit 1 fi if grep -rn --include='*.rs' -E '^[[:space:]]*(use|extern crate)[[:space:]]+makepad' \ "crates/$crate"; then echo "Makepad imported in $crate" exit 1 fi done echo 'no Makepad dependency in payment domain, storage or platform' # Review item 5.1: JNI and platform SDK calls belong in the platform # crate's adapters. The domain and storage crates must stay portable. - name: Domain and storage must not reach the platform SDK run: | set -e for crate in nigig-pay-domain nigig-pay-storage; do if grep -rn --include='*.rs' -E '^[[:space:]]*use[[:space:]]+(jni|robius_ussd|robius_sms|robius_fingerprinting)' \ "crates/$crate"; then echo "platform SDK imported in $crate" exit 1 fi done echo 'no platform SDK imports in payment domain or storage' # Review item 5.4 / 0.5. The guard is a compile_error!, so the check is # that the build genuinely fails. A passing build here means a release # artifact could fabricate payment confirmations. - name: Mock gateway must not compile into a release build run: | set -e if cargo build --manifest-path crates/nigig-pay-platform/Cargo.toml \ --locked --release --features mock >/dev/null 2>&1; then echo 'the mock gateway compiled into a release build' exit 1 fi echo 'mock gateway is correctly refused in release builds' # Review item 7.3: the payment UI crate's tests must run in CI rather than # being skipped. They were previously unrunnable here because the Makepad # link step needs native libraries; `tools/makepad-native-libs.sh` now # installs exactly the set required (including ALSA/PulseAudio/OpenSSL, # which are needed to *link* a test binary even though `cargo check` # succeeds without them). payment-ui-tests: runs-on: ubuntu-latest timeout-minutes: 45 steps: - uses: actions/checkout@v4 - name: Install Makepad native libraries run: ./tools/makepad-native-libs.sh --install - name: Install the declared toolchain run: | set -e version="$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' \ rust-toolchain.toml | head -n 1)" curl --fail --location --proto '=https' --tlsv1.2 https://sh.rustup.rs -o /tmp/rustup-init chmod 700 /tmp/rustup-init /tmp/rustup-init -y --profile minimal --default-toolchain "$version" --no-modify-path echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" # The pure payment crates are gated far more strictly elsewhere in this # workflow. Here the goal is that the widget-layer logic — fee # breakdowns, CSV parsing, money formatting — is actually exercised. - name: Payment UI unit tests run: cargo test -p nigig-pay-ui --lib # Review item 6.1 / Phase 6 exit criterion. A settlement tick may only # follow a provider confirmation, so the payment status vocabulary is # owned by `nigig_pay_domain::FlowStage` and must not be hand-written # beside an untrusted signal. This catches the specific regression that # shipped before: "✓ SMS received" and "✓ N/N sent". - name: Payment status must not claim settlement from untrusted signals run: | set -e sheet=crates/apps/nigig-pay-ui/src/shared_pay_sheet.rs if grep -n '✓.*\(SMS\|sent\|Sent\|received\)' "$sheet"; then echo 'a settlement tick is being claimed from an untrusted signal' exit 1 fi for marker in ObservedEvidence NeedsReconciliation; do if grep -n "PayFlowEvent::$marker" -A 6 "$sheet" | grep -q '✓'; then echo "PayFlowEvent::$marker renders a settlement tick" exit 1 fi done echo 'no settlement claims from untrusted signals' # Review items 6.6 / U7. Batch progress must be derived from # `PaymentBatch`, which records one outcome per item, rather than from a # bare counter incremented at each match arm. The unbounded version let # a single item with two terminal USSD events report "3/2". - name: Batch progress must not be an unbounded counter run: | set -e handler=crates/apps/nigig-pay-ui/src/pay_flow/pay_flow_handler.rs if grep -n '^[^/]*bulk_done[[:space:]]*+=' "$handler"; then echo 'bulk_done is being incremented directly; use settle_bulk_item()' exit 1 fi echo 'batch progress is derived, not counted' # Review defect S2, the fail-open biometric. On Android # `robius_fingerprinting::authenticate` returns Ok(()) as soon as the # prompt is *shown*, not when the user answers. Dispatch must therefore # be gated on an AuthorizationAttempt grant, never on that early Ok. - name: USSD dispatch must be gated on an authorization grant run: | set -e handler=crates/apps/nigig-pay-ui/src/pay_flow/pay_flow_handler.rs if ! grep -q 'attempt.consume()' "$handler"; then echo 'dispatch_ussd no longer consumes an authorization grant' exit 1 fi if ! grep -q 'AuthorizationSignal::Succeeded' "$handler"; then echo 'no explicit success signal feeds the authorization attempt' exit 1 fi echo 'dispatch is gated on an explicit authorization grant' # The demo gate must be reachable by the obvious command. Without a # forwarding feature on the app crates, `cargo run -p nigig-pay # --features demo` fails with "does not contain this feature", and the # only way in is the non-obvious `--features nigig-pay-ui/demo`. That # made the intended tracker-only message look like a broken build. - name: USSD automation must be on by default and switchable off run: | set -e for app in nigig-pay nigig-mpesa; do grep -q 'default = \["demo"\]' "crates/apps/$app/Cargo.toml" \ || { echo "$app no longer enables demo by default"; exit 1; } done grep -q 'default = \["native", "demo"\]' crates/pageflipnav/Cargo.toml \ || { echo "the APK crate no longer enables demo by default"; exit 1; } # A packaging build must still be able to turn it off. Without # `default-features = false` on the inner deps this silently # re-enables itself, which is how the first attempt at this leaked. for app in nigig-pay nigig-mpesa; do grep -q 'nigig-pay-ui = { path = "../nigig-pay-ui", default-features = false }' \ "crates/apps/$app/Cargo.toml" \ || { echo "$app would re-enable demo under --no-default-features"; exit 1; } done cargo check -p nigig-pay cargo check -p nigig-pay --no-default-features echo 'automation on by default; packaging opt-out intact' # A gated feature must explain the product boundary, not the binary. # "unavailable in this build" was reported as a bug three times: it # reads like something is broken and gives the user no next step. # Checks displayed strings only, so the explanatory comments stay legal. - name: Gated features must not report themselves as build errors run: | set -e if grep -rn --include='*.rs' 'set_status(.*unavailable in this build' crates/; then echo 'a user-facing string describes the build instead of the product' exit 1 fi echo 'gated features explain the product boundary' # Review item 0.3: remove custom PIN capture from the default product. # `form_pin` must not exist unless `demo` is enabled, so a stale UI # definition, a hot reload, or a merely-hidden widget cannot populate # it. Asserted by compiling a reference to the field and requiring the # default build to reject it. # USSD automation is now on by default (the app's primary function), # so the meaningful assertion is that a *packaging* build can still # produce a tracker with no PIN capture at all. - name: A packaging build must not contain a PIN field run: ./tools/check-no-pin-capture.sh # pageflipnav is the crate that produces the shipped APK. If it does # not forward `demo`, the flag is unreachable in a real build: the Pay # sheet reports dispatch unavailable and there is no way to turn the # *334# automation on. That shipped, and was only found by a user. - name: The APK crate must be able to reach the demo flag run: | set -e grep -q 'demo = \["nigig-pay/demo", "nigig-mpesa/demo"\]' \ crates/pageflipnav/Cargo.toml \ || { echo "pageflipnav does not forward the demo feature"; exit 1; } echo 'the APK crate can reach the demo flag' # Review items 1.2 / Q2. The three pure payment crates already deny # unwrap_used; nigig-pay-ui did not, and the one production `.unwrap()` # it held sat on the payment dispatch path. The ratchet is in # lib.rs; this makes CI enforce it. # # Scoped with `--no-deps`: `matrix_client` and `robius-ussd` carry # pre-existing warnings that are not this crate's to fix, and failing on # them would make the gate someone else's problem and get it disabled. - name: Payment UI quality ratchet run: cargo clippy -p nigig-pay-ui --all-targets --no-deps -- -D warnings # Review item S7. The exchange client forged `origin`/`referer` for # api2.bybit.com and p2p.binance.com — impersonating those exchanges' # own web clients against internal endpoints. Both copies of the client # are fixed; this stops either regrowing. - name: Exchange clients must not impersonate a browser run: | set -e if grep -rn --include='*.rs' 'set_header("origin"\|set_header("referer"' crates/; then echo 'an exchange client is forging browser identity headers' exit 1 fi if grep -rn --include='*.rs' 'Robrix/1.0 Makepad' crates/ | grep -v '^\S*: *///'; then echo 'a request advertises the UI framework in its User-Agent' exit 1 fi # S8: TLS verification must never be weakened, and every dial site # must go through the host allowlist. if grep -rn --include='*.rs' 'set_ignore_ssl_cert(' crates/; then echo 'a request disables TLS certificate verification' exit 1 fi for f in crates/apps/nigig-pay/src/payments_frame/pages/mpesa/exchange/api.rs \ crates/apps/nigig-mpesa/src/pages/exchange/api.rs; do grep -q 'fn check_transport' "$f" \ || { echo "$f lost its transport allowlist"; exit 1; } done echo 'no browser impersonation; TLS intact; allowlist present' # Phase 4's draw_walk fixes live in these two crates. Compiling them is # what makes that work verifiable rather than merely written. - name: Payment app crates must compile run: | set -e cargo check -p nigig-pay-ui cargo check -p nigig-pay cargo check -p nigig-mpesa