diff --git a/.agents/upstream-tracking.md b/.agents/upstream-tracking.md index d4a4ec11..de13acd0 100644 --- a/.agents/upstream-tracking.md +++ b/.agents/upstream-tracking.md @@ -2,14 +2,14 @@ How this repo stays current with the projects it documents. -Upstream repos fall into three groups, and the group decides the procedure. +Upstream repos fall into four groups, and the group decides the procedure. -| | Vendored (submodule) | Watched | Reference | -|---|---|---|---| -| Which | `motoko`, `internetidentity`, `examples` | the `watched` array in `.sources/upstream.json` | the `reference` array | -| Why | the build opens their files | a release can silently invalidate a lot of published content | drawn on too lightly, or another check already catches the drift | -| Pin | the gitlink | `pinned` in `upstream.json` | none; verify against the latest release | -| Release issue | only `examples`, and only when a quoted file moved | yes | no | +| | Vendored (submodule) | Synced | Watched | Reference | +|---|---|---|---|---| +| Which | `motoko`, `internetidentity`, `examples` | the `synced` array in `.sources/upstream.json` | the `watched` array | the `reference` array | +| Why | the build opens their files | their own pages are published here as-is | a release can silently invalidate a lot of published content | drawn on too lightly, or another check already catches the drift | +| Pin | the gitlink | `pinned` in `upstream.json` | `pinned` in `upstream.json` | none; verify against the latest release | +| Release issue | only `examples`, and only when a quoted file moved | no; its own workflow opens the bump PR | yes | no | Deciding between the last two is a judgment about blast radius, and the `why` field on each `reference` entry records the footprint that decided it. Promote an @@ -236,6 +236,72 @@ git -C .sources/ fetch --unshallow git -C .sources/ checkout ``` +## Synced trees + +A synced tree is a set of pages this repo publishes but does not write. Upstream +owns the prose; a sync script fetches it at a pinned ref, adapts the few things +that only make sense on this site, and writes the result into `docs/`. + +| Tree | Upstream | Script | Workflow | +|---|---|---|---| +| `docs/guides/frontends/static-site/` | `dfinity/certified-assets` `docs/` | `scripts/sync-static-site.mjs` | `.github/workflows/sync-static-site.yml` | + +`motoko` and `internetidentity` are synced too, but they are submodules because +the build also opens their files, so they follow the vendored procedure above. +This group is for the case where nothing is resolved at build time, only +markdown links, which is why there is no submodule to hold the pin. + +### How the pin moves + +The workflow runs weekly, resolves the latest release tag, and opens a bump PR +for every release the pin does not already contain. Two shapes come out of it: + +- **Pages changed.** The usual case: review the diff. +- **Nothing under `docs/` changed.** The pages are byte-identical and the only + diff is `source_ref` on each of them, but the PR still opens, because that is + what moves the pin off a commit and onto a release tag. Skipping these would + strand a temporary commit pin for good. + +The PR body says which of the two it is. The recipe version readers type is a +separate axis, covered by the `static-site` entry under `watched`. + +To sync by hand, or to trial a ref before pinning it: + +```bash +npm run sync:static-site # uses the pin in .sources/upstream.json +node scripts/sync-static-site.mjs --ref main +``` + +The script exits non-zero rather than publishing something broken: missing +frontmatter, an absolute link to this site it cannot map, a relative link that +does not resolve, or a banned character that survived normalization. Fix the +cause, do not hand-edit the output. + +### The pin may sit ahead of the latest release + +`.sources/VERSIONS` forbids pinning a submodule past its latest release, so that +docs cannot describe behavior users cannot run yet. A synced tree can need the +opposite: a docs-only fix upstream is published before the next release, and the +pages must be syncable now. `certified-assets` started exactly there, pinned to a +commit because the `v0.3.3` tag predated the frontmatter contract the sync +requires (`certified-assets#124`). + +The rule that matters is the one behind it: never pin past a commit that +documents unreleased behavior. Docs-only commits are safe, so check what the +range contains before pinning past a tag, and record why in the entry's +`$comment`. Move the pin back onto release tags as soon as one includes the +change. + +### On bump, check + +1. The page diffs, for content changes; the tree is regenerated wholesale +2. Whether a behavior change contradicts our own pages: `certification.md` + describes what this canister certifies, `asset-canister.md` contrasts the two + recipes +3. Whether prose naming the recipe version needs bumping with it +4. Whether a normalization the script reports should be fixed upstream instead, + so it becomes a no-op + ## Build dependencies (npm) The packages in `package.json` are a separate axis: they decide whether the site diff --git a/.github/workflows/sync-static-site.yml b/.github/workflows/sync-static-site.yml new file mode 100644 index 00000000..419e8e43 --- /dev/null +++ b/.github/workflows/sync-static-site.yml @@ -0,0 +1,194 @@ +name: Sync static-site docs + +on: + schedule: + - cron: '0 9 * * 3' # Weekly on Wednesday + workflow_dispatch: + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + + - name: Create GitHub App Token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + id: app-token + with: + client-id: ${{ vars.PR_AUTOMATION_BOT_PUBLIC_CLIENT_ID }} + private-key: ${{ secrets.PR_AUTOMATION_BOT_PUBLIC_PRIVATE_KEY }} + + # certified-assets is not a submodule: the build resolves no file from it. + # A blobless clone is only needed to reason about refs (is the latest + # release already contained in the pin, did docs/ change), which the + # contents API cannot answer. + - name: Clone certified-assets + run: git clone --filter=blob:none --no-checkout https://github.com/dfinity/certified-assets.git /tmp/certified-assets + + - name: Resolve the ref to sync + id: check + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + PIN=$(node -p "require('./.sources/upstream.json').synced.find(e => e.repo === 'dfinity/certified-assets').pinned") + echo "pin=$PIN" >> $GITHUB_OUTPUT + + # Stable releases only, the same pattern the upstream.json watcher uses. + # `^v[0-9]` would accept v0.4.0-rc.1 and publish docs for a prerelease. + TAG=$(git -C /tmp/certified-assets tag --sort=-version:refname \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) + echo "Pinned: $PIN. Latest release: $TAG." + echo "tag=$TAG" >> $GITHUB_OUTPUT + + # The pin is allowed to sit ahead of the latest release while a docs + # fix has shipped but a release has not (the state this sync started + # in). Syncing the tag then would publish older prose. + if git -C /tmp/certified-assets merge-base --is-ancestor "$TAG" "$PIN"; then + echo "Pin already contains $TAG. Nothing to sync." + echo "needed=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Skip only when a PR is actually open. A branch on its own proves + # nothing: if a previous run pushed and then failed at `gh pr create`, + # treating the branch as a PR would strand that release forever, with + # nothing to review and no further attempts. + BRANCH="infra/sync-static-site-${TAG}" + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + if [ -n "$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')" ]; then + echo "A PR for $BRANCH is already open. Skipping." + echo "needed=false" >> $GITHUB_OUTPUT + exit 0 + fi + if git ls-remote --exit-code origin "refs/heads/${BRANCH}" > /dev/null 2>&1; then + echo "Branch $BRANCH exists with no open PR (an earlier run stopped" + echo "between push and PR creation). Deleting it so this run can retry." + git push origin --delete "$BRANCH" + fi + + # A release that ships canister changes without touching docs/ leaves + # the synced pages byte-identical, so there is no content to review. + # The pin still has to move: it is allowed to sit on a commit only + # while no release carries the pages, and skipping here would strand + # it on that commit for good. So the PR is opened either way, and the + # body says which of the two it is. + CHANGED=$(git -C /tmp/certified-assets diff --name-only "${PIN}..${TAG}" -- docs/) + echo "needed=true" >> $GITHUB_OUTPUT + if [ -z "$CHANGED" ]; then + echo "No docs/ changes between $PIN and $TAG: advancing the pin only." + echo "pin_only=true" >> $GITHUB_OUTPUT + else + echo "Changed upstream pages:" + echo "$CHANGED" + echo "pin_only=false" >> $GITHUB_OUTPUT + echo "changed_files<> $GITHUB_OUTPUT + echo "$CHANGED" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + + - name: Move the pin to the release tag + if: steps.check.outputs.needed == 'true' + run: | + node -e ' + const fs = require("fs"); + const file = ".sources/upstream.json"; + const config = JSON.parse(fs.readFileSync(file, "utf8")); + const entry = config.synced.find((e) => e.repo === "dfinity/certified-assets"); + entry.pinned = process.argv[1]; + fs.writeFileSync(file, JSON.stringify(config, null, 2) + "\n"); + ' "$TAG" + env: + TAG: ${{ steps.check.outputs.tag }} + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + if: steps.check.outputs.needed == 'true' + with: + node-version: 22 + cache: npm + + - name: Install dependencies + if: steps.check.outputs.needed == 'true' + run: npm ci + + - name: Run static-site docs sync + if: steps.check.outputs.needed == 'true' + run: npm run sync:static-site + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + + # The synced tree is deliberately not in the validator's SYNCED allowlist: + # the sync normalizes and rewrites so that the pages pass the same checks + # as a hand-written page, and an exemption would hide the day they stop. + - name: Validate + if: steps.check.outputs.needed == 'true' + run: npm run validate + + # Same set and the same URL rewrite as build.yml. `.gitmodules` uses SSH + # URLs, which a runner cannot fetch, and `.sources/motoko` is required + # because pages under docs/languages/motoko/ pull code through + # `file=/...` includes: without it they render empty + # without failing the build, so this step would have reported a passing + # build for a site with empty Motoko pages. + - name: Initialize submodules (required for build) + if: steps.check.outputs.needed == 'true' + run: | + git config --global url."https://github.com/".insteadOf "git@github.com:" + git submodule update --init --depth 1 .sources/examples .sources/motoko + + - name: Build check + if: steps.check.outputs.needed == 'true' + run: npm run build + + - name: Create PR + if: steps.check.outputs.needed == 'true' + run: | + git config user.name "pr-automation-bot-public[bot]" + git config user.email "pr-automation-bot-public[bot]@users.noreply.github.com" + + BRANCH="infra/sync-static-site-${TAG}" + git checkout -b "$BRANCH" + git add .sources/upstream.json docs/guides/frontends/static-site + git commit -m "chore: sync static-site docs to dfinity/certified-assets ${TAG}" + git push -u origin "$BRANCH" + + { + echo "## Summary" + echo "" + echo "Automated sync of the certified-assets user docs." + echo "" + echo "**Release:** \`$TAG\` (pinned from \`$PIN\`)" + echo "" + if [ "$PIN_ONLY" = "true" ]; then + echo "No page changed in this range. The pin moves off a commit and onto" + echo "the release tag, so the only diff is \`source_ref\` on each page." + else + echo "**Changed upstream files:**" + while IFS= read -r f; do + [ -n "$f" ] && echo "- \`$f\`" + done <<< "$CHANGED" + fi + echo "" + echo "- Ran \`npm run sync:static-site\`, regenerating \`docs/guides/frontends/static-site/\`" + echo "- Validator and build passed" + echo "" + echo "## Checklist" + echo "" + echo "- [ ] Review the page diffs for content changes" + echo "- [ ] Check whether a behavior change contradicts our own Frontends pages (\`certification.md\`, \`asset-canister.md\`)" + echo "- [ ] Check whether the recipe version named in \`icp.yaml\` examples needs bumping with it" + } > /tmp/pr-body.md + + gh pr create \ + --title "chore: sync static-site docs to dfinity/certified-assets ${TAG}" \ + --body-file /tmp/pr-body.md + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + # Values from the upstream repo reach the shell as data, never as + # script: a filename containing shell metacharacters would otherwise + # be interpolated into this step's source. + TAG: ${{ steps.check.outputs.tag }} + PIN: ${{ steps.check.outputs.pin }} + CHANGED: ${{ steps.check.outputs.changed_files }} + PIN_ONLY: ${{ steps.check.outputs.pin_only }} diff --git a/.gitignore b/.gitignore index 9c4d5b33..d3c722cb 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ skills-lock.json # Upstream release check output (scripts/check-upstream-releases.mjs) .upstream-checks/ + +# Scratch space for scripts/sync-static-site.mjs (removed on success) +.sync-staging/ diff --git a/.sources/upstream.json b/.sources/upstream.json index 152852f5..6ba7cda9 100644 --- a/.sources/upstream.json +++ b/.sources/upstream.json @@ -1,11 +1,17 @@ { "$comment": [ - "Every upstream this repo tracks, in three groups.", + "Every upstream this repo tracks, in four groups.", "", "`watched` are repos where a release can silently invalidate a lot of published", "content, so they get a pinned ref and a weekly issue when they move. Verify", "claims against the pinned ref, not against main, so a review is reproducible.", "", + "`synced` are repos whose own pages are copied into docs/ by a sync script. The", + "pin is recorded here because there is no gitlink to hold it: the build resolves", + "no file from them, so they are not submodules. Their sync workflow does its own", + "release check and opens the bump PR, the way sync-motoko.yml and sync-ii-spec.yml", + "do, so check-upstream-releases.mjs ignores this group.", + "", "`vendored` are submodules whose pin has no sync workflow of its own. Their pin", "is the gitlink, so git stays the single source of truth. `motoko` and", "`internetidentity` are absent on purpose: sync-motoko.yml and sync-ii-spec.yml", @@ -39,7 +45,30 @@ "", "Fields (vendored): path, repo, track 'branch', branch, affects, and an optional", "pathFilter. 'snippets' reports only when the range touched a file quoted by a", - "`snippet=` in docs/, so an unrelated commit on an active repo stays silent." + "`snippet=` in docs/, so an unrelated commit on an active repo stays silent.", + "", + "Fields (synced): repo, pinned, source, target, script, and affects." + ], + "synced": [ + { + "repo": "dfinity/certified-assets", + "pinned": "65c0f32", + "source": "docs/", + "target": "docs/guides/frontends/static-site/", + "script": "scripts/sync-static-site.mjs", + "affects": "The synced pages are regenerated wholesale, so review the diff rather than the pages. Behavior changes also reach our own Frontends pages: certification.md describes what this canister certifies, and asset-canister.md contrasts the two recipes. The recipe version readers type is tracked separately as the `static-site` entry under `watched`, and the two move in lockstep.", + "$comment": [ + "The `pinned` field above is the ref of record; this note is the policy, not", + "a description of it. This entry may pin a commit rather than a release tag:", + ".sources/VERSIONS forbids that for submodules so docs cannot describe", + "unshipped behavior, and docs-only commits are not that case. Check what the", + "range contains before pinning past a tag, and prefer a tag whenever one", + "carries the pages. The sync workflow moves the pin onto a tag at the next", + "release even when no page changed, so a commit pin is never permanent.", + "History: the first sync used d9cb7df, because certified-assets#124 landed", + "the frontmatter contract this sync requires after the v0.3.3 tag." + ] + } ], "vendored": [ { @@ -111,7 +140,7 @@ "pinned": "static-site-v0.3.3", "track": "release", "tagPattern": "^static-site-v\\d+\\.\\d+\\.\\d+$", - "affects": "Not yet documented (dfinity/certified-assets#124). This repo's own icp.yaml deploys with it, so a release also affects the docs site itself. Each recipe in this repo tags and releases on its own prefix, so it gets its own entry: a single pattern across prefixes would compare `rust-` against `static-site-` textually and only ever surface one of them.", + "affects": "The canister's own pages are synced from dfinity/certified-assets into docs/guides/frontends/static-site/ and follow that repo's releases, not this one, so a recipe release mainly moves the version readers pin: check the `@dfinity/static-site@` examples in guides/frontends/, getting-started/ and references/application-canisters.md. This repo's own icp.yaml deploys with it, so a release also affects the docs site itself. Each recipe in this repo tags and releases on its own prefix, so it gets its own entry: a single pattern across prefixes would compare `rust-` against `static-site-` textually and only ever surface one of them.", "reference": "https://github.com/dfinity/icp-cli-recipes/releases" }, { diff --git a/AGENTS.md b/AGENTS.md index 00684b0d..be6d73cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -155,14 +155,15 @@ EOF - Default to `.md`; use `.mdx` only for interactive components. Tab order: Motoko → Rust → others. - Complete frontmatter on every page (title + description required) - Document structural decisions in the PR description +- Add a new page under `guides/frontends/` to `sidebar.mjs` as well: that one group lists its pages explicitly, so `sidebar.order` alone does not place it (the group's own pages under `static-site/` are autogenerated and need nothing) ## Ask first - Creating or removing top-level sections - Removing existing pages - Changing the frontmatter schema -- Modifying sidebar configuration (`astro.config.mjs`) -- Adding a new `.sources/` submodule +- Modifying sidebar configuration (`sidebar.mjs`, or `astro.config.mjs` for anything else) +- Adding a new `.sources/` submodule or a new synced tree ## Never @@ -170,7 +171,7 @@ EOF - Use `mo:base` — use `mo:core` instead. Critical replacements: `Buffer` → `List`, `HashMap`/`TrieMap`/`Trie`/`RBTree` → `Map`, `Deque` → `Queue`, `OrderedMap` → `pure/Map`, `OrderedSet` → `pure/Set` - Create `.mdx` without a clear need for interactive components - Duplicate content that lives in external docs (icp-cli site, JS SDK docs, the IC skills) -- Edit synced files directly (`docs/languages/motoko/`, `docs/references/internet-identity-spec.md`, `docs/references/verifiable-credentials-spec.md`) +- Edit synced files directly (`docs/languages/motoko/`, `docs/guides/frontends/static-site/`, `docs/references/internet-identity-spec.md`, `docs/references/verifiable-credentials-spec.md`) - Nest sidebar items more than 3 levels deep - Add `Co-Authored-By` or any AI attribution to commits or PR descriptions - Link to `internetcomputer.org/docs/` (retired) or `learn.internetcomputer.org` (content is now in this repo under `docs/concepts/`) @@ -187,8 +188,9 @@ EOF - `docs/` — All documentation (`.md` by default). `src/content/docs/` symlinks here. - `docs/languages/motoko/` — Auto-synced from `caffeinelabs/motoko` (do not edit directly) +- `docs/guides/frontends/static-site/` (synced from `dfinity/certified-assets`, do not edit directly) - `docs/references/internet-identity-spec.md`, `docs/references/verifiable-credentials-spec.md` — Synced from `dfinity/internet-identity` (do not edit directly) -- `.sources/` — Vendored submodules, read-only, plus `upstream.json` (watched repos) and `VERSIONS` (submodule pins) +- `.sources/` — Vendored submodules, read-only, plus `upstream.json` (watched and synced repos) and `VERSIONS` (submodule pins) - `.claude/skills/` — Skills. IC skills are mirrored by `.claude/sync-ic-skills.sh` and not committed; `icp-brand-design`, `icp-brand-voice`, and `technical-documentation` are maintained here - `.agents/upstream-tracking.md` — How upstreams are tracked and bumped (maintainer use) @@ -215,7 +217,7 @@ docs/ ## Source material -Upstream repos are tracked two ways. Which one decides where you read from. +Upstream repos are tracked four ways. Which one decides where you read from. **Vendored as submodules** — three repos, because the build opens their files. Read them from disk; do not edit them. @@ -236,6 +238,37 @@ Pinned versions: [`.sources/VERSIONS`](.sources/VERSIONS). `motoko` and `snippet=` quotes; the build already fails if such a path or `#region` marker stops resolving. Canister IDs and code patterns are in the skills (see "Skills"). +**Synced without a submodule** is for a repo whose own pages are published here +as-is. Nothing about them is resolved at build time, only markdown links, so the +pin is a ref in [`.sources/upstream.json`](.sources/upstream.json) under `synced` +and the sync script fetches the pages at that ref. + +| Topic | Repo | Synced tree | +|-------|------|-------------| +| certified-assets canister behavior | `dfinity/certified-assets` | `docs/guides/frontends/static-site/` | + +Rules for every synced tree, this one included: + +- **Never hand-edit a page in it.** Content changes belong upstream, and the next + sync overwrites anything else. The pages carry a do-not-edit marker and record + `source_repo` / `source_ref` in their frontmatter. +- **They are exempt from the directory-based Diataxis rule and from + `## Next steps`.** Upstream owns one flat set of pages that mixes how-to, + reference, and explanation, and filing them by type would scatter one source of + truth across three directories. No synced page in this repo carries + `## Next steps`. Our own pages around the tree still follow both rules. +- **They are not exempt from the validator.** Only `docs/languages/motoko/` is, + for historical reasons. A sync script normalizes and rewrites so that its + output passes the same checks as a hand-written page, and it exits non-zero + instead of publishing something that does not. +- Sidebar placement comes from the sync target directory plus upstream's + `sidebar.order`, so adding a page upstream needs no change here. +- A synced page may carry a bare `https://cli.internetcomputer.org` root even + though the "Never" section bans it in our own pages. That root meta-refreshes + to the newest version, which is what upstream wants for a page also read on + GitHub, and rewriting it would publish text that differs from the source repo. + Leave it. + **Watched** are the repos where a release can silently invalidate a lot of what is published, so [`.sources/upstream.json`](.sources/upstream.json) records the ref the docs are verified against and a weekly workflow opens an issue when one @@ -361,6 +394,9 @@ For design work (CSS, UI, marketing copy), also load `icp-brand-design`. ## Content rules - **Spelling:** "onchain" and "offchain" (no hyphens). "icp-cli" in prose; `icp` in code blocks only. +- **Recipe names vs canister names:** never put both in one label, such as "static-site (certified-assets)". Name the reader's goal in prose, headings, and the sidebar ("a static site"); name the recipe where they type it (`@dfinity/static-site`, in code or when pinning a version); name the canister only where its identity matters (its Candid interface, state-hash verification, or contrasting it with another canister). The distinction matters most for static sites, where the recipe and the canister have different names. +- **Which of the three names to use.** "Frontend canister" is the role, and is the default in prose that is about what the component does (architecture, concepts, the `ic_env` cookie). "Static site" is the task, and is what task-oriented sentences and link text use. "certified-assets" is the canister's identity, for its interface and its guarantees. For the first two, the link goes to `guides/frontends/static-site/overview.md` ("Deploy a static site"), which is the only page that documents how to get one. When you name the **canister**, link to its repo, : that is where its source, interface, and releases live, and it is the exception to "link internally when an internal page exists", which still governs its behavior and its Candid surface. Link once per section, not per mention, since both of these pages are entered through deep links. +- **Put the link on the term, and match it to the target.** Never link part of a compound term: "the asset [canister](...)" splits a name the reader is trying to learn, so either link the whole term or leave it unlinked. Link the goal phrase to the guide that covers it; a recipe name links to its releases or the CLI docs, never to a page about canister behavior. When one of two paths is legacy, check that a link lands on the one the sentence is actually about. - **Internal links:** `.md` extension always, even for `.mdx` targets. Relative paths only — never absolute like `/getting-started/quickstart/`. - **No headings inside `` blocks** — use **bold text** instead. - **Motoko:** use `mo:core` (`mops.one/core`), never `mo:base`. @@ -370,6 +406,7 @@ For design work (CSS, UI, marketing copy), also load `icp-brand-design`. - `guides/` — task-oriented how-to - `references/` — precise lookups, no tutorials - End every page with a `## Next steps` section. +- Synced trees are exempt from the two rules above; see "Source material". ## Linking rules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e46fbf8e..0e159e1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,12 +102,14 @@ The build generates `/llms.txt` and per-page `.md` endpoints from your content. ## Source material -Upstream repos are tracked two ways, because most of them are only ever read to check a fact. +Upstream repos are tracked four ways, because most of them are only ever read to check a fact. -- **Vendored as submodules** (`.sources/motoko`, `internetidentity`, `examples`) — three repos whose content is resolved during the build. **Do not edit files in `.sources/` directly**; they are read-only, and changes go to the upstream repo. Pins are in [`.sources/VERSIONS`](.sources/VERSIONS). -- **Watched, not vendored** — everything else, listed in [`.sources/upstream.json`](.sources/upstream.json) with the ref the docs are verified against. A weekly workflow opens an issue when one of them ships something newer. +- **Vendored as submodules** (`.sources/motoko`, `internetidentity`, `examples`): three repos whose content is resolved during the build. **Do not edit files in `.sources/` directly**; they are read-only, and changes go to the upstream repo. Pins are in [`.sources/VERSIONS`](.sources/VERSIONS). +- **Synced without a submodule**: a repo whose own pages are published here, pinned under `synced` in [`.sources/upstream.json`](.sources/upstream.json). Nothing is resolved at build time, so there is no submodule to hold the pin. Its own weekly workflow opens the bump PR. +- **Watched**: repos where a release can invalidate a lot of published content, listed under `watched` in [`.sources/upstream.json`](.sources/upstream.json) with the ref the docs are verified against. A weekly workflow opens an issue when one of them ships something newer. +- **Reference**: repos the docs draw on too lightly to be worth a weekly issue, listed under `reference`. They carry no pin, so verify against the latest release when you touch a page that relies on one. -Bumping either is a maintainer task — follow [`.agents/upstream-tracking.md`](.agents/upstream-tracking.md) for the procedure. +Each group has its own procedure, and all of them are maintainer tasks: see [`.agents/upstream-tracking.md`](.agents/upstream-tracking.md) for bumping a submodule, moving a synced pin, and closing a release issue. ## Synced content @@ -116,6 +118,7 @@ Some files are auto-synced from other repositories. Currently synced: - `docs/languages/motoko/` — from `caffeinelabs/motoko` +- `docs/guides/frontends/static-site/` (from `dfinity/certified-assets`) - `docs/references/internet-identity-spec.md`, `docs/references/verifiable-credentials-spec.md` — from `dfinity/internet-identity` - `.claude/skills/` (except `icp-brand-design`, `icp-brand-voice`, and `technical-documentation`) — from [skills.internetcomputer.org](https://skills.internetcomputer.org), refreshed on session start and not committed. See AGENTS.md "Skills" for how to consume them outside Claude Code. diff --git a/docs/404.mdx b/docs/404.mdx index dfa8a52b..3b696ab4 100644 --- a/docs/404.mdx +++ b/docs/404.mdx @@ -13,7 +13,7 @@ Pick a guide that matches what you're building. - **[All guides](/guides/)**: Task-oriented how-tos for backends, frontends, auth, testing, and deployment. - **[Backends](/guides/backends/data-persistence/)**: Persist data, make HTTPS outcalls, schedule timers, and generate randomness. -- **[Frontends](/guides/frontends/asset-canister/)**: Serve assets, integrate frameworks, configure custom domains, and certify responses. +- **[Frontends](/guides/frontends/static-site/overview/)**: Host a static site, integrate frameworks, configure custom domains, and certify responses. - **[Authentication](/guides/authentication/internet-identity/)**: Add passwordless login and verifiable user identity with Internet Identity. - **[Chain Fusion](/guides/chain-fusion/bitcoin/)**: Connect canisters to Bitcoin, Ethereum, and Solana. - **[Security](/guides/security/identity-and-access-management/)**: Access control, DoS prevention, and safe upgrade patterns. diff --git a/docs/concepts/certified-data.md b/docs/concepts/certified-data.md index 3ae8703a..a80c4b25 100644 --- a/docs/concepts/certified-data.md +++ b/docs/concepts/certified-data.md @@ -42,7 +42,7 @@ This pattern allows canisters to provide both fast responses (query, no consensu Certified data is used throughout ICP for exactly this reason: - **Certified variables in canisters.** Any canister can certify its state for client verification. See the [Certified variables guide](../guides/backends/certified-variables.md) for how to implement this. -- **Certified assets.** The asset canister uses certified variables to produce certified HTTP responses. When a browser fetches a page served by an ICP canister, the HTTP gateway verifies the certificate before serving the response, so the browser sees only content that was committed through consensus. +- **Certified assets.** A [frontend canister](../guides/frontends/static-site/overview.md) uses certified variables to produce certified HTTP responses. When a browser fetches a page through a verifying HTTP gateway, that gateway checks the certificate before serving the response, so the browser sees only content that was committed through consensus. Whether the check happens is a property of the gateway the URL points at, not of the canister. - **Internet Identity.** The Internet Identity service certifies its delegations, so clients can verify that an authentication delegation is authentic without trusting the individual replica that served the query. ## Relationship to chain-key cryptography diff --git a/docs/concepts/edge-infrastructure.md b/docs/concepts/edge-infrastructure.md index ec4e2350..58a1ea7a 100644 --- a/docs/concepts/edge-infrastructure.md +++ b/docs/concepts/edge-infrastructure.md @@ -58,7 +58,7 @@ ICP solves this through **asset certification**: a mechanism for canisters to pr - When a subnet responds to a message, the response includes a certificate chain: the subnet's signature on the response and the NNS certificate on the subnet's key. Any client can verify this chain using only the ICP network's public key. - Because generating a subnet certificate requires agreement from at least two thirds of the subnet's nodes (using [chain-key cryptography](chain-key-cryptography.md#threshold-bls-signatures)), a certified response represents network-level consensus, not a single node's assertion. - Query calls do not go through consensus and are not automatically certified. To serve certified query responses, canisters use **certified variables**: the canister stores a certificate for a piece of data in the replicated state during an update call. Any user can later retrieve both the data and its certificate via a query call and verify the certificate independently. -- For web assets (HTML, CSS, JavaScript, images), canisters can certify all assets upfront. The asset canister provided by DFINITY handles this automatically: developers specify a folder of assets and the asset canister manages and certifies them. +- For web assets (HTML, CSS, JavaScript, images), canisters can certify all assets upfront. The [frontend canisters](../guides/frontends/static-site/overview.md) DFINITY provides handle this automatically: developers specify a folder of assets, and the canister manages and certifies them. When the HTTP gateway receives a canister response that includes a certificate, it verifies the certificate before passing the response to the client. This is what makes ICP-hosted web content verifiable end-to-end without trusting any single node. diff --git a/docs/concepts/security.md b/docs/concepts/security.md index e45d75fa..d6afbdef 100644 --- a/docs/concepts/security.md +++ b/docs/concepts/security.md @@ -61,7 +61,7 @@ The following threats are your responsibility to mitigate: Every update method is publicly callable. If you do not check the caller, anyone can invoke admin functions, drain funds, or corrupt state. The anonymous principal (`2vxsx-fae`) is a particularly common gap: it must be explicitly rejected in any authenticated endpoint, because otherwise it acts as a shared identity that anyone can use. -See [Access management](../guides/security/identity-and-access-management.md#reject-anonymous-callers) for implementation patterns. +See [Access management](../guides/security/identity-and-access-management.md#disallow-the-anonymous-principal-in-authenticated-calls) for implementation patterns. ### Reentrancy and async interleaving diff --git a/docs/concepts/vetkeys.md b/docs/concepts/vetkeys.md index 2dea117c..06f4e64c 100644 --- a/docs/concepts/vetkeys.md +++ b/docs/concepts/vetkeys.md @@ -71,7 +71,7 @@ The only supported curve is `bls12_381_g2`. Two key names are available: | `test_key_1` | Local + mainnet | Development and testing | 10,000,000,000 | | `key_1` | Mainnet only | Production | 26,153,846,153 | -`vetkd_public_key` carries no cycle cost. `vetkd_derive_key` consumes cycles at the rates above. If a canister may be blackholed or called by other canisters, send more cycles than the advertised cost: unused cycles are refunded, and this ensures calls succeed if the subnet grows in size. See [Cycle costs](../references/cycle-costs.md#vetkd) for USD equivalents and full details. +`vetkd_public_key` carries no cycle cost. `vetkd_derive_key` consumes cycles at the rates above. If a canister may be blackholed or called by other canisters, send more cycles than the advertised cost: unused cycles are refunded, and this ensures calls succeed if the subnet grows in size. See [Cycle costs](../references/cycle-costs.md#vetkeys) for USD equivalents and full details. ## Use cases diff --git a/docs/developer-tools/index.md b/docs/developer-tools/index.md index 3ccac0b7..321f57c2 100644 --- a/docs/developer-tools/index.md +++ b/docs/developer-tools/index.md @@ -14,7 +14,7 @@ Developer tools are used to create, manage, and interact with canisters. ICP pro `icp-cli` is the primary tool for building and deploying applications on the Internet Computer. It manages the full development lifecycle: creating projects, building canisters, deploying to local or mainnet environments, managing identities, and handling cycles and ICP tokens. Key features: -- **Recipes**: reusable, versioned build templates for Rust, Motoko, and asset canisters +- **Recipes**: reusable, versioned build templates for Rust, Motoko, and static sites - **Environments**: named deployment targets that combine a network, canister set, and settings (e.g., local, staging, production) - **Project scaffolding**: `icp new` bootstraps new projects from official templates diff --git a/docs/getting-started/app-architecture.md b/docs/getting-started/app-architecture.md index d6e3b115..eb211c53 100644 --- a/docs/getting-started/app-architecture.md +++ b/docs/getting-started/app-architecture.md @@ -14,7 +14,7 @@ An application on the Internet Computer typically consists of one or more [canis Most ICP applications start with two canisters: - **Backend canister**: contains your application logic and data. You write it in Motoko or Rust (the official CDKs). Community-supported languages like TypeScript and Python are also available: see [Languages](../languages/index.md). Your code is compiled locally to WebAssembly and executed by the network. -- **Frontend (asset) canister**: serves your web UI. It is a standard canister that hosts static files (HTML, CSS, JavaScript, images) and delivers them over HTTP. +- **Frontend canister**: serves your web UI. It is a standard canister that hosts static files (HTML, CSS, JavaScript, images) and delivers them over HTTP. See [Deploy a static site](../guides/frontends/static-site/overview.md). When a user opens your application in a browser: @@ -32,7 +32,7 @@ This flow replaces the traditional web stack. There is no separate web server, a |---------|-------------------|-----------------| | **Compute** | Application server (Node, Django, etc.) | [Backend canister](../concepts/canisters.md) (Wasm) | | **Storage** | Database (Postgres, MongoDB, etc.) | [Canister stable memory](../concepts/orthogonal-persistence.md) (up to 500 GiB) | -| **Frontend hosting** | CDN + static file server | [Asset canister](../guides/frontends/asset-canister.md) | +| **Frontend hosting** | CDN + static file server | [Frontend canister](../guides/frontends/static-site/overview.md) (certified responses) | | **Authentication** | OAuth provider or custom auth | [Internet Identity](../guides/authentication/internet-identity.md) (passkey or OAuth)\* | | **Scheduled tasks** | Cron jobs, worker queues | [Canister timers](../concepts/timers.md) | | **External API calls** | Server-side HTTP requests | [HTTPS outcalls](../concepts/https-outcalls.md) | @@ -97,11 +97,11 @@ For small to medium datasets, stable memory is straightforward. For applications ## Frontend options -Not every ICP application needs the default asset canister. Your options: +Not every ICP application needs a frontend canister. Your options: -- **Asset canister**: the standard approach. Deploy your built frontend (React, Svelte, vanilla JS, etc.) to an asset canister that serves it over HTTP. See [Asset canister](../guides/frontends/asset-canister.md). +- **Static site**: the standard approach. Deploy your built frontend (React, Svelte, vanilla JS, etc.) to a canister that serves it over HTTP with every response certified. See [Hosting a static site](../guides/frontends/static-site/overview.md). Existing projects on the older recipe are covered by [Asset canister (legacy)](../guides/frontends/asset-canister.md). - **Framework-specific canister**: use a framework like Juno that provides a more opinionated hosting solution on ICP. -- **Offchain frontend**: host your frontend on traditional infrastructure (Vercel, Netlify, etc.) and call ICP canisters from JavaScript using [`@icp-sdk/core/agent`](https://js.icp.build/core/latest/libs/agent). Useful during migration or when you need features that asset canisters don't support. +- **Offchain frontend**: host your frontend on traditional infrastructure (Vercel, Netlify, etc.) and call ICP canisters from JavaScript using [`@icp-sdk/core/agent`](https://js.icp.build/core/latest/libs/agent). Useful during migration or when you need features a static file host cannot provide, such as server-side rendering. - **No frontend**: backend-only canisters that expose a Candid API for other canisters or CLI tools to call. ## Choosing an architecture @@ -110,7 +110,7 @@ Start with a [single canister](#single-canister): it is the right choice for mos | Question | If yes | If no | |----------|--------|-------| -| Does the app have a web UI? | Add an [asset canister](#frontend-options) | Backend-only canister | +| Does the app have a web UI? | Add a [frontend canister](#frontend-options) | Backend-only canister | | Do you need separation of concerns or hit platform limits? | [Canister-per-service](#canister-per-service) | Stay with a single canister | | Do you need to scale beyond one subnet? | [Canister-per-subnet](#canister-per-subnet) | Stay on one subnet | | Is user sovereignty over data a core requirement and are you prepared for high dev cost? | [Canister-per-user](#canister-per-user) (experimental) | None of the above | @@ -121,7 +121,7 @@ Start with the simplest architecture that meets your requirements. You can alway - [Choose your path](choose-your-path.md): pick a development track based on what you want to build - [Inter-canister calls](../guides/canister-calls/inter-canister-calls.md): inter-canister communication patterns -- [Asset canister](../guides/frontends/asset-canister.md): frontend deployment +- [Hosting a static site](../guides/frontends/static-site/overview.md): frontend deployment - [Canisters](../concepts/canisters.md): canister internals diff --git a/docs/getting-started/choose-your-path.md b/docs/getting-started/choose-your-path.md index 669b1178..69bcaf0c 100644 --- a/docs/getting-started/choose-your-path.md +++ b/docs/getting-started/choose-your-path.md @@ -46,7 +46,7 @@ This is where most developers start after the quickstart. The backend guides cov ICP can serve web assets directly from canisters, giving you a tamperproof application with no external hosting required. -**Start with:** [Asset canister](../guides/frontends/asset-canister.md): deploy a frontend alongside your backend canister. +**Start with:** [Hosting a static site](../guides/frontends/static-site/overview.md): deploy a frontend alongside your backend canister. **Then explore:** diff --git a/docs/getting-started/project-structure.mdx b/docs/getting-started/project-structure.mdx index 0aad7e0a..55ad65ac 100644 --- a/docs/getting-started/project-structure.mdx +++ b/docs/getting-started/project-structure.mdx @@ -30,11 +30,11 @@ my-project/ │ └── src/ │ └── lib.rs # Canister source code ├── frontend/ -│ ├── canister.yaml # Asset canister configuration +│ ├── canister.yaml # Frontend canister configuration │ ├── package.json # Node dependencies (binding generation) │ └── app/ # Frontend application (React + Vite) │ ├── src/ -│ ├── dist/ # Built assets (uploaded to the asset canister) ← created by icp deploy +│ ├── dist/ # Built assets (uploaded to the frontend canister) ← created by icp deploy │ └── package.json └── .gitignore # Ignores .icp/cache/ (but tracks .icp/data/) ``` @@ -118,12 +118,12 @@ The `package` parameter (the Cargo package to build) defaults to the canister `n ### Frontend canister -The frontend uses the `@dfinity/asset-canister` recipe, which builds the frontend app and uploads the output to an asset canister: +The frontend uses the `@dfinity/static-site` recipe, which builds the frontend app and uploads the output to a canister that serves it with response certification: ```yaml name: frontend recipe: - type: "@dfinity/asset-canister@v2.3.0" + type: "@dfinity/static-site@v0.3.3" configuration: build: - npm install @@ -132,20 +132,23 @@ recipe: dir: app/dist ``` -The `build` commands run in order: install dependencies, generate TypeScript bindings from the backend's Candid file, then build the Vite app. The `dir` field tells the recipe which directory to upload to the asset canister. +The `build` commands run in order: install dependencies, generate TypeScript bindings from the backend's Candid file, then build the Vite app. The `dir` field tells the recipe which directory to upload. + +The recipe pins a matched canister and sync-plugin pair, so the recipe version is the canister version. See [Hosting a static site](../guides/frontends/static-site/overview.md) for the configuration fields, and [Asset canister (legacy)](../guides/frontends/asset-canister.md) for the older `@dfinity/asset-canister` recipe that existing projects may still use. ## Recipes Recipes are reusable build templates that expand into full canister build and sync steps. Instead of writing shell commands from scratch, you reference a recipe with a version pin and pass configuration parameters. -The four official recipes cover the most common patterns: +The official recipes cover the most common patterns: | Recipe | Purpose | |--------|---------| | `@dfinity/rust@` | Rust canisters with Cargo | | `@dfinity/motoko@` | Motoko canisters | -| `@dfinity/asset-canister@` | Asset canisters for static files | +| `@dfinity/static-site@` | Static files, served with response certification | | `@dfinity/prebuilt@` | Pre-compiled WASM files | +| `@dfinity/asset-canister@` | Static files on the legacy asset canister | To see what a recipe expands to after template rendering: @@ -192,7 +195,7 @@ During deployment: ### Frontend reads backend IDs -The asset canister exposes injected canister IDs through a cookie called `ic_env`. Your frontend JavaScript reads this cookie to discover backend canister IDs at runtime, with no code changes needed between environments: +The frontend canister exposes injected canister IDs through a cookie called `ic_env`. Your frontend JavaScript reads this cookie to discover backend canister IDs at runtime, with no code changes needed between environments: ```typescript import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env"; @@ -252,7 +255,7 @@ For a deep dive on binding generation, see [Binding generation](../guides/canist - [What next?](choose-your-path.md): pick a development path based on what you want to build - [Binding generation](../guides/canister-calls/candid.md#binding-generation): deep dive on generating type-safe client code -- [Asset canister](../guides/frontends/asset-canister.md): how the frontend recipe and asset upload work +- [Hosting a static site](../guides/frontends/static-site/overview.md): how the frontend recipe and asset upload work - [Canister lifecycle](../guides/canister-management/lifecycle.md): build, deploy, upgrade, and manage canisters - [icp-cli reference](https://cli.internetcomputer.org/1.5/reference/cli): full CLI and configuration documentation diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index cc8d3264..8dd8fe58 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -109,7 +109,7 @@ The hello-world template deploys two [canisters](../concepts/canisters.md) that 1. **Backend canister**: Your Motoko code compiled to WebAssembly. It exposes a `greet` function through a [Candid](../references/candid-spec.md) interface, making it callable from any client. -2. **Frontend canister**: An asset canister that serves your React app. It automatically provides the backend's canister ID to your frontend code via a cookie, so the two canisters can communicate without manual configuration. +2. **Frontend canister**: Serves your React app as a [static site](../guides/frontends/static-site/overview.md). It automatically provides the backend's canister ID to your frontend code via a cookie, so the two canisters can communicate without manual configuration. The `icp.yaml` file ties everything together: diff --git a/docs/guides/authentication/internet-identity.mdx b/docs/guides/authentication/internet-identity.mdx index 6b89724b..e6533505 100644 --- a/docs/guides/authentication/internet-identity.mdx +++ b/docs/guides/authentication/internet-identity.mdx @@ -57,7 +57,7 @@ import { AuthClient } from "@icp-sdk/auth/client"; import { HttpAgent, Actor } from "@icp-sdk/core/agent"; import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env"; -// Read the ic_env cookie set by the asset canister or Vite dev server. +// Read the ic_env cookie set by the frontend canister or Vite dev server. // Contains IC_ROOT_KEY and canister IDs: works in both local and production without // environment branching. Available in browser contexts only; see note below for Node.js. const canisterEnv = safeGetCanisterEnv(); @@ -146,7 +146,7 @@ async function createAuthenticatedActor(identity, canisterId, idlFactory) { ``` :::note[Node.js environments] -`safeGetCanisterEnv()` reads the `ic_env` cookie set by the asset canister or Vite dev server (it only works in browser contexts. For Node.js scripts or tests connecting to a **local** replica, create the agent normally and call `await agent.fetchRootKey()` explicitly after creation. Never call `fetchRootKey()` against a mainnet endpoint) on mainnet the root key is pre-trusted, and fetching it at runtime exposes a man-in-the-middle risk. +`safeGetCanisterEnv()` reads the `ic_env` cookie set by the frontend canister or Vite dev server (it only works in browser contexts. For Node.js scripts or tests connecting to a **local** replica, create the agent normally and call `await agent.fetchRootKey()` explicitly after creation. Never call `fetchRootKey()` against a mainnet endpoint) on mainnet the root key is pre-trusted, and fetching it at runtime exposes a man-in-the-middle risk. ::: ### Requesting identity attributes @@ -571,7 +571,17 @@ To keep principals consistent across your own custom domains, configure **altern A maximum of 100 alternative origins can be listed. No trailing slashes or paths. -2. **Configure the asset canister** to serve the `.well-known` directory. Add an `.ic-assets.json5` in your frontend source: +2. **Serve it with the right content type and CORS headers.** II reads the file cross-origin, and nothing is set for you. + + On a [static site](../frontends/static-site/overview.md), `.well-known/` is uploaded automatically; declare the two headers in a `_headers` file at the root of your build directory: + + ```text + /.well-known/ii-alternative-origins + Content-Type: application/json + Access-Control-Allow-Origin: * + ``` + + On the [legacy asset canister](../frontends/asset-canister.md), the directory has to be un-ignored as well, in `.ic-assets.json5`: ```json [ @@ -626,7 +636,20 @@ All three fields are optional, and unknown fields are ignored, so a document sta - Only the shape of `logo` (a non-empty URL on the document's own origin) is part of the validation above. Once it passes, a logo that cannot be fetched or decoded, or that breaks the content type, size, or dimension rules, costs you the logo alone: the name and description still render. Fetching a second resource can fail transiently, so that is treated differently from a mistake in the document itself. - The document must not exceed 8 KiB, must be answered with `200`, and must not redirect. II requests it without credentials and gives up after 10 seconds. -Both the document and the logo are read cross-origin, so they need CORS headers. Extend the `.ic-assets.json5` shown under [Alternative origins](#alternative-origins) with an entry for each: +Both the document and the logo are read cross-origin, so they need CORS headers too. Extend the configuration shown under [Alternative origins](#alternative-origins) with an entry for each. + +With `_headers`: + +```text +/.well-known/ii-app-metadata + Content-Type: application/json + Access-Control-Allow-Origin: * + +/logo.png + Access-Control-Allow-Origin: * +``` + +With `.ic-assets.json5`: ```json [ diff --git a/docs/guides/backends/certified-variables.md b/docs/guides/backends/certified-variables.md index da8e424f..0b07be67 100644 --- a/docs/guides/backends/certified-variables.md +++ b/docs/guides/backends/certified-variables.md @@ -353,14 +353,14 @@ icp canister call backend get '("key")' ## HTTP asset certification -For canisters that serve HTTP responses directly through the HTTP Gateway, responses must be certified so the boundary node can verify them. This is a separate protocol built on top of certified data, handled by the `ic-http-certification` crate. For frontend assets (HTML, CSS, JS), use the asset canister, which handles HTTP certification automatically. +For canisters that serve HTTP responses directly through the HTTP Gateway, responses must be certified so the boundary node can verify them. This is a separate protocol built on top of certified data, handled by the `ic-http-certification` crate. For frontend assets (HTML, CSS, JS), [host a static site](../frontends/static-site/overview.md) instead, which handles HTTP certification automatically. -See [Frontend certification](../../guides/frontends/certification.md) for the asset canister and HTTP certification workflow. +See [Frontend certification](../../guides/frontends/certification.md) for how the frontend canisters certify responses, and what a custom HTTP canister has to do itself. ## Next steps - [Security concepts](../../concepts/security.md): why query integrity matters and when to use certified variables vs replicated queries -- [Frontend certification](../../guides/frontends/certification.md): HTTP asset certification for the asset canister +- [Frontend certification](../../guides/frontends/certification.md): HTTP asset certification for a frontend canister - [IC Interface Specification: Certified Data](../../references/ic-interface-spec/canister-interface.md#system-api-certified-data): the certified data system API - [IC Interface Specification: Certification](../../references/ic-interface-spec/certification.md): certificate format and delegation diff --git a/docs/guides/canister-calls/calling-from-clients.md b/docs/guides/canister-calls/calling-from-clients.md index 3341c3f1..dba7a59f 100644 --- a/docs/guides/canister-calls/calling-from-clients.md +++ b/docs/guides/canister-calls/calling-from-clients.md @@ -79,13 +79,13 @@ For Vite projects, use the Vite plugin to regenerate bindings automatically duri ### Creating an actor (browser) -In a browser frontend served by an asset canister, read the canister ID from the environment cookie that icp-cli injects at deploy time: +In a browser frontend served by a [frontend canister](../frontends/static-site/overview.md), read the canister ID from the environment cookie that icp-cli injects at deploy time: ```typescript import { createActor } from "./backend/api/backend"; import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env"; -// Declare the environment variables your asset canister exposes. +// Declare the environment variables your frontend canister exposes. // icp-cli injects PUBLIC_CANISTER_ID: for every canister in the project. interface CanisterEnv { readonly "PUBLIC_CANISTER_ID:backend": string; @@ -105,7 +105,7 @@ const actor = createActor(canisterId, { }); ``` -`getCanisterEnv` reads the `ic_env` cookie that the asset canister sets automatically. See [Canister discovery](#canister-discovery) below for how this works. +`getCanisterEnv` reads the `ic_env` cookie that the frontend canister sets automatically. See [Canister discovery](#canister-discovery) below for how this works. ### Creating an actor (Node.js) @@ -247,12 +247,12 @@ PUBLIC_CANISTER_ID:frontend → bd3sg-teaaa-aaaaa-qaaba-cai ### Frontend: reading the cookie -The asset canister exposes these variables via an `ic_env` cookie, along with the network's root key (`IC_ROOT_KEY`). Use `getCanisterEnv` from `@icp-sdk/core` to read the cookie: +The frontend canister exposes these variables via an `ic_env` cookie, along with the network's root key (`IC_ROOT_KEY`). Use `getCanisterEnv` from `@icp-sdk/core` to read the cookie: ```typescript import { getCanisterEnv } from "@icp-sdk/core/agent/canister-env"; -// Declare the environment variables your asset canister exposes. +// Declare the environment variables your frontend canister exposes. // icp-cli injects PUBLIC_CANISTER_ID: for every canister in the project. interface CanisterEnv { readonly "PUBLIC_CANISTER_ID:backend": string; @@ -267,7 +267,7 @@ This works identically on local networks and mainnet without code changes. ### Local development with a dev server -During development, your dev server runs outside the asset canister and the `ic_env` cookie is not set automatically. Simulate it by configuring your dev server to inject the cookie. With Vite: +During development, your dev server runs outside the frontend canister and the `ic_env` cookie is not set automatically. Simulate it by configuring your dev server to inject the cookie. With Vite: ```typescript // vite.config.ts @@ -316,6 +316,6 @@ const agent = await HttpAgent.create({ - [Candid and binding generation](candid.md): generate typed clients from `.did` files - [Inter-canister calls](inter-canister-calls.md): canister-to-canister calls from within the IC - [Internet Identity](../authentication/internet-identity.md): adding user authentication to offchain calls -- [Asset canister](../frontends/asset-canister.md): deploying the frontend that makes these calls +- [Hosting a static site](../frontends/static-site/overview.md): deploying the frontend that makes these calls diff --git a/docs/guides/canister-management/troubleshooting.md b/docs/guides/canister-management/troubleshooting.md index 4746e232..5a7b55d5 100644 --- a/docs/guides/canister-management/troubleshooting.md +++ b/docs/guides/canister-management/troubleshooting.md @@ -89,7 +89,7 @@ A frontend deployed to the mainnet returns a blank screen and the browser consol **Check for client-side firewall or proxy interference.** Some corporate firewalls and browser extensions block requests to `*.icp.net` domains. If the frontend loads on a different network, a firewall or proxy is the likely cause. -**Verify the asset canister is deployed correctly.** Run `icp canister status -e ic` and confirm the module hash is populated. If the hash is `None`, the canister exists but has no code installed. +**Verify the frontend canister is deployed correctly.** Run `icp canister status -e ic` and confirm the module hash is populated. If the hash is `None`, the canister exists but has no code installed. ## Problem: Frontend violates Content Security Policy @@ -100,19 +100,21 @@ Refused to connect to 'https://ic0.app/api/v2/canister//read_state' because it violates the document's Content Security Policy. ``` -This happens when the asset canister was installed without the current security headers, or when the CSP headers have drifted out of sync with the deployed code. +**On a [static site](../frontends/static-site/overview.md)** the canister adds no default security headers and no CSP (it manages only what it needs to serve and certify: `Content-Type`, `ETag`, the certification headers, and its `ic_env` cookie), so a CSP error means your own policy is missing or not matching. `_headers` patterns match the file that was served, not the URL the visitor asked for, so a rule written against a client route (`/dashboard/*`) matches nothing. Write it against the file instead (`/index.html` or `/*.html`), redeploy, and check the response: -**Fix:** reinstall the asset canister to refresh the CSP headers: +```bash +curl -sI https://.icp.net/ | grep -i content-security-policy +``` + +**On the legacy asset canister** this happens when the canister was installed without the current security headers, or when they have drifted out of sync with the deployed code. Reinstall to refresh them: ```bash icp deploy --mode reinstall -e ic ``` -After reinstall, the asset canister serves updated security headers on every request. - ## Problem: Security policy warning "This project does not define a security policy for some assets" -This warning appears when your project includes an asset canister but `.ic-assets.json5` does not define a security policy. +This warning comes from the legacy asset canister when `.ic-assets.json5` does not define a security policy. A static site does not warn, because the canister never adds headers for you: whatever you declare in `_headers` is what gets served. **Fix:** add a security policy to `.ic-assets.json5` in your frontend asset directory: @@ -139,7 +141,7 @@ The `standard` policy applies a default Content Security Policy and security hea ] ``` -See [Asset canister](../frontends/asset-canister.md#ic-assets-json5) for the full `.ic-assets.json5` reference. +See [Asset canister (legacy)](../frontends/asset-canister.md#ic-assetsjson5) for the full `.ic-assets.json5` reference, and [Custom headers](../frontends/static-site/headers.md) for the static site equivalent. ## Problem: Rust canister fails to install with "invalid import section" @@ -167,7 +169,8 @@ Caused by: Wasm module has an invalid import section - [Canister settings](./settings.md): compute allocation, memory allocation, and freezing threshold - [Subnet selection](./subnet-selection.md): choosing a subnet when latency is a deployment constraint - [Optimization](./optimization.md): reducing Wasm binary size and cycle costs -- [Asset canister](../frontends/asset-canister.md): frontend deployment and `.ic-assets.json5` configuration +- [Hosting a static site](../frontends/static-site/overview.md): frontend deployment, headers, and redirects +- [Asset canister (legacy)](../frontends/asset-canister.md): `.ic-assets.json5` configuration - [Certified variables](../backends/certified-variables.md): tamperproof query responses diff --git a/docs/guides/frontends/asset-canister.md b/docs/guides/frontends/asset-canister.md index fa50b32f..c97e82c3 100644 --- a/docs/guides/frontends/asset-canister.md +++ b/docs/guides/frontends/asset-canister.md @@ -1,11 +1,17 @@ --- -title: "Asset canister" -description: "Deploy and serve frontend assets from an ICP canister with SPA routing, canister discovery, programmatic uploads, and security configuration" +title: "Asset canister (legacy)" +description: "Maintain a frontend on the legacy @dfinity/asset-canister recipe, and migrate it to a static site" sidebar: - order: 1 + order: 6 --- -The asset [canister](../../concepts/canisters.md) hosts static files (HTML, CSS, JavaScript, images) directly on the Internet Computer. It serves web frontends over HTTP, with responses certified by the [subnet](../../concepts/network-overview.md#subnets) so that [HTTP gateways](../../concepts/edge-infrastructure.md#http-gateways) and browsers can verify that content was served tamperproof by the network rather than a centralized server. +The asset canister hosts static files (HTML, CSS, JavaScript, images) directly on the Internet Computer. It serves web frontends over HTTP, with responses certified by the [subnet](../../concepts/network-overview.md#subnets) so that [HTTP gateways](../../concepts/edge-infrastructure.md#http-gateways) and browsers can verify that content was served tamperproof by the network rather than a centralized server. + +:::caution[This is the legacy path, with one exception] +New projects should [host a static site](static-site/overview.md) instead. That is a different canister with a different configuration format, and it is what the project templates ship. This page is for projects already running the `@dfinity/asset-canister` recipe; to move one over, see [Migrate to a static site](#migrate-to-a-static-site). + +**The exception is a frontend governed by an SNS.** Proposal-gated asset updates need this canister; see [When to stay on this canister](#when-to-stay-on-this-canister). +::: This guide covers configuring the asset canister recipe in `icp.yaml`, deploying frontends, configuring SPA routing with `.ic-assets.json5`, connecting frontends to backend canisters, and uploading assets programmatically. @@ -199,7 +205,9 @@ See the [frontend-environment-variables example](https://github.com/dfinity/icp- ## Programmatic uploads with @icp-sdk/canisters -For uploading files from code rather than through `icp deploy`, use the `AssetManager` from `@icp-sdk/canisters`: +Uploading assets from application code is not a recommended pattern, and it exists only on this canister: [certified-assets](https://github.com/dfinity/certified-assets) has no per-asset write endpoint and will not grow one, because finalizing a sync recomputes the state hash that makes a build provable. An app that stores user-generated content should keep it in a canister of its own and leave its frontend a published build. + +It is documented here for projects already doing it. The `AssetManager` from `@icp-sdk/canisters` uploads files from code rather than through `icp deploy`: ```javascript import { AssetManager } from "@icp-sdk/canisters/assets"; @@ -318,12 +326,64 @@ icp canister call frontend http_request '(record { **Content types are wrong for programmatic uploads.** The asset canister infers content types from file extensions for files uploaded via `icp deploy`. When uploading programmatically with `AssetManager`, pass the `contentType` option explicitly. +## When to stay on this canister + +One case still requires the asset canister: **a frontend whose updates are governed by an SNS.** That workflow depends on staging a batch and having the governance canister commit it after a vote (`propose_commit_batch`, an `ExecuteGenericNervousSystemFunction` proposal, then `commit_proposed_batch`), and on the `Prepare`/`Commit` permission split that keeps developers from committing directly. + +[certified-assets](https://github.com/dfinity/certified-assets) has no equivalent. Its interface has no proposal-gated commit and no staged-batch evidence to vote on, and its authorization model is controllers plus a flat set of authorized syncers, all of whom can sync at will. An SNS could hold the controller, but there would be nothing for token holders to approve. So if community-governed frontend updates are a requirement, keep the frontend here for now and see [Asset canister updates](../governance/managing.md#asset-canister-updates). + +Everything else should migrate. + +## Migrate to a static site + +[Hosting a static site](static-site/overview.md) means deploying a different canister, [certified-assets](https://github.com/dfinity/certified-assets), with its own configuration format. The `@dfinity/static-site` recipe replaces `@dfinity/asset-canister` in your configuration. Migrating buys automatic clean URLs, [access protection](static-site/access-protection.md) for private and preview sites, and a [reproducible state hash](static-site/verifying-contents.md) that lets anyone prove the canister serves exactly a known build. + +### You cannot upgrade in place + +The two canisters have unrelated Candid interfaces, so repointing the recipe and running a plain `icp deploy` stops at the pre-install compatibility check with `Candid interface compatibility check failed`. Nothing is installed and the running canister is untouched. Two ways forward: + +- **A new canister.** Add a new entry with the `@dfinity/static-site` recipe and deploy it. You get a new canister ID, so any custom domain registration and hardcoded ID has to be updated. +- **A reinstall, keeping the canister ID.** Point the existing canister's recipe at `@dfinity/static-site` and run `icp deploy --mode reinstall frontend -e ic`. Reinstall skips the Candid check, replaces the wasm, and discards all canister state, after which the sync plugin uploads the whole directory again. The canister ID and its URL survive. + +Do not force the upgrade through with `--yes`. That skips the compatibility check and installs onto stable memory the certified-assets canister cannot read, which leaves a live canister serving nothing. + +### Configuration mapping + +Delete `.ic-assets.json5` and split its concerns into `_headers` and `_redirects` at the root of your build directory: + +| `.ic-assets.json5` | certified-assets | +|---|---| +| `enable_aliasing: true` (SPA fallback) | `/* /index.html 200` in [`_redirects`](static-site/redirects.md) | +| `headers: { ... }` | a block in [`_headers`](static-site/headers.md), matched against the file path rather than the visitor's URL | +| `security_policy: "standard"` | no default: write the security headers yourself | +| `allow_raw_access: false` | no equivalent, [by design](static-site/how-it-works.md#the-raw-hosts-skip-verification) | +| `{ match: ".well-known", ignore: false }` | not needed, `.well-known/` is uploaded automatically | +| `**` and `?` glob patterns | a single `*` wildcard, trailing `/*` for a subtree | + +Also drop any `configuration.version` field: with `@dfinity/static-site` the recipe version is the canister version. + +### Uploads and permissions change shape + +`AssetManager` from `@icp-sdk/canisters/assets` targets this canister only and stops working. certified-assets has no per-file write endpoint: uploads happen in one exclusive sync session, and the final call recomputes the site's state hash, which is what makes a build provable. Runtime writes would make that hash drift from any published build, so the two goals are incompatible rather than merely unimplemented. + +If your app stores user-generated content, keep serving the frontend as a static site and store uploads in a separate canister that the frontend calls. Mixing a mutable file store into your deploy target means every user upload changes what your site is. + +The three upload roles (Prepare, Commit, ManagePermissions) collapse to controllers plus a flat set of authorized syncers, so re-grant any CI principal after migrating: + +```bash +icp canister call frontend authorize '(principal "")' -e ic +``` + +### What stays the same + +The `ic_env` cookie is served on HTML responses by both canisters, so frontend code reading canister IDs or the root key needs no change. The `build`, `presync`, and `metadata` recipe fields behave the same way, and the mainnet URL is still `https://.icp.net`. + ## Next steps -- [Framework integration](frameworks.md): set up React, Svelte, or Vue with the asset canister +- [Hosting a static site](static-site/overview.md): the recommended path for new frontends +- [Framework integration](frameworks.md): set up React, Svelte, or Vue with your frontend canister - [Custom domains](custom-domains.md): serve your frontend from your own domain -- [Response certification](certification.md): verify that asset canister responses are authentic +- [Response certification](certification.md): verify that responses are authentic - [Authentication with Internet Identity](../authentication/internet-identity.md): add user login to your frontend -- [photo-storage example](https://github.com/dfinity/examples/tree/master/hosting/photo-storage): programmatic uploads with AssetManager - + diff --git a/docs/guides/frontends/certification.md b/docs/guides/frontends/certification.md index a94b3c3a..07882173 100644 --- a/docs/guides/frontends/certification.md +++ b/docs/guides/frontends/certification.md @@ -7,11 +7,11 @@ sidebar: Query responses on ICP are answered by a single replica without going through consensus. A malicious or faulty replica could return fabricated data. **Response certification** solves this: canisters commit a cryptographic hash to the subnet's certified state, and query responses include a certificate signed by the subnet's threshold BLS key. [HTTP gateways](../../concepts/edge-infrastructure.md#http-gateways) ([boundary nodes](../../concepts/edge-infrastructure.md#api-boundary-nodes)) verify every response automatically, so users are protected without any extra client-side code: as long as the canister certifies its responses. -This guide explains how certification works at the HTTP layer, what the asset canister does automatically, when you need custom certification, and how to verify certificates client-side. +This guide explains how certification works at the HTTP layer, what each frontend recipe does automatically, when you need custom certification, and how to verify certificates client-side. ## How HTTP response certification works -The asset canister implements **HTTP certification v2**, a protocol on top of certified data: +Both frontend recipes implement **HTTP certification v2**, a protocol on top of certified data: 1. **Certification setup (update call)**: when an asset is uploaded, the canister inserts its path, response headers, and body hash into a Merkle tree and commits the tree's root hash via `certified_data_set`. The subnet includes this root hash in its certified state each consensus round. @@ -42,14 +42,18 @@ The browser receives only responses that have passed this check. Because verific ## Certified vs uncertified access -The asset canister supports two serving modes: +Through the standard ICP gateway, a canister that serves HTTP is reachable on two kinds of hostname: one where the gateway verifies the response certificate before forwarding it, and a `raw` one where it forwards the response without checking. Which hostnames a gateway exposes, and which of them verify, is a property of how that gateway is deployed rather than of the canister: | Domain | Certification | Notes | |--------|--------------|-------| -| `.icp.net` | Verified | Boundary node checks every response | -| `.raw.icp.net` | None | Responses not verified: use only when necessary | +| `.icp.net` | Verified | The gateway checks the proof on every response | +| `.raw.icp.net` | None | The canister still attaches the certificate; the gateway discards it | -Raw access is enabled by default. Disable it in `.ic-assets.json5` for any assets that must not be served unverified: +What you can do about the raw host depends on which canister you deployed. + +**Static site.** The canister certifies every response and accepts only version 2 of the certification protocol, so nothing it serves is uncertified. There is deliberately no raw-access switch: the canister's only clue about the hostname is the `Host` header, which the client supplies and nothing authenticates, and which hostnames verify is a property of how a given gateway is deployed rather than anything the gateway protocol defines. Verification is the gateway's job, so picking the gateway is the trust decision. Link to a verifying host, and treat a raw URL as a debugging tool rather than a way to serve or visit a site. See [Who verifies the certificate](static-site/how-it-works.md#who-verifies-the-certificate). + +**Asset canister (legacy).** Raw access is enabled by default, and the canister can refuse it. Disable it in `.ic-assets.json5` for any assets that must not be served unverified: ```json5 [ @@ -62,21 +66,17 @@ Raw access is enabled by default. Disable it in `.ic-assets.json5` for any asset With `allow_raw_access` set to `false`, requests to the `raw.icp.net` domain are redirected to the certified domain automatically. -## What the asset canister handles automatically +## What each recipe certifies automatically -When you deploy a frontend with `icp deploy`, the asset canister: +Neither recipe needs certification code from you. What differs is how much of the response is covered. -- Inserts every uploaded file into the HTTP certification tree -- Sets the certified root hash after each sync -- Returns the correct `IC-Certificate` and `IC-Certificate-Expression` headers on every `http_request` query -- Updates certification when files change on subsequent deploys -- Certifies `Content-Type` and any headers specified in `.ic-assets.json5` +**Static site.** Certifies every response it serves, including status code, body, and the headers you declare in [`_headers`](static-site/headers.md). There is no way to turn certification off and no uncertified header path, which is why redirects and headers are limited to what can be enumerated ahead of time, and why the sync plugin rejects [reserved headers](static-site/headers.md#reserved-headers) at deploy time instead of serving a value it cannot certify. Note that it adds no default headers at all: no `Cache-Control`, no CSP. Anything you want certified, you declare. -You do not need to write any certification code to use the standard asset canister workflow. See [Asset canister](asset-canister.md) for the deployment configuration. +**The asset canister** inserts every uploaded file into the HTTP certification tree, sets the certified root hash after each sync, returns the `IC-Certificate` and `IC-Certificate-Expression` headers on every `http_request` query, and re-certifies on subsequent deploys. It certifies `Content-Type` plus the headers you list in `.ic-assets.json5`. ### What gets certified -The asset canister certifies the full response: path, response body, status code, and the response headers you configure in `.ic-assets.json5`. Headers that are not listed are not included in the certification, which means a malicious replica could inject arbitrary values for uncertified headers. +With the asset canister, headers that are not listed in `.ic-assets.json5` are left out of the certification, which means a malicious replica could inject arbitrary values for them. Always certify headers that affect browser behavior. In particular: @@ -85,9 +85,11 @@ Always certify headers that affect browser behavior. In particular: The `security_policy: "standard"` option in `.ic-assets.json5` certifies a baseline set of security headers. For custom headers, list them explicitly in `headers`: the asset canister certifies everything in that object. +This whole class of mistake does not exist on a static site, which certifies the full response. + ## Custom HTTP canisters -If you are writing a canister that serves HTTP responses directly (not through the asset canister), you must handle certification yourself using the `ic-http-certification` or `ic-asset-certification` Rust crates. +If you are writing a canister that serves HTTP responses directly (not through one of the frontend recipes), you must handle certification yourself using the `ic-http-certification` or `ic-asset-certification` Rust crates. ### When to use custom certification @@ -95,9 +97,9 @@ Use custom HTTP certification when: - Your canister serves HTTP responses via `http_request` and you need boundary nodes to verify them - You need to certify dynamic responses (generated per request, not pre-uploaded assets) -- You are building a canister that functions as its own frontend without using the standard asset canister +- You are building a canister that functions as its own frontend without using one of the frontend recipes -For static assets (HTML, CSS, JS, images), use the standard asset canister instead: it handles all certification automatically and is more efficient. +For static assets (HTML, CSS, JS, images), [host a static site](static-site/overview.md) instead: certification is handled for you and is more efficient. ### Using ic-asset-certification @@ -200,7 +202,7 @@ For more control (certifying dynamic responses, certifying only specific headers ## Client-side certificate verification -For standard asset serving via the asset canister, verification is transparent: the boundary node verifies every response before forwarding it to the browser, and you do not need any JavaScript verification code. +For standard asset serving through either frontend recipe, verification is transparent on a verifying hostname: the boundary node checks every response before forwarding it to the browser, and you do not need any JavaScript verification code. On a `raw` hostname nothing checks it, which is why a raw URL is a debugging tool rather than a way to serve a site. For custom canisters returning certified data over the Candid interface (not HTTP), you may need to verify the certificate in JavaScript. This is the pattern covered in [Certified variables](../backends/certified-variables.md): the canister returns `(data, certificate, witness)` as Candid values, and the frontend verifies them with `@dfinity/certificate-verification`. @@ -301,9 +303,11 @@ For the full working example including a backend canister, see the [certified-co ## Common mistakes -**Not disabling raw access for sensitive assets.** By default `allow_raw_access` is `true`, meaning assets are also available on `raw.icp.net` where no verification occurs. Set `"allow_raw_access": false` in `.ic-assets.json5` for any assets that must not be served unverified. +**Sharing a raw URL.** A raw link is copy-pasteable and gets passed on, and nothing in the response tells the visitor it arrived unverified. Link to a verifying hostname, and keep raw for debugging. On the asset canister you can also refuse it outright with `"allow_raw_access": false`, which is worth setting for anything sensitive. + +**Assuming certification implies verification.** A certificate only helps if somebody checks it, and in a browser that is the gateway, chosen by whoever wrote the URL. A canister that certifies every response still gives a visitor on a non-verifying gateway no better assurance than an ordinary web host. -**Not certifying Content-Type and security headers.** Headers not listed in `.ic-assets.json5` are not included in the certification. A malicious replica could inject arbitrary values for uncertified headers. Always certify `Content-Type` and any security headers your application relies on. +**Not certifying Content-Type and security headers (asset canister).** Headers not listed in `.ic-assets.json5` are not included in the certification. A malicious replica could inject arbitrary values for uncertified headers. Always certify `Content-Type` and any security headers your application relies on. **Fetching the root key on mainnet.** Calling `agent.fetchRootKey()` or setting `shouldFetchRootKey: true` against mainnet allows a man-in-the-middle to supply a fake root key. Use the hardcoded key (default behavior of the JS SDK) for all mainnet deployments. @@ -315,9 +319,10 @@ For the full working example including a backend canister, see the [certified-co ## Next steps -- [Asset canister](asset-canister.md): deploy and configure the standard asset canister with automatic certification +- [Hosting a static site](static-site/overview.md): deploy a frontend whose every response is certified +- [Asset canister (legacy)](asset-canister.md): certification on the older recipe, and how to migrate - [Certified variables](../backends/certified-variables.md): certify Candid query responses from backend canisters - [Security concepts](../../concepts/security.md): why query integrity matters - [HTTP Gateway specification](../../references/http-gateway-protocol-spec.md): how boundary nodes verify responses - + diff --git a/docs/guides/frontends/custom-domains.md b/docs/guides/frontends/custom-domains.md index da6611a8..20553f52 100644 --- a/docs/guides/frontends/custom-domains.md +++ b/docs/guides/frontends/custom-domains.md @@ -2,7 +2,7 @@ title: "Custom domains" description: "Point a custom domain to your ICP-hosted frontend with DNS and boundary node configuration" sidebar: - order: 2 + order: 3 --- By default, every canister on ICP is accessible at `https://.icp.net`. To serve your frontend under your own domain (e.g., `app.example.com`), you register it with the HTTP gateway custom domain service. @@ -13,7 +13,7 @@ The service handles TLS certificate provisioning, renewal, and routing automatic - A registered domain from any registrar (Namecheap, GoDaddy, Cloudflare, Route 53, etc.) - Access to edit DNS records for that domain -- A deployed asset canister (see [Asset canister](asset-canister.md)) +- A deployed frontend canister (see [Hosting a static site](static-site/overview.md)) - `curl` for the registration API calls ## Overview @@ -58,29 +58,26 @@ app.example.com www.example.com ``` -**Placement for asset canisters:** Hidden directories (starting with `.`) are excluded by the asset canister by default. To include `.well-known/`: +**Placement.** Put the file in the directory your build tool copies verbatim into its output: `public/` for Vite, Next.js, and Nuxt, `static/` for older SvelteKit versions. -1. Place the file in your `public/` directory (Vite, SvelteKit, Nuxt) or `static/` directory (older SvelteKit versions) so the build tool copies it to the output directory. For Next.js, place it in `public/`. Most frameworks have a dedicated directory for static files that are copied as-is to the build output: - - ``` - public/ - ├── .ic-assets.json5 - └── .well-known/ - └── ic-domains - ``` +```text +public/ +└── .well-known/ + └── ic-domains +``` -2. Add a rule to your `.ic-assets.json5` to allow the hidden directory: +On a [static site](static-site/overview.md) that is all it takes. Dotfiles are skipped on upload, but `.well-known/` is the documented exception and is traversed normally, so the file is served at `/.well-known/ic-domains` with no configuration. - ```json5 - [ - { - "match": ".well-known", - "ignore": false - } - ] - ``` +On the [legacy asset canister](asset-canister.md), hidden directories are excluded unless you un-ignore them. Add a rule to `.ic-assets.json5` in the same directory, or to the existing array if you already have one: - If you already have an `.ic-assets.json5`, add this rule to the existing array. +```json5 +[ + { + "match": ".well-known", + "ignore": false + } +] +``` ## Step 3: Deploy your canister @@ -130,7 +127,7 @@ If validation fails, the response indicates what is wrong: | 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 (no extra spaces or quotes) | | More than one DNS TXT record | Remove duplicate `_canister-id` TXT records: keep exactly one | -| Failed to retrieve known domains | Ensure `.well-known/ic-domains` is deployed and served (`ignore: false` in `.ic-assets.json5`) | +| Failed to retrieve known domains | Ensure `.well-known/ic-domains` is deployed and served (on the legacy asset canister, that needs `ignore: false` in `.ic-assets.json5`) | | Domain missing from list | Add the domain to the `ic-domains` file and redeploy | ## Step 5: Register the domain @@ -222,7 +219,7 @@ const agent = await HttpAgent.create({ host }); Without this, `HttpAgent` falls back to using the page origin as the API host: which will fail on custom domains since they do not proxy IC API traffic. -For local development, you also need to pass `shouldFetchRootKey: true` so the agent can fetch the replica's root key. See [Asset canister](asset-canister.md) for a complete local + mainnet agent setup example. +For local development, you also need to pass `shouldFetchRootKey: true` so the agent can fetch the replica's root key. See [Reading canister IDs and the root key](frameworks.md#reading-canister-ids-and-the-root-key) for a complete local and mainnet agent setup example. ## Updating a custom domain @@ -328,7 +325,7 @@ dig TXT _canister-id.CUSTOM_DOMAIN The `.well-known/ic-domains` file is not accessible on your canister. Check: 1. The file exists in the correct location in your build output -2. `.ic-assets.json5` contains `{ "match": ".well-known", "ignore": false }` +2. On the legacy asset canister, `.ic-assets.json5` contains `{ "match": ".well-known", "ignore": false }` 3. The canister was redeployed after adding the file Verify directly: diff --git a/docs/guides/frontends/frameworks.md b/docs/guides/frontends/frameworks.md index a73ca1b0..9216fbe5 100644 --- a/docs/guides/frontends/frameworks.md +++ b/docs/guides/frontends/frameworks.md @@ -1,19 +1,19 @@ --- title: "Frontend frameworks" -description: "Integrate React, Vue, Svelte, Next.js, and game engines with ICP canisters using the asset canister and icp-cli" +description: "Integrate React, Vue, Svelte, Next.js, and game engines with ICP canisters using icp-cli" sidebar: - order: 5 + order: 2 --- -ICP hosts frontend applications as asset [canisters](../../concepts/canisters.md): static files (HTML, CSS, JavaScript) deployed to the network and served with certified responses. Any framework that can produce a static build output works: React, Vue, Svelte, Next.js, and even game engines like Unity WebGL and Godot. +ICP hosts frontend applications in [canisters](../../concepts/canisters.md): static files (HTML, CSS, JavaScript) deployed to the network and served with certified responses. Any framework that can produce a static build output works: React, Vue, Svelte, Next.js, and even game engines like Unity WebGL and Godot. -This guide shows you how to configure your framework's build pipeline, wire up the ICP JavaScript SDK, and deploy to an asset canister. +This guide shows you how to configure your framework's build pipeline, wire up the ICP JavaScript SDK, and deploy it as a [static site](static-site/overview.md). ## Prerequisites - [icp-cli](https://cli.internetcomputer.org/1.5/guides/installation) installed: `npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm` - A backend canister deployed (or a static-only site with no backend) -- Familiarity with [asset canisters](asset-canister.md) +- Familiarity with [hosting a static site](static-site/overview.md) ## The deployment model @@ -21,10 +21,10 @@ Every frontend framework integration follows the same pattern: 1. Configure `icp.yaml` to point at your framework's build output directory 2. Optionally add a Vite plugin (`@icp-sdk/bindgen`) to generate typed canister bindings at build time -3. Use `@icp-sdk/core` in your app to read canister IDs and the root key at runtime from the `ic_env` cookie served by the asset canister +3. Use `@icp-sdk/core` in your app to read canister IDs and the root key at runtime from the `ic_env` cookie the canister serves 4. Deploy with `icp deploy` -The asset canister injects an `ic_env` cookie into every HTML response. This cookie carries the root key and any `PUBLIC_CANISTER_ID:` environment variables you set: so your frontend never needs canister IDs baked into the build artifact. +The canister injects an `ic_env` cookie into every HTML response. This cookie carries the root key and any `PUBLIC_CANISTER_ID:` environment variables you set: so your frontend never needs canister IDs baked into the build artifact. ## React with Vite @@ -36,7 +36,7 @@ The [hello-world template](../../getting-started/project-structure.md) uses Reac canisters: - name: frontend recipe: - type: "@dfinity/asset-canister@v2.3.0" + type: "@dfinity/static-site@v0.3.3" configuration: build: - npm install @@ -45,7 +45,7 @@ canisters: dir: app/dist ``` -The `build` array runs before the asset canister uploads files. `npm run generate` regenerates TypeScript bindings from the backend `.did` file; `npm run build` runs Vite. +The `build` array runs before the canister is deployed and its files uploaded. `npm run generate` regenerates TypeScript bindings from the backend `.did` file; `npm run build` runs Vite. ### vite.config.ts @@ -70,7 +70,7 @@ export default defineConfig({ ], server: { headers: { - // Simulate the ic_env cookie that the asset canister injects in production. + // Simulate the ic_env cookie that the canister injects in production. "Set-Cookie": `ic_env=${encodeURIComponent( `ic_root_key=${IC_ROOT_KEY_HEX}&PUBLIC_CANISTER_ID:backend=${BACKEND_CANISTER_ID}` )}; SameSite=Lax;`, @@ -87,7 +87,7 @@ export default defineConfig({ The `icpBindgen` Vite plugin regenerates TypeScript bindings whenever the `.did` file changes during development. -The `server.headers` block simulates the `ic_env` cookie during `vite dev`. In production, the asset canister injects this cookie automatically: your code reads it without any build-time environment variables. +The `server.headers` block simulates the `ic_env` cookie during `vite dev`. In production, the canister injects this cookie automatically: your code reads it without any build-time environment variables. Install the required packages: @@ -106,7 +106,7 @@ interface CanisterEnv { readonly "PUBLIC_CANISTER_ID:backend": string; } -// Reads from the ic_env cookie injected by the asset canister (production) +// Reads from the ic_env cookie injected by the canister (production) // or the Set-Cookie header set in vite.config.ts (development). const canisterEnv = getCanisterEnv(); const canisterId = canisterEnv["PUBLIC_CANISTER_ID:backend"]; @@ -125,27 +125,29 @@ The `createActor` function is generated by `@icp-sdk/bindgen` from your `.did` f ### SPA routing -React apps use client-side routing. Without a fallback, refreshing on `/about` returns a 404 from the asset canister. Add a `.ic-assets.json5` file to your `public/` directory so it ends up in `dist/`: - -```json5 -[ - { - // Apply security policy to all paths. Two separate rules are needed because - // `security_policy` and `enable_aliasing` interact: the aliasing rule must - // be evaluated last so it only applies to paths with no matching file. - "match": "**/*", - "security_policy": "standard", - "allow_raw_access": false - }, - { - // SPA fallback: serve index.html for any path that has no matching file. - "match": "**/*", - "enable_aliasing": true - } -] +React apps use client-side routing. Without a fallback, refreshing on `/about` returns a 404. Add a `_redirects` file to your `public/` directory so it ends up in `dist/`: + +```text +# Serve the app shell for any path with no matching file. The 200 makes this a +# rewrite, so the URL the visitor typed stays in the address bar. +/* /index.html 200 ``` -See [asset canister configuration](asset-canister.md) for the full `.ic-assets.json5` reference. +The canister adds no default headers, so declare the ones you want in a `_headers` file next to it: + +```text +/* + X-Frame-Options: DENY + X-Content-Type-Options: nosniff + Referrer-Policy: strict-origin-when-cross-origin + +/assets/* + Cache-Control: public, max-age=31536000, immutable +``` + +Link assets with absolute paths (`/assets/app.js`), never relative ones: under a `/*` rewrite a relative URL resolves against the client route, so the browser asks for `/dashboard/assets/app.js` and gets the HTML shell back. + +See [Redirects and rewrites](static-site/redirects.md) and [Custom headers](static-site/headers.md) for the full syntax, and [Asset canister (legacy)](asset-canister.md#configuration-mapping) if you are moving a project off `.ic-assets.json5`. ## Vue with Vite @@ -186,7 +188,7 @@ Authentication with Internet Identity is framework-agnostic. The `@icp-sdk/auth` ## Svelte and SvelteKit -For SvelteKit, you must configure static export mode before deploying. The asset canister serves static files and does not support server-side rendering. +For SvelteKit, you must configure static export mode before deploying. The canister serves static files and does not support server-side rendering. ### SvelteKit with static adapter @@ -214,7 +216,7 @@ export default { canisters: - name: frontend recipe: - type: "@dfinity/asset-canister@v2.3.0" + type: "@dfinity/static-site@v0.3.3" configuration: build: - npm install @@ -226,7 +228,7 @@ For Svelte (without SvelteKit), Vite is the standard build tool. The `vite.confi ## Next.js -Next.js requires static export mode. Server components, API routes, and `getServerSideProps` are not supported in an asset canister. The canister only serves static files. +Next.js requires static export mode. Server components, API routes, and `getServerSideProps` are not supported. The canister only serves static files. Enable static export in your Next.js config: @@ -246,7 +248,7 @@ This outputs static files to the `out/` directory. canisters: - name: frontend recipe: - type: "@dfinity/asset-canister@v2.3.0" + type: "@dfinity/static-site@v0.3.3" configuration: build: - npm install @@ -260,7 +262,7 @@ Only Next.js pages that can be statically generated are compatible with ICP. Any ## Game engines -Game engines that export HTML5 or WebGL builds can be deployed as asset canisters without a backend canister. The build output is pre-generated in the export step of the engine: `icp.yaml` just copies the files into place. +Game engines that export HTML5 or WebGL builds can be deployed without a backend canister. The build output is pre-generated in the export step of the engine: `icp.yaml` just copies the files into place. ### Unity WebGL @@ -271,7 +273,7 @@ Export your game from Unity Editor: **File → Build Settings → WebGL → Buil canisters: - name: unity_webgl_template_assets recipe: - type: "@dfinity/asset-canister@v2.3.0" + type: "@dfinity/static-site@v0.3.3" configuration: dir: dist build: @@ -293,7 +295,7 @@ Export your game from Godot Editor: **Project → Export → HTML5 → Export Pr canisters: - name: godot_html5_assets recipe: - type: "@dfinity/asset-canister@v2.3.0" + type: "@dfinity/static-site@v0.3.3" configuration: dir: dist build: @@ -312,14 +314,14 @@ Both game engine templates deploy with standard icp-cli commands: # Start local network icp network start -d -# Deploy the asset canister +# Deploy the frontend canister icp deploy # Access your game locally # http://.localhost:8000 ``` -No Vite plugin or JS SDK integration is needed for game builds. The asset canister serves the pre-built HTML and JavaScript files directly. +No Vite plugin or JS SDK integration is needed for game builds. The canister serves the pre-built HTML and JavaScript files directly. ## Static sites @@ -330,7 +332,7 @@ For sites with no backend canister (portfolios, landing pages, documentation): canisters: - name: frontend recipe: - type: "@dfinity/asset-canister@v2.3.0" + type: "@dfinity/static-site@v0.3.3" configuration: build: - npm install @@ -338,7 +340,7 @@ canisters: dir: dist ``` -No JS SDK integration is needed. The asset canister serves your files, and you can configure headers and caching in `.ic-assets.json5`. +No JS SDK integration is needed. The canister serves your files, and you configure headers and caching in [`_headers`](static-site/headers.md). See the [React hosting example](https://github.com/dfinity/examples/tree/master/hosting/react) for a minimal static frontend without a backend canister. @@ -355,7 +357,7 @@ icp deploy icp deploy -e ic ``` -After deployment, the asset canister URL depends on your canister ID: +After deployment, the URL depends on your canister ID: | Environment | URL | |-------------|-----| @@ -370,8 +372,8 @@ icp canister settings show frontend -i ## Next steps -- [Asset canister](asset-canister.md): configure headers, caching, and SPA routing in `.ic-assets.json5` +- [Hosting a static site](static-site/overview.md): headers, redirects, clean URLs, and access protection - [Internet Identity](../authentication/internet-identity.md): add authentication to your frontend - [Project structure](../../getting-started/project-structure.md): explore the hello-world template with React, Vite, and a Motoko backend - + diff --git a/docs/guides/frontends/service-discoverability.md b/docs/guides/frontends/service-discoverability.md index 5698b3cb..f2ecd728 100644 --- a/docs/guides/frontends/service-discoverability.md +++ b/docs/guides/frontends/service-discoverability.md @@ -2,7 +2,7 @@ title: "Service discoverability" description: "What an app exposes so ICP MCP can discover its canisters, interfaces, behavior, data, and identity from just its URL." sidebar: - order: 3 + order: 5 --- When an agent working through ICP MCP is handed only your app's URL (for example, `https://yourapp.com`), it should be able to work out the rest on its own: which canisters your app comprises, what each one does, how to call them, how to query their data, and how to act as the signed-in user. No human supplying canister IDs, no bespoke integration. @@ -81,7 +81,7 @@ This is the way an app declares its composition. It is recommended to create thi - Serve real JSON with `Content-Type: application/json`. The most common failure is a single-page-app catch-all returning `index.html` for unknown paths. Exempt `/.well-known/*` from the SPA rewrite wherever your frontend is served. - Generate it at deploy time. Canister IDs differ per network (local, staging, mainnet), so the file must be produced by the deploy pipeline (which already knows the IDs) rather than committed with hard-coded values. -The exact configuration depends on how you host the frontend; the requirement is only that `/.well-known/*` is served as a static file, not rewritten to `index.html`. If you serve assets from an asset canister, see [Asset canister](asset-canister.md#ic-assetsjson5) for including the hidden `.well-known` directory and configuring SPA aliasing, and [Custom domains](custom-domains.md#step-2-create-the-ic-domains-file) for the same `.well-known` pattern applied to domain ownership. +The exact configuration depends on how you host the frontend; the requirement is only that `/.well-known/*` is served as a static file, not rewritten to `index.html`. On a [static site](static-site/overview.md) that needs no configuration: `.well-known/` is uploaded automatically, and a real file always wins over a `/*` rewrite. On the [legacy asset canister](asset-canister.md#ic-assetsjson5) the directory has to be un-ignored, and SPA aliasing configured around it. [Custom domains](custom-domains.md#step-2-create-the-ic-domains-file) applies the same `.well-known` pattern to domain ownership. ## Layer 2: Interface discovery diff --git a/docs/guides/frontends/static-site/access-protection.md b/docs/guides/frontends/static-site/access-protection.md new file mode 100644 index 00000000..6c72b786 --- /dev/null +++ b/docs/guides/frontends/static-site/access-protection.md @@ -0,0 +1,200 @@ +--- +title: "Access protection" +description: "Put a login page in front of a private or preview site with revocable, expiring access tokens" +sidebar: + order: 6 +source_repo: "dfinity/certified-assets" +source_ref: "65c0f32" +--- + +By default every deployed app is public. **Access protection** puts a login screen +in front of it: unauthenticated visitors get a certified redirect to your login +page (or a `401` for non-page assets) instead of your content. It's meant to +**deter casual/public access** to in-progress or preview work, not to provide +confidentiality against a determined attacker (see [Threat model](#threat-model)). + +Access is a set of **labeled tokens**. The cookie *is* the token: a visitor presents +a token, the canister sets it as a cookie, and every request re-validates that cookie +against the token store. A classic single "password" is just one long-lived token. + +When protection is off, the app is **completely unchanged**: no gate, no extra +headers, no cost difference. Everything below applies only once you enable it. + +## Quick start + +Protection is configured through controller-only canister methods, so you call them +with `icp canister call` as the canister's controller. + +```sh +# 1. Add a login page to your site (see "The login page" below) and deploy it. +icp deploy + +# 2. Turn the gate on, naming your login page. +icp canister call frontend enable_protection '("/login.html")' + +# 3. Mint a credential. Here, a chosen "password" valid for ~1 year. +icp canister call frontend issue_token \ + '(record { label = "owner"; ttl_secs = 31536000 : nat32; value = opt "my-passphrase" })' +``` + +That's it: `https://.icp.net/` now redirects strangers to +`/login.html`, and visitors who present `my-passphrase` get in. + +> **Enable before the first sync for a brand-new private app.** Enabling on an empty +> canister, *then* syncing, means there is never a window where your assets are +> world-readable. Enabling on an already-public app necessarily had a public period. + +## Managing tokens + +| Method | Call | Effect | +|---|---|---| +| Issue | `issue_token '(record { label = "