Skip to content

build: import the stack-* crates, node bindings and Go module - #1001

Merged
auxesis merged 823 commits into
mainfrom
build/import-stack-crates
Oct 2, 2026
Merged

auxesis merged 823 commits into
mainfrom
build/import-stack-crates

Conversation

@auxesis

@auxesis auxesis commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

This PR brings six Rust crates, three bindings and the Go module into this repository from cipherstash/cipherstash-suite, with 807 commits of their history. It then makes them build and test here. The crates are stack-auth, stack-profile, stack-kms, stack-encrypt, stack-encrypt-derive and stack-guest-abi. The bindings, which let JavaScript call the Rust code, are @cipherstash/auth, @cipherstash/profile and stack-auth-wasm. The Go module lands in languages/golang.

This is PR B of 6 in the stack crates import, which moves this code here from the private suite repository. The stack crates import plan in Linear lists every step, and Linear issue CIP-4274 tracks the work.

This PR is stacked on PR A, #1000. Its base branch is PR A's branch, so the diff shows only this PR's own changes. It stays a draft until the freeze on Friday 2 October 2026, Pacific time. The freeze is the window in which the six PRs merge in order: #1000, #1001, #1003, #1002, #1009, then #1010. The steps for that day are in §9.1 of the plan.

Check the import merge by its file list, and read the 15 later commits

The first commit, d0cea7cc, merges the suite's history into this repository. It adds 408 files, so check its shape rather than reading it:

  • git diff --name-status b3ffccd4 d0cea7cc prints 408 lines, and every one is an added file (A). No existing file changes.
  • git rev-list --count d0cea7cc^2 prints 807, the number of suite commits it brings in.
  • The merge holds no edits of its own. Every fix is a later commit.

Read the 15 commits after the merge line by line. You can skim three parts of them:

  • 2371f731 adds four Cargo.lock files, which hold 15,526 of its 16,410 added lines.
  • fa05f243 reformats 21 files and changes nothing else.
  • a981590a renames the Go module in 36 files, 53 lines. A script on the migration machine, stack-migration/go-rename.sh, generated it.

The import merge keeps the suite's history and changes no file

The merge brings in an export: a copy of the suite's history that keeps only the files that move. git filter-repo, a tool that rewrites history to keep chosen paths, made it from suite commit e059b8ed. The export's head is 82a85b4d, with 807 commits and 408 files, and its history starts on 11 February 2026.

The two histories share no commit, and the merge changes nothing inside the files. The suite's 13 workflows land in .github/imported-workflows/, where GitHub never runs them, and the next commit deletes them. PR C ports the CI.

Commit messages name suite PRs as cipherstash/cipherstash-suite#NNNN. That way, GitHub does not link them to this repository's PRs with the same numbers.

Each commit after the merge makes one change

  1. 359a6f18 cleans up the imported files. It deletes the imported workflows and stray files, and fixes links in the moved docs. It marks @cipherstash/profile private, because it was never published. It also points the Go guests' dependencies at crates.io, because the suite folders they named do not exist here. The guests are the Rust crates that the Go module runs as WebAssembly.
  2. 6bb5775d stops Biome, the formatter and linter, from changing two kinds of file. The first is the 15 files that @cipherstash/auth publishes. The release gate, scripts/release-gate.mjs, runs on every push to main before anything publishes to npm. From commit 6, it compares those 15 files byte for byte with version 0.44.0 on npm, so a reformat would make it fail. The second is generated files.
  3. fa05f243 reformats the imported bindings with this repository's Biome config. The suite used double quotes and semicolons, and this repository does not. The commit holds formatting changes and Biome's safe fixes only.
  4. d94d6a77 fixes two lint errors that the reformat brings to light in the auth binding's tests. The suite's linter was off for these files. A missing failure now fails the assertion, instead of throwing a TypeError.
  5. 2371f731 makes the repository root a Rust and Go workspace. It adds the root Cargo.toml and Cargo.lock, and lockfiles for the three crates that hold the fuzz tests. It refreshes the two Go guests' lockfiles, and those five crates each stay a separate Cargo workspace. It also adds entries for mise, pnpm and turbo, and splits the binding scripts so build and test never run cargo. mise is the tool that pins this repository's tool versions and runs its tasks.
  6. 8c86f4bf freezes @cipherstash/auth and its six platform packages in the release gate. Each platform package holds the native binary for one platform, such as darwin-arm64. A frozen package fails the gate if its version changes, or if its published files differ from npm's copy. The commit also adds Dependabot entries, and a temporary check, lint-no-auth-changeset, that fails if a pending changeset names an auth package. A changeset is a file in .changeset/ that says which packages a change releases.
  7. 542fa511 adds the new workspaces to the repository checks that list every workspace, such as the checks on Cargo publish settings and lockfile freshness.
  8. 1c52c8f6 describes the crates, bindings and Go module in AGENTS.md, SECURITY.md, CODEOWNERS and CONTRIBUTING.md.
  9. f7b00319 moves the six cargo test tools into mise.test.toml, at exact versions. mise passes the root config down to every subfolder, so the EQL and protect-ffi CI jobs built these tools too. They built them with the runner's Rust 1.92 and failed, because cargo-udeps 0.1.61 needs Rust 1.93. mise reads mise.test.toml only for test runs, so those jobs no longer get the tools.
  10. 8e8dd7ab makes the profile binding write its generated typings to native.d.ts, as the auth binding does. Before, its build overwrote the hand-written index.d.ts, which left the tree changed and the package's types wrong.
  11. 0dc76af1 builds the auth and profile bindings in tests.yml before the test run. Without the build, pnpm run test failed to load the profile binding. This step started in PR C and moved here, so PR B passes CI on its own.
  12. 5147dd00 gives cts-common and zerokms-protocol caret version requirements, such as ^0.43.0. A caret requirement accepts any later compatible version. The published stack-auth inherits these requirements, so an exact pin would stop the suite sharing one copy of cts-common with it. The lockfiles still hold the exact versions, so no resolved version changes.
  13. a981590a renames the Go module from github.com/cipherstash/cipherstash-suite/bindings/go to github.com/cipherstash/stack/languages/golang. The old path names a folder in a private repository, so go get could never fetch it. Nothing outside the module imported the old path, and no version of it was ever tagged.
  14. d3524b87 adds a test that go.mod declares the new path, and that no file outside docs/plans/ names the old one. A later export from the suite could bring the old path back, and this test would catch it.
  15. 1ad1055d fixes wasm:wasi-check, which could never fail in CI. The check searches cargo tree output for crates such as wasm-bindgen, which must never be in a build for WASI. WASI is the WebAssembly System Interface that the guests target. CI turns on coloured output, and the colour codes stopped the search matching. The check now asks for --color never, and fails when a deliberate break adds wasm-bindgen to stack-kms.

The shared suite crates come from crates.io

The imported crates depend on four crates that stay in the suite: cts-common 0.43.0, cllw-ore 0.5.0, recipher 0.3.1 and zerokms-protocol 0.12.31. The root workspace takes them from crates.io. Phase 0 of the plan released them from suite main, and their source matches suite main.

Publishing stays off until PR E

This PR brings @cipherstash/auth and its six platform packages here, but the release gate freezes them. A version bump of any of them makes the gate fail, so nothing publishes. PR E removes the freeze.

The checks pass locally and in CI

  • cargo metadata --locked passes in all 8 Cargo workspaces. It fails if a lockfile is out of date.
  • The full build passes.
  • nextest passes 759 of 759 tests, including the trybuild UI snapshots. nextest is the Rust test runner that CI uses, and it runs each test in its own process. A trybuild UI snapshot compiles code that must fail, and compares the compiler's messages with a saved copy.
  • cargo tree -d lists crates that appear in more than one version. It shows one cts-common and one set of vitaminc crates.
  • go:test and go:lint pass.
  • pnpm install --frozen-lockfile, code:check and test:scripts pass, with 1,020 script tests.
  • The release gate passes. It fails, as it should, on a test version bump of @cipherstash/auth.
  • CI: 16 checks pass and 11 are skipped.

Plain cargo test fails 3 tests, so CI uses nextest

Plain cargo test --workspace fails 3 stack-kms tests. A test helper sets environment variables while other tests run in parallel, so the tests race. nextest runs each test in its own process, so CI passes. PR C adds a test that stops any workflow running plain cargo test over the workspace.

A Developers team member must merge this PR

main requires signed commits. git filter-repo rewrote the 807 suite commits, so they carry no signature. A member of the GitHub Developers team must merge this PR, and bypass only the signature rule.

Merge it with a merge commit, which keeps every commit, and never with a squash or a rebase. A squash would replace the 807 suite commits with one, and lose the history that this PR exists to keep.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a

coderdan and others added 30 commits August 31, 2026 17:53
…ld is never handed the caller's context

Review of the context-parameter commit found the derive's where clause
and the docs disagreeing in both directions. A row whose fields all carry
literals left `__Ctx` unbounded, so `encrypt_into_with_context(&cipher,
tenant)` compiled against it and silently dropped `tenant` — while the
docs said the compiler turns that form away. And a `from` field with no
literal made the impl demand `SuppliedContext` regardless of the field's
type, so a row nesting another (all-literal) row lost the context-free
forms it had before, and the context the caller was then forced to pass
never reached a leaf. The same bound landed on `DecryptInto` for one-way
term fields that decryption never opens.

One rule replaces all of that: a `from` field is derived under its own
`context`, or under `()` if it has none — never under the caller's. Its
type decides whether `()` will do: a nested row accepts it, a leaf refuses
it at the field (the obligation is checked in the body, spanned at the
field type) until it is given a literal. A row therefore never passes the
caller's context anywhere and is implemented for `Ctx = ()` exactly, which
makes `encrypt_into` / `decrypt_from` the only forms that compile, as
documented. Handing every column of a row one shared context was the
cross-column transplant the per-field contexts exist to prevent, so
nothing legitimate is lost; `#[stash(row = ..)]` fills the literal in.

The derive's context bound is now `__Ctx: Clone` — the per-field bounds
already imply `EncryptContext` / `DecryptContext`, and the extra impl
lifetime went with it. `Record::derived()` replaces three inlined filters
and the by-field candidate list is computed once.

Also from the review:

- `EncryptFrom` / `DecryptInto` `on_unimplemented` notes said "if `{Ctx}`
  is `()` … use `encrypt_into_with_context`" unconditionally, which never
  fired for `()` (the `SuppliedContext` note wins) and gave false advice
  on a source-type mismatch. Both now state the rule; `SuppliedContext`'s
  note covers `from` fields and how a context type of your own opts in.
- The "extend with your own SEM type" recipe guarded with
  `context.as_bytes().is_empty()` on an encoded `PrfContext`, which is
  never true. `is_degenerate_aad` / `is_degenerate_prf_context` are
  public; the recipe and the `PrefixTerm` example use them.
- `DecryptFrom<S, C>`'s blanket impl had dropped its `S: DecryptInto`
  clause, so the trait held for every triple and meant nothing as a
  bound. It mirrors `EncryptInto` now: no trait parameters.
- Decrypt leaves bound `Ctx: SuppliedContext<'c>` alone; it implies
  `DecryptContext`.
- Stale spellings from the rename: `encrypt_into::<EqualityTerm>(..)`,
  two-parameter `EncryptFrom<P, _>` / `DecryptInto<P, _>`, and the design
  doc's `#[encrypted(source = ..)]` / "omit `source`".

Pinned by `tests/ui/row_with_context.rs` (`_with_context` against a row),
`tests/ui/from_leaf_without_context.rs` (a `from` leaf with no literal,
reported at the field) and `a_row_nests_in_a_row_without_a_context`.

Claude-Session: https://claude.ai/code/session_01HU1Bbw4eQp9kEEneXxDcKr
…_context sugar

Review fixes on the context-parameter change:

- A derived `DecryptInto` generic over the caller's context bounds it by
  `DecryptContext` again. A term field's `DecryptField` accepts any
  context (it opens nothing), so field bounds alone let a record whose
  ciphertext field carries a literal accept — and silently discard — any
  `Clone` value as its decrypt context. Pinned by an expansion test and a
  `compile_fail` doctest on `DecryptContext`.
- `encrypt_into_with_context` / `decrypt_from_with_context` bound
  `Ctx: SuppliedContext`, so `()` is refused on the sugar and misusing a
  row now gets the guided E0277 on the decrypt side too, not a bare
  E0308.
- `SuppliedContext: DecryptContext` is declared, not hand-mirrored, so
  the documented implication holds by construction; the transitional
  `is_degenerate_*` predicates now say in rustdoc that they are deleted
  when vitaminc#291 lands.
- The empty-`context` derive error no longer advises a `from` field to
  drop the attribute — a dead end, since a `from` field is never handed
  the record's context — and the check runs after parsing so attribute
  order cannot change the advice.
- Derive internals: one `FieldContext` classification replaces the four
  parallel projections of literal/unit/caller; the impl scaffolding is
  shared between the two derives; encrypt-side field bounds are spanned
  at the field type as decrypt's already were; the stray raw derived-
  field filter uses `Record::derived`, computed once per expansion.
- The hand-written composite examples inherit the leaves' context policy
  through per-field bounds — the clauses the derive emits — instead of
  restating it, and RFC 0002 no longer claims a row "leaves the context
  unbounded" where the implementation is `()` alone.

Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
The Naming section still recorded `#[derive(Encrypted)]` as the macro
name, and the implementation notes still called the derive unbuilt and
the row snippet "future". `stack-encrypt-derive` ships
`#[derive(EncryptFrom)]` / `#[derive(DecryptInto)]` under `#[stash(..)]`,
each named after the trait it emits — which is also why the sketched
noun could not stand: it names neither trait, and one noun cannot cover
both directions.

Claude-Session: https://claude.ai/code/session_01VEKAfJiDDhSxEZRAVJQJPX
Review findings from cipherstash/cipherstash-suite#2163:

- The derived encrypt impl now bounds its caller context by
  `EncryptContext`, mirroring the decrypt side. Without it, a
  third-party leaf generic over its context let the raw
  `EncryptFrom::encrypt_from` accept — and silently discard — any
  `Clone` value as its context. Pinned by
  `a_caller_context_must_be_an_encrypt_context`.
- The `encrypt_into_with_context` sketch in the design doc now shows
  the `Ctx: SuppliedContext<'c>` bound the shipped signature has.
- The `seal_pending` doc no longer names `encrypt_into::<StackCipherText>`,
  a call that no longer compiles.
- The `SuppliedContext` doc no longer claims coverage of composites
  containing `()`, and the crate re-exports `IntoPrfContext` /
  `PrfContext` so a context newtype needs no direct `vitaminc-prf`
  dependency.

Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
…s `from` and context

A row no longer needs an attribute on any field. `row = User` says the
record is a row of the struct `User`: every derived field is derived from
the plaintext field of its own name, under the context
`"<snake_case type>/<plaintext field>"` — `age` from `user.age` under
`"user/age"`, a tuple row's `.0` under `"user/0"`. Both halves name the
column, not the encrypted struct, so `#[stash(from = email_address)]` (the
override for a name that differs) is derived under `"user/email_address"`;
`#[stash(context = "..")]` is taken verbatim. Nothing is pluralised or
otherwise guessed. `row` is exclusive with `plaintext`, and must name a
struct directly.

Because the inferred context is the AAD of every stored ciphertext in the
column, renaming the plaintext type or a field is a data migration; the
docs say to pin the old literal with `context = ".."` first.

A field the plaintext does not have is reported by rustc at the field
(`tests/ui/row_field_missing.rs`); `row` + `plaintext` at the attribute
(`tests/ui/row_with_plaintext.rs`).

Claude-Session: https://claude.ai/code/session_01HU1Bbw4eQp9kEEneXxDcKr
…nested` hands a field `()`

`row = User` now requires `context = "users"` beside it; each field is
derived under `"<context>/<field>"`. The prefix is part of the stored
data's identity — the AAD of every ciphertext in the row and the domain
of every term — so it is never inferred from the Rust type's name: two
plaintext types with the same name in different modules can no longer
silently share every column context (byte-identical index terms across
their tables, ciphertexts transplantable between them), and renaming a
struct can no longer silently change the AAD of every stored row.

`#[stash(nested)]` opts a row field out of the inferred context: it is
handed `()`, which a nested row accepts and a leaf refuses — the
()-handoff the docs promised now exists in row mode, not only under
`plaintext = ..`.

Also addressed from the same review:

- `supplied_aad` / `supplied_prf_context` are the one public choke
  point for validating-and-encoding a supplied context; the
  `is_degenerate_*` predicates return to crate-private, and the
  third-party-leaf recipe goes through the choke point, whose signature
  survives the vitaminc#291 migration.
- `DecryptField` carries a `#[diagnostic::on_unimplemented]` pointing a
  term-only bundle inside an auto-mode record at `#[stash(decrypt)]`.
- The deferred exactly-one-decryptable check for generic records is
  pinned by a `compile_fail` doctest on `Decryptable` (trybuild runs
  `cargo check`, which never evaluates post-monomorphization consts, so
  a ui test cannot reach it).
- The empty-container fail-fast loss and the caller context a
  literal-carrying record discards on decrypt are documented where they
  bite (the container impls, `DecryptContext`, the attributes guide).
- The `SuppliedContext` roster notes its coupling to vitaminc's
  `IntoAad` implementor list and the orphan-rule consequence.
- Both derives build field bounds through one shared
  `push_field_bounds`, and the generic-source vs listed-plaintexts
  emission fork collapses to one loop per derive.

Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
Missed in bd3b61824: docs/target-directed-encryption.md and RFC 0002 §7
still described the row prefix as inferred from the snake-cased type
name.

Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
…ke point

`is_degenerate_aad` went back to crate-private when `supplied_aad`
became the one public way to validate and encode a supplied context, so
the design doc's `StackCipherText` bridge no longer matched the impl it
quotes.

Claude-Session: https://claude.ai/code/session_01VEKAfJiDDhSxEZRAVJQJPX
Review finding on cipherstash/cipherstash-suite#2164: the row decrypt now runs before the
field-alone plan decrypt, so the counter reads exactly 1 where the
other counter assertions in the file are exact too, instead of the
`>= 1` that hid the total.

Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
…h/claude/stack-encrypt-derive-row

feat(stack-encrypt-derive): `#[stash(row = User, context = "users")]` infers each field's `from` and context
…ASI without reqwest

Phase 1 of the stack-encrypt Go bindings (docs/stack-encrypt-go-bindings.md).
On wasm32-wasip1 reqwest 0.13.4 selects a native backend (tokio-full and the
aws-lc-sys TLS provider) that does not build for WASI, and it was the only
thing standing between the three stack crates and the target. HTTP is
host-provided under wazero, so it has to be out of the WASI build by
construction:

- `http` feature, default on, in all three crates:
  stack-encrypt/http -> stack-kms/http -> stack-auth/http -> dep:reqwest.
  stack-auth keeps the token model, `AuthStrategy`, `AuthStrategyFn` and
  `StaticTokenStrategy` unconditionally; every HTTP-speaking strategy, the
  refresh engine, device binding and `Token::refresh` sit behind the feature.
  stack-kms keeps `Client<C>`, key derivation and the key-source traits;
  `HttpConnection`, its options and `StackKmsBuilder` sit behind it.
  stack-encrypt keeps `StackCipher::builder().kms(..)`; `StackCipher::new()`
  and the from-environment `init` sit behind it. stack-kms and stack-encrypt
  take stack-auth as a path dep with `default-features = false` (a workspace
  dep's defaults cannot be turned off by a member).
- `StackKms<C, Conn = HttpConnection>` with `StackKms::connect(opts,
  credentials, client_key)`: the transport-injecting constructor a host with
  its own `ZeroKMSConnection` builds through, and the only one without
  `http`. `ZeroKMSConnection` gains `ensure_base_url` / `has_base_url` so
  endpoint discovery from the token's `services` claim works over any
  connection (previously inherent to `HttpConnection`).
- `Error::ConnectionInit` boxes the connection's own init error.
- `StackCipher::builder()` lives on `impl StackCipher<FromEnv>` so it
  resolves without a type annotation whether or not `http` is on.
- `wasm:wasi-check` now gates stack-auth, stack-kms and stack-encrypt
  (`--no-default-features`) alongside the core crates; the CI workflow's
  paths cover them.
- A unit test drives `StackKms` end to end over the in-memory
  `TestConnection`.

Verified: `mise run wasm:wasi-check` passes for all eight crates with no
reqwest/hyper/aws-lc-sys in any wasip1 tree; `mise run lint` clean; 451
tests pass with `--all-features` and with `--no-default-features`; doc tests
and rustdoc (`-D warnings`) pass.

BREAKING CHANGE: stack-auth's `RequestError` tuple payload is now
`Box<dyn std::error::Error + Send + Sync + 'static>` instead of
`reqwest::Error`. Construct it via `RequestError::from(reqwest_error)`
(or box the error yourself) instead of `RequestError(reqwest_error)`, and
recover the concrete error with `.0.downcast_ref::<reqwest::Error>()`
instead of using `.0` as a `reqwest::Error` directly. `source()` behaviour
is unchanged; the `Display` message is now "Request to the auth server
failed" (previously "HTTP request failed").

Claude-Session: https://claude.ai/code/session_01HU1Bbw4eQp9kEEneXxDcKr
…h item-level http gates

Without the http feature the crate previously silenced dead-code analysis
entirely via #![cfg_attr(not(feature = "http"), allow(dead_code))]. Gate
the eleven affected helpers (URL massaging, clock sharing, refusal
classification, token setters, AccessKey secret access) individually with
#[cfg(feature = "http")] instead, so the compiler verifies the feature
partition in both directions: no-http code reaching an http helper fails
to compile, and newly dead code warns instead of being swallowed.

Test fallout handled so no coverage regresses in the no-http run:
- workspace_crn tests assign Token.region directly instead of the now
  http-only set_region, keeping workspace_crn covered without http
- test_refresh_debug_does_not_leak_tokens loses its http gate (it never
  needed HTTP) so the Debug secret-leak regression test runs in the
  no-default-features shape the WASI guest ships
- http-only test helpers (TestClock, crn_with_workspace,
  jwt_with_workspace, classify_issuance_failure_tests) are gated with
  their consumers

The one honest residual is AccessKey's inner field: parsing is
unconditional token-model API, but only the http-gated AccessKeyStrategy
consumes the secret, so that single field keeps a scoped
cfg_attr(not(http), allow(dead_code)).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
Feature additivity (the two API-shape bugs):
- stack-auth: RequestError has one definition with an always-boxed payload;
  the http-gated From<reqwest::Error> does the boxing. Its public field's
  type no longer changes under feature unification, which would have broken
  a no-http host the moment anything else in its graph enabled http.
- stack-encrypt: Error::Config is unconditional with a boxed source (the
  http-gated From<StackKmsBuilderError> boxes), so the enum's variant set
  no longer tracks the feature.

Build config:
- stack-auth's workspace entry is now default-features = false (the same
  pattern as cllw-ore/cts-common); stack-kms and stack-encrypt use the
  workspace dep again instead of hand-written path+version pins, and the
  consumers that rely on the default transport re-enable it with
  features = ["http"] (cipherstash-client, cipherstash-cli, cts-web
  dev-dep, stack-auth node/wasm bindings). One version pin remains, at the
  root. Cargo.lock is unchanged.

Docs and doctests:
- cargo test --no-default-features now passes including doctests in all
  three crates: http-only examples (README via include_str, token_store,
  StackKmsBuilder quick-start, StackCipher::new) are doc-gated on the
  feature with short no-http fallbacks, and the no-http rustdoc's broken
  intra-doc links are fixed. RUSTDOCFLAGS=-D warnings is clean in both
  shapes; default-features doctest coverage is unchanged.

API cleanups:
- ZeroKMSConnection::ensure_base_url / has_base_url get default impls
  (no-op / true) with the first-value-wins contract documented, so
  transports that don't do endpoint discovery (TestConnection, and hosts
  that pin at init) no longer stub them; HttpConnection keeps its
  overrides.
- The unused From<ConnectionInitError> for Error impl is deleted;
  StackKms::connect is the one construction path for Error::ConnectionInit.
- StackCipherBuilder::new() (+ Default) is the canonical builder entry
  point; StackCipher::builder() stays as a thin alias on the phantom
  FromEnv impl for annotation-free inference.
- token.rs's scattered per-test http gates collapse into one gated
  refresh_tests module; make_token and the Debug secret-leak test stay
  ungated so they keep running without http.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
…-web dev-dep

wasm:wasi-check only runs cargo check, so the no-default-features shape's
unit tests, doctests, and rustdoc had no gate — the 7 doctest failures and
21 broken intra-doc links fixed in the previous commit merged green. Add
wasm:no-http-test (per-crate cargo test + RUSTDOCFLAGS=-D warnings cargo
doc, default features off, one crate per invocation so dev-dep feature
unification can't switch http back on) and run it from test-wasi.yml,
whose path filters already cover the three crates.

cts-web's stack-auth dev-dep only uses StaticTokenStrategy, which is part
of the unconditional token model, so it doesn't need the http feature.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
Keep HttpConnection failure Display to the status line — full body and
headers can carry sensitive response content into logs and are unbounded;
they remain available via Debug. Align RequestError's message with its
transport-agnostic shape: the payload is no longer necessarily an HTTP
error, so say 'request to the auth server failed' instead.

Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
…x Phase 1 contract docs

Review follow-ups from cipherstash/cipherstash-suite#2158:

1. `cargo test -p stack-encrypt --no-default-features` still resolved
   reqwest: the stack-kms dev-dependency didn't set
   `default-features = false`, so dev-dep feature unification pulled
   stack-kms/http -> stack-auth/http -> reqwest into the graph
   `wasm:no-http-test` claimed was HTTP-free. Disable defaults on the
   dev-dep (the tests only need `test-support` for FakeDataKeySource;
   none are HTTP-dependent). Verified:
   `cargo tree --no-default-features -e normal,dev,build -i reqwest`
   matches nothing, and the full no-default-features test/doctest run
   passes.

2. The plan doc named StaticTokenStrategy as part of the unconditional
   no-http surface, but it is (correctly) gated behind
   `cfg(any(test, feature = "test-utils"))` and has no production
   users. Revise the Phase 1 contract to name AuthStrategyFn (and the
   guest's HostTokenStrategy) as the supported production path;
   StaticTokenStrategy stays a test double.

Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
… manifests

Review follow-up on cipherstash/cipherstash-suite#2158: two manifest comments still described
StaticTokenStrategy as part of the unconditional no-`http` surface,
contradicting the corrected plan and the actual
`cfg(any(test, feature = "test-utils"))` gate. Name AuthStrategyFn as
the production path and state where the test double lives.

Comments only; no dependency or feature change.

Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
The variant had {0} in the message and #[source] on the same field, so
chain printers (anyhow {:#}, tracing) showed the inner error twice —
against this file's own convention that Display stays static and the
source chain carries the detail. Flagged by Toby on cipherstash/cipherstash-suite#2158.

Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
The Go/wazero binding needs byte formats both languages agree on before the
guest is written. This freezes the two commitments stack-encrypt makes:

SealedValue leaf: to_bytes/from_bytes with the canonical layout
`version(1) ‖ iv(16) ‖ tag_len(u16 LE) ‖ tag ‖ local_ciphertext`. The
version byte is bound into every leaf's AAD through a new labelled
derivation — PAE("stack-encrypt/leaf", version, derived_aad, tag), replacing
the unlabelled (aad, tag) tuple — mirroring how vitaminc binds its inner
wire version, so bytes relabelled with a future version byte fail
authentication instead of parsing under the wrong rules. The derivation
bytes are pinned by a unit test; decode failures surface as the new
LeafBytesError.

Index terms: every term type now exposes its frozen encoding.
EqualityTerm is the 32 PRF bytes as-is and OreTerm/OpeTerm are the raw CLLW
ciphertext bytes — byte-identical to what the EQL layer hex-encodes into
hm/oc/op, so rows written through a binding compare against rows the
Rust/EQL path wrote. MatchTerm encodes its sorted positions as
little-endian u16s (EQL sends bf as a JSON integer array, so the
byte-string form is this crate's own). cllw-ore's variable-width
ciphertext types gain length-validating TryFrom<&[u8]> for the decode path
(the direction the existing From<Vec<u8>> FIXME asks for).

tests/frozen_bytes.rs carries the golden vectors (fixed hex the Go decoder
tests against) plus structural-rejection and real-leaf round-trip coverage;
tests/term_bytes.rs continues to pin the derivations these encodings wrap.

Part of CIP-3553 (stack-encrypt Go bindings), phase 2 of
docs/plans/stack-encrypt-go-bindings.md.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
…te layout

Expand SealedValue's frozen-encoding rustdoc with an offset table for the
v1 envelope and a diagram of the two nested framings (this crate's
envelope around vitaminc's LocalCipherText), spelling out where each
version byte lives and which AAD derivation binds it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
…e formats

- cllw-ore variable-width types: one private length predicate per type,
  applied by both `from_bytes(Vec<u8>) -> Result` and `TryFrom<&[u8]>`.
  The unchecked path is now `from_bytes_unchecked` (replacing the FIXME'd
  `From<Vec<u8>>`); it stays `pub` and `FromHex` stays permissive because
  cipherstash-client's ste_vec terms carry a tagged bit stream (65/66
  bytes) that the byte-aligned rule would reject. Callers renamed.
- `SealedValue` is valid-by-construction: `from_parts` and serde
  deserialisation reject tags longer than the u16 length field, so
  `to_bytes()` is infallible. `TryFrom<&[u8]>` rustdoc no longer claims a
  generic-codec justification.
- Document that the labelled, versioned `leaf_aad` cannot open leaves
  sealed under the phase-1 AAD (plain AEAD failure, no version signal).
- Reword the "Byte encodings" docs: same shape as v1 / cipherstash-client
  terms, values are not comparable across the two.

Claude-Session: https://claude.ai/code/session_01AhxMmV52dSYwjAT8KENHRV
… byte APIs

- `MatchTerm::from_positions`/`from_bytes` reject positions outside the
  filter size fixed by `O: MatchConfig`, so a mis-decoded position list
  fails loudly instead of producing a term that never matches.
- Replace the stringly `TermError::MalformedTermBytes(&str)` with a
  `TermBytesError` enum (`PartialEq`), mirroring `LeafBytesError`; CLLW
  decode failures report the offending length rather than the
  deliberately opaque `cllw_ore::Error`.
- Every term type now exposes `to_bytes()` and `TryFrom<&[u8]>`;
  `as_bytes()` only where a contiguous buffer exists. Plan doc states the
  per-type surface instead of "on every term type".
- Reframe the match term's LE-u16 byte string as the frozen FFI transport
  encoding; the stored and queried contract is the position list.
- Scope the `SealedValue` byte-format commitment to ciphertext and link
  the index-term encodings.

Claude-Session: https://claude.ai/code/session_01AhxMmV52dSYwjAT8KENHRV
- Move the sealed-leaf AAD breaking change out of `SealedValue`'s rustdoc
  and into a new packages/stack-encrypt/CHANGELOG.md. Release history does
  not belong on the type.

- Drop the cross-reference from `SealedValue`'s public docs to the
  `cipher` module docs. `mod cipher` is private, so its module-level
  `//!` docs render nowhere in `cargo doc` output except the source
  listing — the pointer sent readers to documentation the HTML does not
  contain. The leaf-AAD derivation it referred to is already stated inline.

- Write `LocalCipherText` and `Aes256Cipher` as code spans rather than
  intra-doc links. Both are private imports of vitaminc types, so rustdoc
  resolves them only when dependency docs are built; under `--no-deps`
  (what CI and `wasm:no-http-test` run) the links silently degrade to
  literal `[Name]` text, with no warning. Code spans render the same
  either way.

- Mark `LeafBytesError`, `TermBytesError` and `TermError`
  `#[non_exhaustive]` so the versioned decoders can gain variants without
  a source break.

Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
The seal path constructed SealedValue directly from the DataKeyWithTag a
DataKeySource returned, without the tag_fits_length_field check every
other construction site applies. A custom source returning a tag longer
than the u16 length field would (in release builds) produce a leaf whose
to_bytes saturates the length field but appends the whole tag, so
from_bytes no longer inverts the encoding.

seal_leaf now validates the generated tag before building the leaf, and
a seal-path boundary test drives encrypt through a DataKeySource that
inflates its tags past u16::MAX, asserting the seal fails rather than
mis-encodes.

Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
The cipher module was private with its items re-exported at the crate
root, so the 68 lines of module-level internals documentation (batching,
AAD derivation, wire format) never appeared in cargo doc output — the
crate docs pointed readers at src/cipher.rs source instead. The module
is now pub (matching sem and target) and the crate docs link to it.

The rustdoc lints this surfaces (a link to the private leaf_aad, two
redundant explicit StackKms link targets) were fixed in the previous
commit; cargo doc is warning-free with and without default features.

Claude-Session: https://claude.ai/code/session_01P5YHK3w6Kj9ajTnmkaXCHW
…h/claude/stack-encrypt-wasi-phase2

feat(stack-encrypt): freeze the byte formats the crate owns (WASI phase 2)
…raits

Two additive API changes the WASI guest (next commit) needs:

- PendingStackCipherText::into_pending — turn a pending tree into a
  Pending request carrier without settling it, so several independently
  built trees (e.g. one per record field, decoded from FFI values that
  are not Clone) merge with Pending::zip/all and seal in one batched
  generate_keys call. seal() is now expressed through it; same sealing
  path either way.
- sem re-exports CllwOreEncrypt/CllwOpeEncrypt: they already appear in
  the module's public bounds (ore_term, OreTerm, ...), so a caller
  writing a generic wrapper over the term APIs has to be able to name
  them without depending on cllw-ore directly.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
…se 3)

The wasm32-wasip1 guest at bindings/go/stackencrypt/guest (a detached
workspace, like the fuzz crates), per phase 3 of
docs/plans/stack-encrypt-go-bindings.md. Control stays in Rust: request
assembly, key derivation, batching and AAD/PRF context binding run
unmodified inside the guest; the host provides exactly two imports.

- Exports (vitaminc guest ABI conventions: buffer registry with zeroizing
  dealloc, packed-u64 results, hostile-input validation, no handle-id
  reuse): se_alloc/se_dealloc, se_cipher_init/se_cipher_free,
  se_encrypt/se_decrypt (+_element), se_encrypt_record/se_decrypt_record,
  se_term. Status codes 1-4 match vitaminc's; 5-11 map the ZeroKMS
  request outcomes and term failures so the Go caller can tell a bad
  token from a tampered ciphertext.
- Host imports (module cipherstash_transport): transport_send —
  cipherstash/cipherstash-suite#2099's import generalised to (method, url, headers, body) with
  'name: value' line headers — and token_get (phase-1 auth: the host
  owns minting and refresh). WasiHostConnection implements
  stack_kms::ZeroKMSConnection over it, with the endpoint pinned from
  the init config or discovered from the token's services claim;
  HostTokenStrategy implements stack_auth::AuthStrategy over token_get.
- Values cross in the vitaminc FFI codec (one codec, shared with the
  vitaminc guest); ciphertext-tree leaves are the frozen phase-2
  SealedValue byte encoding, so a leaf lifted out of a tree is exactly
  what a database column holds.
- Records: a plan {field -> {context, outputs: [c|eq|match|ore|ope]}}
  drives per-field ciphertexts and locally derived terms; all rows of a
  batch seal in one generate_keys via the new
  PendingStackCipherText::into_pending. Terms ride the result tree as
  passthrough bytes nodes.
- Native tests (26) run the same ops the ABI drives against
  FakeDataKeySource: codec round trips, native-decryptable leaves,
  term bytes equal to the native sem derivations, a counting key source
  pinning the one-call batching, and status mapping for hostile inputs.
  The release .wasm's import surface is exactly WASI +
  cipherstash_transport.
- mise tasks: wasm:guest:build, wasm:guest:test.

Extracting the shared ABI modules into a common vitaminc crate is a
follow-up in the vitaminc repository; the registry/session modules here
are copies with pointers back. bridge.go and the integration harness
land with the Go module (phases 4-5).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDes81qQM2ConLobY5G5JD
…nsports

`HttpConnection` and the WASI guest each carried their own copy of the
same table: 2xx must be JSON and deserialize, and 404/401/403/409 map to
specific `ViturRequestErrorKind`s so callers can tell a bad token from a
missing keyset without parsing strings. Two copies of a protocol contract
drift.

Move it to `connection::classify`, outside the `http` feature gate, so a
guest built without `http` reaches the same verdicts as the default
transport. `HttpConnection` now reads the response once and delegates;
`BaseUrlUnresolved`, `FailureResponse` and `UnexpectedContentType` are
public so a host bringing its own transport can return the same errors.

`Display` on the two response errors stays the concise form landed in
f3c87fbcc for cipherstash/cipherstash-suite#2158 — status and expectation only. Body and headers are
unbounded, attacker-influenced text and these types' `Display` reaches
logs; both remain available through `Debug`.

Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
`ClientKey::from_hex_v1` is strict lowercase hex, but the encodings a
user actually holds are not: `secretkey.json` serialises standard padded
base64, and hex pasted from elsewhere may be upper case. Front-ends that
take key material from an untyped boundary — an environment variable, a
config file, the WASI guest's FFI config object — were rejecting valid
keys for their encoding alone.

Add `from_encoded_v1`, the lenient counterpart, matching what
`SecretKey::from_hex` and `EnvKeyProvider` already accept. Decoding stays
constant-time (`base16ct` / `base64ct`) and the intermediate bytes are
wiped on every path.

Claude-Session: https://claude.ai/code/session_01BpqczxAVwUsTYdCRWh9dYb
auxesis and others added 5 commits October 2, 2026 17:55
`pnpm run test` now reaches the @cipherstash/auth and
@cipherstash/profile vitest suites, which load the napi module. Their
`test` scripts do not build it, and nothing else in run-tests does, so
profile-store.test.ts failed with "Failed to load native binding for
linux-x64" on this PR (Run Tests, Node 22 and 24).

run-tests now builds both bindings with `build:debug` after the
protect-ffi binding, before the test steps. The previous commit has
`build:debug` write its typings to the committed native.d.ts, so the
build leaves the tree clean. This step and that fix came from the CI
port (#1003); they land here because this PR merges first and has to
pass on its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
The root [workspace.dependencies] pinned both exactly, and stack-auth
inherits the requirement when it is packaged, so stack-auth 0.43.0 would
require cts-common =0.43.0 and zerokms-protocol =0.12.31. The suite then
could not unify registry stack-auth with a later compatible cts-common
patch: its [patch.crates-io] entry would go unused, and a second
cts-common would break the Crn, Region and WorkspaceId types that
stack-auth exposes. That defeats the cts-common release decision in plan
section 13.7.

Use caret requirements for the two crates. Cargo.lock still holds the
exact versions, so this changes no resolved version: every lock passes
cargo metadata --locked unchanged. recipher and cllw-ore stay exact,
because only unpublished crates use them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
Rename the module from github.com/cipherstash/cipherstash-suite/bindings/go
to github.com/cipherstash/stack/languages/golang, as plan section 3.4
decides. The old path names a private repository whose bindings/go folder
the suite removal deletes, so a go get of it could never resolve.

Nothing outside the module imports the old path, and no version of it was
ever tagged or fetchable through the Go proxy. The change is generated by
stack-migration/go-rename.sh (go mod edit -module, plus a text replace in
the module's .go and .md files): 36 files, 53 lines. go.sum is unchanged,
and go vet, go build and go test pass under the new path. At the freeze,
re-run the script rather than replaying this commit, because the cutover
re-export can bring the old path back in new files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
go vet catches an import of the old suite module path, but not a stale
go get line or pkg.go.dev link in a README. Check that go.mod declares
the stack path, and that no tracked file outside docs/plans/ names the
suite path, so the cutover re-export cannot bring it back unnoticed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
wasm:wasi-check greps cargo tree output for crates that must never link
into a WASI guest (wasm-bindgen, web-sys, js-sys, reqwest, hyper,
aws-lc-sys). CI sets CARGO_TERM_COLOR=always, so cargo wraps each line's
tree prefix in ANSI colour codes, and the grep, which anchors on that
prefix, can never match. A deliberate-break run on 2 October 2026 added
wasm-bindgen to stack-kms, and the CI step still printed "all WASI
crates compile with no JS-host or native-HTTP deps". Only the cargo
check half of the step was working.

Ask cargo tree for --color never. With CARGO_TERM_COLOR=always, the gate
now fails on that break and passes on the clean tree. A script test
checks every cargo tree call in the task asks for uncoloured output, and
records why with a coloured sample the patterns cannot match.

cipherstash-suite has the same task and the same CI setting, so its gate
is dead too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
@auxesis
auxesis force-pushed the refactor/typescript-to-languages branch from 229f473 to b3ffccd Compare October 2, 2026 08:07
@auxesis
auxesis force-pushed the build/import-stack-crates branch from bceb0bc to 1ad1055 Compare October 2, 2026 08:07
@auxesis
auxesis marked this pull request as ready for review October 2, 2026 17:56
@auxesis
auxesis requested a review from a team as a code owner October 2, 2026 17:56
Base automatically changed from refactor/typescript-to-languages to main October 2, 2026 18:01
@@ -0,0 +1,68 @@
import fs from 'node:fs'
import parseChangeset from '@changesets/parse'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing calls this script now. Commit 359a6f18 deleted its workflow, require-auth-npm-changeset.yml, and no package.json script or workflow names it.

Its rule is also the opposite of the rule in lint-no-auth-changeset.mjs. This script fails when a stack-auth change has no @cipherstash/auth changeset. The new lint fails when a changeset names one. AGENTS.md warns about this case for parked changesets: two guards with opposite rules, and one of them half-retired. If PR E connects this script again before it deletes the lint, the two will fail each other.

The script also imports @changesets/parse, which is not a root dependency. Under the strict pnpm layout, that import may not resolve.

Please delete this script in this PR. PR E can bring the rule back when it arms publishing.

Comment thread AGENTS.md
The stack-* crates came from `cipherstash/cipherstash-suite`, which still owns
`cipherstash-client`, `cts-common`, `zerokms-protocol`, `recipher` and
`cllw-ore`. Here those come from crates.io, pinned exactly in the root
`Cargo.toml`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says the suite crates are "pinned exactly in the root Cargo.toml". Commit 5147dd00 changed cts-common and zerokms-protocol to caret requirements, so only recipher and cllw-ore keep an exact = pin. The comment in Cargo.toml gives the reason. Please make this sentence agree. For example: "Here those come from crates.io, and Cargo.lock holds the exact versions. recipher and cllw-ore are pinned with =. cts-common and zerokms-protocol take caret requirements, because the published stack-auth inherits them."

Comment thread turbo.json
// beforehand; `test:cargo` runs the crate's own tests. `wasm/**` is the
// output of `build:wasm`, cached with the native build it ships beside.
"@cipherstash/auth#build:native": {
"outputs": ["*.node", "native.d.ts", "wasm/**"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entry has two problems. Nothing runs turbo run build:native today, so neither problem occurs yet.

  • The cache key is $TURBO_DEFAULT$ for languages/typescript/packages/auth only. The binding compiles packages/stack-auth through a path dependency, and that folder is outside the package. When only the stack-auth crate changes, turbo gets a cache hit and restores the previous *.node. The stash#build entry above shows the fix: add $TURBO_ROOT$/packages/stack-auth/**, the root Cargo.toml and Cargo.lock to inputs.
  • build:native does not make wasm/**. build:wasm makes it, from ../stack-auth-wasm. With wasm/** in this list, turbo stores whatever wasm/ is on disk when build:native ends, and a later cache hit restores it, even if it is stale.

If no workflow will call this task through turbo, delete the entry. If one will, fix inputs and remove wasm/**.

Comment thread docs/npm-releases.md
@@ -0,0 +1,189 @@
# npm releases (changesets)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker. This document describes the release process of the suite, and most of it is wrong in this repository:

  • release-npm.yml, publish-auth-npm.yml and publish-profile-npm.yml do not exist here.
  • The paths packages/stack-auth/node and packages/stack-profile/node do not exist.
  • It says that changesets never sees the platform packages. In this repository, pnpm-workspace.yaml now includes them.
  • It says that @cipherstash/profile publishes on the first Version Packages merge. This PR marks it private.

An agent that reads this file gets wrong instructions. Please add a short note at the top that says the file is historical (the process of the suite, which PR E replaces with release.yml), or move the file into docs/plans/.

# `index.node`. Each `build:debug` writes its typings to the committed
# `native.d.ts`, so the build leaves the tree clean.
- name: Build the auth and profile node bindings
run: pnpm --filter @cipherstash/auth --filter @cipherstash/profile run build:debug

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker. In each run-tests matrix leg, this step compiles two napi bindings from cold. It has no Rust cache, and it uses the default rustc of the runner, not the 1.94.1 pin in mise.toml. The protect-ffi step above restores index.node from a content-hash cache. If PR C does not add a cache here, please record that as a follow-up.

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review of 1ad1055d. I read the 15 commits after the import merge, but not the lockfiles or the formatting-only commit fa05f243. I checked the merge by its shape only, as the description asks.

The release-gate work is good. The files and noTreeBytes shapes fail closed, a missing listed file throws, and the Biome exclusions stop a reformat from turning into a gate skew. The wasi-check fix is correct: the coloured tree prefix contains m, so ^[^a-z]* could never match it. The new test records why.

I found no blocker. Please fix items 1 and 2 before merge, because they are small. Items 3 to 5 can be follow-ups. Each item has an inline comment.

  1. scripts/check-auth-npm-changeset.mjs has no caller, and its rule is the opposite of the rule in the new lint-no-auth-changeset.mjs.
  2. AGENTS.md says the suite crates are "pinned exactly". Cargo.toml gives two of them caret requirements.
  3. The cache key of the @cipherstash/auth#build:native turbo entry does not include the stack-auth crate, and the entry lists wasm/**, which a different script makes. This is latent, because nothing calls the task through turbo yet.
  4. docs/npm-releases.md describes the release process of the suite, not the process in this repository.
  5. tests.yml now compiles two bindings from cold in each matrix leg.

What I did not check: the content of the 408 imported files, the Go and Rust code, and the new tests in release-gate.test.mjs. A changeset is not necessary: @cipherstash/auth is frozen, the new lint forbids a changeset for it, and no other published surface changes.

@coderdan coderdan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Light review of 1ad1055d, done from the diff against the merge base b3ffccd4 with the suite checkout alongside for comparison.

Verified

  • The six packages/stack-* crates are byte-identical to suite main except tasks.toml (paths and mise x --env test prefixes) and the fuzz crates' now-tracked Cargo.lock.
  • The Go module and the three TypeScript bindings differ from the suite only by the module path rename, npm/ to platforms/, workspace:* pins, the Biome reformat, and the script renames that keep cargo off pnpm test.
  • cargo metadata --locked passes for the root lock and the five detached locks. cargo fmt --check is clean on the root workspace.
  • The script guards pass locally (1,019 tests), including the new Go module path, auth changeset, WASI gate and publish opt-out guards.
  • The freeze plumbing is coherent: FROZEN_PUBLISHERS carries the seven auth packages, the files digest covers the wrapper's 15 published files, Biome is kept off exactly those files, and the changeset lint stops a bump on the PR rather than on main.

Nothing blocking. Four inline nits, none of which need to land before the freeze. The CI gap (no Rust tests run on the crates in this PR) is as the description says: PR C ports it, and the merge order covers it.


Generated by Claude Code

# NOTE: this must stay the LAST command — the crap-stack-auth.yml CI workflow runs
# this task and relies on mise appending its trailing args (e.g. --format github)
# to this `cargo crap` invocation.
"mise x --env test -- cargo crap --path packages/stack-auth --lcov {{config_root}}/target/stack-auth-lcov.info --exclude 'node/**' --exclude 'wasm/**' --exclude 'examples/**' --exclude '**/tests.rs' --exclude '**/tests/**' --fail-above",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: --exclude 'node/**' and --exclude 'wasm/**' are stale. Those directories moved to languages/typescript/packages/auth and stack-auth-wasm, so nothing under packages/stack-auth matches them now. Harmless, but worth dropping in the CI port or a follow-up.


Generated by Claude Code

Comment thread .github/dependabot.yml
- minor
- patch
ignore:
# Released from cipherstash-suite and pinned with exact `=` requirements

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this comment predates 5147dd00. cts-common and zerokms-protocol now take caret requirements in the root Cargo.toml; only recipher and cllw-ore (and the two Go guests) still pin with =. The ignore list itself is still right, since all four move in lockstep with the suite by hand. Same stale wording as the AGENTS.md line already flagged above.


Generated by Claude Code

# path; this step builds it, as the protect-ffi step above does for
# `index.node`. Each `build:debug` writes its typings to the committed
# `native.d.ts`, so the build leaves the tree clean.
- name: Build the auth and profile node bindings

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: this step compiles the root workspace with whatever Rust the runner ships, not the 1.94.1 pinned in the root mise.toml. Nothing in this job runs mise install at the repo root (the protect-ffi action runs mise from its own folder, which does not pin Rust). It passes today, and napi build only needs the crates to compile, so this is fine for PR B. Worth keeping in mind for the CI port, where the trybuild snapshots and clippy do depend on the pinned toolchain.


Generated by Claude Code


```bash
mise run wasm:guest:build wasm:auth-guest:build
cd bindings/go && go run ./stackencrypt/example

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: a few prose references to the old bindings/go folder survived the rename, because go-module-path.test.mjs only checks the full module URL. The ones that are actual instructions are this line and example/explicit/README.md:27,31 (cd bindings/go no longer exists). The rest are comments: example/main.go:7, packages/stack-encrypt/CONTEXT.md:6, packages/stack-guest-abi/Cargo.toml:12, packages/stack-guest-abi/tasks.toml:2, stack-guest-abi/src/{lib,buffers}.rs, stackauth/guest/src/lib.rs:52, both guests' Cargo.toml, and ADR 0005. Fine as a follow-up; if you want the guard to catch these too, a second needle on bindings/go with an allowlist for the vitaminc/bindings/go/... imports would do it.


Generated by Claude Code

@auxesis
auxesis merged commit f1895d8 into main Oct 2, 2026
28 checks passed
@auxesis
auxesis deleted the build/import-stack-crates branch October 2, 2026 18:25
auxesis added a commit that referenced this pull request Oct 2, 2026
AGENTS.md and a comment in .github/dependabot.yml still said the suite
crates are pinned exactly in the root Cargo.toml. PR B (#1001) gave
cts-common and zerokms-protocol caret requirements, because the
published stack-auth inherits them, so only recipher and cllw-ore keep
an exact pin there. freshtonic and coderdan raised both in review of
#1001.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PaY5xYydZUWhv8Nex9Sw8a
@auxesis

auxesis commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

@freshtonic and @coderdan, thank you both. This PR merged at 18:25 UTC as part of today's cutover. Here is where each item went.

freshtonic's two items for before the merge

  1. scripts/check-auth-npm-changeset.mjs has no caller, and its rule is the opposite of lint-no-auth-changeset.mjs. PR E, ci: arm publishing for @cipherstash/auth and the stack-* crates #1009, handles this in order. Its first commit deletes lint-no-auth-changeset.mjs, and its second commit connects this script again, with its paths renamed and @changesets/parse added as a root dependency. So the two guards are never active together. Until PR E merges later today, the script sits on main with no caller.
  2. AGENTS.md says the suite crates are "pinned exactly". Commit 17a5df14 on chore(release): stack-auth and stack-profile 0.43.0 #1010 corrects the sentence, and coderdan's matching comment in .github/dependabot.yml. Only recipher and cllw-ore keep an exact = pin in the root Cargo.toml.

Follow-up issues for the rest

  • The cache inputs and outputs of the @cipherstash/auth#build:native turbo task: CIP-4277.
  • docs/npm-releases.md describes the suite's release process: CIP-4278.
  • The binding build in tests.yml has no cache and uses the runner's Rust: CIP-4279.
  • The stale node/** and wasm/** excludes in packages/stack-auth/tasks.toml: CIP-4280.
  • The leftover bindings/go references, and a wider Go path guard: CIP-4281.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants