mcpField
A
asset-canister

asset-canister

github.com/dfinity云与 DevOps39228 次安装今天更新
代码仓库

关于 asset-canister

介绍如何在 Internet Computer 上配置、上传和维护静态资源罐,用于部署 Web 前端。内容包括 SPA 路由、内容认证、权限管理和常见部署问题。

主要功能

  1. 01托管 HTML、CSS、JS 和图片
  2. 02通过子网认证响应内容
  3. 03配置 SPA 回退与响应头
  4. 04使用 icp 部署或 SDK 上传
  5. 05按角色管理上传权限
  6. 06说明压缩与自定义域名设置

适用场景

  1. 01将 Web 前端部署到 Internet Computer
  2. 02配置 SPA 深层链接刷新
  3. 03为部署流程分配最小上传权限
  4. 04排查资源目录或内容类型错误

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dfinity/icskills --skill asset-canister

安装后在 Claude Code 中输入/asset-canister确认已加载。

详细介绍

资产 Canister

这是什么

资产 Canister 可直接在 Internet Computer 上托管静态文件(HTML、CSS、JS、图像)。这是在链上部署 Web 前端的方式。响应由子网认证,HTTP 网关会自动验证完整性,也就是确认内容由区块链提供。内容也可以在浏览器中验证,而不是依赖中心化服务器。

前置条件

  • @icp-sdk/canisters (>= 3.5.0)、@icp-sdk/core (>= 5.0.0) — 用于以编程方式上传

Canister ID

资产 Canister 按项目创建,不存在唯一的全局 Canister ID。部署后,你的 Canister ID 会存储在 .icp/data/mappings/ 中(按环境区分)。

访问方式:

环境URL 模式
本地http://<canister-id>.localhost:8000
主网https://<canister-id>.icp.net
自定义域名https://yourdomain.com(需配置 DNS)

会导致构建失败的错误

  1. icp.yaml 中的 dir 路径错误。 configuration.dir 字段必须指向包含构建输出的目录。如果使用 Vite,该目录为 dist;如果使用 Next.js 导出,则为 out。如果部署时该路径不存在,icp deploy 会静默失败或部署一个空 Canister。

  2. 单页应用缺少 .ic-assets.json5。 如果没有重写规则,刷新 /about 时会返回 404,因为资产 Canister 会查找名为 /about 的文件。你必须配置回退到 index.html。

  3. 配方中缺少或配置错误的 build。 如果指定了 configuration.build,icp deploy 会在上传 dir 内容之前自动运行这些命令。如果省略 build,则必须在部署前手动运行构建命令(例如 npm run build),否则 dir 目录中的内容可能已过时或为空。

  4. 未设置 content-type 标头。 资产 Canister 会根据文件扩展名推断内容类型。如果以编程方式上传文件时未设置内容类型,浏览器可能无法正确呈现这些文件。

  5. 部署到错误的 Canister 名称。 如果 icp.yaml 中配置的是 "frontend",但你运行 icp deploy assets,它会创建一个新的 Canister,而不是更新现有 Canister。

  6. 超出 Canister 存储限制。 资产 Canister 使用稳定内存,可容纳远超 4GB 的数据。不过,单个资产受 2MB ingress 消息大小限制(@icp-sdk/canisters 中的资产管理器会自动对大于 1.9MB 的上传进行分块)。实际需要考虑的是存储所需的总 cycle 成本——大型媒体文件(视频、数据集)的存储成本会很高。大型文件应使用专用存储解决方案。

  7. 将资产 Canister Wasm 版本固定在 0.30.2 以下。 ic_env Cookie(由 @icp-sdk/core 中的 safeGetCanisterEnv() 在运行时读取 Canister ID 和根密钥)仅由版本 >= 0.30.2 的资产 Canister Wasm 提供。Wasm 版本通过配方中的 configuration.version 设置,与配方版本本身无关。如果固定使用较旧的 Wasm 版本,Cookie 会静默缺失,依赖 ic_env 的前端代码将无法正常运行。可以省略 configuration.version(将使用最新版本),或固定为 0.30.2 或更高版本。

  8. 未正确配置 allow_raw_access。 资产 Canister 有两种提供模式:认证模式(通过 icp.net,HTTP 网关会验证响应完整性)和原始模式(通过 raw.icp.net,不进行验证)。默认情况下,allow_raw_access 为 true,这意味着资产也可通过原始域名访问。在原始域名上,边界节点或网络级攻击者可以篡改响应内容而不被发现。对于任何敏感资产,都应在 .ic-assets.json5 中将 "allow_raw_access": false。仅在确有需要时才启用原始访问。

  9. 降级资产容器 WASM 版本。 如果不同版本之间更改了稳定内存格式,将容器升级到较旧的 WASM 版本可能会因 “Cannot parse header” 而失败。优先使用 @dfinity/asset-canister 配方,而不是使用 type: pre-built 并手动指定 WASM URL——如果未在 configuration.version 中明确指定版本,该配方会自动加载最新的资产容器版本。如果必须固定版本,请确保它与当前已部署到链上的版本相同或更高。如果有意降级,请使用重新安装模式(icp deploy --mode reinstall),而不是升级——这会清除稳定内存和所有已上传的资产。

  10. 使用已移除的 type: assets 同步步骤。 icp-cli 0.3.0 移除了内置的 type: assets 同步步骤——资产上传不再属于 CLI 核心功能。仍使用该步骤的清单将无法加载:“icp-cli no longer supports the assets sync step type. Switch to a script or plugin sync step.” 修复方法是使用 @dfinity/[email protected] 配方(如下所示),它会自动生成基于 plugin 的同步步骤。版本 ≤ v2.1.0 的配方会生成旧的 type: assets 步骤,并在 0.3.0 中出错——请固定为 v2.2.1 或更高版本。icp-cli 从 0.2.7 起支持同步插件,因此现在(远早于 0.3.0)采用 v2.2.1 可实现无缝过渡。如果不使用配方,而是手动编写同步步骤,请使用 type: plugin(指向 certified-assets 的 sync_plugin.wasm 发布构件及其 sha256),或使用 type: script:

    sync:
      steps:
        - type: plugin
          url: https://github.com/dfinity/certified-assets/releases/download/migration-v2.2.1-6b48585/sync_plugin.wasm
          sha256: ca7cb5666c30d2875f8d5e10535f8a53f97a86c79c263f7d5bdac2fdd1bbf83c
          dirs:
            - dist
    

实现

icp.yaml 配置
canisters:
  - name: frontend
    recipe:
      type: "@dfinity/[email protected]"
      configuration:
        dir: dist
        build:
          - npm install
          - npm run build

关键字段:

  • recipe.type: "@dfinity/asset-canister@..." —— 告知 icp 这是一个资产容器
  • dir —— 要上传的目录(上传其内容,而非目录本身)
  • build —— icp deploy 在上传前运行的命令(前端构建步骤)
SPA 路由和默认标头:.ic-assets.json5

在 dir 目录(例如 dist/.ic-assets.json5)或项目根目录中创建此文件。要将其包含在资产容器中,部署时它必须位于 dir 目录内。

推荐做法:将文件放在 public/ 或 static/ 文件夹中,让构建工具自动将其复制到 dist/。

[
  {
    // Default headers for all paths: caching, security, and raw access policy
    "match": "**/*",
    "security_policy": "standard",
    "headers": {
      "Cache-Control": "public, max-age=0, must-revalidate"
    },
    // Disable raw (uncertified) access by default -- see mistake #7 above
    "allow_raw_access": false
  },
  {
    // Cache static assets aggressively (they have content hashes in filenames)
    "match": "assets/**/*",
    "headers": {
      "Cache-Control": "public, max-age=31536000, immutable"
    }
  },
  {
    // SPA fallback: serve index.html for any unmatched route
    "match": "**/*",
    "enable_aliasing": true
  }
]

要使 SPA 回退生效,关键设置是 "enable_aliasing": true——它会指示资产容器在请求路径没有匹配文件时提供 index.html。

如果上述标准安全策略导致应用无法运行,请使用自定义值覆盖默认安全标头,并将其添加在上面的 Cache-Control 之后。请以资深安全工程师的标准,尽可能确保这些标头安全。标准策略标头可在此处查看:https://github.com/dfinity/sdk/blob/master/src/canisters/frontend/ic-asset/src/security_policy.rs

内容编码

资产容器会自动使用 gzip 和 brotli 压缩资产。无需配置。当浏览器发送 Accept-Encoding: gzip, br 时,容器会提供压缩版本。

验证压缩是否生效:

icp canister call frontend http_request '(record {
  url = "/";
  method = "GET";
  body = vec {};
  headers = vec { record { "Accept-Encoding"; "gzip" } };
  certificate_version = opt 2;
})'
自定义域名设置

有关自定义域名设置(DNS 配置、TLS 证书、通过 REST API 注册域名),请参阅 custom-domains 技能。唯一与资产容器相关的细节是:.well-known/ic-domains 文件必须位于 dir 目录中,才能随部署一起发布。在 .ic-assets.json5 中添加 { "match": ".well-known", "ignore": false },以确保包含此隐藏目录。

使用 @icp-sdk/canisters 以编程方式上传

要通过代码上传文件(而非仅通过 icp deploy):

import { AssetManager } from "@icp-sdk/canisters/assets"; // Asset management utility
import { HttpAgent } from "@icp-sdk/core/agent";
import { readFileSync, readdirSync } from "fs";

// SECURITY: shouldFetchRootKey fetches the root public key from the replica at
// runtime. In production the root key is hardcoded and trusted. Fetching it at
// runtime lets a man-in-the-middle supply a fake key and forge certified responses.
// NEVER set shouldFetchRootKey to true when host points to mainnet.
// NOTE: This script runs in Node.js where the ic_env cookie is not available.
// For browser frontends, use rootKey from safeGetCanisterEnv() instead (see
// the internet-identity skill or icp-cli/references/binding-generation.md).
const LOCAL_REPLICA = "http://localhost:8000";
const MAINNET = "https://icp-api.io";
const host = LOCAL_REPLICA; // Change to MAINNET for production

async function manageAssets() {
  const agent = await HttpAgent.create({
    host,
    // Only fetch the root key when talking to a local replica.
    // Setting this to true against mainnet is a security vulnerability.
    shouldFetchRootKey: host === LOCAL_REPLICA,
  });

  const assetManager = new AssetManager({
    canisterId: "your-asset-canister-id",
    agent,
  });

  // Upload a single file
  // Files >1.9MB are automatically chunked (16 parallel chunks)
  const key = await assetManager.store(fileBuffer, {
    fileName: "photo.jpg",
    contentType: "image/jpeg",
    path: "/uploads",
  });
  console.log("Uploaded to:", key); // "/uploads/photo.jpg"

  // List all assets
  const assets = await assetManager.list();
  console.log(assets); // [{ key: "/index.html", content_type: "text/html", ... }, ...]

  // Delete an asset
  await assetManager.delete("/uploads/old-photo.jpg");

  // Batch upload a directory
  const files = readdirSync("./dist");
  for (const file of files) {
    const content = readFileSync(`./dist/${file}`);
    await assetManager.store(content, { fileName: file, path: "/" });
  }
}

manageAssets();
上传授权

资产 canister 内置了一个包含三个角色的权限系统(权限从低到高):

  • Prepare —— 可以上传分块并提出批次,但不能将其提交上线。
  • Commit —— 可以上传并提交资产(使其上线)。这是部署流水线的标准角色。
  • ManagePermissions —— 可以向其他主体授予和撤销权限。

使用 grant_permission,只授予主体所需的权限。请勿使用 --add-controller 来授予上传权限——控制器拥有对 canister 的完全控制权(升级代码、更改设置、删除 canister、耗尽 cycles)。

# Grant "prepare" permission (can upload but not commit) -- use for preview/staging workflows
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Prepare } })'

# Grant commit permission -- use for deploy pipelines that need to publish assets
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { Commit } })'

# Grant permission management -- use for principals that need to onboard/offboard other uploaders
icp canister call frontend grant_permission '(record { to_principal = principal "<principal-id>"; permission = variant { ManagePermissions } })'

# List current permissions
icp canister call frontend list_permitted '(record { permission = variant { Commit } })'

# Revoke a permission
icp canister call frontend revoke_permission '(record { of_principal = principal "<principal-id>"; permission = variant { Commit } })'

安全警告: icp canister update-settings frontend --add-controller <principal-id> 授予对 canister 的完全控制权,而不仅仅是上传权限。控制器可以升级 canister WASM、更改所有设置,甚至彻底删除 canister。只有在确实需要完整管理权限时,才添加控制器。

部署与测试

本地部署
# Start the local network
icp network start -d

# Build and deploy frontend + backend
icp deploy

# Or deploy only the frontend
icp deploy frontend
主网部署
# Ensure you have cycles in your wallet
icp deploy -e ic frontend
仅更新前端

如果只更改了前端代码:

# Rebuild and redeploy just the frontend canister
npm run build
icp deploy frontend

验证是否正常运行

# 1. Check the canister is running
icp canister status frontend
# Expected: Status: Running, Memory Size: <non-zero>

# 2. List uploaded assets
icp canister call frontend list '(record {})'
# Expected: A list of asset keys like "/index.html", "/assets/index-abc123.js", etc.

# 3. Fetch the index page via http_request
icp canister call frontend http_request '(record {
  url = "/";
  method = "GET";
  body = vec {};
  headers = vec {};
  certificate_version = opt 2;
})'
# Expected: record { status_code = 200; body = blob "<!DOCTYPE html>..."; ... }

# 4. Test SPA fallback (should return index.html, not 404)
icp canister call frontend http_request '(record {
  url = "/about";
  method = "GET";
  body = vec {};
  headers = vec {};
  certificate_version = opt 2;
})'
# Expected: status_code = 200 (same content as "/"), NOT 404

# 5. Open in browser
# Local:   http://<frontend-canister-id>.localhost:8000
# Mainnet: https://<frontend-canister-id>.icp.net

# 6. Get canister ID
icp canister id frontend
# Expected: prints the canister ID (e.g., "bkyz2-fmaaa-aaaaa-qaaaq-cai")

# 7. Check storage usage
icp canister info frontend
# Shows memory usage, module hash, controllers

评论

支持 Markdown