Contributing
Prerequisites
Section titled “Prerequisites”- Rust: Install a stable toolchain with rustup. Continuous integration (CI) uses the
stabletoolchain and the Docker image builds withrust:1.99. The workspace does not specify a minimum supported Rust version (MSRV). - cargo-nextest: Use this to run tests. Install with
cargo install cargo-nextest. - cargo-lambda: Install this if you need to build Lambda binaries. Install with
cargo install cargo-lambda. - Docker: Install this to run DynamoDB Local integration tests.
- jj (Jujutsu): Use this for 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), a Git-compatible version control system. It records working-copy changes automatically and has no staging area.
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 edits become 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 separates business logic from infrastructure through interfaces called ports. This lets tests replace infrastructure with mock implementations:
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 with mocks. They do not use the network or 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/) start an axum router with mock adapters and send 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.
The crate dependencies keep core logic separate from infrastructure.
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 adapters and 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 specific requirements, such as Apple, add a module in
crates/providers/src/implementingIdentityProvider. - Add an adapter name and register it in provider construction in
crates/server/src/bootstrap.rs.
Code standards
Section titled “Code standards”Writing guidelines
Section titled “Writing guidelines”These guidelines apply to documentation, code comments, commit and pull request messages, and replies to users.
- Write clear, complete sentences. Keep the detail proportional to the task.
- Remove filler, repetition, and unrelated details.
- Follow the plain language principles of ISO 24495-1:2023. Use short sentences with one idea each. Define terms on first use.
- Do not use em dashes or teasers such as “Here’s the thing” or “But there’s a catch”.
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 queried the database before checking expiry.Repeated requests with expired tokens added unnecessary load.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.