# Code map

One entry per unit in the workspace: what it does, its key public API, and what
it depends on. For how these units fit together, see `architecture.md`.

## Workspace layout

```
grantor/
├── Cargo.toml                    # workspace: members = grantor-verify, grantor-issuer
├── contracts/                    # Solidity + Foundry (not a Cargo member)
│   ├── src/GrantorRegistry.sol
│   ├── test/                     # GrantorRegistry.t.sol, .invariant.t.sol, mocks/
│   └── out/                      # forge build artifacts (ABI+bytecode) — consumed by
│                                  # grantor-issuer's alloy `sol!` e2e test bindings
├── crates/
│   ├── grantor-verify/              # pure Rust: JWT verify + on-chain-agnostic trust check
│   └── grantor-issuer/              # Rust + Poem: the OIDC/OAuth2 issuer HTTP server
└── docs/                         # this doc, guides, OpenAPI, llms.txt
```

Builds use `CARGO_TARGET_DIR=~/.cargo-target` (set in the shell profile on this
box) — build artifacts land there, not in `./target`.

---

## `contracts/` — `GrantorRegistry`

**What:** the canonical on-chain trust anchor + credit-subscription billing
engine + multiwallet org registry. One per network. An ERC-721 "org pass"
(`tokenId == tenantId`) mints on `createTenant`.

**Key public API** (`contracts/src/GrantorRegistry.sol`):

- `isActive(bytes32 keyId) → bool` (line 105) — **the** trust check: true iff
  `keyId` is registered to a tenant whose derived `status()` is `Active` or
  `Grace`.
- `isAgentKey[tenantId][address] → bool` — public mapping; is `address`
  registered as an agent (multiwallet) key for that tenant.
- `status(uint256 id) → Status` (line 95) — `Inactive | Active | Grace`,
  derived live from `periodEnd` / `graceWindow` / `block.timestamp` (never
  stored, never stale).
- Lifecycle: `createTenant`, `addAdmin` / `removeAdmin`.
- Tiers: `setTierConfig` (simple/unlimited), `setTierConfigFull` (usage caps +
  optional `trialDuration`), `disableTier`, `setTier`.
- Billing: `topUp`, `drawPeriod` (permissionless keeper call; charges the
  tenant's *snapshotted* fee/period, not live tier config — the owner cannot
  retroactively drain a tenant), `withdrawBalance`, `setAutoRenew`.
- Registry: `registerIssuerKey` / `deregisterIssuerKey` (issuer-key ↔ tenant),
  `registerApp`, `registerAgentKey` (all enforce per-tier usage caps).
- Admin surface: `pause` / `unpause`, `setGraceWindow` — owner-only, minimal by
  design (money-handling contract).

**Depends on:** OpenZeppelin v5 (`ERC721`, `Ownable`, `Pausable`,
`ReentrancyGuard`, `SafeERC20`/`IERC20`); an immutable `usdc` token address and
`treasury` address set at construction.

**Tests:** `contracts/test/GrantorRegistry.t.sol` (unit, 415 lines) +
`GrantorRegistry.invariant.t.sol` (invariant/fuzz, 80 lines) + `mocks/`
(`MockUSDC`). Built with Foundry (`forge`); the JSON artifacts under
`contracts/out/` are loaded directly by `grantor-issuer`'s `alloy::sol!` macro
in its anvil e2e tests (`crates/grantor-issuer/src/chain.rs` test module),
so the Rust and Solidity sides stay bit-for-bit in sync without hand-copied
ABIs.

---

## `crates/grantor-verify`

**What:** verifies a Grantor JWT and confirms the issuer key is trusted.
Pure library — no chain access; the "is this key active?" check is *injected*
via a trait, so its tests need no network. Chain-free by design (AD-5): it's
meant to also compile to WASM to back a thin TS relying-app SDK later, so it
carries no `alloy` dependency.

**Key public API** (`crates/grantor-verify/src/lib.rs`):

- `verify_jwt(token, jwk_pem_pub, expected_aud) → Result<Claims, VerifyError>`
  — ES256 signature check, 60s `exp` leeway, `aud` match (line 40).
- `issuer_key_id(jwk_json) → Result<[u8; 32], VerifyError>` — RFC 7638 JWK
  thumbprint over the canonical `{crv,kty,x,y}` EC member set; this is the
  exact `[u8; 32]` the Solidity registry stores as `keyId` (line 52).
- `TrustSource` trait — `fn is_active(&self, key_id: [u8; 32]) -> bool`
  (synchronous; line 77) — the injection point for "is this key active
  on-chain."
- `verify(token, jwk_json, jwk_pem_pub, expected_aud, trust) → Result<Claims, VerifyError>`
  — composes `verify_jwt` + `issuer_key_id` + `TrustSource::is_active` into
  the one full check a relying app needs (line 83).
- `Claims { iss, sub, aud, exp, iat, tid, nonce }` — the JWT claim shape.

**Depends on:** `jsonwebtoken`, `serde`/`serde_json`, `sha2`. 6 tests.

**Non-default `sovereign` feature** (`sovereign.rs`/`sovereign_gate.rs`, SP8
Phase 1): RP-side verification of a self-certifying sovereign-tier agent
token directly against the on-chain `GrantorRegistry` — no `grantor-issuer`
process on this path at all (structurally enforced: this crate carries no
`grantor-issuer` dependency, checked in CI). Pulls in `semaphore-rs` + `alloy`
only when enabled. See `docs/sovereign-tier.md` and
`crates/grantor-verify/tests/sovereign_e2e.rs`.

---

## `crates/grantor-issuer`

**What:** the OIDC/OAuth2 issuer — holds the ES256 signing key, publishes
`/jwks`, and mints Grantor JWTs for both grants, gated by the on-chain check.
Built on Poem (AD-4, for `poem-openapi`-friendly typed HTTP handlers). Depends
on `grantor-verify` (re-derives/round-trips through `issuer_key_id` and
`verify_jwt` in its own tests) plus `alloy` (chain reads), `siwe` (EIP-4361),
and `poem`/`tokio` for the server.

Re-exports (`crates/grantor-issuer/src/lib.rs`): `CachedIsActive`, `Clock`,
`IsActiveSource`, `SystemClock`, `AlloyReader`, `ChainError`, `ClientStore`,
`ClientStoreError`, `FileClientStore`, `ConfigError`, `IssuerConfig`,
`ChainGate`, `TrustGate`, `recover_login_address`, `verify_login`,
`LoginError`, `AppState`, `AuthCode`, `pairwise_sub`, `IssuerError`,
`IssuerKey`, `MintParams`.

### `config` — `crates/grantor-issuer/src/config.rs`

`IssuerConfig { domain, signing_key_pem, contract_addr, rpc_url, tenant_id,
tenant_salt, token_ttl_secs, clients_file }`. `from_env()` / the testable
`from_map(get: impl Fn(&str) -> Option<String>)`. `token_ttl_secs` defaults to
3600 if unset; every other field is required (`ConfigError::Missing`/`Bad`).
Ships a **manual, redacting `Debug`** impl — `signing_key_pem` and
`tenant_salt` print as `<redacted>` so secrets can't leak into logs.

### `clients` — `crates/grantor-issuer/src/clients.rs`

`ClientStore` trait: `is_valid_redirect(client_id, redirect_uri)`,
`is_allowed_audience(audience)` (both exact-string match). `FileClientStore`:
the only implementation, loaded from a TOML file (`[[client]] id, redirect_uris`
/ `[[resource]] audience`) via `load(path)` or `from_toml_str`. Off-chain and
swappable by design — see `architecture.md`'s client-registry section for why.

### `gate` — `crates/grantor-issuer/src/gate.rs`

`TrustGate` trait (`async_trait`, object-safe): `is_active(key_id)`,
`is_agent_key(tenant_id, key)`. `ChainGate`: the production impl — `is_active`
routed through `CachedIsActive<AlloyReader, SystemClock>`; `is_agent_key` read
live via a cloned `AlloyReader` (uncached — changes at admin-tx frequency).

### `chain` — `crates/grantor-issuer/src/chain.rs`

`AlloyReader { rpc_url, registry }` (`Clone`) — reads `GrantorRegistry`
on-chain via an `alloy::sol!` interface (`IGrantorRegistry`):
`is_active(key_id) → Result<bool, ChainError>`,
`is_agent_key(tenant_id, key) → Result<bool, ChainError>`. Async by nature (RPC
round-trip) — deliberately distinct from `grantor_verify::TrustSource`
(sync). Its test module spawns a live **anvil** node, deploys `MockUSDC` +
`GrantorRegistry` from `contracts/out/*.json`, runs a full tenant lifecycle
(configure tier → create tenant → fund → draw a period → register issuer +
agent keys), then asserts the reader sees the right `is_active`/`is_agent_key`
results.

### `cache` — `crates/grantor-issuer/src/cache.rs`

`IsActiveSource` trait (the raw, uncached read; implemented for `AlloyReader`).
`Clock` trait + `SystemClock`. `CachedIsActive<S, C>` — TTL-fresh cache hits
skip the RPC call; on a source failure, serves the last good value within
`staleness_secs`, then fails closed (`false`) past that bound, and on a cold
cache with no prior value. Unit-tested entirely against a fake source + a
controllable fake clock (no anvil needed).

### `state` — `crates/grantor-issuer/src/state.rs`

`AppState { key: IssuerKey, cfg: IssuerConfig, gate: Arc<dyn TrustGate>,
clients: Arc<dyn ClientStore>, .. }` plus private single-use stores: `codes`
(`put_code`/`take_code`, expiry-checked) and `nonces` (`use_nonce`, with a lazy
sweep of stale entries). `AuthCode { sub, tid, aud, nonce, pkce_s256,
redirect_uri, expires_at }`. `#[cfg(test)] AppState::for_test()` builds a
ready-to-use state with a generated key, a permissive fake `TrustGate`, and a
seeded `app-1` / `resource-api` client store — used throughout the HTTP tests.

### `token` — `crates/grantor-issuer/src/token.rs`

`IssuerKey` — the ES256 (P-256) signing key plus its published JWK identity.
`generate()` / `from_pkcs8_pem(pem)`; `jwk_json()` (bare public JWK),
`jwks_json()` (the `/jwks` document, `kid` = `key_id_hex()`), `key_id_hex()`,
and `mint(&MintParams) → String` (signs an ES256 JWT with `kid` set in the
header). `key_id: [u8; 32]` is computed via
`grantor_verify::issuer_key_id` — the exact value that must match the
on-chain `keyId`. `MintParams<'a> { iss, aud, sub, tid, ttl_secs, nonce }`.
`pairwise_sub(wallet, tenant_salt) → String` — `sha256(lowercase(wallet) ++
tenant_salt)`, hex-encoded; same wallet, different (unlinkable) subject per
tenant salt.

### `login` — `crates/grantor-issuer/src/login.rs`

The EIP-191 crypto primitive shared by both grants: `recover_login_address
(message, signature) → Result<Address, LoginError>` (via
`alloy_primitives::PrimitiveSignature::recover_address_from_msg`) and
`verify_login(message, signature, expected) → Result<(), LoginError>`.
`LoginError { Signature, Mismatch }`. Used directly by Grant A's SIWE
signature check and, via `http/assertion.rs`, by Grant C's client-assertion
check.

### `http/` — `crates/grantor-issuer/src/http/`

- **`mod.rs`** — `routes(state: Arc<AppState>) -> BoxEndpoint<'static>`: mounts
  discovery, `/jwks`, `/token`, `/authorize` (+`/callback`), and `/assets`
  (static files, served from `CARGO_MANIFEST_DIR/assets` so it's CWD-robust),
  then attaches shared state via `.data(state)`. `health_routes()` is a
  separate minimal router for `/healthz`.
- **`discovery.rs`** — `openid_configuration` (OIDC discovery JSON off
  `cfg.domain`) and `jwks` (`st.key.jwks_json()`, `application/json`).
- **`authorize.rs`** — Grant A's two endpoints. `authorize` (`GET /authorize`):
  requires PKCE `S256`, checks `ClientStore::is_valid_redirect`, renders
  `page::render`. `callback` (`POST /authorize/callback`): parses the SIWE
  message (`siwe` crate), checks the SIWE `domain` against `cfg.domain`,
  verifies the EIP-191 signature via `msg.verify_eip191` *before* burning the
  single-use SIWE nonce (ordering fixed during review to prevent
  signature-less nonce griefing), calls `TrustGate::is_active`, re-validates
  the redirect allowlist, mints a one-time `AuthCode` keyed by a random 32-byte
  hex code, and `302`s to `redirect_uri?code=…&state=…` built via
  `url::Url::query_pairs_mut` (percent-encoded, no manual string concatenation
  → no open-redirect / double-`?` bugs).
- **`token.rs`** — `POST /token` dispatcher (`token()` matches `grant_type`).
  `client_credentials()` (Grant C): validates `client_assertion`/`nonce`/
  `issued_at`/`audience` are present, checks `ClientStore::is_allowed_audience`,
  delegates signature/nonce/skew checks to `assertion::verify`, then checks
  `TrustGate::is_agent_key` and `TrustGate::is_active` before minting with a
  pairwise `sub` of the recovered agent address.
- **`token_code.rs`** — `authorization_code()` (Grant A's code exchange):
  `AppState::take_code` (single-use + expiry), requires `redirect_uri` to
  match the code's stored value (RFC 6749 §4.1.3), verifies PKCE
  (`base64url(sha256(code_verifier)) == stored code_challenge`), re-checks
  `TrustGate::is_active` at exchange time, and mints the JWT carrying the
  code's stored `sub`/`tid`/`aud`/`nonce`.
- **`assertion.rs`** — Grant C's client-assertion primitive.
  `build_message(domain, tenant_id, nonce, issued_at)` builds the canonical
  challenge string (`"Grantor client-credentials\ndomain: …\ntenant: …\nnonce:
  …\nissued-at: …"`). `verify()` recovers the signer via
  `login::recover_login_address` *before* consuming the nonce (same anti-grief
  ordering as `authorize.rs`), then checks the `±120s` skew (`SKEW_SECS`) and
  the single-use nonce.
- **`page.rs`** — the one HTML page in the system: a minimal connect-and-sign
  form. `render()` HTML-attribute-escapes all five reflected query params
  (`escape_attr`) — fixed a reflected-XSS finding found during code review;
  there is a regression test for it.

### `main` — `crates/grantor-issuer/src/main.rs`

Binary wiring: `IssuerConfig::from_env()` → `IssuerKey::from_pkcs8_pem` →
`AlloyReader::new(rpc_url, contract_addr)` → `ChainGate::new(reader, 30, 120)`
(30s cache TTL, 120s staleness bound) → `FileClientStore::load(clients_file)`
(propagated with `?` — fail-closed boot) → `AppState::new(..)` →
`grantor_issuer::http::routes(state)`, served via `poem::Server` bound to
`BIND` (default `0.0.0.0:8080`).

### `tests/` (integration)

`grant_a_e2e.rs` / `grant_c_e2e.rs` — live-anvil end-to-end tests: deploy
`MockUSDC` + `GrantorRegistry`, run the real tenant lifecycle, wire a real
`ChainGate` (not a fake), then drive the actual HTTP flow (SIWE → code → token
→ `verify_jwt`, and the client-credentials equivalent) to prove the whole
stack — not just each unit — works together.

### `assets/grantor-login.js`

The wallet-side script for the connect-and-sign page: uses viem (+
WalletConnect) to connect a wallet, build the EIP-4361 SIWE message, sign it,
and submit `http/page.rs`'s form to `/authorize/callback`.
