An easy-to-host PDS on the ATProtocol, MacOS. Grandma-approved.

chore: remove ticket and AC references from source code

Strip MM-NNN and AC-numbered comments from .rs files and CLAUDE.md files.
Source comments now describe system behavior, not ticket traceability.
Adds a CLAUDE.md convention to prevent recurrence.

authored by malpercio.dev and committed by

Tangled 44db00d7 cbf43921

+79 -117
+1
CLAUDE.md
··· 59 59 - Workspace-level dependency versions in root Cargo.toml; crates use `{ workspace = true }` 60 60 - All crates share version (0.1.0) and edition (2021) via workspace.package 61 61 - publish = false (not intended for crates.io) 62 + - **No ticket or AC references in source code.** Do not add comments like `// MM-123`, `// AC2.1:`, or `// MM-84.AC3: description` to `.rs` files or CLAUDE.md files. Design plans and test plans in `docs/` are the right home for ticket traceability. Source code comments should describe *why* in terms of the system, not which ticket required it. 62 63 63 64 ## Boundaries 64 65 - Never edit: `flake.lock` by hand (managed by `nix flake update`)
+3 -6
crates/common/src/config.rs
··· 750 750 #[test] 751 751 fn admin_token_is_optional() { 752 752 let config = validate_and_build(minimal_raw()).unwrap(); 753 - // MM-92.AC7.5 754 753 assert!(config.admin_token.is_none()); 755 754 } 756 755 757 756 #[test] 758 757 fn signing_key_master_key_is_optional() { 759 758 let config = validate_and_build(minimal_raw()).unwrap(); 760 - // MM-92.AC7.5 761 759 assert!(config.signing_key_master_key.is_none()); 762 760 } 763 761 764 762 #[test] 765 763 fn env_override_admin_token() { 766 - // MM-92.AC7.1 767 764 let env = HashMap::from([("EZPDS_ADMIN_TOKEN".to_string(), "secret-token".to_string())]); 768 765 let raw = apply_env_overrides(minimal_raw(), &env).unwrap(); 769 766 let config = validate_and_build(raw).unwrap(); ··· 772 769 773 770 #[test] 774 771 fn env_override_signing_key_master_key_valid_hex() { 775 - // MM-92.AC7.2: 64 valid hex chars → [u8; 32] 772 + // 64 valid hex chars → [u8; 32] 776 773 let hex_key = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"; 777 774 let env = HashMap::from([( 778 775 "EZPDS_SIGNING_KEY_MASTER_KEY".to_string(), ··· 794 791 795 792 #[test] 796 793 fn env_override_signing_key_master_key_wrong_length_returns_error() { 797 - // MM-92.AC7.3: 62 hex chars (31 bytes) — wrong length 794 + // 62 hex chars (31 bytes) — wrong length 798 795 let short_key = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; 799 796 let env = HashMap::from([( 800 797 "EZPDS_SIGNING_KEY_MASTER_KEY".to_string(), ··· 807 804 808 805 #[test] 809 806 fn env_override_signing_key_master_key_non_hex_returns_error() { 810 - // MM-92.AC7.4: contains 'g' which is not a valid hex character 807 + // contains 'g' which is not a valid hex character 811 808 let invalid_key = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fgg"; 812 809 let env = HashMap::from([( 813 810 "EZPDS_SIGNING_KEY_MASTER_KEY".to_string(),
+6 -6
crates/crypto/CLAUDE.md
··· 48 48 - Reconstructs secret from 2 distinct shares (indices [1,3]) 49 49 - Returns `CryptoError::SecretReconstruction` if indices are duplicate or out of range 50 50 51 - **`build_did_plc_genesis_op`** (new, MM-89) 51 + **`build_did_plc_genesis_op`** 52 52 ```rust 53 53 pub fn build_did_plc_genesis_op( 54 54 rotation_key: &DidKeyUri, // user's root rotation key (rotationKeys[0]) ··· 65 65 - Deterministic: same inputs → same DID (RFC 6979 ECDSA + SHA-256 + base32) 66 66 - Errors: `CryptoError::PlcOperation` if `signing_private_key` is an invalid P-256 scalar 67 67 68 - **`verify_genesis_op`** (new, MM-90) 68 + **`verify_genesis_op`** 69 69 ```rust 70 70 pub fn verify_genesis_op( 71 71 signed_op_json: &str, // JSON-encoded signed genesis op from client ··· 85 85 - `public_key`: multibase base58btc compressed point (no prefix) 86 86 - `private_key_bytes`: `Zeroizing<[u8; 32]>` (zeroized on drop) 87 87 88 - **`PlcGenesisOp`** (new, MM-89) 88 + **`PlcGenesisOp`** 89 89 - `did`: `"did:plc:xxxx..."` (28 chars total) 90 90 - `signed_op_json`: contains `type`, `rotationKeys`, `verificationMethods`, `alsoKnownAs`, `services`, `prev` (null), `sig` 91 91 92 - **`VerifiedGenesisOp`** (new, MM-90) 92 + **`VerifiedGenesisOp`** 93 93 - `did`: derived DID string 94 94 - `rotation_keys`: full `rotationKeys` array from the op 95 95 - `also_known_as`: full `alsoKnownAs` array from the op ··· 101 101 - `data`: `Zeroizing<[u8; 32]>` (zeroized on drop) 102 102 103 103 **`CryptoError`** variants: 104 - - `KeyGeneration`, `Encryption`, `Decryption`, `SecretSharing`, `SecretReconstruction`, `PlcOperation` (new, MM-89) 104 + - `KeyGeneration`, `Encryption`, `Decryption`, `SecretSharing`, `SecretReconstruction`, `PlcOperation` 105 105 106 106 ### Format guarantees 107 107 ··· 125 125 ## Key Files 126 126 - `src/lib.rs` - Re-exports public API 127 127 - `src/keys.rs` - P-256 key generation, AES-256-GCM encrypt/decrypt 128 - - `src/plc.rs` - did:plc genesis operation builder (MM-89) and verifier (MM-90) 128 + - `src/plc.rs` - did:plc genesis operation builder and verifier 129 129 - `src/shamir.rs` - Shamir Secret Sharing (split/combine, GF(2^8) arithmetic) 130 130 - `src/error.rs` - CryptoError enum
+5 -5
crates/crypto/src/keys.rs
··· 184 184 assert_eq!(keypair.private_key_bytes.len(), 32); 185 185 } 186 186 187 - /// MM-92.AC3.1: Round-trip encrypt → decrypt returns original bytes. 187 + /// Round-trip encrypt → decrypt returns original bytes. 188 188 #[test] 189 189 fn encrypt_decrypt_round_trip() { 190 190 let master_key = [0xab_u8; 32]; ··· 196 196 assert_eq!(*decrypted, private_key); 197 197 } 198 198 199 - /// MM-92.AC3.2: Wrong master key returns CryptoError::Decryption. 199 + /// Wrong master key returns CryptoError::Decryption. 200 200 #[test] 201 201 fn decrypt_with_wrong_master_key_fails() { 202 202 let master_key = [0xab_u8; 32]; ··· 212 212 ); 213 213 } 214 214 215 - /// MM-92.AC3.3: Malformed base64 returns CryptoError::Decryption. 215 + /// Malformed base64 returns CryptoError::Decryption. 216 216 #[test] 217 217 fn decrypt_invalid_base64_fails() { 218 218 let master_key = [0xab_u8; 32]; ··· 221 221 assert!(matches!(result, Err(CryptoError::Decryption(_)))); 222 222 } 223 223 224 - /// MM-92.AC3.3 (variant): Base64 that decodes but is wrong length. 224 + /// Base64 that decodes but is wrong length. 225 225 #[test] 226 226 fn decrypt_wrong_length_fails() { 227 227 let master_key = [0xab_u8; 32]; ··· 231 231 assert!(matches!(result, Err(CryptoError::Decryption(_)))); 232 232 } 233 233 234 - /// MM-92.AC3.4: Two encryptions of the same key produce different ciphertexts (random nonce). 234 + /// Two encryptions of the same key produce different ciphertexts (random nonce). 235 235 #[test] 236 236 fn encrypt_produces_different_ciphertexts_for_same_input() { 237 237 let master_key = [0xab_u8; 32];
+17 -17
crates/crypto/src/plc.rs
··· 383 383 ) 384 384 } 385 385 386 - /// MM-89.AC1.1: did matches ^did:plc:[a-z2-7]{24}$ 386 + /// did matches ^did:plc:[a-z2-7]{24}$ 387 387 #[test] 388 388 fn did_matches_expected_format() { 389 389 let (_, _, _, op) = make_genesis_op(); ··· 401 401 ); 402 402 } 403 403 404 - /// MM-89.AC1.2: signed_op_json contains all required fields with correct values 404 + /// signed_op_json contains all required fields with correct values 405 405 #[test] 406 406 fn signed_op_json_contains_required_fields() { 407 407 let (_, _, _, op) = make_genesis_op(); ··· 419 419 assert!(v["sig"].is_string(), "sig is string"); 420 420 } 421 421 422 - /// MM-89.AC1.3: rotation_key at rotationKeys[0]; signing_key at rotationKeys[1] and verificationMethods.atproto 422 + /// rotation_key at rotationKeys[0]; signing_key at rotationKeys[1] and verificationMethods.atproto 423 423 #[test] 424 424 fn keys_placed_in_correct_positions() { 425 425 let (rotation_key, signing_key, _, op) = make_genesis_op(); ··· 441 441 ); 442 442 } 443 443 444 - /// MM-89.AC1.4: RFC 6979 determinism — same inputs produce same DID 444 + /// RFC 6979 determinism — same inputs produce same DID 445 445 #[test] 446 446 fn same_inputs_produce_same_did() { 447 447 let rotation_kp = generate_p256_keypair().expect("rotation keypair"); ··· 473 473 ); 474 474 } 475 475 476 - /// MM-89.AC1.5: Invalid signing key (all-zero scalar) returns CryptoError::PlcOperation 476 + /// Invalid signing key (all-zero scalar) returns CryptoError::PlcOperation 477 477 #[test] 478 478 fn invalid_signing_key_returns_error() { 479 479 let rotation_kp = generate_p256_keypair().expect("rotation keypair"); ··· 494 494 ); 495 495 } 496 496 497 - /// MM-89.AC3.2: sig field is base64url (no padding) decoding to exactly 64 bytes 497 + /// sig field is base64url (no padding) decoding to exactly 64 bytes 498 498 #[test] 499 499 fn sig_field_is_base64url_no_padding_and_64_bytes() { 500 500 let (_, _, _, op) = make_genesis_op(); ··· 518 518 ); 519 519 } 520 520 521 - /// MM-89.AC3.3: alsoKnownAs contains at://{handle} 521 + /// alsoKnownAs contains at://{handle} 522 522 #[test] 523 523 fn also_known_as_contains_at_uri() { 524 524 let rotation_kp = generate_p256_keypair().expect("rotation keypair"); ··· 544 544 ); 545 545 } 546 546 547 - // ── MM-90 verify_genesis_op tests ────────────────────────────────────────── 547 + // ── verify_genesis_op tests ──────────────────────────────────────────────── 548 548 549 - /// Returns (signing_key_uri, PlcGenesisOp) for MM-90 verification tests. 549 + /// Returns (signing_key_uri, PlcGenesisOp) for verify_genesis_op tests. 550 550 /// build_did_plc_genesis_op signs with signing_key_bytes; verify_genesis_op 551 551 /// must receive signing_kp.key_id as its rotation_key argument. 552 552 fn make_op_for_verify() -> (DidKeyUri, PlcGenesisOp) { ··· 564 564 (signing_kp.key_id, op) 565 565 } 566 566 567 - /// MM-90.AC1.1: verify_genesis_op returns correct fields 567 + /// verify_genesis_op returns correct fields 568 568 #[test] 569 569 fn verify_valid_op_returns_correct_fields() { 570 570 let (signing_key, op) = make_op_for_verify(); ··· 599 599 ); 600 600 } 601 601 602 - /// MM-90.AC1.2: DID from verify_genesis_op matches build_did_plc_genesis_op 602 + /// DID from verify_genesis_op matches build_did_plc_genesis_op 603 603 #[test] 604 604 fn verify_did_matches_build_did_plc_genesis_op() { 605 605 let (signing_key, genesis_op) = make_op_for_verify(); ··· 614 614 ); 615 615 } 616 616 617 - /// MM-90.AC1.3: Signature verification fails with wrong rotation key 617 + /// Signature verification fails with wrong rotation key 618 618 #[test] 619 619 fn verify_wrong_rotation_key_returns_error() { 620 620 let (_, op) = make_op_for_verify(); ··· 628 628 ); 629 629 } 630 630 631 - /// MM-90.AC1.4: Corrupted signature returns error 631 + /// Corrupted signature returns error 632 632 #[test] 633 633 fn verify_corrupted_signature_returns_error() { 634 634 let (signing_key, op) = make_op_for_verify(); ··· 653 653 ); 654 654 } 655 655 656 - /// MM-90.AC1.5: Unknown fields in JSON are rejected 656 + /// Unknown fields in JSON are rejected 657 657 #[test] 658 658 fn verify_unknown_fields_returns_error() { 659 659 let (signing_key, op) = make_op_for_verify(); ··· 672 672 ); 673 673 } 674 674 675 - /// MM-90.AC2.1: Rotation op (prev != null) is rejected 675 + /// Rotation op (prev != null) is rejected 676 676 #[test] 677 677 fn verify_rotation_op_with_non_null_prev_returns_error() { 678 678 let (signing_key, op) = make_op_for_verify(); ··· 691 691 ); 692 692 } 693 693 694 - /// MM-90.AC2.2: Non-genesis op_type is rejected 694 + /// Non-genesis op_type is rejected 695 695 #[test] 696 696 fn verify_non_genesis_op_type_returns_error() { 697 697 let (signing_key, op) = make_op_for_verify(); ··· 710 710 ); 711 711 } 712 712 713 - /// MM-90.AC3: Canonical usage pattern — rotation key signs and appears at rotationKeys[0]. 713 + /// Canonical usage pattern — rotation key signs and appears at rotationKeys[0]. 714 714 /// The same keypair is both the rotation key and the signing key. 715 715 #[test] 716 716 fn verify_rotation_key_can_verify_own_op() {
+1 -1
crates/relay/src/app.rs
··· 85 85 pub http_client: Client, 86 86 /// Optional DNS provider for subdomain record creation on handle registration. 87 87 /// `None` in v0.1 — operators manage DNS records manually. 88 - /// Wired in by MM-142 (DNS provider integration). 88 + /// Populated by real provider implementations (Cloudflare, Route53) when configured. 89 89 pub dns_provider: Option<Arc<dyn DnsProvider>>, 90 90 /// Optional DNS TXT resolver for handle resolution fallback. 91 91 /// When `None`, `resolveHandle` skips DNS and returns `HandleNotFound` for
+1 -1
crates/relay/src/dns.rs
··· 2 2 // 3 3 // DnsProvider — creates DNS records when handles are registered (POST /v1/handles). 4 4 // For v0.1, AppState carries `dns_provider: None`; operators manage DNS manually. 5 - // MM-142 wires in real provider implementations (Cloudflare, Route53). 5 + // Real provider implementations (Cloudflare, Route53) are wired in when configured. 6 6 // 7 7 // TxtResolver — resolves DNS TXT records for handle lookup fallback 8 8 // (GET /xrpc/com.atproto.identity.resolveHandle).
+4 -14
crates/relay/src/routes/claim_codes.rs
··· 154 154 155 155 #[tokio::test] 156 156 async fn returns_200_with_one_code() { 157 - // MM-86.AC1.1 158 157 let response = app(test_state_with_admin_token().await) 159 158 .oneshot(post_claim_codes( 160 159 r#"{"count": 1, "expiresInHours": 24}"#, ··· 174 173 175 174 #[tokio::test] 176 175 async fn returns_ten_codes_for_batch() { 177 - // MM-86.AC1.2 178 176 let response = app(test_state_with_admin_token().await) 179 177 .oneshot(post_claim_codes( 180 178 r#"{"count": 10, "expiresInHours": 24}"#, ··· 193 191 194 192 #[tokio::test] 195 193 async fn defaults_expires_in_hours_to_24() { 196 - // MM-86.AC1.3: expiresInHours is optional; default = 24h 194 + // expiresInHours is optional; default = 24h 197 195 let state = test_state_with_admin_token().await; 198 196 let db = state.db.clone(); 199 197 ··· 237 235 238 236 #[tokio::test] 239 237 async fn codes_are_6_char_uppercase_alphanumeric() { 240 - // MM-86.AC2.1 241 238 let response = app(test_state_with_admin_token().await) 242 239 .oneshot(post_claim_codes( 243 240 r#"{"count": 5, "expiresInHours": 1}"#, ··· 263 260 264 261 #[tokio::test] 265 262 async fn codes_in_batch_are_unique() { 266 - // MM-86.AC2.2 267 263 let response = app(test_state_with_admin_token().await) 268 264 .oneshot(post_claim_codes( 269 265 r#"{"count": 10, "expiresInHours": 1}"#, ··· 294 290 295 291 #[tokio::test] 296 292 async fn codes_persisted_in_db_with_pending_status() { 297 - // MM-86.AC3.1: stored with redeemed_at NULL (pending) and correct expiry 293 + // stored with redeemed_at NULL (pending) and correct expiry 298 294 let state = test_state_with_admin_token().await; 299 295 let db = state.db.clone(); 300 296 ··· 366 362 367 363 #[tokio::test] 368 364 async fn count_zero_returns_400() { 369 - // MM-86.AC4.1 370 365 let response = app(test_state_with_admin_token().await) 371 366 .oneshot(post_claim_codes( 372 367 r#"{"count": 0, "expiresInHours": 24}"#, ··· 379 374 380 375 #[tokio::test] 381 376 async fn count_eleven_returns_400() { 382 - // MM-86.AC4.2 383 377 let response = app(test_state_with_admin_token().await) 384 378 .oneshot(post_claim_codes( 385 379 r#"{"count": 11, "expiresInHours": 24}"#, ··· 392 386 393 387 #[tokio::test] 394 388 async fn expires_in_hours_zero_returns_400() { 395 - // MM-86.AC4.3 396 389 let response = app(test_state_with_admin_token().await) 397 390 .oneshot(post_claim_codes( 398 391 r#"{"count": 1, "expiresInHours": 0}"#, ··· 405 398 406 399 #[tokio::test] 407 400 async fn missing_count_returns_422() { 408 - // MM-86.AC4.4: serde rejects missing required field 401 + // serde rejects missing required field 409 402 let response = app(test_state_with_admin_token().await) 410 403 .oneshot(post_claim_codes( 411 404 r#"{"expiresInHours": 24}"#, ··· 420 413 421 414 #[tokio::test] 422 415 async fn missing_authorization_header_returns_401() { 423 - // MM-86.AC5.1 424 416 let response = app(test_state_with_admin_token().await) 425 417 .oneshot(post_claim_codes(r#"{"count": 1}"#, None)) 426 418 .await ··· 430 422 431 423 #[tokio::test] 432 424 async fn wrong_bearer_token_returns_401() { 433 - // MM-86.AC5.2 434 425 let response = app(test_state_with_admin_token().await) 435 426 .oneshot(post_claim_codes(r#"{"count": 1}"#, Some("wrong-token"))) 436 427 .await ··· 440 431 441 432 #[tokio::test] 442 433 async fn bare_token_without_bearer_prefix_returns_401() { 443 - // MM-86.AC5.3 444 434 let request = Request::builder() 445 435 .method("POST") 446 436 .uri("/v1/accounts/claim-codes") ··· 458 448 459 449 #[tokio::test] 460 450 async fn admin_token_not_configured_returns_401() { 461 - // MM-86.AC5.4: test_state() leaves admin_token as None 451 + // test_state() leaves admin_token as None 462 452 let response = app(test_state().await) 463 453 .oneshot(post_claim_codes( 464 454 r#"{"count": 1}"#,
+1 -7
crates/relay/src/routes/create_account.rs
··· 314 314 315 315 #[tokio::test] 316 316 async fn returns_201_with_correct_shape() { 317 - // MM-83.AC1: POST creates account and returns claim code 318 317 let response = app(test_state_with_admin_token().await) 319 318 .oneshot(post_create_account( 320 319 r#"{"email":"alice@example.com","handle":"alice.example.com","tier":"free"}"#, ··· 342 341 343 342 #[tokio::test] 344 343 async fn claim_code_is_6_char_uppercase_alphanumeric() { 345 - // MM-83.AC1: claim code format 346 344 let response = app(test_state_with_admin_token().await) 347 345 .oneshot(post_create_account( 348 346 r#"{"email":"bob@example.com","handle":"bob.example.com","tier":"pro"}"#, ··· 366 364 367 365 #[tokio::test] 368 366 async fn records_persisted_in_db() { 369 - // MM-83.AC1: account and claim code stored in DB 370 367 let state = test_state_with_admin_token().await; 371 368 let db = state.db.clone(); 372 369 ··· 419 416 420 417 #[tokio::test] 421 418 async fn duplicate_email_in_pending_returns_409() { 422 - // MM-83.AC2: duplicate email returns 409 Conflict 423 419 let state = test_state_with_admin_token().await; 424 420 let app = app(state); 425 421 ··· 450 446 451 447 #[tokio::test] 452 448 async fn duplicate_email_in_accounts_returns_409() { 453 - // MM-83.AC2: email already used by a fully-provisioned account also returns 409 449 + // email already used by a fully-provisioned account also returns 409 454 450 let state = test_state_with_admin_token().await; 455 451 456 452 // Seed a fully-provisioned account directly. ··· 550 546 551 547 #[tokio::test] 552 548 async fn empty_handle_returns_400() { 553 - // MM-83.AC3: invalid handle format returns 400 554 549 let response = app(test_state_with_admin_token().await) 555 550 .oneshot(post_create_account( 556 551 r#"{"email":"x@example.com","handle":"","tier":"free"}"#, ··· 652 647 653 648 #[tokio::test] 654 649 async fn missing_authorization_header_returns_401() { 655 - // MM-83.AC auth 656 650 let response = app(test_state_with_admin_token().await) 657 651 .oneshot(post_create_account( 658 652 r#"{"email":"x@example.com","handle":"x.example.com","tier":"free"}"#,
+31 -32
crates/relay/src/routes/create_did.rs
··· 35 35 // 13. Return { "did": "did:plc:...", "did_document": {...}, "status": "active", "session_token": "..." } 36 36 // 37 37 // Note: handles are NOT inserted here. Handle creation is the caller's responsibility 38 - // via POST /v1/handles (MM-94), which validates format and optionally creates DNS records. 38 + // via POST /v1/handles, which validates format and optionally creates DNS records. 39 39 // 40 40 // Outputs (success): 200 { "did": "...", "did_document": {...}, "status": "active", "session_token": "..." } 41 41 // Outputs (error): 400 INVALID_CLAIM, 401 UNAUTHORIZED, 409 DID_ALREADY_EXISTS, ··· 451 451 /// Insert prerequisite rows for a DID-creation test. 452 452 /// 453 453 /// Inserts: claim_code, pending_account, device, pending_session. 454 - /// No relay signing key needed for MM-90. 454 + /// No relay signing key needed. 455 455 async fn insert_test_data(db: &sqlx::SqlitePool) -> TestSetup { 456 456 let claim_code = format!("TEST-{}", Uuid::new_v4()); 457 457 sqlx::query( ··· 518 518 } 519 519 520 520 /// Create an AppState with plc_directory_url pointing to the mock server. 521 - /// No signing_key_master_key needed for MM-90. 521 + /// No signing_key_master_key needed. 522 522 async fn test_state_for_did(plc_url: String) -> AppState { 523 523 test_state_with_plc_url(plc_url).await 524 524 } 525 525 526 - /// Build a POST /v1/dids request with the MM-90 body shape. 526 + /// Build a POST /v1/dids request. 527 527 fn create_did_request( 528 528 session_token: &str, 529 529 rotation_key_public: &str, ··· 542 542 .unwrap() 543 543 } 544 544 545 - // ── AC2.1/2.2/2.3/2.4/2.5/4.1/4.2/4.3: Happy path ─────────────────────── 545 + // ── Happy path ──────────────────────────────────────────────────────────── 546 546 547 - /// MM-90.AC2.1, AC2.2, AC2.3, AC2.4, AC2.5, AC4.1, AC4.2, AC4.3: 548 547 /// Valid request promotes account and returns full DID response. 549 548 #[tokio::test] 550 549 async fn happy_path_promotes_account_and_returns_did() { ··· 573 572 .await 574 573 .unwrap(); 575 574 576 - // AC2.1: 200 OK with { did, did_document, status: "active", session_token } 575 + // 200 OK with { did, did_document, status: "active", session_token } 577 576 assert_eq!(response.status(), StatusCode::OK, "expected 200 OK"); 578 577 let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX) 579 578 .await ··· 602 601 let did = body["did"].as_str().unwrap(); 603 602 let doc = &body["did_document"]; 604 603 605 - // AC4.2: alsoKnownAs contains at://{handle} 604 + // alsoKnownAs contains at://{handle} 606 605 let also_known_as = doc["alsoKnownAs"].as_array().expect("alsoKnownAs is array"); 607 606 assert!( 608 607 also_known_as ··· 612 611 setup.handle 613 612 ); 614 613 615 - // AC4.1: verificationMethod has publicKeyMultibase starting with "z" 614 + // verificationMethod has publicKeyMultibase starting with "z" 616 615 let vm = &doc["verificationMethod"][0]; 617 616 let pkm = vm["publicKeyMultibase"] 618 617 .as_str() ··· 622 621 "publicKeyMultibase should start with 'z'" 623 622 ); 624 623 625 - // AC4.3: service entry has serviceEndpoint matching public_url 624 + // service entry has serviceEndpoint matching public_url 626 625 let service = &doc["service"][0]; 627 626 assert_eq!( 628 627 service["serviceEndpoint"].as_str(), ··· 630 629 "serviceEndpoint should match config.public_url" 631 630 ); 632 631 633 - // AC2.2: accounts row with correct did, email; password_hash IS NULL 632 + // accounts row with correct did, email; password_hash IS NULL 634 633 let row: Option<(String, Option<String>)> = 635 634 sqlx::query_as("SELECT email, password_hash FROM accounts WHERE did = ?") 636 635 .bind(did) ··· 644 643 "password_hash should be NULL for device-provisioned account" 645 644 ); 646 645 647 - // AC2.3: did_documents row exists with non-empty document 646 + // did_documents row exists with non-empty document 648 647 let doc_row: Option<(String,)> = 649 648 sqlx::query_as("SELECT document FROM did_documents WHERE did = ?") 650 649 .bind(did) ··· 654 653 let (document,) = doc_row.expect("did_documents row should exist"); 655 654 assert!(!document.is_empty(), "document should be non-empty"); 656 655 657 - // AC2.4: session row created with correct did and matching token_hash 656 + // session row created with correct did and matching token_hash 658 657 let session_token_str = body["session_token"].as_str().unwrap(); 659 658 let token_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD 660 659 .decode(session_token_str) ··· 675 674 let (session_did,) = session_row.expect("sessions row should exist for token_hash"); 676 675 assert_eq!(session_did, did, "sessions.did should match response did"); 677 676 678 - // AC2.4b: handles table should NOT have a row yet (handle created via POST /v1/handles) 677 + // handles table should NOT have a row yet (handle created via POST /v1/handles) 679 678 let handle_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM handles WHERE did = ?") 680 679 .bind(did) 681 680 .fetch_one(&db) ··· 686 685 "handles table should be empty after DID ceremony" 687 686 ); 688 687 689 - // AC2.5: pending_accounts and pending_sessions deleted 688 + // pending_accounts and pending_sessions deleted 690 689 let pending_count: i64 = 691 690 sqlx::query_scalar("SELECT COUNT(*) FROM pending_accounts WHERE id = ?") 692 691 .bind(&setup.account_id) ··· 703 702 .unwrap(); 704 703 assert_eq!(session_count, 0, "pending_sessions rows should be deleted"); 705 704 706 - // AC2.5: devices deleted 705 + // devices deleted 707 706 let device_count: i64 = 708 707 sqlx::query_scalar("SELECT COUNT(*) FROM devices WHERE account_id = ?") 709 708 .bind(&setup.account_id) ··· 713 712 assert_eq!(device_count, 0, "devices rows should be deleted"); 714 713 } 715 714 716 - // ── AC2.6: Retry path skips plc.directory ───────────────────────────────── 715 + // ── Retry path skips plc.directory ──────────────────────────────────────── 717 716 718 - /// MM-90.AC2.6: When pending_did already set, plc.directory is not called. 717 + /// When pending_did already set, plc.directory is not called. 719 718 #[tokio::test] 720 719 async fn retry_with_pending_did_skips_plc_directory() { 721 720 let mock_server = MockServer::start().await; ··· 817 816 assert_eq!(body["error"]["code"], "INTERNAL_ERROR"); 818 817 } 819 818 820 - // ── AC3.1: Invalid signature ─────────────────────────────────────────────── 819 + // ── Invalid signature ───────────────────────────────────────────────────── 821 820 822 - /// MM-90.AC3.1: Corrupted signature returns 400 INVALID_CLAIM. 821 + /// Corrupted signature returns 400 INVALID_CLAIM. 823 822 #[tokio::test] 824 823 async fn invalid_signature_returns_400() { 825 824 let state = test_state_for_did("https://plc.directory".to_string()).await; ··· 852 851 assert_eq!(body["error"]["code"], "INVALID_CLAIM"); 853 852 } 854 853 855 - // ── AC3.2: Wrong handle in alsoKnownAs ──────────────────────────────────── 854 + // ── Wrong handle in alsoKnownAs ─────────────────────────────────────────── 856 855 857 - /// MM-90.AC3.2: alsoKnownAs mismatch returns 400 INVALID_CLAIM. 856 + /// alsoKnownAs mismatch returns 400 INVALID_CLAIM. 858 857 #[tokio::test] 859 858 async fn wrong_handle_in_op_returns_400() { 860 859 let state = test_state_for_did("https://plc.directory".to_string()).await; ··· 882 881 assert_eq!(body["error"]["code"], "INVALID_CLAIM"); 883 882 } 884 883 885 - // ── AC3.3: Wrong service endpoint ───────────────────────────────────────── 884 + // ── Wrong service endpoint ──────────────────────────────────────────────── 886 885 887 - /// MM-90.AC3.3: services.atproto_pds.endpoint mismatch returns 400 INVALID_CLAIM. 886 + /// services.atproto_pds.endpoint mismatch returns 400 INVALID_CLAIM. 888 887 #[tokio::test] 889 888 async fn wrong_service_endpoint_returns_400() { 890 889 let state = test_state_for_did("https://plc.directory".to_string()).await; ··· 912 911 assert_eq!(body["error"]["code"], "INVALID_CLAIM"); 913 912 } 914 913 915 - // ── AC3.4: rotationKeys[0] mismatch ─────────────────────────────────────── 914 + // ── rotationKeys[0] mismatch ────────────────────────────────────────────── 916 915 917 - /// MM-90.AC3.4: rotationKeys[0] in op != rotationKeyPublic in request body → 400 INVALID_CLAIM. 916 + /// rotationKeys[0] in op != rotationKeyPublic in request body → 400 INVALID_CLAIM. 918 917 /// 919 918 /// To isolate semantic validation (not crypto failure): use kp_x as the signer 920 919 /// (signature verifies with kp_x), but put kp_y at rotationKeys[0]. Send kp_x ··· 998 997 assert_eq!(body["error"]["code"], "INVALID_CLAIM"); 999 998 } 1000 999 1001 - // ── AC3.5: Already promoted ──────────────────────────────────────────────── 1000 + // ── Already promoted ────────────────────────────────────────────────────── 1002 1001 1003 - /// MM-90.AC3.5: Account already promoted returns 409 DID_ALREADY_EXISTS. 1002 + /// Account already promoted returns 409 DID_ALREADY_EXISTS. 1004 1003 #[tokio::test] 1005 1004 async fn already_promoted_account_returns_409() { 1006 1005 let state = test_state_for_did("https://plc.directory".to_string()).await; ··· 1043 1042 assert_eq!(body["error"]["code"], "DID_ALREADY_EXISTS"); 1044 1043 } 1045 1044 1046 - // ── AC3.6: Missing auth ──────────────────────────────────────────────────── 1045 + // ── Missing auth ────────────────────────────────────────────────────────── 1047 1046 1048 - /// MM-90.AC3.6: Missing Authorization header returns 401 UNAUTHORIZED. 1047 + /// Missing Authorization header returns 401 UNAUTHORIZED. 1049 1048 #[tokio::test] 1050 1049 async fn missing_auth_returns_401() { 1051 1050 let state = test_state_for_did("https://plc.directory".to_string()).await; ··· 1074 1073 assert_eq!(body["error"]["code"], "UNAUTHORIZED"); 1075 1074 } 1076 1075 1077 - // ── AC3.7: plc.directory error ──────────────────────────────────────────── 1076 + // ── plc.directory error ─────────────────────────────────────────────────── 1078 1077 1079 - /// MM-90.AC3.7: plc.directory non-2xx returns 502 PLC_DIRECTORY_ERROR. 1078 + /// plc.directory non-2xx returns 502 PLC_DIRECTORY_ERROR. 1080 1079 #[tokio::test] 1081 1080 async fn plc_directory_error_returns_502() { 1082 1081 let mock_server = MockServer::start().await;
+2 -7
crates/relay/src/routes/create_mobile_account.rs
··· 420 420 421 421 #[tokio::test] 422 422 async fn returns_201_with_correct_shape() { 423 - // MM-84.AC1: single POST completes account + device + session setup 424 423 let state = test_state().await; 425 424 let claim_code = seed_claim_code(&state.db).await; 426 425 ··· 508 507 509 508 #[tokio::test] 510 509 async fn all_rows_persisted_in_db() { 511 - // MM-84.AC1: transaction atomicity — all three rows must be written 512 510 let state = test_state().await; 513 511 let db = state.db.clone(); 514 512 let claim_code = seed_claim_code(&state.db).await; ··· 657 655 658 656 #[tokio::test] 659 657 async fn invalid_claim_code_returns_404() { 660 - // MM-84.AC2: invalid claim code returns 404 661 658 let response = app(test_state().await) 662 659 .oneshot(post_create_mobile_account( 663 660 r#"{"email":"a@example.com","handle":"a.example.com","devicePublicKey":"dGVzdC1rZXk=","platform":"ios","claimCode":"INVALID"}"#, ··· 675 672 676 673 #[tokio::test] 677 674 async fn expired_claim_code_returns_404() { 678 - // MM-84.AC2: expired claim code returns 404 679 675 let state = test_state().await; 680 676 let code = "EXPRD001"; 681 677 ··· 706 702 707 703 #[tokio::test] 708 704 async fn already_redeemed_claim_code_returns_409() { 709 - // MM-84.AC3: already-redeemed claim code returns 409 710 705 let state = test_state().await; 711 706 let claim_code = seed_claim_code(&state.db).await; 712 707 let application = app(state); ··· 745 740 746 741 #[tokio::test] 747 742 async fn duplicate_email_pre_flight_protects_claim_code() { 748 - // MM-84.AC4: email conflict caught pre-flight — claim code must not be consumed 743 + // email conflict caught pre-flight — claim code must not be consumed 749 744 let state = test_state().await; 750 745 let db = state.db.clone(); 751 746 let claim_code = seed_claim_code(&state.db).await; ··· 783 778 784 779 #[tokio::test] 785 780 async fn duplicate_handle_pre_flight_protects_claim_code() { 786 - // MM-84.AC4: handle conflict caught pre-flight — claim code must not be consumed 781 + // handle conflict caught pre-flight — claim code must not be consumed 787 782 let state = test_state().await; 788 783 let db = state.db.clone(); 789 784 let claim_code = seed_claim_code(&db).await;
+3 -13
crates/relay/src/routes/create_signing_key.rs
··· 147 147 148 148 #[tokio::test] 149 149 async fn create_signing_key_returns_200_with_key_fields() { 150 - // MM-92.AC1.1 151 150 let response = app(test_state_with_keys().await) 152 151 .oneshot(post_keys( 153 152 r#"{"algorithm": "p256"}"#, ··· 163 162 let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); 164 163 assert!(json["keyId"].is_string(), "keyId must be present"); 165 164 assert!(json["publicKey"].is_string(), "publicKey must be present"); 166 - assert_eq!(json["algorithm"], "p256"); // MM-92.AC1.4 165 + assert_eq!(json["algorithm"], "p256"); 167 166 } 168 167 169 168 #[tokio::test] 170 169 async fn key_id_is_did_key_uri() { 171 - // MM-92.AC1.2 172 170 let response = app(test_state_with_keys().await) 173 171 .oneshot(post_keys( 174 172 r#"{"algorithm": "p256"}"#, ··· 190 188 191 189 #[tokio::test] 192 190 async fn public_key_is_multibase_base58btc() { 193 - // MM-92.AC1.3 194 191 let response = app(test_state_with_keys().await) 195 192 .oneshot(post_keys( 196 193 r#"{"algorithm": "p256"}"#, ··· 216 213 217 214 #[tokio::test] 218 215 async fn response_has_no_private_key_field() { 219 - // MM-92.AC2.1 220 216 let response = app(test_state_with_keys().await) 221 217 .oneshot(post_keys( 222 218 r#"{"algorithm": "p256"}"#, ··· 241 237 242 238 #[tokio::test] 243 239 async fn row_persisted_in_db_with_encrypted_private_key() { 244 - // MM-92.AC1.5, MM-92.AC2.2 245 240 let state = test_state_with_keys().await; 246 241 let db = state.db.clone(); 247 242 ··· 289 284 290 285 #[tokio::test] 291 286 async fn missing_authorization_header_returns_401() { 292 - // MM-92.AC4.1 293 287 let response = app(test_state_with_keys().await) 294 288 .oneshot(post_keys(r#"{"algorithm": "p256"}"#, None)) 295 289 .await ··· 299 293 300 294 #[tokio::test] 301 295 async fn wrong_bearer_token_returns_401() { 302 - // MM-92.AC4.2 303 296 let response = app(test_state_with_keys().await) 304 297 .oneshot(post_keys(r#"{"algorithm": "p256"}"#, Some("wrong-token"))) 305 298 .await ··· 309 302 310 303 #[tokio::test] 311 304 async fn bare_token_without_bearer_prefix_returns_401() { 312 - // MM-92.AC4.3: Authorization header present but "Bearer " prefix missing 305 + // Authorization header present but "Bearer " prefix missing 313 306 let request = Request::builder() 314 307 .method("POST") 315 308 .uri("/v1/relay/keys") ··· 329 322 330 323 #[tokio::test] 331 324 async fn unsupported_algorithm_returns_422() { 332 - // MM-92.AC5.1: serde rejects unknown enum variant with 422 Unprocessable Entity 333 325 let response = app(test_state_with_keys().await) 334 326 .oneshot(post_keys( 335 327 r#"{"algorithm": "k256"}"#, ··· 342 334 343 335 #[tokio::test] 344 336 async fn empty_algorithm_returns_422() { 345 - // MM-92.AC5.2: serde rejects empty string for enum variant with 422 Unprocessable Entity 346 337 let response = app(test_state_with_keys().await) 347 338 .oneshot(post_keys(r#"{"algorithm": ""}"#, Some("test-admin-token"))) 348 339 .await ··· 352 343 353 344 #[tokio::test] 354 345 async fn missing_algorithm_field_returns_422() { 355 - // MM-92.AC5.3: missing required field returns 422 Unprocessable Entity 356 346 let response = app(test_state_with_keys().await) 357 347 .oneshot(post_keys(r#"{}"#, Some("test-admin-token"))) 358 348 .await ··· 379 369 380 370 #[tokio::test] 381 371 async fn missing_master_key_returns_503() { 382 - // MM-92.AC6.1: valid Bearer token, but signing_key_master_key not configured → 503 372 + // signing_key_master_key not configured → 503 383 373 let base = test_state().await; 384 374 let mut config = (*base.config).clone(); 385 375 config.admin_token = Some("test-admin-token".to_string());
+4 -8
crates/relay/src/routes/register_device.rs
··· 261 261 262 262 #[tokio::test] 263 263 async fn returns_201_with_correct_shape() { 264 - // MM-87.AC1: valid claim code registers device and returns credentials 265 264 let state = test_state().await; 266 265 let (_, claim_code) = seed_pending_account(&state.db).await; 267 266 ··· 351 350 352 351 #[tokio::test] 353 352 async fn account_id_matches_pending_account() { 354 - // MM-87.AC1: returned account_id matches the pending account bound to the claim code 353 + // returned account_id matches the pending account bound to the claim code 355 354 let state = test_state().await; 356 355 let (expected_account_id, claim_code) = seed_pending_account(&state.db).await; 357 356 ··· 373 372 374 373 #[tokio::test] 375 374 async fn device_persisted_in_db() { 376 - // MM-87.AC3: device appears in account's device list after registration 377 375 let state = test_state().await; 378 376 let db = state.db.clone(); 379 377 let (account_id, claim_code) = seed_pending_account(&state.db).await; ··· 454 452 455 453 #[tokio::test] 456 454 async fn claim_code_marked_redeemed_after_registration() { 457 - // MM-87 requirement: claim code is single-use; marked redeemed on success 455 + // claim code is single-use; marked redeemed on success 458 456 let state = test_state().await; 459 457 let db = state.db.clone(); 460 458 let (_, claim_code) = seed_pending_account(&state.db).await; ··· 532 530 533 531 #[tokio::test] 534 532 async fn invalid_claim_code_returns_400() { 535 - // MM-87.AC2: invalid code returns error 536 533 let response = app(test_state().await) 537 534 .oneshot(post_register_device( 538 535 r#"{"claimCode":"ZZZZZZ","devicePublicKey":"dGVzdC1rZXk=","platform":"ios"}"#, ··· 550 547 551 548 #[tokio::test] 552 549 async fn expired_claim_code_returns_400() { 553 - // MM-87.AC2: expired code returns error 554 550 let state = test_state().await; 555 551 let account_id = uuid::Uuid::new_v4().to_string(); 556 552 let claim_code = "EXPIRD1"; ··· 592 588 593 589 #[tokio::test] 594 590 async fn already_redeemed_claim_code_returns_400() { 595 - // MM-87 requirement: claim code is single-use; second use returns error 591 + // claim code is single-use; second use returns error 596 592 let state = test_state().await; 597 593 let (_, claim_code) = seed_pending_account(&state.db).await; 598 594 ··· 626 622 627 623 #[tokio::test] 628 624 async fn all_valid_platforms_accepted() { 629 - // MM-87 requirement: platform validation (ios, android, macos, linux, windows) 625 + // platform validation (ios, android, macos, linux, windows) 630 626 for platform in ["ios", "android", "macos", "linux", "windows"] { 631 627 let state = test_state().await; 632 628 let (_, claim_code) = seed_pending_account(&state.db).await;