
Security News
PHP Foundation Says Chainguard Broke Security Embargo Ahead of Emergency Patches
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.
@cesdk/node-native
Advanced tools

Native Node.js bindings for the IMG.LY Creative Engine, the core of CE.SDK.
A drop-in alternative to @cesdk/node
(WASM) that loads the engine as a platform-native N-API addon. The native
build adds video and audio export, GPU-accelerated rendering, and removes the
WASM 4 GB linear-memory ceiling.
For full Creative Engine documentation see https://img.ly/docs/cesdk/web/guides/headless/setup_node.
@cesdk/node-nativePick @cesdk/node-native if you need any of the following; otherwise stay on
@cesdk/node (WASM), which has wider platform coverage:
For raw call overhead the native addon also avoids the WASM↔JS boundary, but the practical impact is workload-dependent — measure on your own scenes before relying on a specific multiplier.
| OS | Arch | Node | Minimum OS |
|---|---|---|---|
| macOS | arm64 | ≥ 22 | macOS 12 (Monterey) |
| macOS | x64 | ≥ 22 | macOS 12 (Monterey) |
| Linux | x64 | ≥ 22 | glibc ≥ 2.39 (Ubuntu 24.04, Debian 13, RHEL 10) |
| Windows | x64 | ≥ 22 | Windows 10 / Server 2016 — see limitations |
device: 'gpu' throws and 'auto'
falls back to CPU raster. Expect lower throughput than macOS (Metal) for
raster-heavy exports; throughput scales with CPU cores.Not supported in this release:
arm64arm64@cesdk/node for serverless until a manylinux build lands)npm install @cesdk/node-native succeeds on unsupported hosts (the main
package itself has no platform filter), but the binary-carrying sibling is
filtered out by npm's os / cpu / libc constraints and the first
CreativeEngine.init() throws a clear error pointing at
@cesdk/node (WASM). This is intentional so that conditional installs in
Dockerfiles and CI don't fail at install time.
The addon is a standard Node-API (N-API) native module, so it also runs on JavaScript runtimes that implement Node-API — no runtime-specific code paths or configuration in the package. The full CE.SDK test suite (engine lifecycle, rendering, image/video/audio export, events, asset sources) is run against all three runtimes in CI:
| Runtime | Verified version | Notes |
|---|---|---|
| Node.js | ≥ 22 | Primary target |
| Bun | 1.3.14 | Both CJS require and ESM import |
| Deno | 2.9.3 | Needs permission flags — see below |
The listed versions are the ones CI pins and runs the suite against on every change. Newer releases are expected to work; older ones are not tested and earlier Bun/Deno releases are known to have had incomplete Node-API support.
Bun (the package ships prebuilt binaries and has no install scripts, so no
trustedDependencies entry is needed):
bun add @cesdk/node-native
bun my-script.js
Deno needs a local node_modules/ directory — native addons cannot be loaded
from Deno's global npm cache. deno add sets this up; in an existing project
add "nodeModulesDir": "auto" to deno.json. An existing Node.js project
that already has package.json + node_modules/ runs under Deno as-is.
deno add npm:@cesdk/node-native
deno run --allow-ffi --allow-read --allow-env my-script.js
--allow-ffi, --allow-read, and --allow-env are the required floor
(loading the addon and resolving the bundled assets). Add --allow-net when
scenes reference remote assets or you pass an API-key license, and
--allow-write when your script writes exported files to disk. As with any
native addon, code inside the .node binary itself is not sandboxed by
Deno's permission system.
Node.js loads the addon unless it runs with --no-addons. Under the
permission model (--permission, or --experimental-permission before
Node.js 22.13), add --allow-addons, and read access to node_modules with
--allow-fs-read.
Two caveats that apply to every runtime, including Node.js: the engine must
run on the main thread (CreativeEngine.init() inside a worker_threads
worker is not supported), and one engine instance must only ever be used
from the thread that created it.
npm install @cesdk/node-native
# or, to track nightly builds:
npm install @cesdk/node-native@dev
ES Modules:
import CreativeEngine from '@cesdk/node-native';
const engine = await CreativeEngine.init({ license: 'YOUR_LICENSE_KEY' });
const scene = engine.scene.create();
const page = engine.block.create('page');
engine.block.appendChild(scene, page);
engine.block.setWidth(page, 1920);
engine.block.setHeight(page, 1080);
const png = await engine.block.export(page, 'image/png', {});
// `png` is a Blob (same as @cesdk/node). Use `png.arrayBuffer()` to read.
engine.dispose();
CommonJS:
const CreativeEngine = require('@cesdk/node-native');
CreativeEngine.init({ license: 'YOUR_LICENSE_KEY' }).then(async (engine) => {
// ... same API as above.
engine.dispose();
});
Always call
engine.dispose()when done. The engine runs an internal update timer (driving rendering and export), so until you dispose it the Node.js event loop stays alive and the process will not exit on its own — a short-lived script or a Lambda handler that skipsdispose()will appear to hang until timeout.
The CreativeEditor SDK is a commercial product. Purchase a license at https://img.ly/pricing.
You can run the engine in evaluation mode by passing an empty string as the license key. In evaluation mode the engine prints an IMG.LY banner on stderr and watermarks output. There are three equivalent ways to enter it:
// 1. Empty string at init.
await CreativeEngine.init({ license: '' });
// 2. Omit `license` entirely at init, then unlock later.
const engine = await CreativeEngine.init();
engine.editor.unlockWithLicense(''); // evaluation
// engine.editor.unlockWithLicense('YOUR_KEY'); // upgrade in place
// 3. Inspect the active key (empty string => evaluation mode).
engine.editor.getActiveLicense();
The Linux platform package statically links libc++ / libc++abi /
libstdc++ into the addon, so it needs only the engine's runtime
dependencies. The ubuntu:24.04, debian:13 and node:22-trixie-slim
Docker images do not include them, so install them explicitly.
Every workflow needs these packages, image, scene and archive work
included. The addon links the GStreamer libraries, so it does not load
without them. ca-certificates lets the engine fetch https:// URLs,
such as remote assets and the license server for an API key:
apt-get update && apt-get install -y \
ca-certificates \
libcurl4 \
libssl3 \
libfontconfig1 \
libfreetype6 \
libuuid1 \
libxcb1 \
libgl1 \
libegl1 \
libgstreamer1.0-0 \
libgstreamer-plugins-base1.0-0
When a library that the addon links is missing, init() fails with
... is installed, but the system could not load it, and the next line
names the library.
Video and audio export additionally needs the GStreamer plugin sets:
apt-get update && apt-get install -y \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly \
gstreamer1.0-libav \
gstreamer1.0-gl
A future release will offer a self-contained GStreamer bundle shipped inside the Linux platform package; until then, system GStreamer is required.
Download the Linux x86_64 codec pack from your licensing dashboard. It is
distributed separately from this npm package and requires a CE.SDK license
that includes licensed codecs. GStreamer remains required; codec license
cleanup also needs Python 3 at /usr/bin/python3.
Pass your dashboard API key as the license option to CreativeEngine.init()
to reserve a codec license when processing video or audio.
Extract the pack for the user that runs Node.js. Replace YYYY-MM-DD with
the version in the downloaded filename:
mkdir -p "$HOME/.local/share/cesdk-codecs"
tar -xzf "$HOME/Downloads/licensed-codec-pack-YYYY-MM-DD.tar.gz" \
-C "$HOME/.local/share/cesdk-codecs"
The default directory must directly contain plugins/, presets/, and
misc/. CE.SDK automatically detects the pack, selects the licensed codecs,
and loads its plugins/ directory.
For a custom installation:
mkdir -p "$HOME/my-cesdk-codecs"
tar -xzf "$HOME/Downloads/licensed-codec-pack-YYYY-MM-DD.tar.gz" \
-C "$HOME/my-cesdk-codecs"
export CESDK_LICENSED_CODEC_PACK="$HOME/my-cesdk-codecs"
node index.mjs
You can also set the path directly in Node.js using process.env. Replace
your static CE.SDK import with a dynamic import so the assignment runs
before CE.SDK loads:
process.env.CESDK_LICENSED_CODEC_PACK = `${process.env.HOME}/my-cesdk-codecs`;
const { default: CreativeEngine } = await import('@cesdk/node-native');
For CommonJS, set the variable before require('@cesdk/node-native').
The override points to the pack root containing plugins/, presets/, and
misc/. A non-empty override replaces the default path; an unset or empty
override uses $HOME/.local/share/cesdk-codecs. Set it before loading
CE.SDK and restart the process after installing a pack or changing the path.
The Linux platform package ships with a glibc ≥ 2.39 floor (the
libc: ["glibc"] npm filter blocks Alpine/musl at install time;
a system with a glibc that is too old for the binary installs
successfully but fails at the first init() with
version 'GLIBC_2.XX' not found). Every "yes" image
still needs the packages above.
| Base image | Works? | glibc | Notes |
|---|---|---|---|
ubuntu:24.04 | yes | 2.39 | Reference image |
debian:13 (trixie, and :13-slim) | yes | 2.41 | |
node:22-trixie | yes | 2.41 | |
node:22, node:24, node:lts (and -slim) | no | 2.36 | Debian 12; use the -trixie tags |
gcr.io/distroless/nodejs22-debian13 | no | 2.41 | No apt for the runtime dependencies, so the addon does not load |
node:22-alpine | no | musl | Filtered at install |
amazonlinux:2 / :2023 | no | 2.26 / 2.34 | Below floor; also AWS Lambda base |
public.ecr.aws/lambda/nodejs:22 | no | 2.34 | Below floor; see Lambda note below |
Use @cesdk/node (WASM) on every "no" row above.
The Linux platform package targets glibc 2.39+; AWS Lambda's Amazon Linux
2 / 2023 base images ship glibc 2.26 / 2.34, both below the floor. On
Lambda, CreativeEngine.init() fails with version 'GLIBC_2.XX' not found. Use @cesdk/node (WASM) on Lambda.
The apps/cesdk_web_examples/cookbooks-aws-lambda/ cookbook documents
the WASM-on-Lambda path.
@cesdk/node-native ships a native .node binary that bundlers cannot
inline. Mark the package external and have the bundler emit the
.node + the assets/ resource bundle into the output as-is.
// next.config.js — server side only, never the browser bundle
module.exports = {
webpack(config, { isServer }) {
if (isServer) {
config.externals = [
...(config.externals ?? []),
'@cesdk/node-native',
'@cesdk/node-native-darwin-arm64',
'@cesdk/node-native-darwin-x64',
'@cesdk/node-native-linux-x64',
'@cesdk/node-native-win32-x64',
];
}
return config;
},
// Next.js 13+ App Router with server components:
experimental: {
serverComponentsExternalPackages: ['@cesdk/node-native'],
},
};
await esbuild.build({
// …
platform: 'node',
external: [
'@cesdk/node-native',
'@cesdk/node-native-darwin-arm64',
'@cesdk/node-native-darwin-x64',
'@cesdk/node-native-linux-x64',
'@cesdk/node-native-win32-x64',
],
});
export default defineConfig({
ssr: {
external: ['@cesdk/node-native'],
noExternal: [],
},
});
In every bundler the engine resource bundle (@cesdk/node-native/assets/)
must reach the runtime layout intact. Either keep node_modules/ next to
the bundle output, or copy node_modules/@cesdk/node-native/assets/
into the deploy artifact and point CreativeEngine.init({ baseURL }) at
that location explicitly.
Yarn PnP virtualises the node_modules graph through .pnp.cjs; the
default mode keeps optional dependencies inside the zip cache, which
breaks dlopen against the platform sibling package's .node because
the dynamic linker cannot read into a zip filesystem. The fix is to opt
the platform package out of PnP zipping by declaring it unplugged:
# .yarnrc.yml
nodeLinker: pnp
pnpEnableInlining: false
// package.json
{
"dependencies": { "@cesdk/node-native": "^1.85.0-rc.0" },
"dependenciesMeta": {
"@cesdk/node-native-darwin-arm64": { "unplugged": true },
"@cesdk/node-native-darwin-x64": { "unplugged": true },
"@cesdk/node-native-linux-x64": { "unplugged": true },
"@cesdk/node-native-win32-x64": { "unplugged": true }
}
}
The unplugged setting causes Yarn to extract the matching platform
sibling into .yarn/unplugged/, where the .node resolves to a real
filesystem path that dlopen can consume. The nodeLinker: node-modules
mode (Yarn classic compatibility) doesn't need this — it materialises a
real node_modules/ tree on disk and works out of the box.
@cesdk/nodeThe Block, Scene, Editor, Asset, Event, and Variable APIs are the same. For
the vast majority of code paths, @cesdk/node-native is a drop-in
replacement: change the import and run.
- import CreativeEngine from '@cesdk/node';
+ import CreativeEngine from '@cesdk/node-native';
Binary-returning methods (block.export, block.exportVideo,
block.exportAudio, block.saveToArchive, scene.saveToArchive) return
Blob on both packages. block.export() accepts both the WASM-shaped
(block, { mimeType, ... }) and the positional (block, mimeType, options)
signatures, so existing call sites don't need rewriting.
Default asset-source helpers (engine.asset.addDefaultAssetSources,
engine.asset.addDemoAssetSources) are available with the same options
(baseURL, excludeAssetSourceIds, sceneMode).
engine.update() is public on @cesdk/node-native (the wrapper pumps
frames internally during exports; you only need it if you're driving
frame-perfect work manually). On WASM it's private — the browser's
animation frame drives it.CreativeEngine.init rejects a baseURL that the engine cannot parse as a
URL, such as one with [ in its path. Percent-encode such a character.
@cesdk/node accepts the value, because its engine reads its bundled files
from the WASM data file.@cesdk/nodeFor test runs or third-party packages that import @cesdk/node by literal
name (e.g. internal importers/exporters), point your bundler/test runner's
module alias at @cesdk/node-native — its export shape matches @cesdk/node,
so no call sites need to change:
// e.g. in a vitest/vite config
resolve: { alias: { '@cesdk/node': '@cesdk/node-native' } }
In application code, prefer importing @cesdk/node-native directly.
@cesdk/node-native is the first IMG.LY-distributed Node binding to expose
video export. The two known foot-guns:
options.duration defaults to 0 for parity with the WASM
@cesdk/node package. The encoder treats 0 as "render a single frame",
which is rarely the intended outcome. Always pass an explicit duration:
await engine.block.exportVideo(page, 'video/mp4', onProgress, {
duration: 3.0,
framerate: 30,
});
Scenes loaded from an archive must wait for AV resources before
exportVideo can make progress; otherwise the encoder spins on resources
that haven't been fetched and the progress callback never fires. Pass
waitForResources: true when loading a video scene whose assets aren't
already in memory, or follow up with engine.block.forceLoadAVResource
(or forceLoadResources) on each video-fill block before calling
exportVideo:
const scene = await engine.scene.loadFromArchiveURL(file_url, {
waitForResources: true,
});
const [page] = engine.block.findByType('page');
const mp4 = await engine.block.exportVideo(page, 'video/mp4', onProgress, {
duration: engine.block.getTotalSceneDuration(scene),
framerate: 30,
});
The wrapper pumps update() internally during init(), so unlike
@cesdk/node (WASM), you do not need to drive a setInterval pump
loop around exportVideo. The export awaits naturally. Only call
engine.update() directly if you're driving frame-perfect work outside
of the built-in export path.
A runnable end-to-end example is in examples/video-export-3s.js of the
source repository.
@cesdk/node-native ships a different .node per platform via npm's
optionalDependencies (the same pattern as esbuild, sharp, etc.). One
rough edge to call out:
npm cion CI using a lockfile generated on a different architecture succeeds at install but fails atinit().
Recommended mitigations (pick one):
npm install --include=optional @cesdk/node-native on CI. Re-resolves
optional dependencies for the actual host. Slightly slower than npm ci,
robust.node:22 Docker
image for Linux consumers).Docker images that bake @cesdk/node-native into a final artifact should
always install inside the target image, never copy node_modules/ from a
different-arch host.
For air-gapped builds or custom distributions, set
CESDK_NATIVE_BINARY_PATH to an absolute path to the .node binary. The
loader uses it directly, bypassing require.resolve:
CESDK_NATIVE_BINARY_PATH=/opt/cesdk/cesdk_native.node node app.js
Set DEBUG=cesdk:native (or CESDK_DEBUG=1) to log loader resolution steps
when triaging install issues.
On first init() the loader streams a SHA-256 over the ~32 MB .node and
compares it to a checksum sidecar — a corruption/truncation guard (it is not
anti-substitution; that's npm provenance). This adds a few tens of milliseconds
to each cold start. For latency-sensitive serverless cold paths (e.g. AWS Lambda)
where the artifact is already integrity-checked by the platform, you can skip it:
CESDK_SKIP_BINARY_VERIFY=1 node app.js
Leave verification on by default; only opt out when cold-start latency matters and the binary's integrity is guaranteed by other means.
EventSubscriptionService::initEventCallbacks errorStateChanged) to
stderr. Capture or redirect stderr (node app.js 2>/dev/null,
child_process.spawn(..., { stdio: ['inherit', 'inherit', 'ignore'] }))
if it interferes with machine-readable output. A setLogLevel API is
tracked for a future release.node doesn't enforce
signatures on dlopen, so the addon loads out of the box for normal
Node.js usage. Electron apps that notarize the containing app must sign
the nested cesdk_native.node with their own Developer ID in their
afterSign hook before notarization. Hosts that enforce strict library
validation (some Electron variants, hardened runtime CI runners) will
reject the unsigned binary; full Developer ID + notarization on our side
is tracked as a follow-up.errorStateChanged
log line when a block transitions to an error state (e.g. a video-fill
resource fails to load). Read the per-block state via
engine.block.getState(blockId) after update() to inspect the failure;
a top-level error subscriber is tracked as a follow-up.Failed to load the @cesdk/node-native addon for ...When the message says that the platform package is installed, but the system could not load it, the next line names the cause. On Linux it is
usually a missing system library: install the packages in
Linux runtime dependencies. The loader names
only the first missing library; this lists all of them:
ldd node_modules/@cesdk/node-native-linux-x64/cesdk_native.node | grep 'not found'
A cause such as
invalid ELF header or not valid mach-o file means a damaged file:
reinstall the package. The sections below cover an old glibc and a blocked
macOS binary.
When the message says that the JavaScript runtime does not allow the native addon, give the runtime the flags in
JavaScript runtimes.
When the message recommends npm install --include=optional, the
platform package or its binary is missing: npm skipped
optionalDependencies, or the install is incomplete. Reinstall:
npm install --include=optional @cesdk/node-native
For pnpm and Yarn, regenerate the lockfile on the target platform/arch
(easiest via a Docker build), or use per-platform lockfiles. Neither
package manager has a direct equivalent of npm's --include=optional.
version 'GLIBC_2.XX' not foundThis build targets glibc ≥ 2.39 — see "Supported platforms". Stay on
@cesdk/node (WASM) for older systems until a manylinux build lands.
library load disallowed by system policyThe published binary is not code-signed. npm install doesn't add the
com.apple.quarantine xattr so this error doesn't show up there, but if
you obtained the package via a browser download or a sideloaded zip, the
quarantine flag will block dlopen. Strip it with:
xattr -d com.apple.quarantine node_modules/@cesdk/node-native-*/cesdk_native.node
The CreativeEditor SDK is a commercial product. Purchase a license at https://img.ly/pricing.
FAQs
CE.SDK Creative Engine for Node.js — native bindings
The npm package @cesdk/node-native receives a total of 5,815 weekly downloads. As such, @cesdk/node-native popularity was classified as popular.
We found that @cesdk/node-native demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 8 open source maintainers collaborating on the project.

Security News
The PHP Foundation says Chainguard broke a security embargo ahead of critical RCE patches, as leaks disrupted the coordinated release.

Security News
PHP releases emergency patches for two critical RCE vulnerabilities as agencies urge organizations to take unpatched sites offline.

Threat Research
/Security News
A supply chain attack compromised LiteLLM 1.107.0 on PyPI and Docker, spreading malware that steals cloud credentials, developer secrets, and AI provider keys.