Skip to content

Latest commit

 

History

History
456 lines (333 loc) · 20.3 KB

CONTRIBUTING.md

File metadata and controls

456 lines (333 loc) · 20.3 KB

Contributing to NyxID

Thank you for your interest in contributing to NyxID. This guide covers the development workflow, coding conventions, and quality expectations for the project.


Table of Contents


Code of Conduct

Be respectful, constructive, and inclusive. Harassment, discrimination, and bad-faith behavior are not tolerated. We are all here to build something useful together.


Getting Started

Prerequisites

Tool Version Purpose
Rust 1.85+ Backend and node-agent compiler
Node.js 20+ Frontend build tooling
MongoDB 8.0 Primary database
Docker 24+ Run MongoDB and Mailpit via Compose

Local Setup

# Clone the repository
git clone https://github.com/ChronoAIProject/NyxID.git
cd NyxID

# Copy environment template and generate an encryption key
cp .env.example .env
openssl rand -hex 32  # Paste output as ENCRYPTION_KEY in .env

# Start infrastructure (MongoDB on 27018, Mailpit on 8025)
docker compose up -d

# Start the backend (auto-generates RSA keys in dev mode)
cargo run

# In a separate terminal, start the frontend
cd frontend && npm install && npm run dev

The backend starts on http://localhost:3001, frontend on http://localhost:3000, and Mailpit web UI on http://localhost:8025.

Pre-Commit Hook

Install the pre-commit hook to catch formatting and lint issues before they reach CI:

ln -sf ../../scripts/pre-commit .git/hooks/pre-commit

The hook runs cargo fmt --check, cargo clippy, and eslint on staged files. It only checks the relevant tool when files of that type are staged.

Verify

curl http://localhost:3001/health
# {"status":"ok","version":"0.2.0"}

Development Workflow

  1. Fork and clone the repository
  2. Create a feature branch from dev: git checkout -b feature/your-feature dev
  3. Make your changes, following the conventions below
  4. Run tests and lint before committing (see Testing)
  5. Commit using conventional commit messages
  6. Push your branch and open a pull request against dev

Branch Naming

Prefix Use Case
feature/ New features
fix/ Bug fixes
refactor/ Code restructuring
docs/ Documentation only
test/ Adding or fixing tests
chore/ Tooling, CI, dependencies

Architecture Rules

NyxID follows a strict layered architecture. Understand these boundaries before contributing.

Backend Layers

handlers/ --> services/ --> models/
  • models/ -- Plain structs with serde derive macros and a COLLECTION_NAME constant. No business logic.
  • services/ -- Business logic. Takes &mongodb::Database and &str for IDs. No HTTP types.
  • handlers/ -- HTTP layer only. Converts AuthUser.user_id (Uuid) to String before calling services. Uses dedicated response structs -- never serialize model structs directly to API responses.
  • crypto/ -- Cryptographic operations (JWT, AES, password hashing, token generation).
  • mw/ -- Axum middleware (auth extraction, rate limiting, security headers).
  • errors/ -- Centralized error types (AppError enum with thiserror).

Adding a New Endpoint

  1. Define request/response types in handlers/<module>.rs
  2. Implement business logic in services/<module>.rs
  3. Register the route in routes.rs
  4. Add audit logging where appropriate

Backend Conventions

MongoDB Model Rules

These are critical -- incorrect serialization silently breaks data persistence:

  • NEVER use #[serde(skip_serializing)] on model fields -- it prevents insert_one(&struct) from storing them
  • ALWAYS use #[serde(with = "bson::serde_helpers::chrono_datetime_as_bson_datetime")] on DateTime<Utc> fields
  • For Option<DateTime<Utc>>, use the custom bson_datetime::optional helper (in models/bson_datetime.rs)
  • IDs are UUID v4 stored as strings in MongoDB _id fields
  • Each model must have a const COLLECTION_NAME: &str constant

Error Handling

Use the AppError enum and AppResult<T> type alias:

fn my_handler() -> AppResult<Json<MyResponse>> {
    // AppResult<T> = Result<T, AppError>
}

Error variants map to HTTP status codes and numeric error codes (1000-3002, 7000, 8000-8003). Internal and database errors must never leak details to clients.

Rust Style

  • Use thiserror for error types
  • Prefer &str over String in service function parameters
  • Keep handler functions thin -- delegate logic to services
  • All key material must use Zeroizing wrappers
  • All Debug impls on sensitive types must redact secrets

Frontend Conventions

  • Validation: Zod schemas in schemas/ directory, one file per domain
  • Forms: React Hook Form with @hookform/resolvers/zod
  • Server state: TanStack Query hooks in hooks/, one file per domain
  • Client state: Zustand store (stores/auth-store.ts)
  • UI components: Radix UI + shadcn/ui pattern (components/ui/)
  • Routing: TanStack Router with type-safe route params
  • No console.log in production code

File Organization

  • Many small files over few large files (200-400 lines typical, 800 max)
  • Organize by feature/domain, not by type
  • Extract reusable components into components/shared/

CLI Wizard Bundle

The CLI embeds a React-based wizard UI (cli/src/wizard/assets/index.html) into its binary via rust_embed, so cargo build -p nyxid-cli does not need a Node toolchain. The bundle is a committed prebuilt artifact — if you edit any file the wizard pulls in (frontend/src/components/cli-wizard/**, any components/ui/ primitive or hooks/, lib/, schemas/, stores/, pages/cli-pair/ file the wizard imports, frontend/wizard.html, frontend/vite.wizard.config.ts, or frontend/package-lock.json), rebuild the bundle and commit the result in the same PR:

npm --prefix frontend run build:wizard
git add cli/src/wizard/

build:wizard now copies the built bundle into cli/src/wizard/assets/index.html, writes the module-graph manifest to cli/src/wizard/bundle-meta/index.manifest, and a source-closure hash to cli/src/wizard/bundle-meta/index.hash. All three files must be committed.

Freshness is enforced by the wizard_bundle_is_fresh integration test in cli/tests/wizard_bundle_freshness.rs, which runs as the dedicated CLI Wizard Bundle Freshness CI job and on cargo test -p nyxid-cli --test wizard_bundle_freshness locally. The test re-hashes the current source closure from the committed manifest and compares against the committed hash — no npm install or vite rebuild is needed. The hash covers every file Vite traversed at build time (so transitive imports like components/ui/button.tsx are caught automatically) plus frontend/package-lock.json, frontend/vite.wizard.config.ts, frontend/wizard.html, frontend/vite-plugins/wizard-manifest.ts, and .node-version.


Security Requirements

All contributions must pass these checks before merge:

  • No hardcoded secrets (API keys, passwords, tokens) -- use environment variables
  • All user inputs validated (Zod on frontend, service-level validation on backend)
  • No SQL/NoSQL injection vectors (use parameterized queries)
  • No XSS vectors (sanitized HTML, proper escaping)
  • Authentication and authorization verified on all protected endpoints
  • Error messages do not leak internal details (database errors, stack traces)
  • Sensitive data encrypted at rest (use the existing crypto/aes.rs envelope encryption)
  • Rate limiting applied to new endpoints where appropriate

If your change touches authentication, encryption, or credential handling, it will receive extra scrutiny during review.


Testing

Running Tests

The full backend suite needs a dedicated, ephemeral MongoDB 8 replica set. It creates and drops a database per DB-backed test, so start that test-only mongod with minSnapshotHistoryWindowInSeconds=0; MongoDB's 300-second default retains dropped WiredTiger idents and file descriptors long enough to exhaust a full run. This is a server-wide parameter: do not set it on a shared development, staging, or production MongoDB.

docker run --rm --detach --name nyxid-test-mongodb --publish 27017:27017 \
  --ulimit nofile=65536:65536 \
  mongo:8.0 --replSet rs0 --bind_ip_all \
  --setParameter minSnapshotHistoryWindowInSeconds=0
docker exec nyxid-test-mongodb mongosh --quiet --eval \
  'rs.initiate({_id:"rs0",members:[{_id:0,host:"127.0.0.1:27017"}]})'
export NYXID_TEST_DATABASE_URL='mongodb://127.0.0.1:27017/?replicaSet=rs0&directConnection=true'

Wait until db.hello().isWritablePrimary is true before starting the suite. The CI workflow uses the same startup contract and verifies the effective parameter with getParameter before running tests.

Database setup shares one 90-second readiness deadline across ping, a unique probe write, and initial run-lease publication. A busy shared mongod can recover after the former five-second probe limit; the original operation waits within this budget without retrying the test. Errors still fail immediately, a server that remains unavailable fails at the deadline, and an explicit URI never falls back to another server. Heartbeat, stale-sweep, and cleanup fencing retain their separate short deadlines.

If readiness fails, inspect the reported phase and elapsed time alongside the resources-backend-test artifact and the job's MongoDB diagnostics. Clustered setup failures alone do not establish the cause of a server stall. Preserve these diagnostics and fix recurring setup failures instead of repeatedly rerunning the full workflow or adding test retries.

# Backend unit tests
cargo test

# Backend tests with all features (including KMS providers)
cargo test --all-features

# Node agent tests
cargo test -p nyxid-cli

# Frontend tests
cd frontend && npm run test

# Frontend lint
cd frontend && npm run lint

# Frontend type check + build
cd frontend && npm run build

Test Expectations

  • All new backend logic should have corresponding unit tests
  • All new frontend components should have tests where behavior is non-trivial
  • Existing tests must not break -- if a test fails, fix the implementation, not the test (unless the test itself is wrong)
  • Frontend linting must pass with zero errors

Commit Messages

Use Conventional Commits:

<type>: <description>

<optional body>
Type Use Case
feat New feature
fix Bug fix
refactor Code restructuring (no behavior change)
docs Documentation only
test Adding or fixing tests
chore Tooling, CI, dependencies
perf Performance improvement

Keep the subject line under 72 characters. Use the body for context on why the change was made, not what was changed (the diff shows that).


Pull Requests

Before Opening a PR

  • All tests pass locally (cargo test, npm run test, npm run lint)
  • No new compiler warnings (cargo build is clean)
  • Your branch is rebased on the latest dev
  • Commit history is clean (squash fixup commits)

PR Description

Include:

  • Summary: What changed and why (1-3 bullet points)
  • Test plan: How you verified the changes work

CI Checks

Every pull request runs the following checks automatically via GitHub Actions. Jobs are gated on path filters so a PR only runs the suites whose files actually changed -- e.g. an SDK-only PR skips backend, CLI, and frontend jobs. CI Pipeline aggregates the required jobs and treats success and skipped as passing. Branch rules must require this status check to enforce the gate at merge time.

Job What it does Triggers on
Rust Format cargo fmt --all -- --check -- ensures consistent formatting any Rust change (backend, cli, mcp-demo, root Cargo files)
Rust Clippy cargo clippy --workspace --all-targets -- -D warnings -- catches common mistakes and enforces idioms any Rust change
Backend Test cargo nextest run -p nyxid -p nyxid-service-adapters against a MongoDB 8.0 replica set -- handler, service, and integration tests backend/** or shared workspace files
Backend Billing Smoke Runs the standalone billing route smoke test with its original Cargo feature set backend changes
Backend Image Inputs Builds the production backend binary and checks that Docker stages every embedded input backend changes
Coverage (Backend) Measures PR-head coverage and enforces the line threshold backend changes
Coverage (Backend Base) Reuses an exact-commit report or measures the base branch for the informational PR delta backend PRs
CLI Test cargo nextest run -p nyxid-cli -- CLI + node-agent unit and integration tests cli/** or shared workspace files
Rust Features Builds with --features aws-kms, gcp-kms, and both -- ensures KMS providers compile backend/** or shared workspace files
Frontend npm run lint, npm run test, npm run build -- lint, test, and type-check frontend/** or .node-version
CLI Wizard Bundle Freshness Re-hashes the wizard's source closure (from cli/src/wizard/bundle-meta/index.manifest) and fails if it no longer matches the committed hash. No npm install or vite rebuild in CI. See CLI Wizard Bundle. frontend/**, cli/src/wizard/**, or .node-version
SDK Build npm run build across all SDK packages -- ensures TypeScript compiles sdk/** or .node-version
CI Pipeline Aggregator for all correctness checks above; the base coverage comparison is informational. Treats skipped as success. always

A change under Cargo.toml / Cargo.lock re-runs both Backend Test and CLI Test because they share the workspace lock file. A change under .github/workflows/** or .github/scripts/** forces every job to run.

Wait for a green CI Pipeline before merging. If a check fails, fix the issue locally and push -- the workflow re-runs automatically.

Backend test and coverage builds use CARGO_PROFILE_TEST_DEBUG=line-tables-only to keep the large test binary within hosted-runner memory. Each mode compiles one archive, then runs four isolated shards with one MongoDB per runner. Inventory verification checks that every expected test ran exactly once across the shards; coverage profiles are merged before enforcing the existing threshold. When a runner exits with code 143 during compilation, inspect the last completed step before treating it as a test failure. Repeated shutdowns at the same compile stage need a build-resource correction rather than repeated full-workflow retries.

The CI profile schedules the capacity-wait heartbeat, trigger grace deadline/recovery, lost-session recovery, stream-bound, and bounded analytics admission tests alone within each shard to keep concurrent database creation and cleanup out of their timing windows. The profile and reusable workflow are included in the coverage fingerprint. Base coverage reuses an exact-commit published report when available; it is informational and does not rerun the base suite. The reusable backend workflow sets RUST_MIN_STACK=8388608 (8 MiB) for ordinary and instrumented native journeys. Each build and execution shard has a 60-minute budget. Tests retain the CI profile's five-minute deadline and one retry; every retried test is named in the job summary, and a second failure fails the run.

For failed backend runs, open the verification job summary or download backend-test-diagnostics / backend-coverage-diagnostics (retained for 14 days). These contain available shard JUnit reports, failures/retries/skips, missing reports and the slowest cases; completeness and coverage remain separate gates. See the CI stability follow-up for focused reproduction commands and cache changes, and service history for the credential usage/edit timestamp contract.

Rollup Health Triage

Rollup-health watchdog issues are incident-routing records, not a second source of CI truth. The rollup PR checks, annotations, and uploaded artifacts remain authoritative for whether integration is blocked.

When a rollup-health issue is opened, keep the repair scoped to the failing check evidence:

  • Record the rollup PR, head SHA, failed check names, red-start time, and any immutable artifact or log link used for diagnosis.
  • Reproduce the failing job locally with the same package-level commands where possible, then fix the smallest contract break that restores the check.
  • Open or keep a separate issue only when it adds ownership or durable work that the rollup PR status cannot carry: cross-package coordination, runbook updates, owner routing, bisect/revert tracking, or automation follow-up.
  • If the same rollup-health class recurs for a third time in a quarter, the next repair must include either a class-level guardrail/runbook update or an explicit recurrence waiver explaining why a one-off fix is still the right boundary.

Review Process

  • PRs target the main branch by default
  • At least one approval is required before merge
  • All CI checks must pass (see above)
  • Security-sensitive changes require explicit security review
  • Merge branch-sync PRs with Create a merge commit. Squashing drops the merged branch's ancestry, so GitHub can still report the same conflicts.

Documentation

  • Update docs/API.md if you add or change API endpoints
  • Update docs/ARCHITECTURE.md if you add new collections, services, or architectural patterns
  • Update README.md if you add new features visible to users
  • Do not create new documentation files unless the feature is significant enough to warrant a standalone guide

In-Code Documentation

  • Add comments only where the logic is non-obvious
  • Do not add boilerplate docstrings to every function
  • Do not add // removed comments for deleted code -- just delete it

Releases

Releases are triggered by pushing a semver tag to main:

git tag v0.2.0
git push origin v0.2.0

The release workflow:

  1. Runs the full CI suite as a gate
  2. Builds and pushes Docker images to GitHub Container Registry (ghcr.io)
  3. Creates a GitHub Release with an auto-generated changelog

Only maintainers should create release tags. Contributors do not need to worry about releases -- just open PRs against dev.


Resources

Document Description
README.md Project overview, quick start, features
docs/API.md Full API reference with request/response schemas
docs/ARCHITECTURE.md System architecture, data flows, database schema
docs/DEVELOPER_GUIDE.md In-depth development patterns and conventions
docs/SECURITY.md Security architecture and threat model
docs/DEPLOYMENT.md Production deployment guide
docs/ENCRYPTION_ARCHITECTURE.md Envelope encryption and KMS integration
docs/NODE_PROXY.md Credential node setup and usage
docs/NYXID_NODE.md Node agent CLI reference

License

By contributing to NyxID, you agree that your contributions will be licensed under the MIT License.