Skip to content

Commit 6ddd2fb

Browse files
committed
feat: Make internet identity available at id.ai.localhost
1 parent 49a8298 commit 6ddd2fb

File tree

6 files changed

+101
-8
lines changed

6 files changed

+101
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Unreleased
22

3+
Important: A network launcher more recent than v12.0.0-83c3f95e8c4ce28e02493df83df5f84a166451c0 is
4+
required to use internet identity.
5+
36
* feat: Many more commands support `--json` and `--quiet`.
7+
* feat: When a local network is started internet identity is available at id.ai.localhost
48
* fix: Network would fail to start if a stale descriptor was present
59

610
# v0.2.1

crates/icp-canister-interfaces/src/internet_identity.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use candid::Principal;
22

3+
pub const INTERNET_IDENTITY_FRONTEND_CID: &str = "uqzsh-gqaaa-aaaaq-qaada-cai";
34
pub const INTERNET_IDENTITY_CID: &str = "rdmx6-jaaaa-aaaaa-aaadq-cai";
45
pub const INTERNET_IDENTITY_PRINCIPAL: Principal =
56
Principal::from_slice(&[0, 0, 0, 0, 0, 0, 0, 7, 1, 1]);
@@ -12,4 +13,10 @@ mod tests {
1213
fn internet_identity_cid_and_principal_match() {
1314
assert_eq!(INTERNET_IDENTITY_CID, INTERNET_IDENTITY_PRINCIPAL.to_text());
1415
}
16+
17+
#[test]
18+
fn internet_identity_frontend_cid_is_valid() {
19+
// Verify the CID is a valid principal
20+
Principal::from_text(INTERNET_IDENTITY_FRONTEND_CID).unwrap();
21+
}
1522
}

crates/icp/src/context/mod.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -548,9 +548,15 @@ impl Context {
548548
env_mappings.insert(env_name.clone(), mapping);
549549
}
550550
}
551-
if let Err(e) =
552-
crate::network::custom_domains::write_custom_domains(status_dir, domain, &env_mappings)
553-
{
551+
let extra: Vec<_> = crate::network::custom_domains::ii_custom_domain_entry(desc.ii, domain)
552+
.into_iter()
553+
.collect();
554+
if let Err(e) = crate::network::custom_domains::write_custom_domains(
555+
status_dir,
556+
domain,
557+
&env_mappings,
558+
&extra,
559+
) {
554560
tracing::warn!("Failed to update custom domains: {e}");
555561
}
556562
}

crates/icp/src/network/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ pub struct NetworkDescriptorModel {
7575
pub candid_ui_canister_id: Option<Principal>,
7676
/// Canister ID of the deployed proxy canister, if any.
7777
pub proxy_canister_id: Option<Principal>,
78+
/// Whether the Internet Identity canister is deployed on this network.
79+
#[serde(default)]
80+
pub ii: bool,
7881
/// Path to the status directory shared with the network launcher.
7982
/// Used to write `custom-domains.txt` for friendly domain routing.
8083
#[serde(default)]

crates/icp/src/network/custom_domains.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,43 @@ use crate::{prelude::*, store_id::IdMapping};
1111
/// Each line has the format `<canister_name>.<env_name>.<domain>:<principal>`.
1212
/// The file is written fresh each time from the full set of current mappings
1313
/// across all environments sharing this network.
14+
///
15+
/// `extra_entries` are raw `(full_domain, canister_id)` pairs appended after the
16+
/// environment-based entries (e.g. system canisters like Internet Identity).
1417
pub fn write_custom_domains(
1518
status_dir: &Path,
1619
domain: &str,
1720
env_mappings: &BTreeMap<String, IdMapping>,
21+
extra_entries: &[(String, String)],
1822
) -> Result<(), WriteCustomDomainsError> {
1923
let file_path = status_dir.join("custom-domains.txt");
20-
let content: String = env_mappings
24+
let mut content: String = env_mappings
2125
.iter()
2226
.flat_map(|(env_name, mappings)| {
2327
mappings
2428
.iter()
2529
.map(move |(name, principal)| format!("{name}.{env_name}.{domain}:{principal}\n"))
2630
})
2731
.collect();
32+
for (full_domain, canister_id) in extra_entries {
33+
content.push_str(&format!("{full_domain}:{canister_id}\n"));
34+
}
2835
crate::fs::write(&file_path, content.as_bytes())?;
2936
Ok(())
3037
}
3138

39+
/// Returns the custom domain entry for the II frontend canister, if II is enabled.
40+
pub fn ii_custom_domain_entry(ii: bool, domain: &str) -> Option<(String, String)> {
41+
if ii {
42+
Some((
43+
format!("id.ai.{domain}"),
44+
icp_canister_interfaces::internet_identity::INTERNET_IDENTITY_FRONTEND_CID.to_string(),
45+
))
46+
} else {
47+
None
48+
}
49+
}
50+
3251
/// Extracts the domain authority from a gateway URL for use in subdomain-based
3352
/// canister routing.
3453
///
@@ -110,7 +129,7 @@ mod tests {
110129
);
111130
env_mappings.insert("staging".to_string(), staging_mappings);
112131

113-
write_custom_domains(dir.path(), "localhost", &env_mappings).unwrap();
132+
write_custom_domains(dir.path(), "localhost", &env_mappings, &[]).unwrap();
114133

115134
let content = std::fs::read_to_string(dir.path().join("custom-domains.txt")).unwrap();
116135
// BTreeMap is ordered, so local comes before staging
@@ -122,6 +141,38 @@ mod tests {
122141
);
123142
}
124143

144+
#[test]
145+
fn write_custom_domains_with_extra_entries() {
146+
let dir = camino_tempfile::Utf8TempDir::new().unwrap();
147+
let env_mappings = BTreeMap::new();
148+
let extra = vec![(
149+
"id.ai.localhost".to_string(),
150+
"uqzsh-gqaaa-aaaaq-qaada-cai".to_string(),
151+
)];
152+
153+
write_custom_domains(dir.path(), "localhost", &env_mappings, &extra).unwrap();
154+
155+
let content = std::fs::read_to_string(dir.path().join("custom-domains.txt")).unwrap();
156+
assert_eq!(content, "id.ai.localhost:uqzsh-gqaaa-aaaaq-qaada-cai\n");
157+
}
158+
159+
#[test]
160+
fn ii_custom_domain_entry_returns_entry_when_enabled() {
161+
let entry = ii_custom_domain_entry(true, "localhost");
162+
assert_eq!(
163+
entry,
164+
Some((
165+
"id.ai.localhost".to_string(),
166+
"uqzsh-gqaaa-aaaaq-qaada-cai".to_string()
167+
))
168+
);
169+
}
170+
171+
#[test]
172+
fn ii_custom_domain_entry_returns_none_when_disabled() {
173+
assert_eq!(ii_custom_domain_entry(false, "localhost"), None);
174+
}
175+
125176
#[test]
126177
fn canister_gateway_url_with_friendly_domain() {
127178
let base: Url = "http://localhost:8000".parse().unwrap();

crates/icp/src/network/managed/run.rs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,12 @@ async fn run_network_launcher(
241241
info!("Network started on port {}", instance.gateway_port);
242242
}
243243

244+
let gateway_url: Url = format!("http://{}:{}", gateway.host, gateway.port)
245+
.parse()
246+
.unwrap();
247+
244248
let (candid_ui_canister_id, proxy_canister_id) = initialize_network(
245-
&format!("http://{}:{}", gateway.host, gateway.port)
246-
.parse()
247-
.unwrap(),
249+
&gateway_url,
248250
&instance.root_key,
249251
all_identities,
250252
default_identity,
@@ -253,6 +255,8 @@ async fn run_network_launcher(
253255
)
254256
.await?;
255257

258+
let ii = matches!(&config.mode, ManagedMode::Launcher(cfg) if cfg.ii);
259+
256260
network_root
257261
.with_write(async |root| -> Result<_, RunNetworkLauncherError> {
258262
// Acquire locks for all fixed ports
@@ -274,6 +278,7 @@ async fn run_network_launcher(
274278
pocketic_instance_id: instance.pocketic_instance_id,
275279
candid_ui_canister_id,
276280
proxy_canister_id,
281+
ii,
277282
status_dir: Some(status_dir_path.clone()),
278283
use_friendly_domains: instance.use_friendly_domains,
279284
};
@@ -284,6 +289,23 @@ async fn run_network_launcher(
284289
Ok(())
285290
})
286291
.await??;
292+
293+
// Write initial custom-domains.txt with system canister entries (e.g. II)
294+
if instance.use_friendly_domains
295+
&& let Some(domain) = crate::network::custom_domains::gateway_domain(&gateway_url)
296+
{
297+
let extra: Vec<_> = crate::network::custom_domains::ii_custom_domain_entry(ii, domain)
298+
.into_iter()
299+
.collect();
300+
if !extra.is_empty() {
301+
let _ = crate::network::custom_domains::write_custom_domains(
302+
&status_dir_path,
303+
domain,
304+
&std::collections::BTreeMap::new(),
305+
&extra,
306+
);
307+
}
308+
}
287309
if background {
288310
info!("To stop the network, run `icp network stop`");
289311
guard.defuse();

0 commit comments

Comments
 (0)