@ensdomains/ens-avatar

中文文档
1.0.6发布于 2026/10/01MITTypeScriptESM + CJS近 30 天下载 2566

Avatar resolver library for both nodejs and browser.

原始使用文档
文档目录25 节
  1. Important Notes
  2. Platform Support
  3. Getting started
  4. Prerequisites
  5. Installation
  6. Usage
  7. Supported avatar specs
  8. NFTs
  9. URIs
  10. Options
  11. Cache (Default: Disabled)
  12. Custom IPFS Gateway (Default: https://ipfs.io )
  13. Custom Arweave Gateway (Default: https://arweave.net )
  14. Marketplace Api Keys (Default: {})
  15. URL DenyList (Default: [])
  16. Limits (Defaults: 30000 ms, 1 MiB, 5 redirects, 256 KiB SVG)
  17. Custom Agents (Node.js only)
  18. Allow Private IPs (Default: false) - Development Only
  19. Cloudflare Workers
  20. Security
  21. XSS Protection
  22. SSRF Protection
  23. Guarding CCIP-read
  24. Serving avatars
  25. Demo

ens-avatar

Avatar resolver library for Node.js, browsers, and edge runtimes (Cloudflare Workers etc.).

Important Notes

  • Bring your own chain client. AvatarResolver takes a ChainClient instead of a raw provider, so the core ships with no ethers in the bundle. Wrap your SDK with a one-line adapter:
    • ethers: import { fromEthers } from '@ensdomains/ens-avatar/ethers' → new AvatarResolver(fromEthers(provider))
    • viem: import { fromViem } from '@ensdomains/ens-avatar/viem' → new AvatarResolver(fromViem(client))
    • ethers and viem are optional peer dependencies — install only the one you use.
  • Version 1.0.4+ uses the native Fetch API for maximum compatibility across platforms including Cloudflare Workers and other edge runtimes.
  • Name resolution uses the ENS Universal Resolver, so offchain (gateway/L2) and ENSIP-10 wildcard names resolve out of the box. The ethers adapter fetches the owner address and the avatar/header record in a single CCIP-read-aware call (plus one more only to classify an ambiguous failure); the viem adapter uses viem's getEnsText and getEnsAddress (two calls). Override the contract via the adapter's universalResolverAddress option for non-default networks.
  • NFT avatars must be on the client's chain. Both adapters report their chain id, and an eip155:<chainId>/… avatar on another chain throws ChainMismatch instead of being read from the wrong chain. A custom ChainClient opts in by implementing getChainId().

Platform Support

This library works seamlessly across:

  • ✅ Node.js 22+ (uses native fetch or http adapter)
  • ✅ Browsers (all modern browsers)
  • ✅ Cloudflare Workers
  • ✅ Other edge runtimes that support standard Fetch API

Getting started

Prerequisites

  • Have an ethers v6 provider or a viem public client ready.

And good to go! SVG sanitization is built in and runs identically in browsers, Node.js, and edge runtimes (Cloudflare Workers) — no DOM polyfill (jsdom) required.

Installation

# npm
npm i @ensdomains/ens-avatar
# pnpm
pnpm add @ensdomains/ens-avatar
# yarn
yarn add @ensdomains/ens-avatar

Usage

With ethers:

import { JsonRpcProvider } from 'ethers';
import { AvatarResolver, utils as avtUtils } from '@ensdomains/ens-avatar';
import { fromEthers } from '@ensdomains/ens-avatar/ethers';

const provider = new JsonRpcProvider(/* ... */);
const resolver = new AvatarResolver(fromEthers(provider));

async function getAvatar() {
    const avatarURI = await resolver.getAvatar('tanrikulu.eth');
    // avatarURI = https://ipfs.io/ipfs/QmUShgfoZQSHK3TQyuTfUpsc8UfeNfD8KwPUvDBUdZ4nmR
}

async function getHeader() {
    const headerURI = await resolver.getHeader('tanrikulu.eth');
    // headerURI = https://ipfs.io/ipfs/QmRFnn6c9rj6NuHenFVyKXb6tuKxynAvGiw7yszQJ2EsjN
}

async function getAvatarMetadata() {
    const avatarMetadata = await resolver.getMetadata('tanrikulu.eth');
    // avatarMetadata = { image: ... , uri: ... , name: ... , description: ... }
    const headerMetadata = await resolver.getMetadata('tanrikulu.eth', 'header');
    // headerMetadata = { image: ... , uri: ... , name: ... , description: ... }
    const avatarURI = avtUtils.getImageURI({ metadata: avatarMetadata });
    // avatarURI = https://ipfs.io/ipfs/QmUShgfoZQSHK3TQyuTfUpsc8UfeNfD8KwPUvDBUdZ4nmR
}

With viem:

import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { AvatarResolver } from '@ensdomains/ens-avatar';
import { fromViem } from '@ensdomains/ens-avatar/viem';

const client = createPublicClient({ chain: mainnet, transport: http() });
const resolver = new AvatarResolver(fromViem(client));

const avatarURI = await resolver.getAvatar('tanrikulu.eth');

Supported avatar specs

NFTs

  • ERC721
  • ERC1155

URIs

  • HTTP
  • Base64
  • IPFS

Options

Cache (Default: Disabled)

const avt = new AvatarResolver(provider, { cache: 300 }); // 5 min response cache in memory

Custom IPFS Gateway (Default: https://ipfs.io)

const avt = new AvatarResolver(provider, { ipfs: 'https://dweb.link' });

Custom Arweave Gateway (Default: https://arweave.net)

const avt = new AvatarResolver(provider, { arweave: 'https://arweave.net' });

Marketplace Api Keys (Default: {})

const avt = new AvatarResolver(provider, {
  apiKey: {
    opensea: 'YOUR_API_KEY',
  },
});

URL DenyList (Default: [])

Hostnames to refuse (subdomains included). Entries are hostnames, not URLs; matching ignores case and trailing dots.

const avt = new AvatarResolver(client, {
  urlDenyList: ['maliciouswebsite.com'],
});

Limits (Defaults: 30000 ms, 1 MiB, 5 redirects, 256 KiB SVG)

  • timeout — deadline for each HTTP request as a whole: redirects, headers and body.
  • maxContentLength — maximum size of a fetched response body (e.g. NFT metadata JSON); larger responses are rejected.
  • maxRedirects — redirects followed per request. Each hop is a subrequest; with the default, one resolution makes at most ~30 HTTP requests (under Cloudflare Workers Free's 50).
  • maxSvgLength — maximum length of an inline / data: SVG avatar; larger ones resolve to null. utils.sanitizeSVG(svg, { maxLength }) takes the same limit.

Values must be positive integers (maxRedirects may be 0); NaN, Infinity and the like throw instead of silently disabling a limit.

const avt = new AvatarResolver(client, {
  timeout: 8000,
  maxContentLength: 512 * 1024,
  maxRedirects: 3,
  maxSvgLength: 128 * 1024,
});

Custom Agents (Node.js only)

You can provide custom HTTP/HTTPS agents for advanced use cases:

import http from 'http';
import https from 'https';

const avt = new AvatarResolver(provider, {
  agents: {
    httpAgent: new http.Agent({ keepAlive: true }),
    httpsAgent: new https.Agent({ keepAlive: true }),
  },
});

⚠️ SECURITY WARNING: When you provide custom agents, ens-avatar will use them as-is without applying SSRF protection. You are responsible for ensuring your custom agents have appropriate security measures to prevent Server-Side Request Forgery attacks.

If you need SSRF protection with custom agents, wrap them with ssrf-req-filter:

import http from 'http';
import https from 'https';
const { requestFilterHandler } = require('ssrf-req-filter');

const avt = new AvatarResolver(provider, {
  agents: {
    httpAgent: requestFilterHandler(new http.Agent({ keepAlive: true })),
    httpsAgent: requestFilterHandler(new https.Agent({ keepAlive: true })),
  },
});

Allow Private IPs (Default: false) - Development Only

For local development when you need to access localhost or private network services:

const avt = new AvatarResolver(provider, {
  allowPrivateIPs: true, // Allows localhost, 127.0.0.1, 10.x.x.x, 192.168.x.x, etc.
});

⚠️ WARNING: This disables SSRF protection. Only use for local development (e.g., local IPFS nodes, test servers). NEVER enable this in production.

Common local development scenarios:

  • Local IPFS node: http://127.0.0.1:5001
  • Local Ethereum node: http://localhost:8545
  • Docker containers on private networks

Note: This flag is ignored if you provide custom agents (you control security in that case).

Cloudflare Workers

  • viem: create the client with a chain. Without one the adapter has to ask the RPC for eth_chainId; it does so without viem's request dedupe, because Workers hang when a promise started in one request is awaited in another.
  • viem transport: don't enable request batching (http(url, { batch: true })) or batch: { multicall: true } on a client shared across requests: both hold calls in a queue whose promises can end up awaited by a different request, which hangs.
  • Requires viem 2.35 or later (Universal Resolver v3 error handling).
  • ethers: 6.13 or later is required (earlier 6.x releases never time out a hung request, even with FetchRequest.timeout set). Use new JsonRpcProvider(url, network, { staticNetwork: true, batchMaxCount: 1 }), so the provider neither re-detects the network nor holds calls in a batch shared across requests.

Security

getMetadata returns metadata as published. Fields like image, image_url and image_data are raw, attacker-controlled values; render them only through getAvatar / getHeader / utils.getImageURI, which validate and sanitize them.

XSS Protection

All user-generated SVG content is automatically sanitized to prevent XSS attacks:

  • A single, parser-based sanitizer (sanitize-html + postcss) runs identically in browsers, Node.js, and edge runtimes (Cloudflare Workers) — no DOM or jsdom required.
  • Tags and attributes are restricted to a strict SVG allowlist (no script, foreignObject, event handlers, or external href/use/image targets).
  • CSS in style attributes and <style> blocks is filtered against a property allowlist, and every CSS function must be on a function allowlist (colors, calc/min/max/clamp, transforms, filter functions, basic shapes, timing functions; not var, env, attr, src, image-set, …). Presentation attributes (fill, stroke, filter, mask, clip-path, marker-*, …) are checked the same way.
  • url() is internal-only everywhere — attributes, style attributes and <style> blocks: it may only reference #id within the document, so gradients, clips, masks, filters and markers keep working while external loading (tracking, exfiltration) is blocked. href (and xlink:href, which is renamed to href) is likewise limited to #id, plus data:image/* on <image>/<feImage>.
  • Every <svg> element is pinned to the SVG namespace (xmlns is added when missing), so a document can't be switched to XHTML.
  • Common editor output survives: Figma filter chains (fe* primitives incl. feDropShadow), Illustrator <style> blocks wrapped in a single CDATA section, gradient inheritance via href, and presentation attributes on any element.

Raster avatars (png, jpeg, gif, webp, …), whether served as data: URIs or remote URLs, are passed through after validation — they are inert as <img> sources and are not (and need not be) rewritten.

Inline vs. remote SVGs

How an SVG avatar is handled depends on where it comes from:

  • Inline SVGs — on-chain <svg>…</svg> records and data:image/svg+xml URIs — are returned already sanitized. getAvatar / utils.getImageURI hand back a data:image/svg+xml;base64,… URI with scripts, event handlers, and external references stripped.
  • Remote SVGs — an avatar whose record is an http(s) URL that points at an SVG — are returned as the raw URL, unsanitized. ens-avatar is a resolver: for remote images it returns where the image lives, not its bytes, so the safe-rendering strategy is yours to pick. (The content-type check performed during resolution is not a safety guarantee — a server can advertise image/svg+xml and still return a hostile body.)

When you render a remote SVG, either load it in a context that already sandboxes it — an <img src> tag, a CSS background-image, or an <image href> inside another SVG, none of which execute scripts or load external sub-resources — or, if you fetch the bytes to serve, store or inline them, sanitize them first with the same engine the library uses for inline SVGs:

import { utils as avtUtils } from '@ensdomains/ens-avatar';

// `avatarUrl` came back from resolver.getAvatar(...) and points at an SVG
const svg = await fetch(avatarUrl).then(res => res.text());
const safeSvg = avtUtils.sanitizeSVGDocument(svg, { maxLength: 1024 * 1024 });
// safeSvg: a standalone '<svg …>…</svg>' document, or null if nothing usable remains

sanitizeSVGDocument(svg, { maxLength }): string | null returns only the root <svg> element, so the result is a valid standalone document (text outside the root, e.g. a DOCTYPE's internal subset, is dropped), and keeps whitespace as is. maxLength defaults to 256 KiB. sanitizeSVG(svg, { maxLength }) is the lower-level function: it returns the sanitized markup as a string ('' when rejected), including any text outside the root. Both reject malformed input up front: a < that doesn't start a tag, comment, CDATA section, processing instruction or declaration isn't valid XML (no SVG renderer would display it) and is the most expensive input to sanitize.

For an SSRF-safe fetch (private-address blocking, redirect re-validation, size caps — see below), use the library's own fetcher instead of the global fetch: const { get } = avtUtils.createFetcher();.

SSRF Protection

By default, ens-avatar includes built-in protection against Server-Side Request Forgery (SSRF) attacks in Node.js environments. This prevents malicious actors from using avatar URLs to probe internal networks.

Default behavior (recommended for production):

  • ✅ Only http: and https: URLs are fetched
  • ✅ Only globally routable unicast addresses are allowed: loopback, private, link-local, CGNAT, multicast, reserved, documentation and other special-purpose ranges are blocked, in every textual encoding (IPv4-mapped/NAT64 IPv6, trailing dots, …)
  • ✅ Every redirect hop is re-validated, and request headers other than Accept/Range (API keys) are dropped once a redirect leaves the original origin
  • ✅ Each request has one deadline covering the body, response bodies are size-capped, and redirects are limited (see Limits)

Custom agents: If you provide your own HTTP/HTTPS agents, ens-avatar will use them as-is without applying SSRF protection. You are responsible for securing your custom agents.

Local development: Set allowPrivateIPs: true to disable SSRF protection when you need to access local services (e.g., local IPFS nodes). Never use this in production.

See the Custom Agents section for more details.

Guarding CCIP-read

ens-avatar never makes CCIP-read (EIP-3668) requests itself: offchain lookups are performed by your chain client (ethers' provider or viem's ccipRead), with that client's HTTP stack and limits. A name's gateway is attacker-controlled input, so on a server:

Guard it with your client's hook — viem: ccipRead: { request } on createPublicClient; ethers: FetchRequest.registerGetUrl — and in it:

  • Check the URL you actually request, i.e. after substituting {sender} and {data} into the gateway template. Checking the template is not enough: https://{data}/ with data 0x7f000001 targets 127.0.0.1. Use avtUtils.validateUrl(url).
  • Don't let fetch follow redirects. Use redirect: 'manual' and run validateUrl on every Location hop, with a hop limit.
  • Bound each request (timeout, response size) and the lookup as a whole: the number of gateway URLs tried and nested lookups (ethers stops at 10, MAX_CCIP_REDIRECTS; viem has no limit of its own).
  • On Node, also check resolved addresses. validateUrl only sees the URL: a gateway hostname whose DNS points at a private address passes it. Make the gateway requests through an agent that checks the IP it connects to (e.g. an undici Agent with a validating lookup, or ssrf-req-filter).

Serving avatars

If you proxy or re-serve avatar bytes from your own origin, send X-Content-Type-Options: nosniff and a sandboxing CSP such as Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox, so a hostile file can't run as a page on your domain.


⚠️ SECURITY DISCLAIMER

While ens-avatar implements security measures to help protect against XSS and SSRF attacks, you are ultimately responsible for the security of your application. We strongly recommend:

  • Conducting your own security audits before deploying to production
  • Implementing additional security layers appropriate for your use case
  • Keeping the library updated to receive security patches
  • Following security best practices when handling user-generated content
  • Properly configuring all security-related options for your environment

This library is provided "as-is" without warranty. The maintainers are not liable for any security vulnerabilities in applications using this library.

Demo

  • Create .env file with INFURA_KEY env variable

  • Build the library

  • Node example

node example/node.js ENS_NAME
  • Browser example
pnpm build:demo
http-server example

版本与发布记录

@ensdomains/ens-avatar 共发布 38 个版本,首次发布于 2022/02/22;过去 12 个月发布了 8 个版本。

版本发布日期距今
2.0.0-alpha.4alpha2026/10/081 个月前
1.0.6latest2026/10/011 个月前
1.0.52026/09/301 个月前
2.0.0-alpha.32026/09/251 个月前
2.0.0-alpha.22026/06/234 个月前
2.0.0-alpha.12026/04/077 个月前

关于 @ensdomains/ens-avatar 的常见问题

@ensdomains/ens-avatar 是什么?

Avatar resolver library for both nodejs and browser.

如何安装 @ensdomains/ens-avatar?

在项目目录运行 npm install @ensdomains/ens-avatar,或使用 pnpm add / yarn add / bun add。

@ensdomains/ens-avatar 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ensdomains/ens-avatar 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@ensdomains/ens-avatar 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ensdomains/ens-avatar 可以商用吗?

@ensdomains/ens-avatar 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ensdomains/ens-avatar 的下载量有多少?

根据 npm 官方下载统计,@ensdomains/ens-avatar 在 2026/10/14 – 2026/11/12 这 30 天内被下载 2,566 次,日均约 86 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ensdomains/ens-avatar 还在维护吗?

最新版本 1.0.6 发布于 2026/10/01(1 个月前);至今共发布 38 个版本,其中过去 12 个月发布了 8 个;首次发布于 2022/02/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。