diff --git a/evaluations/custom-domains.json b/evaluations/custom-domains.json new file mode 100644 index 0000000..f01d086 --- /dev/null +++ b/evaluations/custom-domains.json @@ -0,0 +1,103 @@ +{ + "skill": "custom-domains", + "description": "Evaluation cases for the custom-domains skill. Tests whether agents produce correct DNS records, use the right registration API endpoints, and warn about common pitfalls like provider SSL interference and stale ACME records.", + + "output_evals": [ + { + "name": "DNS record configuration", + "prompt": "What DNS records do I need to set up to serve my canister ryjl3-tyaaa-aaaaa-aaaba-cai under app.example.com on the IC? Just the DNS records, no code or deploy steps.", + "expected_behaviors": [ + "Shows a CNAME record for app.example.com pointing to app.example.com.icp1.io", + "Shows a TXT record for _canister-id.app.example.com with the canister ID ryjl3-tyaaa-aaaaa-aaaba-cai", + "Shows a CNAME record for _acme-challenge.app.example.com pointing to _acme-challenge.app.example.com.icp2.io", + "Does NOT hallucinate incorrect subdomains or record types" + ] + }, + { + "name": "Domain registration API", + "prompt": "I've set up my DNS records and deployed my canister with the ic-domains file. What curl commands do I run to register my custom domain app.example.com? Just the commands.", + "expected_behaviors": [ + "Uses the validate endpoint: GET https://icp0.io/custom-domains/v1/app.example.com/validate", + "Uses the registration endpoint: POST https://icp0.io/custom-domains/v1/app.example.com", + "Uses the status check endpoint: GET https://icp0.io/custom-domains/v1/app.example.com", + "Does NOT invent non-existent API endpoints or parameters" + ] + }, + { + "name": "ic-domains file setup", + "prompt": "I have a Vite-based IC frontend project created with dfx new. Where exactly do I put the ic-domains file and what should it contain for mydomain.com? Just the file placement and content.", + "expected_behaviors": [ + "Places .well-known/ic-domains inside the public/ directory, not src/", + "Lists the domain in the ic-domains file (mydomain.com)", + "Mentions .ic-assets.json5 with match .well-known and ignore false", + "Does NOT place the file in src/ where Vite would try to compile it" + ] + }, + { + "name": "Cloudflare SSL pitfall", + "prompt": "I'm using Cloudflare as my DNS provider for my IC custom domain. The registration keeps failing. What's likely wrong? Keep it brief.", + "expected_behaviors": [ + "Identifies Cloudflare's Universal SSL as the likely culprit", + "Advises disabling Cloudflare's SSL/TLS certificate features", + "Explains that provider SSL interferes with the IC's ACME challenge" + ] + }, + { + "name": "Apex domain limitation", + "prompt": "I want to use example.com (no subdomain) as my custom domain on the IC, but my DNS provider won't let me set a CNAME on the apex. What are my options? Keep it brief.", + "expected_behaviors": [ + "Explains that many providers don't allow CNAME on apex domains", + "Suggests using ANAME or ALIAS record types (CNAME flattening)", + "Suggests using a subdomain like www.example.com as an alternative" + ] + }, + { + "name": "HttpAgent host configuration", + "prompt": "My IC frontend works fine on icp0.io but API calls fail when I access it through my custom domain. What's wrong? Just the fix.", + "expected_behaviors": [ + "Identifies that HttpAgent cannot auto-detect the IC API host on custom domains", + "Shows setting host to https://icp-api.io for mainnet", + "Shows HttpAgent.create({ host }) or equivalent configuration" + ] + }, + { + "name": "Domain update flow", + "prompt": "I want to point my existing custom domain to a different canister. What do I need to do? Just the steps.", + "expected_behaviors": [ + "Updates the _canister-id TXT record to the new canister ID", + "Uses PATCH https://icp0.io/custom-domains/v1/DOMAIN to notify the service", + "Does NOT say to delete and re-register the domain" + ] + }, + { + "name": "Domain removal flow", + "prompt": "How do I remove a custom domain registration from the IC? Just the steps.", + "expected_behaviors": [ + "Removes the _canister-id TXT and _acme-challenge CNAME DNS records", + "Uses DELETE https://icp0.io/custom-domains/v1/DOMAIN to notify the service", + "Does NOT suggest only removing DNS records without calling the API" + ] + } + ], + + "trigger_evals": { + "description": "Queries to test whether the skill activates correctly.", + "should_trigger": [ + "How do I set up a custom domain for my IC canister?", + "My custom domain registration is failing", + "What DNS records do I need for an IC custom domain?", + "How do I update my custom domain to point to a different canister?", + "How do I remove a custom domain from the IC?", + "The ACME challenge for my IC domain keeps failing", + "How do I validate my custom domain configuration?" + ], + "should_not_trigger": [ + "How do I deploy a frontend on the IC?", + "Set up Internet Identity for my app", + "How do I make inter-canister calls?", + "Configure .ic-assets.json5 for SPA routing", + "How do I transfer ICP tokens?", + "Deploy a Rust backend canister" + ] + } +} diff --git a/skills/custom-domains/SKILL.md b/skills/custom-domains/SKILL.md new file mode 100644 index 0000000..c103f03 --- /dev/null +++ b/skills/custom-domains/SKILL.md @@ -0,0 +1,267 @@ +--- +name: custom-domains +description: "Register and manage custom domains for IC canisters via the HTTP gateway custom domain service. Covers DNS record configuration (CNAME, TXT, ACME challenge), the .well-known/ic-domains file, domain registration/validation/update/deletion via the REST API, TLS certificate provisioning, and HttpAgent host configuration. Use when the user wants to serve a canister under a custom domain, configure DNS for IC, register a domain with boundary nodes, troubleshoot custom domain issues, or update/remove a custom domain. Do NOT use for general frontend hosting or asset canister configuration without custom domains — use asset-canister instead." +license: Apache-2.0 +compatibility: "curl, DNS registrar access, deployed canister" +metadata: + title: "Custom Domains" + category: Frontend +--- + +# Custom Domains + +## What This Is + +By default, canisters are accessible at `.icp0.io`. The custom domains service lets you serve any canister under your own domain (e.g., `yourdomain.com`). You configure DNS, deploy a domain ownership file to your canister, and register via a REST API. The HTTP gateways then handle TLS certificate provisioning, renewal, and routing automatically. + +## Prerequisites + +- A registered domain from any registrar (e.g., Namecheap, GoDaddy, Cloudflare) +- Access to edit DNS records for that domain +- A deployed canister (typically an asset canister serving a frontend) +- `curl` for the registration API calls +- `jq` (optional, for formatting JSON responses) + +## Mistakes That Break Your Setup + +1. **Not disabling your DNS provider's SSL/TLS.** Providers like Cloudflare enable Universal SSL by default. This interferes with the ACME challenge the IC uses to provision certificates and can prevent certificate renewal. Disable any certificate/SSL/TLS offering from your DNS provider before registering. + +2. **Setting a CNAME on the apex domain.** Many DNS providers don't allow CNAME records on the apex (e.g., `example.com` with no subdomain). Use ANAME or ALIAS record types (CNAME flattening) if your provider supports them. Otherwise, use a subdomain like `www.example.com`. + +3. **Missing the `_acme-challenge` CNAME.** Without `_acme-challenge.CUSTOM_DOMAIN` pointing to `_acme-challenge.CUSTOM_DOMAIN.icp2.io`, the HTTP gateways cannot obtain a TLS certificate. Registration will fail. + +4. **Multiple TXT records on `_canister-id`.** If more than one TXT record exists for `_canister-id.CUSTOM_DOMAIN`, registration fails. Keep exactly one containing your canister ID. + +5. **Forgetting the `.well-known/ic-domains` file.** The canister must serve `/.well-known/ic-domains` listing your custom domain. Without it, domain ownership verification fails during registration. + +6. **Not deploying `.well-known` because it's a hidden directory.** By default, `icp` / `dfx` ignores directories starting with `.`. You need a `.ic-assets.json5` file with `{ "match": ".well-known", "ignore": false }` so the directory gets included in the deployment. + +7. **Stale `_acme-challenge` TXT records from your DNS provider.** Previous ACME challenges by your provider may leave TXT records on `_acme-challenge.CUSTOM_DOMAIN` that don't appear in your dashboard. These conflict with the IC's ACME flow. Disable all TLS offerings from your provider to clear them. Verify with `dig TXT _acme-challenge.CUSTOM_DOMAIN`. + +8. **Not explicitly registering the domain.** DNS configuration alone is not enough. You must call `POST /custom-domains/v1/CUSTOM_DOMAIN` to start registration. It is not automatic. + +9. **Not setting `host` in HttpAgent on custom domains.** When serving from a custom domain, the `HttpAgent` cannot automatically infer the IC API host like it can on `icp0.io`. You must set `host: "https://icp-api.io"` explicitly for mainnet. + +10. **Forgetting alternative origins for Internet Identity.** II principals depend on the origin domain. Switching from a canister URL to a custom domain changes principals. Configure `.well-known/ii-alternative-origins` to keep the same principals. See the `internet-identity` skill. + +## Implementation + +### Step 1: Configure DNS Records + +Add three DNS records (replace `CUSTOM_DOMAIN` with your domain, e.g., `app.example.com`): + +| Record Type | Host | Value | +|---|---|---| +| CNAME | `CUSTOM_DOMAIN` | `CUSTOM_DOMAIN.icp1.io` | +| TXT | `_canister-id.CUSTOM_DOMAIN` | your canister ID (e.g., `hwvjt-wqaaa-aaaam-qadra-cai`) | +| CNAME | `_acme-challenge.CUSTOM_DOMAIN` | `_acme-challenge.CUSTOM_DOMAIN.icp2.io` | + +Some DNS providers omit the main domain suffix. For `app.example.com` on such providers: +- `app` instead of `app.example.com` +- `_canister-id.app` instead of `_canister-id.app.example.com` +- `_acme-challenge.app` instead of `_acme-challenge.app.example.com` + +For apex domains without CNAME support, use your provider's ANAME or ALIAS record type pointing to `CUSTOM_DOMAIN.icp1.io`. + +### Step 2: Create the `ic-domains` File + +Create `.well-known/ic-domains` in your canister's served directory. List each custom domain on its own line: + +```text +app.example.com +www.example.com +``` + +**For Vite-based projects** (created with `dfx new` or `icp init`), place `.well-known/` inside `public/` so it gets copied to `dist/` unchanged: + +``` +src/project_frontend/ +├── public/ +│ ├── .ic-assets.json5 +│ └── .well-known/ +│ └── ic-domains +``` + +**For projects where the asset canister serves directly from source:** + +``` +src/project_frontend/ +├── src/ +│ └── .well-known/ +│ └── ic-domains +``` + +### Step 3: Configure `.ic-assets.json5` + +Place alongside the `.well-known` directory to ensure it gets deployed: + +```json5 +[ + { + "match": ".well-known", + "ignore": false + } +] +``` + +If you already have a `.ic-assets.json5`, add this entry to the existing array. + +### Step 4: Deploy + +```bash +icp deploy frontend +``` + +### Step 5: Validate + +Check DNS records and canister configuration before registering: + +```bash +curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN/validate" | jq +``` + +Success response: + +```json +{ + "status": "success", + "message": "Domain is eligible for registration: DNS records are valid and canister ownership is verified", + "data": { + "domain": "CUSTOM_DOMAIN", + "canister_id": "CANISTER_ID", + "validation_status": "valid" + } +} +``` + +If validation fails, common errors and fixes: + +| Error | Fix | +|---|---| +| Missing DNS CNAME record | Add the `_acme-challenge` CNAME pointing to `_acme-challenge.CUSTOM_DOMAIN.icp2.io` | +| Missing DNS TXT record | Add the `_canister-id` TXT record with your canister ID | +| Invalid DNS TXT record | Ensure the TXT value is a valid canister ID | +| More than one DNS TXT record | Remove duplicate `_canister-id` TXT records, keep one | +| Failed to retrieve known domains | Ensure `.well-known/ic-domains` is deployed (check `.ic-assets.json5`) | +| Domain missing from list | Add the domain to the `ic-domains` file and redeploy | + +### Step 6: Register + +```bash +curl -sL -X POST "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +``` + +Success response: + +```json +{ + "status": "success", + "message": "Domain registration request accepted and may take a few minutes to process", + "data": { + "domain": "CUSTOM_DOMAIN", + "canister_id": "CANISTER_ID" + } +} +``` + +### Step 7: Wait for Certificate Provisioning + +Poll until `registration_status` is `registered`: + +```bash +curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +``` + +Status values: `registering` → `registered` (success), or `failed` (check error message). + +After `registered`, wait a few more minutes for propagation to all HTTP gateways before testing. + +## Updating a Custom Domain + +To point an existing custom domain at a different canister: + +1. Update the `_canister-id` TXT record to the new canister ID. +2. Notify the service: + +```bash +curl -sL -X PATCH "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +``` + +3. Check status: + +```bash +curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +``` + +## Removing a Custom Domain + +1. Remove the `_canister-id` TXT record and `_acme-challenge` CNAME from DNS. +2. Notify the service: + +```bash +curl -sL -X DELETE "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +``` + +3. Confirm deletion (should return 404): + +```bash +curl -sL -X GET "https://icp0.io/custom-domains/v1/CUSTOM_DOMAIN" | jq +``` + +## HttpAgent Configuration + +On custom domains, the agent cannot auto-detect the IC API host. Set it explicitly: + +```typescript +import { HttpAgent } from "@icp-sdk/core/agent"; + +const isProduction = process.env.NODE_ENV === "production"; +const host = isProduction ? "https://icp-api.io" : undefined; +const agent = await HttpAgent.create({ host }); +``` + +## Deploy & Test + +```bash +# 1. Deploy canister with ic-domains file +icp deploy frontend + +# 2. Validate DNS + canister config +curl -sL -X GET "https://icp0.io/custom-domains/v1/yourdomain.com/validate" | jq + +# 3. Register +curl -sL -X POST "https://icp0.io/custom-domains/v1/yourdomain.com" | jq + +# 4. Poll until registered +curl -sL -X GET "https://icp0.io/custom-domains/v1/yourdomain.com" | jq +``` + +## Verify It Works + +```bash +# 1. Verify DNS records +dig CNAME yourdomain.com +# Expected: yourdomain.com. CNAME yourdomain.com.icp1.io. + +dig TXT _canister-id.yourdomain.com +# Expected: "" + +dig CNAME _acme-challenge.yourdomain.com +# Expected: _acme-challenge.yourdomain.com. CNAME _acme-challenge.yourdomain.com.icp2.io. + +# 2. Verify ic-domains file is served by the canister +curl -sL "https://.icp0.io/.well-known/ic-domains" +# Expected: your domain listed + +# 3. Verify registration status is "registered" +curl -sL -X GET "https://icp0.io/custom-domains/v1/yourdomain.com" | jq '.data.registration_status' +# Expected: "registered" + +# 4. Verify the custom domain serves your canister +curl -sI "https://yourdomain.com" +# Expected: HTTP/2 200 + +# 5. Verify no stale ACME TXT records +dig TXT _acme-challenge.yourdomain.com +# Expected: no TXT records (only the CNAME) +```