use std::collections::HashSet;
use std::thread;
use std::time::{Duration, Instant};
use anyhow::{anyhow, bail, Context, Result};
use chrono::{DateTime, Utc};
use serde_json::Value;
use super::agent::client::AgentHttpClient;
use super::agent::session::AgentSession;
use crate::tui::{self, LogTx};
const APPROVAL_TIMEOUT: Duration = Duration::from_secs(120);
const POLL_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Clone, Debug, PartialEq, Eq)]
struct ClientDevice {
device_id: String,
public_key: String,
status: String,
created_at: DateTime<Utc>,
}
fn parse_device(value: &Value) -> Result<ClientDevice> {
let device_id = value
.get("device_id")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("device payload missing 'device_id' string field"))?
.to_string();
let public_key = value
.get("public_key_multibase")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("device payload missing 'public_key_multibase' string field"))?
.to_string();
let status = value
.get("status")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("device payload missing 'status' string field"))?
.to_string();
let created_at_raw = value
.get("created_at")
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("device payload missing 'created_at' timestamp field"))?;
let created_at = DateTime::parse_from_rfc3339(created_at_raw)
.map_err(|error| anyhow!("device payload has malformed 'created_at' timestamp: {error}"))?
.with_timezone(&Utc);
Ok(ClientDevice {
device_id,
public_key,
status,
created_at,
})
}
fn select_candidate(
initial_keys: &HashSet<String>,
activated_at: DateTime<Utc>,
devices: &[ClientDevice],
) -> Result<Option<ClientDevice>> {
let mut eligible = devices.iter().filter(|device| {
device.status == "pending"
&& !initial_keys.contains(&device.public_key)
&& device.created_at >= activated_at
});
let Some(first) = eligible.next().cloned() else {
return Ok(None);
};
if eligible.next().is_some() {
bail!("multiple new pending devices appeared; refusing to auto-approve any of them");
}
Ok(Some(first))
}
fn validate_loopback_base_url(base_url: &str) -> Result<()> {
let authority = base_url
.strip_prefix("http://")
.ok_or_else(|| anyhow!("operation mode requires an http:// loopback base URL"))?;
if authority.contains('@') {
bail!("operation-mode base URL must not contain user info");
}
if authority.contains('?') {
bail!("operation-mode base URL must not contain a query string");
}
if authority.contains('#') {
bail!("operation-mode base URL must not contain a fragment");
}
if authority.contains('/') {
bail!("operation-mode base URL must not contain a path");
}
let (host, port) = authority
.rsplit_once(':')
.ok_or_else(|| anyhow!("operation-mode base URL must include an explicit port"))?;
if host != "127.0.0.1" && host != "localhost" {
bail!("operation-mode base URL host must be loopback (127.0.0.1 or localhost)");
}
if port.is_empty() || !port.bytes().all(|b| b.is_ascii_digit()) {
bail!("operation-mode base URL must include a numeric port");
}
Ok(())
}
fn redact_device_id(device_id: &str) -> String {
let prefix: String = device_id.chars().take(8).collect();
format!("{prefix}…")
}
trait DeviceApprovalApi {
fn list(&self, token: &str) -> Result<Vec<Value>>;
fn confirm(&self, token: &str, device_id: &str) -> Result<Value>;
}
impl DeviceApprovalApi for AgentHttpClient {
fn list(&self, token: &str) -> Result<Vec<Value>> {
self.list_did_devices(token)
}
fn confirm(&self, token: &str, device_id: &str) -> Result<Value> {
self.confirm_did_device(token, device_id)
}
}
fn list_devices(api: &impl DeviceApprovalApi, token: &str) -> Result<Vec<ClientDevice>> {
api.list(token)?
.iter()
.map(parse_device)
.collect::<Result<Vec<_>>>()
}
#[allow(clippy::too_many_arguments)] fn poll_for_approval(
api: &impl DeviceApprovalApi,
token: &str,
initial_keys: &HashSet<String>,
activated_at: DateTime<Utc>,
instance: &str,
timeout: Duration,
poll_interval: Duration,
cancel: &dyn Fn() -> bool,
log_tx: Option<&LogTx>,
) -> Result<String> {
let deadline = Instant::now() + timeout;
let (device_id, public_key) = loop {
if cancel() {
bail!("operation-mode approval cancelled before a device was approved");
}
if Instant::now() >= deadline {
bail!(
"operation-mode approval timed out after {timeout:?} waiting for a new browser \
device for instance {instance}"
);
}
let devices = list_devices(api, token)?;
if let Some(candidate) = select_candidate(initial_keys, activated_at, &devices)? {
break (candidate.device_id, candidate.public_key);
}
if cancel() {
bail!("operation-mode approval cancelled before a device was approved");
}
thread::sleep(poll_interval);
};
tui::sys_log(
log_tx,
format!(
"client-node operation mode [{instance}]: approving device {}",
redact_device_id(&public_key)
),
);
api.confirm(token, &device_id).with_context(|| {
format!(
"failed to confirm device {} for instance {instance}",
redact_device_id(&public_key)
)
})?;
loop {
if cancel() {
bail!("operation-mode approval cancelled while awaiting device activation");
}
if Instant::now() >= deadline {
bail!(
"operation-mode approval timed out after {timeout:?} waiting for device {} to \
become active for instance {instance}",
redact_device_id(&public_key)
);
}
let devices = list_devices(api, token)?;
if let Some(device) = devices.into_iter().find(|d| d.public_key == public_key) {
match device.status.as_str() {
"active" => return Ok(public_key),
"rejected" | "revoked" | "expired" => {
bail!(
"device {} entered terminal state '{}'; aborting operation-mode approval \
for instance {instance}",
redact_device_id(&public_key),
device.status
);
}
_ => {}
}
}
if cancel() {
bail!("operation-mode approval cancelled while awaiting device activation");
}
thread::sleep(poll_interval);
}
}
const SNAPSHOT_READY_TIMEOUT: Duration = Duration::from_secs(90);
const SNAPSHOT_RETRY_INTERVAL: Duration = Duration::from_millis(750);
fn list_devices_once_ready(
client: &AgentHttpClient,
token: &str,
timeout: Duration,
) -> Result<Vec<Value>> {
let deadline = Instant::now() + timeout;
loop {
match client.list_did_devices(token) {
Ok(devices) => return Ok(devices),
Err(error) => {
if !is_surface_unavailable(&error) {
return Err(error);
}
if Instant::now() >= deadline {
return Err(error.context(format!(
"device surface did not become available within {timeout:?}; \
onboarding.device.list never registered a provider"
)));
}
thread::sleep(SNAPSHOT_RETRY_INTERVAL);
}
}
}
}
fn is_surface_unavailable(error: &anyhow::Error) -> bool {
let text = format!("{error:#}");
text.contains("503")
|| text.contains("Service Unavailable")
|| text.contains("SERVICE_UNAVAILABLE")
|| text.contains("Connection refused")
|| text.contains("connection refused")
}
pub struct PreparedOperationMode {
activated_at: DateTime<Utc>,
client: AgentHttpClient,
initial_keys: HashSet<String>,
instance: String,
session: AgentSession,
}
pub fn prepare(session: AgentSession) -> Result<PreparedOperationMode> {
validate_loopback_base_url(&session.base_url)?;
let client = AgentHttpClient::new(session.base_url.clone());
let initial = list_devices_once_ready(&client, &session.token, SNAPSHOT_READY_TIMEOUT)?;
let initial_keys = initial
.iter()
.map(parse_device)
.collect::<Result<Vec<_>>>()?
.into_iter()
.filter(|device| device.status == "pending")
.map(|device| device.public_key)
.collect();
Ok(PreparedOperationMode {
activated_at: Utc::now(),
client,
initial_keys,
instance: session.instance.clone(),
session,
})
}
impl PreparedOperationMode {
fn run(
&self,
log_tx: Option<&LogTx>,
timeout: Duration,
poll_interval: Duration,
) -> Result<String> {
poll_for_approval(
&self.client,
&self.session.token,
&self.initial_keys,
self.activated_at,
&self.instance,
timeout,
poll_interval,
&super::is_cancelled,
log_tx,
)
}
pub fn spawn(self, log_tx: Option<LogTx>) -> std::thread::JoinHandle<()> {
std::thread::spawn(move || {
tui::sys_log(
log_tx.as_ref(),
format!(
"client-node operation mode [{}]: waiting for a new browser device",
self.instance
),
);
match self.run(log_tx.as_ref(), APPROVAL_TIMEOUT, POLL_INTERVAL) {
Ok(device_id) => tui::sys_log(
log_tx.as_ref(),
format!(
"client-node operation mode [{}]: device {} active; browser may enter operation mode",
self.instance,
redact_device_id(&device_id)
),
),
Err(error) => tui::sys_log(
log_tx.as_ref(),
format!(
"client-node operation mode [{}] stopped: {error:#}",
self.instance
),
),
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::VecDeque;
use std::sync::Mutex;
use serde_json::json;
#[test]
fn only_the_unavailable_shape_is_treated_as_not_up_yet() {
for transient in [
"GET /api/v2/did-devices failed: http://127.0.0.1:3301/api/v2/did-devices: status code 503",
"Service Unavailable",
"{\"error\":{\"code\":\"SERVICE_UNAVAILABLE\"}}",
"tcp connect error: Connection refused (os error 61)",
] {
assert!(
is_surface_unavailable(&anyhow!(transient.to_string())),
"{transient:?} should be retried"
);
}
for fatal in [
"GET /api/v2/did-devices failed: status code 401",
"Missing or invalid Authorization header",
"device list response missing 'devices' array",
"status code 500",
] {
assert!(
!is_surface_unavailable(&anyhow!(fatal.to_string())),
"{fatal:?} must fail immediately, never be waited out"
);
}
}
fn device(id: &str, public_key: &str, status: &str, created_at: &str) -> ClientDevice {
ClientDevice {
device_id: id.into(),
public_key: public_key.into(),
status: status.into(),
created_at: DateTime::parse_from_rfc3339(created_at)
.unwrap()
.with_timezone(&Utc),
}
}
#[test]
fn selects_new_public_key_sharing_constant_device_id() {
let initial = HashSet::from(["zStale".to_string()]);
let selected = select_candidate(
&initial,
"2026-07-25T00:00:00Z".parse().unwrap(),
&[device(
"client_node_on_macos",
"zFresh",
"pending",
"2026-07-25T08:38:33Z",
)],
)
.unwrap()
.unwrap();
assert_eq!(selected.public_key, "zFresh");
assert_eq!(selected.device_id, "client_node_on_macos");
}
#[test]
fn ignores_candidates_present_in_initial_snapshot() {
let initial = HashSet::from(["zStaleKey".to_string()]);
let selected = select_candidate(
&initial,
"2026-07-25T00:00:00Z".parse().unwrap(),
&[device(
"some_other_device_id",
"zStaleKey",
"pending",
"2026-07-25T00:00:01Z",
)],
)
.unwrap();
assert_eq!(selected, None);
}
#[test]
fn ignores_candidates_created_before_activation() {
let selected = select_candidate(
&HashSet::new(),
"2026-07-25T00:00:10Z".parse().unwrap(),
&[device("old", "zOld", "pending", "2026-07-25T00:00:09Z")],
)
.unwrap();
assert_eq!(selected, None);
}
#[test]
fn selects_exactly_one_new_pending_candidate() {
let selected = select_candidate(
&HashSet::new(),
"2026-07-25T00:00:00Z".parse().unwrap(),
&[device("fresh", "zFresh", "pending", "2026-07-25T00:00:01Z")],
)
.unwrap()
.unwrap();
assert_eq!(selected.public_key, "zFresh");
}
#[test]
fn rejects_multiple_new_pending_candidates() {
let error = select_candidate(
&HashSet::new(),
"2026-07-25T00:00:00Z".parse().unwrap(),
&[
device(
"client_node_on_macos",
"zOne",
"pending",
"2026-07-25T00:00:01Z",
),
device(
"client_node_on_macos",
"zTwo",
"pending",
"2026-07-25T00:00:02Z",
),
],
)
.unwrap_err();
assert!(error.to_string().contains("multiple"));
assert!(!error.to_string().contains("Bearer"));
}
#[test]
fn accepts_candidate_created_at_the_activation_instant() {
let selected = select_candidate(
&HashSet::new(),
"2026-07-25T00:00:00Z".parse().unwrap(),
&[device("edge", "zEdge", "pending", "2026-07-25T00:00:00Z")],
)
.unwrap()
.unwrap();
assert_eq!(selected.public_key, "zEdge");
}
#[test]
fn ignores_active_devices_when_selecting() {
let selected = select_candidate(
&HashSet::new(),
"2026-07-25T00:00:00Z".parse().unwrap(),
&[device(
"already",
"zActive",
"active",
"2026-07-25T00:00:01Z",
)],
)
.unwrap();
assert_eq!(selected, None);
}
#[test]
fn selects_device_sharing_a_snapshot_device_id_but_with_new_public_key() {
let initial = HashSet::from(["zSnapshotKey".to_string()]);
let selected = select_candidate(
&initial,
"2026-07-25T00:00:00Z".parse().unwrap(),
&[
device(
"client_node_on_macos",
"zSnapshotKey",
"pending",
"2026-07-25T05:25:24Z",
),
device(
"client_node_on_macos",
"zNewKey",
"pending",
"2026-07-25T08:38:33Z",
),
],
)
.unwrap()
.unwrap();
assert_eq!(selected.public_key, "zNewKey");
assert_eq!(selected.device_id, "client_node_on_macos");
}
#[test]
fn parse_device_reads_all_required_fields() {
let parsed = parse_device(&json!({
"device_id": "browser-abc",
"public_key_multibase": "zBrowserAbcKey",
"status": "pending",
"created_at": "2026-07-25T00:00:01Z"
}))
.unwrap();
assert_eq!(parsed.device_id, "browser-abc");
assert_eq!(parsed.public_key, "zBrowserAbcKey");
assert_eq!(parsed.status, "pending");
}
#[test]
fn parse_device_rejects_missing_created_at() {
let error = parse_device(&json!({
"device_id": "browser-abc",
"public_key_multibase": "zBrowserAbcKey",
"status": "pending"
}))
.unwrap_err();
assert!(error.to_string().contains("created_at"));
}
#[test]
fn parse_device_rejects_malformed_created_at() {
let error = parse_device(&json!({
"device_id": "browser-abc",
"public_key_multibase": "zBrowserAbcKey",
"status": "pending",
"created_at": "not-a-timestamp"
}))
.unwrap_err();
assert!(error.to_string().contains("created_at"));
}
#[test]
fn parse_device_rejects_missing_device_id() {
let error = parse_device(&json!({
"public_key_multibase": "zBrowserAbcKey",
"status": "pending",
"created_at": "2026-07-25T00:00:01Z"
}))
.unwrap_err();
assert!(error.to_string().contains("device_id"));
}
#[test]
fn parse_device_rejects_missing_public_key_multibase() {
let error = parse_device(&json!({
"device_id": "browser-abc",
"status": "pending",
"created_at": "2026-07-25T00:00:01Z"
}))
.unwrap_err();
assert!(error.to_string().contains("public_key_multibase"));
}
#[test]
fn accepts_loopback_ipv4_with_port() {
validate_loopback_base_url("http://127.0.0.1:3001").unwrap();
}
#[test]
fn accepts_localhost_with_port() {
validate_loopback_base_url("http://localhost:8080").unwrap();
}
#[test]
fn rejects_https_scheme() {
assert!(validate_loopback_base_url("https://127.0.0.1:3001").is_err());
}
#[test]
fn rejects_missing_port() {
assert!(validate_loopback_base_url("http://127.0.0.1").is_err());
assert!(validate_loopback_base_url("http://localhost").is_err());
}
#[test]
fn rejects_empty_port() {
assert!(validate_loopback_base_url("http://127.0.0.1:").is_err());
}
#[test]
fn rejects_non_numeric_port() {
assert!(validate_loopback_base_url("http://127.0.0.1:abc").is_err());
}
#[test]
fn rejects_user_info() {
assert!(validate_loopback_base_url("http://user@127.0.0.1:3001").is_err());
}
#[test]
fn rejects_query_string() {
assert!(validate_loopback_base_url("http://127.0.0.1:3001?token=x").is_err());
}
#[test]
fn rejects_fragment() {
assert!(validate_loopback_base_url("http://127.0.0.1:3001#frag").is_err());
}
#[test]
fn rejects_path() {
assert!(validate_loopback_base_url("http://127.0.0.1:3001/api").is_err());
}
#[test]
fn rejects_non_loopback_host() {
assert!(validate_loopback_base_url("http://evil.example.com:3001").is_err());
assert!(validate_loopback_base_url("http://10.0.0.5:3001").is_err());
}
#[test]
fn redacts_long_device_id_to_eight_chars_plus_ellipsis() {
let redacted = redact_device_id("browser-device-1234567890");
assert_eq!(redacted, "browser-…");
assert!(!redacted.contains("device-1234567890"));
}
#[test]
fn redacts_short_device_id_without_panicking_on_unicode() {
let redacted = redact_device_id("dév");
assert!(redacted.starts_with("dév"));
assert!(redacted.ends_with('…'));
}
enum ListStep {
Ok(Vec<Value>),
Err(String),
}
struct FakeApi {
steps: Mutex<VecDeque<ListStep>>,
last: Mutex<Vec<Value>>,
confirmations: Mutex<Vec<String>>,
confirm_err: Option<String>,
}
impl FakeApi {
fn new(steps: Vec<ListStep>) -> Self {
Self {
steps: Mutex::new(steps.into_iter().collect()),
last: Mutex::new(Vec::new()),
confirmations: Mutex::new(Vec::new()),
confirm_err: None,
}
}
fn with_confirm_error(mut self, message: &str) -> Self {
self.confirm_err = Some(message.to_string());
self
}
fn confirmations(&self) -> Vec<String> {
self.confirmations.lock().unwrap().clone()
}
}
impl DeviceApprovalApi for FakeApi {
fn list(&self, _token: &str) -> Result<Vec<Value>> {
let mut steps = self.steps.lock().unwrap();
match steps.pop_front() {
Some(ListStep::Ok(devices)) => {
*self.last.lock().unwrap() = devices.clone();
Ok(devices)
}
Some(ListStep::Err(message)) => Err(anyhow!(message)),
None => Ok(self.last.lock().unwrap().clone()),
}
}
fn confirm(&self, _token: &str, device_id: &str) -> Result<Value> {
self.confirmations
.lock()
.unwrap()
.push(device_id.to_string());
match &self.confirm_err {
Some(message) => Err(anyhow!(message.clone())),
None => Ok(json!({ "device_id": device_id, "status": "confirming" })),
}
}
}
fn dev_json(id: &str, public_key: &str, status: &str, created_at: &str) -> Value {
json!({
"device_id": id,
"public_key_multibase": public_key,
"status": status,
"created_at": created_at
})
}
fn no_cancel() -> bool {
false
}
const ACTIVATED_AT: &str = "2026-07-25T00:00:00Z";
const PLATFORM_DEVICE_ID: &str = "client_node_on_macos";
fn run_poll(
api: &impl DeviceApprovalApi,
cancel: &dyn Fn() -> bool,
timeout: Duration,
) -> Result<String> {
poll_for_approval(
api,
"secret-bearer-token-value",
&HashSet::new(),
ACTIVATED_AT.parse().unwrap(),
"alice",
timeout,
Duration::from_millis(1),
cancel,
None,
)
}
#[test]
fn zero_candidates_times_out_without_confirming() {
let api = FakeApi::new(vec![ListStep::Ok(vec![])]);
let error = run_poll(&api, &no_cancel, Duration::from_millis(20)).unwrap_err();
assert!(error.to_string().contains("timed out"));
assert!(api.confirmations().is_empty());
}
#[test]
fn confirms_single_candidate_exactly_once_then_succeeds() {
let api = FakeApi::new(vec![
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"zBrowserX",
"pending",
"2026-07-25T00:00:01Z",
)]),
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"zBrowserX",
"confirming",
"2026-07-25T00:00:01Z",
)]),
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"zBrowserX",
"active",
"2026-07-25T00:00:01Z",
)]),
]);
let public_key = run_poll(&api, &no_cancel, Duration::from_secs(5)).unwrap();
assert_eq!(public_key, "zBrowserX");
assert_eq!(api.confirmations(), vec![PLATFORM_DEVICE_ID.to_string()]);
}
#[test]
fn candidate_already_active_after_confirmation_succeeds() {
let api = FakeApi::new(vec![
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"zBrowserX",
"pending",
"2026-07-25T00:00:01Z",
)]),
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"zBrowserX",
"active",
"2026-07-25T00:00:01Z",
)]),
]);
let public_key = run_poll(&api, &no_cancel, Duration::from_secs(5)).unwrap();
assert_eq!(public_key, "zBrowserX");
assert_eq!(api.confirmations(), vec![PLATFORM_DEVICE_ID.to_string()]);
}
#[test]
fn multiple_candidates_abort_without_confirming() {
let api = FakeApi::new(vec![ListStep::Ok(vec![
dev_json(
PLATFORM_DEVICE_ID,
"zBrowserA",
"pending",
"2026-07-25T00:00:01Z",
),
dev_json(
PLATFORM_DEVICE_ID,
"zBrowserB",
"pending",
"2026-07-25T00:00:02Z",
),
])]);
let error = run_poll(&api, &no_cancel, Duration::from_secs(5)).unwrap_err();
assert!(error.to_string().contains("multiple"));
assert!(api.confirmations().is_empty());
}
#[test]
fn terminal_state_after_confirmation_aborts() {
for terminal in ["rejected", "revoked", "expired"] {
let api = FakeApi::new(vec![
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"zBrowserX",
"pending",
"2026-07-25T00:00:01Z",
)]),
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"zBrowserX",
terminal,
"2026-07-25T00:00:01Z",
)]),
]);
let error = run_poll(&api, &no_cancel, Duration::from_secs(5)).unwrap_err();
assert!(
error.to_string().contains(terminal),
"expected terminal state '{terminal}' in error: {error}"
);
assert_eq!(api.confirmations(), vec![PLATFORM_DEVICE_ID.to_string()]);
}
}
#[test]
fn cancellation_stops_before_confirming() {
let api = FakeApi::new(vec![ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"zBrowserX",
"pending",
"2026-07-25T00:00:01Z",
)])]);
let error = run_poll(&api, &|| true, Duration::from_secs(5)).unwrap_err();
assert!(error.to_string().contains("cancelled"));
assert!(api.confirmations().is_empty());
}
#[test]
fn malformed_response_fails_closed() {
let api = FakeApi::new(vec![ListStep::Ok(vec![json!({
"device_id": PLATFORM_DEVICE_ID,
"public_key_multibase": "zBrowserX",
"status": "pending"
})])]);
let error = run_poll(&api, &no_cancel, Duration::from_secs(5)).unwrap_err();
assert!(error.to_string().contains("created_at"));
assert!(api.confirmations().is_empty());
}
#[test]
fn missing_public_key_fails_closed() {
let api = FakeApi::new(vec![ListStep::Ok(vec![json!({
"device_id": PLATFORM_DEVICE_ID,
"status": "pending",
"created_at": "2026-07-25T00:00:01Z"
})])]);
let error = run_poll(&api, &no_cancel, Duration::from_secs(5)).unwrap_err();
assert!(error.to_string().contains("public_key_multibase"));
assert!(api.confirmations().is_empty());
}
#[test]
fn list_transport_error_fails_closed() {
let api = FakeApi::new(vec![ListStep::Err("network down".to_string())]);
let error = run_poll(&api, &no_cancel, Duration::from_secs(5)).unwrap_err();
assert!(error.to_string().contains("network down"));
assert!(api.confirmations().is_empty());
}
#[test]
fn confirmation_error_does_not_disclose_token() {
let api = FakeApi::new(vec![ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"zBrowserX",
"pending",
"2026-07-25T00:00:01Z",
)])])
.with_confirm_error("simulated confirm failure");
let error = run_poll(&api, &no_cancel, Duration::from_secs(5)).unwrap_err();
let text = format!("{error:#}");
assert!(text.contains("simulated confirm failure"));
assert!(!text.contains("secret-bearer-token-value"));
assert!(!text.contains("Bearer"));
assert_eq!(api.confirmations(), vec![PLATFORM_DEVICE_ID.to_string()]);
}
#[test]
fn logs_redacted_public_key_before_confirming_without_leaking_secrets() {
use crate::tui::TuiEvent;
let (tx, rx) = std::sync::mpsc::sync_channel(16);
let api = FakeApi::new(vec![
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"browser-key-secret-9999",
"pending",
"2026-07-25T00:00:01Z",
)]),
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"browser-key-secret-9999",
"active",
"2026-07-25T00:00:01Z",
)]),
]);
let public_key = poll_for_approval(
&api,
"secret-bearer-token-value",
&HashSet::new(),
ACTIVATED_AT.parse().unwrap(),
"alice",
Duration::from_secs(5),
Duration::from_millis(1),
&no_cancel,
Some(&tx),
)
.unwrap();
assert_eq!(public_key, "browser-key-secret-9999");
drop(tx);
let lines: Vec<String> = rx
.into_iter()
.filter_map(|event| match event {
TuiEvent::Log(entry) => Some(entry.line),
_ => None,
})
.collect();
let approving: Vec<&String> = lines.iter().filter(|l| l.contains("approving")).collect();
assert_eq!(
approving.len(),
1,
"expected exactly one approving log line, got {lines:?}"
);
let line = approving[0];
assert!(
line.contains("browser-…"),
"approving line must carry the redacted public key: {line}"
);
assert!(!line.contains("secret-9999"));
assert!(!line.contains("secret-bearer-token-value"));
}
#[test]
fn production_path_delivers_all_three_lifecycle_lines_on_one_channel_without_leaking_secrets() {
use crate::tui::TuiEvent;
let (tx, rx) = std::sync::mpsc::sync_channel(16);
let api = FakeApi::new(vec![
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"browser-key-secret-abcdef1234",
"pending",
"2026-07-25T00:00:01Z",
)]),
ListStep::Ok(vec![dev_json(
PLATFORM_DEVICE_ID,
"browser-key-secret-abcdef1234",
"active",
"2026-07-25T00:00:01Z",
)]),
]);
let instance = "alice";
const TOKEN: &str = "secret-bearer-token-value";
tui::sys_log(
Some(&tx),
format!("client-node operation mode [{instance}]: waiting for a new browser device"),
);
let device_id = poll_for_approval(
&api,
TOKEN,
&HashSet::new(),
ACTIVATED_AT.parse().unwrap(),
instance,
Duration::from_secs(5),
Duration::from_millis(1),
&no_cancel,
Some(&tx),
)
.unwrap();
tui::sys_log(
Some(&tx),
format!(
"client-node operation mode [{instance}]: device {} active; browser may enter operation mode",
redact_device_id(&device_id)
),
);
drop(tx);
let lines: Vec<String> = rx
.into_iter()
.filter_map(|event| match event {
TuiEvent::Log(entry) => Some(entry.line),
_ => None,
})
.collect();
assert_eq!(
lines.len(),
3,
"expected all three lifecycle lines on one channel: {lines:?}"
);
assert!(lines[0].contains("waiting for a new browser device"));
assert!(
lines[1].contains("approving device") && lines[1].contains("browser-…"),
"line #2 must be the redacted approving line: {}",
lines[1]
);
assert!(
lines[2].contains("active; browser may enter operation mode")
&& lines[2].contains("browser-…"),
"line #3 must reference the same redacted id: {}",
lines[2]
);
for line in &lines {
assert!(
!line.contains("secret-abcdef1234"),
"leaked full device id: {line}"
);
assert!(!line.contains(TOKEN), "leaked bearer token: {line}");
assert!(!line.contains("Bearer"), "leaked Bearer scheme: {line}");
}
}
#[test]
fn run_forwards_log_tx_into_poll_for_approval_instead_of_hardcoding_none() {
let source = include_str!("operation_mode.rs");
assert!(
source.contains("fn run(") && source.contains("log_tx: Option<&LogTx>"),
"PreparedOperationMode::run must accept a log_tx: Option<&LogTx> parameter"
);
assert!(
source.contains("self.run(log_tx.as_ref(), APPROVAL_TIMEOUT, POLL_INTERVAL)"),
"spawn must call run with log_tx.as_ref(), not a hardcoded None"
);
let run_start = source.find("fn run(").expect("run method must exist");
let run_end = source[run_start..]
.find("/// Spawn the coordinator")
.map(|offset| run_start + offset)
.expect("spawn method must follow run");
let run_body = &source[run_start..run_end];
assert!(
run_body.contains("poll_for_approval("),
"run must call poll_for_approval: {run_body}"
);
assert!(
run_body.contains("log_tx,"),
"run must forward log_tx into poll_for_approval: {run_body}"
);
assert!(
!run_body.contains("&super::is_cancelled,\n None,"),
"run must not hardcode log_tx: None when calling poll_for_approval: {run_body}"
);
}
}