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..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`]: 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 @@ -80,6 +84,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/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. 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..98d1d10d4 100644 --- a/packages/eql/mise.toml +++ b/packages/eql/mise.toml @@ -310,9 +310,15 @@ 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 +# 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"] @@ -342,13 +348,14 @@ 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 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 """ [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" }