commits
Add a Troubleshooting section to apps/identity-wallet/CLAUDE.md covering
all five error classes encountered when running cargo tauri ios dev in the
Nix devenv: missing iOS stdlib, simctl not found, -mmacos-version-min
conflict, libiconv not found, and UIKit framework not found.
Also document:
- cargo tauri ios dev must orchestrate the build (not Xcode's Run button)
- nix develop must be run from workspace root
- src-tauri/.cargo/config.toml and why it exists
Update root CLAUDE.md to reflect the switch from languages.rust to rustup
and document DEVELOPER_DIR re-export.
Four interacting issues prevented `cargo tauri ios dev` from working in the
Nix devenv environment:
1. languages.rust used Nix's rust-default which ships no iOS target stdlibs.
Fix: replace with pkgs.rustup + project-local RUSTUP_HOME/CARGO_HOME so
rustup reads rust-toolchain.toml and installs aarch64-apple-ios-sim stdlib.
2. Nix Darwin setup hooks override DEVELOPER_DIR to a stub apple-sdk with no
runtime tools (xcrun simctl fails with "tool not found").
Fix: re-export DEVELOPER_DIR to real Xcode.app in enterShell, which runs
after all Nix hooks and wins.
3. Nix cc-wrapper injects -mmacos-version-min into iOS target CC invocations,
which clang rejects alongside -mios-simulator-version-min.
Fix: src-tauri/.cargo/config.toml sets CC_aarch64_apple_ios_sim to
Xcode's unwrapped clang for build scripts.
4. Nix cc-wrapper uses macOS sysroot for the iOS target final link, failing to
find UIKit and other iOS frameworks.
Fix: .cargo/config.toml sets [target.aarch64-apple-ios-sim].linker (and
aarch64-apple-darwin for proc-macro host builds) to Xcode's clang.
- Remove security.csp=null from tauri.conf.json — restores Tauri's
restrictive default CSP (critical for an identity wallet)
- Fix package.json version 0.0.1 → 0.1.0 to match workspace/tauri.conf
- Fix vite.config.ts host fallback '0.0.0.0' → 'localhost' so standalone
pnpm dev does not expose the dev server to the LAN
- Add #[cfg(test)] block to lib.rs with 3 greet tests (normal, empty,
special chars) establishing the testing pattern for src-tauri
- Commit docs/implementation-plans/2026-03-14-MM-143/ (was untracked)
The flake.nix buildDepsOnly call is scoped to relay-related crates to
avoid compiling Tauri's native dependencies in Nix. This boundary is
important for future workspace additions and belongs in CLAUDE.md.
All three occurrences of 'out: dist' in apps/identity-wallet/CLAUDE.md have been updated to 'pages: dist' to match the actual adapter-static 3.x API used in svelte.config.js:
- Line 19: Updated in Guarantees section (Build output)
- Line 102: Updated in Key Decisions section
- Line 122: Updated in Key Files section (svelte.config.js entry)
The actual svelte.config.js uses the correct 'pages' property; documentation now matches implementation.
- Issue 1 [Critical]: Add @types/node to devDependencies in apps/identity-wallet/package.json to fix 45 svelte-check type errors. Updated to ^22, reinstalled, verified svelte-check passes with 0 errors.
- Issue 2 [Important]: Add pnpm, cargo-tauri, and nodejs_22 to devenv.nix packages list. Phase 3 planned these additions; added early to unblock Phase 2's tauri.conf.json beforeDevCommand/beforeBuildCommand.
- Issue 3 [Minor]: Comment out [profile.release] block in apps/identity-wallet/src-tauri/Cargo.toml to suppress Cargo warning about profiles in non-root workspace packages. Preserved as documentation per design plan.
- Issue 1: Remove package-lock.json from tracking (npm lockfile committed instead of pnpm-lock.yaml). Add apps/identity-wallet/package-lock.json to .gitignore and remove from git tracking. pnpm-lock.yaml generation deferred to Phase 3 when pnpm is added to Nix dev shell.
- Issue 2: Update @sveltejs/vite-plugin-svelte from ^3.1.0 to ^5.0.3 as specified in plan. Resolves Svelte 5 build warnings about version mismatch.
Verification:
- cargo build --workspace: SUCCESS
- cargo clippy --workspace -- -D warnings: SUCCESS
- cargo fmt --all --check: SUCCESS
Covers SvelteKit 2 + Svelte 5 frontend, Tauri v2 Rust backend, Cargo
workspace integration, Nix devenv additions, and suggested CI pipeline.
3 implementation phases.
HTTP well-known resolution is not a DNS concern. Splits dns.rs by protocol:
- dns.rs: TxtResolver, HickoryTxtResolver, DnsProvider, DnsError
- well_known.rs (new): WellKnownResolver, HttpWellKnownResolver, WellKnownError
Updates all import sites: app.rs, main.rs, routes/resolve_handle.rs.
No behaviour changes.
Critical fix: NXDOMAIN in HickoryTxtResolver now returns Ok(vec![]) instead
of Err, so DNS absence falls through to HTTP well-known → HandleNotFound (404)
rather than returning 500. Uses e.is_no_records_found() to distinguish
NXDOMAIN/NODATA from genuine infrastructure failures.
Also:
- Log tracing::warn! for non-UTF-8 TXT bytes instead of silently dropping
- Add ErrTxtResolver test double + dns_infrastructure_error_returns_500
to pin the 500 behavior for real DNS failures
- Add CapturingTxtResolver + dns_lookup_uses_atproto_prefix to verify
the _atproto. prefix is sent in DNS queries
- Add local_db_takes_priority_over_dns: seeds both DB and DNS with
different DIDs, asserts DB result wins
- Add body assertion to unknown_handle_with_empty_dns_response_returns_404
Extends handle resolution to three-step priority chain:
local DB → DNS TXT → HTTP well-known (/.well-known/atproto-did).
Adds WellKnownResolver trait and HttpWellKnownResolver production impl
to dns.rs. Reverts atproto-identity dep (0.8 API uses concrete
TokioResolver, not injectable), restoring hickory-resolver for DNS TXT.
Wires HttpWellKnownResolver into AppState at startup; tests use
FixedWellKnownResolver mock. Covers: well-known resolves DID, returns
404 when absent, DNS takes priority over well-known.
Resolves an ATProto handle to a DID via local DB lookup with DNS TXT
fallback. Adds TxtResolver trait + HickoryTxtResolver production impl,
HandleNotFound error code, and txt_resolver field on AppState.
- Swap DNS/INSERT order: INSERT before create_record so a DB row without a
DNS record is recoverable; a DNS record without a row is an invisible orphan
- Drop SELECT EXISTS pre-check; detect is_unique_violation() on INSERT directly
so a concurrent UNIQUE conflict returns 409 HANDLE_TAKEN instead of 500
- Remove duplicate HandleAlreadyExists error code; use HandleTaken everywhere
- Strip scheme from public_url before passing hostname to DnsProvider
- Add tracing::debug! before base64 decode failure and session lookup miss in
require_session (silent auth errors were unobservable in operator logs)
- Add did field to DNS error tracing::error! for per-account correlation
- Enforce RFC 1035 63-char DNS label limit in validate_handle
- Add DnsError to status_code_mapping test
- Add AlwaysOkDns/AlwaysErrDns test doubles + DNS success/failure integration tests
- Add 4 require_session unit tests (missing header, bad base64, valid, expired)
- Add POST /v1/handles handler (create_handle.rs): validates handle format
(<name>.<available_domain>), enforces uniqueness (409), optionally calls
DnsProvider if configured, inserts into handles table, returns
{ handle, dns_status, did }
- Add DnsProvider trait (dns.rs): object-safe async abstraction using
Pin<Box<dyn Future>>; AppState carries Option<Arc<dyn DnsProvider>>,
always None for v0.1 (MM-142 wires in Cloudflare/Route53)
- Add require_session + SessionInfo to auth.rs: mirrors require_pending_session
but queries sessions table by token_hash for promoted-account Bearer auth
- V009 migration (sessions_v2): rebuilds sessions table with nullable device_id
(devices are deleted at DID promotion) and adds token_hash UNIQUE column
- Modify POST /v1/dids (MM-90): remove shortcut handle insertion; add session
token generation and INSERT sessions in promotion transaction; add session_token
to response so client can immediately call POST /v1/handles
- Add HandleAlreadyExists (409) and DnsError (502) error codes to common
- Add bruno/create_handle.bru (seq 9) for manual API testing
Critical fixes:
- C1: Remove crypto error detail from client response (opaques security oracle)
Changed: 'invalid signed genesis op: {e}' → 'signed genesis op is invalid'
Server-side logging still captures full error detail
Important fixes:
- I1: Replace unwrap_or_default() on service_endpoint with proper error handling
Prevents silent DID document with empty serviceEndpoint
Returns 500 if service endpoint is missing in verified op
- I2: Handle UNIQUE constraint violation on INSERT accounts as 409 not 500
Added is_unique_violation() helper to detect constraint violations
Returns 409 DID_ALREADY_EXISTS instead of 500 INTERNAL_ERROR
- I3: Check rows_affected() on UPDATE pending_accounts SET pending_did
Detects if pending_accounts row vanished during pre-store phase
Returns error if zero rows affected (race condition detection)
- I4: Add explicit emptiness checks for rotation_keys and also_known_as arrays
Checks array is non-empty BEFORE calling first()
Returns specific error for empty arrays vs. element mismatch
Test coverage:
- G2: Add test for retry with mismatched pending_did (tampered retry)
Verifies that DID mismatch returns 500 INTERNAL_ERROR
- G3: Add device row deletion assertion to happy_path test
Verifies devices table cleanup during account promotion
- G4: Add test for malformed rotationKeyPublic format
Verifies format validation (must start with 'did:key:z')
Returns 400 INVALID_CLAIM with valid session token
Note: G5 (expired session coverage) already exists in auth.rs
(pending_session_expired_session_returns_401 test at line 321)
All tests pass: 274 total tests
No clippy warnings, cargo fmt clean
Critical:
- [C2] Validate prev=null and op_type="plc_operation" in verify_genesis_op
immediately after parsing, rejecting rotation ops and non-genesis types.
Prevents clients from submitting rotation ops that bypass plc.directory
validation, which would leave accounts stuck.
Important:
- [I5] Document rotation_key caller obligation in verify_genesis_op docstring:
"The caller is responsible for verifying that the provided key appears in
the op's rotationKeys array; this function only checks that the signature
was made by that key."
Type Design:
- [T1] Apply #[non_exhaustive] to VerifiedGenesisOp to prevent struct-literal
construction outside plc.rs module. Ensures construction only via
verify_genesis_op.
- [T2] Apply #[non_exhaustive] to PlcGenesisOp to prevent struct-literal
construction outside plc.rs module. Ensures construction only via
build_did_plc_genesis_op.
- [T3] Promote P256_MULTICODEC_PREFIX to pub(crate) in keys.rs and import
in plc.rs. Eliminates silent divergence risk between two copies of the
same constant.
- [T4] Improve rotation_key parameter docstring in verify_genesis_op to
clarify its purpose: "The key that must have signed the unsigned operation
— the caller determines which of the op's rotation keys to verify against."
Tests:
- [G1] Add verify_rotation_op_with_non_null_prev_returns_error test
covering prev != null rejection.
- [G1] Add verify_non_genesis_op_type_returns_error test covering non-
"plc_operation" type rejection.
- [G1] Add verify_rotation_key_can_verify_own_op test for canonical usage
pattern where the same keypair both signs and appears at rotationKeys[0].
This was missing coverage — prior tests verified with signing_key only.
Add verify_genesis_op and VerifiedGenesisOp to crypto CLAUDE.md
contracts. Update root CLAUDE.md crypto description to mention
verification.
- Fixed alignment of trailing comments in function call arguments
- Normalized spacing after commas (removed excessive space for alignment)
- Reformatted multi-line assert! macro calls for consistency
- Improved line wrapping for method chains and struct literals
All formatting violations (approx. 18) in the test module have been resolved.
Replaces entire #[cfg(test)] mod tests block in crates/relay/src/routes/create_did.rs.
Changes:
- Remove TEST_MASTER_KEY constant (not needed for device-signed ceremony)
- Remove relay_signing_key insertion from insert_test_data
- Add make_signed_op() helper using crypto::build_did_plc_genesis_op
- Simplify TestSetup struct (remove signing_key_id, rotation_key_id)
- Simplify test_state_for_did (no signing_key_master_key manipulation)
- Update create_did_request to use new MM-90 request shape (rotationKeyPublic, signedCreationOp)
Replaces 7 MM-89 test functions with 9 MM-90 test functions:
✓ happy_path_promotes_account_and_returns_did (AC2.1/2.2/2.3/2.4/2.5/4.1/4.2/4.3)
✓ retry_with_pending_did_skips_plc_directory (AC2.6)
✓ invalid_signature_returns_400 (AC3.1)
✓ wrong_handle_in_op_returns_400 (AC3.2)
✓ wrong_service_endpoint_returns_400 (AC3.3)
✓ wrong_rotation_key_in_op_returns_400 (AC3.4)
✓ already_promoted_account_returns_409 (AC3.5)
✓ missing_auth_returns_401 (AC3.6)
✓ plc_directory_error_returns_502 (AC3.7)
All 185 relay tests pass. All workspace tests pass. Clippy: 0 warnings.
Device-signing model replaces relay-signing. Key decisions:
- verify_genesis_op() returns typed VerifiedGenesisOp (signature + DID derivation in one pure fn)
- Strict semantic validation: rotationKeys[0], alsoKnownAs, services endpoint
- In-house CBOR (ciborium) for byte-level consistency with MM-89 signing
- Relay key absent from genesis op; added later via key rotation
- 2 implementation phases: crypto crate, then relay route replacement
Critical: correct V006 migration comment — SQLite does not auto-update FK
references in child tables on RENAME; the migration is safe because all
tables are empty (no DML-time FK checks fire).
Important:
- Add UNIQUE INDEX idx_devices_token_hash on devices.device_token_hash
- Add max-length check (512 chars) on devicePublicKey input
- Add #[tracing::instrument] + claim_code field to redeem_and_register;
distinguish RowNotFound from other errors in log messages
- Fix seed_pending_account helper to generate unique codes/email/handle
per call so it is safe to invoke multiple times on the same pool
- Add orphaned_claim_code_returns_500_and_does_not_redeem_code test
(verifies atomicity: transaction rolls back if pending_accounts lookup
fails, leaving claim code unredeemed)
- Extend closed_db_pool_returns_500 and platform_is_case_sensitive tests
to assert error code in response body
- Add oversized_public_key_returns_400 test
V003 and V006 migrations have dedicated PRAGMA table_info tests verifying column
presence and types. V008 (nullable password_hash on accounts, pending_did column
on pending_accounts) had none.
Added four new tests:
1. v008_accounts_password_hash_is_nullable - Verify password_hash has notnull=0
2. v008_pending_accounts_has_pending_did_column - Verify pending_did column exists
3. v008_accounts_can_insert_null_password_hash - Test nullable behavior
4. v008_pending_accounts_pending_did_nullable_and_updatable - Test NULL and UPDATE
All tests use PRAGMA table_info to examine schema and verify migration correctness.
reqwest::Client::new() creates a client with no timeout. A hung plc.directory
connection never errors and holds pending_did pre-stored but unregistered forever.
Replace Client::new() with Client::builder().timeout(Duration::from_secs(10)).build()
in:
- main.rs: Production HTTP client initialization
- app.rs: test_state_with_plc_url test helper
- create_did.rs: test_state_for_did test helper
10 second timeout is reasonable for most PLC directory operations.
require_admin_token uses inspect_err to log non-UTF-8 header encoding issues,
but require_pending_session silently dropped such errors with no logging.
Applied the same pattern from require_admin_token to require_pending_session
to ensure consistent behavior across both auth functions.
- Critical #1: Retry path ignores pre-stored pending_did
Added comparison between derived DID and pre-stored DID on retry path.
If they don't match, return InternalError explaining the mismatch.
This prevents undetected DID mismatches when client inputs change between attempts.
- Critical #2: PLC directory response body never logged
After checking !response.status().is_success(), now consume the response body
with response.text().await and include it in the tracing::error! log.
Operators will now see the actual error response instead of just the HTTP status.
- Updated retry test to pre-store the actually-derived DID so it matches
what the handler will re-derive on the retry path.
- [Critical] Fix formatting in auth.rs (lines 91-128) and create_did.rs
(multiple locations): run cargo fmt --all to auto-format both files
to comply with CI gate requirements.
- [Minor] Remove unnecessary #[allow(dead_code)] from AppState.http_client
field in app.rs line 81. The field is actively used by create_did_handler
at create_did.rs:161 for plc.directory HTTP calls.
Completed brainstorming session. Design includes:
- crypto crate: pure build_did_plc_genesis_op function (CBOR, ECDSA P-256, RFC 6979, base32 DID derivation)
- relay crate: POST /v1/dids with pending_session auth, pre-store retry resilience, atomic account promotion
- 2 implementation phases
Add a Troubleshooting section to apps/identity-wallet/CLAUDE.md covering
all five error classes encountered when running cargo tauri ios dev in the
Nix devenv: missing iOS stdlib, simctl not found, -mmacos-version-min
conflict, libiconv not found, and UIKit framework not found.
Also document:
- cargo tauri ios dev must orchestrate the build (not Xcode's Run button)
- nix develop must be run from workspace root
- src-tauri/.cargo/config.toml and why it exists
Update root CLAUDE.md to reflect the switch from languages.rust to rustup
and document DEVELOPER_DIR re-export.
Four interacting issues prevented `cargo tauri ios dev` from working in the
Nix devenv environment:
1. languages.rust used Nix's rust-default which ships no iOS target stdlibs.
Fix: replace with pkgs.rustup + project-local RUSTUP_HOME/CARGO_HOME so
rustup reads rust-toolchain.toml and installs aarch64-apple-ios-sim stdlib.
2. Nix Darwin setup hooks override DEVELOPER_DIR to a stub apple-sdk with no
runtime tools (xcrun simctl fails with "tool not found").
Fix: re-export DEVELOPER_DIR to real Xcode.app in enterShell, which runs
after all Nix hooks and wins.
3. Nix cc-wrapper injects -mmacos-version-min into iOS target CC invocations,
which clang rejects alongside -mios-simulator-version-min.
Fix: src-tauri/.cargo/config.toml sets CC_aarch64_apple_ios_sim to
Xcode's unwrapped clang for build scripts.
4. Nix cc-wrapper uses macOS sysroot for the iOS target final link, failing to
find UIKit and other iOS frameworks.
Fix: .cargo/config.toml sets [target.aarch64-apple-ios-sim].linker (and
aarch64-apple-darwin for proc-macro host builds) to Xcode's clang.
- Remove security.csp=null from tauri.conf.json — restores Tauri's
restrictive default CSP (critical for an identity wallet)
- Fix package.json version 0.0.1 → 0.1.0 to match workspace/tauri.conf
- Fix vite.config.ts host fallback '0.0.0.0' → 'localhost' so standalone
pnpm dev does not expose the dev server to the LAN
- Add #[cfg(test)] block to lib.rs with 3 greet tests (normal, empty,
special chars) establishing the testing pattern for src-tauri
- Commit docs/implementation-plans/2026-03-14-MM-143/ (was untracked)
All three occurrences of 'out: dist' in apps/identity-wallet/CLAUDE.md have been updated to 'pages: dist' to match the actual adapter-static 3.x API used in svelte.config.js:
- Line 19: Updated in Guarantees section (Build output)
- Line 102: Updated in Key Decisions section
- Line 122: Updated in Key Files section (svelte.config.js entry)
The actual svelte.config.js uses the correct 'pages' property; documentation now matches implementation.
- Issue 1 [Critical]: Add @types/node to devDependencies in apps/identity-wallet/package.json to fix 45 svelte-check type errors. Updated to ^22, reinstalled, verified svelte-check passes with 0 errors.
- Issue 2 [Important]: Add pnpm, cargo-tauri, and nodejs_22 to devenv.nix packages list. Phase 3 planned these additions; added early to unblock Phase 2's tauri.conf.json beforeDevCommand/beforeBuildCommand.
- Issue 3 [Minor]: Comment out [profile.release] block in apps/identity-wallet/src-tauri/Cargo.toml to suppress Cargo warning about profiles in non-root workspace packages. Preserved as documentation per design plan.
- Issue 1: Remove package-lock.json from tracking (npm lockfile committed instead of pnpm-lock.yaml). Add apps/identity-wallet/package-lock.json to .gitignore and remove from git tracking. pnpm-lock.yaml generation deferred to Phase 3 when pnpm is added to Nix dev shell.
- Issue 2: Update @sveltejs/vite-plugin-svelte from ^3.1.0 to ^5.0.3 as specified in plan. Resolves Svelte 5 build warnings about version mismatch.
Verification:
- cargo build --workspace: SUCCESS
- cargo clippy --workspace -- -D warnings: SUCCESS
- cargo fmt --all --check: SUCCESS
HTTP well-known resolution is not a DNS concern. Splits dns.rs by protocol:
- dns.rs: TxtResolver, HickoryTxtResolver, DnsProvider, DnsError
- well_known.rs (new): WellKnownResolver, HttpWellKnownResolver, WellKnownError
Updates all import sites: app.rs, main.rs, routes/resolve_handle.rs.
No behaviour changes.
Critical fix: NXDOMAIN in HickoryTxtResolver now returns Ok(vec![]) instead
of Err, so DNS absence falls through to HTTP well-known → HandleNotFound (404)
rather than returning 500. Uses e.is_no_records_found() to distinguish
NXDOMAIN/NODATA from genuine infrastructure failures.
Also:
- Log tracing::warn! for non-UTF-8 TXT bytes instead of silently dropping
- Add ErrTxtResolver test double + dns_infrastructure_error_returns_500
to pin the 500 behavior for real DNS failures
- Add CapturingTxtResolver + dns_lookup_uses_atproto_prefix to verify
the _atproto. prefix is sent in DNS queries
- Add local_db_takes_priority_over_dns: seeds both DB and DNS with
different DIDs, asserts DB result wins
- Add body assertion to unknown_handle_with_empty_dns_response_returns_404
Extends handle resolution to three-step priority chain:
local DB → DNS TXT → HTTP well-known (/.well-known/atproto-did).
Adds WellKnownResolver trait and HttpWellKnownResolver production impl
to dns.rs. Reverts atproto-identity dep (0.8 API uses concrete
TokioResolver, not injectable), restoring hickory-resolver for DNS TXT.
Wires HttpWellKnownResolver into AppState at startup; tests use
FixedWellKnownResolver mock. Covers: well-known resolves DID, returns
404 when absent, DNS takes priority over well-known.
- Swap DNS/INSERT order: INSERT before create_record so a DB row without a
DNS record is recoverable; a DNS record without a row is an invisible orphan
- Drop SELECT EXISTS pre-check; detect is_unique_violation() on INSERT directly
so a concurrent UNIQUE conflict returns 409 HANDLE_TAKEN instead of 500
- Remove duplicate HandleAlreadyExists error code; use HandleTaken everywhere
- Strip scheme from public_url before passing hostname to DnsProvider
- Add tracing::debug! before base64 decode failure and session lookup miss in
require_session (silent auth errors were unobservable in operator logs)
- Add did field to DNS error tracing::error! for per-account correlation
- Enforce RFC 1035 63-char DNS label limit in validate_handle
- Add DnsError to status_code_mapping test
- Add AlwaysOkDns/AlwaysErrDns test doubles + DNS success/failure integration tests
- Add 4 require_session unit tests (missing header, bad base64, valid, expired)
- Add POST /v1/handles handler (create_handle.rs): validates handle format
(<name>.<available_domain>), enforces uniqueness (409), optionally calls
DnsProvider if configured, inserts into handles table, returns
{ handle, dns_status, did }
- Add DnsProvider trait (dns.rs): object-safe async abstraction using
Pin<Box<dyn Future>>; AppState carries Option<Arc<dyn DnsProvider>>,
always None for v0.1 (MM-142 wires in Cloudflare/Route53)
- Add require_session + SessionInfo to auth.rs: mirrors require_pending_session
but queries sessions table by token_hash for promoted-account Bearer auth
- V009 migration (sessions_v2): rebuilds sessions table with nullable device_id
(devices are deleted at DID promotion) and adds token_hash UNIQUE column
- Modify POST /v1/dids (MM-90): remove shortcut handle insertion; add session
token generation and INSERT sessions in promotion transaction; add session_token
to response so client can immediately call POST /v1/handles
- Add HandleAlreadyExists (409) and DnsError (502) error codes to common
- Add bruno/create_handle.bru (seq 9) for manual API testing
Critical fixes:
- C1: Remove crypto error detail from client response (opaques security oracle)
Changed: 'invalid signed genesis op: {e}' → 'signed genesis op is invalid'
Server-side logging still captures full error detail
Important fixes:
- I1: Replace unwrap_or_default() on service_endpoint with proper error handling
Prevents silent DID document with empty serviceEndpoint
Returns 500 if service endpoint is missing in verified op
- I2: Handle UNIQUE constraint violation on INSERT accounts as 409 not 500
Added is_unique_violation() helper to detect constraint violations
Returns 409 DID_ALREADY_EXISTS instead of 500 INTERNAL_ERROR
- I3: Check rows_affected() on UPDATE pending_accounts SET pending_did
Detects if pending_accounts row vanished during pre-store phase
Returns error if zero rows affected (race condition detection)
- I4: Add explicit emptiness checks for rotation_keys and also_known_as arrays
Checks array is non-empty BEFORE calling first()
Returns specific error for empty arrays vs. element mismatch
Test coverage:
- G2: Add test for retry with mismatched pending_did (tampered retry)
Verifies that DID mismatch returns 500 INTERNAL_ERROR
- G3: Add device row deletion assertion to happy_path test
Verifies devices table cleanup during account promotion
- G4: Add test for malformed rotationKeyPublic format
Verifies format validation (must start with 'did:key:z')
Returns 400 INVALID_CLAIM with valid session token
Note: G5 (expired session coverage) already exists in auth.rs
(pending_session_expired_session_returns_401 test at line 321)
All tests pass: 274 total tests
No clippy warnings, cargo fmt clean
Critical:
- [C2] Validate prev=null and op_type="plc_operation" in verify_genesis_op
immediately after parsing, rejecting rotation ops and non-genesis types.
Prevents clients from submitting rotation ops that bypass plc.directory
validation, which would leave accounts stuck.
Important:
- [I5] Document rotation_key caller obligation in verify_genesis_op docstring:
"The caller is responsible for verifying that the provided key appears in
the op's rotationKeys array; this function only checks that the signature
was made by that key."
Type Design:
- [T1] Apply #[non_exhaustive] to VerifiedGenesisOp to prevent struct-literal
construction outside plc.rs module. Ensures construction only via
verify_genesis_op.
- [T2] Apply #[non_exhaustive] to PlcGenesisOp to prevent struct-literal
construction outside plc.rs module. Ensures construction only via
build_did_plc_genesis_op.
- [T3] Promote P256_MULTICODEC_PREFIX to pub(crate) in keys.rs and import
in plc.rs. Eliminates silent divergence risk between two copies of the
same constant.
- [T4] Improve rotation_key parameter docstring in verify_genesis_op to
clarify its purpose: "The key that must have signed the unsigned operation
— the caller determines which of the op's rotation keys to verify against."
Tests:
- [G1] Add verify_rotation_op_with_non_null_prev_returns_error test
covering prev != null rejection.
- [G1] Add verify_non_genesis_op_type_returns_error test covering non-
"plc_operation" type rejection.
- [G1] Add verify_rotation_key_can_verify_own_op test for canonical usage
pattern where the same keypair both signs and appears at rotationKeys[0].
This was missing coverage — prior tests verified with signing_key only.
- Fixed alignment of trailing comments in function call arguments
- Normalized spacing after commas (removed excessive space for alignment)
- Reformatted multi-line assert! macro calls for consistency
- Improved line wrapping for method chains and struct literals
All formatting violations (approx. 18) in the test module have been resolved.
Replaces entire #[cfg(test)] mod tests block in crates/relay/src/routes/create_did.rs.
Changes:
- Remove TEST_MASTER_KEY constant (not needed for device-signed ceremony)
- Remove relay_signing_key insertion from insert_test_data
- Add make_signed_op() helper using crypto::build_did_plc_genesis_op
- Simplify TestSetup struct (remove signing_key_id, rotation_key_id)
- Simplify test_state_for_did (no signing_key_master_key manipulation)
- Update create_did_request to use new MM-90 request shape (rotationKeyPublic, signedCreationOp)
Replaces 7 MM-89 test functions with 9 MM-90 test functions:
✓ happy_path_promotes_account_and_returns_did (AC2.1/2.2/2.3/2.4/2.5/4.1/4.2/4.3)
✓ retry_with_pending_did_skips_plc_directory (AC2.6)
✓ invalid_signature_returns_400 (AC3.1)
✓ wrong_handle_in_op_returns_400 (AC3.2)
✓ wrong_service_endpoint_returns_400 (AC3.3)
✓ wrong_rotation_key_in_op_returns_400 (AC3.4)
✓ already_promoted_account_returns_409 (AC3.5)
✓ missing_auth_returns_401 (AC3.6)
✓ plc_directory_error_returns_502 (AC3.7)
All 185 relay tests pass. All workspace tests pass. Clippy: 0 warnings.
Device-signing model replaces relay-signing. Key decisions:
- verify_genesis_op() returns typed VerifiedGenesisOp (signature + DID derivation in one pure fn)
- Strict semantic validation: rotationKeys[0], alsoKnownAs, services endpoint
- In-house CBOR (ciborium) for byte-level consistency with MM-89 signing
- Relay key absent from genesis op; added later via key rotation
- 2 implementation phases: crypto crate, then relay route replacement
Critical: correct V006 migration comment — SQLite does not auto-update FK
references in child tables on RENAME; the migration is safe because all
tables are empty (no DML-time FK checks fire).
Important:
- Add UNIQUE INDEX idx_devices_token_hash on devices.device_token_hash
- Add max-length check (512 chars) on devicePublicKey input
- Add #[tracing::instrument] + claim_code field to redeem_and_register;
distinguish RowNotFound from other errors in log messages
- Fix seed_pending_account helper to generate unique codes/email/handle
per call so it is safe to invoke multiple times on the same pool
- Add orphaned_claim_code_returns_500_and_does_not_redeem_code test
(verifies atomicity: transaction rolls back if pending_accounts lookup
fails, leaving claim code unredeemed)
- Extend closed_db_pool_returns_500 and platform_is_case_sensitive tests
to assert error code in response body
- Add oversized_public_key_returns_400 test
V003 and V006 migrations have dedicated PRAGMA table_info tests verifying column
presence and types. V008 (nullable password_hash on accounts, pending_did column
on pending_accounts) had none.
Added four new tests:
1. v008_accounts_password_hash_is_nullable - Verify password_hash has notnull=0
2. v008_pending_accounts_has_pending_did_column - Verify pending_did column exists
3. v008_accounts_can_insert_null_password_hash - Test nullable behavior
4. v008_pending_accounts_pending_did_nullable_and_updatable - Test NULL and UPDATE
All tests use PRAGMA table_info to examine schema and verify migration correctness.
reqwest::Client::new() creates a client with no timeout. A hung plc.directory
connection never errors and holds pending_did pre-stored but unregistered forever.
Replace Client::new() with Client::builder().timeout(Duration::from_secs(10)).build()
in:
- main.rs: Production HTTP client initialization
- app.rs: test_state_with_plc_url test helper
- create_did.rs: test_state_for_did test helper
10 second timeout is reasonable for most PLC directory operations.
- Critical #1: Retry path ignores pre-stored pending_did
Added comparison between derived DID and pre-stored DID on retry path.
If they don't match, return InternalError explaining the mismatch.
This prevents undetected DID mismatches when client inputs change between attempts.
- Critical #2: PLC directory response body never logged
After checking !response.status().is_success(), now consume the response body
with response.text().await and include it in the tracing::error! log.
Operators will now see the actual error response instead of just the HTTP status.
- Updated retry test to pre-store the actually-derived DID so it matches
what the handler will re-derive on the retry path.
- [Critical] Fix formatting in auth.rs (lines 91-128) and create_did.rs
(multiple locations): run cargo fmt --all to auto-format both files
to comply with CI gate requirements.
- [Minor] Remove unnecessary #[allow(dead_code)] from AppState.http_client
field in app.rs line 81. The field is actively used by create_did_handler
at create_did.rs:161 for plc.directory HTTP calls.