Contributing
Prerequisites
Section titled “Prerequisites”- Rust: stable toolchain. CI builds and tests on stable rustc 1.98; no MSRV is pinned in the workspace. Install via rustup.
- cargo-nextest: test runner. Install with
cargo install cargo-nextest. - cargo-lambda: required only for building Lambda binaries. Install with
cargo install cargo-lambda. - Docker: required for DynamoDB Local integration tests.
- jj (Jujutsu): version control. Install from martinvonz/jj. This project uses jj exclusively; do not use git CLI commands.
Clone and build
Section titled “Clone and build”jj git clone <repo-url> oidc-exchangecd oidc-exchangecargo buildAny editor with rust-analyzer support works. The workspace root Cargo.toml defines shared dependency versions.
Version control with jj
Section titled “Version control with jj”This project uses Jujutsu (jj) for version control. jj is a Git-compatible VCS with a simpler mental model: there is no staging area, and every working-copy state is automatically committed.
Common workflows
Section titled “Common workflows”# See statusjj status
# Describe the current changejj describe -m "add domain allowlist validation"
# Create a new empty change on top of the current onejj new
# View the logjj log
# Push to remotejj git pushBookmarks (branching)
Section titled “Bookmarks (branching)”jj uses bookmarks instead of branches:
# Create a bookmarkjj bookmark create my-feature
# Move a bookmark to the current changejj bookmark set my-feature
# Push a bookmarkjj git push --bookmark my-featureKey differences from git
Section titled “Key differences from git”- No staging area: all file changes are part of the current change automatically.
- Immutable commits:
jj describe,jj squash, andjj rebasecreate new commit IDs. This is safe; jj tracks the rewrite. - Conflict markers in files: jj allows conflicted states to exist in the working copy. Resolve conflicts, then
jj statusconfirms resolution. jj newinstead ofgit commit: when your current change is ready, runjj newto start a fresh change on top of it.
Testing
Section titled “Testing”Test runner
Section titled “Test runner”All tests run through cargo-nextest, configured in .config/nextest.toml.
# Run the full test suitecargo nextest run --workspace
# Run tests for a specific cratecargo nextest run -p oidc-exchange-corecargo nextest run -p oidc-exchange-adapterscargo nextest run -p oidc-exchange # server crate
# Run a single test by namecargo nextest run --workspace -E 'test(exchange_valid_code)'
# Use the CI profile (stricter: 2 retries, fail-fast)cargo nextest run --workspace --profile ciIntegration tests
Section titled “Integration tests”Some adapter tests require external services and are marked #[ignore]. To run them:
# Start DynamoDB Localdocker run -d -p 8000:8000 amazon/dynamodb-local
# Run ignored testscargo nextest run -p oidc-exchange-adapters -- --ignoredTest architecture
Section titled “Test architecture”The codebase uses hexagonal architecture to make testing straightforward:
crates/test-utils/: provides mock implementations of all port traits (MockRepository,MockKeyManager,MockAuditLog,MockRateLimiter,MockIdentityProvider,MockUserSync). These are in-memory implementations used by core service tests and server E2E tests.- Core tests (
crates/core/tests/): test business logic in isolation using mocks. No network, no filesystem. - Adapter tests (
crates/adapters/tests/): test infrastructure integrations. HTTP-based adapters use wiremock for deterministic HTTP mocking. DynamoDB tests require DynamoDB Local. - Server E2E tests (
crates/server/tests/): spin up a full axum router with mock adapters and issue real HTTP requests.
Writing tests
Section titled “Writing tests”- Place unit tests in the module they test (standard Rust
#[cfg(test)]blocks). - Place integration tests in the crate’s
tests/directory. - Use the mock implementations from
test-utils; do not duplicate mock logic. - Tests that need external services must be
#[ignore]so the defaultcargo nextest runworks without Docker.
Code organization
Section titled “Code organization”Crate structure
Section titled “Crate structure”| Crate | Package name | Purpose |
|---|---|---|
crates/core |
oidc-exchange-core |
Domain types, port traits, service logic. Zero infrastructure dependencies. |
crates/adapters |
oidc-exchange-adapters |
Implementations of port traits for DynamoDB, KMS, SQS, OIDC, webhooks. |
crates/providers |
oidc-exchange-providers |
Non-standard identity provider modules (Apple). |
crates/server |
oidc-exchange |
HTTP layer (axum), middleware, telemetry, and the binary entrypoint. |
crates/ffi |
oidc-exchange-ffi |
Language-agnostic request/response FFI wrapper over the server router. Reused by the Node.js and Python bindings. |
crates/test-utils |
oidc-exchange-test-utils |
Mock implementations of all ports. Dev-dependency only. |
Dependency rules
Section titled “Dependency rules”coredepends on nothing infrastructure-specific (no AWS SDKs, no HTTP clients).adaptersandprovidersdepend oncorefor trait definitions.serverdepends oncore,adapters, andproviders.ffidepends onserver(router construction) andcore; the Node.js and Python bindings depend onffi.test-utilsdepends only oncore.
These boundaries are enforced by the Cargo workspace. If core compiles, the domain logic is free of infrastructure coupling.
Adding a new adapter
Section titled “Adding a new adapter”- Define the implementation in
crates/adapters/src/. - Implement the relevant port trait from
crates/core/src/ports/. - Add a builder function (e.g.,
from_config()) that constructs the adapter from the TOML config. - Wire it into the adapter selection in
crates/server/src/bootstrap.rs. - Add tests: use wiremock for HTTP-based adapters, Docker services for database adapters.
Adding a new identity provider
Section titled “Adding a new identity provider”- If the provider follows standard OIDC, it only needs a config entry, with no code required.
- If the provider has quirks (like Apple), add a module in
crates/providers/src/implementingIdentityProvider. - Add an adapter name and wire it into provider construction in
crates/server/src/bootstrap.rs.
Code standards
Section titled “Code standards”Formatting and linting
Section titled “Formatting and linting”# Formatcargo fmt --all
# Lintcargo clippy --workspace --all-targetsBoth must pass with zero warnings before pushing.
Error handling
Section titled “Error handling”- Use
thiserrorfor error enums. All domain errors are incrates/core/src/error.rs. - Return domain errors from service methods. The server crate maps these to HTTP responses.
- Do not use
.unwrap()outside of tests.
Configuration
Section titled “Configuration”- New config fields go in
crates/core/src/config.rsas strongly-typed structs with serde. - All secrets use
${VAR_NAME}placeholder syntax; never hardcode secrets in TOML defaults. - New config sections need a corresponding entry in
config/default.toml.
Commit messages
Section titled “Commit messages”Write concise commit messages that describe what changed and why. Use jj describe to set the current change’s description.
fix: reject expired refresh tokens before database lookup
The previous flow hit the database before checking expiry,adding unnecessary load during token storms.Prefix with a type when it helps clarity: fix:, feat:, refactor:, test:, docs:, chore:.
Running the full stack locally
Section titled “Running the full stack locally”-
Start DynamoDB Local:
Terminal window docker run -d -p 8000:8000 amazon/dynamodb-local -
Generate a local signing key:
Terminal window openssl genpkey -algorithm ed25519 -out keys/dev.pem -
Create
config/local.tomlwithadapter = "local"for key manager and DynamoDB pointed athttp://localhost:8000. -
Run:
Terminal window OIDC_EXCHANGE_ENV=local cargo run
The server starts on http://localhost:8080. Use the /health endpoint to verify it is running.