From a43ddac261f2c3750e0f3e3ff00d769b09dc4592 Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 23 Sep 2026 12:54:40 +0200 Subject: [PATCH 1/4] docs(internet-identity): cover local II minting with auth 10 - Local II needs agentOptions (host + root key): the client mints delegations via the II canister and its agent defaults to mainnet - Local II must come from a recent network launcher (`icp network update`) - Pitfalls: getIdentity() inside a subscribe() listener, and logout timers set from the short-lived delegation's expiration - Evals 27-29 for the new pitfalls --- evaluations/internet-identity.json | 30 ++++++++++++++++++++++++++++++ skills/internet-identity/SKILL.md | 23 ++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/evaluations/internet-identity.json b/evaluations/internet-identity.json index 7424eb4..db9e369 100644 --- a/evaluations/internet-identity.json +++ b/evaluations/internet-identity.json @@ -264,6 +264,36 @@ "Offers a concrete resolution: either move core to ^6 alongside @icp-sdk/auth@^10, or stay on @icp-sdk/auth@^9 which peers core ^5", "Does NOT recommend --legacy-peer-deps or --force to get past the peer conflict" ] + }, + { + "name": "Adversarial: local II sign-in without agentOptions", + "prompt": "I run Internet Identity on my local network (`ii: true` in icp.yaml) and use @icp-sdk/auth 10 with `new AuthClient({ identityProvider: { authorizeUrl: 'http://id.ai.localhost:8000/authorize', canisterId: 'rdmx6-jaaaa-aaaaa-aaadq-cai' } })`. The II popup opens and I finish the ceremony, but signIn() still fails. What am I missing? Just the fix and a short explanation, no full app.", + "expected_behaviors": [ + "Adds agentOptions to the AuthClient constructor with the local host (e.g. window.location.origin) and the root key from the ic_env cookie (IC_ROOT_KEY)", + "Explains that the client mints its delegations by calling the II canister and that the agent for those calls defaults to mainnet (icp-api.io)", + "Mentions that an outdated local II may lack the minting methods, fixed by running `icp network update` and restarting the network", + "Does NOT suggest fetchRootKey() or shouldFetchRootKey, and does NOT suggest switching to mainnet II as the fix" + ] + }, + { + "name": "Adversarial: getIdentity inside a subscribe listener", + "prompt": "With @icp-sdk/auth 10 I rebuild my backend actor in `authClient.subscribe(async () => { actor = createActor(await authClient.getIdentity()); })`. Right after signIn() the listener throws SessionNotHeldError. Why, and what should the listener look like instead? Just the explanation and the corrected listener.", + "expected_behaviors": [ + "Explains that a subscribe listener runs as soon as the sign-in record changes, before the client has installed the new identity", + "The corrected listener reads only getStatus() or isAuthenticated(), not getIdentity()", + "Gets the identity from the value signIn() resolves to, or calls getIdentity() at the time a canister call is made", + "Does NOT suggest catching and ignoring SessionNotHeldError, or adding a setTimeout/retry delay before getIdentity()" + ] + }, + { + "name": "Adversarial: logout timer from the delegation expiration", + "prompt": "After upgrading to @icp-sdk/auth 10 my users are logged out after a few minutes. I schedule `setTimeout(logout, expirationMs - Date.now())` from `identity.getDelegation().delegations[0].delegation.expiration`. What should I do instead? Just the fix.", + "expected_behaviors": [ + "Explains that in 9.x and later the delegation getIdentity() signs with is short-lived and replaced by the client as it ages, so its expiration is not the end of the session", + "Removes the timer", + "Reacts to the session ending through authClient.subscribe(), checking isAuthenticated() or an 'expired' status from getStatus()", + "Does NOT suggest re-scheduling the timer whenever the delegation is refreshed" + ] } ], "trigger_evals": { diff --git a/skills/internet-identity/SKILL.md b/skills/internet-identity/SKILL.md index 41e5b80..19dde4e 100644 --- a/skills/internet-identity/SKILL.md +++ b/skills/internet-identity/SKILL.md @@ -75,6 +75,12 @@ Internet Identity (II) is the Internet Computer's native authentication system. Do not clear it with `--legacy-peer-deps` — that skips the peer check and installs the mismatched pair anyway. Pin `@icp-sdk/auth@^10` with `@icp-sdk/core@^6`, or stay on `@icp-sdk/auth@^9` if something else holds you on core 5. +18. **Signing in against a local II without `agentOptions`.** The client mints its delegations by calling the II canister, through an agent that defaults to `https://icp-api.io`. With a local II (`ii: true`), the popup opens at `http://id.ai.localhost:8000/authorize` and the ceremony completes, but the mint goes to mainnet II, which rejects the local session. Pass `agentOptions: { host, rootKey }` with the same values as your backend actor. With mainnet II (the default), leave `agentOptions` unset. A local II from an older network launcher lacks the minting methods entirely: run `icp network update` and restart the network. See "Fallback: deploy II locally". + +19. **Calling `getIdentity()` inside a `subscribe()` listener.** A listener runs as soon as the record of the sign-in changes — during your own `signIn()`, and when another tab signs in — before this client has installed the identity that goes with it. `getIdentity()` then throws `SessionNotHeldError` ("A sign-in exists for this domain, but this origin holds no credential for it"). In the listener, read `getStatus()` or `isAuthenticated()` only. Take the identity from what `signIn()` resolves to, or call `getIdentity()` when you make a call. + +20. **Scheduling a logout from the delegation's expiration.** In 9.x and later the delegation `getIdentity()` signs with is short-lived and replaced by the client as it ages, so a timer set from `identity.getDelegation()`'s expiration signs the user out after minutes, not at the end of the session. The session's end arrives as an `expired` status: subscribe, and leave the signed-in view when `isAuthenticated()` turns false. + ## Using II during local development **Default: use mainnet II from your local network.** Starting with `icp-cli >= 0.2.4`, the local network (pocket-ic, launched by `icp-cli-network-launcher`) is configured to trust the mainnet subnet's BLS signatures. Delegations signed by `https://id.ai` are accepted by your local replica, so both the sign-in flow *and* authenticated calls to a locally-deployed backend just work — no extra config in `icp.yaml`, no local II canister to manage, and the UI is the real one your users will see. @@ -94,6 +100,20 @@ networks: This deploys the II canisters automatically when the local network is started. The II frontend will be available at `http://id.ai.localhost:8000`, so the client is constructed with `identityProvider: { authorizeUrl: 'http://id.ai.localhost:8000/authorize', canisterId: 'rdmx6-jaaaa-aaaaa-aaadq-cai' }` — the canister id is the same locally, since system canisters keep their mainnet ids on the local network. No canister entry is needed in your project — II is not part of your project's canisters. For the full `icp.yaml` canister configuration, see the **icp-cli** and **static-site** skills. +The client mints its delegations by calling that canister itself, and the agent it makes those calls with defaults to `https://icp-api.io` — mainnet. Point it at the local network with `agentOptions`, using the same host and root key as your backend actor, or the ceremony completes and the mint then goes to mainnet II, which rejects the local session: + +```javascript +const authClient = new AuthClient({ + identityProvider: { + authorizeUrl: "http://id.ai.localhost:8000/authorize", + canisterId: "rdmx6-jaaaa-aaaaa-aaadq-cai", + }, + agentOptions: { host: window.location.origin, rootKey: canisterEnv?.IC_ROOT_KEY }, +}); +``` + +The local II must also be recent enough to mint: `@icp-sdk/auth` 9.x and later call its `app_prepare_delegation` / `app_get_delegation` methods, which the II bundled with older network launchers does not have. Run `icp network update` to fetch the latest launcher, then restart the local network. + ### Frontend: Vanilla JavaScript/TypeScript Sign-In Flow This is framework-agnostic. Adapt the DOM manipulation to your framework. @@ -166,7 +186,8 @@ async function init() { // Re-render when who is signed in here changes, including in another tab: // getStatus() is 'signed-in' | 'signed-in-elsewhere' | 'expired' | - // 'signed-out', and the last three each want a different screen. + // 'signed-out', and the last three each want a different screen. Read only + // the status here: a listener runs before a sign-in installs its identity. authClient.subscribe(() => render(authClient.getStatus())); } From c19ec7794b023c3c508b11ca1da43a9aae4d51bb Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 23 Sep 2026 14:21:32 +0200 Subject: [PATCH 2/4] docs(internet-identity): local II needs only the root key, not host The auth client's agent already resolves to the page origin on localhost; without the local root key the mint fails certificate verification. Eval 27 updated accordingly, and its mainnet-II check scoped to the fix. --- evaluations/internet-identity.json | 7 ++++--- skills/internet-identity/SKILL.md | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/evaluations/internet-identity.json b/evaluations/internet-identity.json index db9e369..0ea90ed 100644 --- a/evaluations/internet-identity.json +++ b/evaluations/internet-identity.json @@ -269,10 +269,11 @@ "name": "Adversarial: local II sign-in without agentOptions", "prompt": "I run Internet Identity on my local network (`ii: true` in icp.yaml) and use @icp-sdk/auth 10 with `new AuthClient({ identityProvider: { authorizeUrl: 'http://id.ai.localhost:8000/authorize', canisterId: 'rdmx6-jaaaa-aaaaa-aaadq-cai' } })`. The II popup opens and I finish the ceremony, but signIn() still fails. What am I missing? Just the fix and a short explanation, no full app.", "expected_behaviors": [ - "Adds agentOptions to the AuthClient constructor with the local host (e.g. window.location.origin) and the root key from the ic_env cookie (IC_ROOT_KEY)", - "Explains that the client mints its delegations by calling the II canister and that the agent for those calls defaults to mainnet (icp-api.io)", + "Adds agentOptions to the AuthClient constructor with the root key from the ic_env cookie (IC_ROOT_KEY)", + "Explains that the client mints its delegations by calling the II canister and that, without the local root key, it verifies the local replica's response against the mainnet root key and fails", "Mentions that an outdated local II may lack the minting methods, fixed by running `icp network update` and restarting the network", - "Does NOT suggest fetchRootKey() or shouldFetchRootKey, and does NOT suggest switching to mainnet II as the fix" + "Does NOT set host (e.g. window.location.origin) in agentOptions, and does NOT suggest fetchRootKey() or shouldFetchRootKey", + "Fixes the local II setup rather than replacing it: switching to mainnet II is NOT presented as the fix (mentioning it as the default alternative is fine)" ] }, { diff --git a/skills/internet-identity/SKILL.md b/skills/internet-identity/SKILL.md index 19dde4e..2b636a9 100644 --- a/skills/internet-identity/SKILL.md +++ b/skills/internet-identity/SKILL.md @@ -75,7 +75,7 @@ Internet Identity (II) is the Internet Computer's native authentication system. Do not clear it with `--legacy-peer-deps` — that skips the peer check and installs the mismatched pair anyway. Pin `@icp-sdk/auth@^10` with `@icp-sdk/core@^6`, or stay on `@icp-sdk/auth@^9` if something else holds you on core 5. -18. **Signing in against a local II without `agentOptions`.** The client mints its delegations by calling the II canister, through an agent that defaults to `https://icp-api.io`. With a local II (`ii: true`), the popup opens at `http://id.ai.localhost:8000/authorize` and the ceremony completes, but the mint goes to mainnet II, which rejects the local session. Pass `agentOptions: { host, rootKey }` with the same values as your backend actor. With mainnet II (the default), leave `agentOptions` unset. A local II from an older network launcher lacks the minting methods entirely: run `icp network update` and restart the network. See "Fallback: deploy II locally". +18. **Signing in against a local II without `agentOptions`.** The client mints its delegations by calling the II canister, through an agent that verifies responses against the mainnet root key by default. With a local II (`ii: true`), the popup opens at `http://id.ai.localhost:8000/authorize` and the ceremony completes, but the mint then fails with `TrustError: Certificate verification error` (`"Invalid signature"`). Pass `agentOptions: { rootKey }` with the root key from the `ic_env` cookie, and leave `host` unset. With mainnet II (the default), leave `agentOptions` unset. A local II from an older network launcher lacks the minting methods entirely: run `icp network update` and restart the network. See "Fallback: deploy II locally". 19. **Calling `getIdentity()` inside a `subscribe()` listener.** A listener runs as soon as the record of the sign-in changes — during your own `signIn()`, and when another tab signs in — before this client has installed the identity that goes with it. `getIdentity()` then throws `SessionNotHeldError` ("A sign-in exists for this domain, but this origin holds no credential for it"). In the listener, read `getStatus()` or `isAuthenticated()` only. Take the identity from what `signIn()` resolves to, or call `getIdentity()` when you make a call. @@ -100,7 +100,7 @@ networks: This deploys the II canisters automatically when the local network is started. The II frontend will be available at `http://id.ai.localhost:8000`, so the client is constructed with `identityProvider: { authorizeUrl: 'http://id.ai.localhost:8000/authorize', canisterId: 'rdmx6-jaaaa-aaaaa-aaadq-cai' }` — the canister id is the same locally, since system canisters keep their mainnet ids on the local network. No canister entry is needed in your project — II is not part of your project's canisters. For the full `icp.yaml` canister configuration, see the **icp-cli** and **static-site** skills. -The client mints its delegations by calling that canister itself, and the agent it makes those calls with defaults to `https://icp-api.io` — mainnet. Point it at the local network with `agentOptions`, using the same host and root key as your backend actor, or the ceremony completes and the mint then goes to mainnet II, which rejects the local session: +The client mints its delegations by calling that canister itself, through an agent that verifies responses against the mainnet root key unless told otherwise. Pass the local root key from the `ic_env` cookie via `agentOptions`, or the ceremony completes and the mint then fails with `TrustError: Certificate verification error` (`"Invalid signature"`). Do not set `host`: the agent's default already resolves to the page origin on `localhost` (see the **icp-cli** skill's binding-generation reference). ```javascript const authClient = new AuthClient({ @@ -108,7 +108,7 @@ const authClient = new AuthClient({ authorizeUrl: "http://id.ai.localhost:8000/authorize", canisterId: "rdmx6-jaaaa-aaaaa-aaadq-cai", }, - agentOptions: { host: window.location.origin, rootKey: canisterEnv?.IC_ROOT_KEY }, + agentOptions: { rootKey: canisterEnv?.IC_ROOT_KEY }, }); ``` From 70c4c61f0c16df56ff0de09743de68a230efb9df Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 23 Sep 2026 16:13:29 +0200 Subject: [PATCH 3/4] docs(internet-identity): drop the subscribe-listener pitfall, fixed in icp-js-auth#198 --- evaluations/internet-identity.json | 10 ---------- skills/internet-identity/SKILL.md | 7 ++----- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/evaluations/internet-identity.json b/evaluations/internet-identity.json index 0ea90ed..22dd956 100644 --- a/evaluations/internet-identity.json +++ b/evaluations/internet-identity.json @@ -276,16 +276,6 @@ "Fixes the local II setup rather than replacing it: switching to mainnet II is NOT presented as the fix (mentioning it as the default alternative is fine)" ] }, - { - "name": "Adversarial: getIdentity inside a subscribe listener", - "prompt": "With @icp-sdk/auth 10 I rebuild my backend actor in `authClient.subscribe(async () => { actor = createActor(await authClient.getIdentity()); })`. Right after signIn() the listener throws SessionNotHeldError. Why, and what should the listener look like instead? Just the explanation and the corrected listener.", - "expected_behaviors": [ - "Explains that a subscribe listener runs as soon as the sign-in record changes, before the client has installed the new identity", - "The corrected listener reads only getStatus() or isAuthenticated(), not getIdentity()", - "Gets the identity from the value signIn() resolves to, or calls getIdentity() at the time a canister call is made", - "Does NOT suggest catching and ignoring SessionNotHeldError, or adding a setTimeout/retry delay before getIdentity()" - ] - }, { "name": "Adversarial: logout timer from the delegation expiration", "prompt": "After upgrading to @icp-sdk/auth 10 my users are logged out after a few minutes. I schedule `setTimeout(logout, expirationMs - Date.now())` from `identity.getDelegation().delegations[0].delegation.expiration`. What should I do instead? Just the fix.", diff --git a/skills/internet-identity/SKILL.md b/skills/internet-identity/SKILL.md index 2b636a9..3c8e21c 100644 --- a/skills/internet-identity/SKILL.md +++ b/skills/internet-identity/SKILL.md @@ -77,9 +77,7 @@ Internet Identity (II) is the Internet Computer's native authentication system. 18. **Signing in against a local II without `agentOptions`.** The client mints its delegations by calling the II canister, through an agent that verifies responses against the mainnet root key by default. With a local II (`ii: true`), the popup opens at `http://id.ai.localhost:8000/authorize` and the ceremony completes, but the mint then fails with `TrustError: Certificate verification error` (`"Invalid signature"`). Pass `agentOptions: { rootKey }` with the root key from the `ic_env` cookie, and leave `host` unset. With mainnet II (the default), leave `agentOptions` unset. A local II from an older network launcher lacks the minting methods entirely: run `icp network update` and restart the network. See "Fallback: deploy II locally". -19. **Calling `getIdentity()` inside a `subscribe()` listener.** A listener runs as soon as the record of the sign-in changes — during your own `signIn()`, and when another tab signs in — before this client has installed the identity that goes with it. `getIdentity()` then throws `SessionNotHeldError` ("A sign-in exists for this domain, but this origin holds no credential for it"). In the listener, read `getStatus()` or `isAuthenticated()` only. Take the identity from what `signIn()` resolves to, or call `getIdentity()` when you make a call. - -20. **Scheduling a logout from the delegation's expiration.** In 9.x and later the delegation `getIdentity()` signs with is short-lived and replaced by the client as it ages, so a timer set from `identity.getDelegation()`'s expiration signs the user out after minutes, not at the end of the session. The session's end arrives as an `expired` status: subscribe, and leave the signed-in view when `isAuthenticated()` turns false. +19. **Scheduling a logout from the delegation's expiration.** In 9.x and later the delegation `getIdentity()` signs with is short-lived and replaced by the client as it ages, so a timer set from `identity.getDelegation()`'s expiration signs the user out after minutes, not at the end of the session. The session's end arrives as an `expired` status: subscribe, and leave the signed-in view when `isAuthenticated()` turns false. ## Using II during local development @@ -186,8 +184,7 @@ async function init() { // Re-render when who is signed in here changes, including in another tab: // getStatus() is 'signed-in' | 'signed-in-elsewhere' | 'expired' | - // 'signed-out', and the last three each want a different screen. Read only - // the status here: a listener runs before a sign-in installs its identity. + // 'signed-out', and the last three each want a different screen. authClient.subscribe(() => render(authClient.getStatus())); } From 2a001a1740899f3ff8a0f640f18d5dd08d4bc65d Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Wed, 23 Sep 2026 18:37:26 +0200 Subject: [PATCH 4/4] fix(internet-identity): drop host from the authenticated actor example --- evaluations/internet-identity.json | 2 +- skills/internet-identity/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/evaluations/internet-identity.json b/evaluations/internet-identity.json index 22dd956..67b4844 100644 --- a/evaluations/internet-identity.json +++ b/evaluations/internet-identity.json @@ -40,7 +40,7 @@ "Creates an HttpAgent with the identity", "Creates an actor using Actor.createActor with the agent", "All await calls are inside async functions — no bare top-level await", - "Uses rootKey from ic_env cookie (safeGetCanisterEnv) or host: window.location.origin — does NOT use shouldFetchRootKey or hardcoded host branching" + "Uses rootKey from ic_env cookie (safeGetCanisterEnv) and leaves host unset — does NOT set host: window.location.origin, use shouldFetchRootKey, or branch on a hardcoded host" ] }, { diff --git a/skills/internet-identity/SKILL.md b/skills/internet-identity/SKILL.md index 3c8e21c..ab5501b 100644 --- a/skills/internet-identity/SKILL.md +++ b/skills/internet-identity/SKILL.md @@ -162,10 +162,10 @@ async function signOut() { // Create an authenticated agent and actor. // Uses rootKey from the ic_env cookie — no shouldFetchRootKey or environment branching needed. +// No host: the default resolves correctly locally, on mainnet and on custom domains. async function createAuthenticatedActor(identity, canisterId, idlFactory) { const agent = await HttpAgent.create({ identity, - host: window.location.origin, rootKey: canisterEnv?.IC_ROOT_KEY, });