[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6GPVV10ASKNNVCKS9SVC7A8":3,"skill-same:01M6GPVV10ASKNNVCKS9SVC7A8":94},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":16,"sourceHtmlUrl":17,"categories":18,"keywords":27,"publishedAt":48,"linkOnly":49,"markdownContent":50,"markdownBody":51,"frontMatter":52,"refName":57,"commitSha":58,"skillDirectory":59,"forksCount":60,"repositoryHtmlUrl":61,"repositoryHomepageUrl":6,"ownerHtmlUrl":62,"ownerBlogUrl":63,"ownerDescription":64,"updatedAt":65,"install":66,"resources":79},"01M6GPVV10ASKNNVCKS9SVC7A8","internet-identity",null,"Add Internet Identity sign-in to a frontend or canister: passkey and OpenID (Google, Apple, Microsoft) sign-in with @icp-sdk\u002Fauth, sending users to an organization's SSO with ssoDomain, the client lifecycle, and local development. Also covers the primary origin an app's principals are permanently tied to: electing it, setting derivationOrigin, the ii-alternative-origins and ii-derivation-origin documents, and the ii-app-metadata document that names the app on the sign-in screen. Use when adding sign-in, login, auth, passkeys or Internet Identity to an app, when an app gains a custom domain or a sibling app, or when the same user arrives as a different principal on two origins. For a verified email or name in the backend use internet-identity-attributes; for web push use internet-identity-notifications; to set up an organization's own SSO use internet-identity-sso. Do NOT use for wallet or ICRC signer flows (use wallet-integration) or for an agent or CLI signing in as a user (use agent-web-identity).","jiayaoqijia\u002Fcryptoskill","jiayaoqijia","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F1453879?v=4","01M6GMYTX7Q7S504VC6E756ENR","YQ","01M6GMYV1QKJ1CC75MTTGT9PR0",80,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fdfinity-official-internet-identity\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fdfinity-official-internet-identity\u002FSKILL.md",[19,24],{"categoryNo":20,"categoryName":21,"taxonomyType":22,"taxonomyTypeDesc":23},"01JBSEEDCAT000000000000009","前端开发","SCENARIO","使用场景",{"categoryNo":25,"categoryName":26,"taxonomyType":22,"taxonomyTypeDesc":23},"01JBSEEDCAT000000000000010","后端与接口",[28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47],"identity","internet identity","internet","passkey","Custom Domain","when","documents","local","up","SET","cli","apple","notifications","icp","frontend","flows","auth","Wallet","attributes","app","2026-11-10 10:12:30",false,"---\nname: internet-identity\ndescription: \"Add Internet Identity sign-in to a frontend or canister: passkey and OpenID (Google, Apple, Microsoft) sign-in with @icp-sdk\u002Fauth, sending users to an organization's SSO with ssoDomain, the client lifecycle, and local development. Also covers the primary origin an app's principals are permanently tied to: electing it, setting derivationOrigin, the ii-alternative-origins and ii-derivation-origin documents, and the ii-app-metadata document that names the app on the sign-in screen. Use when adding sign-in, login, auth, passkeys or Internet Identity to an app, when an app gains a custom domain or a sibling app, or when the same user arrives as a different principal on two origins. For a verified email or name in the backend use internet-identity-attributes; for web push use internet-identity-notifications; to set up an organization's own SSO use internet-identity-sso. Do NOT use for wallet or ICRC signer flows (use wallet-integration) or for an agent or CLI signing in as a user (use agent-web-identity).\"\nlicense: Apache-2.0\ncompatibility: \"icp-cli >= 0.2.4, Node.js >= 22\"\nmetadata:\n  title: Internet Identity\n  category: Auth\n---\n\n# Internet Identity Authentication\n\n## What This Is\n\nInternet Identity (II) is the Internet Computer's native authentication system. Users sign in to II-powered apps with passkeys, with an OpenID account (Google, Apple, Microsoft), or through their organization's own SSO; no usernames or passwords. Each user gets a unique principal per app origin, preventing cross-app tracking.\n\n## Prerequisites\n\n- `@icp-sdk\u002Fauth@^11` with `@icp-sdk\u002Fcore@^6`. **Pin both majors together.** auth 11 and 10 peer `@icp-sdk\u002Fcore@^6`; auth 9 peers `^5`.\n\n## Canister IDs\n\n| Canister | ID | URL | Purpose |\n|---|---|---|---|\n| Internet Identity (backend) | `rdmx6-jaaaa-aaaaa-aaadq-cai` |  | What sign-in runs against; signs attribute bundles |\n| Internet Identity (frontend) | `uqzsh-gqaaa-aaaaq-qaada-cai` | `https:\u002F\u002Fid.ai` | Serves the sign-in UI; `authorizeUrl` points here |\n\nBoth IDs are identical on mainnet and on a local network. Hardcode them.\n\n## Mistakes That Break Your Build\n\n1. **Wrong II URL for the environment.** `authorizeUrl` points at the **frontend** canister: `https:\u002F\u002Fid.ai\u002Fauthorize` on mainnet, `http:\u002F\u002Fid.ai.localhost:8000\u002Fauthorize` for a local II (`ii: true`). The URL is used verbatim, so include `\u002Fauthorize`: `https:\u002F\u002Fid.ai` opens the II home page and never returns a delegation.\n2. **Passing `identityProvider` as a string, or half of it.** It is an object, `{ authorizeUrl, canisterId }`, both required together; a string or `URL` throws a `TypeError`. Omit the option entirely to get mainnet II, which is what most apps want.\n3. **Top-level `await` in frontend code.** Vite 6 and earlier target `es2020`-era browsers by default, which have no top-level await, so the build fails; Vite 7 allows it. Wrap the setup in an `async function init()` (or a framework lifecycle hook). Do NOT change `build.target` to `esnext` to make it compile.\n4. **Treating `maxTimeToLive` as the lifetime of the signing key.** It bounds the **session** at II, and `maxTimeToIdle` ends a session nobody uses; the delegation your calls are signed with is short-lived and replaced by the client. Leave both unset unless the app has its own policy: II applies seven days idle and thirty days in total.\n5. **Not awaiting `signIn()` or skipping the `try`\u002F`catch`.** `signIn()` rejects when the user closes the popup or authentication fails.\n6. **`shouldFetchRootKey` or `fetchRootKey()` in browser code.** The `ic_env` cookie (set by the frontend canister or the Vite dev server) carries the root key as `IC_ROOT_KEY`: pass it as `rootKey` to `HttpAgent.create()` and leave `host` unset. Fetching the root key at runtime lets a man-in-the-middle substitute a fake one on mainnet.\n7. **Getting `2vxsx-fae` as the principal.** That is the anonymous principal: authentication silently failed. Usual causes: a wrong `authorizeUrl` (especially missing `\u002Fauthorize`), an unhandled `signIn()` rejection, or reading the identity before `signIn()` resolved. `getIdentity()` throws `SessionNotHeldError` (not an anonymous identity) when a sign-in exists that this origin holds no credential for.\n8. **Passing the principal as an argument.** The backend reads the caller from the IC protocol: `msg.caller` in Motoko, `ic_cdk::api::msg_caller()` in Rust. An argument is whatever the caller typed and proves nothing. Role guards and anonymous-principal rejection are in the `canister-security` skill.\n9. **Letting the first origin with users be one you can lose.** Decided in \"Your app's identity is its primary origin\" below, before writing any sign-in code. The origin an account is created under is permanent: repointing it hands every returning user a new, empty account, so whether this app will have siblings has to be settled *before* it has users. What is *not* a mistake: setting `derivationOrigin` to the origin you are already served at is a validated no-op, not an error. II compares the two in canonical form and skips validation entirely, which is also why `ic0.app`, `icp0.io` and `icp.net` are interchangeable and a single-origin app needs no configuration at all.\n10. **Sharing a cookie domain without a shared `derivationOrigin`.** Elect the primary origin first (\"Your app's identity is its primary origin\" below), then build the rest from the `internet-identity-shared-sessions` skill. Subdomains share a sign-in only when they share a principal, and a cookie does not produce one: without one primary origin authorized for all of them, the shared record names an account the reading origin can never hold, and `\u002Freauth` bounces the user forever.\n11. **A silent re-issue without `hint`, on the default transport, or to an undeclared callback.** All three are the `\u002Freauth` route of a shared sign-in, which the `internet-identity-shared-sessions` skill builds end to end. `prompt: 'none'` without `hint` fails with `InteractionRequiredError` (`reason` `account_selection_required`) when II holds more than one session. It runs on page load with no user gesture, so it needs `transport: 'redirect'` on a route of its own, declared in the origin's `\u002F.well-known\u002Fii-auth-callbacks`, or the redirect never comes back.\n12. **Serving `\u002F.well-known\u002Fii-app-metadata` on the wrong origin, without CORS, or with one bad field.** Every rule is in \"App metadata on the sign-in screen\" below, including which characters and sizes are rejected. In short: II reads the document from the primary origin (when set) or the request origin, the document and the logo both need CORS, and one invalid field invalidates the whole thing rather than just that field.\n13. **Installing majors that do not pair.** auth 11 peers `@icp-sdk\u002Fcore@^6`; pinning core to `^5` out of habit breaks the build.\n\n## Using II during local development\n\n**Default: use mainnet II from the local network.** With `icp-cli >= 0.2.4` the local network trusts the mainnet subnet's signatures, so delegations from `https:\u002F\u002Fid.ai` work against a locally deployed backend. Construct the client with no `identityProvider`.\n\n**Fallback: a local II,** only for fully offline development or a specific II build. Add `ii: true` to the local network in `icp.yaml`:\n\n```yaml\nnetworks:\n  - name: local\n    mode: managed\n    ii: true\n```\n\nII is then served at `http:\u002F\u002Fid.ai.localhost:8000`, with the same canister IDs. No canister entry is needed in your project. Pass the local root key:\n\n```javascript\nconst authClient = new AuthClient({\n  identityProvider: {\n    authorizeUrl: \"http:\u002F\u002Fid.ai.localhost:8000\u002Fauthorize\",\n    canisterId: \"rdmx6-jaaaa-aaaaa-aaadq-cai\",\n  },\n  agentOptions: { rootKey: canisterEnv?.IC_ROOT_KEY },\n});\n```\n\n## Frontend: Sign-In Flow\n\nFramework-agnostic; adapt the DOM parts.\n\n```javascript\nimport { AuthClient } from \"@icp-sdk\u002Fauth\u002Fclient\";\nimport { HttpAgent, Actor } from \"@icp-sdk\u002Fcore\u002Fagent\";\nimport { safeGetCanisterEnv } from \"@icp-sdk\u002Fcore\u002Fagent\u002Fcanister-env\";\n\n\u002F\u002F The ic_env cookie carries the root key and canister IDs, locally and on mainnet.\nconst canisterEnv = safeGetCanisterEnv();\n\n\u002F\u002F Mainnet II is the default. derivationOrigin, openIdProvider, ssoDomain,\n\u002F\u002F stateStorage, transport, prompt, and hint are also constructor options.\nconst authClient = new AuthClient();\n\nasync function signIn() {\n  try {\n    \u002F\u002F Optional session bounds: signIn({ maxTimeToIdle, maxTimeToLive }), in ns.\n    return await authClient.signIn();\n  } catch (error) {\n    console.error(\"Sign-in failed:\", error);\n    throw error;\n  }\n}\n\n\u002F\u002F Ends the session at II: every tab of this origin is signed out.\nasync function signOut() {\n  await authClient.signOut();\n}\n\n\u002F\u002F No host: the default resolves correctly locally, on mainnet, and on custom domains.\nasync function createAuthenticatedActor(identity, canisterId, idlFactory) {\n  const agent = await HttpAgent.create({ identity, rootKey: canisterEnv?.IC_ROOT_KEY });\n  return Actor.createActor(idlFactory, { agent, canisterId });\n}\n\n\u002F\u002F Wrapped in a function: Vite 6 and earlier reject top-level await by default.\nasync function init() {\n  \u002F\u002F isAuthenticated() is sync; getIdentity() is async.\n  if (authClient.isAuthenticated()) {\n    const identity = await authClient.getIdentity();\n    const actor = await createAuthenticatedActor(identity, canisterId, idlFactory);\n  }\n\n  \u002F\u002F getStatus(): 'signed-in' | 'signed-in-elsewhere' | 'expired' | 'signed-out'.\n  \u002F\u002F subscribe() fires on any change, including in another tab.\n  authClient.subscribe(() => render(authClient.getStatus()));\n}\n\ninit();\n```\n\n**Lifecycle.** One client for the page, or one per component: both read and write the same sign-in. A client a view owns is disposed with it: `dispose()` releases its listeners, subscription, and scheduled refresh, and is **not** a sign-out.\n\n## One-click sign-in\n\n`openIdProvider` (`'google'`, `'apple'`, `'microsoft'`) or `ssoDomain` (an organization's domain) skips II's method screen. They are mutually exclusive (a type error, and a throw at construction) and constructor-only.\n\n```javascript\nconst google = new AuthClient({ openIdProvider: \"google\" });\nconst acme = new AuthClient({ ssoDomain: \"acme.com\" });\n```\n\n### Checking an organization's domain\n\nA client built with `ssoDomain` checks its own domain through II as soon as it is constructed and reports the result like the session: a synchronous snapshot from `getSsoStatus()`, change notifications from `subscribe()`. No exceptions, no `AbortSignal`.\n\n```typescript\ntype SsoStatus =\n  | { state: \"checking\" }\n  | { state: \"available\"; name?: string }      \u002F\u002F name from the org's file\n  | { state: \"invalid\"; reason: string }       \u002F\u002F not a domain: the user fixes the input\n  | { state: \"unavailable\"; retryAfter?: Date; reason?: string };\n```\n\nChecking while the user types, one client per pause rather than per keystroke. A superseded client is disposed before it can publish, so stale results never render:\n\n```javascript\nlet client;\nlet timer;\n\ninput.addEventListener(\"input\", () => {\n  client?.dispose();  \u002F\u002F also stops the old client's check\n  client = undefined; \u002F\u002F Continue never signs in to the old domain\n  showSpinner();\n\n  clearTimeout(timer); \u002F\u002F debounce\n  timer = setTimeout(() => {\n    const next = new AuthClient({ ssoDomain: input.value }); \u002F\u002F never throws for the domain\n    next.subscribe(() => render(next.getSsoStatus()));\n    render(next.getSsoStatus());\n    client = next;\n  }, 300);\n});\n\n\u002F\u002F Synchronous in the click handler: never await the check first.\ncontinueButton.addEventListener(\"click\", () => {\n  client?.signIn().then(showApp, showSignInError);\n});\n```\n\n- `refreshSsoStatus()` re-runs the check for a \"Try again\" button. While `retryAfter` is in the future, II answers `unavailable` again at once with no new fetch, so disable the button and show a countdown. It does nothing on an `invalid` client, and a client never re-checks by itself.\n- `signIn()` proceeds in every state: II resolves the domain itself and shows its own loading and error screens, for an `invalid` value too.\n- `invalid` follows II's own rules: at least two labels of 1 to 63 letters, digits, or hyphens, no hyphen at either end, no `--` in a label's third and fourth places (punycode `xn--` aside), at most 253 characters, and a port only on a loopback host. Its `reason` names every problem found.\n- The check warms II's cache, so `signIn()` on the same client starts fast.\n\nThe organization's own side of this, which is the OIDC client, the `ii-openid-configuration` file, per-app access, the limits that reject the file and the caching windows, is the `internet-identity-sso` skill.\n\n## Your app's identity is its primary origin\n\nInternet Identity derives a principal per origin. Nothing in the sign-in flow above sets one, and that is correct: with no `derivationOrigin`, principals derive from the origin you are served at, which for a new app is its canister address. You already have an app identity, and it is already anchored on something nobody can take away.\n\nYou reach for `derivationOrigin` only when that stops being enough, which happens in exactly two situations: the app gains a second origin, usually a custom domain, or it gains a sibling app whose users have to be the same people. Both are below.\n\n### Decide before your first sign-in\n\nThe origin an account is created under cannot be changed afterwards: repointing it orphans every account made there. So the question to settle before shipping is not \"do I have a custom domain yet\" but **is this one app, or the first of several?**\n\n- **One app.** Nothing to do. The canister address is the identity, now and later. Adding a domain later is handled below and costs nothing.\n- **The first of several.** Create the canister that will serve the primary origin before the first app ships, and point that app at it from its first deploy.\n\nYou do not need to know what the suite will contain. The `ii-alternative-origins` list grows as each app ships; what cannot change is which origin the list lives on.\n\n### Adding a domain\n\nA custom domain is a second origin, so left alone it is a second set of users. Keep the identity on the canister address and let the domain derive against it.\n\n1. The domain passes `derivationOrigin`; the canister origin does not:\n\n```javascript\nconst authClient = new AuthClient({ derivationOrigin: \"https:\u002F\u002F\u003Ccanister-id>.icp.net\" });\n```\n\n2. The canister origin serves `\u002F.well-known\u002Fii-alternative-origins`:\n\n```json\n{ \"alternativeOrigins\": [\"https:\u002F\u002Fshop.example.com\"] }\n```\n\nAt most **100** entries, origins only (no trailing slash, no path). Over the cap II rejects the **entire list**, so every alternative origin stops authenticating, not just the extras. II canonicalizes `ic0.app`, `icp0.io` and `icp.net` to one form, so one entry per origin covers all three gateway spellings.\n\n3. With `@dfinity\u002Fstatic-site`, add a `_headers` block (the file has no extension, and certified assets set no CORS header by default):\n\n```\n\u002F.well-known\u002Fii-alternative-origins\n  Content-Type: application\u002Fjson\n  Access-Control-Allow-Origin: *\n```\n\nPointing the derivation origin at the domain instead works, since II validates it like any other origin, but is not recommended. A domain can lapse, be sold or be rebranded, and every account derived from it goes with it. A canister address cannot be taken away.\n\n### A suite that shares one sign-in\n\nApps that share a sign-in must share a principal, so they must all derive from one origin, and someone has to elect it. Elect the origin that represents the whole product rather than one app inside it, such as an app switcher, a landing page or an account portal. Failing that, elect a canister that exists only to serve the well-known documents.\n\nThree properties make a good primary origin:\n\n- **It is the front door.** `ii-app-metadata` is read from the primary origin, so the elected canister names the *whole suite* on every app's sign-in screen. Elect one product canister and every sibling signs in under that product's name.\n- **It deploys first.** Nothing can derive against an origin that does not exist yet.\n- **It is never retired.** It holds the suite's identity for as long as the accounts do. Principals derive from the origin, so the canister keeps its code and state at your discretion (an upgrade or a reinstall changes nothing, as long as it goes on serving the well-known documents); what it can never do is hand the role to a different canister id, because that is a different origin.\n\nThe user sees their own app's origin, not the primary one: II shows the origin being signed in to and reads only the name, description and logo from the primary origin.\n\nElecting the origin is the whole of the decision; the rest of a shared sign-in is a build. The cookie record, the `\u002Freauth` route and the per-app `ii-auth-callbacks` document are in the `internet-identity-shared-sessions` skill, which assumes this section is already settled.\n\n### Per environment\n\nCanister ids differ per environment, so the primary origin does too. Bake it at build time from the environment's manifest rather than hardcoding one. Principals differ across environments as a result, which is expected.\n\n### Both directions\n\nTwo well-known documents describe the same relation from opposite ends, and they have different readers:\n\n| Document | Served at | Says | Read by |\n|---|---|---|---|\n| `ii-alternative-origins` | the primary origin | who may derive against me | Internet Identity, validating `derivationOrigin` |\n| `ii-derivation-origin` | every app origin | which origin I derive from | an agent, which has no constructor to read |\n\nII never reads the second: a browser passes `derivationOrigin` directly. It exists because an agent acting for the user has only a URL. Its body is the canonical `https:\u002F\u002Fhost` on one line, it is omitted when an app derives for its own visible origin, and **it must agree with what the client passes.** A missing or stale file hands an agent a different principal than the browser gives the same user, with no error.\n\nDo not read `ii-alternative-origins` to discover the derivation origin. It is the inverse relation and there is no reverse lookup; used backwards it yields a plausible wrong principal rather than a failure.\n\n### What the election also decides\n\nChoosing the primary origin settles more than principals:\n\n- the name, description and logo on every sign-in screen (`ii-app-metadata`);\n- which canisters may notify, and the 10-sender cap they share (`ii-notification-senders`);\n- the 100-entry budget in `ii-alternative-origins`, one per app origin.\n\n`ii-auth-callbacks` is the exception: every app serves its own.\n\n## App metadata on the sign-in screen\n\nBy default the sign-in screens identify an app by its **origin** alone. To also show a name, a short tagline and a logo, serve a JSON document at `\u002F.well-known\u002Fii-app-metadata`. Publishing it is permissionless: there is no list to join and no approval step. A valid document replaces the curated entry II still ships for a small set of known apps.\n\n```json\n{\n  \"name\": \"Example App\",\n  \"description\": \"A short tagline shown on the sign-in screen\",\n  \"logo\": \"\u002Flogo.png\"\n}\n```\n\n### Which origin serves it\n\nII fetches it from the origin identities are derived for: the validated `derivationOrigin` when the auth request sets one, and the request's own origin otherwise. Publish it once on the primary origin and every alternative origin that origin certifies shows the same name, description and logo. A copy served only on the alternative origin the user visits is never read.\n\nThis also names the app on the notifications it sends, not only on the sign-in screen, and `ii-notification-senders` is read from the same origin by the same rule.\n\n### Validation rules\n\nAll three fields are optional and unknown fields are ignored, so a document stays valid as II adds fields.\n\n- `name` is at most 40 characters and `description` at most 120, counted in **Unicode code points on the value as served**, before whitespace collapsing. Collapsing is display-only and never rescues an over-long value.\n- Each field must contain at least one visible character. A field holding only whitespace or invisible characters is rejected, not treated as absent.\n- Rejected characters: control characters (other than the ASCII whitespace `\\t`, `\\n`, `\\v`, `\\f`, `\\r`), `U+FEFF`, and the bidirectional embeddings and overrides `U+202A` to `U+202E`. Accepted: the marks `U+200E`, `U+200F`, `U+061C`, the isolates `U+2066` to `U+2069`, and the zero-width characters `U+200B` to `U+200D`. Isolates must be **balanced**: a field closes every isolate it opens and none it did not open.\n- **One bad field invalidates the whole document.** None of it is applied (not half of it); II falls back to its curated entry if it ships one for the app, and to the origin alone otherwise. II logs the offending field to the browser console on the sign-in screen. A document carrying no field II recognises is ignored too.\n- `logo` must be a **raster** image URL on the **same origin** as the document (relative URLs resolve against it), served as `image\u002Fpng`, `image\u002Fjpeg`, `image\u002Fwebp`, `image\u002Fgif`, or `image\u002Favif`, at most 1 MiB and 4096 pixels per axis. `image\u002Fsvg+xml` is rejected: serve a rasterised copy. II downloads the image (it is never hotlinked), redraws it at up to 512 pixels on its longest side (an animated image is flattened to its first frame), and renders that copy, so ship a roughly square PNG or WebP of about 512 pixels.\n- **Write the logo URL relative** (`\u002Flogo.png`). II normalizes a canister gateway origin onto `ic0.app` and tries its `icp0.io` and `icp.net` twins in turn, so the document may be fetched from a sibling gateway domain of the same canister, and the same-origin check runs against whichever answered. An absolute URL pinned to one of them is cross-origin at the other two.\n- Only the *shape* of `logo` (a non-empty URL on the document's own origin) is part of document validation. Once that passes, a logo that cannot be fetched or decoded, or that breaks the content-type, size, or dimension rules, costs the logo alone: the name and description still render.\n- The document must not exceed 8 KiB, must be answered with `200`, and must not redirect (II follows redirects for neither the document nor the logo). II requests it without credentials and gives up after 10 seconds.\n\n### CORS\n\nThe document and the logo are both read cross-origin, so both need CORS headers. With `@dfinity\u002Fstatic-site`, add them to the `_headers` file at the root of the build directory:\n\n```\n\u002F.well-known\u002Fii-app-metadata\n  Content-Type: application\u002Fjson\n  Access-Control-Allow-Origin: *\n\n\u002Flogo.png\n  Access-Control-Allow-Origin: *\n```\n\n`.well-known\u002F` is uploaded automatically, but the document has no file extension, so its media type must be set with the bare `Content-Type:` line.\n\nMissing, unreachable, or invalid metadata never blocks sign-in. Publishing it verifies nothing about the app: II keeps showing the origin next to whatever the document provides, because the origin is the part users can check.\n\n## Older `@icp-sdk\u002Fauth` majors\n\nThis skill targets `@icp-sdk\u002Fauth` 11.x. On an older major the same flow differs as follows.\n\n### 10.x\n\nNo notification consent: `requestNotificationConsent`, `getNotificationConsentStatus` and `revokeNotificationConsent` arrive in 11.1.0. Earlier 11.x majors do not have them either.\n\nOtherwise the same API, except the SSO domain check:\n\n- `isValidSsoDomain(domain, signal)` fetched `\u002F.well-known\u002Fii-openid-configuration` from the browser and returned a boolean; the organization had to serve it with `Access-Control-Allow-Origin: *`. 11.x removes it: a client built with `ssoDomain` checks the domain through Internet Identity and reports `getSsoStatus()`.\n- `new AuthClient({ ssoDomain })` threw on a malformed domain; 11.x reports `{ state: 'invalid', reason }` from `getSsoStatus()` instead and never throws for the domain. 11.x also reports `invalid` for a port on any host other than a loopback host (`localhost`, `127.0.0.1`, `*.localhost`), and for a malformed name such as `foo..com` or `ab--cd.com`.\n\n10.x peers `@icp-sdk\u002Fcore@^6`.\n\n### 9.x\n\nThe same API as 10.x. 10.x changed only its peer, from `@icp-sdk\u002Fcore@^5` to `^6`, so 9.x is what you use if you are held on core 5. With core 6 a read-only session signs in instead of throwing \"this session is read-only, which `@icp-sdk\u002Fauth` cannot act for yet\".\n\n### 8.x\n\nWhat 9.x changed:\n\n- `identityProvider` was a URL string; it is now `{ authorizeUrl, canisterId }`, and a string throws.\n- `storage` and its `IdbStorage` \u002F `LocalStorage` classes became `credentialStorage` with `IdbCredentialStorage` (the default), `LocalCredentialStorage`, `MemoryCredentialStorage`, and `SharedMemoryCredentialStorage`. `stateStorage` is new and holds the record of who is signed in, which is what makes tabs converge; `CookieStateStorage` extends that to sibling subdomains.\n- `IdleManager` and its options (`idleOptions`, `onIdle`, `idleTimeout`, `disableIdle`) were removed. The session is bounded at Internet Identity instead, via `maxTimeToIdle` and `maxTimeToLive` on `signIn()`.\n- `maxTimeToLive` bounded a delegation and capped every sign-in at 8 hours; it now bounds the session, and unset means the provider's own 30 days.\n- `getStatus()`, `subscribe()`, `getPrincipal()`, and `dispose()` are new; the `identity`, `keyType`, and `targets` options are gone.\n\n### 5.x\n\nA callback-based API:\n\n- `await AuthClient.create({...})` instead of `new AuthClient({...})`.\n- `identityProvider` passed per call to `login({...})` rather than at construction.\n- `authClient.login({ onSuccess, onError })`, which needs a promise wrapper.\n- `authClient.logout()` instead of `authClient.signOut()`.\n- `await authClient.isAuthenticated()` (async) instead of sync; `authClient.getIdentity()` (sync) instead of async.\n- 5.x appends `\u002Fauthorize` to the `identityProvider` URL, so passing `https:\u002F\u002Fid.ai` works there.\n- No `requestAttributes` \u002F `AttributesIdentity`: the identity-attributes flow requires 7.x or later.\n\nUpgrade when you can: the promise-based API is harder to misuse, and from 9.x the credentials a session's calls are signed with are short-lived and replaced for you, rather than one key being held for the session's whole length.\n\n## Backend: Access Control\n\nAnonymous-principal rejection, role guards, and caller binding in async functions are not II-specific: see the `canister-security` skill.\n\n## Where to go next\n\n- Load `internet-identity-shared-sessions` to carry one sign-in across sibling subdomains, once the primary origin above is elected.\n- Load `internet-identity-attributes` for a verified email or display name in the backend.\n- Load `internet-identity-notifications` to notify a signed-in user while the app is closed.\n- Load `internet-identity-sso` to set up an organization's own SSO, the side `ssoDomain` consumes.\n- Load `canister-security` for anonymous-principal rejection and role guards.\n","# Internet Identity Authentication\n\n## What This Is\n\nInternet Identity (II) is the Internet Computer's native authentication system. Users sign in to II-powered apps with passkeys, with an OpenID account (Google, Apple, Microsoft), or through their organization's own SSO; no usernames or passwords. Each user gets a unique principal per app origin, preventing cross-app tracking.\n\n## Prerequisites\n\n- `@icp-sdk\u002Fauth@^11` with `@icp-sdk\u002Fcore@^6`. **Pin both majors together.** auth 11 and 10 peer `@icp-sdk\u002Fcore@^6`; auth 9 peers `^5`.\n\n## Canister IDs\n\n| Canister | ID | URL | Purpose |\n|---|---|---|---|\n| Internet Identity (backend) | `rdmx6-jaaaa-aaaaa-aaadq-cai` |  | What sign-in runs against; signs attribute bundles |\n| Internet Identity (frontend) | `uqzsh-gqaaa-aaaaq-qaada-cai` | `https:\u002F\u002Fid.ai` | Serves the sign-in UI; `authorizeUrl` points here |\n\nBoth IDs are identical on mainnet and on a local network. Hardcode them.\n\n## Mistakes That Break Your Build\n\n1. **Wrong II URL for the environment.** `authorizeUrl` points at the **frontend** canister: `https:\u002F\u002Fid.ai\u002Fauthorize` on mainnet, `http:\u002F\u002Fid.ai.localhost:8000\u002Fauthorize` for a local II (`ii: true`). The URL is used verbatim, so include `\u002Fauthorize`: `https:\u002F\u002Fid.ai` opens the II home page and never returns a delegation.\n2. **Passing `identityProvider` as a string, or half of it.** It is an object, `{ authorizeUrl, canisterId }`, both required together; a string or `URL` throws a `TypeError`. Omit the option entirely to get mainnet II, which is what most apps want.\n3. **Top-level `await` in frontend code.** Vite 6 and earlier target `es2020`-era browsers by default, which have no top-level await, so the build fails; Vite 7 allows it. Wrap the setup in an `async function init()` (or a framework lifecycle hook). Do NOT change `build.target` to `esnext` to make it compile.\n4. **Treating `maxTimeToLive` as the lifetime of the signing key.** It bounds the **session** at II, and `maxTimeToIdle` ends a session nobody uses; the delegation your calls are signed with is short-lived and replaced by the client. Leave both unset unless the app has its own policy: II applies seven days idle and thirty days in total.\n5. **Not awaiting `signIn()` or skipping the `try`\u002F`catch`.** `signIn()` rejects when the user closes the popup or authentication fails.\n6. **`shouldFetchRootKey` or `fetchRootKey()` in browser code.** The `ic_env` cookie (set by the frontend canister or the Vite dev server) carries the root key as `IC_ROOT_KEY`: pass it as `rootKey` to `HttpAgent.create()` and leave `host` unset. Fetching the root key at runtime lets a man-in-the-middle substitute a fake one on mainnet.\n7. **Getting `2vxsx-fae` as the principal.** That is the anonymous principal: authentication silently failed. Usual causes: a wrong `authorizeUrl` (especially missing `\u002Fauthorize`), an unhandled `signIn()` rejection, or reading the identity before `signIn()` resolved. `getIdentity()` throws `SessionNotHeldError` (not an anonymous identity) when a sign-in exists that this origin holds no credential for.\n8. **Passing the principal as an argument.** The backend reads the caller from the IC protocol: `msg.caller` in Motoko, `ic_cdk::api::msg_caller()` in Rust. An argument is whatever the caller typed and proves nothing. Role guards and anonymous-principal rejection are in the `canister-security` skill.\n9. **Letting the first origin with users be one you can lose.** Decided in \"Your app's identity is its primary origin\" below, before writing any sign-in code. The origin an account is created under is permanent: repointing it hands every returning user a new, empty account, so whether this app will have siblings has to be settled *before* it has users. What is *not* a mistake: setting `derivationOrigin` to the origin you are already served at is a validated no-op, not an error. II compares the two in canonical form and skips validation entirely, which is also why `ic0.app`, `icp0.io` and `icp.net` are interchangeable and a single-origin app needs no configuration at all.\n10. **Sharing a cookie domain without a shared `derivationOrigin`.** Elect the primary origin first (\"Your app's identity is its primary origin\" below), then build the rest from the `internet-identity-shared-sessions` skill. Subdomains share a sign-in only when they share a principal, and a cookie does not produce one: without one primary origin authorized for all of them, the shared record names an account the reading origin can never hold, and `\u002Freauth` bounces the user forever.\n11. **A silent re-issue without `hint`, on the default transport, or to an undeclared callback.** All three are the `\u002Freauth` route of a shared sign-in, which the `internet-identity-shared-sessions` skill builds end to end. `prompt: 'none'` without `hint` fails with `InteractionRequiredError` (`reason` `account_selection_required`) when II holds more than one session. It runs on page load with no user gesture, so it needs `transport: 'redirect'` on a route of its own, declared in the origin's `\u002F.well-known\u002Fii-auth-callbacks`, or the redirect never comes back.\n12. **Serving `\u002F.well-known\u002Fii-app-metadata` on the wrong origin, without CORS, or with one bad field.** Every rule is in \"App metadata on the sign-in screen\" below, including which characters and sizes are rejected. In short: II reads the document from the primary origin (when set) or the request origin, the document and the logo both need CORS, and one invalid field invalidates the whole thing rather than just that field.\n13. **Installing majors that do not pair.** auth 11 peers `@icp-sdk\u002Fcore@^6`; pinning core to `^5` out of habit breaks the build.\n\n## Using II during local development\n\n**Default: use mainnet II from the local network.** With `icp-cli >= 0.2.4` the local network trusts the mainnet subnet's signatures, so delegations from `https:\u002F\u002Fid.ai` work against a locally deployed backend. Construct the client with no `identityProvider`.\n\n**Fallback: a local II,** only for fully offline development or a specific II build. Add `ii: true` to the local network in `icp.yaml`:\n\n```yaml\nnetworks:\n  - name: local\n    mode: managed\n    ii: true\n```\n\nII is then served at `http:\u002F\u002Fid.ai.localhost:8000`, with the same canister IDs. No canister entry is needed in your project. Pass the local root key:\n\n```javascript\nconst authClient = new AuthClient({\n  identityProvider: {\n    authorizeUrl: \"http:\u002F\u002Fid.ai.localhost:8000\u002Fauthorize\",\n    canisterId: \"rdmx6-jaaaa-aaaaa-aaadq-cai\",\n  },\n  agentOptions: { rootKey: canisterEnv?.IC_ROOT_KEY },\n});\n```\n\n## Frontend: Sign-In Flow\n\nFramework-agnostic; adapt the DOM parts.\n\n```javascript\nimport { AuthClient } from \"@icp-sdk\u002Fauth\u002Fclient\";\nimport { HttpAgent, Actor } from \"@icp-sdk\u002Fcore\u002Fagent\";\nimport { safeGetCanisterEnv } from \"@icp-sdk\u002Fcore\u002Fagent\u002Fcanister-env\";\n\n\u002F\u002F The ic_env cookie carries the root key and canister IDs, locally and on mainnet.\nconst canisterEnv = safeGetCanisterEnv();\n\n\u002F\u002F Mainnet II is the default. derivationOrigin, openIdProvider, ssoDomain,\n\u002F\u002F stateStorage, transport, prompt, and hint are also constructor options.\nconst authClient = new AuthClient();\n\nasync function signIn() {\n  try {\n    \u002F\u002F Optional session bounds: signIn({ maxTimeToIdle, maxTimeToLive }), in ns.\n    return await authClient.signIn();\n  } catch (error) {\n    console.error(\"Sign-in failed:\", error);\n    throw error;\n  }\n}\n\n\u002F\u002F Ends the session at II: every tab of this origin is signed out.\nasync function signOut() {\n  await authClient.signOut();\n}\n\n\u002F\u002F No host: the default resolves correctly locally, on mainnet, and on custom domains.\nasync function createAuthenticatedActor(identity, canisterId, idlFactory) {\n  const agent = await HttpAgent.create({ identity, rootKey: canisterEnv?.IC_ROOT_KEY });\n  return Actor.createActor(idlFactory, { agent, canisterId });\n}\n\n\u002F\u002F Wrapped in a function: Vite 6 and earlier reject top-level await by default.\nasync function init() {\n  \u002F\u002F isAuthenticated() is sync; getIdentity() is async.\n  if (authClient.isAuthenticated()) {\n    const identity = await authClient.getIdentity();\n    const actor = await createAuthenticatedActor(identity, canisterId, idlFactory);\n  }\n\n  \u002F\u002F getStatus(): 'signed-in' | 'signed-in-elsewhere' | 'expired' | 'signed-out'.\n  \u002F\u002F subscribe() fires on any change, including in another tab.\n  authClient.subscribe(() => render(authClient.getStatus()));\n}\n\ninit();\n```\n\n**Lifecycle.** One client for the page, or one per component: both read and write the same sign-in. A client a view owns is disposed with it: `dispose()` releases its listeners, subscription, and scheduled refresh, and is **not** a sign-out.\n\n## One-click sign-in\n\n`openIdProvider` (`'google'`, `'apple'`, `'microsoft'`) or `ssoDomain` (an organization's domain) skips II's method screen. They are mutually exclusive (a type error, and a throw at construction) and constructor-only.\n\n```javascript\nconst google = new AuthClient({ openIdProvider: \"google\" });\nconst acme = new AuthClient({ ssoDomain: \"acme.com\" });\n```\n\n### Checking an organization's domain\n\nA client built with `ssoDomain` checks its own domain through II as soon as it is constructed and reports the result like the session: a synchronous snapshot from `getSsoStatus()`, change notifications from `subscribe()`. No exceptions, no `AbortSignal`.\n\n```typescript\ntype SsoStatus =\n  | { state: \"checking\" }\n  | { state: \"available\"; name?: string }      \u002F\u002F name from the org's file\n  | { state: \"invalid\"; reason: string }       \u002F\u002F not a domain: the user fixes the input\n  | { state: \"unavailable\"; retryAfter?: Date; reason?: string };\n```\n\nChecking while the user types, one client per pause rather than per keystroke. A superseded client is disposed before it can publish, so stale results never render:\n\n```javascript\nlet client;\nlet timer;\n\ninput.addEventListener(\"input\", () => {\n  client?.dispose();  \u002F\u002F also stops the old client's check\n  client = undefined; \u002F\u002F Continue never signs in to the old domain\n  showSpinner();\n\n  clearTimeout(timer); \u002F\u002F debounce\n  timer = setTimeout(() => {\n    const next = new AuthClient({ ssoDomain: input.value }); \u002F\u002F never throws for the domain\n    next.subscribe(() => render(next.getSsoStatus()));\n    render(next.getSsoStatus());\n    client = next;\n  }, 300);\n});\n\n\u002F\u002F Synchronous in the click handler: never await the check first.\ncontinueButton.addEventListener(\"click\", () => {\n  client?.signIn().then(showApp, showSignInError);\n});\n```\n\n- `refreshSsoStatus()` re-runs the check for a \"Try again\" button. While `retryAfter` is in the future, II answers `unavailable` again at once with no new fetch, so disable the button and show a countdown. It does nothing on an `invalid` client, and a client never re-checks by itself.\n- `signIn()` proceeds in every state: II resolves the domain itself and shows its own loading and error screens, for an `invalid` value too.\n- `invalid` follows II's own rules: at least two labels of 1 to 63 letters, digits, or hyphens, no hyphen at either end, no `--` in a label's third and fourth places (punycode `xn--` aside), at most 253 characters, and a port only on a loopback host. Its `reason` names every problem found.\n- The check warms II's cache, so `signIn()` on the same client starts fast.\n\nThe organization's own side of this, which is the OIDC client, the `ii-openid-configuration` file, per-app access, the limits that reject the file and the caching windows, is the `internet-identity-sso` skill.\n\n## Your app's identity is its primary origin\n\nInternet Identity derives a principal per origin. Nothing in the sign-in flow above sets one, and that is correct: with no `derivationOrigin`, principals derive from the origin you are served at, which for a new app is its canister address. You already have an app identity, and it is already anchored on something nobody can take away.\n\nYou reach for `derivationOrigin` only when that stops being enough, which happens in exactly two situations: the app gains a second origin, usually a custom domain, or it gains a sibling app whose users have to be the same people. Both are below.\n\n### Decide before your first sign-in\n\nThe origin an account is created under cannot be changed afterwards: repointing it orphans every account made there. So the question to settle before shipping is not \"do I have a custom domain yet\" but **is this one app, or the first of several?**\n\n- **One app.** Nothing to do. The canister address is the identity, now and later. Adding a domain later is handled below and costs nothing.\n- **The first of several.** Create the canister that will serve the primary origin before the first app ships, and point that app at it from its first deploy.\n\nYou do not need to know what the suite will contain. The `ii-alternative-origins` list grows as each app ships; what cannot change is which origin the list lives on.\n\n### Adding a domain\n\nA custom domain is a second origin, so left alone it is a second set of users. Keep the identity on the canister address and let the domain derive against it.\n\n1. The domain passes `derivationOrigin`; the canister origin does not:\n\n```javascript\nconst authClient = new AuthClient({ derivationOrigin: \"https:\u002F\u002F\u003Ccanister-id>.icp.net\" });\n```\n\n2. The canister origin serves `\u002F.well-known\u002Fii-alternative-origins`:\n\n```json\n{ \"alternativeOrigins\": [\"https:\u002F\u002Fshop.example.com\"] }\n```\n\nAt most **100** entries, origins only (no trailing slash, no path). Over the cap II rejects the **entire list**, so every alternative origin stops authenticating, not just the extras. II canonicalizes `ic0.app`, `icp0.io` and `icp.net` to one form, so one entry per origin covers all three gateway spellings.\n\n3. With `@dfinity\u002Fstatic-site`, add a `_headers` block (the file has no extension, and certified assets set no CORS header by default):\n\n```\n\u002F.well-known\u002Fii-alternative-origins\n  Content-Type: application\u002Fjson\n  Access-Control-Allow-Origin: *\n```\n\nPointing the derivation origin at the domain instead works, since II validates it like any other origin, but is not recommended. A domain can lapse, be sold or be rebranded, and every account derived from it goes with it. A canister address cannot be taken away.\n\n### A suite that shares one sign-in\n\nApps that share a sign-in must share a principal, so they must all derive from one origin, and someone has to elect it. Elect the origin that represents the whole product rather than one app inside it, such as an app switcher, a landing page or an account portal. Failing that, elect a canister that exists only to serve the well-known documents.\n\nThree properties make a good primary origin:\n\n- **It is the front door.** `ii-app-metadata` is read from the primary origin, so the elected canister names the *whole suite* on every app's sign-in screen. Elect one product canister and every sibling signs in under that product's name.\n- **It deploys first.** Nothing can derive against an origin that does not exist yet.\n- **It is never retired.** It holds the suite's identity for as long as the accounts do. Principals derive from the origin, so the canister keeps its code and state at your discretion (an upgrade or a reinstall changes nothing, as long as it goes on serving the well-known documents); what it can never do is hand the role to a different canister id, because that is a different origin.\n\nThe user sees their own app's origin, not the primary one: II shows the origin being signed in to and reads only the name, description and logo from the primary origin.\n\nElecting the origin is the whole of the decision; the rest of a shared sign-in is a build. The cookie record, the `\u002Freauth` route and the per-app `ii-auth-callbacks` document are in the `internet-identity-shared-sessions` skill, which assumes this section is already settled.\n\n### Per environment\n\nCanister ids differ per environment, so the primary origin does too. Bake it at build time from the environment's manifest rather than hardcoding one. Principals differ across environments as a result, which is expected.\n\n### Both directions\n\nTwo well-known documents describe the same relation from opposite ends, and they have different readers:\n\n| Document | Served at | Says | Read by |\n|---|---|---|---|\n| `ii-alternative-origins` | the primary origin | who may derive against me | Internet Identity, validating `derivationOrigin` |\n| `ii-derivation-origin` | every app origin | which origin I derive from | an agent, which has no constructor to read |\n\nII never reads the second: a browser passes `derivationOrigin` directly. It exists because an agent acting for the user has only a URL. Its body is the canonical `https:\u002F\u002Fhost` on one line, it is omitted when an app derives for its own visible origin, and **it must agree with what the client passes.** A missing or stale file hands an agent a different principal than the browser gives the same user, with no error.\n\nDo not read `ii-alternative-origins` to discover the derivation origin. It is the inverse relation and there is no reverse lookup; used backwards it yields a plausible wrong principal rather than a failure.\n\n### What the election also decides\n\nChoosing the primary origin settles more than principals:\n\n- the name, description and logo on every sign-in screen (`ii-app-metadata`);\n- which canisters may notify, and the 10-sender cap they share (`ii-notification-senders`);\n- the 100-entry budget in `ii-alternative-origins`, one per app origin.\n\n`ii-auth-callbacks` is the exception: every app serves its own.\n\n## App metadata on the sign-in screen\n\nBy default the sign-in screens identify an app by its **origin** alone. To also show a name, a short tagline and a logo, serve a JSON document at `\u002F.well-known\u002Fii-app-metadata`. Publishing it is permissionless: there is no list to join and no approval step. A valid document replaces the curated entry II still ships for a small set of known apps.\n\n```json\n{\n  \"name\": \"Example App\",\n  \"description\": \"A short tagline shown on the sign-in screen\",\n  \"logo\": \"\u002Flogo.png\"\n}\n```\n\n### Which origin serves it\n\nII fetches it from the origin identities are derived for: the validated `derivationOrigin` when the auth request sets one, and the request's own origin otherwise. Publish it once on the primary origin and every alternative origin that origin certifies shows the same name, description and logo. A copy served only on the alternative origin the user visits is never read.\n\nThis also names the app on the notifications it sends, not only on the sign-in screen, and `ii-notification-senders` is read from the same origin by the same rule.\n\n### Validation rules\n\nAll three fields are optional and unknown fields are ignored, so a document stays valid as II adds fields.\n\n- `name` is at most 40 characters and `description` at most 120, counted in **Unicode code points on the value as served**, before whitespace collapsing. Collapsing is display-only and never rescues an over-long value.\n- Each field must contain at least one visible character. A field holding only whitespace or invisible characters is rejected, not treated as absent.\n- Rejected characters: control characters (other than the ASCII whitespace `\\t`, `\\n`, `\\v`, `\\f`, `\\r`), `U+FEFF`, and the bidirectional embeddings and overrides `U+202A` to `U+202E`. Accepted: the marks `U+200E`, `U+200F`, `U+061C`, the isolates `U+2066` to `U+2069`, and the zero-width characters `U+200B` to `U+200D`. Isolates must be **balanced**: a field closes every isolate it opens and none it did not open.\n- **One bad field invalidates the whole document.** None of it is applied (not half of it); II falls back to its curated entry if it ships one for the app, and to the origin alone otherwise. II logs the offending field to the browser console on the sign-in screen. A document carrying no field II recognises is ignored too.\n- `logo` must be a **raster** image URL on the **same origin** as the document (relative URLs resolve against it), served as `image\u002Fpng`, `image\u002Fjpeg`, `image\u002Fwebp`, `image\u002Fgif`, or `image\u002Favif`, at most 1 MiB and 4096 pixels per axis. `image\u002Fsvg+xml` is rejected: serve a rasterised copy. II downloads the image (it is never hotlinked), redraws it at up to 512 pixels on its longest side (an animated image is flattened to its first frame), and renders that copy, so ship a roughly square PNG or WebP of about 512 pixels.\n- **Write the logo URL relative** (`\u002Flogo.png`). II normalizes a canister gateway origin onto `ic0.app` and tries its `icp0.io` and `icp.net` twins in turn, so the document may be fetched from a sibling gateway domain of the same canister, and the same-origin check runs against whichever answered. An absolute URL pinned to one of them is cross-origin at the other two.\n- Only the *shape* of `logo` (a non-empty URL on the document's own origin) is part of document validation. Once that passes, a logo that cannot be fetched or decoded, or that breaks the content-type, size, or dimension rules, costs the logo alone: the name and description still render.\n- The document must not exceed 8 KiB, must be answered with `200`, and must not redirect (II follows redirects for neither the document nor the logo). II requests it without credentials and gives up after 10 seconds.\n\n### CORS\n\nThe document and the logo are both read cross-origin, so both need CORS headers. With `@dfinity\u002Fstatic-site`, add them to the `_headers` file at the root of the build directory:\n\n```\n\u002F.well-known\u002Fii-app-metadata\n  Content-Type: application\u002Fjson\n  Access-Control-Allow-Origin: *\n\n\u002Flogo.png\n  Access-Control-Allow-Origin: *\n```\n\n`.well-known\u002F` is uploaded automatically, but the document has no file extension, so its media type must be set with the bare `Content-Type:` line.\n\nMissing, unreachable, or invalid metadata never blocks sign-in. Publishing it verifies nothing about the app: II keeps showing the origin next to whatever the document provides, because the origin is the part users can check.\n\n## Older `@icp-sdk\u002Fauth` majors\n\nThis skill targets `@icp-sdk\u002Fauth` 11.x. On an older major the same flow differs as follows.\n\n### 10.x\n\nNo notification consent: `requestNotificationConsent`, `getNotificationConsentStatus` and `revokeNotificationConsent` arrive in 11.1.0. Earlier 11.x majors do not have them either.\n\nOtherwise the same API, except the SSO domain check:\n\n- `isValidSsoDomain(domain, signal)` fetched `\u002F.well-known\u002Fii-openid-configuration` from the browser and returned a boolean; the organization had to serve it with `Access-Control-Allow-Origin: *`. 11.x removes it: a client built with `ssoDomain` checks the domain through Internet Identity and reports `getSsoStatus()`.\n- `new AuthClient({ ssoDomain })` threw on a malformed domain; 11.x reports `{ state: 'invalid', reason }` from `getSsoStatus()` instead and never throws for the domain. 11.x also reports `invalid` for a port on any host other than a loopback host (`localhost`, `127.0.0.1`, `*.localhost`), and for a malformed name such as `foo..com` or `ab--cd.com`.\n\n10.x peers `@icp-sdk\u002Fcore@^6`.\n\n### 9.x\n\nThe same API as 10.x. 10.x changed only its peer, from `@icp-sdk\u002Fcore@^5` to `^6`, so 9.x is what you use if you are held on core 5. With core 6 a read-only session signs in instead of throwing \"this session is read-only, which `@icp-sdk\u002Fauth` cannot act for yet\".\n\n### 8.x\n\nWhat 9.x changed:\n\n- `identityProvider` was a URL string; it is now `{ authorizeUrl, canisterId }`, and a string throws.\n- `storage` and its `IdbStorage` \u002F `LocalStorage` classes became `credentialStorage` with `IdbCredentialStorage` (the default), `LocalCredentialStorage`, `MemoryCredentialStorage`, and `SharedMemoryCredentialStorage`. `stateStorage` is new and holds the record of who is signed in, which is what makes tabs converge; `CookieStateStorage` extends that to sibling subdomains.\n- `IdleManager` and its options (`idleOptions`, `onIdle`, `idleTimeout`, `disableIdle`) were removed. The session is bounded at Internet Identity instead, via `maxTimeToIdle` and `maxTimeToLive` on `signIn()`.\n- `maxTimeToLive` bounded a delegation and capped every sign-in at 8 hours; it now bounds the session, and unset means the provider's own 30 days.\n- `getStatus()`, `subscribe()`, `getPrincipal()`, and `dispose()` are new; the `identity`, `keyType`, and `targets` options are gone.\n\n### 5.x\n\nA callback-based API:\n\n- `await AuthClient.create({...})` instead of `new AuthClient({...})`.\n- `identityProvider` passed per call to `login({...})` rather than at construction.\n- `authClient.login({ onSuccess, onError })`, which needs a promise wrapper.\n- `authClient.logout()` instead of `authClient.signOut()`.\n- `await authClient.isAuthenticated()` (async) instead of sync; `authClient.getIdentity()` (sync) instead of async.\n- 5.x appends `\u002Fauthorize` to the `identityProvider` URL, so passing `https:\u002F\u002Fid.ai` works there.\n- No `requestAttributes` \u002F `AttributesIdentity`: the identity-attributes flow requires 7.x or later.\n\nUpgrade when you can: the promise-based API is harder to misuse, and from 9.x the credentials a session's calls are signed with are short-lived and replaced for you, rather than one key being held for the session's whole length.\n\n## Backend: Access Control\n\nAnonymous-principal rejection, role guards, and caller binding in async functions are not II-specific: see the `canister-security` skill.\n\n## Where to go next\n\n- Load `internet-identity-shared-sessions` to carry one sign-in across sibling subdomains, once the primary origin above is elected.\n- Load `internet-identity-attributes` for a verified email or display name in the backend.\n- Load `internet-identity-notifications` to notify a signed-in user while the app is closed.\n- Load `internet-identity-sso` to set up an organization's own SSO, the side `ssoDomain` consumes.\n- Load `canister-security` for anonymous-principal rejection and role guards.\n",{"name":5,"license":15,"metadata":53,"description":7,"compatibility":56},{"title":54,"category":55},"Internet Identity","Auth","icp-cli >= 0.2.4, Node.js >= 22","main","a0e3932db53d006f5d56699646599cc846a13d6e","skills\u002Fchains\u002Fdfinity-official-internet-identity",17,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia","https:\u002F\u002Feth2030.com","Hardcore infra. Building @alt-research and applied cryptography.","2026-11-11 08:47:40",{"source":8,"skillName":5,"commands":67,"prompt":77,"zipAvailable":78},[68,71,74],{"tool":69,"command":70},"npx","npx skills add jiayaoqijia\u002Fcryptoskill --skill internet-identity",{"tool":72,"command":73},"bunx","bunx skills add jiayaoqijia\u002Fcryptoskill --skill internet-identity",{"tool":75,"command":76},"pnpm","pnpm dlx skills add jiayaoqijia\u002Fcryptoskill --skill internet-identity","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fdfinity-official-internet-identity\u002FSKILL.md",true,[80,84,90],{"path":81,"resourceType":81,"resourceTypeDesc":82,"sizeBytes":83,"sourceHtmlUrl":16},"LICENSE","许可证",10770,{"path":85,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":88,"sourceHtmlUrl":89},"SOURCE.md","REFERENCE","参考文档",335,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fdfinity-official-internet-identity\u002FSOURCE.md",{"path":91,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":92,"sourceHtmlUrl":93},"TRUST.auto.yaml",2191,"https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fchains\u002Fdfinity-official-internet-identity\u002FTRUST.auto.yaml",[95,123,145,162,175,185],{"skillNo":96,"name":97,"nameZh":6,"description":98,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":99,"licenseFileUrl":100,"sourceHtmlUrl":101,"categories":102,"keywords":103,"publishedAt":48},"01M712C436EQYE43342Q4E7GG1","recipe-performance","Diagnose a specific MetaMask user flow's real-app performance and Sentry critical-user-flow telemetry with deterministic mm-harness recipes. Use for CUF boundaries, cold\u002Fwarm\u002Fbackground-resume behavior, latency regressions, lifecycle tags, duplicate or blocking network requests, long tasks, layout\u002Fpaint cost, screen redraws, React rerenders, or browser trace analysis where tests alone cannot prove the real app.","MIT","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fwallets\u002Fmetamask-official-recipe-performance\u002FSKILL.md",[],[104,105,106,107,108,109,47,110,111,112,113,114,115,116,117,118,119,120,121,122],"performance","recipe performance","recipe","mm","sentry","harness","deterministic","recipes","flow","cold","critical","analysis","tasks","react","browser","layout","screen","network","metamask",{"skillNo":124,"name":125,"nameZh":6,"description":126,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":127,"sourceHtmlUrl":128,"categories":129,"keywords":130,"publishedAt":48},"01M70XT2F8A5CQZ81JBP98KSW3","mexc-spot-websocket","MEXC Spot WebSocket workflows for public streams, protobuf handling, depth maintenance, listen keys, and private streams without exposing API keys.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-websocket\u002FSKILL.md",[],[131,132,133,134,135,136,137,138,139,140,141,142,143,144],"mexc spot websocket","spot","websocket","api","workflows","public","listen","streams","depth","keys","for","api keys","maintenance","protobuf",{"skillNo":146,"name":147,"nameZh":6,"description":148,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":149,"sourceHtmlUrl":150,"categories":151,"keywords":152,"publishedAt":48},"01M70XT0MBXN3WZY1BKN0CPNKR","mexc-spot-rest","MEXC Spot REST workflows for public data, signed account requests, orders, wallet\u002Fadmin actions, and secure signing without exposing API keys.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-spot-rest\u002FSKILL.md",[],[153,132,154,155,156,45,157,142,141,140,158,159,160,136,161,135,134],"mexc spot rest","REST","admin","signing","requests","actions","secure","data","orders",{"skillNo":163,"name":164,"nameZh":6,"description":165,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":166,"sourceHtmlUrl":167,"categories":168,"keywords":169,"publishedAt":48},"01M70XSYSVZ2N0N91XCAR2CAK0","mexc-futures-websocket","MEXC Futures WebSocket workflows for public streams, private account\u002Forder\u002Fposition streams, ping\u002Fpong, subscriptions, and depth maintenance.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fexchanges\u002Fmncrftfrcnm-mexc-futures-websocket\u002FSKILL.md",[],[170,171,133,172,173,143,141,174,139,138,136,135],"mexc futures websocket","futures","position","ping","subscriptions",{"skillNo":176,"name":177,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":99,"licenseFileUrl":178,"sourceHtmlUrl":179,"categories":180,"keywords":181,"publishedAt":48},"01M6H0ZWHVE5W04WG2DKQE2SQC","0xklen-version-and-refresh-sanctions-lists","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-version-and-refresh-sanctions-lists\u002FSKILL.md",[],[182,183,184],"refresh","0xklen version and refresh sanctions lists","version",{"skillNo":186,"name":187,"nameZh":6,"description":188,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":99,"licenseFileUrl":189,"sourceHtmlUrl":190,"categories":191,"keywords":192,"publishedAt":48},"01M6H0Z28Q3R8J0S799GJQWXM3","verify-tests-cover-changed-lines","Use when a PR adds code that the existing suite may not exercise. Measures patch coverage — the share of newly changed lines hit by tests — before approving.","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fjiayaoqijia\u002Fcryptoskill\u002Fblob\u002Fmain\u002Fskills\u002Fpayments\u002F0xklen-verify-tests-cover-changed-lines\u002FSKILL.md",[],[193,194,195,196,197,198,199,200,201,202,203],"tests","cover","verify tests cover changed lines","verify","coverage","pr","exercise","use","patch","code","share"]