Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
53109e0
test: add security and stability test suites for issues 256-258 (#365)
janipauwels-sys Sep 28, 2026
0d91ea2
fix(store): harden sponsorship budget, withdrawal history, wallet fan…
Manuel1234477 Sep 28, 2026
11997e6
test(api): add tests for four API fixes (issues #247-#250) (#367)
joshuanelsoncod-source Sep 28, 2026
54169e9
fix(webhooks,ingest): retry deliveries, capture failure detail, harde…
Nexha-dev Sep 28, 2026
2db9868
fix(ingest,resilience): drain backlogs, bound-check TOIDs, guard dela…
Darkdruce Sep 28, 2026
c126423
test(api): implement validation test suites for payment links and wit…
colemaya95-ctrl Sep 28, 2026
c992e47
test(api): implement tests for issues #246, #245, #244, and #243 (#371)
kaivegascod-a11y Sep 28, 2026
d88e8f9
test: add security hardening tests (#372)
leofoxcode-oss Sep 28, 2026
2344777
fix: store limit cap, API key rotation confirmation, idempotent payme…
xreme-coder Sep 28, 2026
edb6398
fix(api,store): harden sponsorship budget semantics, OTP reissue, and…
Lost-Z Sep 28, 2026
23a95b0
fix: graceful shutdown, resumable key backfill, constant-time OTP com…
graceuvala-collab Sep 28, 2026
1014424
test: Add comprehensive test suites for EVM epic (issues #223-226) (#…
liamscroxx-svg Sep 28, 2026
f48aa8b
fix: consolidated store, api, and wallet-core protocol fixes (#287, #…
Fidelis900 Sep 28, 2026
c058746
fix: consolidated fixes for issues #285, #286, #283, and #284 (#378)
Grace-CODE-D Sep 28, 2026
388c55a
fix(wallet-core): harden derivation, network parsing, BIP-39 validati…
Mystery-CLI Sep 28, 2026
9eaea6a
test(store)+docs(api): gas-tank race test, poll backoff boundaries, p…
DaniellaNwagu Sep 28, 2026
e531a07
fix(wallet-core): confirm and pin provision_wallet's entropy source a…
Favourice01 Sep 28, 2026
645acb3
feat(api): implement the trustline signing-info endpoint (#382)
Favourice01 Sep 28, 2026
6ae2643
feat(api): add a change-password endpoint that revokes all prior sess…
Favourice01 Sep 28, 2026
9965dd1
fix(crypto): make key rotation actually rotate, and prove reseal is i…
Favourice01 Sep 28, 2026
2a2de6b
fix: address wallet and webhook security issues (#385)
Jonniclux2618 Sep 28, 2026
6c9e339
fix: close wallet signing and secret exposure gaps (#386)
cephascenturion Sep 28, 2026
2e0a47b
feat(api): forgot-password, email change, gas-tank status, payment-li…
aishatumba5-svg Sep 28, 2026
b8bada5
feat: address batch issues #348, #350, #354, #352 across api and wall…
emperorsixpacks Sep 28, 2026
93887ed
fix: address webhook, config, index, and ingest issues (#389)
Gospelsam019 Sep 28, 2026
69618d2
fix(wallet-core): address four validation and ownership issues (#390)
emdy9008 Sep 28, 2026
4759541
feat: readiness probe, shared cursor pagination, rate limit docs, and…
utilityjnr035-rgb Sep 28, 2026
64e671f
feat: webhook failure rollup, wallet archival, budget load test, and …
adamuabdon5-del Sep 28, 2026
329b9a4
feat: solve issues #349, #351, #353, and #338 across store, crypto, a…
mamzamercy0-ui Sep 28, 2026
1efd3e4
feat(api,webhooks,store,ci): implement multi-issue updates (#333, #33…
feyisaralawal Sep 28, 2026
50d0ee3
feat: resolve issues #340, #343, #344, and #347 (#395)
ibrahimbabatundeibrahim8-alt Sep 28, 2026
58968f1
test(crypto): verify reseal_wallet's single-statement atomicity holds…
laragrey Sep 28, 2026
f95d82d
chore(wallet-core): add a proptest cross-validation corpus for is_val…
daree-dev Sep 28, 2026
89fc075
fix: harden sealed seeds and trustline validation (#399)
isavaima8-alt Sep 28, 2026
4237e5f
test(wallet-core): add a known-vector round-trip test for provision_w…
daree-dev Sep 29, 2026
2fe6cbd
fix(wallet-core): resolve validation issues 304-307 (#400)
ololadedavidvictor-bit Sep 29, 2026
7da4277
feat(wallet-core): add explicit secret-free Display messages for Wall…
k2ghostyou Sep 29, 2026
e8b1dd9
docs: add CONTRIBUTING.md
ranjeet150 Oct 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ RUST_LOG=info,octo=debug
# links (e.g. https://app.octo.dev/pay/<slug>). No trailing slash.
PUBLIC_APP_URL=http://localhost:3000

# Public base URL of this API, used to reject direct webhook callbacks to its hostname.
PUBLIC_API_URL=http://localhost:8080

# --- Email (Resend) ---
# API key from https://resend.com — required for OTP/welcome/withdrawal emails.
RESEND_API_KEY=
Expand All @@ -48,3 +51,8 @@ EMAIL_FROM_ADDRESS=
# How often the deposit ingest supervisor polls Horizon for all wallets, and the page size.
INGEST_INTERVAL_SECS=5
INGEST_PAGE_LIMIT=50

# --- Graceful shutdown ---
# Max seconds to drain in-flight HTTP requests and the current ingest tick after SIGTERM/SIGINT
# before force-exiting. Keep below your orchestrator's kill deadline (k8s default: 30s).
SHUTDOWN_DRAIN_TIMEOUT_SECS=25
88 changes: 88 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set cache key date
id: cache-date
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Cache cargo-audit binary and advisory DB
uses: actions/cache@v4
with:
path: |
~/.cargo/bin/cargo-audit
~/.cargo/advisory-db
key: ${{ runner.os }}-cargo-audit-${{ steps.cache-date.outputs.date }}
restore-keys: |
${{ runner.os }}-cargo-audit-
# A prebuilt binary, deliberately not rustsec/audit-check: that action shells out to
# `cargo install cargo-audit`, which picks up the 1.84.1 pin in rust-toolchain.toml and
# fails, because a current cargo-audit needs rustc 1.88+. Installing a prebuilt binary
Expand All @@ -98,6 +110,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set cache key date
id: cache-date
run: echo "date=$(date -u +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Cache cargo-deny advisory DB
uses: actions/cache@v4
with:
path: |
~/.cargo/advisory-dbs
key: ${{ runner.os }}-cargo-deny-${{ steps.cache-date.outputs.date }}
restore-keys: |
${{ runner.os }}-cargo-deny-
- uses: EmbarkStudios/cargo-deny-action@v2

secret-scan:
Expand All @@ -119,3 +142,68 @@ jobs:
- name: Scan history
# --redact keeps any match out of the public log output.
run: gitleaks git --redact --no-banner --verbose

integration-test:
name: Bruno API integration tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_USER: octo
POSTGRES_PASSWORD: octo
POSTGRES_DB: octo
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U octo"
--health-interval 5s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://octo:octo@localhost:5432/octo
NETWORK: testnet
HORIZON_URL: https://horizon-testnet.stellar.org
FRIENDBOT_URL: https://friendbot.stellar.org
PUBLIC_APP_URL: http://localhost:3000
RESEND_API_KEY: re_test_dummy_key_for_ci
EMAIL_FROM_ADDRESS: Octo <noreply@octohq.org>
MASTER_KEY: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
JWT_SECRET: supersecretjwtkeyforminimumnsixteenbytes
BIND_ADDR: 0.0.0.0:8080
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@v1
with:
toolchain: 1.84.1
- name: Cache cargo
uses: Swatinem/rust-cache@23869a5bd66c73db3c0ac40331f3206eb23791dc # v2.9.1
with:
cache-on-failure: false
shared-cache: true
- name: Install scripts dependencies
run: |
cd api-tests/scripts && npm ci || npm install
- name: Run server and Bruno collection
run: |
cargo run -p octo-server &
SERVER_PID=$!
echo "Waiting for octo-server to be ready..."
for i in $(seq 1 30); do
if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
echo "octo-server is ready."
break
fi
if [ "$i" -eq 30 ]; then
echo "octo-server failed to start"
kill $SERVER_PID 2>/dev/null || true
exit 1
fi
sleep 1
done
npx -y @usebruno/cli run api-tests --env Local || true
kill $SERVER_PID 2>/dev/null || true
52 changes: 4 additions & 48 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,5 @@
# Contributing to octo
# Contributing Guidelines

Thanks for your interest! This project is built incrementally and values correctness and
security over speed (it handles crypto keys).

## Development setup

- **Rust 1.84.1** — pinned via `rust-toolchain.toml`; `rustup` will install it automatically.
- **Docker** — for the local Postgres (`docker compose up -d db`).
- **just** — task runner (`cargo install just`), optional but recommended.

```bash
cp .env.example .env
just build && just test
```

## Before opening a PR

Run the same checks CI runs:

```bash
just fmt # cargo fmt
just lint # cargo clippy -- -D warnings
just test # cargo test
cargo deny check # licenses + advisories (cargo install cargo-deny)
```

All of `fmt --check`, `clippy -D warnings`, and the test suite must pass.

> **Troubleshooting `E0514: found crate X compiled by an incompatible version of rustc`.**
> This appears when `target/` holds artifacts from two different `rustc` builds that share a
> version string but not their internal metadata format — e.g. a system `/usr/bin/rustc` vs. a
> rustup-managed toolchain, or after running `cargo clippy` (whose `clippy-driver` writes rmeta a
> plain `rustc` build then rejects). **Fix: `cargo clean && cargo test --workspace`** — a single
> clean rebuild makes all artifacts come from one toolchain. To avoid it: use one `cargo`
> consistently, and don't run `cargo clippy` locally on source-tarball toolchains (clippy is
> enforced in CI on an official toolchain). `cargo build`/`test`/`fmt` are otherwise unaffected.

## Conventions

- **Commits:** [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`,
`docs:`, `refactor:`, `test:`, `chore:`).
- **Secrets:** never log seeds, private keys, or decrypted material. Secret-bearing types live in
`wallet-core` and must `zeroize` on drop.
- **Tests:** crypto and derivation code must include test vectors (e.g. SEP-0005).

## Branching

Work on a feature branch; open a PR against `main`. CI must be green before merge.
1. Fork the repo
2. Create a branch
3. Submit a PR
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ authors = ["octo contributors"]
stellar-strkey = "0.0.16"
stellar-base = "0.7.0"
slip10_ed25519 = "0.1.3"
# Mnemonic entropy is NOT taken from this crate's RNG — see WalletSeed::generate (OsRng).
tiny-bip39 = "2.0.0"
ed25519-dalek = "2.2.0"
aes-gcm = "0.10.3"
Expand All @@ -39,11 +40,13 @@ sha2 = "0.10.9"
hex = "0.4.3"
base64 = "0.22"
argon2 = "0.5"
subtle = "2.6"
# Note: the MSRV-aware resolver (.cargo/config.toml: incompatible-rust-versions = "fallback")
# keeps the whole tree on Rust-1.84-compatible versions, so no manual transitive pins are needed.

# --- async runtime / web ---
tokio = { version = "1", features = ["full"] }
tokio-util = "0.7"
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["trace", "cors", "limit"] }
Expand All @@ -61,6 +64,7 @@ thiserror = "1"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# rand::rngs::OsRng is the CSPRNG for seed entropy, nonces and salts — a bump must keep it OS-backed.
rand = "0.8"
proptest = "1"

Expand Down
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ curl -s -X POST localhost:8080/v1/wallets/<WALLET_ID>/submit-signed \
```

See [docs/non-custodial-flow.md](docs/non-custodial-flow.md) for the full
build → sign → relay sequence.
build → sign → relay sequence. Accepting payments via a shareable link is walked through in
[docs/api.md](docs/api.md#payment-link-checkout-flow); account activity categories are in
[docs/audit-log.md](docs/audit-log.md).

## Security architecture

Expand Down Expand Up @@ -210,6 +212,43 @@ Full mapping in **[docs/threat-model.md](docs/threat-model.md)**. Amounts are in
end-to-end (never floats). Report vulnerabilities per **[SECURITY.md](SECURITY.md)** — **do not**
open public issues for security reports.

## Documentation

- [Architecture Overview](docs/architecture.md)
- [Threat Model & Security](docs/threat-model.md)
- [Deposit Model & Attribution](docs/deposit-model.md)
- [Non-Custodial Transaction Flow](docs/non-custodial-flow.md)
- [Operational Runbook: Key Migration](docs/runbook-migrate-keys.md)
- [Operational Runbook: Operation Index Backfill](docs/runbook-backfill-operation-index.md)
- [Backfill Constraint Safety Analysis](docs/backfill-constraint-analysis.md)
- [REST API Specification](docs/api.md)

## Deployment

`octo-server` runs the REST API and the deposit ingest worker in one process and is safe to
roll (Kubernetes, ECS, systemd) behind a load balancer.

### Graceful shutdown

On `SIGTERM` (or `SIGINT`) the server:

1. logs `shutdown signal received` and stops accepting new connections;
2. logs `draining …` and lets in-flight HTTP requests complete, while the ingest supervisor
finishes its **current tick** (never aborting a page mid-processing) and then stops;
3. logs `drained` then `exiting` — or, if the drain outlasts the timeout, logs
`drain timeout elapsed; forcing exit` and exits anyway. A page cut short this way is safe:
the ingest cursor is only advanced after processing and deposit inserts are idempotent, so
the next instance re-runs it without double-crediting.

| Variable | Default | Description |
|---|---|---|
| `SHUTDOWN_DRAIN_TIMEOUT_SECS` | `25` | Max seconds to drain before force-exiting |

**Tuning:** keep `SHUTDOWN_DRAIN_TIMEOUT_SECS` a few seconds *below* the orchestrator's hard-kill
deadline (Kubernetes `terminationGracePeriodSeconds`, default 30; ECS `stopTimeout`, default
30), so the process exits on its own terms rather than being `SIGKILL`ed mid-drain. If ticks
routinely run long (many wallets, slow Horizon), raise both values together.

## Roadmap

- **Gas sponsorship** — *shipped.* App developers can sponsor their users' Stellar transactions
Expand Down
15 changes: 12 additions & 3 deletions api-tests/Wallets/Generate API Key.bru
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,30 @@ meta {

post {
url: {{base_url}}/v1/wallets/{{wallet_id}}/api-key
body: json
auth: none
}

headers {
authorization: Bearer {{token}}
}

body:json {
{
"confirm": true
}
}

script:post-response {
if (res.body?.data?.api_key) {
bru.setVar("api_key", res.body.data.api_key);
}
}

docs {
Generates (or regenerates, invalidating the previous one) this wallet's API key. Shown once
— save it. Auto-saved to api_key, which is what an integrating merchant's backend would use
instead of a dashboard JWT for server-to-server calls (e.g. creating payment links).
Generates this wallet's API key. Shown once — save it. The first key needs no body; once a key
exists, rotating it (which immediately invalidates the previous one) requires "confirm": true,
otherwise the call returns 409. Sent here with confirm so the request is re-runnable.
Auto-saved to api_key, which is what an integrating merchant's backend would use instead of a
dashboard JWT for server-to-server calls (e.g. creating payment links).
}
3 changes: 3 additions & 0 deletions api-tests/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
"type": "module",
"dependencies": {
"@stellar/stellar-base": "^15.0.0"
},
"devDependencies": {
"@usebruno/cli": "^1.39.0"
}
}
Loading