Publishing
The repository publishes a VitePress site from docs-site/.
Local Build
pnpm install
pnpm run docs:buildPreview the built site:
pnpm run docs:previewRun the same docs lane used by CI:
./run.sh docsWhat The Default Build Does
The default docs build is intentionally small:
- synchronize the published specification pages from
w3c-spec/ - build VitePress static output into
docs-site/.vitepress/dist/
The synchronized specification pages are generated build outputs and are not committed. The build does not compile Compact contracts, rebuild managed artifacts, run TypeDoc, or start Docker-backed tests.
Optional API Reference
Generated TypeDoc pages remain available as a local maintenance tool:
pnpm run docs:apiThis is not part of the default Pages build because it compiles package outputs and is too heavy for docs-only CI.
Pages Deployment
See GitHub Pages for the repository setting and workflow behavior.
Package And ZK Artifact Publication
The Publish npmjs Packages and ZK Artifacts workflow publishes the five DID packages to npmjs and publishes the matching ZK artifact bundle to GHCR and, for RC/final releases, GitHub Release assets.
The workflow publishes these package workspaces in dependency order:
@midnight-ntwrk/midnight-did-jubjub-schnorr@midnight-ntwrk/midnight-did-contract@midnight-ntwrk/midnight-did-domain@midnight-ntwrk/midnight-did@midnight-ntwrk/midnight-did-api
The root workspace and docs-site remain private. The package workspaces are publishable and keep publishConfig.registry pointed at https://registry.npmjs.org/ with publishConfig.access: "public".
npmjs publication uses npm Trusted Publishing. Build, dependency installation, packing, signing, GHCR publication, and GitHub Release work run outside the npm-release environment. Only the minimal npm-release job has both that environment and id-token: write; it sparsely checks out only immutable publisher scripts plus the Node version pin, downloads the package artifact by immutable ID, verifies the producer archive digest and exact checksummed inventory, rechecks npm CLI >=11.5.1 at the mutation boundary, and publishes the already packed tarballs with lifecycle scripts disabled. No npm publication secret is required. GITHUB_TOKEN remains limited to repository-scoped operations; packages: write is held by the separate GHCR job and is not available to the npm publisher.
Before the first manual publication, npm administrators must add a Trusted Publisher to each package:
| npm package | Organization | Repository | Workflow | Environment |
|---|---|---|---|---|
@midnight-ntwrk/midnight-did-jubjub-schnorr | midnightntwrk | midnight-did | publish.yml | npm-release |
@midnight-ntwrk/midnight-did-contract | midnightntwrk | midnight-did | publish.yml | npm-release |
@midnight-ntwrk/midnight-did-domain | midnightntwrk | midnight-did | publish.yml | npm-release |
@midnight-ntwrk/midnight-did | midnightntwrk | midnight-did | publish.yml | npm-release |
@midnight-ntwrk/midnight-did-api | midnightntwrk | midnight-did | publish.yml | npm-release |
Repository administrators must configure required npm-release reviewers with self-review prevention and exact selected deployment branches main and develop, excluding tags, wildcard branches, and all other branches. Repository code cannot inspect or prove those external settings; an environment administrator must attest them before a publication is dispatched. No additional secret is required. After confirming no other workflow depends on the old organization credential, it can be removed from this normal release path.
The environment deployment-branch restrictions and required reviewers are the actual protection boundary. Workflow event/ref conditions, immutable-SHA checkout, runtime assertions, and secret step scoping are defense in depth, not substitutes for those GitHub settings. An environment administrator must attest the complete configuration before any publication is dispatched.
Publication channels:
| Channel | Trigger | Branches | Version shape | npm tag | ZK artifacts |
|---|---|---|---|---|---|
| Snapshot | Manual workflow dispatch | develop | x.y.z-snapshot.<run>.<sha> | snapshot | Workflow artifact and GHCR OCI artifact |
| RC | Manual workflow dispatch | main, develop | x.y.z-rc{index} | rc | GitHub Release asset and GHCR OCI artifact |
| Release | Manual workflow dispatch | main only | x.y.z | latest | GitHub Release asset and GHCR OCI artifact |
Every manual dispatch requires version to be exactly one stable SemVer base, such as 0.6.0. Do not supply a leading v, prerelease/build suffix, whitespace, or control characters. Snapshot and RC suffixes are generated by the workflow from the selected channel, RC index, run number, and commit SHA. The process environment itself cannot represent NUL: the operating-system process boundary rejects an environment variable containing NUL before the resolver can run. The same stable-SemVer rule rejects every representable control character before any GitHub output record is written.
For RC and final releases, the finalizer extracts the body beneath exactly one Keep-a-Changelog heading matching the stable base version, for example ## [0.6.0] - Unreleased or a valid dated heading. Extraction is dependency-free and fails before the privileged boundary for a missing, duplicate, empty, or malformed section, an unexpected level-two boundary, an invalid version, or an unsafe output path. The privileged publisher receives the generated file only through --notes-file; it does not construct notes from event text or shell arguments. The build job performs the same extraction and canonical-byte check before npm, GHCR, tag, or GitHub Release mutation can begin; the finalizer re-extracts from the same immutable dispatch SHA at the privileged boundary.
The workflow revalidates the event, exact full source ref, branch ref type, channel, base version, resolved version, and RC index immediately before signing or publishing. Snapshots require refs/heads/develop; RCs allow refs/heads/main or refs/heads/develop; final releases require refs/heads/main. A dispatch from another branch or a tag, even one named main or develop, fails in a checkout-free, secret-free job before environment approval or repository checkout. An eligible dispatch then checks out the immutable dispatch SHA rather than a later-moving branch head. These checks consume GitHub's full ref directly rather than trusting short ref names. A release fix made directly on main must also be synchronized back to develop before any later develop snapshot or RC.
The publication workflow has no push trigger. Pushes to every branch, including develop, and PR merges cannot start any publication job. Snapshot, RC, and final publication all require an explicit manual dispatch; the exact-ref checks above still fail closed before a build or privileged job can run.
Trusted Publishing prerequisite check
After the unprivileged build job has uploaded the package artifact, the separate npm-release job downloads it by immutable artifact ID and verifies the producer's archive digest, checksum-manifest digest, exact five-tarball inventory, packed manifest ownership metadata, and all tarball checksums. It then runs a fail-closed prerequisite check directly at the publication boundary. It installs no repository dependencies and runs no package, Compact, or lifecycle build. The checker verifies the exact repository, publish.yml workflow ref, allowed branch ref, GitHub-hosted runner, npm-release environment expectation, and presence of both GitHub OIDC request-capability variables. It does not request, decode, or log an OIDC JWT or any environment secret. Ambient NODE_AUTH_TOKEN, NPM_TOKEN, NPM_ID_TOKEN, npm credential/client-certificate settings, registry redirects, and NODE_OPTIONS runtime injection are rejected before npm is invoked.
The checker requires npm CLI >=11.5.1, derives the canonical five-package inventory from did-workspace-catalog.mjs, and performs bounded unauthenticated public metadata reads. Artifact inventory validation separately checks every packed package's npmjs registry, public access, package name, version, and repository ownership metadata. The publisher rechecks the npm minimum immediately before the first mutation. npm runs without a shell, with isolated auth-free configuration, bounded output/time, and raw provider output suppressed on failure. The publisher repeats the input guard immediately before each mutation and launches npm with a narrowly allowlisted environment containing only isolated npm paths plus required GitHub OIDC/provenance context.
A successful prerequisite check proves only observable repository, workflow, runner, CLI, manifest, and public-readability prerequisites. npm exposes no cheap unauthenticated API for this relationship, so the npm-side Trusted Publisher mapping is not verified and cannot be verified without an actual publish. Five sequential publishes also remain non-transactional.
Distribution Use Cases
Publication supports these consumer paths:
| Use case | Source | Intended consumer |
|---|---|---|
| Snapshot validation | npmjs packages plus GHCR OCI artifact | CI, release engineers, downstream repository smoke tests |
| RC/release validation | npmjs packages plus GitHub Release asset | Release engineers and users who want stable HTTPS assets |
| Public npm consumption | npmjs packages plus GitHub Release asset | Public users who install packages from registry.npmjs.org |
| Server-side runtime bootstrap | GHCR OCI artifact or unpacked release asset | Node services, DID resolver/manager services, and CI jobs that cache proving keys |
The publish workflow rebuilds the packages and managed Compact artifacts, checks package contents, creates a ZK artifact bundle, validates the bundle, publishes packages to npmjs, smoke-tests the exact package version from npmjs, pushes the bundle to GHCR, pulls it back, validates it, and fetches every circuit through FetchZkConfigProvider over runtime HTTP. RC and release runs also upload the bundle to a GitHub Release and download the asset back for the same validation.
Concrete release-train examples in this page are validated against the root package.json version so package versions, package examples, and artifact names move together.
ORAS and GHCR artifacts
The workflow installs ORAS before the GHCR step because GHCR stores the ZK bundle as a generic OCI artifact. npm publish handles TypeScript packages, and the immutable GitHub Release creator submits the complete reviewed asset set in its initial request, but neither path pushes arbitrary provider-key archives to an OCI registry. ORAS provides the registry protocol client for oras push and oras pull.
The publish workflow downloads the configured ORAS_VERSION, verifies the ORAS release checksum, installs the oras binary, pushes the ZK archive and manifest to ghcr.io/<owner>/midnight-did-zk-artifacts:<version>, pulls it back, and then runs bundle validation plus the FetchZkConfigProvider smoke test.
Local developers need ORAS only when manually testing the GHCR artifact path. The Nix development shell includes ORAS. Outside the Nix shell, install it with Homebrew or the upstream release instructions:
nix develop
oras version
brew install oras
oras versionThe local bundle and GitHub Release asset checks do not require ORAS.
Package Artifact Metadata
@midnight-ntwrk/midnight-did-api embeds release-artifact metadata for the same version as the published package. Use it when a service or downstream test needs to derive the matching ZK artifact location instead of hard-coding URLs:
import {
MIDNIGHT_DID_API_VERSION,
createMidnightDidZkArtifactLocations,
} from "@midnight-ntwrk/midnight-did-api";
const locations = createMidnightDidZkArtifactLocations(
MIDNIGHT_DID_API_VERSION,
);
console.log(locations.ghcr.reference);
console.log(locations.githubRelease?.archiveUrl);For snapshot versions, githubRelease is null because snapshots are published as workflow artifacts and GHCR OCI artifacts. For RC and final release versions, githubRelease contains the expected release tag and asset URLs.
Exact npm package versions are immutable. The publication flow is designed for safe reruns after partial failure:
- npm skips an existing version only after verifying its immutable payload and confirming that it already owns the requested dist-tag;
- GHCR preserves an existing version tag, pulls it back, and verifies the bundle payload and manifest instead of overwriting it;
- GitHub Release bodies and assets are immutable: initial creation uses the reviewed generated changelog notes and complete canonical asset multiset; a rerun requires raw byte equality for the LF-terminated body and rejects every duplicate, extra, or missing asset instead of editing or uploading;
- generated
multiple.intoto.jsonlprovenance is cryptographically and semantically verified before creation; reuse downloads and verifies canonical provenance before any remote package or archive is parsed; - both paths bind every non-provenance subject, the exact source SHA/ref, workflow inputs, material, and resolved release tag target, then reverify the downloaded byte set before parsing it;
- ZK archives use a reproducible timestamp, ordering, ownership, and gzip header so equivalent builds produce the same payload.
A remote artifact, non-canonical release body, or mismatched asset multiset fails closed rather than being replaced or repaired. Body equality is raw UTF-8 byte equality: LF line endings and exactly one terminal newline are canonical, while CRLF is a mismatch. Only the exact bounded release not found response permits creation; authentication, rate-limit, server, timeout, output-limit, and malformed failures stop with provider output suppressed. RC and final releases receive their SLSA provenance before the immutable GitHub Release is created, and the reviewed notes file plus all release assets are supplied in the initial creation request. The reusable SLSA workflow is pinned to an exact commit and compiles its generator from that pinned source; release-binary mode is not used because it requires the reusable workflow reference to be a version tag rather than the repository's required immutable commit pin. This keeps a partial publication recoverable without making an immutable release mutable.
npm preflight, partial failure, and retry
Before the first publish, the npm publisher:
- requires the exact
https://registry.npmjs.org/registry and rejects every ambient npm token, credential, OTP, client-certificate input, registry redirect, andNODE_OPTIONSinjection before any npm invocation; - derives the dependency-ordered workspace inventory from
did-workspace-catalog.mjs --publish-workspacesand requires exactly the five canonical pre-packed tarballs; - verifies every packed manifest name/version and records each local tarball integrity; and
- completes all-five public package-name, exact-version, immutable-payload, and dist-tag reads.
An exact-version E404 is considered absent only after package-level public readability succeeds. Missing, extra, malformed, ambiguous, or mismatched evidence fails closed. Before any missing package is published, every existing exact version must already match both the packed payload and requested tag. For non-latest channels, the target version must not own latest.
Missing packages are published in catalog dependency order with the npm CLI:
npm publish --provenance --ignore-scripts --tag "${NPM_TAG}" --access public \
--registry "https://registry.npmjs.org/" "<prepacked-tarball>"npm publish --tag creates the initial requested tag. The normal Trusted Publishing workflow never invokes npm access or mutating npm dist-tag commands. Existing wrong/missing tags or access requiring repair therefore fail closed; an npm administrator must repair them through a separately authorized, documented maintenance process outside this normal OIDC workflow. No such maintenance workflow is currently provided.
Every npm registry command has a strict output and wall-clock bound. Fallback tarball identity downloads have connect/overall timeouts and a 100 MiB size limit. After every successful publish, payload and requested-tag read-back must succeed before the next dependent package can publish; a final all-five read-back is retained. This reduces partial-publication risk but cannot make five writes transactional. If a run fails:
- Record the workflow SHA, exact version, channel/tag, first failing package, and final publisher evidence. Do not replace a version merely to hide a partial snapshot.
- Retry the exact original source SHA and ref with the same version/channel/tag. A publish that succeeded despite a lost response is recognized only when its immutable payload and requested tag both match; only missing packages continue in dependency order.
- Stop when any existing payload/tag differs or any read is ambiguous. Do not overwrite, unpublish, change access, or mutate tags in automated recovery.
- Treat npm publication as complete only after final all-five public metadata, payload, and tag read-back plus the public npmjs smoke test succeeds. Continue using the existing GHCR/GitHub Release pull-back checks for ZK artifacts.
The ZK bundle preserves the provider layout used by Midnight JS:
manifest.json
keys/<circuit>.prover
keys/<circuit>.verifier
zkir/<circuit>.bzkirAfter unpacking the bundle, the directory can be used as the root passed to NodeZkConfigProvider. A future archive-backed provider can download a GitHub Release asset or GHCR OCI artifact once, unpack it into a cache, and delegate to the same layout used by FetchZkConfigProvider or NodeZkConfigProvider.
Local Checks
pnpm run build:all
pnpm run packages:check-contents
export VERSION="0.6.0-snapshot.local"
export ZK_ARCHIVE="artifacts/zk/midnight-did-zk-artifacts-${VERSION}.tar.gz"
pnpm run zk-artifacts:bundle -- --version "${VERSION}"
pnpm run zk-artifacts:check -- "${ZK_ARCHIVE}"
pnpm run published-artifacts:smoke -- --skip-npm --zk-archive "${ZK_ARCHIVE}"Testing Publication
PR CI validates package contents, ZK bundle structure, package imports, docs, and the normal core/API lanes. It does not publish packages or push GHCR artifacts.
Merging a PR is non-publishing. After all five npm Trusted Publisher mappings and the protected npm-release environment prerequisites are configured and attested, a release owner can later dispatch a 0.6.0 snapshot manually from the exact develop branch with:
gh workflow run publish.yml --repo midnightntwrk/midnight-did --ref develop --field channel=snapshot --field version=0.6.0Use the version printed by that workflow's summary for public smoke testing:
export VERSION="0.6.0-snapshot.<run>.<sha>"
export GH_TOKEN="<github-token-with-repo-read>"
export OCI_REF="ghcr.io/midnightntwrk/midnight-did-zk-artifacts:${VERSION}"
pnpm run published-artifacts:smoke -- \
--version "${VERSION}" \
--registry https://registry.npmjs.org \
--oci-ref "${OCI_REF}"For an RC or final release, smoke-test both public distribution paths:
export VERSION="0.6.0-rc1"
export GH_TOKEN="<github-token-with-repo-read>"
export OCI_REF="ghcr.io/midnightntwrk/midnight-did-zk-artifacts:${VERSION}"
pnpm run published-artifacts:smoke -- \
--version "${VERSION}" \
--oci-ref "${OCI_REF}"
pnpm run published-artifacts:smoke -- \
--version "${VERSION}" \
--github-release-tag "v${VERSION}"The smoke test installs the exact package version from the selected registry, imports all package entry points, verifies the API package's embedded artifact metadata matches the requested version, downloads or pulls the ZK bundle, validates the bundle manifest, and fetches every circuit through FetchZkConfigProvider over runtime HTTP.
Standalone Release Smoke
Use the Published Release Standalone Smoke workflow after publishing an RC or release when you need end-to-end confirmation that the published packages and GitHub Release ZK assets work together. The workflow installs the exact @midnight-ntwrk/* package version from npmjs, downloads the matching release archive, unpacks it, boots the standalone Midnight environment, deploys a DID contract, adds a verification method, adds an authentication relation, adds and updates a service, and resolves the updated DID document.
Local equivalent:
export VERSION="0.6.0-rc1"
export GH_TOKEN="<github-token-with-repo-read>"
pnpm run published-standalone:smoke -- \
--version "${VERSION}" \
--github-release-tag "v${VERSION}"GH_TOKEN must be able to read the repository release asset. The script starts Docker Compose from packages/api/standalone.yml by default. If a standalone environment is already running, pass --use-existing-standalone and set INDEXER_URL, INDEXER_WS_URL, NODE_RPC_URL, and PROOF_SERVER_URL as needed.
Published API packages can use unpacked release keys by setting MIDNIGHT_DID_ZK_CONFIG_PATH to the bundle root containing manifest.json, keys/, and zkir/. The API also prefers the installed contract package's dist/managed/did directory when bundled managed artifacts are available.