Status, versions and releases

Where each package is in its life cycle, what is stable, and what is still moving.

This repository produces one crate on crates.io, two packages on npm, and several units that are deliberately unpublishable. All of it is pre-1.0. This page states where each unit is, what changed in each released version, and which parts of the release machinery a consumer should care about — with no forward-looking dates, because none are promised.

What is published today#

UnitRegistryStatus
candid-corecrates.ioPublished — latest 0.1.0-beta.3. Also on the registry: 0.1.0-beta.1, 0.1.0-beta.2. None yanked.
@candid-core/schemanpmPublished — latest 0.2.0; beta 0.3.0-beta.1. Also on the registry: 0.0.0-bootstrap, 0.1.0, 0.1.1.
@candid-core/clinpmPublished — latest 0.1.0; beta 0.2.0-beta.1. Also on the registry: 0.0.0-bootstrap.
candid-core-ts—publish = false by design. Use it as a path or git dependency from this repository, or not at all.
candid-core-wasm—publish = false. Its own workspace root; its deliverable is the npm CLI package above.
candid-core-fuzz—publish = false. Internal fuzz harness, its own workspace root and lockfile.
Published as a beta

The table above is what the registries hold. latest is still @candid-core/schema 0.2.0 and @candid-core/cli 0.1.0. The repository has moved past them: @candid-core/schema 0.3.0-beta.1 and @candid-core/cli 0.2.0-beta.1, published on 2026-10-02 under the npm beta dist-tag, change value shapes, remove subpaths and change what the generator emits. Migrating from 0.2.0 lists every change with before and after code, and each is recorded under those versions in the schema and CLI changelogs. Every other page describes the repository, which today is what the betas ship.

The dependency lines that exist are these:

tomlREADME.md
# every feature on by default
candid-core = "=0.1.0-beta.3"
bash
# latest
npm install @candid-core/schema
npm install --save-exact @candid-core/cli@0.1.0
# the 0.3 beta pair, under the beta dist-tag
npm install --save-exact @candid-core/schema@0.3.0-beta.1
npm install --save-dev --save-exact @candid-core/cli@0.2.0-beta.1
A caret requirement selects nothing today

Every published version of candid-core is a prerelease, and Cargo only picks a prerelease when the requirement names one. So candid-core = "0.1" resolves to nothing, and a bare cargo install candid-core fails with could not find candid-core in registry crates-io with version *. Write the exact version: candid-core = "=0.1.0-beta.3", or cargo install candid-core --version 0.1.0-beta.3 --locked for the binary. Pinning with = is the right choice for a second reason too — with the API and the wire format both unstable, moving to the next prerelease should be something you opt into.

The CLI#

@candid-core/cli compiles the same pipeline to WebAssembly, so a JavaScript-only project can go from a .did file to a generated module and a Contract envelope with no Rust toolchain. Its binary name is candid-core-cli. Version 0.1.0 was published on 2026-08-27 by a dispatch of its release workflow; the first publish of an npm name is permanent, and it was the owner's explicit act. 0.2.0-beta.1 followed on 2026-10-02 under the beta dist-tag, dispatched from a65d7e2 after @candid-core/schema 0.3.0-beta.1 (its run, same commit), whose exact version it declares as its optional peer.

bash
npx @candid-core/cli@0.1.0 gen ./service.did -o ./generated         # latest
npx @candid-core/cli@0.2.0-beta.1 gen ./service.did -o ./generated  # beta

From a clone, the build is:

bashcrates/candid-core-wasm/npm/package.json
# from crates/candid-core-wasm/npm
npm run build
# runs: cd .. && wasm-pack build --target web --out-dir npm/wasm --no-pack --release

Its changelog names the candid-core revision each release embeds — 0.1.0-beta.3, the crate's current release, in both 0.1.0 and 0.2.0-beta.1 — rather than pairing by version number. 0.2.0-beta.1's embedded source is ahead of that crates.io archive by one change not yet in a crate release (a leading UTF-8 byte order mark is accepted), and its changelog entry says so. See @candid-core/cli for what it does.

The pre-1.0 policy, in full#

Both changelogs state it, and it is not boilerplate. Until 1.0, any release may change the public Rust API, the serialized Contract, Compilation and envelope shapes, the canonical bytes, and therefore every identity computed over them.

Read the last clause carefully, because it is the one with teeth. contract_id, interface_id and source_bundle_id are SHA-256 digests over canonical bytes. If a release changes how those bytes are produced, every identity moves — for inputs that did not change at all. Such a change would be a canonicalization-profile change and would be recorded in the changelog, but a stored identity from one version is not guaranteed to match a recomputation under the next. Treat identities as stable within a pinned version.

The consequences for you: pin an exact version everywhere, in Cargo and in npm. Do not rely on a caret range. If you persist an identity in a database or a signature, record the crate version alongside it. And do not treat the Contract format as a stable v1 — the project explicitly does not promote it to one, and the verification ledger explains why.

Independent versioning#

The Rust crate and @candid-core/schema do not share a version number and are released on separate cycles, so the numbers carry no pairing information at all. Because of that, every entry in the schema package's changelog names the crate version it pairs with (@candid-core/cli pairs by the revision it embeds instead, as above):

@candid-core/schemaReleasedPairs with candid-core
0.3.0-beta.1 (beta)2026-10-020.1.0-beta.3
0.2.02026-08-240.1.0-beta.3
0.1.12026-08-030.1.0-beta.2
0.1.02026-07-310.1.0-beta.2
0.0.0-bootstrap2026-07-31not a usable release

0.0.0-bootstrap exists only because npm cannot attach a trusted publisher to a name that does not yet exist on the registry. It is tagged bootstrap and never latest. Do not install it.

License, toolchain and targets#

License
Apache-2.0, declared by the root crate, the generator, the wasm crate and both npm manifests. The internal fuzz harness declares no license field of its own; it is never published. The root LICENSE carries the complete text.
Rust edition
2021, across the root crate, the generator, the wasm crate and the fuzz harness.
Minimum supported Rust version
1.78, declared as rust-version in the root crate's and the generator's manifests. CI runs the locked dependency graph against 1.78, so an incompatible direct or transitive dependency update fails before merge rather than for a consumer.
Rust targets exercised in CI
Linux, macOS and Windows on current stable; Linux on 1.78 for the MSRV suite; and wasm32-unknown-unknown for the library across every feature configuration meant to work there, with a browser runtime suite on an exact-pinned Chrome for Testing build.
@candid-core/schema runtime
TypeScript ≥ 5.0 (below that it is a parse error, not a type error); moduleResolution of node16, nodenext or bundler — node10 cannot resolve the package at all; ESM only, with no CommonJS build; Node ≥ 16 for every subpath. The package has no runtime dependencies and no peers. The published 0.2.0 differs here, as the note above says: it also exports ./transport-icp, which needs Node ≥ 20.19 and an optional @icp-sdk/core ≥ 6 peer.

What does not work on bare WASM#

Two target facts, both deliberate and both fail-closed rather than surprising.

  • WorkspaceResolver and compile_did_file are native. They still compile for wasm32-unknown-unknown, because the filesystem capability crate is declared under cfg(not(target_os = "unknown")) as well as behind its feature — but there is no directory to open there, so WorkspaceResolver::new fails with did_workspace_root_error. A browser host uses MemoryResolver or supplies its own synchronous resolver.
  • Bare wasm32-unknown-unknown has no clock. Reading it would abort the module, which is exactly the outcome the fail-closed rule exists to prevent, so the crate refuses to read it and reports any configured deadline as already elapsed — you get the ordinary operation_deadline_exceeded result. No deadline configured stays unbounded, and cancellation plus every byte, count, depth and work limit are unaffected. The crate takes no web-time, js-sys or wasm-bindgen production dependency to paper over this.

What changed in each release#

candid-core on crates.io#

0.1.0-beta.1 — the first published version. It established the shape rather than changing anything: one published package with four build surfaces (the base Contract model plus compiler, filesystem-compiler and host-value, all on by default); browser-WASM compilation of imported bundles through a resolver; the separation of semantic identities from a detached exact-octet artifact identity; and the rule that all untrusted work is bounded, with Contract, ContractEnvelope, Compilation and HostValue deliberately not implementing Deserialize. The semver gate was absent by necessity — there was no published baseline to compare against.

0.1.0-beta.2 — release machinery and gates rather than library behaviour. The tag, the crates.io publish and the GitHub release moved to a dispatch-only workflow behind three separately approved protected environments, and this was the first version released through it. cargo semver-checks began comparing both published surfaces against the published baseline. Two documentation entries record measured behaviour that already existed: the effective ceiling on HostValue record width, and the benchmark comparison governance. The repository also became a Cargo workspace, which is invisible in the archive.

0.1.0-beta.3 — the current version, and the first that carries a fix for a defect in an already-published version. Both type-depth guard walks stopped re-expanding shared subtrees and now charge for the work they do, under a new max_type_preflight_work limit (default 10 000 000) with a matching with_max_type_preflight_work builder. The addition is additive in both senses: the public surface gains two methods and removes nothing, and a configuration that leaves the limit at its profile value still serializes with no such key. The CLI gained compile <path> --envelope, which emits a one-document ContractEnvelope carrying the canonical Contract plus its field-name table — the single document @candid-core/schema's schemaFromContract consumes.

0.1.0-beta.1 and 0.1.0-beta.2 carry a reachable defect

Both walks that enforce max_type_depth traversed declaration graphs as trees, re-expanding a subtree once per incoming edge. A sub-kilobyte .did file with shared type aliases produced work doubling per level, and — the part that matters — nothing was charged for it, so no limit ever refused it and the walk returned Ok. It is reachable from the public compile_did entry point under default limits, which means every consumer that compiles .did source it did not write inherited it in both published versions. 0.1.0-beta.3 fixes it. A published version cannot be corrected in place, only superseded, so upgrade rather than pinning either earlier version.

@candid-core/schema on npm#

0.1.0 — the first real release: the schema runtime extracted into its own package with a manifest, a build, a packaged-consumer smoke test and publish machinery. Seven subpath exports, ESM-only, published with npm provenance from a protected environment.

0.1.1 — an audit-fix release; every change is a fail-closed correction found by reviewing the 0.1.0 surface, with the module list, exports map and peer metadata unchanged. A blob's declared length is now checked against the remaining input before any allocation is charged for it; a named declaration targeting the actor-root class node is refused; variant arms are classified structurally so the inferred union matches what the validator and codec actually do; and the AnyFieldSchema bound is carried through the codec, actor and form entry points. If you are on 0.1.0, those corrections are the reason to move.

0.2.0 — the latest published version, and the one that carries a deliberate type-level breaking change: c.principal now types as the structural PrincipalValue ({ toText(): string }) instead of the SDK Principal class, and FuncValue.principal and service schemas moved with it. Runtime behaviour is unchanged — no wire encoding and no validation verdict moved. The break is in the type surface telling the truth: the codec is self-contained and never constructs an SDK class, so a decoded principal has always carried exactly toText(), and the only code the change breaks was already failing at runtime. Encode-side code is untouched, because SDK instances satisfy the structural shape.

The minor bump is what carries that break rather than a patch: under npm's pre-1.0 caret rules a ^0.1.1 dependency accepts 0.1.2 and refuses 0.2.0, so shipping it as a patch would have upgraded every existing dependent into it unasked. 0.2.0 also fixes a real 0.1.1 defect — the shipped declarations imported @icp-sdk/core outside the transport, so a consumer without the optional peer could hit TS2307 deep in node_modules, or silently degrade Principal to any under skipLibCheck. Every subpath except ./transport-icp now compiles and runs with no peer installed. Additions in the same release: the new ./transport-icp subpath with a tested httpTransport, one-document envelope loading in schemaFromContract, isResultSchema and unwrapResult on ./validate, resolveSchema and serviceMethods on the root, and JSDoc on all 28 builder members — which is what a consumer's editor actually reads.

0.3.0-beta.1 — published 2026-10-02 under the beta dist-tag by a dispatch of its release workflow from a65d7e2, leaving latest on 0.2.0. It is a minor, because it removes published subpaths: under npm's pre-1.0 caret rules a ^0.2.0 dependency refuses it. No wire byte, Contract document or identity moves. What changes is on Migrating from 0.2.0, in the order the changelog records it: the package becomes the Candid layer only (./actor, ./transport-icp, ./forms and ./labels leave the export map, and the optional SDK peer with them); principals are canonical text, branded Principal, with principal() and isPrincipal() and a strict encode; opt opt T, opt null and opt reserved are boxed as { some: T } | null instead of refused; every vec nat8 is a Uint8Array; encoded bytes stop depending on how a schema was built, and a misspelled or non-integer option throws TypeError; the walkers no longer recurse on the host stack, so the configured limits are the only bounds; and host stack exhaustion is reported as its own resource, stack. Generated modules bind $-prefixed locals and carry the .did's doc comments as JSDoc, and the generator omits a declaration it cannot represent instead of refusing the interface. The CLI beta that embeds that generator, @candid-core/cli 0.2.0-beta.1, peers exactly 0.3.0-beta.1: while the two packages move in lockstep, every schema beta is paired with a new CLI beta.

Release mechanics a consumer should know#

You do not run any of this, but two properties of it change how you should treat a version number.

Publishing is dispatch-only and separately authorized. The Release workflow triggers on manual dispatch alone, declares permissions: {} at workflow level and elevates per job. A guard job runs before any approval is requested and fails closed on a commit not reachable from the default branch, a version that does not match the manifest, an existing tag, the wrong Cargo version, a missing release note, a packaging violation, or a repackaged archive whose digest differs from the one recorded as evidence. The tag, the publish and the GitHub release then run in three GitHub Environments with required reviewers — three separate human approvals — and a confirm job reads the checksum back from crates.io and compares it again after publication. No long-lived registry credential exists anywhere: both registries are published to through Trusted Publishing, which also means the workflows reachable from a pull request are structurally unable to publish anything.

Publishing is irreversible in both registries, and yanking is not deletion. A published crates.io version can never be deleted or replaced — the name, the version number and the exact archive bytes are permanent, and there is no force-push equivalent. cargo yank marks a version so that new dependency resolution will not pick it; it does not remove the archive, does not remove it from the index, does not break builds that already have it in a lockfile, and does not stop anyone who pins the version explicitly — which is what every consumer of this prerelease is instructed to do. npm names and versions are equally permanent. The actual fix for a bad release is a follow-up version, which is exactly what 0.1.0-beta.3 is.

The two things deliberately not pinned#

Almost everything here is exact-pinned: every production and dev dependency in the manifests and lockfiles, the release and evidence tooling in one file, Node and TypeScript in the npm lockfile, wasm-pack, and the Chrome for Testing build the browser suite runs against.

The exceptions are the stable and nightly toolchain channels used by ordinary CI jobs. Those are intentionally rolling, and that is the point: a new compiler release that breaks this crate should turn a check red here, before it reaches a consumer. Pinning them would convert a canary into a snapshot.

Release tooling is the opposite case and is pinned hard, for a concrete reason: cargo package is byte-stable within one Cargo version and not across versions. The same tree at the same commit, packaged by two different Cargo releases, unpacks identically and produces different archive digests. Because cargo publish re-packages rather than uploading an archive you hand it, a rolling stable would let the digest recorded as evidence describe an archive nobody published.