From 6ddd2fb37a75df414a680e096876d33db0ccf6ba Mon Sep 17 00:00:00 2001 From: Raymond Khalife Date: Fri, 27 Mar 2026 15:41:56 -0400 Subject: [PATCH 1/2] feat: Make internet identity available at id.ai.localhost --- CHANGELOG.md | 4 ++ .../src/internet_identity.rs | 7 +++ crates/icp/src/context/mod.rs | 12 +++- crates/icp/src/network/config.rs | 3 + crates/icp/src/network/custom_domains.rs | 55 ++++++++++++++++++- crates/icp/src/network/managed/run.rs | 28 +++++++++- 6 files changed, 101 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 822b507be..55f8aaf24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Unreleased +Important: A network launcher more recent than v12.0.0-83c3f95e8c4ce28e02493df83df5f84a166451c0 is +required to use internet identity. + * feat: Many more commands support `--json` and `--quiet`. +* feat: When a local network is started internet identity is available at id.ai.localhost * fix: Network would fail to start if a stale descriptor was present # v0.2.1 diff --git a/crates/icp-canister-interfaces/src/internet_identity.rs b/crates/icp-canister-interfaces/src/internet_identity.rs index fdb8b90bf..e57033d9a 100644 --- a/crates/icp-canister-interfaces/src/internet_identity.rs +++ b/crates/icp-canister-interfaces/src/internet_identity.rs @@ -1,5 +1,6 @@ use candid::Principal; +pub const INTERNET_IDENTITY_FRONTEND_CID: &str = "uqzsh-gqaaa-aaaaq-qaada-cai"; pub const INTERNET_IDENTITY_CID: &str = "rdmx6-jaaaa-aaaaa-aaadq-cai"; pub const INTERNET_IDENTITY_PRINCIPAL: Principal = Principal::from_slice(&[0, 0, 0, 0, 0, 0, 0, 7, 1, 1]); @@ -12,4 +13,10 @@ mod tests { fn internet_identity_cid_and_principal_match() { assert_eq!(INTERNET_IDENTITY_CID, INTERNET_IDENTITY_PRINCIPAL.to_text()); } + + #[test] + fn internet_identity_frontend_cid_is_valid() { + // Verify the CID is a valid principal + Principal::from_text(INTERNET_IDENTITY_FRONTEND_CID).unwrap(); + } } diff --git a/crates/icp/src/context/mod.rs b/crates/icp/src/context/mod.rs index 94be911e9..270d08a52 100644 --- a/crates/icp/src/context/mod.rs +++ b/crates/icp/src/context/mod.rs @@ -548,9 +548,15 @@ impl Context { env_mappings.insert(env_name.clone(), mapping); } } - if let Err(e) = - crate::network::custom_domains::write_custom_domains(status_dir, domain, &env_mappings) - { + let extra: Vec<_> = crate::network::custom_domains::ii_custom_domain_entry(desc.ii, domain) + .into_iter() + .collect(); + if let Err(e) = crate::network::custom_domains::write_custom_domains( + status_dir, + domain, + &env_mappings, + &extra, + ) { tracing::warn!("Failed to update custom domains: {e}"); } } diff --git a/crates/icp/src/network/config.rs b/crates/icp/src/network/config.rs index cc663ff43..c66ac4b2b 100644 --- a/crates/icp/src/network/config.rs +++ b/crates/icp/src/network/config.rs @@ -75,6 +75,9 @@ pub struct NetworkDescriptorModel { pub candid_ui_canister_id: Option, /// Canister ID of the deployed proxy canister, if any. pub proxy_canister_id: Option, + /// Whether the Internet Identity canister is deployed on this network. + #[serde(default)] + pub ii: bool, /// Path to the status directory shared with the network launcher. /// Used to write `custom-domains.txt` for friendly domain routing. #[serde(default)] diff --git a/crates/icp/src/network/custom_domains.rs b/crates/icp/src/network/custom_domains.rs index ce0f615eb..9938a070e 100644 --- a/crates/icp/src/network/custom_domains.rs +++ b/crates/icp/src/network/custom_domains.rs @@ -11,13 +11,17 @@ use crate::{prelude::*, store_id::IdMapping}; /// Each line has the format `..:`. /// The file is written fresh each time from the full set of current mappings /// across all environments sharing this network. +/// +/// `extra_entries` are raw `(full_domain, canister_id)` pairs appended after the +/// environment-based entries (e.g. system canisters like Internet Identity). pub fn write_custom_domains( status_dir: &Path, domain: &str, env_mappings: &BTreeMap, + extra_entries: &[(String, String)], ) -> Result<(), WriteCustomDomainsError> { let file_path = status_dir.join("custom-domains.txt"); - let content: String = env_mappings + let mut content: String = env_mappings .iter() .flat_map(|(env_name, mappings)| { mappings @@ -25,10 +29,25 @@ pub fn write_custom_domains( .map(move |(name, principal)| format!("{name}.{env_name}.{domain}:{principal}\n")) }) .collect(); + for (full_domain, canister_id) in extra_entries { + content.push_str(&format!("{full_domain}:{canister_id}\n")); + } crate::fs::write(&file_path, content.as_bytes())?; Ok(()) } +/// Returns the custom domain entry for the II frontend canister, if II is enabled. +pub fn ii_custom_domain_entry(ii: bool, domain: &str) -> Option<(String, String)> { + if ii { + Some(( + format!("id.ai.{domain}"), + icp_canister_interfaces::internet_identity::INTERNET_IDENTITY_FRONTEND_CID.to_string(), + )) + } else { + None + } +} + /// Extracts the domain authority from a gateway URL for use in subdomain-based /// canister routing. /// @@ -110,7 +129,7 @@ mod tests { ); env_mappings.insert("staging".to_string(), staging_mappings); - write_custom_domains(dir.path(), "localhost", &env_mappings).unwrap(); + write_custom_domains(dir.path(), "localhost", &env_mappings, &[]).unwrap(); let content = std::fs::read_to_string(dir.path().join("custom-domains.txt")).unwrap(); // BTreeMap is ordered, so local comes before staging @@ -122,6 +141,38 @@ mod tests { ); } + #[test] + fn write_custom_domains_with_extra_entries() { + let dir = camino_tempfile::Utf8TempDir::new().unwrap(); + let env_mappings = BTreeMap::new(); + let extra = vec![( + "id.ai.localhost".to_string(), + "uqzsh-gqaaa-aaaaq-qaada-cai".to_string(), + )]; + + write_custom_domains(dir.path(), "localhost", &env_mappings, &extra).unwrap(); + + let content = std::fs::read_to_string(dir.path().join("custom-domains.txt")).unwrap(); + assert_eq!(content, "id.ai.localhost:uqzsh-gqaaa-aaaaq-qaada-cai\n"); + } + + #[test] + fn ii_custom_domain_entry_returns_entry_when_enabled() { + let entry = ii_custom_domain_entry(true, "localhost"); + assert_eq!( + entry, + Some(( + "id.ai.localhost".to_string(), + "uqzsh-gqaaa-aaaaq-qaada-cai".to_string() + )) + ); + } + + #[test] + fn ii_custom_domain_entry_returns_none_when_disabled() { + assert_eq!(ii_custom_domain_entry(false, "localhost"), None); + } + #[test] fn canister_gateway_url_with_friendly_domain() { let base: Url = "http://localhost:8000".parse().unwrap(); diff --git a/crates/icp/src/network/managed/run.rs b/crates/icp/src/network/managed/run.rs index 27ad3c252..dcc47ecae 100644 --- a/crates/icp/src/network/managed/run.rs +++ b/crates/icp/src/network/managed/run.rs @@ -241,10 +241,12 @@ async fn run_network_launcher( info!("Network started on port {}", instance.gateway_port); } + let gateway_url: Url = format!("http://{}:{}", gateway.host, gateway.port) + .parse() + .unwrap(); + let (candid_ui_canister_id, proxy_canister_id) = initialize_network( - &format!("http://{}:{}", gateway.host, gateway.port) - .parse() - .unwrap(), + &gateway_url, &instance.root_key, all_identities, default_identity, @@ -253,6 +255,8 @@ async fn run_network_launcher( ) .await?; + let ii = matches!(&config.mode, ManagedMode::Launcher(cfg) if cfg.ii); + network_root .with_write(async |root| -> Result<_, RunNetworkLauncherError> { // Acquire locks for all fixed ports @@ -274,6 +278,7 @@ async fn run_network_launcher( pocketic_instance_id: instance.pocketic_instance_id, candid_ui_canister_id, proxy_canister_id, + ii, status_dir: Some(status_dir_path.clone()), use_friendly_domains: instance.use_friendly_domains, }; @@ -284,6 +289,23 @@ async fn run_network_launcher( Ok(()) }) .await??; + + // Write initial custom-domains.txt with system canister entries (e.g. II) + if instance.use_friendly_domains + && let Some(domain) = crate::network::custom_domains::gateway_domain(&gateway_url) + { + let extra: Vec<_> = crate::network::custom_domains::ii_custom_domain_entry(ii, domain) + .into_iter() + .collect(); + if !extra.is_empty() { + let _ = crate::network::custom_domains::write_custom_domains( + &status_dir_path, + domain, + &std::collections::BTreeMap::new(), + &extra, + ); + } + } if background { info!("To stop the network, run `icp network stop`"); guard.defuse(); From c3d61558c510c985bc70925c4afcdeda299c40be Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Fri, 27 Mar 2026 16:17:43 -0400 Subject: [PATCH 2/2] refactor: Rename internet identity constants to match frontend canister Rename INTERNET_IDENTITY_CID/INTERNET_IDENTITY_PRINCIPAL to INTERNET_IDENTITY_FRONTEND_CID/INTERNET_IDENTITY_FRONTEND_PRINCIPAL and update test references accordingly. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/internet_identity.rs | 18 +++++++----------- crates/icp-cli/tests/network_tests.rs | 4 ++-- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/crates/icp-canister-interfaces/src/internet_identity.rs b/crates/icp-canister-interfaces/src/internet_identity.rs index e57033d9a..d6208fc94 100644 --- a/crates/icp-canister-interfaces/src/internet_identity.rs +++ b/crates/icp-canister-interfaces/src/internet_identity.rs @@ -1,22 +1,18 @@ use candid::Principal; pub const INTERNET_IDENTITY_FRONTEND_CID: &str = "uqzsh-gqaaa-aaaaq-qaada-cai"; -pub const INTERNET_IDENTITY_CID: &str = "rdmx6-jaaaa-aaaaa-aaadq-cai"; -pub const INTERNET_IDENTITY_PRINCIPAL: Principal = - Principal::from_slice(&[0, 0, 0, 0, 0, 0, 0, 7, 1, 1]); +pub const INTERNET_IDENTITY_FRONTEND_PRINCIPAL: Principal = + Principal::from_slice(&[0, 0, 0, 0, 2, 16, 0, 6, 1, 1]); #[cfg(test)] mod tests { use super::*; #[test] - fn internet_identity_cid_and_principal_match() { - assert_eq!(INTERNET_IDENTITY_CID, INTERNET_IDENTITY_PRINCIPAL.to_text()); - } - - #[test] - fn internet_identity_frontend_cid_is_valid() { - // Verify the CID is a valid principal - Principal::from_text(INTERNET_IDENTITY_FRONTEND_CID).unwrap(); + fn internet_identity_frontend_cid_and_principal_match() { + assert_eq!( + INTERNET_IDENTITY_FRONTEND_CID, + INTERNET_IDENTITY_FRONTEND_PRINCIPAL.to_text() + ); } } diff --git a/crates/icp-cli/tests/network_tests.rs b/crates/icp-cli/tests/network_tests.rs index 8f652f98b..320e31570 100644 --- a/crates/icp-cli/tests/network_tests.rs +++ b/crates/icp-cli/tests/network_tests.rs @@ -4,7 +4,7 @@ use candid::Principal; use icp_canister_interfaces::{ cycles_ledger::CYCLES_LEDGER_PRINCIPAL, cycles_minting_canister::CYCLES_MINTING_CANISTER_PRINCIPAL, icp_ledger::ICP_LEDGER_PRINCIPAL, - internet_identity::INTERNET_IDENTITY_PRINCIPAL, registry::REGISTRY_PRINCIPAL, + internet_identity::INTERNET_IDENTITY_FRONTEND_PRINCIPAL, registry::REGISTRY_PRINCIPAL, }; use indoc::{formatdoc, indoc}; use predicates::{ @@ -495,7 +495,7 @@ async fn network_starts_with_canisters_preset() { .unwrap(); // Internet identity agent - .read_state_canister_module_hash(INTERNET_IDENTITY_PRINCIPAL) + .read_state_canister_module_hash(INTERNET_IDENTITY_FRONTEND_PRINCIPAL) .await .unwrap(); }