Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
481890a
feat(golang): stashgen generator library and encrypt/gensupport
coderdan Oct 6, 2026
698b3af
feat(golang): stashgen command, refusal tests and the stub contract c…
coderdan Oct 6, 2026
579eb8d
feat(golang): declarations from a policy, and protosource
coderdan Oct 6, 2026
d0c4e2c
refactor(golang)!: rename stackencrypt to encrypt and stackauth to auth
coderdan Oct 6, 2026
739ecf1
feat(golang)!: the generated API replaces the value and record calls
coderdan Oct 6, 2026
3b53ce9
fix(golang): every field type the generator accepts round-trips
coderdan Oct 6, 2026
711c08a
fix(golang): a term the engine cannot derive names its field and index
coderdan Oct 6, 2026
21995e6
fix(golang): the deterministic test guest lives under testdata, out o…
coderdan Oct 6, 2026
4573a05
fix(golang): Get reads every type the generator accepts, and a model …
coderdan Oct 6, 2026
cf04a94
fix(golang): se_targets entries are read whole against the agreed wir…
coderdan Oct 6, 2026
60c2821
test(golang): the refusals hold against the embedded engine, not only…
coderdan Oct 6, 2026
2695b38
test(golang): residency on every pull request, and the fixture test f…
coderdan Oct 6, 2026
772be38
refactor(golang): one DeterministicSource, protoc-gen-go's GoName, an…
coderdan Oct 6, 2026
e739fdf
chore(stack-kms): DeterministicSource debugs opaquely; stack-encrypt …
coderdan Oct 7, 2026
fa106c4
fix(golang)!: index-only fields and nil interface passthroughs round-…
coderdan Oct 7, 2026
62f223d
chore(golang): mark stashgen output as linguist-generated
auxesis Oct 7, 2026
b7a4a72
chore(golang): keep generated *_stash.go files expanded in review
auxesis Oct 7, 2026
49e7480
fix(golang): Decrypt errors hold no plaintext and wrap ErrEncoding
coderdan Oct 7, 2026
115c783
fix(golang): docs say what this build does; -redact covers %#v
coderdan Oct 7, 2026
9f7e191
fix(golang): a field added to an embedded struct stops the build; CI …
coderdan Oct 7, 2026
8337266
fix(golang)!: the policy path checks names and takes its output as a …
coderdan Oct 7, 2026
c1741ba
refactor(golang)!: names that do not repeat their package, in Go's sp…
coderdan Oct 7, 2026
2a802ad
docs(stack-encrypt): drop the Go plan.Custom note, the package is rem…
coderdan Oct 7, 2026
34f95f7
fix(golang): parseTarget refuses an unknown kind or index
coderdan Oct 7, 2026
ac6897a
docs(golang): say what the SDK does not protect, and fix the example'…
coderdan Oct 7, 2026
57e9ad1
feat(stack-encrypt): a data plan can take its context from a field of…
coderdan Oct 7, 2026
cfa9664
feat(golang): the guest opens a record under the context the host exp…
coderdan Oct 7, 2026
4c92d5f
feat(golang): a context_field tag binds each row to its own context
coderdan Oct 7, 2026
8309934
fix(stack-kms): the deterministic source refuses a wrong context as f…
coderdan Oct 7, 2026
c1bb0dd
test(golang): code generated with -for and from a policy runs against…
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
7 changes: 7 additions & 0 deletions docs/plans/2026-10-04-plan-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ the builder itself, #1071), with a second round of decisions: decrypt is
spelled `open`, the two starts and the three context sources, the typed
verb and the picker, the derive narrowed before it emits the plan, and EQL
types assembled per language with no registry.
Amended 2026-10-07 (#1094): where this document says the typed parts "have
no data form" and lists `context_field` among them ("Consolidation", and
item 3 under "Why the first draft was dropped"), it is wrong about
`context_field`. That verb is not typed, and the data grammar now spells it
as a plan-level `"context_field"` key; the Go SDK spells it as the
`context_field` tag word. The typed parts that have no data form are
`encrypt_into`, the picker and the one-value start.
**Date:** 2026-10-04
**Issue:** #1046
**Builds on:** #1050 (one context per column; `Label`, `Describe`), #971 (EQL v3
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