What Changed
Until now, the only JavaScript available inside a Lit Action was what shipped with the runtime:ethers (the v5 version) and the Lit.Actions SDK. If you needed anything else, you had to inline it or bundle it into your action code before uploading to IPFS.
That limitation is gone. Lit Actions can now import ES modules at runtime from jsDelivr, a public CDN that serves npm packages as ready-to-run ESM. You write a standard import statement with a version-pinned package specifier and the runtime resolves it to jsDelivr, fetches, verifies, caches, and executes it.
How It Works
Every import goes through three stages before any bytes reach the V8 engine.1. Resolution
The runtime resolves the import specifier to a jsDelivr URL. You can write a short npm specifier (zod@3.22.4), an explicit ESM specifier (zod@3.22.4/+esm), or a full URL (https://cdn.jsdelivr.net/npm/zod@3.22.4/+esm). When no file path is specified after the version, the runtime automatically appends /+esm to request the ESM entry point from jsDelivr. Bare package names without a version (import { z } from "zod") and relative paths (./util.js) are rejected. Every import must include a pinned version.
2. Integrity Verification
Each module URL is checked against anintegrity.lock manifest that maps URLs to their expected SHA-384 hash. If the hash of the downloaded content does not match, the import fails and the action does not execute.
For modules not yet in the manifest, the system uses trust-on-first-use (TOFU) with up to four-way verification: it fetches the module twice from jsDelivr, independently computes the SHA-384 of each response, verifies both against jsDelivr’s SRI hash header when available, and if the import specifier includes an inline #sha384- hash, verifies against that as well. The module is accepted only if all checks agree. The verified hash is then pinned to the lockfile so all future fetches are verified against it.
3. Caching
Once a module is verified, its source is held in an in-memory cache. Subsequent imports of the same URL (from any action execution) are served from cache without a network request.Import Syntax
Imports use an npm-style specifier with a pinned version. The runtime automatically resolves these to jsDelivr URLs and appends/+esm when no file path is specified.
@<version> pin is required and ensures you always get the exact same bytes. The /<file> part is optional. When omitted, /+esm is automatically appended to request the package’s ESM entry point from jsDelivr. You can also specify a path to a specific file in the package.
Inline Integrity Hash
You can append a#sha384-<hash> fragment to any import specifier to declare the expected integrity hash directly in your code. The runtime will verify the fetched content against this hash before execution.
integrity.lock. The /+esm suffix is still auto-appended when no file path precedes the # fragment.
The fragment is never sent over the network (per the URL specification). It is stripped before fetching and used only for local verification.
Full URLs
Full jsDelivr URLs are also accepted, with or without an inline hash:Examples
Validate Input with Zod
Format and Sign a Timestamp Proof
Encode Data as CBOR Before Signing
JSON Schema Validation with AJV
Package Compatibility
Not every npm package works. The package must meet these requirements:
Most modern packages ship ESM. Some well-known examples that work:
- zod — Schema validation
- ajv — JSON Schema validation
- date-fns / date-fns-tz — Date utilities
- cbor-x — CBOR encoding/decoding
- uuid — UUID generation
- lodash-es — Utility functions (ESM build)
- preact — Lightweight UI rendering (for server-side HTML generation)
- superstruct — Structural validation
- axios — depends on Node.js
httpmodule - lodash (non-ESM) — CJS only, use
lodash-esinstead - sharp — native binary addon
- bcrypt — native binary addon
If you are unsure whether a package ships ESM, check its
package.json for an "exports" or "module" field, or test the jsDelivr URL directly in a browser: https://cdn.jsdelivr.net/npm/<package>@<version>/+esmComposability — Publish Your Own Packages
The same import mechanism that pulls inzod or date-fns works for packages you publish yourself. This is the recommended way to share business logic and reusable functions across many Lit Actions: extract the common code into an npm package, publish it, and import it — version-pinned — in every action that needs it.
Lit Actions are standalone programs. There is no shared filesystem between them, relative imports (./util.js) are rejected, and there is no project-level bundling step. Without a shared module mechanism, every action would have to inline its own copy of any helper. Publishing reusable code to npm turns that copy-paste into a real dependency:
- One source of truth — fix a bug or add a feature in the package, publish a new version, and bump the pinned version in each action that consumes it.
- Smaller, readable actions — each action expresses only its unique logic; shared validation, encoding, RPC helpers, and domain logic live in the package.
- Auditable, immutable dependencies — every published version is integrity-verified and pinned exactly like any third-party package.
Requirements for your package
Your package must meet the same compatibility requirements as any other import:- Ship ESM. Set
"type": "module"and point"exports"(or"module") at an ESM build. If you write TypeScript, compile to ESM ("module": "ESNext","target": "ESNext") and publish the.jsoutput. - No Node.js built-ins or native addons. The code runs in the Deno/V8 sandbox — avoid
fs,path,crypto,http, and the like.ethers(v5) and theLit.ActionsSDK are runtime globals; reference them as globals or accept them as arguments rather than bundling them. - Keep the dependency tree small and ESM-only. Each transitive dependency is fetched and verified too, and must itself ship ESM. Zero-dependency packages are the easiest to audit and the most reliable.
package.json:
Consuming it in an action
Once published to npm, import it like any other package — with a pinned version:integrity.lock. Every subsequent execution — from any action — is served from cache and checked against the pinned hash.
Versioning and action identity
Because the import specifier (including the version) is part of your action’s source, the version you pin is baked into the action’s IPFS CID:- A published npm version is immutable — npm will not let you republish different bytes under the same version number. So a pinned import always resolves to the same code, which keeps your action’s CID — and therefore its action-derived identity key — stable.
- Bumping the version (
@1.0.0→@1.1.0) changes the action source, which produces a new CID and a new action identity. Treat a dependency bump like any other change to action code: re-deploy, re-permission, and — if you rely on action-identity signing — update any verifier that pins the old address.
Why jsDelivr
We evaluated several CDN options for serving npm packages as ESM. The choice came down to a set of non-negotiable requirements for running third-party code inside a cryptographic signing environment.The Requirements
- Immutability — The same URL must return the exact same bytes forever. If content can change, integrity hashes become meaningless.
- No server-side transformation — The CDN must serve the original files from the npm tarball. Any server-side bundling or transpilation introduces a layer we cannot audit or pin.
- Version pinning — URLs must support exact version locks (
@3.22.4) so that the resolved content is deterministic. - SRI hash support — The CDN should support Subresource Integrity headers so hashes can be computed and verified against the original source.
How jsDelivr Meets Them
jsDelivr with pinned versions serves raw files directly from npm packages at version-pinned URLs that are guaranteed immutable. Once a version is published to npm, the content behindhttps://cdn.jsdelivr.net/npm/zod@3.22.4/+esm never changes. jsDelivr does not perform any transformation, bundling, or minification on the source files. What the package author published to npm is exactly what gets served. Chipotle enforces this immutability guarantee by validating the SHA-384 hash of every module on every fetch, so even if a CDN were to serve altered content, the integrity check would catch it and reject the module before execution.
jsDelivr also provides built-in SRI hash support and is backed by a multi-CDN infrastructure (Cloudflare, Fastly, and others) with high availability and global edge caching.
Alternatives Considered
The key constraint is that the package must already ship ESM in its published npm tarball. jsDelivr does not convert CJS to ESM. Most modern packages do ship ESM, but older CJS-only packages will not work without a conversion step that happens before publishing.
Security Model
Module imports operate under the same security model as the rest of the Lit Actions runtime. Every module is verified before it reaches V8.Limits
Module imports are subject to the same resource limits as the rest of your action:- Memory — Imported modules count toward the action’s memory limit (default 128 MB).
- Timeout — Module fetch time counts toward the action’s execution timeout (default 15 minutes).
- Network — Module fetches use a separate HTTP client from the action’s
fetch()API and do not count toward the per-action fetch limit. However, they share the same execution timeout. - Module size — Individual modules are capped at 10 MB.
Debugging Imports
UseLit.Actions.showImportDetails() to inspect which modules were loaded and their integrity hashes. This is useful for debugging import resolution, verifying that the expected modules were fetched, and auditing the integrity of imported code.
console.log output in the response logs.
Next Steps
- Examples — More action patterns using the built-in SDK
- Patterns — Advanced patterns like gating logic and action-identity signing
- Lit Actions SDK — Full API reference