Skip to content

chore: release v0.2.0#58

Merged
stormer78 merged 160 commits into
mainfrom
release/v0.2.0
May 5, 2026
Merged

chore: release v0.2.0#58
stormer78 merged 160 commits into
mainfrom
release/v0.2.0

Conversation

@stormer78
Copy link
Copy Markdown
Contributor

@stormer78 stormer78 commented May 5, 2026

Summary

Bump all workspace crates to v0.2.0 — major release. Branch renamed from release/v0.1.6 to release/v0.2.0 mid-flight (supersedes #40).

Workspace consolidation

  • openvtc-cli (legacy CLI) deleted
  • openvtc-cli2 renamed to openvtc — package and binary
  • openvtc-lib renamed to openvtc-core — frees the unsuffixed name for the binary, matches Rust CLI-first conventions (uv, ruff, deno, cargo)
  • vta-sdk switched from local path pin to published 0.5 on crates.io
  • Dead VtaAuthenticate setup page removed

DIDComm & Messaging

  • Replaced manual messaging layer with affinidi-messaging-didcomm-service (now 0.3)
  • Router-based dispatch with automatic reconnection and message pickup
  • Multi-DID listener support for persona + relationship DIDs
  • Periodic keepalive ping (60s) with live RTT latency in header
  • Trust-pings only respond to mediator or established relationships
  • Outbound message retry via send_message_with_retry

Full TUI (8 panels)

Inbox / Relationships / Credentials / Settings / VTA Service / Logs / Help-Status / Quit. Full task lifecycle, inline alias edit, R-DID toggle, trust-ping with RTT, raw VRC JSON copy, export/import, hardware-token management. Plus bracketed paste, Up/Down navigation everywhere, dynamic DID display width, timestamped activity log.

R-DID privacy

  • R-DID generation for both BIP32 and VTA backends
  • Dynamic listener registration for new R-DIDs
  • All message flows (accept, finalize, ping, VRC) use the correct relationship DID
  • R-DID recommendation shown when sender uses one

Protocol

  • RelationshipRequestBody.name for friendly names; auto-set contact alias on accept
  • Config versioning with migration framework

Post-release deep-review pass

After cutting v0.2.0 a multi-axis review (security, architecture, tests, CI, docs) flagged a set of findings; rather than rolling another major, they all land on this same release branch before merge. The diff stays readable because the pre-review work is already in [0.2.0] above the ### Post-release deep-review pass divider in the CHANGELOG.

Security hardening

  • Per-entry random Argon2 salt with transparent v1 → v2 migration. New passphrase_encrypt_v2 writes [OPV2 | salt(16) | nonce(12) | ct+tag]; decrypt path auto-detects the magic prefix so existing exports keep opening. Argon2id parameters bumped to OWASP "high-value KEK" floor (m=128 MiB, t=4, p=1).
  • did-git-sign signing policy — refuses to sign unless the parent process is git* or ssh-keygen; every signing attempt (accepted or denied) lands in ~/.config/did-git-sign/audit.log (mode 0600).
  • DIDComm replay window + seen-message LRU in process_inbound_message: drops messages outside ±48h / +5m skew, drops past-expires_time, dedupes via 1024-entry process-lifetime ID LRU.
  • DID validation uses a real W3C DID Core 1.0 syntax parser; rejects bidi/zero-width chars in DID fields.
  • Display sanitization strips bidi/isolate/zero-width/BOM/ANSI/control chars and clamps inbound contact aliases to 64 chars.
  • Bounded DIDComm event channel (256-entry) — overflow logs and drops; mediator pickup redelivers.
  • did.jsonl write path is now the resolved profile dir (was: cwd).
  • Dependabot transitives bumped via cargo update; nine open advisories cleared.

Architecture & code quality

  • State-handler split. state_handler/mod.rs 2,255 → 813 lines (-64%). Per-domain matches extracted to dispatch(action, ctx).await entry points in each sub-module.
  • Layering: colors.rs and the dialoguer passphrase prompt moved out of openvtc-core so the daemon and automation crates no longer pull in ratatui + dialoguer transitively.
  • DID-truncation helpers consolidated into openvtc-core::display.
  • Tightened openvtc-core public surface — dropped a dead re-export, scoped two helpers to pub(crate).
  • Surfaced silent failures via log_error; replaced four router-init .expect("valid route") panics with ?; replaced a panic-on-log-file-open with stderr + continue.
  • DIDComm-only VTA fallback in relationships.rs now uses build_runtime_vta_client instead of REST-only challenge_response.

Tests & CI

  • In-process mediator harness (openvtc-core/tests/common/mod.rs) wraps affinidi-messaging-test-mediator 0.2 via TestMediator::with_users(["alice", "bob"]). Returns Alice + Bob as ALLOW_ALL accounts whose DIDComm service URI is the mediator's DID — the routing/2.0 shape required for forwards to short-circuit to local delivery. The migration drops ~400 lines of in-tree fixture code and four dev-deps (affinidi-messaging-mediator, -mediator-common, -sdk, sha256).
  • Integration tests drive a real Alice→Mediator→Bob round-trip, a RelationshipRequestBody round-trip, and a two-leg VRC request/reject round-trip — all in ~350ms once the mediator is up. Plus a smoke test on the well-known endpoint. Marked #[ignore]; CI's coverage job runs them with --include-ignored.
  • 38 new unit tests across setup_flow/navigation (25 table-driven), BIP32 derivation (7 known-answer vectors), AES-GCM tampering (6).
  • CI adds a cargo-deny job (advisories + licenses + bans + sources) and a cargo-llvm-cov coverage job. MSRV check bumped 1.91 → 1.94 to match Cargo.toml.

Dependency refresh

  • affinidi-tdk 0.6 → 0.7 — accessor-method API on TDKSharedState/TDKEnvironment/TDKProfile. TDKSharedState::default().await (removed in tdk 0.6) replaced with TDKSharedState::new(TDKConfig::headless()?).await? in openvtc-service.
  • affinidi-messaging-didcomm-service 0.2 → 0.3?-transparent via the upstream From<ACLError> for ATMError.
  • affinidi-messaging-test-mediator 0.1 → 0.2 (dev-deps).

Community contributions

Three open community PRs against main were assessed and folded into this release; each PR is closed with a comment pointing here. Co-authored-by: trailers preserve attribution.

Checklist

  • All tests pass — workspace cargo test + four integration tests under --ignored
  • CHANGELOG.md updated (comprehensive, including post-review pass + dep refresh)
  • No new clippy warnings
  • Documentation aligned with post-rename workspace shape
  • Security audit findings addressed
  • DIDCommService integration tested over the in-process mediator
  • R-DID flows tested between two clients

… (Phase 1)

Wire up inbound DIDComm message processing so messages are no longer
silently dropped. Auto-process messages that don't need user input
(pong, accept/finalize/reject) and queue interactive tasks in the
inbox (relationship requests, VRC requests, VRC issued).

- Add rich state structures for all main menu panels (inbox,
  relationships, credentials, settings)
- Add sync_from_config() to rebuild display state from Config
- Change InboundMessage event to carry full Message object
- Create message_dispatch.rs porting logic from openvtc-cli fetch.rs

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Render inbox tasks with type indicators, DID, and timestamps.
Support task list navigation (Up/Down), detail view (Enter), and
actions (a=accept, r=reject, d=dismiss, Esc=back).

- Add inbox action variants (accept/reject relationship, accept VRC,
  dismiss task)
- Create inbox_actions.rs with Config/TDK handlers for each action
- Update content_panel.rs with full inbox, relationships, credentials,
  and settings panel rendering (relationships/credentials/settings are
  display-only for now, interactive features in Phases 3-5)
- Add content panel key event handling in MainPage

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…se 3)

List relationships with state/alias, view details, create new
relationship requests, send trust-pings, and remove relationships.

- Add relationship action variants (select, new request, submit,
  ping, remove, input update, back)
- Create relationship_actions.rs porting request/ping/remove logic
  from openvtc-cli
- Add three-mode relationships UI (list, detail, new-request form)
  with inline text input for DID/alias/reason
- Wire all relationship actions into state handler

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
List VRCs with Received/Issued tab switching, view credential details,
and request new VRCs from established relationships.

- Add credential action variants (switch tab, select, detail, new
  request, submit, cancel, reason update)
- Create credential_actions.rs porting VRC request logic from
  openvtc-cli
- Add three-mode credentials UI (list with tabs, detail view,
  new-request with relationship picker and reason input)
- Wire all credential actions into state handler

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
… (Phase 5)

View and edit friendly name, mediator DID, and org DID with inline
text editing. Export config to encrypted file with passphrase.

- Add settings action variants (select, start/cancel/submit edit,
  edit update, export config)
- Create settings_actions.rs with config save/update/export handlers
- Add settings key handling with view/edit/export modes
- Update settings rendering with inline edit fields, export form
  with masked passphrase

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Extract panel renderers from content_panel.rs (822 lines) into
separate files: inbox_panel.rs, relationships_panel.rs,
credentials_panel.rs, settings_panel.rs. Content_panel.rs is now
a thin 80-line dispatcher.

Add VRC issuance flow: when accepting an inbound VRC request from
the inbox, create, sign (DataIntegrityProof), and send a VRC back
to the requester via DIDComm.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add token management sub-screen in settings with detect tokens and
factory reset capabilities. Shows protection type, detected token
count, and operation status messages.

- Add TokenManagementState and TokenManagement mode to SettingsState
- Add token action variants (detect, factory reset, back)
- Update settings view with protection type display and token
  management entry point (cfg-gated behind openpgp-card feature)
- Update settings index layout: 0-2=editable fields, 3=persona DID,
  4=protection type, 5=export, 6=token management

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…agement

Replace confusing "Plaintext" label with descriptive protection type
display: "Keyring Only (no additional encryption)", "Passphrase
Encrypted", or "Hardware Token (id)".

Add ability to change config protection level from Settings:
- Set passphrase: two-field form with confirmation and mismatch
  detection, derives key via Argon2id and re-saves config encrypted
- Remove passphrase: reverts to keyring-only protection
- Protection line in settings now shows [Enter to change] hint

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add a scrolling activity log panel at the bottom of the main screen
showing real-time events (messages received, tasks created, config
saved, errors). Log entries are pushed from all action handlers.

Close feature gaps with openvtc-cli:
- Add reject VRC request (send rejection message back to requester)
- Add remove VRC (delete from issued/received collections)
- Add clear all inbox tasks
- Add activity log calls throughout all action handlers
- Show relationship created dates in list view

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add a toggle in the new-relationship-request form to generate a
random relationship DID (did:peer) instead of using the persona DID.
This provides privacy by not exposing the persona DID to every
relationship partner.

- Add generate_r_did toggle (Space key) to form (field index 3)
- Port create_relationship_did from openvtc-cli: derives Ed25519
  and X25519 keys from BIP32 path m/3'/1'/1'/<n>', creates did:peer
  with mediator routing, registers secrets with TDK resolver
- Falls back to persona DID when BIP32 backend not available or
  toggle is off

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Replace static Help panel with Status/Help showing persona DID,
mediator DID, protection type, relationship/task/VRC counts,
connection status, and keyboard shortcuts.

- Add contact management actions (add/remove) with handlers
- Add config import form in settings (validates file, advises restart)
- Show VRC validity date ranges (valid_from → valid_until) in list
- Import Config option at settings index 6, token mgmt moved to 7
- Reusable export/import form renderer with parameterized title

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Config was only saved to disk by settings actions. Inbox, relationship,
credential, and message dispatch actions modified Config in memory but
never persisted, causing data loss on crash or restart.

Add save_config() calls after every mutating action handler:
- 7 inbox actions (accept/reject/dismiss/clear)
- 3 relationship actions (submit/ping/remove)
- 2 credential actions (submit/remove)
- 1 inbound message dispatch

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…d actions

Replace the 0x8000_0000 bit flag hack used to encode "open detail"
in InboxSelectTask/RelationshipSelect/CredentialSelect with separate
InboxOpenDetail/RelationshipOpenDetail/CredentialOpenDetail actions.

Replace the SettingsEditUpdate(String) control character encoding
(\x00-\x04, \t prefixes) with typed action variants:
- SettingsFieldUpdate for inline text editing
- SettingsFormFieldUpdate/SettingsFormTabSwitch for export/import forms
- SettingsProtection* variants for protection change flow
- RelationshipToggleRDid for the R-DID toggle

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Extract ~40 handler functions from the 700+ line action match block
in state_handler/mod.rs. The match block is now a thin dispatcher
where each arm is 1-3 lines calling a named handler function.

Handlers are grouped by domain: inbox (10), relationship (9),
credential (9), contact (2), settings (16 including token mgmt).

Also fix clippy warnings: replace &Box<Config> with &Config in
helper signatures.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Replace Vec<String> with VecDeque<String> for the activity log.
This changes log truncation from O(n) (Vec::remove(0) shifts all
elements) to O(1) (VecDeque::pop_front).

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Bump all workspace crates to 0.2.0.

Changes:
- Full TUI main menu panels (inbox, relationships, credentials, settings, status)
- Activity log panel with real-time events
- R-DID generation for relationship privacy
- VRC issuance from inbox
- Critical fix: config persistence after all mutations
- Code quality: replaced hacks with typed actions, extracted handlers, VecDeque log

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Passphrases for export/import/protection-change were stored as plain
String in SettingsMode variants, which is part of State that gets
cloned every tick and sent to the UI channel. Multiple copies existed
in memory without zeroization.

Fix: replace passphrase strings in State with length-only fields
(passphrase_len, confirm_len). Actual passphrase content is now kept
only in MainPage's local buffers (passphrase_buffer, confirm_buffer)
which are never cloned. Passphrases are consumed via std::mem::take
on submit and cleared on cancel.

Also wrap token_admin_pin in Arc<SecretString> so State clones share
a single allocation instead of creating independent copies.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
- Validate message body size (1MB limit) before deserialization to
  prevent DoS via oversized JSON payloads
- Check for duplicate task IDs before creating new tasks to prevent
  hijacking via crafted message IDs/thread IDs
- Validate sender identity on thid-referencing messages (reject/accept)
  to prevent unauthorized state mutations
- Add collection size bounds: max 10K tasks, 5K relationships —
  reject new entries when limits reached
- Sanitize all untrusted display text (DIDs, aliases, reasons from
  remote peers) by stripping ANSI escape codes and control characters,
  truncating to 256 chars

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…tion

- Document key material zeroization limitation (ed25519-dalek-bip32
  lacks Zeroize impl) with explicit drop() calls after use
- Add unlock attempt rate limiting: max 5 attempts with exponential
  backoff (2s, 4s, 8s delay after 3 failures)
- Redact home directory paths from user-facing error messages
  (replace with ~), keep full paths in debug logs
- Truncate DIDs to 30 chars in activity log entries to limit
  information exposure on shared terminals
- Validate export/import file paths: reject empty paths and path
  traversal (..) attempts
- Add structured audit log entries for config export and import
  operations

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Replace all .unwrap()/.expect() in fallible contexts with proper
error propagation (?, ok_or_else, map_err):
- cli.rs: get_user_pin now returns Result
- main.rs: signal handler, password loop, config loading
- setup_sequence/config.rs: 9 expect/unwrap calls replaced
- did_keys_export_show.rs: clipboard operations gracefully degrade
  with tracing::warn instead of panicking

Additional improvements:
- Add #[must_use] to pure functions (sanitize_display, shorten_did,
  collect_vrcs, truncate_did)
- Add doc comments to State, ActivePage, MediatorStatus,
  ConnectionState types and variants
- Replace blanket #[allow(dead_code)] with targeted per-field
  annotations with explanatory comments
- Use explicit Arc::clone() instead of .clone() on 27 Arc values
  across inbox_actions, relationship_actions, credential_actions
- Simplify SecretString allocation chains: remove unnecessary
  .to_string() in "literal".to_string().into() patterns

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Return Cow::Borrowed when the DID is short enough (<=30 chars),
avoiding a String allocation on the common path. Cow::Owned is
only used when truncation actually occurs.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add config_version field to PublicConfig (defaults to 0 for
pre-existing configs via serde(default)). On load, if the version
is below CONFIG_VERSION, run stepwise migrations. On save, stamp
the current CONFIG_VERSION.

Migration framework: each version increment gets a match arm in
migrate_config(). Version 0→1 is a no-op (just adds the field).
Future format changes add new arms.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…ates

Switch from tokio::sync::mpsc::unbounded_channel to tokio::sync::watch
for State communication between StateHandler and UI. The watch channel
ensures the UI always sees the latest state, automatically dropping
intermediate updates when the UI is slower than state changes.

Benefits:
- No unbounded queue growth if state changes faster than rendering
- UI always renders the most recent state, never stale intermediates
- Simpler mental model: "current value" vs "message queue"

Updated 9 files: state_handler mod.rs, ui/mod.rs, setup_wizard.rs,
setup_vta_actions.rs, setup_token_actions.rs, setup_did_actions.rs,
setup_sequence/config.rs, setup_sequence/openpgp_card.rs,
setup_sequence/did_keys.rs.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
When the mediator DID is changed in settings, automatically tear
down the existing DIDComm WebSocket loop and spawn a new connection
with the updated mediator. Also adds an explicit SettingsReconnectMediator
action for manual reconnection.

The existing conn_result_rx channel is reused so the reconnection
result is handled by the same select arm that handles initial
connection setup.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add OutboundQueue that stores failed outbound messages for automatic
retry when connectivity is restored. Queue is bounded (1K messages,
5 retries per message) with oldest-message eviction.

On successful mediator reconnection, queued messages are
automatically retried via pack_and_send. Failed retries are
re-queued. Queue count is displayed in the inbox connection status.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Split the flat ~65-variant Action enum into domain-specific sub-enums:
- InboxAction (10 variants)
- RelationshipAction (10 variants)
- CredentialAction (10 variants)
- ContactAction (2 variants)
- SettingsAction (20 variants incl. token management)

The main Action enum now dispatches to sub-enums via wrapper
variants (Action::Inbox(InboxAction::...), etc.). Setup/VTA/WebVH
actions remain flat in the main enum since they're stable.

Benefits: each domain is self-contained, easier to add features,
cleaner match blocks in the state handler dispatcher.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Define a Panel trait with a render() method that unifies the four
panel render signatures behind a single interface. Each panel module
(inbox, relationships, credentials, settings) now exports a struct
implementing the trait.

The content_panel dispatcher uses dyn Panel trait objects for the
four domain panels, with direct rendering for Help/Status and Quit.
This establishes the pattern for incrementally moving key handling
into each panel in the future.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Add test coverage for core non-async handler functions:

- outbound_queue (4 tests): enqueue/len, drain retry count,
  max retries drop, queue size limit
- sanitize_display (5 tests): control chars, ANSI escapes,
  truncation, spaces, empty input
- shorten_did (2 tests): short passthrough, long truncation
- activity_log (1 test): bounded VecDeque behavior
- validate_file_path (5 tests): empty, traversal, normal paths,
  dot-slash, hidden traversal
- main_panel_switch (1 test): panel toggle logic

Also fixes a bug in sanitize_display where ANSI escape stripping
was dead code (control char filter removed ESC byte first).

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…iom changes

Add security hardening section (14 findings fixed), architectural
changes (config versioning, watch channel, mediator reconnect,
outbound queue, action grouping, panel trait, test harness), Rust
idiom improvements, and bug fixes to the v0.2.0 release notes.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Enable crossterm's bracketed paste mode so that paste operations
arrive as a single Event::Paste(String) instead of hundreds of
individual KeyEvent characters. This eliminates the per-character
render cycle overhead when pasting large strings like VTA credential
bundles (typically 500+ chars).

The VTA credential paste page now handles Event::Paste by setting
the entire input value at once via tui_input::Input::new(), making
paste effectively instant instead of taking several seconds.

Also adds handle_paste_event to the Component trait with a default
no-op implementation, and flows paste events through AppRouter →
SetupFlow → VtaCredentialPaste.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
stormer78 and others added 23 commits May 6, 2026 00:15
…patch

Tightens process_inbound_message with the two M-tier defenses called
out in the v0.2.0 review:

  * Replay window — drop messages whose `created_time` is more than 48
    hours old (matching our outbound 48h `expires_time` horizon) or
    more than 5 minutes in the future (clock-skew tolerance), and drop
    messages whose `expires_time` has already passed. Stops stale
    captures from re-driving relationship/VRC state long after the
    legitimate exchange has been torn down.

  * Seen-message LRU — process-lifetime 1024-entry deduplicator on
    message IDs. The TDK already filters unpack-level duplicates, but
    mediator pickup retries and replay attempts both surface as the
    same packed bytes from the mediator's perspective. Belt-and-braces
    backstop, evicted FIFO at capacity.

The `from`-required check already covered the M1 anoncrypt-vs-authcrypt
concern in practice: HandlerContext.sender_did is only `Some(_)` after
the TDK confirms an authcrypt'd sender, and process_inbound_message
rejects messages with `from = None` for everything except trust-pong
(where thid linkage is the equivalent authentication channel).

Threads `&mut SeenMessages` through from the StateHandler main loop
where the LRU is instantiated once. 8 new unit tests cover both helpers.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Three boundary fixes flagged in the v0.2.0 review:

* DID validation no longer trusts a `did:` prefix. The previous
  validate_did would accept anything ≥ 8 bytes that started with
  "did:", including newlines, spaces, and zero-width characters that
  downstream routing logic took as identity. The replacement parses
  did = "did:" method-name ":" method-specific-id per W3C DID Core
  1.0 syntax and rejects malformed inputs at the message-dispatch
  boundary.

* Inbox display-name sanitisation extends sanitize_display to strip
  bidi-override / isolate / zero-width / BOM codepoints in addition
  to ANSI / control bytes. The accept-relationship handler now runs
  this over `request.name` and clamps to 64 chars before persisting
  the value as a contact alias, so a malicious peer can't spoof a
  contact's displayed identity via RLO/RLM tricks.

* Relationships' VTA-client fallback no longer hand-rolls
  challenge_response against `vta_url`. The DIDComm-only VTA case
  (empty vta_url, non-empty mediator_did) was failing with "vta_url
  is empty" because the fallback was REST-only. It now defers to
  build_runtime_vta_client, which already does the right thing for
  both transports.

Six new unit tests cover the strengthened validate_did. Workspace
test suite passes (243 total).

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Three CI improvements + one tracking bump:

* deny.toml + new "Cargo Deny" job runs `cargo deny check advisories
  licenses bans sources`. Two advisories are explicitly ignored with
  rationale: RUSTSEC-2023-0071 (rsa Marvin Attack — no upstream fix,
  reachable only on local key paths, not network-observable timing)
  and RUSTSEC-2024-0370 (proc-macro-error unmaintained — build-time
  only, pulled in by the json-ld stack inside affinidi-did-resolver).
  Two non-OSI-but-permissive licenses are allow-listed: bzip2-1.0.6
  (libbz2-rs-sys via pgp) and CDLA-Permissive-2.0 (webpki-roots).
  rand 0.8 is in skip-tree to suppress the multiple-versions noise
  the workspace already documents in Cargo.toml.

* "Coverage (llvm-cov)" job runs `cargo llvm-cov --workspace --lcov`
  with `--include-ignored` so any future #[ignore]-gated integration
  tests (e.g. mock-mediator E2E) still contribute. Uploads lcov.info
  as an artifact for inspection or later upload to a coverage host.

* MSRV check moved 1.91.0 -> 1.94.0 to match the workspace
  rust-version. Was the only place still pinning the old value.

Side fix: openvtc -> did-git-sign workspace dep gets an explicit
version = "0.1" alongside its path = "../did-git-sign", so the path
dep no longer trips cargo-deny's wildcard-dependency check.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…'t OOM us

The DIDComm router → state-handler event channel was unbounded. A
mediator (or upstream service) that pushed messages faster than the
state handler could drain them would grow the channel without limit,
turning into a memory-exhaustion vector that needs no authentication
to trigger.

Bound the channel at 256 entries (DIDCOMM_EVENT_CHANNEL_CAPACITY).
The four router send sites switch from `tx.send(...)` to
`tx.try_send(...)`:

  * On success the message reaches the dispatcher unchanged.
  * On a full channel we log a warning and drop the message.
    Inbound message pickup is at-least-once via the mediator —
    once the dispatcher catches up and drains, the mediator
    redelivers anything we shed under load. That keeps the
    handler closures non-blocking and keeps the DIDCommService
    progress decoupled from state-handler pace.

Capacity is sized so that bursty real operator activity doesn't ever
overflow; the cap is purely a defense against pathological volume.
The other in-process channels (action_rx, lifecycle_log_tx,
progress_tx, etc.) are deferred — they aren't attacker-controllable
and an Arc<State> migration is the higher-leverage follow-up there.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
setup_flow/navigation.rs is a 260-line pure function — a perfect target
for exhaustive unit testing — and it had zero tests. Adds 25 tests
covering every SetupEvent variant outside the openpgp-card feature,
both flow branches that key off SetupState (webvh-server-pick vs manual,
unlock-code-set webvh vs manual), and the structural shape of every
NavResult variant.

Locks in the wizard flow before the larger state-handler split refactor
that will move pieces of it around. A regression in navigate() is one
of the highest-impact bug classes the binary can ship — broken page
routing leaves the operator stuck mid-setup with no recovery path.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Two test additions targeted at silent-corruption regressions in the
crypto stack — exactly the bug class that doesn't fail loudly:

* bip32: 7 known-answer tests pin BIP32-ed25519 derivation behaviour
  for the production paths (m/0'/0'/0' persona, m/3'/1'/1'/0'
  relationship). Verifies that signing/authentication purposes share
  the same Ed25519 public key, that encryption goes through ed25519 ->
  x25519 conversion (different bytes), that distinct seeds produce
  distinct keys, and that invalid paths / unknown purposes return
  errors. A future ed25519-dalek-bip32 / affinidi-crypto bump that
  silently changes derivation will trip these.

* config/encryption: 6 tampering tests assert the AES-256-GCM AEAD
  fails closed on any modification to the stored ciphertext — nonce
  byte flip, ciphertext byte flip, GCM tag byte flip, truncation,
  append, and a "frankenstein" splice (nonce of one message onto the
  body+tag of another). Catches silent on-disk-data-edit attacks and
  AEAD-construction regressions.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
state_handler::main_loop's Action::Inbox arm was a 30-line nested match
fronting nine private handlers further down in mod.rs. Both pieces now
live in `inbox_actions.rs` next to the protocol-level helpers they wrap,
and the main loop calls a single `inbox_actions::dispatch(action, ...)`
entry point. mod.rs drops ~250 lines (2255 -> 2007).

The wrapper handlers (handle_inbox_select / open_detail / accept_*
/ reject_* / dismiss / clear_all) plus the small `save_and_sync` and
`record_error` UI helpers all move with their related protocol code.
External behaviour is unchanged — same call shape, same arguments,
same per-action effects. Workspace test suite still passes (250+
tests; same as before).

This is the first slice of B5; Relationship / Credential / Settings
will follow the same pattern.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…_actions

Same pattern as the Inbox slice: the 60-line Action::Relationship match
arm fronting 10 private handlers in mod.rs becomes a single
relationship_actions::dispatch(...) call. The wrapper handlers
(open_detail / start_new_request / cancel_or_back / input_update /
toggle_r_did / submit / ping / remove / edit_alias / request_vrc) plus
their RelationshipsMode-mutation helpers move to relationship_actions.rs
next to the protocol-level helpers they wrap.

The Ping arm side-effect — setting the main loop's `ping_sent_at` so
the inbound pong can be correlated for RTT — is preserved by
threading `&mut Option<Instant>` into the dispatch signature.

Two helpers used by relationship handlers (and Credential / Settings
handlers later) — `log_did` and `resolve_did_to_display` — are
promoted from private fns in mod.rs to `pub(crate)` so they can be
called from the per-domain modules without re-implementing.

mod.rs drops another ~390 lines (2007 -> 1617). Workspace test suite
still green.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…ions

Same per-domain extraction as the Inbox and Relationship slices: the
Action::Credential match in main_loop is replaced with a single
credential_actions::dispatch(...) call, and the eight handler wrappers
(switch_tab / open_detail / back / start_new_request /
select_relationship / reason_update / submit_request / remove) move
into credential_actions.rs alongside the protocol-level VRC helpers
they were already calling.

mod.rs drops another ~120 lines (1617 -> 1475). Workspace test suite
still passes.

Contact dispatch and Settings dispatch remain inline in mod.rs;
Contact's match is six lines (not worth the move) and Settings is a
larger refactor that's worth its own slice.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…gs_actions

The largest of the per-domain extractions: the 163-line Action::Settings
match (with two inline 30-line reconnect-listener blocks) and the
6-line Action::Contact match both move into settings_actions.rs as
`dispatch` (Settings) and `dispatch_contact` (Contact).

The two reconnect-listener inline blocks dedupe to a new
didcomm::reconnect_persona_listener helper that returns a
`ReconnectOutcome::Connected | Failed(reason)` enum; the dispatcher
calls it for both the SubmitEdit-mediator-change branch and the
ReconnectMediator action.

The wipe-the-profile-then-exit semantic is preserved by having the
dispatcher return a `SettingsOutcome` enum: `Continue` is the common
case, `ExitUserInt` is returned only when the operator confirms the
wipe — the main loop then drives the terminator and breaks out of
main_loop with Interrupted::UserInt as before.

mod.rs drops another ~660 lines (1475 -> 813); cumulative reduction
since B5 started is 2255 -> 813, a 64% shrink. Workspace test suite
still passes (250+ tests, same as before this refactor).

That's the four big domains (Inbox, Relationship, Credential, Settings)
plus Contact extracted; the action match in main_loop is now a clean
five-line dispatch table.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…gration

The v0.2.0 review (H2) flagged that derive_passphrase_key derived its
Argon2 salt deterministically from the info label. Two operators with
the same passphrase produced the same KEK, exported config files
encrypted with the same passphrase were byte-comparable, and an
attacker could amortise a dictionary attack across all OpenVTC users.

Fix:

  * derive_passphrase_key_v2(passphrase, salt) takes an explicit
    per-entry random salt. Same Argon2id parameters as v1.
  * derive_passphrase_key (the legacy entry point) is kept and clearly
    re-documented as legacy. It's now used only on the v1 decrypt path
    for backward compat.
  * passphrase_encrypt_v2(pass, info, plaintext) generates a random
    16-byte salt, derives a v2 key, AEAD-encrypts via the existing
    unlock_code_encrypt, and prepends a magic+salt header:

        [b"OPV2" : 4][salt : 16][nonce : 12][ct+tag]

  * passphrase_decrypt(pass, info, blob) auto-detects format by magic
    prefix:
        starts with b"OPV2" -> v2 path (parse salt from blob)
        otherwise            -> v1 path (deterministic info-salt)

  * Config::export switches to v2. Existing v1-encrypted backup files
    are still readable through passphrase_decrypt (transparent
    migration: an exported v1 blob re-encrypted by the operator after
    upgrade lands as v2 on the next export).

The unlock-code path against the SecuredConfig keyring entry still
uses derive_passphrase_key directly (the salt has no on-disk slot
there yet); migrating that requires a versioned keyring entry format
and is intentionally deferred.

Seven new tests cover the v2 round-trip, that two encrypts of the same
plaintext under the same passphrase produce distinct blobs, that
legacy v1 ciphertexts decrypt cleanly via the new API, that wrong
passphrase fails, that v1 blobs need their original info-label to
decrypt (since info IS the salt for v1), that v2 blobs no longer
depend on info (since the salt is in the blob), and that tampering
with the salt bytes fails authentication.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
The v0.2.0 review's B9 line item — a real mediator binary, not a
wiremock stub — drops in cleanly because affinidi-messaging-mediator
0.14 exposes a `MediatorBuilder` that stands up the whole server
(handlers, store, secrets resolver, listener) inside the calling
process, returning a `MediatorHandle` with the bound HTTP/WS endpoints.

  * `tests/common/mod.rs` adds `MockMediator::start()` — generates a
    fresh `did:peer` for the mediator with a random Ed25519/X25519 key
    pair, loads the secrets into a `ThreadedSecretsResolver`, picks
    `MediatorBuilder::memory_store()` so nothing touches disk, binds
    on `127.0.0.1:0` (ephemeral) so parallel test invocations don't
    collide, and returns a guard that triggers shutdown on drop.
  * `tests/mediator_smoke.rs` exercises the harness end-to-end: the
    mediator binds, the `well-known/did.json` endpoint either serves
    or 404s (both indicate the HTTP server is up), and the handle
    reports a sensible mediator/admin DID.
  * Mediator-spawning tests are marked `#[ignore]` so the default
    `cargo test` stays fast. CI runs them via the existing coverage
    job, which already passes `--include-ignored` to `cargo llvm-cov`.

dev-deps added on openvtc-core: affinidi-messaging-mediator (with
default features off, didcomm + memory-backend on so we don't pull
Redis), affinidi-secrets-resolver, reqwest (workspace), tokio (with
macros + rt-multi-thread), tokio-util.

Verified by running `cargo test -p openvtc-core --test mediator_smoke
-- --ignored` — the in-process mediator boots in ~150ms and the
smoke assertions pass. The full default `cargo test --workspace`
suite remains unchanged.

This is the foundation for the B11 integration tests (relationship
E2E, VRC round-trip, trust-ping over the mediator, did-git-sign git
commit E2E, full setup-wizard happy path) — each can layer on
`MockMediator` and stand up a couple of TDKProfile instances against it.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Lands the file-and-comment scaffolding for the first B11 integration
test so the intended shape is recorded next to the working harness.
The body is gated on a profile-registration helper that doesn't yet
exist (the mediator's admin-account / ACL provisioning flow needs its
own helper before two TDKProfiles can register with the test
mediator and start exchanging routable DIDComm).

The test is `#[ignore]` and runs as a no-op apart from confirming the
mediator boot path is still intact. The full B11 backlog (relationship
E2E, VRC issue/verify, trust-ping over the mediator, did-git-sign git
commit E2E, full setup-wizard happy path) layers on the same harness
once the registration helper lands.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Lands the first working end-to-end integration test on the
[`MockMediator`] harness. Two profiles (Alice + Bob) connect to the
in-process mediator, Alice sends a typed DIDComm message to Bob, the
mediator routes it through its forwarding/pickup pipeline, and Bob's
router receives it via a capture handler. All in-process, no external
services, sub-second on a warm cache.

Required harness extensions to actually drive a real mediator:

  * Pre-bind the listener so the mediator's URL is known *before* the
    DID document is generated. Drop and reuse the port — the window
    is microseconds and 127.0.0.1, no realistic CI collision.

  * Construct the mediator's `did:peer` directly via
    `affinidi-did-common`'s `DID::generate_peer` (the TDK 0.6 helper
    only supports a single service entry). Publish three services:
        - `dm`        : DIDComm messaging endpoint (Long-form so the
                         SDK's `_find_endpoint` can read the JSON map)
        - `#auth`     : `Authentication` service URI ending in
                         `/authenticate` so the SDK appends the right
                         path; Uri form because did-authentication's
                         `get_uri` mishandles Long-form (round-trips
                         through Value::to_string and re-emits JSON
                         quotes around the URL).
        - `#ws`       : WebSocket endpoint (Long-form, ws:// scheme)

  * Inject a real Ed25519 JWT signing keypair via `config_mut`. The
    headless mediator config ships zero-byte JWT keys; the auth
    handler 500s on `InvalidEddsaKey` the moment it tries to mint an
    access token.

  * Switch the mediator's global ACL defaults to the SDK's "allow_all"
    preset (mediator-mode `ExplicitAllow`, per-account
    `access_list_mode = ExplicitDeny`, all flags on). Default deny mode
    flips `did_local` off, which trips the websocket handler's
    "DID isn't local" 403, and the recipient ACL's empty allow-list
    blocks all forwarded messages.

The test is `#[ignore]` (slow: ~1s mediator boot + auth handshake +
WS connect for two profiles). CI's coverage job runs all ignored
tests via `cargo llvm-cov ... -- --include-ignored`.

This unblocks the rest of the B11 integration tests (relationship E2E,
VRC issue/verify, trust-ping over the mediator, did-git-sign git
commit, full setup-wizard happy path) — each layers on the same
`MockMediator::start()` / `make_profile()` harness.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
… pass

Two pieces of remaining v0.2.0 work:

* Adds a second integration test, `relationship_request_round_trip`,
  that drives a real `openvtc_core::relationships::RelationshipRequestBody`
  through the in-process mediator and asserts the receiving side can
  deserialise it back into the protocol type. Same connect / wait /
  send / receive shape as the basic round-trip but the body is the
  production protocol struct, so a future serde change on either side
  trips the test. Refactored the original test to share a
  `connect_alice_and_bob` helper so subsequent integration tests can
  layer on without repeating the boilerplate.

* CHANGELOG: the original `[0.2.0]` entry was written when the rename
  branch landed; the 23 review-driven commits that followed (security
  hardening, Argon2 salt migration, did-git-sign signing policy,
  state-handler split, DIDComm replay window, integration-test
  harness, CI hardening, doc cleanup) had no entry. Adds a
  "Post-release deep-review pass" subsection under `[0.2.0]` so the
  release notes describe what's actually in the tag.

Both integration tests pass under `cargo test -p openvtc-core
-- --ignored`. Default `cargo test --workspace` is unchanged.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Adds the third B11 integration test. Two legs through the mediator:

  Alice -> Bob: VrcRequest { reason }                typed as VRC_REQUEST
  Bob -> Alice: VRCRequestReject { reason }          typed as VRC_REJECTED

Each side deserialises the inbound DIDComm body back into the
openvtc-core protocol type; a serde regression on either field name
trips this test the same way relationship_request_round_trip does for
the relationship pair. The reject's `thid` links it to the original
request id, matching how the production protocol correlates the two
legs.

That's three of the audit's five recommended integration tests
covering the harness end-to-end: basic round-trip, relationship
request, VRC request/reject. The remaining two — did-git-sign git E2E
(needs a mock VTA stack) and the full setup-wizard happy path
(needs the TUI decoupled from the wizard's setup_wizard.rs) — are
each separate infra investments and stay flagged as follow-up work
in the harness module docs.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…or 0.1

Replace the hand-rolled mediator harness with the just-published
`affinidi-messaging-test-mediator = "0.1"` crate. The fixture handles
port pre-bind, the mediator did:peer (with dm/#auth/#ws services), the
JWT signing keypair, and global ACL defaults so we no longer carry that
boilerplate locally.

End-user DIDs now use the **mediator's DID** (not its HTTP URL) as the
DIDComm service endpoint. The mediator's routing/2.0 logic only
short-circuits to local delivery when the next-hop URI string equals
the mediator's DID; an HTTP URL — even one pointing back at the same
mediator — is treated as remote and enqueued to FORWARD_Q. Since the
mediator's DID isn't known at builder time (content-addressed off keys
generated during spawn), we can't use the test-mediator's `local_dids`
shortcut: instead, we pass our own `MemoryStore`, spawn the mediator,
then register Alice + Bob as ALLOW_ALL accounts directly through the
shared store handle.

All four integration tests (smoke + 3 round-trips) pass.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Catches up the workspace to the May 2026 upstream releases:

- affinidi-tdk 0.6 → 0.7 (workspace) — accessor-method API on
  TDKSharedState / TDKEnvironment; field access (`.secrets_resolver`,
  `.environment`, `.profiles`, `.default_mediator`) now goes through
  methods. Affected sites: openvtc-core, openvtc, openvtc-service,
  robotic-maintainers.
- TDKSharedState::default() (also removed in 0.6) replaced with
  TDKSharedState::new(TDKConfig::headless()?).await? in openvtc-service.
- affinidi-messaging-didcomm-service 0.2 → 0.3 (workspace).
- affinidi-messaging-test-mediator 0.1 → 0.2 (openvtc-core dev-deps).
  Replaces the hand-rolled MemoryStore + ALLOW_ALL registration dance
  with the new TestMediator::with_users(["alice", "bob"]) helper, which
  mints user DIDs whose service URI is the mediator's DID rather than
  its HTTP URL — the routing-2.0 shape that short-circuits forwards to
  local delivery. Drops affinidi-messaging-mediator,
  -mediator-common, -sdk and sha256 from dev-deps now that the
  workaround is gone.
- tests/common/mod.rs trims from ~150 lines to ~95.

Verified: `cargo build --workspace`, `cargo test --workspace`, all four
mediator integration tests (`--test relationship_e2e --test
mediator_smoke -- --ignored`), `cargo clippy --workspace --all-targets`.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
… badge

CHANGELOG v0.2.0 post-release deep-review pass:
- Rewrote the in-process mediator harness bullet to reflect the upstream
  test-mediator 0.2 fixture (`TestMediator::with_users`) rather than the
  hand-rolled `MockMediator::start` boot-the-mediator-yourself shape that
  predated the published crate.
- Added the integration tests bullet (smoke + 3 round-trips, ~350ms warm).
- Added a "Dependency refresh" subsection covering tdk 0.6 → 0.7
  (accessor-method API, TDKSharedState::default removal),
  didcomm-service 0.2 → 0.3, and test-mediator 0.1 → 0.2 (drops four
  dev-deps). Notes the IPv6 + mediator-common feature-gating follow-ups
  landing post-publication; neither is on openvtc's loopback path.

openvtc-core README: MSRV badge 1.91.0 → 1.94.0 to match Cargo.toml's
`rust-version` and the top-level README.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Folds in @sameerchore's PR #57. `validate_profile_name` now trims
leading/trailing whitespace before validating, so `" default "` is
treated as `"default"`, and the empty/whitespace check runs *before*
the character validation so a whitespace-only input ("   ") gets a
clear "cannot be empty or contain only whitespace" error instead of
the confusing "Invalid profile name '   '" the alphanumeric check
used to produce.

Adds the three integration tests from the PR:
- whitespace_only_profile_name_rejected
- padded_default_profile_name_accepted
- padded_valid_profile_name_accepted

Closes #57.

Co-authored-by: Sameer Chore <sameerchore5@gmail.com>
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…#51, #47)

Folds in @krsatyamthakur-droid's PR #51 (resolves #47). Two improvements:

1. Windows config location. `profile_dir` and `get_lock_file` now use
   `dirs::config_dir()` on Windows (typically `%APPDATA%\openvtc`),
   matching the platform convention. Unix/macOS continues to use
   `~/.config/openvtc/` so existing installs don't move.

2. PathBuf throughout. `get_config_path` and `get_lock_file` return
   `PathBuf` instead of `String`, so callers don't round-trip through
   a (potentially non-UTF-8) string and don't have to re-parse with
   `Path::new`. `create_lock_file` and `remove_lock_file` now take
   `impl AsRef<Path>` for ergonomic call sites.

Tests in `public_config::tests` rewritten to be cross-platform —
asserts use `PathBuf::push` rather than concatenated string literals,
and the `OPENVTC_CONFIG_PATH` test uses platform-appropriate `C:\` or
`/tmp` bases. Adds `test_get_config_path_fallback` covering the
no-env-var path.

Closes #51, closes #47.

Co-authored-by: satyam kumar <krsatyamthakur@gmail.com>
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
…ate (#34)

Folds in the structural-defence parts of @ojasshelke's PR #34.

## Tagged-variant downgrade defence (Layer 1)

`SecuredConfigFormat` switches from `#[serde(untagged)]` to
`#[serde(tag = "format")]` so every blob carries an explicit `"format"`
discriminator on disk. Without that tag, an attacker with write access
to the OS keychain (or any caller fed a crafted blob) could substitute
a `PasswordEncrypted` blob with `{"text": "<plaintext>"}` and serde
would silently match it as `PlainText` — bypassing AES-256-GCM and
delivering raw identity material. Tagged format means any blob lacking
`"format"` is rejected at parse time.

## Caller-intent cross-validation (Layer 2)

New `assert_format_matches_intent` runs **before** any decrypt or
re-save. Even if an attacker substitutes a *correctly-tagged* but
weaker variant — e.g. a tagged `PlainText` blob where
`PasswordEncrypted` is expected — this gate refuses to proceed and
emits a logged "Security violation" error.

## Transparent migration

`SecuredConfig::load` first tries the new tagged format; on parse
failure it falls back to a `LegacySecuredConfigFormat` (kept solely
for migration), then re-saves in the tagged shape on next save. The
intent gate runs before the re-save, so a downgrade attack can't be
laundered through migration.

## Out of scope

PR #34's HKDF v2 with fixed `HKDF_SALT` constant is **superseded** by
the random-per-entry-salt v2 we shipped earlier in this release
(`passphrase_encrypt_v2`, `OPV2` magic prefix in
`openvtc-core::config::secured_config`). Random salt is strictly
stronger — it prevents rainbow tables across users with the same
passphrase. We've kept our random-salt v2 path; only the
serde-format defence from #34 is folded.

## Audit advisories

`.cargo/audit.toml` and `deny.toml` pick up entries the May 2026
dep refresh didn't resolve: `RUSTSEC-2024-0370` (proc-macro-error
unmaintained, transitive via the affinidi DID resolver's json-ld
stack) and `RUSTSEC-2025-0134` (rustls-pemfile unmaintained,
transitive via reqwest 0.11 and tonic). The two rustls-webpki
entries from the original PR were already cleared by the May 2026
dep bumps and are not added.

## Tests

Four new tests in `secured_config::tests`:
- `tagged_format_writes_explicit_discriminator`
- `legacy_untagged_blobs_round_trip_through_legacy_enum`
- `intent_gate_rejects_plaintext_when_password_expected`
- `intent_gate_accepts_matching_combinations`

Closes #34.

Co-authored-by: Ojas Shelke <ojasshelke733@gmail.com>
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Records the substantive folds from PRs #57 (profile-name validation),
#51 (Windows AppData + PathBuf cross-platform paths, closes #47), and
#34 (SecuredConfig tagged-variant downgrade defence + intent gate)
under v0.2.0's "Post-release deep-review pass". Adds a "Community
contributions" subsection summarising each fold with author credit.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
Addresses the small-but-actionable items from @stormer78's #52 review
of #46. Each fix is independently small; bundling so the issue can be
closed in one go.

1. **Dead inner condition removed** (main.rs `None` arm): the early
   `-Y` block returns before this arm is reached, so the nested
   `cli.operation.is_none()` was always true. Replaced with a comment
   explaining why no inner check is needed.

3. **`process::exit` comment expanded** (main.rs delegation branch):
   spells out that inherited stdio doesn't buffer in-process — bytes
   have already crossed the syscall boundary, so the `Drop`-skip is
   safe — and what would change that (any added `println!` between
   delegation and exit).

4. **`DID_GIT_SIGN_SSH_KEYGEN` README warning** (Security Model
   section): explicitly marks the override as test-only and explains
   that an attacker with write access to the env could redirect
   verification to a binary that always returns success. Notes the
   override has no effect on the signing path.

6. **`setup_git_never_writes_user_email` invariant pinned**
   (init.rs): captures `original_cwd` before the inner block and
   asserts it after, so a future edit that drops `CwdGuard` or moves
   the verify command inside the guard's scope is caught loudly.

7. **`did_key_id` doc-comment corrected** (config.rs): the field's
   comment claimed "DID#key-id to use as git user.email" — stale from
   the pre-#46 era. Replaced with an accurate description of what it
   actually does (recorded in the SSH signature's `Signed-By`
   principal, not written to `user.email`).

Items 2 (clap catch-all for forward-compat with future Git flags) and
R2 (full-loop integration test) are deferred — both larger than this
fold-and-close pass should carry. Items 5 (`user.signingKey` `.json`
path tracking) and R1 (active-unset of stale DID-shaped `user.email`
on upgrade) are tracked for v0.3.

Closes #52.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78 stormer78 merged commit 1f31455 into main May 5, 2026
11 of 13 checks passed
@stormer78 stormer78 deleted the release/v0.2.0 branch May 21, 2026 04:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Hardcoded path delimiters break Windows compatibility in public_config.rs.

2 participants