From 102a2f4640dc832085a7c0efc26e74ffa2499772 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sun, 18 Oct 2026 18:15:13 +1100 Subject: [PATCH 1/3] feat(eql-bindings): put the by-name dispatch behind a targets feature, and keep dynamic out of stack-encrypt eql-bindings' stack-encrypt feature turned on Stack Encrypt's dynamic layer for every user, although only the by-name dispatch a language binding needs uses it: encryption::targets and the generated v4::targets take a ValueFrame and name an EQL type at run time. A Rust program that encrypts an IntegerEq through encrypt_as compiled the whole dynamic layer for nothing. stack-encrypt now gives the typed EQL v4 types alone, with Stack Encrypt's default features off and no dynamic. A new targets feature turns on stack-encrypt, Stack Encrypt's dynamic feature, and zeroize and thiserror, which only the dispatch uses, and compiles the two targets modules. The Go guest's eql build and the encryption tests enable targets. test:encryption tests eql-bindings under both feature sets, and the wasm32 check builds targets, as the guest does. docs.rs documents targets. The comment on the encryption::targets declaration is a plain comment: a doc comment there would resolve the module's own intra-doc links from the parent scope. Refs #1252. --- .changeset/eql-plan-field-targets.md | 2 +- .changeset/eql-target-match-options.md | 2 +- .changeset/eql-v4-bindings.md | 2 +- languages/golang/encrypt/guest/Cargo.toml | 2 +- packages/eql/crates/eql-bindings/CHANGELOG.md | 14 ++++++--- packages/eql/crates/eql-bindings/Cargo.toml | 31 +++++++++++++------ packages/eql/crates/eql-bindings/README.md | 13 +++++++- .../eql/crates/eql-bindings/src/encryption.rs | 10 ++++-- packages/eql/crates/eql-bindings/src/lib.rs | 4 +-- .../eql/crates/eql-bindings/src/v4/mod.rs | 2 ++ .../eql/crates/eql-codegen/src/bindings.rs | 2 +- .../eql/crates/eql-codegen/src/targets.rs | 2 +- packages/eql/mise.toml | 7 +++-- packages/eql/tests/encryption/Cargo.toml | 2 +- 14 files changed, 67 insertions(+), 28 deletions(-) diff --git a/.changeset/eql-plan-field-targets.md b/.changeset/eql-plan-field-targets.md index ea51799c8..18b37b590 100644 --- a/.changeset/eql-plan-field-targets.md +++ b/.changeset/eql-plan-field-targets.md @@ -2,4 +2,4 @@ '@cipherstash/eql': minor --- -**The `eql-bindings` crate resolves an EQL type named as a string to its own Stack Encrypt plan** (`stack-encrypt` feature). `eql_bindings::encryption::targets` carries a catalog-generated table of every EQL type a data plan may name as a field target — its name across languages, family and suffix, the plaintext `ValueKind` it takes, the indexes it carries, its query twin, and whether the engine can produce it today with the reason when not — and `encrypt` / `decrypt` / `query` entry points that dispatch on the name and run the type's derived `EncryptFrom` / `DecryptInto`, resolving to the EQL value's JSON bytes through the engine's `Pending` so a guest batches it with the rest of a plan. This is the EQL half of EQL types as plan field targets (cipherstash/stack#1062): a Go data plan names `TextEq` and the guest returns the finished EQL value instead of assembling one. Every target is an `eql_v4_*` domain. `TextEq` is the only producible type; every other name is refused with the plan's reason. The SQL surface and the TypeScript package are unchanged. +**The `eql-bindings` crate resolves an EQL type named as a string to its own Stack Encrypt plan** (`targets` feature, which turns on `stack-encrypt`). `eql_bindings::encryption::targets` carries a catalog-generated table of every EQL type a data plan may name as a field target — its name across languages, family and suffix, the plaintext `ValueKind` it takes, the indexes it carries, its query twin, and whether the engine can produce it today with the reason when not — and `encrypt` / `decrypt` / `query` entry points that dispatch on the name and run the type's derived `EncryptFrom` / `DecryptInto`, resolving to the EQL value's JSON bytes through the engine's `Pending` so a guest batches it with the rest of a plan. This is the EQL half of EQL types as plan field targets (cipherstash/stack#1062): a Go data plan names `TextEq` and the guest returns the finished EQL value instead of assembling one. Every target is an `eql_v4_*` domain. `TextEq` is the only producible type; every other name is refused with the plan's reason. The SQL surface and the TypeScript package are unchanged. diff --git a/.changeset/eql-target-match-options.md b/.changeset/eql-target-match-options.md index 51e94f0fa..49368aa91 100644 --- a/.changeset/eql-target-match-options.md +++ b/.changeset/eql-target-match-options.md @@ -2,4 +2,4 @@ '@cipherstash/eql': minor --- -**`eql-bindings` says which match options an EQL type's filter is built under** (`stack-encrypt` feature). `Target::match_options()` returns `EqlMatch`'s options for a type with a match index (`TextMatch`) and `None` otherwise, so Stack Encrypt can answer `matches` through the type's query twin: it cuts the query's tokens for `verify` exactly as the stored filter was cut. The SQL surface and the TypeScript package are unchanged. +**`eql-bindings` says which match options an EQL type's filter is built under** (`targets` feature). `Target::match_options()` returns `EqlMatch`'s options for a type with a match index (`TextMatch`) and `None` otherwise, so Stack Encrypt can answer `matches` through the type's query twin: it cuts the query's tokens for `verify` exactly as the stored filter was cut. The SQL surface and the TypeScript package are unchanged. diff --git a/.changeset/eql-v4-bindings.md b/.changeset/eql-v4-bindings.md index a60b3e43f..88887e454 100644 --- a/.changeset/eql-v4-bindings.md +++ b/.changeset/eql-v4-bindings.md @@ -2,6 +2,6 @@ '@cipherstash/eql': minor --- -**The `eql-bindings` crate has an `eql_bindings::v4` module, with the `stack-encrypt` feature: payload types for the EQL v4 domains.** EQL v4 is the v3 SQL installed under a second name (ADR-0002), for values written by Stack Encrypt, so the module exists only with that feature and its types derive Stack Encrypt's traits without a further gate. The module is generated from the same catalog as `v3`, with `eql_v4_*` domain names and `"v": 4`. `SchemaVersion` is now generic over the version it admits: `v3::SchemaVersion` is `SchemaVersion<3>` and `v4::SchemaVersion` is `SchemaVersion<4>`, each refusing the other's payloads, and plain `SchemaVersion` still means v3. Only v4 types derive Stack Encrypt's `EncryptFrom` and `DecryptInto`, and the Stack Encrypt target table (now `eql_bindings::v4::targets`) names v4 domains. Besides `TextEq`, Stack Encrypt now produces the storage-only types `Text`, `Integer`, `Bigint`, `Boolean`, `Real` and `Double`, and `TextMatch` (3-grams, `k = 6`, a 2048-bit filter, pinned as `eql_bindings::encryption::EqlMatch`; text shorter than three characters is refused, stored or queried, and only `stored @@ query` matches until #1171 is fixed), from the Rust primitives `i32`, `i64`, `bool`, `f32` and `f64`; the Go SDK's `encrypt/eql` table marks them producible. Stack Encrypt also produces the number equality types `IntegerEq`, `BigintEq`, `RealEq` and `DoubleEq`, whose `=` compares as PostgreSQL compares the plain types: an integer has one term whatever its width, and for floats `-0.0 = 0.0` and `NaN = NaN` (ADR-0002's semantic baseline). Every type it cannot produce yet names the issue that tracks its blocker. The by-name target entry points (`eql_bindings::encryption::targets::encrypt`, `decrypt` and `query`) exchange the plaintext as Stack Encrypt's `dynamic::ValueFrame`, one value as its transport frame, rather than a value tree. +**The `eql-bindings` crate has an `eql_bindings::v4` module, with the `stack-encrypt` feature: payload types for the EQL v4 domains.** EQL v4 is the v3 SQL installed under a second name (ADR-0002), for values written by Stack Encrypt, so the module exists only with that feature and its types derive Stack Encrypt's traits without a further gate. The module is generated from the same catalog as `v3`, with `eql_v4_*` domain names and `"v": 4`. `SchemaVersion` is now generic over the version it admits: `v3::SchemaVersion` is `SchemaVersion<3>` and `v4::SchemaVersion` is `SchemaVersion<4>`, each refusing the other's payloads, and plain `SchemaVersion` still means v3. Only v4 types derive Stack Encrypt's `EncryptFrom` and `DecryptInto`, and the Stack Encrypt target table (now `eql_bindings::v4::targets`) names v4 domains. Besides `TextEq`, Stack Encrypt now produces the storage-only types `Text`, `Integer`, `Bigint`, `Boolean`, `Real` and `Double`, and `TextMatch` (3-grams, `k = 6`, a 2048-bit filter, pinned as `eql_bindings::encryption::EqlMatch`; text shorter than three characters is refused, stored or queried, and only `stored @@ query` matches until #1171 is fixed), from the Rust primitives `i32`, `i64`, `bool`, `f32` and `f64`; the Go SDK's `encrypt/eql` table marks them producible. Stack Encrypt also produces the number equality types `IntegerEq`, `BigintEq`, `RealEq` and `DoubleEq`, whose `=` compares as PostgreSQL compares the plain types: an integer has one term whatever its width, and for floats `-0.0 = 0.0` and `NaN = NaN` (ADR-0002's semantic baseline). Every type it cannot produce yet names the issue that tracks its blocker. The by-name target entry points (`eql_bindings::encryption::targets::encrypt`, `decrypt` and `query`) exchange the plaintext as Stack Encrypt's `dynamic::ValueFrame`, one value as its transport frame, rather than a value tree. They, and the generated `v4::targets` table, need the new `targets` feature, which turns on `stack-encrypt` and Stack Encrypt's `dynamic` feature. `stack-encrypt` alone no longer builds Stack Encrypt's `dynamic` layer, so a Rust program using the typed v4 types through `encrypt_as` doesn't compile it. Nothing a released version shipped has changed. The v3 types, the v3 SQL and the TypeScript package are as they were; the `stack-encrypt` derives on the v3 text types and the `v3::targets` module were added after the last release and are moved to v4 here, not removed from a release. diff --git a/languages/golang/encrypt/guest/Cargo.toml b/languages/golang/encrypt/guest/Cargo.toml index 69f72c4fd..08acd7086 100644 --- a/languages/golang/encrypt/guest/Cargo.toml +++ b/languages/golang/encrypt/guest/Cargo.toml @@ -58,7 +58,7 @@ stack-kms = { path = "../../../../packages/stack-kms", default-features = false # The EQL types, for the `eql` build only. By path, like every other # eql-bindings dependency in the repository: the SQL that stores a payload # and the Rust that emits it must agree (scripts/lint-no-eql-registry-pins.mjs). -eql-bindings = { path = "../../../../packages/eql/crates/eql-bindings", features = ["stack-encrypt"], optional = true } +eql-bindings = { path = "../../../../packages/eql/crates/eql-bindings", features = ["targets"], optional = true } zerokms-protocol = "=0.12.31" # The transport frame and ciphertext codec, shared with vitaminc's own diff --git a/packages/eql/crates/eql-bindings/CHANGELOG.md b/packages/eql/crates/eql-bindings/CHANGELOG.md index c10bfab21..199d7e626 100644 --- a/packages/eql/crates/eql-bindings/CHANGELOG.md +++ b/packages/eql/crates/eql-bindings/CHANGELOG.md @@ -13,17 +13,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Encrypt produces an EQL v4 value (ADR-0002), so the module is compiled with that feature alone, and its producible types derive `EncryptFrom` and `DecryptInto` with no attribute of their own gated on it. +- **A `targets` feature for the by-name dispatch.** `encryption::targets` and + `v4::targets`, which name an EQL type at run time and take a `ValueFrame`, + need `targets`, which turns on `stack-encrypt` and Stack Encrypt's + `dynamic` feature. `stack-encrypt` alone builds Stack Encrypt with no + default features and no `dynamic` layer, so a Rust program that encrypts + the v4 types through `encrypt_as` no longer compiles it. - **`IntegerEq`, `BigintEq`, `RealEq` and `DoubleEq` are producible** (`stack-encrypt` feature), from `i32`, `i64`, `f32` and `f64`. Their equality term follows ADR-0002's semantic baseline, so `=` compares as PostgreSQL's own integer and `float8 =` do: one term per integer value whatever its width, `-0.0 = 0.0` and `NaN = NaN`. The ciphertext keeps the exact value. -- **`Target::match_options`** (`stack-encrypt` feature): the options a type's +- **`Target::match_options`** (`targets` feature): the options a type's match index is built under, `EqlMatch`'s, for a type that carries one (`TextMatch`), and `None` otherwise. Stack Encrypt answers `matches` through the type with them, cutting the query's tokens for `verify` as the stored filter was cut. -- **EQL types as plan field targets, by name** (`stack-encrypt` feature). +- **EQL types as plan field targets, by name** (`targets` feature). `eql_bindings::encryption::targets` holds a catalog-generated table (`targets()`) of every EQL type a Stack Encrypt data plan may name as a field target — name, family, suffix, plaintext `ValueKind`, SQL domain, @@ -71,8 +77,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **The `stack-encrypt` feature builds on vitaminc ADR 0001.** `encryption::targets` takes `ValueFrame` and `ValueKind` from - `stack_encrypt::dynamic` (the feature turns on stack-encrypt's `dynamic` - feature) in place of `vitaminc-aead-value`, which is gone, and the + `stack_encrypt::dynamic` (the `targets` feature turns on stack-encrypt's + `dynamic` feature) in place of `vitaminc-aead-value`, which is gone, and the `Ciphertext` target takes any vitaminc `Encrypt` plaintext and opens into any `Decrypt + Send` type, as stack-encrypt's own targets do. diff --git a/packages/eql/crates/eql-bindings/Cargo.toml b/packages/eql/crates/eql-bindings/Cargo.toml index 27f33c0a2..eae736220 100644 --- a/packages/eql/crates/eql-bindings/Cargo.toml +++ b/packages/eql/crates/eql-bindings/Cargo.toml @@ -43,29 +43,42 @@ schemars = "1" # from the PACKAGED crate against the registry stack-encrypt instead: # `cargo publish --dry-run --all-features` in test-eql.yml at PR time, and # `publish_all_features` in ../../release-plz.toml at release time. -# `dynamic`: `ValueFrame` / `ValueKind`, the runtime plaintext a plan field -# target takes and the kind names a plan's `"type"` key spells -# (`encryption::targets`). -stack-encrypt = { path = "../../../stack-encrypt", version = "0.3.0", optional = true, default-features = false, features = ["dynamic"] } -# The plaintext a target reads out of its `Protected` runtime value is held -# in `Zeroizing` until it is sealed, so the copy is wiped like the original. +# No default features and no `dynamic`: the typed EQL v4 types need only +# Stack Encrypt's typed API. The `targets` feature turns `dynamic` on. +stack-encrypt = { path = "../../../stack-encrypt", version = "0.3.0", optional = true, default-features = false } +# `targets` only. The plaintext a target reads out of its `Protected` +# runtime value is held in `Zeroizing` until it is sealed, so the copy is +# wiped like the original. zeroize = { version = "1", optional = true } +# `targets` only: `TargetError`. thiserror = { version = "2", optional = true } base64 = { version = "0.22", optional = true } hex = { version = "0.4", optional = true } [features] default = [] +# The EQL v4 types as Stack Encrypt targets: `eql_bindings::v4`, with +# `EncryptFrom` / `DecryptInto` on every producible type, for a Rust program +# that encrypts through `encrypt_as`. No `dynamic` layer. stack-encrypt = [ "dep:stack-encrypt", - "dep:zeroize", - "dep:thiserror", "dep:base64", "dep:hex", ] +# The by-name dispatch a language binding needs (`encryption::targets`, +# `v4::targets`): an EQL type named at run time, a `ValueFrame` plaintext, +# and the target table a guest serializes. Turns on stack-encrypt's +# `dynamic` feature. The Go guest's `eql` build and the encryption tests +# enable it. +targets = [ + "stack-encrypt", + "stack-encrypt/dynamic", + "dep:zeroize", + "dep:thiserror", +] [package.metadata.docs.rs] -features = ["stack-encrypt"] +features = ["targets"] [dev-dependencies] # Parity oracle: tests/catalog_parity.rs asserts the v3 domain inventory diff --git a/packages/eql/crates/eql-bindings/README.md b/packages/eql/crates/eql-bindings/README.md index 94544fa15..179b34330 100644 --- a/packages/eql/crates/eql-bindings/README.md +++ b/packages/eql/crates/eql-bindings/README.md @@ -235,7 +235,7 @@ let opened: ValueFrame = targets::decrypt("TextEq", &cipher, &column, &stored)?. ``` The table and the dispatch are generated into `src/v4/targets.rs` beside -the v4 family modules (`mise run types:generate`), gated to the `stack-encrypt` +the v4 family modules (`mise run types:generate`), gated to the `targets` feature, and drift-gated by the same parity tests. The same generator writes the Go package `languages/golang/encrypt/eql` from the same rows (`eql-codegen go-eql`, also under `types:generate` / `types:check`). The @@ -248,6 +248,17 @@ when its generated struct carries the `stack-encrypt` derives plan; `targets::targets()` lists them, each with `producible` and, when it is `false`, the reason. +### Features + +- `stack-encrypt`: the EQL v4 types (`eql_bindings::v4`) with Stack + Encrypt's `EncryptFrom` and `DecryptInto`, for a Rust program that + encrypts through `encrypt_as`. It builds Stack Encrypt with no default + features and without its `dynamic` layer. +- `targets`: `stack-encrypt`, plus the by-name dispatch a language binding + needs (`encryption::targets`, `v4::targets`), where the EQL type is named at + run time and the plaintext is a `ValueFrame`. It turns on Stack Encrypt's + `dynamic` feature. The Go guest's `eql` build enables it. + ### Developing the `stack-encrypt` feature Stack Encrypt lives in this repository (`packages/stack-encrypt`), and the diff --git a/packages/eql/crates/eql-bindings/src/encryption.rs b/packages/eql/crates/eql-bindings/src/encryption.rs index 76c1a1f85..1c95e437a 100644 --- a/packages/eql/crates/eql-bindings/src/encryption.rs +++ b/packages/eql/crates/eql-bindings/src/encryption.rs @@ -45,9 +45,9 @@ //! credentials. The encryption and decryption calls are identical. //! //! A binding that holds no Rust type to name reaches the same plans by the -//! type's *name* through [`targets`]: the catalog-generated table of EQL -//! types a data plan may name as a field target, and `encrypt` / `decrypt` / -//! `query` dispatching on that name. +//! type's *name* through `targets`, with the `targets` feature: the +//! catalog-generated table of EQL types a data plan may name as a field +//! target, and `encrypt` / `decrypt` / `query` dispatching on that name. //! //! Use the same table and column identifier for writes and queries. Passing //! `column.into()` on decryption also checks that the stored identifier matches @@ -80,6 +80,10 @@ //! require::(); //! ``` +// The by-name dispatch over the EQL v4 types (`targets` feature). A plain +// comment: a doc comment here would resolve the module's own links from +// this scope. +#[cfg(feature = "targets")] pub mod targets; use base64::{engine::general_purpose::STANDARD, Engine as _}; diff --git a/packages/eql/crates/eql-bindings/src/lib.rs b/packages/eql/crates/eql-bindings/src/lib.rs index 5b1e7249e..fe529797d 100644 --- a/packages/eql/crates/eql-bindings/src/lib.rs +++ b/packages/eql/crates/eql-bindings/src/lib.rs @@ -33,8 +33,8 @@ doc = "See the [complete encryption example](encryption#example), including cipher setup, table/column identifiers, and decryption." )] #![cfg_attr( - feature = "stack-encrypt", - doc = "A binding that names an EQL type as a string reaches the same plans through [`encryption::targets`]: the catalog-generated table of types a data plan may name as a field target, and the by-name `encrypt` / `decrypt` / `query` dispatch." + feature = "targets", + doc = "A binding that names an EQL type as a string reaches the same plans through [`encryption::targets`] (`targets` feature): the catalog-generated table of types a data plan may name as a field target, and the by-name `encrypt` / `decrypt` / `query` dispatch." )] use schemars::JsonSchema; diff --git a/packages/eql/crates/eql-bindings/src/v4/mod.rs b/packages/eql/crates/eql-bindings/src/v4/mod.rs index 2714884b1..b795fb9fa 100644 --- a/packages/eql/crates/eql-bindings/src/v4/mod.rs +++ b/packages/eql/crates/eql-bindings/src/v4/mod.rs @@ -36,6 +36,8 @@ pub mod smallint; /// a field target (`TARGETS`) and the by-name dispatch that runs a /// producible type's own plan. The descriptor type, the errors and the /// public entry points are hand-written in [`crate::encryption::targets`]. +/// `targets` feature only. +#[cfg(feature = "targets")] pub mod targets; pub mod text; pub mod timestamp; diff --git a/packages/eql/crates/eql-codegen/src/bindings.rs b/packages/eql/crates/eql-codegen/src/bindings.rs index 25c93dd96..6219c1c64 100644 --- a/packages/eql/crates/eql-codegen/src/bindings.rs +++ b/packages/eql/crates/eql-codegen/src/bindings.rs @@ -1051,7 +1051,7 @@ fn render_bindings(dir: &Path) -> Vec<(PathBuf, String)> { /// Render the v4 bindings: one module per family with a scalar domain (a /// mixed family's scalar domains under its own name, since v4 has no /// hand-written SteVec module to sit beside), and the Stack Encrypt target -/// table and dispatch (`targets.rs`, under the `stack-encrypt` feature with +/// table and dispatch (`targets.rs`, declared under the `targets` feature in /// the hand-written `v4/mod.rs`). Stack Encrypt produces v4 only, so the /// target table lives here. fn render_v4_bindings(dir: &Path) -> Vec<(PathBuf, String)> { diff --git a/packages/eql/crates/eql-codegen/src/targets.rs b/packages/eql/crates/eql-codegen/src/targets.rs index 982905db1..99af51c0d 100644 --- a/packages/eql/crates/eql-codegen/src/targets.rs +++ b/packages/eql/crates/eql-codegen/src/targets.rs @@ -3,7 +3,7 @@ //! types a Stack Encrypt data plan may name as a field target, and the //! by-name dispatch that runs a named type's own Rust plan. Generated beside //! the v4 family modules by the same mechanism ([`crate::bindings`]), and -//! under the `stack-encrypt` feature with the rest of `v4`. +//! under the `targets` feature, inside `v4`. //! Stack Encrypt produces EQL v4 only (ADR-0002), so every target is an //! `eql_v4_*` domain. //! diff --git a/packages/eql/mise.toml b/packages/eql/mise.toml index ddf57ee62..6b22fdc32 100644 --- a/packages/eql/mise.toml +++ b/packages/eql/mise.toml @@ -310,7 +310,10 @@ run = """ # on purpose. `--locked`: this is a CHECK, and the EQL lockfile is the one the # lockstep bump moves (see "Working on EQL" in the root AGENTS.md). set -euo pipefail +# Both feature sets: the typed types alone, as a Rust program builds them, +# and with the by-name dispatch, as the Go guest does. cargo test --locked -p eql-bindings --features stack-encrypt +cargo test --locked -p eql-bindings --features targets cargo test --locked -p eql-encryption-tests cargo clippy --locked -p eql-bindings -p eql-encryption-tests --all-features --all-targets -- -D warnings """ @@ -342,13 +345,13 @@ done """ [tasks."check:encryption:wasi"] -description = "Compile eql-bindings with the stack-encrypt feature for wasm32-wasip1 (no HTTP), as the Go guest builds it" +description = "Compile eql-bindings with the targets feature for wasm32-wasip1 (no HTTP), as the Go guest builds it" dir = "{{config_root}}" run = """ #!/usr/bin/env bash set -euo pipefail rustup target add wasm32-wasip1 -cargo check --locked -p eql-bindings --features stack-encrypt --target wasm32-wasip1 +cargo check --locked -p eql-bindings --features targets --target wasm32-wasip1 """ [tasks."types:generate"] diff --git a/packages/eql/tests/encryption/Cargo.toml b/packages/eql/tests/encryption/Cargo.toml index 948c10906..bd5ec44ea 100644 --- a/packages/eql/tests/encryption/Cargo.toml +++ b/packages/eql/tests/encryption/Cargo.toml @@ -13,7 +13,7 @@ harness = false # Keep crypto/Postgres test dependencies out of eql-bindings' default test build. [dev-dependencies] -eql-bindings = { path = "../../crates/eql-bindings", features = ["stack-encrypt"] } +eql-bindings = { path = "../../crates/eql-bindings", features = ["targets"] } # The parity oracle for the generated target table (tests/targets.rs): the # table must name every stored catalog domain, in catalog order. eql-domains = { path = "../../crates/eql-domains" } From 52e6fd24cd91ef304b673603e6c0cb0edb42bcef Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sun, 18 Oct 2026 18:48:46 +1100 Subject: [PATCH 2/3] ci(eql-bindings): lint and wasm-check the stack-encrypt set on its own Before the targets split, stack-encrypt was the only feature, so clippy with -D warnings and the wasm32-wasip1 check covered the typed path. After it, both ran only with targets on, and the typed set was built only by a host cargo test without -D warnings. A lint that fires only without targets, or a wasm32 break in the typed path, would have passed CI. test:encryption now also runs clippy on eql-bindings with --features stack-encrypt alone, and check:encryption:wasi also checks that set for wasm32-wasip1. Both are clean. Refs #1252 --- packages/eql/mise.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/eql/mise.toml b/packages/eql/mise.toml index 6b22fdc32..98d1d10d4 100644 --- a/packages/eql/mise.toml +++ b/packages/eql/mise.toml @@ -316,6 +316,9 @@ cargo test --locked -p eql-bindings --features stack-encrypt cargo test --locked -p eql-bindings --features targets cargo test --locked -p eql-encryption-tests cargo clippy --locked -p eql-bindings -p eql-encryption-tests --all-features --all-targets -- -D warnings +# The typed set alone, linted on its own: a lint that fires only without +# `targets` (an import only the dispatch uses) is invisible to the line above. +cargo clippy --locked -p eql-bindings --features stack-encrypt --all-targets -- -D warnings """ [tasks."test:encryption:postgres"] @@ -345,12 +348,13 @@ done """ [tasks."check:encryption:wasi"] -description = "Compile eql-bindings with the targets feature for wasm32-wasip1 (no HTTP), as the Go guest builds it" +description = "Compile eql-bindings for wasm32-wasip1 (no HTTP): the typed stack-encrypt set alone, and with the targets feature as the Go guest builds it" dir = "{{config_root}}" run = """ #!/usr/bin/env bash set -euo pipefail rustup target add wasm32-wasip1 +cargo check --locked -p eql-bindings --features stack-encrypt --target wasm32-wasip1 cargo check --locked -p eql-bindings --features targets --target wasm32-wasip1 """ From e43dfaff9ca046e8565c07cbf729b50c7268fd4c Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sun, 18 Oct 2026 18:48:46 +1100 Subject: [PATCH 3/3] docs(eql-bindings): say the by-name dispatch needs the targets feature The encryption module docs lost their link to `targets` when the module went behind the feature, because a link to a missing module fails the doc build. Link it with cfg_attr when the feature is on, as lib.rs does, and keep the plain text otherwise. docs.rs builds with targets and the crate has no docsrs cfg, so no feature badge shows. Say in the targets module docs that it needs the feature. Refs #1252 --- packages/eql/crates/eql-bindings/src/encryption.rs | 12 ++++++++---- .../crates/eql-bindings/src/encryption/targets.rs | 4 ++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/eql/crates/eql-bindings/src/encryption.rs b/packages/eql/crates/eql-bindings/src/encryption.rs index 1c95e437a..f95172c1f 100644 --- a/packages/eql/crates/eql-bindings/src/encryption.rs +++ b/packages/eql/crates/eql-bindings/src/encryption.rs @@ -44,10 +44,14 @@ //! builder with `StackCipher::new().await?`, using your configured CipherStash //! credentials. The encryption and decryption calls are identical. //! -//! A binding that holds no Rust type to name reaches the same plans by the -//! type's *name* through `targets`, with the `targets` feature: the -//! catalog-generated table of EQL types a data plan may name as a field -//! target, and `encrypt` / `decrypt` / `query` dispatching on that name. +#![cfg_attr( + feature = "targets", + doc = "A binding that holds no Rust type to name reaches the same plans by the type's *name* through [`targets`] (`targets` feature): the catalog-generated table of EQL types a data plan may name as a field target, and `encrypt` / `decrypt` / `query` dispatching on that name." +)] +#![cfg_attr( + not(feature = "targets"), + doc = "A binding that holds no Rust type to name reaches the same plans by the type's *name* through `targets`, with the `targets` feature: the catalog-generated table of EQL types a data plan may name as a field target, and `encrypt` / `decrypt` / `query` dispatching on that name." +)] //! //! Use the same table and column identifier for writes and queries. Passing //! `column.into()` on decryption also checks that the stored identifier matches diff --git a/packages/eql/crates/eql-bindings/src/encryption/targets.rs b/packages/eql/crates/eql-bindings/src/encryption/targets.rs index 17488d732..61f71170f 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/targets.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/targets.rs @@ -13,6 +13,10 @@ //! [`crate::v4::targets`], beside the v4 family modules, so a type cannot be in the //! catalog and missing from the table. //! +//! This module needs the `targets` feature, which also turns on Stack +//! Encrypt's `dynamic` feature. The `stack-encrypt` feature alone gives only +//! the typed EQL v4 types. +//! //! The guest build that holds the EQL types links this module; the build //! without them has no `eql-bindings` and refuses a target name before //! reaching here.