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, published on 2026-10-12. Also on the registry: 0.1.0-beta.1, 0.1.0-beta.2, 0.1.0-beta.3. None yanked.
@candid-core/schemanpmPublished — latest 0.5.0; beta 0.3.0-beta.1. Also on the registry: 0.0.0-bootstrap, 0.1.0, 0.1.1, 0.2.0, 0.3.0, 0.4.0.
@candid-core/clinpmPublished — latest 0.4.0; beta 0.2.0-beta.1. Also on the registry: 0.0.0-bootstrap, 0.1.0, 0.2.0, 0.3.0.
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.
Upgrading from an earlier release

The table above is what the registries hold. latest is @candid-core/schema 0.5.0 and @candid-core/cli 0.4.0, published on 2026-10-27 (below). Against the previous stable releases, 0.4.0 and 0.3.0, published on 2026-10-10, they type decode and decodeArgs values by their schemas, add the CLI's didToDocAttributes and each entry’s identities in gen --json, render a declared blob, record {} or variant {} the same at every use, and write envelopes whose producer is meant 0.1.0; Migrating from 0.4.0 says what the upgrade means, and every change is recorded under 0.5.0 and 0.4.0 in the two changelogs. Against the stable releases before those, 0.3.0 and 0.2.0, published on 2026-10-06, the 0.4 / 0.3 pair added method modes to the generated Actor (WithMode, ModeOf), declared each generated type under its Candid name, and added the CLI's project and check; Migrating from 0.3.0 says what that upgrade means, and every change is recorded under 0.4.0 and 0.3.0 in the schema and CLI changelogs. Against the ones before those, 0.2.0 and 0.1.0, the 0.3 line changed value shapes, removed subpaths and changed what the generator emits, as the betas before it did (0.3.0-beta.1 and 0.2.0-beta.1, published on 2026-10-02 and still under the npm beta dist-tag, which lack the changes recorded under 0.3.0 and 0.2.0 and every stable release since). Migrating from 0.2.0 shows what each of those breaks means for your code. Every other page describes the repository; for the TypeScript packages, that is what latest holds.

The dependency lines that exist are these:

tomlREADME.md
# every feature on by default
candid-core = "=0.1.0"
bash
npm install --save-exact @candid-core/schema
npm install --save-dev --save-exact @candid-core/cli
Pin the exact version

0.1.0 is the first candid-core version without a prerelease suffix, so candid-core = "0.1" resolves to it, and to any later 0.1.x. Before 1.0 any release may change the API, the serialized shapes and the identities, so write the exact version: candid-core = "=0.1.0", or cargo install candid-core --version 0.1.0 --locked for the binary. Moving to the next release 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. 0.2.0 followed on 2026-10-06 under latest, dispatched from acabe0f after @candid-core/schema 0.3.0 (its run, same commit), whose exact version it declares as its optional peer; beta still names 0.2.0-beta.1. 0.3.0 followed on 2026-10-10 under latest, dispatched from 552e16f after @candid-core/schema 0.4.0 (its run, same commit), whose exact version it declares as its optional peer. 0.4.0 followed on 2026-10-27 under latest, dispatched from b331f0c after @candid-core/schema 0.5.0 (its run, same commit), whose exact version it declares as its optional peer.

bash
npx @candid-core/[email protected] gen ./service.did -o ./generated

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 release before 0.1.0, in 0.1.0, 0.2.0-beta.1, 0.2.0 and 0.3.0 — 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; 0.2.0's is ahead by four: that one; a run of more than 256 consecutive comments is refused; a type on a cycle that passes only through opt is refused; and the crate's Contract loaders refuse a struct written as a JSON array, which no input of the CLI reaches, since it reads .did sources and never a Contract document. Its entry says so too. 0.3.0's is ahead by five: those four, and the crate's Contract loaders refuse a unit variant written as a JSON map, which no input of the CLI reaches either. 0.4.0 embeds meant 0.1.0, the crate published as candid-core before its rename, with those five and two more (a record field whose id is 4294967295 is refused, and collecting the provenance sidecar no longer grows with the square of the declarations); its source is ahead of that archive by one change, the doc attributes reader behind didToDocAttributes, which moves no Contract and no identity. 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.5.0 (latest)2026-10-27meant 0.1.0, the crate published as candid-core before its rename
0.4.02026-10-100.1.0-beta.3
0.3.02026-10-060.1.0-beta.3
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. An earlier stable release, 0.2.0, differs here: 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 first version 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 — the current version, published on 2026-10-12, and the first without a prerelease suffix. It is still pre-1.0, so pin it exactly: candid-core = "=0.1.0". The public API is identical to 0.1.0-beta.3's, and no change is marked breaking. Each change refuses an input that crashed, stalled or was misread before: a record field whose id is 4294967295 is refused instead of panicking a debug build or misnumbering the next field in a release build; a long run of consecutive comments is refused under max_source_nesting instead of overflowing the stack; a type on a cycle that passes only through opt is refused; and the Contract loaders refuse a struct written as a JSON array and a unit variant written as a map. A .did source may now begin with a UTF-8 byte order mark, and collecting the provenance sidecar takes time proportional to the source instead of its square.

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 last stable version before the 0.3 line, 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.

0.3.0 — published 2026-10-06 under the latest dist-tag by a dispatch of its release workflow from acabe0f, with @candid-core/cli 0.2.0 (its run, same commit). It is 0.3.0-beta.1 plus what changed on the repository since: schemaFromContract refuses the Contract documents candid-core's own loader refuses (unknown keys, unreachable type nodes, a non-empty arena with no root), with three new ContractIssueCode members; a thrown value whose prototype cannot be read no longer escapes the codec; a decode issue after an absorbed coercion mismatch is reported at the right path; and validate, encode and decode no longer charge a rec hop against maxDepth or maxElements, so a generated, a Contract-loaded and a hand-built schema of one type accept the same values. That change refuses three narrow kinds of input accepted before: a tag-only variant at exactly a caller-set maxDepth; a message that omits an opt, null or reserved field, whose null is now charged at the field's own level and as one element, at a maxDepth edge or past maxElements, which a large message reaches at the default limits; and an omitted required field that an enclosing opt used to absorb, when an omitted opt-like field before it in field-id order crosses a limit first. It pairs with @candid-core/cli 0.2.0, which peers it exactly and refuses a run of more than 256 consecutive comments and a type on a cycle that passes only through opt. The beta dist-tag stays on the betas.

0.4.0 — published 2026-10-10 under the latest dist-tag by a dispatch of its release workflow from 552e16f, with @candid-core/cli 0.3.0 (its run, same commit). It is 0.3.0 plus two type-only exports, WithMode and ModeOf, which carry a method's mode in the type system: a generated Actor types each method as its call signature intersected with WithMode<mode>, and ModeOf reads the mode back, so a call layer can refuse an update where it builds a read at compile time. Nothing else in the API moves, and the shipped JavaScript is 0.3.0's but for one comment, so no verdict, byte, code or path changes. It is a minor because it adds API. It pairs with @candid-core/cli 0.3.0, which peers it exactly, because every module that CLI generates with an actor method imports WithMode; that CLI also declares each generated type under its Candid name and adds the project and check commands. Migrating from 0.3.0 has the upgrade, which for code is regenerating committed modules. The beta dist-tag stays on the betas.

0.5.0 — published 2026-10-27 under the latest dist-tag by a dispatch of its release workflow from b331f0c, with @candid-core/cli 0.4.0 (its run, same commit). It is 0.4.0 plus two changes. decode and decodeArgs type their values by their schemas, where 0.4.0 gives unknown; nothing changes at run time for it, but a cast or annotation from a decoded value to a type its schema does not describe stops compiling, which makes it a breaking change at the type level. And schemaFromContract omits a declaration of a blob, record {} or variant {} under a reserved name on its own, where 0.4.0 also left out every declaration and method holding a plain value of that shape. It pairs with meant 0.1.0, the crate published as candid-core before its rename, and with @candid-core/cli 0.4.0, which peers it exactly and adds didToDocAttributes and each entry's identities in gen --json, renders a declared blob, record {} or variant {} the same at every use, and writes envelopes whose producer is meant 0.1.0. Migrating from 0.4.0 has the upgrade. The beta dist-tag stays on the betas.

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.