Skip to content

feat: wallet-ownership proof, rate limiting, per-intent payment addresses, and direct image upload - #203

Merged
Emmyt24 merged 1 commit into
mainfrom
feat/tier1-uploads-and-payment-intent-addresses
Aug 2, 2026
Merged

feat: wallet-ownership proof, rate limiting, per-intent payment addresses, and direct image upload#203
Emmyt24 merged 1 commit into
mainfrom
feat/tier1-uploads-and-payment-intent-addresses

Conversation

@Emmyt24

@Emmyt24 Emmyt24 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

This is the Tier 1 security-hardening work plus the Cloudinary image-upload feature — it had been sitting uncommitted in the local working directory (pre-dating the Render-deploy compile fixes already merged into main), which is why GET /v1/uploads/signature was 404ing in production: the route existed only locally and had never reached main.

  • Wallet-ownership proof: POST /v1/wallets now requires a signed ownership challenge (GET /v1/wallets/challenge) alongside public_key — registering a wallet requires proving control of the private key, not just knowing a public address. octo_wallet_core::verify_account_signature added for ed25519 verification.
  • Rate limiting: crates/api/src/rate_limit.rs, applied to signup, login, and payment-link intent creation.
  • Exact payment-intent matching: migration 0015 gives each payment intent its own deposit address (previously shared per-link), so concurrent payers on the same link can no longer be cross-matched. Ingest tries the exact per-intent address first, falls back to the old oldest-pending-by-link-address path for pre-migration intents, and now rejects underpayment (stays pending instead of confirming for less than owed).
  • Direct-to-Cloudinary image upload: crates/api/src/routes/uploads.rs signs the upload request server-side; the API never touches file bytes. This is the route the frontend's payment-link image upload depends on.
  • Migration 0016: ingest last-polled tracking.
  • New shared test helpers (crates/api/tests/common/) plus coverage for the challenge flow, rate limiting, and concurrent per-intent address allocation.

Rebase notes

This was rebased onto current main (which had drifted 42 commits since this work started). Resolved conflicts against main's own Render-deploy fixes by keeping both sides' intent — e.g. Claims.jti, verify_hs256 visibility, and the custodial withdraw/trustline 410 tombstones all came from main; the per-intent address matching and challenge-signing logic came from this branch. A few test helpers (authz_matrix_tests.rs, malformed_body_tests.rs) built wallet-creation requests without the new challenge/signature fields — pre-dating this feature reaching those files — and needed updating to use the new shared common::wallet_body helper.

Test plan

  • cargo test --workspace --locked — all green (37 test binaries, 0 failures)
  • cargo clippy --workspace --all-targets --locked -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • cargo build --release -p octo-server — succeeds

…sses, and direct image upload

Tier 1 security hardening plus the Cloudinary upload feature, rebased
onto current main (this work predated main's Render-deploy compile
fixes and had been sitting uncommitted locally):

- Wallet-ownership proof: POST /v1/wallets now requires a signed
  ownership challenge (GET /v1/wallets/challenge) alongside
  public_key, so registering a wallet requires proving control of the
  private key rather than just knowing a public address.
  octo_wallet_core::verify_account_signature added for the ed25519
  verification; verify_hs256 widened to pub(crate) for the challenge
  HMAC check from routes/wallets.rs.
- Rate limiting: crates/api/src/rate_limit.rs, applied to signup,
  login, and payment-link intent creation.
- Exact payment-intent matching: migration 0015 gives each payment
  intent its own deposit address (previously shared per-link), so
  concurrent payers on the same link can no longer be cross-matched.
  Ingest's confirm_payment_link tries the exact per-intent address
  first, falling back to the old oldest-pending-by-link-address path
  for pre-migration intents, and now rejects underpayment.
  crates/store: pending_payment_by_address, get_payment_link.
- Direct-to-Cloudinary image upload: crates/api/src/routes/uploads.rs
  signs the upload request; the API never touches file bytes.
- migration 0016: ingest last-polled tracking.
- Test coverage: crates/api/tests/common/ (shared signed_challenge /
  wallet_body helpers), plus new tests for the challenge flow, rate
  limiting, and concurrent per-intent address allocation.

Rebase notes: resolved conflicts against main's own Render-deploy
fixes (Claims.jti, verify_hs256 visibility, the custodial withdraw/
trustline 410 tombstones) by keeping both sides' intent. Several test
helpers across authz_matrix_tests.rs and malformed_body_tests.rs
still built wallet-creation requests without the new challenge/
signature fields (pre-dating this feature in those files) and needed
updating to the challenge-signing helper to match.

Verified: cargo test/clippy/fmt --workspace all clean; a release
build of the server binary succeeds.
@Emmyt24
Emmyt24 merged commit 0fca038 into main Aug 2, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant