commits
Eliminate duplicated patterns identified in a full codebase scan:
- P1: New `routes::token` module — `generate_token()`, `sha256_hex()`,
`hash_bearer_token()` replace 10+ copy-pasted token generation/hashing
blocks across route handlers and auth functions.
- P2: Centralize `is_unique_violation()` and `unique_violation_column()`
in `db::mod` — replaces 4 divergent implementations of SQLite constraint
classification in claim_codes, create_account, create_did, and
create_mobile_account.
- P3: New `routes::uniqueness` module — `email_taken()` / `handle_taken()`
extract the identical OR EXISTS pre-flight queries from create_account
and create_mobile_account.
- P4: Replace string-based `is_valid_platform()` with a `Platform` enum
that deserializes via serde, moving validation to the type system.
- P5: Extract `base32_lowercase()` helper in crypto::plc to deduplicate
the base32 encoding setup used in both build and verify paths.
- P6: Break up the 270-line `create_did_handler` into a 45-line
orchestrator calling six focused helpers: load_pending_account,
verify_and_validate_genesis_op, pre_store_did, check_already_promoted,
post_to_plc_directory, promote_account.
- P7: Remove AC-prefixed test section comments in crypto::shamir per
project convention (no ticket references in source code).
Net: -505 lines added / +669 lines = ~114 fewer lines of production code
(new lines are the extracted modules + design plan doc). All 328 tests pass.
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.
Replaces `next_step: String` in CreateMobileAccountResponse and
CreateAccountResult with a typed `NextStep` enum. Serde rejects any
relay response with an unrecognized next_step value at deserialization
time, surfacing it as CreateAccountError::Unknown rather than silently
forwarding an unknown string to the frontend.
TypeScript now sees `nextStep: 'did_creation'` (a literal type), so
the redundant if/else check in +page.svelte is removed — the type
system and Rust enum together guarantee the value at this point.
Adds three NextStep serde tests: correct deserialization, correct
serialization, and rejection of unknown values.
- Complete compensation path: session-token failure now also deletes
device-token to avoid orphaned credentials
- Extract map_409_subcode() helper; test calls the real function instead
of duplicating its logic
- HandleScreen: tighten handle regex to RFC 1035 DNS label (no dots or
underscores — these create multi-label handles or violate DNS spec)
- CLAUDE.md: document delete_item in both keychain listings
CRITICAL fix:
- Prevent orphaned private key on token-storage failure: add keychain::delete_item()
and best-effort cleanup when device-token or session-token write fails after relay
201 success. This prevents a situation where the account is created on relay but
the device has no tokens due to local Keychain failure.
IMPORTANT fixes:
- Map keychain failures to new CreateAccountError::KeychainError (not Unknown),
giving users accurate error message: 'Couldn't save credentials to your device.
Try again.' instead of misleading 'Couldn't reach the server.'
- Distinguish UNKNOWN from NETWORK_ERROR: NETWORK_ERROR is genuine connectivity
failure, UNKNOWN is for relay-reachable errors (e.g., unrecognized 409 subcodes).
Frontend routes each to different screens with appropriate messages.
- Fix 404 comment to clarify relay returns 404 for both invalid and expired codes.
- Add unit test for 409 subcode dispatch table to prevent typos in
CLAIM_CODE_REDEEMED/ACCOUNT_EXISTS/HANDLE_TAKEN mapping.
- Fix HandleScreen handle validation: change from non-empty string to ATProto
regex /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/ to prevent relying on
relay validation for invalid formats.
- Update CreateMobileAccountResponse comment to note that relay returns 5 fields
but only 3 are captured (additional fields ignored by serde).
- Add console.error for unexpected nextStep values in submit flow.
- Update ipc.ts with KEYCHAIN_ERROR variant and clean up JSDoc.
- Add Cargo.toml comment explaining rustls-tls requirement (no OpenSSL on iOS).
- Update CLAUDE.md to clarify keypair generation behavior: fresh keypair per
attempt (with best-effort Keychain cleanup on failure), not retry reuse.
TESTS:
- Add CreateAccountError::KeychainError serialization test
- Add CreateAccountError::Unknown serialization test
- Add 409 subcode dispatch table test (verifies correct mapping of
CLAIM_CODE_REDEEMED, ACCOUNT_EXISTS, HANDLE_TAKEN, unknown subcodes)
All tests pass: cargo test --workspace (231 tests)
Clippy: no warnings
Formatting: passes
Frontend: pnpm build succeeds, svelte-check passes
Critical:
- Add 7 unit tests for serde serialization contract (AC2.2, AC2.5, AC3.1-AC3.5)
- create_mobile_account_request_serializes_camel_case: verifies camelCase field names
- create_account_result_serializes_camel_case: verifies nextStep serialization
- error_*_serializes_correctly: 5 tests for all CreateAccountError variants
Important:
- Change CreateAccountResult and CreateAccountError from interface to type
(pure data shapes, not class contracts; CreateAccountParams correctly stays interface)
- Use LazyLock<RelayClient> static for RELAY_CLIENT to avoid allocating new
connection pool and TLS session per IPC invocation
Minor:
- Remove greet dead code: greet command, tests, and generate_handler! entry
- Document OnboardingStep deviation: per-screen error rewinding is better UX
than a dedicated error screen (no 'error' step needed)
Verification:
- cargo test --lib: 7/7 tests pass
- cargo build --workspace: success
- cargo clippy --workspace -- -D warnings: success
- cargo fmt --all --check: success
- pnpm build: success
- svelte-check: 0 errors, 0 warnings
Reflects new contracts added during the mobile onboarding flow:
create_account IPC command, keychain/http modules, onboarding
components, and cross-crate crypto dependency.
- Add crypto workspace dependency to src-tauri Cargo.toml
- Implement create_account async Tauri command with:
- P-256 keypair generation
- Private key storage in iOS Keychain before network call
- POST to relay with account creation request
- Token storage in Keychain on success
- Typed error handling for relay responses
- Add types for request/response envelopes and error variants
- Register create_account command in Tauri handler
- Remove dead_code suppressions from keychain and http modules (now in use)
Verifies MM-144.AC2.1-5 and MM-144.AC3.1-5 and MM-144.AC4.1-3
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.
Eliminate duplicated patterns identified in a full codebase scan:
- P1: New `routes::token` module — `generate_token()`, `sha256_hex()`,
`hash_bearer_token()` replace 10+ copy-pasted token generation/hashing
blocks across route handlers and auth functions.
- P2: Centralize `is_unique_violation()` and `unique_violation_column()`
in `db::mod` — replaces 4 divergent implementations of SQLite constraint
classification in claim_codes, create_account, create_did, and
create_mobile_account.
- P3: New `routes::uniqueness` module — `email_taken()` / `handle_taken()`
extract the identical OR EXISTS pre-flight queries from create_account
and create_mobile_account.
- P4: Replace string-based `is_valid_platform()` with a `Platform` enum
that deserializes via serde, moving validation to the type system.
- P5: Extract `base32_lowercase()` helper in crypto::plc to deduplicate
the base32 encoding setup used in both build and verify paths.
- P6: Break up the 270-line `create_did_handler` into a 45-line
orchestrator calling six focused helpers: load_pending_account,
verify_and_validate_genesis_op, pre_store_did, check_already_promoted,
post_to_plc_directory, promote_account.
- P7: Remove AC-prefixed test section comments in crypto::shamir per
project convention (no ticket references in source code).
Net: -505 lines added / +669 lines = ~114 fewer lines of production code
(new lines are the extracted modules + design plan doc). All 328 tests pass.
Replaces `next_step: String` in CreateMobileAccountResponse and
CreateAccountResult with a typed `NextStep` enum. Serde rejects any
relay response with an unrecognized next_step value at deserialization
time, surfacing it as CreateAccountError::Unknown rather than silently
forwarding an unknown string to the frontend.
TypeScript now sees `nextStep: 'did_creation'` (a literal type), so
the redundant if/else check in +page.svelte is removed — the type
system and Rust enum together guarantee the value at this point.
Adds three NextStep serde tests: correct deserialization, correct
serialization, and rejection of unknown values.
- Complete compensation path: session-token failure now also deletes
device-token to avoid orphaned credentials
- Extract map_409_subcode() helper; test calls the real function instead
of duplicating its logic
- HandleScreen: tighten handle regex to RFC 1035 DNS label (no dots or
underscores — these create multi-label handles or violate DNS spec)
- CLAUDE.md: document delete_item in both keychain listings
CRITICAL fix:
- Prevent orphaned private key on token-storage failure: add keychain::delete_item()
and best-effort cleanup when device-token or session-token write fails after relay
201 success. This prevents a situation where the account is created on relay but
the device has no tokens due to local Keychain failure.
IMPORTANT fixes:
- Map keychain failures to new CreateAccountError::KeychainError (not Unknown),
giving users accurate error message: 'Couldn't save credentials to your device.
Try again.' instead of misleading 'Couldn't reach the server.'
- Distinguish UNKNOWN from NETWORK_ERROR: NETWORK_ERROR is genuine connectivity
failure, UNKNOWN is for relay-reachable errors (e.g., unrecognized 409 subcodes).
Frontend routes each to different screens with appropriate messages.
- Fix 404 comment to clarify relay returns 404 for both invalid and expired codes.
- Add unit test for 409 subcode dispatch table to prevent typos in
CLAIM_CODE_REDEEMED/ACCOUNT_EXISTS/HANDLE_TAKEN mapping.
- Fix HandleScreen handle validation: change from non-empty string to ATProto
regex /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/ to prevent relying on
relay validation for invalid formats.
- Update CreateMobileAccountResponse comment to note that relay returns 5 fields
but only 3 are captured (additional fields ignored by serde).
- Add console.error for unexpected nextStep values in submit flow.
- Update ipc.ts with KEYCHAIN_ERROR variant and clean up JSDoc.
- Add Cargo.toml comment explaining rustls-tls requirement (no OpenSSL on iOS).
- Update CLAUDE.md to clarify keypair generation behavior: fresh keypair per
attempt (with best-effort Keychain cleanup on failure), not retry reuse.
TESTS:
- Add CreateAccountError::KeychainError serialization test
- Add CreateAccountError::Unknown serialization test
- Add 409 subcode dispatch table test (verifies correct mapping of
CLAIM_CODE_REDEEMED, ACCOUNT_EXISTS, HANDLE_TAKEN, unknown subcodes)
All tests pass: cargo test --workspace (231 tests)
Clippy: no warnings
Formatting: passes
Frontend: pnpm build succeeds, svelte-check passes
Critical:
- Add 7 unit tests for serde serialization contract (AC2.2, AC2.5, AC3.1-AC3.5)
- create_mobile_account_request_serializes_camel_case: verifies camelCase field names
- create_account_result_serializes_camel_case: verifies nextStep serialization
- error_*_serializes_correctly: 5 tests for all CreateAccountError variants
Important:
- Change CreateAccountResult and CreateAccountError from interface to type
(pure data shapes, not class contracts; CreateAccountParams correctly stays interface)
- Use LazyLock<RelayClient> static for RELAY_CLIENT to avoid allocating new
connection pool and TLS session per IPC invocation
Minor:
- Remove greet dead code: greet command, tests, and generate_handler! entry
- Document OnboardingStep deviation: per-screen error rewinding is better UX
than a dedicated error screen (no 'error' step needed)
Verification:
- cargo test --lib: 7/7 tests pass
- cargo build --workspace: success
- cargo clippy --workspace -- -D warnings: success
- cargo fmt --all --check: success
- pnpm build: success
- svelte-check: 0 errors, 0 warnings
- Add crypto workspace dependency to src-tauri Cargo.toml
- Implement create_account async Tauri command with:
- P-256 keypair generation
- Private key storage in iOS Keychain before network call
- POST to relay with account creation request
- Token storage in Keychain on success
- Typed error handling for relay responses
- Add types for request/response envelopes and error variants
- Register create_account command in Tauri handler
- Remove dead_code suppressions from keychain and http modules (now in use)
Verifies MM-144.AC2.1-5 and MM-144.AC3.1-5 and MM-144.AC4.1-3
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.