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:

  1. Shelling out to a Ruby/Python CLI (process overhead, version drift, two runtimes).
  2. 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