Node.js release pipeline
Most release pipelines are JavaScript — npm publish, semantic-release, GitHub Actions with Node actions. Until now, adding threshold signing to a JS pipeline meant either:
- Shelling out to a Ruby/Python CLI (process overhead, version drift, two runtimes).
- Calling the JSON-RPC daemon over a socket (correct, but another service to operate).
@confium/confium-node adds a third option: sign in-process
from JavaScript. No shell-out, no IPC, one runtime.
What changed
The Node binding (NAPI) shipped with the same surface as the
Ruby and Python bindings: composite sign + verify, transparency
log, PKI + CMS, threshold sessions (Cmp20, Gg18, FrostP256).
A Node release pipeline can now:
- Sign the npm package tarball before
npm publish - Sign the Docker image digest after
docker build - Sign the SBOM generated by
cdxgen/syft - Anchor every signing event in the transparency log
All from the same Node process that’s already running the release.
The pattern
// .github/actions/sign-release/index.js
const core = require("@actions/core");
const { CompositeSignature, Transparency } = require("@confium/confium-node");
const { execSync } = require("node:child_process");
const { readFileSync } = require("node:fs");
async function run() {
const artifactPath = core.getInput("artifact");
const secretHex = process.env.RELEASE_SIGNING_KEY_HEX;
const artifact = readFileSync(artifactPath);
// Sign
const sig = CompositeSignature.sign_ed25519({
message: artifact,
secretKey: Buffer.from(secretHex, "hex"),
});
// Anchor in transparency log
const tree = new Transparency.MerkleTree({
coordinatorUrl: process.env.COORDINATOR_URL,
});
const seq = await tree.append({
artifactType: "release_artifact",
artifactHash: sha256(artifact),
});
// Attach signature as an OCI ref / npm provenance / etc.
console.log(`Signed. seq=${seq}, sig=${sig.signature.toString("hex")}`);
}
run().catch((e) => core.setFailed(e.message));
Use cases this unlocks
| Pipeline step | What you sign |
|---|---|
npm publish |
The published tarball (package.tgz) |
| Docker build | The image digest (via docker build --digestfile) |
| GitHub Release | The release assets (.tar.gz, .zip) |
| Helm chart | The chart package (.tgz) |
| SBOM generation | The CycloneDX/SPDX document |
| Terraform module | The module archive |
Why not just use the JSON-RPC daemon?
You can. For teams that already operate confiumd in their
CI cluster, the daemon is simpler — one process, many consumers.
@confium/confium-node is for teams that don’t want to operate
a daemon in CI. The signing key lives in a CI secret; the
signature is produced in the same Node process that’s already
running the release. Fewer moving parts.
See also
- Node.js binding — full API surface.
- Container image signing — the multi-maintainer version of this pattern (threshold signing across multiple release engineers).
- SBOM signing — sign the SBOM alongside the artifact.
- Polyglot verification — when the daemon is the right answer instead.