diff --git a/.claude/upstream.md b/.claude/upstream.md index a35a6e24..fc3d7372 100644 --- a/.claude/upstream.md +++ b/.claude/upstream.md @@ -80,9 +80,9 @@ Upstream file paths and the tracking model (release-tag vs commit) are listed pe ## static-site - **Upstream:** https://github.com/dfinity/certified-assets -- **Tag:** v0.3.3 -- **Commit:** ac2a8e71802b40abb56078ca759b4c0f94f5fe35 -- **Last synced:** 2026-08-03 +- **Tag:** v0.4.0 +- **Commit:** 4987b0857bda9793b38355096e836e4ddffa8cde +- **Last synced:** 2026-09-23 - **Upstream files:** `docs/` — the certified-assets user documentation is the source of truth for the static-site recipe. The sync check diffs **all files** in `docs/` recursively (`scripts/sync-upstream-check.sh` uses the Git Trees API with `?recursive=1`), so nested `docs//` files are covered. Files today (all top-level): `overview.md`, `routing.md`, `redirects.md`, `headers.md`, `site-files.md`, `access-protection.md`, `how-it-works.md`, `verifying-contents.md`. - **Relationship:** the icskills `static-site` SKILL.md is **derived, not 1:1** — it is an agent-focused condensation of the upstream docs plus icskills-only material (legacy asset-canister reference, migration guide, cross-skill links). Treat the upstream diff as an **advisory review trigger**: when `docs/` changes, review whether the SKILL.md or its references need updating; do not mechanically overwrite. - **icskills-owned sections (entirely icskills-authored, never overwrite from upstream):** diff --git a/evaluations/static-site.json b/evaluations/static-site.json index d095e7a3..5a9ca2b2 100644 --- a/evaluations/static-site.json +++ b/evaluations/static-site.json @@ -80,9 +80,29 @@ "Does NOT use the legacy ic0.app or icp0.io gateway domains", "Does NOT use icp-api.io (that is the API endpoint, not the browser gateway)" ] + }, + { + "name": "Adversarial: do not share raw.icp.net links (unverified, phishable login)", + "prompt": "My static-site preview on mainnet has access protection enabled. A teammate says to share https://.raw.icp.net with testers because it's the 'direct' URL. Is that fine, or should I send a different link? Just the key points.", + "expected_behaviors": [ + "Says not to share the raw.icp.net URL and to share https://.icp.net (a verifying gateway) instead", + "Explains the raw host forwards responses without verifying the certificate, so visitors get no certification guarantee", + "Notes that with access protection the redirect and login page arrive unverified over raw, so a fake login form could harvest tokens", + "Does NOT claim the canister can be configured to block or disable raw access (there is no allow_raw_access setting on certified-assets)" + ] + }, + { + "name": "Third-party verification of a static-site state hash", + "prompt": "I want to independently verify that the static-site canister on mainnet serves exactly the build from its public repo. Give me the steps as commands, briefly, no deploy steps.", + "expected_behaviors": [ + "Reproduces the build from source to get the served directory (e.g. dist/)", + "Queries the canister's version and builds the verifier from the matching certified-assets tag with cargo install --git https://github.com/dfinity/certified-assets --tag --locked state-hash-cli", + "Computes the local hash with state-hash ./dist", + "Reads the canister hash with icp canister call state_hash '()' -n ic (by principal with -n, not -e) and compares the two", + "Does NOT treat the hash printed by icp deploy (or handed over by the operator) as independent verification" + ] } ], - "trigger_evals": { "description": "Queries to test whether the skill activates correctly.", "should_trigger": [ diff --git a/skills/deploy-to-cloud-engine/SKILL.md b/skills/deploy-to-cloud-engine/SKILL.md index 0f948b57..5773e254 100644 --- a/skills/deploy-to-cloud-engine/SKILL.md +++ b/skills/deploy-to-cloud-engine/SKILL.md @@ -159,7 +159,7 @@ Set them under each canister's `settings.environment_variables` — this is vali # frontend/canister.yaml name: frontend recipe: - type: "@dfinity/static-site@v0.3.3" + type: "@dfinity/static-site@v0.4.0" configuration: build: - npm install diff --git a/skills/icp-cli/SKILL.md b/skills/icp-cli/SKILL.md index 79deb6f0..16b7ebbc 100644 --- a/skills/icp-cli/SKILL.md +++ b/skills/icp-cli/SKILL.md @@ -102,7 +102,7 @@ npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm canisters: - name: frontend recipe: - type: "@dfinity/static-site@v0.3.3" # recommended frontend recipe (certified-assets) + type: "@dfinity/static-site@v0.4.0" # recommended frontend recipe (certified-assets) configuration: dir: dist build: @@ -251,7 +251,7 @@ Source Code → [Build] → WASM → [Deploy] → Running Canister → [Sync] `icp deploy` runs all three phases in sequence: 1. **Build** — Compile canisters to WASM (via recipes or explicit build steps) 2. **Deploy** — Create canisters (if new), apply settings, install WASM -3. **Sync** — Post-deployment operations via `script` or `plugin` steps (e.g., uploading assets). Asset uploading is not built into the CLI: a frontend recipe supplies a `plugin` sync step that uploads the `dir` contents — the recommended `@dfinity/static-site@v0.3.3` (certified-assets) or the legacy `@dfinity/asset-canister@v2.2.1`. The legacy built-in `type: assets` step is removed in icp-cli 0.3.0 — see the `static-site` skill. +3. **Sync** — Post-deployment operations via `script` or `plugin` steps (e.g., uploading assets). Asset uploading is not built into the CLI: a frontend recipe supplies a `plugin` sync step that uploads the `dir` contents — the recommended `@dfinity/static-site@v0.4.0` (certified-assets) or the legacy `@dfinity/asset-canister@v2.2.1`. The legacy built-in `type: assets` step is removed in icp-cli 0.3.0 — see the `static-site` skill. Run phases separately for more control: ```bash @@ -403,7 +403,7 @@ The canister name (`backend`) must exactly match between `icp.yaml` and `mops.to canisters: - name: frontend recipe: - type: "@dfinity/static-site@v0.3.3" + type: "@dfinity/static-site@v0.4.0" configuration: dir: dist build: @@ -441,7 +441,7 @@ canisters: |--------|------------|-----------------|-----------------| | Rust | `@dfinity/rust@v3.3.0` | — | `package` (defaults to canister name), `candid`, `locked`, `shrink`, `compress`, `metadata` | | Motoko | `@dfinity/motoko@v5.0.0` | — | `shrink`, `compress`, `metadata` | -| Static site (frontend) | `@dfinity/static-site@v0.3.3` | `dir` | `build`, `presync`, `metadata` | +| Static site (frontend) | `@dfinity/static-site@v0.4.0` | `dir` | `build`, `presync`, `metadata` | | Asset (legacy frontend) | `@dfinity/asset-canister@v2.2.1` | `dir` | `build`, `version` | | Prebuilt | `@dfinity/prebuilt@v2.1.0` | `path` | `sha256`, `shrink`, `compress`, `metadata` | diff --git a/skills/icp-cli/references/dfx-migration.md b/skills/icp-cli/references/dfx-migration.md index 0bd76937..f5a83f2d 100644 --- a/skills/icp-cli/references/dfx-migration.md +++ b/skills/icp-cli/references/dfx-migration.md @@ -97,7 +97,7 @@ Steps: |----------|----------| | `"type": "rust"` | `recipe.type: "@dfinity/rust@v3.3.0"` | | `"type": "motoko"` | `recipe.type: "@dfinity/motoko@v5.0.0"` | -| `"type": "assets"` | `recipe.type: "@dfinity/static-site@v0.3.3"` (recommended) or `@dfinity/asset-canister@v2.2.1` (legacy). Not a drop-in: static-site is a different canister using `_headers`/`_redirects` (not `.ic-assets.json5`), and switching a deployed canister needs a reinstall — see the `static-site` skill's migration guide. | +| `"type": "assets"` | `recipe.type: "@dfinity/static-site@v0.4.0"` (recommended) or `@dfinity/asset-canister@v2.2.1` (legacy). Not a drop-in: static-site is a different canister using `_headers`/`_redirects` (not `.ic-assets.json5`), and switching a deployed canister needs a reinstall — see the `static-site` skill's migration guide. | | `"package": "X"` | `recipe.configuration.package: X` — optional as of `@dfinity/rust@v3.3.0`, which defaults it to the canister `name`. Drop it when the Cargo `[package] name` already matches. | | `"candid": "X"` | `recipe.configuration.candid: X` | | `"main": "X"` | **Not** `recipe.configuration.main`. The `@dfinity/motoko@v5+` recipe compiles via `mops build`, so `main` moves to `mops.toml` as `[canisters.] main = "X"`. The `[canisters.]` key must exactly match the canister `name` in `icp.yaml`. | diff --git a/skills/service-discoverability/SKILL.md b/skills/service-discoverability/SKILL.md index de0685d3..bf0d615f 100644 --- a/skills/service-discoverability/SKILL.md +++ b/skills/service-discoverability/SKILL.md @@ -115,7 +115,7 @@ Canister IDs differ per network (local, staging, mainnet), so **never commit har ```yaml name: frontend recipe: - type: "@dfinity/static-site@v0.3.3" + type: "@dfinity/static-site@v0.4.0" configuration: build: # Runs BEFORE the canister exists — no canister IDs available here. @@ -131,7 +131,7 @@ recipe: - The static-site recipe exports each project canister's ID into `presync` as `ICP_CLI_CID_` (name upper-cased, non-alphanumerics → `_`; e.g. `backend` → `ICP_CLI_CID_BACKEND`). Other vars: `ICP_CLI_CID` (this canister), `ICP_CLI_NETWORK`. - `$ICP_CLI_ENVIRONMENT` is the environment being deployed (e.g. `local`, `ic`), exported into the `presync` shell. It selects the matching template, so the same hook serves every network. - **`presync` runs with the canister directory as its working directory.** The relative `ic-architecture/...` path therefore resolves *inside the frontend canister directory* — put the templates at `frontend/ic-architecture/`, alongside `canister.yaml` (as the example project does). A path resolved from the repo root instead would make `envsubst` read nothing and silently write an empty manifest (Pitfall 7). -- Pin the recipe to the current release (`@dfinity/static-site@v0.3.3` here); check the static-site recipe releases and the `static-site` skill for the latest. +- Pin the recipe to the current release (`@dfinity/static-site@v0.4.0` here); check the static-site recipe releases and the `static-site` skill for the latest. Keep one template per environment under `frontend/ic-architecture/`, with `envsubst` placeholders for the IDs. diff --git a/skills/static-site/SKILL.md b/skills/static-site/SKILL.md index efc02d3e..f1d17415 100644 --- a/skills/static-site/SKILL.md +++ b/skills/static-site/SKILL.md @@ -12,7 +12,7 @@ metadata: ## What This Is -The **`@dfinity/static-site` recipe** deploys a static site — a built frontend, docs, or any folder of files — to the **certified-assets canister** on the Internet Computer, which serves it over HTTP with **response certification**. Every response carries a cryptographic proof, and the IC HTTP gateway verifies that proof before handing the response to the browser: visitors get content the canister provably committed to, not something a boundary node or gateway altered in transit. +The **`@dfinity/static-site` recipe** deploys a static site — a built frontend, docs, or any folder of files — to the **certified-assets canister** on the Internet Computer, which serves it over HTTP with **response certification**. The canister certifies **every** response (certification v2 only, never the `no_certification` escape hatch, no way to turn it off), and a **verifying** HTTP gateway checks that proof before handing the response to the browser: visitors get content the canister provably committed to, not something altered in transit. Whether the proof is *checked* depends on the gateway the visitor goes through: `https://.icp.net` verifies, `https://.raw.icp.net` does not (Pitfall 14). **This is the recommended way to host a frontend on the IC going forward.** The recipe bundles a matched pair — the canister and its sync plugin — pinned together by one version. You point it at your build directory; `icp deploy` uploads, certifies, and serves. @@ -40,7 +40,7 @@ Static-site canisters are created per-project — there is no global canister ID canisters: - name: frontend recipe: - type: "@dfinity/static-site@v0.3.3" + type: "@dfinity/static-site@v0.4.0" configuration: build: - npm ci @@ -81,12 +81,14 @@ The recipe takes four configuration fields: 10. **Switching an existing project from the legacy asset canister to static-site.** Repointing `recipe:` at `@dfinity/static-site` and running a plain `icp deploy` **fails before anything is installed**: these are two unrelated canisters with unrelated Candid interfaces, so icp-cli's pre-install check aborts with `Candid interface compatibility check failed: '' … You are making a BREAKING change`. Run **`icp deploy --mode reinstall`** instead. That replaces the wasm with the certified-assets canister, **discards the old stable memory** — every legacy asset, permission, and `.ic-assets.json5`-derived setting is gone — and leaves the canister with empty state, after which the sync plugin uploads your whole `dir` from scratch. Deploying static-site as a brand-new canister avoids the question entirely. Do **not** silence the check with `--yes`: that pushes the in-place upgrade through onto stable memory certified-assets cannot read, leaving a live canister that serves nothing. See the migration reference. -11. **Assuming a recipe version bump re-installs itself.** Moving between certified-assets' **own** releases is gentler than the legacy switch above — the Candid interface is stable across a release series, so nothing blocks the deploy — but a breaking bump still needs a reinstall **you run yourself**. The canister and plugin are version-locked, so after bumping the recipe `icp deploy` upgrades in place and the sync plugin then refuses, reporting `assets canister version mismatch: canister is X, this plugin is Y` plus the fix: `icp canister install --mode upgrade` for a **patch** bump (state preserved) or `icp canister install --mode reinstall` for a **breaking** (pre-1.0 minor, post-1.0 major) bump, which wipes state so the next sync re-uploads every asset and redirect rule. A failed sync right after a version bump is this, not a bug. +11. **Assuming a recipe version bump re-installs itself.** Moving between certified-assets' **own** releases is gentler than the legacy switch above — the Candid interface is stable across a release series, so nothing blocks the deploy — but a breaking bump still needs a reinstall **you run yourself**. The canister and plugin are version-locked, so after bumping the recipe `icp deploy` upgrades in place and the sync plugin then refuses, reporting `assets canister version mismatch: canister is X, this plugin is Y` plus the fix: `icp canister install --mode upgrade` for a **patch** bump (state preserved) or `icp canister install --mode reinstall` for a **breaking** (pre-1.0 minor, post-1.0 major) bump, which wipes state so the next sync re-uploads every asset and redirect rule. (**`v0.3.x` → `v0.4.0` is breaking**: reinstall, then `icp deploy`.) A failed sync right after a version bump is expected — this, not a bug. 12. **`.well-known/` is uploaded automatically — no config needed.** The plugin skips dotfiles and dot-directories *except* `.well-known/`, which it traverses normally. So `dir/.well-known/ic-domains` is served at `/.well-known/ic-domains` with no extra setting. (This is the opposite of the legacy canister, which needed an explicit `.ic-assets.json5` un-ignore rule.) 13. **Access protection ordering.** The recipe's `icp deploy` installs the canister **and** syncs assets together, so a plain deploy-then-`enable_protection` briefly serves your content publicly. For a brand-new *private* app, enable protection **before your real assets are synced** — deploy a `dir` containing only `login.html`, `enable_protection`, then deploy the full site — so assets are never world-readable. The login page must be **fully self-contained** (inline CSS/JS, `data:` URIs) — it is the only gate-exempt path, and any external subresource it references would itself be gated. See [Access protection](#access-protection-private-apps). +14. **Sharing or linking a `raw.icp.net` URL.** `.raw.icp.net` serves the same site as `.icp.net`, but the gateway forwards responses **without checking the certificate**, so the visitor gets no more assurance than from an ordinary web host. There is no canister setting to refuse `raw` requests (unlike the legacy canister's `allow_raw_access`): the only clue is the unauthenticated `Host` header, and which hostnames verify is a gateway deployment detail, not protocol. Link to `https://.icp.net` (or a custom domain on a verifying gateway) and treat `raw` as a debugging tool only. With access protection on, a `raw` URL is actively dangerous: the redirect and login page arrive unverified, so a non-verifying gateway could serve a fake login form and harvest tokens. The access cookie is host-only, so a session on the certified host does not carry over to `raw`. + ## SPA Routing and Redirects: `_redirects` Add a `_redirects` file to the root of your `dir`. Syntax follows [Netlify's `_redirects`](https://docs.netlify.com/manage/routing/redirects/overview/) — one rule per line, three whitespace-separated fields (`from`, `to`, `status`); blank lines and `#` comments are ignored. @@ -149,6 +151,7 @@ Key rules: - **Patterns match the file (asset key), not the visitor's URL.** Write `/index.html`, not `/`. For a SPA, a `Cache-Control` on `/index.html` (or `/*.html`) is what every `/*`-rewritten client route gets; a block written against a route like `/dashboard/*` matches no file and does nothing. - **All matching blocks contribute** — a file matching several blocks gets every block's headers (same-name values are combined comma-separated; `Set-Cookie` stays separate). - **`Content-Type` is special** — the bare `Content-Type: ` form overrides the stored media type of the matching file (use it for extension-less files like `/llms.txt`); it is single-valued, first-match-wins, and is *not* emitted as an ordinary header. +- **`Set-Cookie` is allowed, but don't use the name `ic_env`.** The canister adds its own `ic_env` cookie to HTML responses as **two** `Set-Cookie` headers with the same name and value: one `SameSite=Lax`, one `SameSite=None; Partitioned` (so it survives in a cross-site iframe). Seeing two `ic_env` headers is expected, not a duplicate-header bug. - Reserved headers are rejected at deploy time — see Pitfall 6. ## Clean URLs and the 404 Page @@ -202,7 +205,7 @@ icp canister call frontend issue_token '(record { label = "owner"; ttl_secs = 31 Always pass the argument explicitly — `'()'` for the methods that take none. Called with no argument, `icp canister call` opens an interactive prompt instead of sending an empty one. -This is **access gating, not confidentiality**: node operators can read asset bytes and the token store, there is no rate-limiting, and it relies on the honest-replica/honest-gateway assumption. Use high-entropy random tokens for share links; enable *before* the first sync for a new private app (Pitfall 13). Full details in the [certified-assets access-protection docs](https://github.com/dfinity/certified-assets/blob/v0.3.3/docs/access-protection.md). +This is **access gating, not confidentiality**: node operators can read asset bytes and the token store, there is no rate-limiting, and it relies on the honest-replica/honest-gateway assumption. The gate runs canister-side on every hostname, but its redirect and login page are only verified through a verifying gateway, so never send people to a `raw` URL of a protected app (Pitfall 14). Use high-entropy random tokens for share links; enable *before* the first sync for a new private app (Pitfall 13). Full details in the [certified-assets access-protection docs](https://github.com/dfinity/certified-assets/blob/v0.4.0/docs/access-protection.md). ## Authorizing Uploaders @@ -229,7 +232,7 @@ icp canister call frontend deauthorize '(principal "")' canisters: - name: frontend recipe: - type: "@dfinity/static-site@v0.3.3" + type: "@dfinity/static-site@v0.4.0" configuration: dir: dist presync: @@ -246,12 +249,12 @@ Alternatively, read canister IDs at **runtime** in the browser from the `ic_env` No configuration needed — on by default: -- **Response certification** — every response is certified and gateway-verified. +- **Response certification** — every response is certified; a verifying gateway (not `raw`) checks the proof. - **Clean URLs** — `307` canonicalization (above). - **Compression** — compressible assets are stored gzip + Brotli alongside the original and negotiated per request via `Accept-Encoding`. Compressible means: any `text/*`; any `+json` or `+xml` suffix (so `image/svg+xml`, `application/xhtml+xml`); `application/javascript`, `application/json`, `application/xml`, `application/wasm`; and `font/*` **except** `woff`/`woff2` (already compressed). An encoding is kept only if it actually came out smaller than the original. - **ETag / `304 Not Modified`** — content-hash ETag; unchanged files aren't re-downloaded. - **A default certified `404`** — replaceable with your own `/404.html`. -- **The `ic_env` cookie** — on HTML responses, carrying canister IDs and the root key for the frontend. +- **The `ic_env` cookie** — on HTML responses (as two `Set-Cookie` headers, `Lax` and `None; Partitioned`), carrying `PUBLIC_*` env vars (canister IDs) and the root key for the frontend. ## Deploy & Verify @@ -293,6 +296,32 @@ icp canister call frontend http_request '(record { # Mainnet: https://.icp.net ``` +## Verifying Deployed Contents (state hash) + +Certification proves responses match what the canister committed to; the **state hash** proves *what it committed to* matches a build reproduced from public source. The hash `icp deploy` prints (`canister reports state hash `) comes from the canister itself — a deploy self-consistency check, **not** third-party verification. To verify a canister: + +```bash +# 1. Reproduce the build from source at the deployed version → ./dist (incl. _headers/_redirects) + +# 2. Ask the canister its release, then build the verifier from THAT tag. +# Not published as a binary or on crates.io. --locked is required: Cargo.lock pins the +# compressor builds whose output bytes the hash covers; without it the hash differs. +icp canister call version '()' -n ic --query +# (record { major = 0 : nat32; minor = 4 : nat32; patch = 0 : nat32 }) +cargo install --git https://github.com/dfinity/certified-assets --tag v0.4.0 --locked state-hash-cli + +# 3. Compute the hash locally (prints 64 hex chars) +state-hash ./dist + +# 4. Read the canister's hash (an update call, so consensus-backed); the hash is the last 32 bytes +icp canister call state_hash '()' -n ic -o hex | tail -c 65 +``` + +- Target the canister by **principal** with `-n `; `-e ` resolves a canister *name* from a local project, which a third-party verifier doesn't have. Pass `'()'` explicitly. +- **32 zero bytes is not a hash**: the canister has never finished a sync, or one is in progress (a sync drops the cached hash when it starts). Re-read after the deploy completes. +- A mismatch means content, headers, or redirects differ from the source — or the canister was prepared with non-standard compressors (platforms embedding the crates may do this; the tool only reproduces what `icp deploy` does). +- **Not covered by the hash:** the `ic_env` cookie (a controller can repoint a frontend's backend canister ID via env vars with a byte-identical build) and access protection (a matching hash says the canister holds your build, not that a visitor can reach it). + ## Legacy Asset Canister and Migration - **Maintaining an existing `@dfinity/asset-canister` project** (`.ic-assets.json5`, `AssetManager` uploads, `grant_permission` roles, `allow_raw_access`): see [`references/legacy-asset-canister.md`](references/legacy-asset-canister.md). @@ -303,4 +332,4 @@ icp canister call frontend http_request '(record { - Load `icp-cli` for the recipe system, `icp.yaml` structure, canister-ID injection, and the `ic_env` cookie / `safeGetCanisterEnv()` pattern. - Load `custom-domains` for DNS records, ACME challenge, and TLS provisioning of a custom domain. - Load `internet-identity` for reading the root key and canister IDs from `ic_env` in a frontend. -- Full upstream user docs: [certified-assets docs](https://github.com/dfinity/certified-assets/blob/v0.3.3/docs/overview.md). +- Full upstream user docs: [certified-assets docs](https://github.com/dfinity/certified-assets/blob/v0.4.0/docs/overview.md). diff --git a/skills/static-site/references/migrating-from-asset-canister.md b/skills/static-site/references/migrating-from-asset-canister.md index 5ee8f49c..9bb0fcda 100644 --- a/skills/static-site/references/migrating-from-asset-canister.md +++ b/skills/static-site/references/migrating-from-asset-canister.md @@ -28,7 +28,7 @@ The two are **unrelated canisters with unrelated Candid interfaces**, so a plain - name: frontend recipe: - type: "@dfinity/asset-canister@v2.2.1" -+ type: "@dfinity/static-site@v0.3.3" ++ type: "@dfinity/static-site@v0.4.0" configuration: dir: dist build: