Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5cca158
feat(golang): stashgen generator library and encrypt/gensupport
coderdan Oct 6, 2026
16546c3
feat(golang): stashgen command, refusal tests and the stub contract c…
coderdan Oct 6, 2026
dc9e348
feat(golang): declarations from a policy, and protosource
coderdan Oct 6, 2026
fc591d8
refactor(golang)!: rename stackencrypt to encrypt and stackauth to auth
coderdan Oct 6, 2026
2c635ef
feat(golang)!: the generated API replaces the value and record calls
coderdan Oct 6, 2026
35680fa
fix(golang): every field type the generator accepts round-trips
coderdan Oct 6, 2026
b127482
fix(golang): a term the engine cannot derive names its field and index
coderdan Oct 6, 2026
4f93d35
fix(golang): the deterministic test guest lives under testdata, out o…
coderdan Oct 6, 2026
b51efd6
fix(golang): Get reads every type the generator accepts, and a model …
coderdan Oct 6, 2026
b9348c1
fix(golang): se_targets entries are read whole against the agreed wir…
coderdan Oct 6, 2026
6e3e9ef
test(golang): the refusals hold against the embedded engine, not only…
coderdan Oct 6, 2026
54618a2
test(golang): residency on every pull request, and the fixture test f…
coderdan Oct 6, 2026
65039a3
refactor(golang): one DeterministicSource, protoc-gen-go's GoName, an…
coderdan Oct 6, 2026
d7a2dd8
chore(stack-kms): DeterministicSource debugs opaquely; stack-encrypt …
coderdan Oct 7, 2026
2eeb79c
fix(golang)!: index-only fields and nil interface passthroughs round-…
coderdan Oct 7, 2026
af1b266
chore(golang): mark stashgen output as linguist-generated
auxesis Oct 7, 2026
811b70b
chore(golang): keep generated *_stash.go files expanded in review
auxesis Oct 7, 2026
5f0c393
fix(golang): Decrypt errors hold no plaintext and wrap ErrEncoding
coderdan Oct 7, 2026
0c5417e
fix(golang): docs say what this build does; -redact covers %#v
coderdan Oct 7, 2026
8a63d8e
fix(golang): a field added to an embedded struct stops the build; CI …
coderdan Oct 7, 2026
bbcf03e
fix(golang)!: the policy path checks names and takes its output as a …
coderdan Oct 7, 2026
d7e7f81
refactor(golang)!: names that do not repeat their package, in Go's sp…
coderdan Oct 7, 2026
d680c27
docs(stack-encrypt): drop the Go plan.Custom note, the package is rem…
coderdan Oct 7, 2026
8903021
fix(golang): parseTarget refuses an unknown kind or index
coderdan Oct 7, 2026
8619046
docs(golang): say what the SDK does not protect, and fix the example'…
coderdan Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@ updates:
- /packages/stack-auth/fuzz
- /packages/stack-kms/fuzz
- /packages/stack-encrypt/fuzz
- /languages/golang/stackencrypt/guest
- /languages/golang/stackauth/guest
- /languages/golang/encrypt/guest
- /languages/golang/auth/guest
# Monthly, matching the other two cargo entries.
schedule:
interval: monthly
Expand Down
58 changes: 44 additions & 14 deletions .github/workflows/tests-golang.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ name: Tests (Go)
# wasi-check the stack crates build for wasm32-wasip1 with no JS-host or
# native-HTTP dependencies, the no-http shape passes its tests
# and docs, both guests pass lint and tests and are built with
# their import surfaces checked, their sha256 is recorded, and
# `go:test` runs against them.
# their import surfaces checked (the stack-encrypt guest twice:
# the real build and the deterministic-kms test build), their
# sha256 is recorded, `go:test` runs against them, and
# `go generate` leaves the tree unchanged.
# go-lint golangci-lint, Linux only.
# go-binding-cross
# the same Go tests on macOS and Windows, against the guests
Expand Down Expand Up @@ -122,8 +124,8 @@ jobs:
with:
workspaces: |
.
languages/golang/stackencrypt/guest
languages/golang/stackauth/guest
languages/golang/encrypt/guest
languages/golang/auth/guest

# The HTTP-free core compiles for wasm32-wasip1 with no JS-host backend
# and no native HTTP/TLS stack in its graph: the invariant the wazero
Expand All @@ -146,6 +148,14 @@ jobs:
- name: stack-encrypt guest release build and import-surface gate
run: mise run wasm:guest:build

# The deterministic-kms TEST build, under encrypt/testdata where no
# build or embed sees it: the Go tests open the record fixture Rust
# sealed through it, and run round trips with no ZeroKMS. The tests
# skip when it is absent, so a build that forgets this step would pass
# with less coverage, which is why the job builds it unconditionally.
- name: stack-encrypt guest deterministic test build
run: mise run wasm:guest:build:deterministic

- name: Credential guest lint and tests
run: mise run wasm:auth-guest:test

Expand All @@ -157,7 +167,7 @@ jobs:
# same bytes rather than a stale or rebuilt guest.
- name: Record the guests' checksums
run: |
for guest in stackencrypt/wasm/stack_encrypt_guest.wasm stackauth/wasm/stack_auth_guest.wasm; do
for guest in encrypt/wasm/stack_encrypt_guest.wasm encrypt/testdata/stack_encrypt_guest_deterministic.wasm auth/wasm/stack_auth_guest.wasm; do
(cd languages/golang && openssl dgst -sha256 "$guest" | awk '{print $NF}' > "$guest.sha256" && echo "$guest sha256 $(cat "$guest.sha256")")
done

Expand All @@ -166,25 +176,45 @@ jobs:
with:
name: wasm-guests
path: |
languages/golang/stackencrypt/wasm/stack_encrypt_guest.wasm
languages/golang/stackencrypt/wasm/stack_encrypt_guest.wasm.sha256
languages/golang/stackauth/wasm/stack_auth_guest.wasm
languages/golang/stackauth/wasm/stack_auth_guest.wasm.sha256
languages/golang/encrypt/wasm/stack_encrypt_guest.wasm
languages/golang/encrypt/wasm/stack_encrypt_guest.wasm.sha256
languages/golang/encrypt/testdata/stack_encrypt_guest_deterministic.wasm
languages/golang/encrypt/testdata/stack_encrypt_guest_deterministic.wasm.sha256
languages/golang/auth/wasm/stack_auth_guest.wasm
languages/golang/auth/wasm/stack_auth_guest.wasm.sha256
if-no-files-found: error
retention-days: 1

# Format, vet and hermetic tests on amd64 and 386. The guest's memory
# lock is best effort, so its test skips where RLIMIT_MEMLOCK refuses
# it; CI raises the limit and sets STACKENCRYPT_TESTS_REQUIRE_LOCK so
# it; CI raises the limit and sets STACK_ENCRYPT_TESTS_REQUIRE_LOCK so
# the skip is an error here.
- name: Go binding
env:
STACKENCRYPT_TESTS_REQUIRE_LOCK: "1"
STACK_ENCRYPT_TESTS_REQUIRE_LOCK: "1"
run: |
ulimit -l "$(ulimit -H -l)"
echo "RLIMIT_MEMLOCK: $(ulimit -l) KiB"
mise run go:test

# What is encrypted is fixed before the program ships: every generated
# file is committed, and this fails when `go generate` would change
# one. It runs the real stashgen against the guest just built, so it
# also proves the generator and the engine agree on the module's own
# examples. `git diff` sees only tracked files, so the status check
# catches a generated file that was never committed.
- name: Generated code is committed
working-directory: languages/golang
run: |
CGO_ENABLED=0 go generate ./...
git diff --exit-code -- .

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fix in a follow-up: this step does not find a generated file that was never committed.

Impact: Go10 says "CI fails when one is out of date." git diff ignores untracked files. If a developer adds a //go:generate line and does not commit its output, this step passes. The build fails only if other code uses the missing file.

Evidence:

  • This line runs git diff --exit-code -- ., which compares tracked files only.
  • The recipe for users has the same gap: languages/golang/encrypt/README.md:66, languages/golang/cmd/stashgen/README.md:55 and languages/golang/encrypt/doc.go:45.

Fix: Also fail on an untracked file, here and in the three recipes:

CGO_ENABLED=0 go generate ./...
git diff --exit-code -- .
test -z "$(git status --porcelain -- .)"

Found by 1 model: claude

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d4dc295. The CI step now also fails on untracked files under languages/golang and prints them, and the three recipes add test -z "$(git status --porcelain)".

untracked="$(git status --porcelain -- .)"
if [ -n "$untracked" ]; then
echo "::error::go generate wrote files that are not committed:"
echo "$untracked"
exit 1
fi

# Linux only: macOS and Windows would report the same findings. Needs no
# guests: the packages embed a directory and compile without them.
go-lint:
Expand Down Expand Up @@ -260,7 +290,7 @@ jobs:
- name: The guests are the ones Linux built and checked
run: |
cd languages/golang
for guest in stackencrypt/wasm/stack_encrypt_guest.wasm stackauth/wasm/stack_auth_guest.wasm; do
for guest in encrypt/wasm/stack_encrypt_guest.wasm encrypt/testdata/stack_encrypt_guest_deterministic.wasm auth/wasm/stack_auth_guest.wasm; do
want=$(cat "$guest.sha256")
got=$(openssl dgst -sha256 "$guest" | awk '{print $NF}')
if [ "$want" != "$got" ]; then
Expand Down Expand Up @@ -326,7 +356,7 @@ jobs:
- name: The guests are the ones the wasi-check job built and checked
run: |
cd languages/golang
for guest in stackencrypt/wasm/stack_encrypt_guest.wasm stackauth/wasm/stack_auth_guest.wasm; do
for guest in encrypt/wasm/stack_encrypt_guest.wasm encrypt/testdata/stack_encrypt_guest_deterministic.wasm auth/wasm/stack_auth_guest.wasm; do
want=$(cat "$guest.sha256")
got=$(openssl dgst -sha256 "$guest" | awk '{print $NF}')
if [ "$want" != "$got" ]; then
Expand All @@ -340,7 +370,7 @@ jobs:
# call `liveClient`, and not all of them are named `TestLive*`.
- name: Go live tests
working-directory: languages/golang
run: CGO_ENABLED=0 go test -v ./stackencrypt/...
run: CGO_ENABLED=0 go test -v ./encrypt/...

- name: stack-encrypt examples
run: |
Expand Down
12 changes: 8 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ mutants.out/

# The Go module's embedded WASI guests: build outputs of `mise run
# wasm:guest:build` and `mise run wasm:auth-guest:build`.
languages/golang/stackencrypt/wasm/*.wasm
languages/golang/stackauth/wasm/*.wasm
languages/golang/stackencrypt/wasm/*.sha256
languages/golang/stackauth/wasm/*.sha256
languages/golang/encrypt/wasm/*.wasm
languages/golang/auth/wasm/*.wasm
languages/golang/encrypt/wasm/*.sha256
languages/golang/auth/wasm/*.sha256
# The deterministic-kms TEST build of the stack-encrypt guest, from `mise run
# wasm:guest:build:deterministic`; under testdata so no binary embeds it.
languages/golang/encrypt/testdata/*.wasm
languages/golang/encrypt/testdata/*.sha256
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Every npm package except EQL lives under `languages/typescript/`: packages in `l
links are provenance only.
- `packages/stack-auth`, `packages/stack-profile`, `packages/stack-kms`, `packages/stack-encrypt`, `packages/stack-encrypt-derive`, `packages/stack-guest-abi`: The Rust crates imported from `cipherstash/cipherstash-suite` with their history — `stack-auth` and `stack-profile` (published to crates.io, one version group), `stack-kms` (published to crates.io from 0.1.0, its own version group, re-exported by `stack-encrypt` as `stack_encrypt::kms`), `stack-encrypt` and `stack-encrypt-derive` (published to crates.io from 0.1.0, one version group; `eql-bindings`' `stack-encrypt` feature depends on them from the registry), and `stack-guest-abi` (`publish = false`). They are the members of the **root Cargo workspace**, with the three node binding crates below. See "Working on the Rust crates".
- `languages/typescript/packages/auth`, `languages/typescript/packages/profile`, `languages/typescript/packages/stack-auth-wasm`: The node bindings of those crates. `@cipherstash/auth` (napi-rs v2) and its six `platforms/*` packages are published to npm from this repository by `release.yml` (`auth-artifacts`, `publish-auth`); a change to what it ships, the `stack-auth` crate included, needs an `@cipherstash/auth` changeset (`require-auth-npm-changeset.yml`). `@cipherstash/profile` and its platforms are private and never published; `@cipherstash/stack-auth-wasm` is private and builds the wasm that `@cipherstash/auth` ships. Their `build` and `test` scripts never invoke cargo; `build:native`, `build:debug` and `test:cargo` do.
- `languages/golang`: The Go module (`stackencrypt`, `stackauth`, `internal`), a wazero host with no cgo. Its two WASI guests (`*/guest`) are detached Cargo workspaces built by `mise run wasm:guest:build` and `mise run wasm:auth-guest:build`; the `.wasm` files they embed are gitignored. There is no Go release process yet.
- `languages/golang`: The Go module — the SDK `encrypt` with its generated-code support `encrypt/gensupport` and the policy packages `encrypt/policy` and `encrypt/policy/protosource`; the credential package `auth`; the generator `stashgen` and its command `cmd/stashgen`; and `internal` (the shared guest plumbing and the `record` wire model). A wazero host with no cgo. Its two WASI guests (`encrypt/guest`, `auth/guest`) are detached Cargo workspaces built by `mise run wasm:guest:build` and `mise run wasm:auth-guest:build`; `mise run wasm:guest:build:deterministic` builds the seeded TEST build the hermetic Go tests use; the `.wasm` files they embed are gitignored. Generated `*_stash.go` files are committed and CI fails when `go generate ./...` changes one. There is no Go release process yet.
- `e2e/*`: Cross-package end-to-end tests (package managers, supply chain, Prisma example README)
- `languages/typescript/examples/*`: Working apps (basic, prisma, supabase-worker)
- `docs/plans/*`: Internal design plans. User-facing documentation lives at https://cipherstash.com/docs (not in this repo).
Expand Down
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ exclude = [
"packages/stack-encrypt/fuzz",
# WASI guests for the Go module, built through `wasm:guest:build` and
# `wasm:auth-guest:build`.
"languages/golang/stackencrypt/guest",
"languages/golang/stackauth/guest",
"languages/golang/encrypt/guest",
"languages/golang/auth/guest",
]

[workspace.package]
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ It also carries the source of five Rust crates published to crates.io,
`packages/stack-profile`) and **`stack-kms`**, **`stack-encrypt`** and
**`stack-encrypt-derive`** (`packages/stack-kms`, `packages/stack-encrypt`,
`packages/stack-encrypt-derive`), and of the **Go module** at
`languages/golang` (`stackencrypt` and `stackauth`, over WASI guests built
`languages/golang` (`encrypt` and `auth`, over WASI guests built
from the stack-* crates), which has no release yet. All of these are in scope
for security reports on the same terms as the npm packages above.

Expand Down
10 changes: 10 additions & 0 deletions languages/golang/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# stashgen's golden test expectations. Marked `linguist-generated` so GitHub
# collapses them in pull-request and commit diffs by default and excludes them
# from repository language statistics. This is a GitHub display hint only:
# nothing about Git, CI, or the build changes.
#
# The generated *_stash.go files are deliberately NOT marked. They are the
# committed record of which fields are encrypted, with which indexes and under
# which context, so a change to them must stay expanded for the reviewer to
# read (docs/sdk-design-principles.md, Go principle 10).
*.golden linguist-generated
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# stackauth
# auth

The Go binding of the developer profile — the directory `stash auth login`
writes — read through the `stack-profile` Rust crate running inside a WASI
guest under [wazero], with `CGO_ENABLED=0`. It is the credential half of
the Go SDK: it hands a [`stackencrypt`](../stackencrypt) client its client
the Go SDK: it hands an [`encrypt`](../encrypt) client its client
key and its bearer token without either package re-deriving the profile's
layout, and without either importing the other.

Expand All @@ -16,30 +16,30 @@ cross-process refresh lock for device sessions.

## Use

Most applications never call this package directly: a `stackencrypt`
Most applications never call this package directly: an `encrypt`
client built with `NewClient(ctx)` and no options resolves its credentials with
`stackencrypt.AutoCredentials`, which reads the environment first and then
`encrypt.AutoCredentials`, which reads the environment first and then
the profile, through this package. Use it directly to take the profile
apart yourself:

```go
import (
"context"

"github.com/cipherstash/stack/languages/golang/stackauth"
"github.com/cipherstash/stack/languages/golang/stackencrypt"
"github.com/cipherstash/stack/languages/golang/auth"
"github.com/cipherstash/stack/languages/golang/encrypt"
)

func run(ctx context.Context) error {
profile, err := stackauth.Resolve(ctx) // CS_CONFIG_PATH, else ~/.cipherstash
profile, err := auth.Resolve(ctx) // CS_CONFIG_PATH, else ~/.cipherstash
if err != nil {
return err
}
defer profile.Close()

workspace, err := profile.CurrentWorkspaceStore(ctx)
if err != nil {
return err // stackauth.ErrNoCurrentWorkspace: run `stash auth login`
return err // auth.ErrNoCurrentWorkspace: run `stash auth login`
}
clientID, clientKey, err := workspace.SecretKey(ctx)
if err != nil {
Expand All @@ -50,9 +50,9 @@ func run(ctx context.Context) error {
return err
}
defer source.Close()
client, err := stackencrypt.NewClient(ctx,
client, err := encrypt.NewClient(ctx,
// The key is consumed and wiped by NewClient.
stackencrypt.WithCredentials(stackencrypt.NewCredentials(clientID, clientKey, source)),
encrypt.WithCredentials(encrypt.NewCredentials(clientID, clientKey, source)),
)
if err != nil {
return err
Expand All @@ -63,32 +63,32 @@ func run(ctx context.Context) error {
}
```

`stackauth.ClientKey` and `stackencrypt.ClientKey` are one type, so the
`auth.ClientKey` and `encrypt.ClientKey` are one type, so the
key goes straight from the profile into the credentials. The profile and
the strategy are the caller's: the client asks the strategy for a token on
every request but never closes it, so both stay open until the client is
closed (the deferred calls above run in that order).

A stackencrypt client takes its token only from a strategy, never a raw
An encrypt client takes its token only from a strategy, never a raw
string: a raw token cannot be refreshed when it expires, and would bypass
the cross-process lock a device-session refresh holds with the `stash` CLI
(the IdP revokes a whole refresh-token chain when one is used twice).
`workspace.Token(ctx)` still reads the stored token, for inspection.

With no profile directory at all (CI, a container, a server authenticating
by federation), `stackauth.OpenWithoutProfile(ctx)` runs the guest with
by federation), `auth.OpenWithoutProfile(ctx)` runs the guest with
nothing mounted: the access-key and OIDC strategies work, and every profile
read is `ErrNoProfile`.

`profile.AccessKey(ctx, crn, key)`, `profile.OIDC(ctx, crn, provider)`, and
`profile.Auto(ctx)` also return strategies that `stackencrypt.NewCredentials`
`profile.Auto(ctx)` also return strategies that `encrypt.NewCredentials`
takes. `Auto` checks `CS_CLIENT_ACCESS_KEY` and
`CS_WORKSPACE_CRN` first, then the current workspace's stored device session.
The OIDC provider is a one-method `Token(context.Context) (string, error)`
interface, called on every token fetch for the JWT of the user the call is
for; each distinct JWT is exchanged once while its CTS token lasts. Use
`stackauth.OAuth2TokenSource(source)` to adapt a
`golang.org/x/oauth2.TokenSource`. `WithAuthBaseURL(url)` overrides service
`auth.OAuth2TokenSource(source)` to adapt a
`golang.org/x/oauth2.TokenSource`. `WithBaseURL(url)` overrides service
discovery for local tests or a custom CTS host; `WithCacheCapacity(n)` sets
how many users' CTS tokens an OIDC strategy keeps (1024 unless set), sized to
the users it serves within a CTS token's lifetime.
Expand All @@ -107,7 +107,7 @@ session refresh call, on the path `ProfileStore.LockPath` names. A fresh
token is read without the lock; on refresh the guest re-reads
auth.json after acquisition and saves refreshed tokens before release.

The crypto guest behind `stackencrypt` is not widened by this package
The crypto guest behind `encrypt` is not widened by this package
existing: it still has no filesystem and no environment.

## Build
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package stackauth
package auth

import "github.com/cipherstash/stack/languages/golang/internal/guest"

// ClientKey is the ZeroKMS client key as [ProfileStore.SecretKey] reads it
// out of secretkey.json: opaque (it prints a redaction under every verb and
// hands its bytes to no caller) and wiped once consumed. It is the same
// type as stackencrypt.ClientKey, by identity, so a key read here goes
// straight into stackencrypt.NewCredentials. This package does not import
// stackencrypt: a binary that only wants the profile does not carry the
// crypto guest. (stackencrypt imports this one, for AutoCredentials.)
// type as encrypt.ClientKey, by identity, so a key read here goes
// straight into encrypt.NewCredentials. This package does not import
// encrypt: a binary that only wants the profile does not carry the
// crypto guest. (encrypt imports this one, for AutoCredentials.)
type ClientKey = guest.ClientKey
Loading
Loading