diff --git a/.changeset/auth-emudhra-spki-blocklist.md b/.changeset/auth-emudhra-spki-blocklist.md deleted file mode 100644 index 35f81f7b6..000000000 --- a/.changeset/auth-emudhra-spki-blocklist.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"@cipherstash/auth": patch ---- - -The native binaries now refuse a TLS certificate chain that carries one of the -eight eMudhra intermediate CA keys reported compromised on 2026-10-17, including -their Sectigo cross-signs. On Linux the platform verifier checks no revocation, -so these intermediates were still trusted through the distro CA bundle. Such a -chain now fails as a revoked certificate. The package's API is unchanged, and -the WASM build is unaffected because the runtime's `fetch` handles TLS there. diff --git a/.changeset/auth-error-codes-and-help.md b/.changeset/auth-error-codes-and-help.md deleted file mode 100644 index 3fdac5dda..000000000 --- a/.changeset/auth-error-codes-and-help.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@cipherstash/auth": patch ---- - -Auth failures carry more help, and their messages never quote a credential or another library's text. A failure's `type` (`NOT_AUTHENTICATED`, `INVALID_CRN`, ...) is unchanged. - -- `REQUEST_ERROR`'s message no longer repeats the transport's own error, which can carry a URL with its query string. It gains `help` saying what to check. -- `INVALID_TOKEN` for a token whose claims do not decode no longer quotes the decoder's message, which could carry a byte or a claim of the token. -- A failed device binding reports ZeroKMS's status, not its response body. -- `SERVER_ERROR` for a refused token exchange names the HTTP status and the auth server's `error_description`, not the response body, which from the edge in front of it is an HTML page and can echo the access key. A body that is not JSON is reported by where it broke, not by the parser's message. -- A profile file that is not valid JSON is reported by error kind, line and column, not by the parser's message, which could quote the file. -- `INVALID_GRANT`, `INVALID_WORKSPACE_ID` and `ALREADY_CONSUMED` gain `help`, and `NOT_AUTHENTICATED`'s help names `stash auth login`. -- A `STORE_ERROR` carries the help of the profile failure underneath it, such as logging in again when the profile file is missing. diff --git a/.changeset/auth-rustls-0-23-45.md b/.changeset/auth-rustls-0-23-45.md deleted file mode 100644 index 02ee2d408..000000000 --- a/.changeset/auth-rustls-0-23-45.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@cipherstash/auth": patch ---- - -The native binaries now use rustls 0.23.45, which fixes GHSA-2mjx-qc3c-rqvc -(affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged. diff --git a/.changeset/cancelled-auth-refresh.md b/.changeset/cancelled-auth-refresh.md deleted file mode 100644 index ccd0e5d70..000000000 --- a/.changeset/cancelled-auth-refresh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@cipherstash/auth': patch ---- - -Keep native token refreshes running when their caller is cancelled, so rotated device-session credentials are saved and subsequent requests can still authenticate. Close a refresh-waiter notification race, and give the caller that started a refresh the new token rather than the expiring one it replaced. diff --git a/.changeset/column-migration-api.md b/.changeset/column-migration-api.md deleted file mode 100644 index 4ca556384..000000000 --- a/.changeset/column-migration-api.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@cipherstash/migrate': minor -'stash': patch ---- - -`@cipherstash/migrate` adds `ColumnMigration`, one place for the rules of a column migration in either direction: which twin column it pairs with (the flag, the recorded pairing, the manifest, then the naming convention), its phase, whether a forced pass is owed, and which open migration in the other direction blocks it. -`ColumnMigration.load()` reads it from `cipherstash.cs_migrations`; `confirmDualWrites()`, `recordDropGenerated()` and `recordSourceRemoved()` record events, refusing those the rules forbid with a `ColumnMigrationError`. -The pure rules are exported as `deriveColumnMigration`, `openCounterpart`, `planPassFor`, `passProgress`, `deriveTwinName` and `manifestTwin`. - -`runBackfill` and `runDecryptBackfill` now apply the rules themselves rather than leaving them to the caller. -Every event records whether its pass was forced. -An interrupted forced pass is finished as one by the next run, with or without `force`, and the result says why in `forcedBy`. -A forward backfill resumes after a failed run from its last committed position, counts its total once when it resumes a forced pass, and refuses to start while a reverse migration of the same columns is open. -`runBackfill` now throws a `ColumnMigrationError` with code `twin-mismatch` when `encryptedColumn` differs from the encrypted column already recorded for that column, rather than continuing into a different one. -A reverse backfill over a twin that already holds values is forced unless the caller confirmed dual-writes with the `plaintext-current` waiver. -Error events store the error's class (`errorName`) and safe details instead of its message, which could carry row data. - -`stash decrypt plan`, `status`, `backfill` and `drop` resolve every column through `ColumnMigration`, so they always agree on the twin and the phase. -`decrypt status` flags `forward-open`, `twin-has-values` and `forced-pass-owed`. -`decrypt backfill --confirm-plaintext-current` is accepted on a later run as long as no forced pass has started, and refused while a started forced pass is owed. diff --git a/.changeset/eql-3-1-0-migration.md b/.changeset/eql-3-1-0-migration.md deleted file mode 100644 index 39e2388a0..000000000 --- a/.changeset/eql-3-1-0-migration.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -'@cipherstash/stack-prisma': minor ---- - -Move the bundled EQL v3 migrations to **eql-3.1.0**. Its install SQL changes -the version stamp (`eql_v3.version()` returns `'3.1.0'` and the `eql_v3` -schema comment reads `'3.1.0'`) and the ORE block comparator behind every -ordered ORE operator and the ORE operator class. The comparator now runs a -constant number of operations whatever two values share, closing a timing -leak. Ordering results are unchanged for well-formed values, but a block-ORE -text value with a malformed or mismatched-width term now raises on every -comparison against it. Check your `_ore` text columns before migrating; see -[U-001](../../../../packages/eql/docs/upgrading/v3.1.md#u-001-block-ore-text-comparisons-check-every-term). -No domain, wire format or index term changes. - -Two artefacts carry the new bundle: - -- A new upgrade edge, `20261005T0000_upgrade_eql_v3_3_1_0`, carrying the - invariant `cipherstash:upgrade-eql-v3-bundle-3.1.0-v1`. Existing databases - re-install through it on the next `prisma-next migration plan` followed by - `prisma-next migrate`. **`migrate` alone is not enough**: only - `migration plan` copies the new directory into your repo. -- The baseline install migration `20260601T0100_install_eql_v3_bundle`, which - now bakes 3.1.0 and gains a sixth op, a no-SQL carrier for the new - invariant, so fresh databases land on 3.1.0 from the single all-additive - genesis edge and `db init` keeps working. - -**A 1.2.x database walks one edge. A 1.0.0 or 1.1.x database walks three.** -1.2.x shipped eql-3.0.6, so `migrate` walks only the 3.1.0 edge. 1.0.0 and -1.1.x shipped eql-3.0.4, so `migrate` walks the 3.0.5, 3.0.6 and 3.1.0 edges in -one transaction, re-installing the ~2.6 MB bundle three times. As with every EQL -upgrade, each re-install opens with `DROP SCHEMA IF EXISTS eql_v3 CASCADE`: -re-run your grant script, and recreate functional indexes and anything else -depending on `eql_v3`. - -**Action required, including on 1.2.x: the baseline changed again.** 1.2.0 -and 1.2.1 shipped the baseline at eql-3.0.6, and this release changes those -published bytes. 1.0.0 to 1.1.1 shipped it at eql-3.0.4. If your project has a -`migrations/cipherstash/` directory generated against any earlier version, -delete it and re-run `prisma-next migration plan`: - -```bash -rm -rf migrations/cipherstash -npx prisma-next migration plan -``` - -Your database keeps its markers, so already-applied invariants are not re-run. -If you skip the delete, a fresh `db init` from a 1.2.x directory refuses with -`Operation cipherstash.upgrade-eql-v3-bundle-3.1.0 has class "data" which is -not allowed by policy.` From a 1.0.0 or 1.1.x directory it refuses on the 3.0.5 -edge instead. See "Upgrading from 1.0.0, 1.1.x or 1.2.x" in the package README. - -**Why the baseline was re-emitted rather than given a second genesis edge.** -The 3.0.5 and 3.0.6 entries said this trade must be re-argued on adoption -numbers at each bump. Unlike the 3.0.6 re-emit, this one changes a published -baseline. The team chose it on these numbers: `@cipherstash/stack-prisma` had -481 npm downloads in the 30 days to 3 October 2026, and 384 in the last 7 of -those days, 305 of them on the 1.2.0 release day. The append-only alternative, -a second `from: null` genesis edge, would add another permanent ~2.6 MB copy of -the bundle. That choice was made when 3.1.0 changed only the version stamp; the -comparator change came after it and does not alter the trade, since every -database reaches the same bundle either way. diff --git a/.changeset/eql-ore-block-256-constant-op-count.md b/.changeset/eql-ore-block-256-constant-op-count.md deleted file mode 100644 index ef0ac0a27..000000000 --- a/.changeset/eql-ore-block-256-constant-op-count.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -'@cipherstash/eql': minor ---- - -Security: the ORE block comparator (`eql_v3_internal.compare_ore_block_256_term` -and `compare_ore_block_256_terms`, behind every ordered ORE operator and the ORE -operator class) now runs a constant number of operations for operands of the -same shape (term count, term width, NULL positions), whether and wherever they -differ. Previously its run time depended on where two values first differ, so a -party able to time queries could learn how long a prefix their query value -shares with stored values. For text the effect was large: a comparison took -about 11 µs when the first term differs and 47 µs when only the sixth does. -Results are unchanged. Single-term comparisons cost about 6% more when the -terms differ and about 36% more (roughly 1 µs) when they are equal, since equal -terms now run the hash too; text comparisons now take about 18 µs whatever the -operands. Multi-term (text) values now check every paired term, so a -malformed or mismatched-width term after the first difference, or one paired -with a NULL element, raises instead of being skipped; see -[U-001](../../docs/upgrading/v3.1.md#u-001-block-ore-text-comparisons-check-every-term) -for how to find such rows before upgrading. A timing harness is in -`packages/eql/tests/timing/ore_block_256/`. diff --git a/.changeset/eql-plan-field-targets.md b/.changeset/eql-plan-field-targets.md deleted file mode 100644 index e1fd16c14..000000000 --- a/.changeset/eql-plan-field-targets.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@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. `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-text-equality-transcoding.md b/.changeset/eql-text-equality-transcoding.md deleted file mode 100644 index 969aa9653..000000000 --- a/.changeset/eql-text-equality-transcoding.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@cipherstash/eql': minor ---- - -**Rust `TextEq` and `TextEqQuery` support Stack Encrypt's target-directed API behind the `stack-encrypt` feature.** The stored table and column identifier supplies the encryption context; native ciphertext and equality terms are transcoded into EQL payloads while Vitamin C owns plaintext encoding. This is a new producer profile with exact string equality, independent of existing cipherstash-client ciphertext and terms. Query operands carry no recoverable ciphertext. diff --git a/.changeset/impact-gate-pip-supply-chain.md b/.changeset/impact-gate-pip-supply-chain.md deleted file mode 100644 index d526908c0..000000000 --- a/.changeset/impact-gate-pip-supply-chain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'stash': patch ---- - -The supply-chain skill names `pip` among the Dependabot ecosystems the repository monitors, and says that a pip requirements file installed in CI should pin every package with a hash and be installed with `pip install --require-hashes --no-deps`. diff --git a/.changeset/managed-platforms-skill-frontmatter.md b/.changeset/managed-platforms-skill-frontmatter.md deleted file mode 100644 index a9b6077a0..000000000 --- a/.changeset/managed-platforms-skill-frontmatter.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'stash': patch ---- - -Fix invalid YAML in `skills/stash-managed-platforms/SKILL.md` frontmatter: the -unquoted `description` contained a colon-space, so any YAML parser rejected the -file (this broke cipherstash.com deploys, which mirror these skills at build -time). Rewritten as a folded block scalar; the parsed description is -byte-for-byte identical. diff --git a/.changeset/musl-build-image-dependabot.md b/.changeset/musl-build-image-dependabot.md deleted file mode 100644 index 5045b81fe..000000000 --- a/.changeset/musl-build-image-dependabot.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'stash': patch ---- - -The supply-chain skill names every Dependabot ecosystem the repository monitors, including the Go module and the Docker image the musl binaries are built in. diff --git a/.changeset/oidc-federation-per-user-docs.md b/.changeset/oidc-federation-per-user-docs.md deleted file mode 100644 index 1e280f903..000000000 --- a/.changeset/oidc-federation-per-user-docs.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@cipherstash/stack": patch -"stash": patch ---- - -Document how `OidcFederationStrategy` serves many users through one client: the -`getJwt` callback runs on every operation, in the caller's async context, and -must return the JWT of the user behind the current request, and each user's -JWT is exchanged for that user's own token (stack-auth's fix for -cipherstash/stack#1045). One caveat is documented for the `@cipherstash/stack` -client on the native entry, which calls the strategy from a background task so -a request read from `AsyncLocalStorage` is not visible inside `getJwt` there -yet: capture the request in the closure or build the client per request until -cipherstash/stack#1065 lands. The docs and the `stash-auth`, `stash-encryption`, -`stash-supabase` and `stash-edge` skills say so, and say that on earlier -`@cipherstash/auth` releases a strategy caches one token for everyone, so a -client there must serve one user. diff --git a/.changeset/oidc-federation-per-user-tokens.md b/.changeset/oidc-federation-per-user-tokens.md deleted file mode 100644 index cbaafed99..000000000 --- a/.changeset/oidc-federation-per-user-tokens.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -"@cipherstash/auth": minor ---- - -`OidcFederationStrategy` now keeps one CTS token per distinct provider JWT -instead of one token for everyone. `getJwt` is called on every `getToken()` -and must return the JWT of the user the current request is for; a JWT is -exchanged once and its CTS token reused while that token is valid *and* the -JWT's entry is still in the bounded least-recently-used cache (eviction before -expiry, or `cacheCapacity: 0`, means another exchange; see below). Previously -the strategy cached the first user's token and handed it to -every caller until it expired, so on a server serving many users through one -client a value one user encrypted under a lock context could be bound to -another user's identity (cipherstash/stack#1045). - -`getToken()` now calls `getJwt` in the caller's own async context and hands the -JWT to a new `getTokenForJwt(jwt)` method, on both the Node and `wasm-inline` -entries. Previously the Node binding ran `getJwt` through a napi threadsafe -function, in the async context of `create()`, so a callback that read the -request from `AsyncLocalStorage` (Clerk's `auth()`, Next.js `headers()`) saw -no request from a module-level strategy, or the creating request from one -created inside a request. One long-lived strategy now serves every user from -such a callback. `getTokenForJwt` is also public, for a caller that already -holds the user's JWT; it shares the strategy's cache with `getToken()`. - -A token persisted through `createWithStore` (for example a cookie) is now -served only to the JWT it was federated from, so a store shared across users, -or a cookie left over from a previous sign-in, is a cache miss rather than -another user's token. The store is still one slot, so a store shared by -several users caches only the most recent of them; give each user their own -(a per-browser cookie already is). The stored JSON carries a new -`federated_from` field; tokens stored by earlier releases have none and are -re-federated once. - -Because the cache is keyed on the whole JWT, an identity provider that rotates -JWTs faster than the CTS token lifetime (about 15 minutes) re-federates on each -rotation, where earlier releases reused the stored token. - -The cache holds 1024 JWTs unless told otherwise. `create` and `createWithStore` -take an optional `cacheCapacity` after `baseUrl` (the `cacheCapacity` option on -`@cipherstash/auth/wasm-inline`); when the cache is full the least recently -used JWT's token is dropped, that user is exchanged again on their next call, -and the eviction is logged at `debug`. `0` caches nothing. Existing calls are -unchanged. diff --git a/.changeset/prisma-skill-eql-310.md b/.changeset/prisma-skill-eql-310.md deleted file mode 100644 index 92aacd896..000000000 --- a/.changeset/prisma-skill-eql-310.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'stash': patch ---- - -`skills/stash-prisma`: the stale-`migrations/cipherstash/` table now describes -eql-3.1.0. The baseline bakes 3.1.0, 1.2.x shipped 3.0.6, and the table names -the `db init` refusal a stale 1.2.x directory produces. diff --git a/.changeset/protect-ffi-rustls-0-23-45.md b/.changeset/protect-ffi-rustls-0-23-45.md deleted file mode 100644 index 9bcf792bd..000000000 --- a/.changeset/protect-ffi-rustls-0-23-45.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -"@cipherstash/protect-ffi": patch -"@cipherstash/protect-ffi-darwin-arm64": patch -"@cipherstash/protect-ffi-darwin-x64": patch -"@cipherstash/protect-ffi-linux-arm64-gnu": patch -"@cipherstash/protect-ffi-linux-x64-gnu": patch -"@cipherstash/protect-ffi-linux-x64-musl": patch -"@cipherstash/protect-ffi-win32-x64-msvc": patch ---- - -The native binaries now use rustls 0.23.45, which fixes GHSA-2mjx-qc3c-rqvc -(affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged. diff --git a/.changeset/readme-unwrap-oidc-federation-result.md b/.changeset/readme-unwrap-oidc-federation-result.md deleted file mode 100644 index d8eccf2d0..000000000 --- a/.changeset/readme-unwrap-oidc-federation-result.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@cipherstash/stack": patch ---- - -Fix the README's OIDC federation example, which passed the `Result` from `OidcFederationStrategy.create()` straight to `config.authStrategy` and failed in `Encryption()` with `opts.strategy.getToken is missing`. -It now unwraps the `Result` and passes `strategy.data`, and so does the `LockContext` JSDoc example. diff --git a/.changeset/source-links-languages-typescript.md b/.changeset/source-links-languages-typescript.md deleted file mode 100644 index 1f29f7dc7..000000000 --- a/.changeset/source-links-languages-typescript.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@cipherstash/migrate": patch -"@cipherstash/nextjs": patch -"@cipherstash/protect-ffi": patch -"@cipherstash/protect-ffi-darwin-arm64": patch -"@cipherstash/protect-ffi-darwin-x64": patch -"@cipherstash/protect-ffi-linux-arm64-gnu": patch -"@cipherstash/protect-ffi-linux-x64-gnu": patch -"@cipherstash/protect-ffi-linux-x64-musl": patch -"@cipherstash/protect-ffi-win32-x64-msvc": patch ---- - -The package's source links now point at its folder under -`languages/typescript/` in cipherstash/stack. The previous release linked to -its old folder under `packages/`, which no longer exists, so the README's -source links and the npm page's repository link returned 404. diff --git a/.changeset/stack-encrypt-json-null-result.md b/.changeset/stack-encrypt-json-null-result.md deleted file mode 100644 index 6111f982a..000000000 --- a/.changeset/stack-encrypt-json-null-result.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@cipherstash/stack': minor -'@cipherstash/stack-supabase': patch -'stash': patch ---- - -`encrypt` on a `types.Json` column now types its result as `Encrypted | null` when the plaintext may be `null`. A `null` document has always resolved to `{ data: null }` at runtime (it is stored as SQL NULL, not encrypted), but the result was typed `Encrypted`, so `result.data.c` compiled and then threw. `encrypt` is now generic over the plaintext's type: a literal `null`, or a value typed `JsonDocument`, gives `data: Encrypted | null`, through `.withLockContext()` and `.audit()` too. A value TypeScript knows is non-null, and every scalar column, still gives `data: Encrypted`. No runtime change. If your code reads `result.data` after encrypting a Json value that may be `null`, it must now check for `null` first. `EncryptOperation` and `EncryptOperationWithLockContext` take an optional result type parameter (default `Encrypted`), and the `EncryptResult
` helper type is exported from `@cipherstash/stack/encryption`. - -`@cipherstash/stack-supabase`: the per-term query-encryption fallback (used when the client has no `bulkEncrypt`) now rejects a `null` envelope instead of sending the string `"null"` as a filter operand, matching the bulk path. - -`stash`: the `stash-encryption` skill now says what `encrypt` returns for a `null` Json document. diff --git a/.changeset/stack-encrypt-ops-json-plaintext.md b/.changeset/stack-encrypt-ops-json-plaintext.md deleted file mode 100644 index fb7b1952c..000000000 --- a/.changeset/stack-encrypt-ops-json-plaintext.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@cipherstash/stack': patch ---- - -Internal type cleanup of the encryption client (no runtime change). The exported operation classes `EncryptOperation`, `EncryptQueryOperation`, `BatchEncryptQueryOperation` and `BulkEncryptOperation` now declare that they accept a `types.Json` document as plaintext, which they always did at runtime; their `getOperation()` return types widen to match. The `EncryptionClient` interface changes only in what `encrypt` returns for a `types.Json` column; see the `EncryptResult` changeset. diff --git a/.changeset/stash-decrypt-reverse-migration.md b/.changeset/stash-decrypt-reverse-migration.md deleted file mode 100644 index 3bff0401a..000000000 --- a/.changeset/stash-decrypt-reverse-migration.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'stash': minor -'@cipherstash/migrate': minor ---- - -`stash decrypt status|plan|backfill|drop` moves encrypted columns back to plaintext — the encrypt lifecycle run in reverse, with the same safety rails. `decrypt plan` adds a nullable plaintext twin per EQL v3 column with its type mapped back from the domain (`text*` → `text`, `timestamp*` → `timestamptz`, `json*` → `jsonb`, …): Drizzle schema lines for `drizzle-kit generate`, or a reviewed SQL migration elsewhere. `decrypt backfill` decrypts in resumable, checkpointed chunks, one `bulkDecrypt` (one ZeroKMS request) per chunk, default 500 rows. It refuses a plaintext column that a forward encryption started from and never dropped: only one migration may be open on a pair of columns, so finish or abandon the forward one first. When that column was already removed outside stash, `decrypt plan` finds it missing and records the forward migration as dropped (marked as observed), so the reverse migration can start. Its first run also refuses a plaintext column that already holds values, because those values may be stale and the backfill only fills NULLs: pass `--force` to overwrite them from the ciphertext or `--confirm-plaintext-current` to keep them. Once `--force` is chosen, or any forced run starts, every later run is forced until a forced pass completes, so an interrupted overwrite is always finished. The application keeps writing both columns until the drop migration is applied. Values bound to an end-user lock context cannot be decrypted with workspace credentials; they are skipped, counted, left NULL and reported, and `decrypt drop` refuses while any remain. `decrypt drop` re-verifies coverage at apply time, drops the indexes built on the column's EQL terms, drops the column, and with `--uninstall-eql` removes EQL v3 when it was the last encrypted column in the database. - -`@cipherstash/migrate`: `runBackfill` now runs on a shared chunked engine (`runChunkedMigration`), which also drives the new `runDecryptBackfill`. Forward behaviour and recorded event details are unchanged. Adds `runDecryptBackfill` (resumes after a failed run; `PlaintextWriteError` keeps decrypted values out of write errors; chunk size capped at `MAX_DECRYPT_CHUNK_SIZE`), `columnHasValues`, `countUndecrypted`, `fetchUndecryptedPage`, `plaintextTypeForEqlDomain`, `columnSqlType`, `listColumnIndexes`, `primaryKeyColumns`, `countEqlColumnsInDatabase`, `removeManifestColumn`, and the reverse event/phase vocabulary (`decrypt_started`, `decrypt_checkpoint`, `decrypted`, `encrypted_dropped`; `decrypt-dual-writing`, `decrypting`, `decrypted`, `encrypted-dropped`), recorded against the encrypted column. `MigrationPhase` and `MigrationEvent` gain those members, so an exhaustive `switch` over them needs the new cases. diff --git a/.changeset/wizard-mcp-sdk-security-pin.md b/.changeset/wizard-mcp-sdk-security-pin.md deleted file mode 100644 index 96eba07d6..000000000 --- a/.changeset/wizard-mcp-sdk-security-pin.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@cipherstash/wizard": patch ---- - -Add `@modelcontextprotocol/sdk` `^1.32.0` as a direct dependency so the -auto-installed peer of `@anthropic-ai/claude-agent-sdk` resolves to a release -patched against GHSA-6qxp-vccf-f47h / CVE-2026-104850, instead of the vulnerable -1.29.0 previously locked. The wizard never imports the MCP SDK directly — this -is a peer-resolution pin only; no API change. diff --git a/e2e/CHANGELOG.md b/e2e/CHANGELOG.md index 6dc766ccd..123d050f7 100644 --- a/e2e/CHANGELOG.md +++ b/e2e/CHANGELOG.md @@ -1,5 +1,24 @@ # @cipherstash/e2e +## 0.0.8 + +### Patch Changes + +- Updated dependencies [bde5b73] +- Updated dependencies [587d06e] +- Updated dependencies [bd7e55f] +- Updated dependencies [e5a3575] +- Updated dependencies [fd0ee91] +- Updated dependencies [ae63dcd] +- Updated dependencies [4b14ee6] +- Updated dependencies [bbb1bfa] +- Updated dependencies [20ccab3] +- Updated dependencies [bca02b6] +- Updated dependencies [472cbf0] + - stash@1.3.0 + - @cipherstash/stack@1.3.0 + - @cipherstash/wizard@1.3.0 + ## 0.0.7 ### Patch Changes diff --git a/e2e/package.json b/e2e/package.json index 0e2ef5b9f..4703a4018 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/e2e", - "version": "0.0.7", + "version": "0.0.8", "private": true, "description": "End-to-end tests that exercise built CipherStash binaries and cross-package behaviour.", "type": "module", diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index 88e3723f7..e20b52db6 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -699,7 +699,7 @@ dependencies = [ [[package]] name = "eql-bindings" -version = "3.0.6" +version = "3.1.0" dependencies = [ "base64", "hex", diff --git a/languages/typescript/examples/basic/CHANGELOG.md b/languages/typescript/examples/basic/CHANGELOG.md index d1a153dbd..ca8de7560 100644 --- a/languages/typescript/examples/basic/CHANGELOG.md +++ b/languages/typescript/examples/basic/CHANGELOG.md @@ -1,5 +1,16 @@ # @cipherstash/basic-example +## 1.2.19 + +### Patch Changes + +- Updated dependencies [fd0ee91] +- Updated dependencies [4b14ee6] +- Updated dependencies [bbb1bfa] +- Updated dependencies [20ccab3] + - @cipherstash/stack@1.3.0 + - @cipherstash/stack-drizzle@1.3.0 + ## 1.2.18 ### Patch Changes diff --git a/languages/typescript/examples/basic/package.json b/languages/typescript/examples/basic/package.json index bc17a717a..bcccadcb1 100644 --- a/languages/typescript/examples/basic/package.json +++ b/languages/typescript/examples/basic/package.json @@ -1,7 +1,7 @@ { "name": "@cipherstash/basic-example", "private": true, - "version": "1.2.18", + "version": "1.2.19", "type": "module", "scripts": { "start": "tsx index.ts", diff --git a/languages/typescript/examples/prisma/CHANGELOG.md b/languages/typescript/examples/prisma/CHANGELOG.md index ff21e3cc6..86d991c9a 100644 --- a/languages/typescript/examples/prisma/CHANGELOG.md +++ b/languages/typescript/examples/prisma/CHANGELOG.md @@ -1,5 +1,17 @@ # @cipherstash/prisma-next-example +## 0.1.5 + +### Patch Changes + +- Updated dependencies [ae63dcd] +- Updated dependencies [fd0ee91] +- Updated dependencies [4b14ee6] +- Updated dependencies [bbb1bfa] +- Updated dependencies [20ccab3] + - @cipherstash/stack-prisma@1.3.0 + - @cipherstash/stack@1.3.0 + ## 0.1.4 ### Patch Changes diff --git a/languages/typescript/examples/prisma/package.json b/languages/typescript/examples/prisma/package.json index b274ab4f0..c09145086 100644 --- a/languages/typescript/examples/prisma/package.json +++ b/languages/typescript/examples/prisma/package.json @@ -1,7 +1,7 @@ { "name": "@cipherstash/prisma-example", "private": true, - "version": "0.1.4", + "version": "0.1.5", "description": "End-to-end example of @cipherstash/stack-prisma: searchable application-layer encryption for Postgres with Prisma Next, using @cipherstash/stack as the SDK.", "type": "module", "scripts": { diff --git a/languages/typescript/packages/auth/CHANGELOG.md b/languages/typescript/packages/auth/CHANGELOG.md index 4d4f3c134..cacc42b3a 100644 --- a/languages/typescript/packages/auth/CHANGELOG.md +++ b/languages/typescript/packages/auth/CHANGELOG.md @@ -1,5 +1,78 @@ # Changelog +## 1.0.0 + +### Minor Changes + +- fd0ee91: `OidcFederationStrategy` now keeps one CTS token per distinct provider JWT + instead of one token for everyone. `getJwt` is called on every `getToken()` + and must return the JWT of the user the current request is for; a JWT is + exchanged once and its CTS token reused while that token is valid _and_ the + JWT's entry is still in the bounded least-recently-used cache (eviction before + expiry, or `cacheCapacity: 0`, means another exchange; see below). Previously + the strategy cached the first user's token and handed it to + every caller until it expired, so on a server serving many users through one + client a value one user encrypted under a lock context could be bound to + another user's identity (cipherstash/stack#1045). + + `getToken()` now calls `getJwt` in the caller's own async context and hands the + JWT to a new `getTokenForJwt(jwt)` method, on both the Node and `wasm-inline` + entries. Previously the Node binding ran `getJwt` through a napi threadsafe + function, in the async context of `create()`, so a callback that read the + request from `AsyncLocalStorage` (Clerk's `auth()`, Next.js `headers()`) saw + no request from a module-level strategy, or the creating request from one + created inside a request. One long-lived strategy now serves every user from + such a callback. `getTokenForJwt` is also public, for a caller that already + holds the user's JWT; it shares the strategy's cache with `getToken()`. + + A token persisted through `createWithStore` (for example a cookie) is now + served only to the JWT it was federated from, so a store shared across users, + or a cookie left over from a previous sign-in, is a cache miss rather than + another user's token. The store is still one slot, so a store shared by + several users caches only the most recent of them; give each user their own + (a per-browser cookie already is). The stored JSON carries a new + `federated_from` field; tokens stored by earlier releases have none and are + re-federated once. + + Because the cache is keyed on the whole JWT, an identity provider that rotates + JWTs faster than the CTS token lifetime (about 15 minutes) re-federates on each + rotation, where earlier releases reused the stored token. + + The cache holds 1024 JWTs unless told otherwise. `create` and `createWithStore` + take an optional `cacheCapacity` after `baseUrl` (the `cacheCapacity` option on + `@cipherstash/auth/wasm-inline`); when the cache is full the least recently + used JWT's token is dropped, that user is exchanged again on their next call, + and the eviction is logged at `debug`. `0` caches nothing. Existing calls are + unchanged. + +### Patch Changes + +- a1765f1: The native binaries now refuse a TLS certificate chain that carries one of the + eight eMudhra intermediate CA keys reported compromised on 2026-10-17, including + their Sectigo cross-signs. On Linux the platform verifier checks no revocation, + so these intermediates were still trusted through the distro CA bundle. Such a + chain now fails as a revoked certificate. The package's API is unchanged, and + the WASM build is unaffected because the runtime's `fetch` handles TLS there. +- fb75f40: Auth failures carry more help, and their messages never quote a credential or another library's text. A failure's `type` (`NOT_AUTHENTICATED`, `INVALID_CRN`, ...) is unchanged. + + - `REQUEST_ERROR`'s message no longer repeats the transport's own error, which can carry a URL with its query string. It gains `help` saying what to check. + - `INVALID_TOKEN` for a token whose claims do not decode no longer quotes the decoder's message, which could carry a byte or a claim of the token. + - A failed device binding reports ZeroKMS's status, not its response body. + - `SERVER_ERROR` for a refused token exchange names the HTTP status and the auth server's `error_description`, not the response body, which from the edge in front of it is an HTML page and can echo the access key. A body that is not JSON is reported by where it broke, not by the parser's message. + - A profile file that is not valid JSON is reported by error kind, line and column, not by the parser's message, which could quote the file. + - `INVALID_GRANT`, `INVALID_WORKSPACE_ID` and `ALREADY_CONSUMED` gain `help`, and `NOT_AUTHENTICATED`'s help names `stash auth login`. + - A `STORE_ERROR` carries the help of the profile failure underneath it, such as logging in again when the profile file is missing. + +- 457b28e: The native binaries now use rustls 0.23.45, which fixes GHSA-2mjx-qc3c-rqvc + (affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged. +- b480735: Keep native token refreshes running when their caller is cancelled, so rotated device-session credentials are saved and subsequent requests can still authenticate. Close a refresh-waiter notification race, and give the caller that started a refresh the new token rather than the expiring one it replaced. + - @cipherstash/auth-darwin-x64@1.0.0 + - @cipherstash/auth-darwin-arm64@1.0.0 + - @cipherstash/auth-win32-x64-msvc@1.0.0 + - @cipherstash/auth-linux-x64-gnu@1.0.0 + - @cipherstash/auth-linux-arm64-gnu@1.0.0 + - @cipherstash/auth-linux-x64-musl@1.0.0 + ## 0.44.1 ### Patch Changes diff --git a/languages/typescript/packages/auth/package.json b/languages/typescript/packages/auth/package.json index 3863cc6b3..e85a7d670 100644 --- a/languages/typescript/packages/auth/package.json +++ b/languages/typescript/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/auth", - "version": "0.44.1", + "version": "1.0.0", "license": "SEE LICENSE IN LICENSE", "repository": { "type": "git", diff --git a/languages/typescript/packages/auth/platforms/darwin-arm64/CHANGELOG.md b/languages/typescript/packages/auth/platforms/darwin-arm64/CHANGELOG.md index 6233b95e0..93d7b3590 100644 --- a/languages/typescript/packages/auth/platforms/darwin-arm64/CHANGELOG.md +++ b/languages/typescript/packages/auth/platforms/darwin-arm64/CHANGELOG.md @@ -1,3 +1,5 @@ # @cipherstash/auth-darwin-arm64 +## 1.0.0 + ## 0.44.1 diff --git a/languages/typescript/packages/auth/platforms/darwin-arm64/package.json b/languages/typescript/packages/auth/platforms/darwin-arm64/package.json index 0f78ea403..6ca65c3ca 100644 --- a/languages/typescript/packages/auth/platforms/darwin-arm64/package.json +++ b/languages/typescript/packages/auth/platforms/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/auth-darwin-arm64", - "version": "0.44.1", + "version": "1.0.0", "repository": { "type": "git", "url": "git+https://github.com/cipherstash/stack.git", diff --git a/languages/typescript/packages/auth/platforms/darwin-x64/CHANGELOG.md b/languages/typescript/packages/auth/platforms/darwin-x64/CHANGELOG.md index 71677b17e..69775e95f 100644 --- a/languages/typescript/packages/auth/platforms/darwin-x64/CHANGELOG.md +++ b/languages/typescript/packages/auth/platforms/darwin-x64/CHANGELOG.md @@ -1,3 +1,5 @@ # @cipherstash/auth-darwin-x64 +## 1.0.0 + ## 0.44.1 diff --git a/languages/typescript/packages/auth/platforms/darwin-x64/package.json b/languages/typescript/packages/auth/platforms/darwin-x64/package.json index 4fbe06dbf..46ee21de0 100644 --- a/languages/typescript/packages/auth/platforms/darwin-x64/package.json +++ b/languages/typescript/packages/auth/platforms/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/auth-darwin-x64", - "version": "0.44.1", + "version": "1.0.0", "repository": { "type": "git", "url": "git+https://github.com/cipherstash/stack.git", diff --git a/languages/typescript/packages/auth/platforms/linux-arm64-gnu/CHANGELOG.md b/languages/typescript/packages/auth/platforms/linux-arm64-gnu/CHANGELOG.md index 087ccbc94..3e9db8022 100644 --- a/languages/typescript/packages/auth/platforms/linux-arm64-gnu/CHANGELOG.md +++ b/languages/typescript/packages/auth/platforms/linux-arm64-gnu/CHANGELOG.md @@ -1,3 +1,5 @@ # @cipherstash/auth-linux-arm64-gnu +## 1.0.0 + ## 0.44.1 diff --git a/languages/typescript/packages/auth/platforms/linux-arm64-gnu/package.json b/languages/typescript/packages/auth/platforms/linux-arm64-gnu/package.json index 513f608e7..0d73d8665 100644 --- a/languages/typescript/packages/auth/platforms/linux-arm64-gnu/package.json +++ b/languages/typescript/packages/auth/platforms/linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/auth-linux-arm64-gnu", - "version": "0.44.1", + "version": "1.0.0", "repository": { "type": "git", "url": "git+https://github.com/cipherstash/stack.git", diff --git a/languages/typescript/packages/auth/platforms/linux-x64-gnu/CHANGELOG.md b/languages/typescript/packages/auth/platforms/linux-x64-gnu/CHANGELOG.md index b75115ecd..67a7f9b57 100644 --- a/languages/typescript/packages/auth/platforms/linux-x64-gnu/CHANGELOG.md +++ b/languages/typescript/packages/auth/platforms/linux-x64-gnu/CHANGELOG.md @@ -1,3 +1,5 @@ # @cipherstash/auth-linux-x64-gnu +## 1.0.0 + ## 0.44.1 diff --git a/languages/typescript/packages/auth/platforms/linux-x64-gnu/package.json b/languages/typescript/packages/auth/platforms/linux-x64-gnu/package.json index d2c98819d..bd8de88e1 100644 --- a/languages/typescript/packages/auth/platforms/linux-x64-gnu/package.json +++ b/languages/typescript/packages/auth/platforms/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/auth-linux-x64-gnu", - "version": "0.44.1", + "version": "1.0.0", "repository": { "type": "git", "url": "git+https://github.com/cipherstash/stack.git", diff --git a/languages/typescript/packages/auth/platforms/linux-x64-musl/CHANGELOG.md b/languages/typescript/packages/auth/platforms/linux-x64-musl/CHANGELOG.md index b1e5a7fc6..739fb2f26 100644 --- a/languages/typescript/packages/auth/platforms/linux-x64-musl/CHANGELOG.md +++ b/languages/typescript/packages/auth/platforms/linux-x64-musl/CHANGELOG.md @@ -1,3 +1,5 @@ # @cipherstash/auth-linux-x64-musl +## 1.0.0 + ## 0.44.1 diff --git a/languages/typescript/packages/auth/platforms/linux-x64-musl/package.json b/languages/typescript/packages/auth/platforms/linux-x64-musl/package.json index ae152532c..f8bb33cf3 100644 --- a/languages/typescript/packages/auth/platforms/linux-x64-musl/package.json +++ b/languages/typescript/packages/auth/platforms/linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/auth-linux-x64-musl", - "version": "0.44.1", + "version": "1.0.0", "repository": { "type": "git", "url": "git+https://github.com/cipherstash/stack.git", diff --git a/languages/typescript/packages/auth/platforms/win32-x64-msvc/CHANGELOG.md b/languages/typescript/packages/auth/platforms/win32-x64-msvc/CHANGELOG.md index 0073ae5f2..c01cae770 100644 --- a/languages/typescript/packages/auth/platforms/win32-x64-msvc/CHANGELOG.md +++ b/languages/typescript/packages/auth/platforms/win32-x64-msvc/CHANGELOG.md @@ -1,3 +1,5 @@ # @cipherstash/auth-win32-x64-msvc +## 1.0.0 + ## 0.44.1 diff --git a/languages/typescript/packages/auth/platforms/win32-x64-msvc/package.json b/languages/typescript/packages/auth/platforms/win32-x64-msvc/package.json index 15ada84d1..7064c1402 100644 --- a/languages/typescript/packages/auth/platforms/win32-x64-msvc/package.json +++ b/languages/typescript/packages/auth/platforms/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/auth-win32-x64-msvc", - "version": "0.44.1", + "version": "1.0.0", "repository": { "type": "git", "url": "git+https://github.com/cipherstash/stack.git", diff --git a/languages/typescript/packages/bench/CHANGELOG.md b/languages/typescript/packages/bench/CHANGELOG.md index 173625a3f..5100aae7a 100644 --- a/languages/typescript/packages/bench/CHANGELOG.md +++ b/languages/typescript/packages/bench/CHANGELOG.md @@ -1,5 +1,16 @@ # @cipherstash/bench +## 0.0.10 + +### Patch Changes + +- Updated dependencies [fd0ee91] +- Updated dependencies [4b14ee6] +- Updated dependencies [bbb1bfa] +- Updated dependencies [20ccab3] + - @cipherstash/stack@1.3.0 + - @cipherstash/stack-drizzle@1.3.0 + ## 0.0.9 ### Patch Changes diff --git a/languages/typescript/packages/bench/package.json b/languages/typescript/packages/bench/package.json index 0ec748e34..931abfe95 100644 --- a/languages/typescript/packages/bench/package.json +++ b/languages/typescript/packages/bench/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/bench", - "version": "0.0.9", + "version": "0.0.10", "private": true, "description": "Performance / index-engagement benchmarks for stack integrations (Drizzle, encryptedSupabase, Prisma).", "type": "module", diff --git a/languages/typescript/packages/cli/CHANGELOG.md b/languages/typescript/packages/cli/CHANGELOG.md index 3776ded5a..ebb561bfa 100644 --- a/languages/typescript/packages/cli/CHANGELOG.md +++ b/languages/typescript/packages/cli/CHANGELOG.md @@ -1,5 +1,74 @@ # stash +## 1.3.0 + +### Minor Changes + +- bca02b6: `stash decrypt status|plan|backfill|drop` moves encrypted columns back to plaintext — the encrypt lifecycle run in reverse, with the same safety rails. `decrypt plan` adds a nullable plaintext twin per EQL v3 column with its type mapped back from the domain (`text*` → `text`, `timestamp*` → `timestamptz`, `json*` → `jsonb`, …): Drizzle schema lines for `drizzle-kit generate`, or a reviewed SQL migration elsewhere. `decrypt backfill` decrypts in resumable, checkpointed chunks, one `bulkDecrypt` (one ZeroKMS request) per chunk, default 500 rows. It refuses a plaintext column that a forward encryption started from and never dropped: only one migration may be open on a pair of columns, so finish or abandon the forward one first. When that column was already removed outside stash, `decrypt plan` finds it missing and records the forward migration as dropped (marked as observed), so the reverse migration can start. Its first run also refuses a plaintext column that already holds values, because those values may be stale and the backfill only fills NULLs: pass `--force` to overwrite them from the ciphertext or `--confirm-plaintext-current` to keep them. Once `--force` is chosen, or any forced run starts, every later run is forced until a forced pass completes, so an interrupted overwrite is always finished. The application keeps writing both columns until the drop migration is applied. Values bound to an end-user lock context cannot be decrypted with workspace credentials; they are skipped, counted, left NULL and reported, and `decrypt drop` refuses while any remain. `decrypt drop` re-verifies coverage at apply time, drops the indexes built on the column's EQL terms, drops the column, and with `--uninstall-eql` removes EQL v3 when it was the last encrypted column in the database. + + `@cipherstash/migrate`: `runBackfill` now runs on a shared chunked engine (`runChunkedMigration`), which also drives the new `runDecryptBackfill`. Forward behaviour and recorded event details are unchanged. Adds `runDecryptBackfill` (resumes after a failed run; `PlaintextWriteError` keeps decrypted values out of write errors; chunk size capped at `MAX_DECRYPT_CHUNK_SIZE`), `columnHasValues`, `countUndecrypted`, `fetchUndecryptedPage`, `plaintextTypeForEqlDomain`, `columnSqlType`, `listColumnIndexes`, `primaryKeyColumns`, `countEqlColumnsInDatabase`, `removeManifestColumn`, and the reverse event/phase vocabulary (`decrypt_started`, `decrypt_checkpoint`, `decrypted`, `encrypted_dropped`; `decrypt-dual-writing`, `decrypting`, `decrypted`, `encrypted-dropped`), recorded against the encrypted column. `MigrationPhase` and `MigrationEvent` gain those members, so an exhaustive `switch` over them needs the new cases. + +### Patch Changes + +- bde5b73: `@cipherstash/migrate` adds `ColumnMigration`, one place for the rules of a column migration in either direction: which twin column it pairs with (the flag, the recorded pairing, the manifest, then the naming convention), its phase, whether a forced pass is owed, and which open migration in the other direction blocks it. + `ColumnMigration.load()` reads it from `cipherstash.cs_migrations`; `confirmDualWrites()`, `recordDropGenerated()` and `recordSourceRemoved()` record events, refusing those the rules forbid with a `ColumnMigrationError`. + The pure rules are exported as `deriveColumnMigration`, `openCounterpart`, `planPassFor`, `passProgress`, `deriveTwinName` and `manifestTwin`. + + `runBackfill` and `runDecryptBackfill` now apply the rules themselves rather than leaving them to the caller. + Every event records whether its pass was forced. + An interrupted forced pass is finished as one by the next run, with or without `force`, and the result says why in `forcedBy`. + A forward backfill resumes after a failed run from its last committed position, counts its total once when it resumes a forced pass, and refuses to start while a reverse migration of the same columns is open. + `runBackfill` now throws a `ColumnMigrationError` with code `twin-mismatch` when `encryptedColumn` differs from the encrypted column already recorded for that column, rather than continuing into a different one. + A reverse backfill over a twin that already holds values is forced unless the caller confirmed dual-writes with the `plaintext-current` waiver. + Error events store the error's class (`errorName`) and safe details instead of its message, which could carry row data. + + `stash decrypt plan`, `status`, `backfill` and `drop` resolve every column through `ColumnMigration`, so they always agree on the twin and the phase. + `decrypt status` flags `forward-open`, `twin-has-values` and `forced-pass-owed`. + `decrypt backfill --confirm-plaintext-current` is accepted on a later run as long as no forced pass has started, and refused while a started forced pass is owed. + +- 587d06e: The supply-chain skill names `pip` among the Dependabot ecosystems the repository monitors, and says that a pip requirements file installed in CI should pin every package with a hash and be installed with `pip install --require-hashes --no-deps`. +- bd7e55f: Fix invalid YAML in `skills/stash-managed-platforms/SKILL.md` frontmatter: the + unquoted `description` contained a colon-space, so any YAML parser rejected the + file (this broke cipherstash.com deploys, which mirror these skills at build + time). Rewritten as a folded block scalar; the parsed description is + byte-for-byte identical. +- e5a3575: The supply-chain skill names every Dependabot ecosystem the repository monitors, including the Go module and the Docker image the musl binaries are built in. +- fd0ee91: Document how `OidcFederationStrategy` serves many users through one client: the + `getJwt` callback runs on every operation, in the caller's async context, and + must return the JWT of the user behind the current request, and each user's + JWT is exchanged for that user's own token (stack-auth's fix for + cipherstash/stack#1045). One caveat is documented for the `@cipherstash/stack` + client on the native entry, which calls the strategy from a background task so + a request read from `AsyncLocalStorage` is not visible inside `getJwt` there + yet: capture the request in the closure or build the client per request until + cipherstash/stack#1065 lands. The docs and the `stash-auth`, `stash-encryption`, + `stash-supabase` and `stash-edge` skills say so, and say that on earlier + `@cipherstash/auth` releases a strategy caches one token for everyone, so a + client there must serve one user. +- ae63dcd: `skills/stash-prisma`: the stale-`migrations/cipherstash/` table now describes + eql-3.1.0. The baseline bakes 3.1.0, 1.2.x shipped 3.0.6, and the table names + the `db init` refusal a stale 1.2.x directory produces. +- bbb1bfa: `encrypt` on a `types.Json` column now types its result as `Encrypted | null` when the plaintext may be `null`. A `null` document has always resolved to `{ data: null }` at runtime (it is stored as SQL NULL, not encrypted), but the result was typed `Encrypted`, so `result.data.c` compiled and then threw. `encrypt` is now generic over the plaintext's type: a literal `null`, or a value typed `JsonDocument`, gives `data: Encrypted | null`, through `.withLockContext()` and `.audit()` too. A value TypeScript knows is non-null, and every scalar column, still gives `data: Encrypted`. No runtime change. If your code reads `result.data` after encrypting a Json value that may be `null`, it must now check for `null` first. `EncryptOperation` and `EncryptOperationWithLockContext` take an optional result type parameter (default `Encrypted`), and the `EncryptResult
` helper type is exported from `@cipherstash/stack/encryption`.
+
+ `@cipherstash/stack-supabase`: the per-term query-encryption fallback (used when the client has no `bulkEncrypt`) now rejects a `null` envelope instead of sending the string `"null"` as a filter operand, matching the bulk path.
+
+ `stash`: the `stash-encryption` skill now says what `encrypt` returns for a `null` Json document.
+
+- Updated dependencies [a1765f1]
+- Updated dependencies [fb75f40]
+- Updated dependencies [457b28e]
+- Updated dependencies [b480735]
+- Updated dependencies [bde5b73]
+- Updated dependencies [e930e5b]
+- Updated dependencies [0275684]
+- Updated dependencies [c851db5]
+- Updated dependencies [fd0ee91]
+- Updated dependencies [f7f1c76]
+- Updated dependencies [bca02b6]
+ - @cipherstash/auth@1.0.0
+ - @cipherstash/migrate@1.1.0
+ - @cipherstash/eql@3.1.0
+
## 1.2.1
### Patch Changes
diff --git a/languages/typescript/packages/cli/package.json b/languages/typescript/packages/cli/package.json
index f745c2976..6fab99a04 100644
--- a/languages/typescript/packages/cli/package.json
+++ b/languages/typescript/packages/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "stash",
- "version": "1.2.1",
+ "version": "1.3.0",
"description": "CipherStash CLI — the one stash command for auth, init, encryption schema, database setup, and secrets.",
"repository": {
"type": "git",
diff --git a/languages/typescript/packages/migrate/CHANGELOG.md b/languages/typescript/packages/migrate/CHANGELOG.md
index c2e623ce3..850ba2f7a 100644
--- a/languages/typescript/packages/migrate/CHANGELOG.md
+++ b/languages/typescript/packages/migrate/CHANGELOG.md
@@ -1,5 +1,36 @@
# @cipherstash/migrate
+## 1.1.0
+
+### Minor Changes
+
+- bde5b73: `@cipherstash/migrate` adds `ColumnMigration`, one place for the rules of a column migration in either direction: which twin column it pairs with (the flag, the recorded pairing, the manifest, then the naming convention), its phase, whether a forced pass is owed, and which open migration in the other direction blocks it.
+ `ColumnMigration.load()` reads it from `cipherstash.cs_migrations`; `confirmDualWrites()`, `recordDropGenerated()` and `recordSourceRemoved()` record events, refusing those the rules forbid with a `ColumnMigrationError`.
+ The pure rules are exported as `deriveColumnMigration`, `openCounterpart`, `planPassFor`, `passProgress`, `deriveTwinName` and `manifestTwin`.
+
+ `runBackfill` and `runDecryptBackfill` now apply the rules themselves rather than leaving them to the caller.
+ Every event records whether its pass was forced.
+ An interrupted forced pass is finished as one by the next run, with or without `force`, and the result says why in `forcedBy`.
+ A forward backfill resumes after a failed run from its last committed position, counts its total once when it resumes a forced pass, and refuses to start while a reverse migration of the same columns is open.
+ `runBackfill` now throws a `ColumnMigrationError` with code `twin-mismatch` when `encryptedColumn` differs from the encrypted column already recorded for that column, rather than continuing into a different one.
+ A reverse backfill over a twin that already holds values is forced unless the caller confirmed dual-writes with the `plaintext-current` waiver.
+ Error events store the error's class (`errorName`) and safe details instead of its message, which could carry row data.
+
+ `stash decrypt plan`, `status`, `backfill` and `drop` resolve every column through `ColumnMigration`, so they always agree on the twin and the phase.
+ `decrypt status` flags `forward-open`, `twin-has-values` and `forced-pass-owed`.
+ `decrypt backfill --confirm-plaintext-current` is accepted on a later run as long as no forced pass has started, and refused while a started forced pass is owed.
+
+- bca02b6: `stash decrypt status|plan|backfill|drop` moves encrypted columns back to plaintext — the encrypt lifecycle run in reverse, with the same safety rails. `decrypt plan` adds a nullable plaintext twin per EQL v3 column with its type mapped back from the domain (`text*` → `text`, `timestamp*` → `timestamptz`, `json*` → `jsonb`, …): Drizzle schema lines for `drizzle-kit generate`, or a reviewed SQL migration elsewhere. `decrypt backfill` decrypts in resumable, checkpointed chunks, one `bulkDecrypt` (one ZeroKMS request) per chunk, default 500 rows. It refuses a plaintext column that a forward encryption started from and never dropped: only one migration may be open on a pair of columns, so finish or abandon the forward one first. When that column was already removed outside stash, `decrypt plan` finds it missing and records the forward migration as dropped (marked as observed), so the reverse migration can start. Its first run also refuses a plaintext column that already holds values, because those values may be stale and the backfill only fills NULLs: pass `--force` to overwrite them from the ciphertext or `--confirm-plaintext-current` to keep them. Once `--force` is chosen, or any forced run starts, every later run is forced until a forced pass completes, so an interrupted overwrite is always finished. The application keeps writing both columns until the drop migration is applied. Values bound to an end-user lock context cannot be decrypted with workspace credentials; they are skipped, counted, left NULL and reported, and `decrypt drop` refuses while any remain. `decrypt drop` re-verifies coverage at apply time, drops the indexes built on the column's EQL terms, drops the column, and with `--uninstall-eql` removes EQL v3 when it was the last encrypted column in the database.
+
+ `@cipherstash/migrate`: `runBackfill` now runs on a shared chunked engine (`runChunkedMigration`), which also drives the new `runDecryptBackfill`. Forward behaviour and recorded event details are unchanged. Adds `runDecryptBackfill` (resumes after a failed run; `PlaintextWriteError` keeps decrypted values out of write errors; chunk size capped at `MAX_DECRYPT_CHUNK_SIZE`), `columnHasValues`, `countUndecrypted`, `fetchUndecryptedPage`, `plaintextTypeForEqlDomain`, `columnSqlType`, `listColumnIndexes`, `primaryKeyColumns`, `countEqlColumnsInDatabase`, `removeManifestColumn`, and the reverse event/phase vocabulary (`decrypt_started`, `decrypt_checkpoint`, `decrypted`, `encrypted_dropped`; `decrypt-dual-writing`, `decrypting`, `decrypted`, `encrypted-dropped`), recorded against the encrypted column. `MigrationPhase` and `MigrationEvent` gain those members, so an exhaustive `switch` over them needs the new cases.
+
+### Patch Changes
+
+- f7f1c76: The package's source links now point at its folder under
+ `languages/typescript/` in cipherstash/stack. The previous release linked to
+ its old folder under `packages/`, which no longer exists, so the README's
+ source links and the npm page's repository link returned 404.
+
## 1.0.0
### Major Changes
diff --git a/languages/typescript/packages/migrate/package.json b/languages/typescript/packages/migrate/package.json
index a02f0f324..803699a8c 100644
--- a/languages/typescript/packages/migrate/package.json
+++ b/languages/typescript/packages/migrate/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/migrate",
- "version": "1.0.0",
+ "version": "1.1.0",
"description": "Plaintext-to-encrypted column migration for CipherStash: resumable backfill, per-column state, and EQL lifecycle orchestration.",
"repository": {
"type": "git",
diff --git a/languages/typescript/packages/nextjs/CHANGELOG.md b/languages/typescript/packages/nextjs/CHANGELOG.md
index 5014b82af..73d4cae9a 100644
--- a/languages/typescript/packages/nextjs/CHANGELOG.md
+++ b/languages/typescript/packages/nextjs/CHANGELOG.md
@@ -1,5 +1,14 @@
# @cipherstash/nextjs
+## 4.4.1
+
+### Patch Changes
+
+- f7f1c76: The package's source links now point at its folder under
+ `languages/typescript/` in cipherstash/stack. The previous release linked to
+ its old folder under `packages/`, which no longer exists, so the README's
+ source links and the npm page's repository link returned 404.
+
## 4.4.0
### Minor Changes
diff --git a/languages/typescript/packages/nextjs/package.json b/languages/typescript/packages/nextjs/package.json
index a1c6f4817..5242b22f2 100644
--- a/languages/typescript/packages/nextjs/package.json
+++ b/languages/typescript/packages/nextjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/nextjs",
- "version": "4.4.0",
+ "version": "4.4.1",
"description": "Nextjs package for use with @cipherstash/stack",
"keywords": [
"encrypted",
diff --git a/languages/typescript/packages/protect-ffi/CHANGELOG.md b/languages/typescript/packages/protect-ffi/CHANGELOG.md
index 5cfe7a32e..bb17c640d 100644
--- a/languages/typescript/packages/protect-ffi/CHANGELOG.md
+++ b/languages/typescript/packages/protect-ffi/CHANGELOG.md
@@ -1,5 +1,16 @@
# Changelog
+## 0.33.1
+
+### Patch Changes
+
+- 457b28e: The native binaries now use rustls 0.23.45, which fixes GHSA-2mjx-qc3c-rqvc
+ (affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
+- f7f1c76: The package's source links now point at its folder under
+ `languages/typescript/` in cipherstash/stack. The previous release linked to
+ its old folder under `packages/`, which no longer exists, so the README's
+ source links and the npm page's repository link returned 404.
+
## 0.33.0
### Minor Changes
diff --git a/languages/typescript/packages/protect-ffi/Cargo.lock b/languages/typescript/packages/protect-ffi/Cargo.lock
index 9093f4e06..381ac35f5 100644
--- a/languages/typescript/packages/protect-ffi/Cargo.lock
+++ b/languages/typescript/packages/protect-ffi/Cargo.lock
@@ -1059,7 +1059,7 @@ dependencies = [
[[package]]
name = "eql-bindings"
-version = "3.0.6"
+version = "3.1.0"
dependencies = [
"schemars",
"serde",
diff --git a/languages/typescript/packages/protect-ffi/integration-tests/CHANGELOG.md b/languages/typescript/packages/protect-ffi/integration-tests/CHANGELOG.md
index 598477bcd..5c08d1cd1 100644
--- a/languages/typescript/packages/protect-ffi/integration-tests/CHANGELOG.md
+++ b/languages/typescript/packages/protect-ffi/integration-tests/CHANGELOG.md
@@ -1,5 +1,19 @@
# @cipherstash/ffi-integration-tests
+## 1.0.3
+
+### Patch Changes
+
+- Updated dependencies [a1765f1]
+- Updated dependencies [fb75f40]
+- Updated dependencies [457b28e]
+- Updated dependencies [b480735]
+- Updated dependencies [fd0ee91]
+- Updated dependencies [457b28e]
+- Updated dependencies [f7f1c76]
+ - @cipherstash/auth@1.0.0
+ - @cipherstash/protect-ffi@0.33.1
+
## 1.0.2
### Patch Changes
diff --git a/languages/typescript/packages/protect-ffi/integration-tests/package.json b/languages/typescript/packages/protect-ffi/integration-tests/package.json
index 20010bb06..2704134ff 100644
--- a/languages/typescript/packages/protect-ffi/integration-tests/package.json
+++ b/languages/typescript/packages/protect-ffi/integration-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/ffi-integration-tests",
- "version": "1.0.2",
+ "version": "1.0.3",
"private": true,
"description": "Live integration coverage for @cipherstash/protect-ffi — real ZeroKMS, real Postgres, EQL v2 and v3",
"license": "MIT",
diff --git a/languages/typescript/packages/protect-ffi/package.json b/languages/typescript/packages/protect-ffi/package.json
index 6c4dd498e..089bbd3eb 100644
--- a/languages/typescript/packages/protect-ffi/package.json
+++ b/languages/typescript/packages/protect-ffi/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/protect-ffi",
- "version": "0.33.0",
+ "version": "0.33.1",
"repository": {
"type": "git",
"url": "git+https://github.com/cipherstash/stack.git",
diff --git a/languages/typescript/packages/protect-ffi/platforms/darwin-arm64/CHANGELOG.md b/languages/typescript/packages/protect-ffi/platforms/darwin-arm64/CHANGELOG.md
index 807ce0ec6..f3b815a15 100644
--- a/languages/typescript/packages/protect-ffi/platforms/darwin-arm64/CHANGELOG.md
+++ b/languages/typescript/packages/protect-ffi/platforms/darwin-arm64/CHANGELOG.md
@@ -1,5 +1,16 @@
# @cipherstash/protect-ffi-darwin-arm64
+## 0.33.1
+
+### Patch Changes
+
+- 457b28e: The native binaries now use rustls 0.23.45, which fixes GHSA-2mjx-qc3c-rqvc
+ (affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
+- f7f1c76: The package's source links now point at its folder under
+ `languages/typescript/` in cipherstash/stack. The previous release linked to
+ its old folder under `packages/`, which no longer exists, so the README's
+ source links and the npm page's repository link returned 404.
+
## 0.33.0
## 0.32.0
diff --git a/languages/typescript/packages/protect-ffi/platforms/darwin-arm64/package.json b/languages/typescript/packages/protect-ffi/platforms/darwin-arm64/package.json
index 0a4bf082f..9d3472f48 100644
--- a/languages/typescript/packages/protect-ffi/platforms/darwin-arm64/package.json
+++ b/languages/typescript/packages/protect-ffi/platforms/darwin-arm64/package.json
@@ -1,7 +1,7 @@
{
"name": "@cipherstash/protect-ffi-darwin-arm64",
"description": "Prebuilt binary package for `@cipherstash/protect-ffi` on `darwin-arm64`.",
- "version": "0.33.0",
+ "version": "0.33.1",
"repository": {
"type": "git",
"url": "git+https://github.com/cipherstash/stack.git",
diff --git a/languages/typescript/packages/protect-ffi/platforms/darwin-x64/CHANGELOG.md b/languages/typescript/packages/protect-ffi/platforms/darwin-x64/CHANGELOG.md
index f7067ef63..9200dfa88 100644
--- a/languages/typescript/packages/protect-ffi/platforms/darwin-x64/CHANGELOG.md
+++ b/languages/typescript/packages/protect-ffi/platforms/darwin-x64/CHANGELOG.md
@@ -1,5 +1,16 @@
# @cipherstash/protect-ffi-darwin-x64
+## 0.33.1
+
+### Patch Changes
+
+- 457b28e: The native binaries now use rustls 0.23.45, which fixes GHSA-2mjx-qc3c-rqvc
+ (affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
+- f7f1c76: The package's source links now point at its folder under
+ `languages/typescript/` in cipherstash/stack. The previous release linked to
+ its old folder under `packages/`, which no longer exists, so the README's
+ source links and the npm page's repository link returned 404.
+
## 0.33.0
## 0.32.0
diff --git a/languages/typescript/packages/protect-ffi/platforms/darwin-x64/package.json b/languages/typescript/packages/protect-ffi/platforms/darwin-x64/package.json
index eaffc5742..70ef6b19f 100644
--- a/languages/typescript/packages/protect-ffi/platforms/darwin-x64/package.json
+++ b/languages/typescript/packages/protect-ffi/platforms/darwin-x64/package.json
@@ -1,7 +1,7 @@
{
"name": "@cipherstash/protect-ffi-darwin-x64",
"description": "Prebuilt binary package for `@cipherstash/protect-ffi` on `darwin-x64`.",
- "version": "0.33.0",
+ "version": "0.33.1",
"repository": {
"type": "git",
"url": "git+https://github.com/cipherstash/stack.git",
diff --git a/languages/typescript/packages/protect-ffi/platforms/linux-arm64-gnu/CHANGELOG.md b/languages/typescript/packages/protect-ffi/platforms/linux-arm64-gnu/CHANGELOG.md
index 8a79eab40..e161cd302 100644
--- a/languages/typescript/packages/protect-ffi/platforms/linux-arm64-gnu/CHANGELOG.md
+++ b/languages/typescript/packages/protect-ffi/platforms/linux-arm64-gnu/CHANGELOG.md
@@ -1,5 +1,16 @@
# @cipherstash/protect-ffi-linux-arm64-gnu
+## 0.33.1
+
+### Patch Changes
+
+- 457b28e: The native binaries now use rustls 0.23.45, which fixes GHSA-2mjx-qc3c-rqvc
+ (affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
+- f7f1c76: The package's source links now point at its folder under
+ `languages/typescript/` in cipherstash/stack. The previous release linked to
+ its old folder under `packages/`, which no longer exists, so the README's
+ source links and the npm page's repository link returned 404.
+
## 0.33.0
## 0.32.0
diff --git a/languages/typescript/packages/protect-ffi/platforms/linux-arm64-gnu/package.json b/languages/typescript/packages/protect-ffi/platforms/linux-arm64-gnu/package.json
index 7d284be5f..61f528845 100644
--- a/languages/typescript/packages/protect-ffi/platforms/linux-arm64-gnu/package.json
+++ b/languages/typescript/packages/protect-ffi/platforms/linux-arm64-gnu/package.json
@@ -1,7 +1,7 @@
{
"name": "@cipherstash/protect-ffi-linux-arm64-gnu",
"description": "Prebuilt binary package for `@cipherstash/protect-ffi` on `linux-arm64-gnu`.",
- "version": "0.33.0",
+ "version": "0.33.1",
"repository": {
"type": "git",
"url": "git+https://github.com/cipherstash/stack.git",
diff --git a/languages/typescript/packages/protect-ffi/platforms/linux-x64-gnu/CHANGELOG.md b/languages/typescript/packages/protect-ffi/platforms/linux-x64-gnu/CHANGELOG.md
index ccaad566a..449226270 100644
--- a/languages/typescript/packages/protect-ffi/platforms/linux-x64-gnu/CHANGELOG.md
+++ b/languages/typescript/packages/protect-ffi/platforms/linux-x64-gnu/CHANGELOG.md
@@ -1,5 +1,16 @@
# @cipherstash/protect-ffi-linux-x64-gnu
+## 0.33.1
+
+### Patch Changes
+
+- 457b28e: The native binaries now use rustls 0.23.45, which fixes GHSA-2mjx-qc3c-rqvc
+ (affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
+- f7f1c76: The package's source links now point at its folder under
+ `languages/typescript/` in cipherstash/stack. The previous release linked to
+ its old folder under `packages/`, which no longer exists, so the README's
+ source links and the npm page's repository link returned 404.
+
## 0.33.0
## 0.32.0
diff --git a/languages/typescript/packages/protect-ffi/platforms/linux-x64-gnu/package.json b/languages/typescript/packages/protect-ffi/platforms/linux-x64-gnu/package.json
index 83e6266a1..014cd646e 100644
--- a/languages/typescript/packages/protect-ffi/platforms/linux-x64-gnu/package.json
+++ b/languages/typescript/packages/protect-ffi/platforms/linux-x64-gnu/package.json
@@ -1,7 +1,7 @@
{
"name": "@cipherstash/protect-ffi-linux-x64-gnu",
"description": "Prebuilt binary package for `@cipherstash/protect-ffi` on `linux-x64-gnu`.",
- "version": "0.33.0",
+ "version": "0.33.1",
"repository": {
"type": "git",
"url": "git+https://github.com/cipherstash/stack.git",
diff --git a/languages/typescript/packages/protect-ffi/platforms/linux-x64-musl/CHANGELOG.md b/languages/typescript/packages/protect-ffi/platforms/linux-x64-musl/CHANGELOG.md
index 8bacbdb5c..9762de93a 100644
--- a/languages/typescript/packages/protect-ffi/platforms/linux-x64-musl/CHANGELOG.md
+++ b/languages/typescript/packages/protect-ffi/platforms/linux-x64-musl/CHANGELOG.md
@@ -1,5 +1,16 @@
# @cipherstash/protect-ffi-linux-x64-musl
+## 0.33.1
+
+### Patch Changes
+
+- 457b28e: The native binaries now use rustls 0.23.45, which fixes GHSA-2mjx-qc3c-rqvc
+ (affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
+- f7f1c76: The package's source links now point at its folder under
+ `languages/typescript/` in cipherstash/stack. The previous release linked to
+ its old folder under `packages/`, which no longer exists, so the README's
+ source links and the npm page's repository link returned 404.
+
## 0.33.0
## 0.32.0
diff --git a/languages/typescript/packages/protect-ffi/platforms/linux-x64-musl/package.json b/languages/typescript/packages/protect-ffi/platforms/linux-x64-musl/package.json
index 53e72ad3e..77376a4c9 100644
--- a/languages/typescript/packages/protect-ffi/platforms/linux-x64-musl/package.json
+++ b/languages/typescript/packages/protect-ffi/platforms/linux-x64-musl/package.json
@@ -1,7 +1,7 @@
{
"name": "@cipherstash/protect-ffi-linux-x64-musl",
"description": "Prebuilt binary package for `@cipherstash/protect-ffi` on `linux-x64-musl`.",
- "version": "0.33.0",
+ "version": "0.33.1",
"repository": {
"type": "git",
"url": "git+https://github.com/cipherstash/stack.git",
diff --git a/languages/typescript/packages/protect-ffi/platforms/win32-x64-msvc/CHANGELOG.md b/languages/typescript/packages/protect-ffi/platforms/win32-x64-msvc/CHANGELOG.md
index 86a2feda4..2812e3d55 100644
--- a/languages/typescript/packages/protect-ffi/platforms/win32-x64-msvc/CHANGELOG.md
+++ b/languages/typescript/packages/protect-ffi/platforms/win32-x64-msvc/CHANGELOG.md
@@ -1,5 +1,16 @@
# @cipherstash/protect-ffi-win32-x64-msvc
+## 0.33.1
+
+### Patch Changes
+
+- 457b28e: The native binaries now use rustls 0.23.45, which fixes GHSA-2mjx-qc3c-rqvc
+ (affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
+- f7f1c76: The package's source links now point at its folder under
+ `languages/typescript/` in cipherstash/stack. The previous release linked to
+ its old folder under `packages/`, which no longer exists, so the README's
+ source links and the npm page's repository link returned 404.
+
## 0.33.0
## 0.32.0
diff --git a/languages/typescript/packages/protect-ffi/platforms/win32-x64-msvc/package.json b/languages/typescript/packages/protect-ffi/platforms/win32-x64-msvc/package.json
index ebd8fdc96..e067cfa3d 100644
--- a/languages/typescript/packages/protect-ffi/platforms/win32-x64-msvc/package.json
+++ b/languages/typescript/packages/protect-ffi/platforms/win32-x64-msvc/package.json
@@ -1,7 +1,7 @@
{
"name": "@cipherstash/protect-ffi-win32-x64-msvc",
"description": "Prebuilt binary package for `@cipherstash/protect-ffi` on `win32-x64-msvc`.",
- "version": "0.33.0",
+ "version": "0.33.1",
"repository": {
"type": "git",
"url": "git+https://github.com/cipherstash/stack.git",
diff --git a/languages/typescript/packages/stack-drizzle/CHANGELOG.md b/languages/typescript/packages/stack-drizzle/CHANGELOG.md
index f8cedd4fb..a08a2546d 100644
--- a/languages/typescript/packages/stack-drizzle/CHANGELOG.md
+++ b/languages/typescript/packages/stack-drizzle/CHANGELOG.md
@@ -1,5 +1,15 @@
# @cipherstash/stack-drizzle
+## 1.3.0
+
+### Patch Changes
+
+- Updated dependencies [fd0ee91]
+- Updated dependencies [4b14ee6]
+- Updated dependencies [bbb1bfa]
+- Updated dependencies [20ccab3]
+ - @cipherstash/stack@1.3.0
+
## 1.2.1
### Patch Changes
diff --git a/languages/typescript/packages/stack-drizzle/package.json b/languages/typescript/packages/stack-drizzle/package.json
index 03fa61578..16b36492c 100644
--- a/languages/typescript/packages/stack-drizzle/package.json
+++ b/languages/typescript/packages/stack-drizzle/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/stack-drizzle",
- "version": "1.2.1",
+ "version": "1.3.0",
"description": "CipherStash Stack Drizzle ORM integration: searchable, application-layer field-level encryption for PostgreSQL.",
"keywords": [
"encrypted",
diff --git a/languages/typescript/packages/stack-prisma/CHANGELOG.md b/languages/typescript/packages/stack-prisma/CHANGELOG.md
index 737bd9c16..942c6e5de 100644
--- a/languages/typescript/packages/stack-prisma/CHANGELOG.md
+++ b/languages/typescript/packages/stack-prisma/CHANGELOG.md
@@ -1,5 +1,80 @@
# @cipherstash/stack-prisma
+## 1.3.0
+
+### Minor Changes
+
+- ae63dcd: Move the bundled EQL v3 migrations to **eql-3.1.0**. Its install SQL changes
+ the version stamp (`eql_v3.version()` returns `'3.1.0'` and the `eql_v3`
+ schema comment reads `'3.1.0'`) and the ORE block comparator behind every
+ ordered ORE operator and the ORE operator class. The comparator now runs a
+ constant number of operations whatever two values share, closing a timing
+ leak. Ordering results are unchanged for well-formed values, but a block-ORE
+ text value with a malformed or mismatched-width term now raises on every
+ comparison against it. Check your `_ore` text columns before migrating; see
+ [U-001](../../../../packages/eql/docs/upgrading/v3.1.md#u-001-block-ore-text-comparisons-check-every-term).
+ No domain, wire format or index term changes.
+
+ Two artefacts carry the new bundle:
+
+ - A new upgrade edge, `20261005T0000_upgrade_eql_v3_3_1_0`, carrying the
+ invariant `cipherstash:upgrade-eql-v3-bundle-3.1.0-v1`. Existing databases
+ re-install through it on the next `prisma-next migration plan` followed by
+ `prisma-next migrate`. **`migrate` alone is not enough**: only
+ `migration plan` copies the new directory into your repo.
+ - The baseline install migration `20260601T0100_install_eql_v3_bundle`, which
+ now bakes 3.1.0 and gains a sixth op, a no-SQL carrier for the new
+ invariant, so fresh databases land on 3.1.0 from the single all-additive
+ genesis edge and `db init` keeps working.
+
+ **A 1.2.x database walks one edge. A 1.0.0 or 1.1.x database walks three.**
+ 1.2.x shipped eql-3.0.6, so `migrate` walks only the 3.1.0 edge. 1.0.0 and
+ 1.1.x shipped eql-3.0.4, so `migrate` walks the 3.0.5, 3.0.6 and 3.1.0 edges in
+ one transaction, re-installing the ~2.6 MB bundle three times. As with every EQL
+ upgrade, each re-install opens with `DROP SCHEMA IF EXISTS eql_v3 CASCADE`:
+ re-run your grant script, and recreate functional indexes and anything else
+ depending on `eql_v3`.
+
+ **Action required, including on 1.2.x: the baseline changed again.** 1.2.0
+ and 1.2.1 shipped the baseline at eql-3.0.6, and this release changes those
+ published bytes. 1.0.0 to 1.1.1 shipped it at eql-3.0.4. If your project has a
+ `migrations/cipherstash/` directory generated against any earlier version,
+ delete it and re-run `prisma-next migration plan`:
+
+ ```bash
+ rm -rf migrations/cipherstash
+ npx prisma-next migration plan
+ ```
+
+ Your database keeps its markers, so already-applied invariants are not re-run.
+ If you skip the delete, a fresh `db init` from a 1.2.x directory refuses with
+ `Operation cipherstash.upgrade-eql-v3-bundle-3.1.0 has class "data" which is
+not allowed by policy.` From a 1.0.0 or 1.1.x directory it refuses on the 3.0.5
+ edge instead. See "Upgrading from 1.0.0, 1.1.x or 1.2.x" in the package README.
+
+ **Why the baseline was re-emitted rather than given a second genesis edge.**
+ The 3.0.5 and 3.0.6 entries said this trade must be re-argued on adoption
+ numbers at each bump. Unlike the 3.0.6 re-emit, this one changes a published
+ baseline. The team chose it on these numbers: `@cipherstash/stack-prisma` had
+ 481 npm downloads in the 30 days to 3 October 2026, and 384 in the last 7 of
+ those days, 305 of them on the 1.2.0 release day. The append-only alternative,
+ a second `from: null` genesis edge, would add another permanent ~2.6 MB copy of
+ the bundle. That choice was made when 3.1.0 changed only the version stamp; the
+ comparator change came after it and does not alter the trade, since every
+ database reaches the same bundle either way.
+
+### Patch Changes
+
+- Updated dependencies [e930e5b]
+- Updated dependencies [0275684]
+- Updated dependencies [c851db5]
+- Updated dependencies [fd0ee91]
+- Updated dependencies [4b14ee6]
+- Updated dependencies [bbb1bfa]
+- Updated dependencies [20ccab3]
+ - @cipherstash/eql@3.1.0
+ - @cipherstash/stack@1.3.0
+
## 1.2.1
### Patch Changes
diff --git a/languages/typescript/packages/stack-prisma/package.json b/languages/typescript/packages/stack-prisma/package.json
index 3057110c1..60a8cef9c 100644
--- a/languages/typescript/packages/stack-prisma/package.json
+++ b/languages/typescript/packages/stack-prisma/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/stack-prisma",
- "version": "1.2.1",
+ "version": "1.3.0",
"license": "MIT",
"author": "CipherStash ` helper type is exported from `@cipherstash/stack/encryption`.
+
+ `@cipherstash/stack-supabase`: the per-term query-encryption fallback (used when the client has no `bulkEncrypt`) now rejects a `null` envelope instead of sending the string `"null"` as a filter operand, matching the bulk path.
+
+ `stash`: the `stash-encryption` skill now says what `encrypt` returns for a `null` Json document.
+
+- Updated dependencies [fd0ee91]
+- Updated dependencies [4b14ee6]
+- Updated dependencies [bbb1bfa]
+- Updated dependencies [20ccab3]
+ - @cipherstash/stack@1.3.0
+
## 1.2.1
### Patch Changes
diff --git a/languages/typescript/packages/stack-supabase/package.json b/languages/typescript/packages/stack-supabase/package.json
index a9d5472de..64d9f803e 100644
--- a/languages/typescript/packages/stack-supabase/package.json
+++ b/languages/typescript/packages/stack-supabase/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/stack-supabase",
- "version": "1.2.1",
+ "version": "1.3.0",
"description": "CipherStash Stack Supabase integration: transparent, searchable field-level encryption for Supabase.",
"keywords": [
"encrypted",
diff --git a/languages/typescript/packages/stack/CHANGELOG.md b/languages/typescript/packages/stack/CHANGELOG.md
index 0df1c0d2a..97e3513e8 100644
--- a/languages/typescript/packages/stack/CHANGELOG.md
+++ b/languages/typescript/packages/stack/CHANGELOG.md
@@ -1,5 +1,42 @@
# @cipherstash/stack
+## 1.3.0
+
+### Minor Changes
+
+- bbb1bfa: `encrypt` on a `types.Json` column now types its result as `Encrypted | null` when the plaintext may be `null`. A `null` document has always resolved to `{ data: null }` at runtime (it is stored as SQL NULL, not encrypted), but the result was typed `Encrypted`, so `result.data.c` compiled and then threw. `encrypt` is now generic over the plaintext's type: a literal `null`, or a value typed `JsonDocument`, gives `data: Encrypted | null`, through `.withLockContext()` and `.audit()` too. A value TypeScript knows is non-null, and every scalar column, still gives `data: Encrypted`. No runtime change. If your code reads `result.data` after encrypting a Json value that may be `null`, it must now check for `null` first. `EncryptOperation` and `EncryptOperationWithLockContext` take an optional result type parameter (default `Encrypted`), and the `EncryptResult ` helper type is exported from `@cipherstash/stack/encryption`.
+
+ `@cipherstash/stack-supabase`: the per-term query-encryption fallback (used when the client has no `bulkEncrypt`) now rejects a `null` envelope instead of sending the string `"null"` as a filter operand, matching the bulk path.
+
+ `stash`: the `stash-encryption` skill now says what `encrypt` returns for a `null` Json document.
+
+### Patch Changes
+
+- fd0ee91: Document how `OidcFederationStrategy` serves many users through one client: the
+ `getJwt` callback runs on every operation, in the caller's async context, and
+ must return the JWT of the user behind the current request, and each user's
+ JWT is exchanged for that user's own token (stack-auth's fix for
+ cipherstash/stack#1045). One caveat is documented for the `@cipherstash/stack`
+ client on the native entry, which calls the strategy from a background task so
+ a request read from `AsyncLocalStorage` is not visible inside `getJwt` there
+ yet: capture the request in the closure or build the client per request until
+ cipherstash/stack#1065 lands. The docs and the `stash-auth`, `stash-encryption`,
+ `stash-supabase` and `stash-edge` skills say so, and say that on earlier
+ `@cipherstash/auth` releases a strategy caches one token for everyone, so a
+ client there must serve one user.
+- 4b14ee6: Fix the README's OIDC federation example, which passed the `Result` from `OidcFederationStrategy.create()` straight to `config.authStrategy` and failed in `Encryption()` with `opts.strategy.getToken is missing`.
+ It now unwraps the `Result` and passes `strategy.data`, and so does the `LockContext` JSDoc example.
+- 20ccab3: Internal type cleanup of the encryption client (no runtime change). The exported operation classes `EncryptOperation`, `EncryptQueryOperation`, `BatchEncryptQueryOperation` and `BulkEncryptOperation` now declare that they accept a `types.Json` document as plaintext, which they always did at runtime; their `getOperation()` return types widen to match. The `EncryptionClient` interface changes only in what `encrypt` returns for a `types.Json` column; see the `EncryptResult` changeset.
+- Updated dependencies [a1765f1]
+- Updated dependencies [fb75f40]
+- Updated dependencies [457b28e]
+- Updated dependencies [b480735]
+- Updated dependencies [fd0ee91]
+- Updated dependencies [457b28e]
+- Updated dependencies [f7f1c76]
+ - @cipherstash/auth@1.0.0
+ - @cipherstash/protect-ffi@0.33.1
+
## 1.2.1
### Patch Changes
diff --git a/languages/typescript/packages/stack/package.json b/languages/typescript/packages/stack/package.json
index 58065dcdc..688fb56a4 100644
--- a/languages/typescript/packages/stack/package.json
+++ b/languages/typescript/packages/stack/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/stack",
- "version": "1.2.1",
+ "version": "1.3.0",
"description": "CipherStash Stack for TypeScript and JavaScript",
"keywords": [
"encrypted",
diff --git a/languages/typescript/packages/test-kit/CHANGELOG.md b/languages/typescript/packages/test-kit/CHANGELOG.md
index c9c255d9f..577994727 100644
--- a/languages/typescript/packages/test-kit/CHANGELOG.md
+++ b/languages/typescript/packages/test-kit/CHANGELOG.md
@@ -1,5 +1,15 @@
# @cipherstash/test-kit
+## 0.0.6
+
+### Patch Changes
+
+- Updated dependencies [fd0ee91]
+- Updated dependencies [4b14ee6]
+- Updated dependencies [bbb1bfa]
+- Updated dependencies [20ccab3]
+ - @cipherstash/stack@1.3.0
+
## 0.0.5
### Patch Changes
diff --git a/languages/typescript/packages/test-kit/package.json b/languages/typescript/packages/test-kit/package.json
index 861eeb10c..2a01044a9 100644
--- a/languages/typescript/packages/test-kit/package.json
+++ b/languages/typescript/packages/test-kit/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/test-kit",
- "version": "0.0.5",
+ "version": "0.0.6",
"private": true,
"description": "Shared EQL v3 test harness: the domain catalog, the plaintext oracle, and the integration-suite driver. Consumed as TypeScript source — no build step.",
"type": "module",
diff --git a/languages/typescript/packages/wizard/CHANGELOG.md b/languages/typescript/packages/wizard/CHANGELOG.md
index 505ac6630..bf1cbb0d5 100644
--- a/languages/typescript/packages/wizard/CHANGELOG.md
+++ b/languages/typescript/packages/wizard/CHANGELOG.md
@@ -1,5 +1,21 @@
# @cipherstash/wizard
+## 1.3.0
+
+### Patch Changes
+
+- 472cbf0: Add `@modelcontextprotocol/sdk` `^1.32.0` as a direct dependency so the
+ auto-installed peer of `@anthropic-ai/claude-agent-sdk` resolves to a release
+ patched against GHSA-6qxp-vccf-f47h / CVE-2026-104850, instead of the vulnerable
+ 1.29.0 previously locked. The wizard never imports the MCP SDK directly — this
+ is a peer-resolution pin only; no API change.
+- Updated dependencies [a1765f1]
+- Updated dependencies [fb75f40]
+- Updated dependencies [457b28e]
+- Updated dependencies [b480735]
+- Updated dependencies [fd0ee91]
+ - @cipherstash/auth@1.0.0
+
## 1.2.1
### Patch Changes
diff --git a/languages/typescript/packages/wizard/package.json b/languages/typescript/packages/wizard/package.json
index 8b83be750..f70284140 100644
--- a/languages/typescript/packages/wizard/package.json
+++ b/languages/typescript/packages/wizard/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/wizard",
- "version": "1.2.1",
+ "version": "1.3.0",
"description": "AI-powered encryption setup for CipherStash. Reads your codebase, picks columns to encrypt, and wires everything up.",
"repository": {
"type": "git",
diff --git a/packages/eql/Cargo.lock b/packages/eql/Cargo.lock
index 1595871a0..5348e3b79 100644
--- a/packages/eql/Cargo.lock
+++ b/packages/eql/Cargo.lock
@@ -1194,7 +1194,7 @@ dependencies = [
[[package]]
name = "eql-bindings"
-version = "3.0.6"
+version = "3.1.0"
dependencies = [
"base64",
"eql-domains",
diff --git a/packages/eql/crates/eql-bindings/Cargo.toml b/packages/eql/crates/eql-bindings/Cargo.toml
index 602ff7851..17a728172 100644
--- a/packages/eql/crates/eql-bindings/Cargo.toml
+++ b/packages/eql/crates/eql-bindings/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "eql-bindings"
-version = "3.0.6"
+version = "3.1.0"
edition = "2021"
description = "Canonical wire types for EQL payloads — single source of truth for Rust, TypeScript (ts-rs), and JSON Schema (schemars)."
# crates.io metadata. `license` is REQUIRED by crates.io — publish fails without
diff --git a/packages/eql/crates/eql-bindings/sql/cipherstash-encrypt.sql b/packages/eql/crates/eql-bindings/sql/cipherstash-encrypt.sql
index 4b40deee5..39584f55a 100644
--- a/packages/eql/crates/eql-bindings/sql/cipherstash-encrypt.sql
+++ b/packages/eql/crates/eql-bindings/sql/cipherstash-encrypt.sql
@@ -48285,18 +48285,34 @@ $$ LANGUAGE plpgsql;
--! is itself `IMMUTABLE STRICT PARALLEL SAFE` — so `IMMUTABLE` lets the
--! planner fold/cache these in ordering and index contexts. NOT `STRICT`:
--! the NULL-handling branches below are load-bearing for the array overload.
+--! @note Constant operation count. Past the NULL and length checks (public
+--! properties), every comparison executes the same statements whatever the
+--! operands: one statement per block, which evaluates both of the block's
+--! comparisons (integer `|`, which unlike `OR` does not short-circuit) and
+--! latches the first differing block with arithmetic rather than an `IF`;
+--! then one `encrypt()`, two `get_bit()` and one `get_byte()`, on every
+--! path including for equal terms. An earlier form ran extra statements on
+--! every block after the first difference and skipped a comparison on each
+--! of them, so its time depended on where the operands first differ, which
+--! is the information the constant-time comparator in ore.rs keeps from
+--! anyone timing a query. Measured on PostgreSQL 17 (aarch64) through
+--! `compare_ore_block_256_terms`, 200 k samples of 16 comparisons, operands
+--! differing in the first block against operands differing only in the
+--! last: the old form gave t = +4.9 and +13.9 (its extra statements and its
+--! skipped comparisons mostly cancel on that machine, which no platform
+--! guarantees; with the short-circuit removed it gave t = +388, about
+--! 0.7 µs per comparison). See `tests/timing/ore_block_256/` for this
+--! form's figures. PL/pgSQL is not a constant-time environment, so C-level
+--! effects remain (`memcmp` within 16 bytes, the offset `substr` reads at),
+--! orders of magnitude below one statement.
CREATE FUNCTION eql_v3_internal.compare_ore_block_256_term(a eql_v3_internal.ore_block_256_term, b eql_v3_internal.ore_block_256_term)
RETURNS integer
IMMUTABLE
SET search_path = pg_catalog, extensions, public
AS $$
DECLARE
- eq boolean := true;
- unequal_block smallint := 0;
- hash_key bytea;
- data_block bytea;
- encrypt_block bytea;
- target_block bytea;
+ ab bytea;
+ bb bytea;
left_block_size CONSTANT smallint := 16;
right_block_size CONSTANT smallint := 32;
@@ -48311,7 +48327,9 @@ AS $$
left_offset integer; -- ordinal offset of the first left block (1 + N PRP bytes)
right_offset integer; -- ordinal start of the right CT (= total left CT length = 17*N)
- indicator smallint := 0;
+ latch integer := 0; -- 1 + the first differing block, 0 while none differs
+ first_diff integer; -- first differing block (0 if the terms are equal)
+ indicator integer;
BEGIN
IF a IS NULL AND b IS NULL THEN
RETURN 0;
@@ -48337,64 +48355,77 @@ AS $$
RAISE EXCEPTION 'Malformed ORE term: % bytes', octet_length(a.bytes);
END IF;
- n := (octet_length(a.bytes) - 16) / 49;
+ ab := a.bytes;
+ bb := b.bytes;
+ n := (octet_length(ab) - 16) / 49;
left_offset := 1 + n; -- left blocks begin right after the N PRP bytes
right_offset := 17 * n; -- right CT begins right after the 17*N-byte left CT
+ -- One statement per block, the same for every block: both comparisons
+ -- always evaluated, and the first differing block latched as block + 1
+ -- while the latch is still 0.
FOR block IN 0..n-1 LOOP
- -- Compare each PRP byte (the first N bytes) and its 16-byte left block.
- IF
- substr(a.bytes, 1 + block, 1) != substr(b.bytes, 1 + block, 1)
- OR substr(a.bytes, left_offset + left_block_size * block, left_block_size) != substr(b.bytes, left_offset + left_block_size * block, left_block_size)
- THEN
- IF eq THEN
- unequal_block := block;
- END IF;
- eq = false;
- END IF;
+ latch := latch + (latch = 0)::int * (block + 1) * (
+ (substr(ab, 1 + block, 1) <> substr(bb, 1 + block, 1))::int
+ | (substr(ab, left_offset + left_block_size * block, left_block_size)
+ <> substr(bb, left_offset + left_block_size * block, left_block_size))::int);
END LOOP;
- IF eq THEN
- RETURN 0::integer;
- END IF;
-
- -- Hash key is the IV from the right CT of b.
- hash_key := substr(b.bytes, right_offset + 1, 16);
-
- -- First right block is at right_offset + nonce_size (ordinally indexed).
- target_block := substr(b.bytes, right_offset + 17 + (unequal_block * right_block_size), right_block_size);
-
- data_block := substr(a.bytes, left_offset + (left_block_size * unequal_block), left_block_size);
-
- encrypt_block := encrypt(data_block::bytea, hash_key::bytea, 'aes-ecb');
+ -- Equal terms read block 0.
+ first_diff := greatest(latch - 1, 0);
+ -- One hash and one bit read on every path. For equal terms this reads
+ -- block 0 and the result is discarded below. The hash key is the IV at
+ -- the start of b's right CT; the right blocks follow it.
indicator := (
get_bit(
- encrypt_block,
- 0
- ) + get_bit(target_block, get_byte(a.bytes, unequal_block))) % 2;
-
- IF indicator = 1 THEN
- RETURN 1::integer;
- ELSE
- RETURN -1::integer;
- END IF;
+ encrypt(
+ substr(ab, left_offset + left_block_size * first_diff, left_block_size),
+ substr(bb, right_offset + 1, 16),
+ 'aes-ecb'),
+ 0)
+ + get_bit(
+ substr(bb, right_offset + 17 + right_block_size * first_diff, right_block_size),
+ get_byte(ab, first_diff))) % 2;
+
+ -- 0 when equal, otherwise +1 / -1 from the indicator; no branch on the data.
+ RETURN sign(latch) * (2 * indicator - 1);
END;
$$ LANGUAGE plpgsql;
---! @brief Compare arrays of ORE block terms recursively
+--! @brief Compare arrays of ORE block terms lexicographically
--! @internal
--! @param a eql_v3_internal.ore_block_256_term[] First array
--! @param b eql_v3_internal.ore_block_256_term[] Second array
--! @return integer -1/0/1, or NULL if either array is NULL
+--! @throws Exception if paired terms are different lengths, or any paired
+--! term is malformed
+--! @note Single-term values (every type but text) go straight to
+--! `compare_ore_block_256_term`. Multi-term values (text, up to six terms)
+--! are compared with a constant operation count across the terms. A
+--! term-by-term comparison that stops at the first unequal term takes time
+--! proportional to how many leading terms are equal, which reveals the
+--! length of the shared prefix: measured on PostgreSQL 17, six-term values
+--! took about 11 µs when the first terms differ and 47 µs when only the
+--! sixth does (t below -1500). Instead, every paired term is compared whole
+--! in one statement (its PRP bytes and left blocks, which are deterministic)
+--! and the first unequal one is latched with arithmetic. Then exactly one
+--! call to `compare_ore_block_256_term` runs on every path: on the latched
+--! pair, or, when every paired term is equal or the latched pair has a NULL
+--! element, on a pair whose result is discarded. The result is selected
+--! with arithmetic, so neither a shared prefix of different-length arrays
+--! nor a NULL element after equal terms skips the hash. Term counts and the
+--! positions of NULL elements are public structure and may branch. If every
+--! paired term is equal, the shorter array sorts first. C-level effects
+--! remain: the whole-term `memcmp` stops at the first differing byte, a
+--! few nanoseconds over at most a few hundred bytes. See
+--! `tests/timing/ore_block_256/`.
CREATE FUNCTION eql_v3_internal.compare_ore_block_256_terms(a eql_v3_internal.ore_block_256_term[], b eql_v3_internal.ore_block_256_term[])
RETURNS integer
IMMUTABLE
SET search_path = pg_catalog, extensions, public
AS $$
- DECLARE
- cmp_result integer;
BEGIN
IF a IS NULL OR b IS NULL THEN
RETURN NULL;
@@ -48412,13 +48443,73 @@ AS $$
RETURN 1;
END IF;
- cmp_result := eql_v3_internal.compare_ore_block_256_term(a[1], b[1]);
-
- IF cmp_result = 0 THEN
- RETURN eql_v3_internal.compare_ore_block_256_terms(a[2:array_length(a,1)], b[2:array_length(b,1)]);
+ IF cardinality(a) = 1 AND cardinality(b) = 1 THEN
+ RETURN eql_v3_internal.compare_ore_block_256_term(a[1], b[1]);
END IF;
- RETURN cmp_result;
+ -- Multi-term path. Its variables are declared in this inner block, not at
+ -- the top: PL/pgSQL evaluates every initialised declaration on each call,
+ -- and the single-term path above is the operator-class hot path.
+ DECLARE
+ ab bytea;
+ bb bytea;
+ differs integer; -- 1 if the current pair differs
+ null_order integer; -- the current pair's order if it has a NULL element, else 0
+
+ latch integer := 0; -- the first unequal pair's index, 0 while every pair is equal
+ first_null integer := 0; -- the latched pair's NULL ordering, 0 if it has none
+ spare integer := 0; -- the first pair with no NULL element (structure), 0 if none
+ h integer; -- the pair compare_ore_block_256_term runs on
+ r integer;
+ BEGIN
+ FOR t IN 1..least(cardinality(a), cardinality(b)) LOOP
+ ab := a[t].bytes;
+ bb := b[t].bytes;
+
+ -- The length check comes first, as in compare_ore_block_256_term, so
+ -- both paths report the same error for the same pair.
+ IF octet_length(ab) != octet_length(bb) THEN
+ RAISE EXCEPTION 'Ciphertexts are different lengths';
+ END IF;
+ IF ab IS NOT NULL AND (octet_length(ab) <= 16 OR (octet_length(ab) - 16) % 49 != 0) THEN
+ RAISE EXCEPTION 'Malformed ORE term: % bytes', octet_length(ab);
+ END IF;
+ IF bb IS NOT NULL AND (octet_length(bb) <= 16 OR (octet_length(bb) - 16) % 49 != 0) THEN
+ RAISE EXCEPTION 'Malformed ORE term: % bytes', octet_length(bb);
+ END IF;
+
+ -- NULL elements are structure, not data: they may branch.
+ IF ab IS NULL OR bb IS NULL THEN
+ differs := ((ab IS NULL) <> (bb IS NULL))::int;
+ null_order := (bb IS NULL)::int - (ab IS NULL)::int;
+ ELSE
+ -- The PRP bytes and left blocks: the first 17*N bytes.
+ differs := (substr(ab, 1, 17 * ((octet_length(ab) - 16) / 49))
+ <> substr(bb, 1, 17 * ((octet_length(ab) - 16) / 49)))::int;
+ null_order := 0;
+ IF spare = 0 THEN
+ spare := t;
+ END IF;
+ END IF;
+
+ -- Latch the first unequal pair without a branch.
+ first_null := first_null + (latch = 0)::int * differs * null_order;
+ latch := latch + (latch = 0)::int * differs * t;
+ END LOOP;
+
+ -- One term comparison on every path. It runs on the latched pair, or on
+ -- pair 1 when every pair is equal; if that pair has a NULL element, on
+ -- the first pair with none. Only when no pair is free of NULLs (which
+ -- is structure) does it take its NULL branch.
+ h := greatest(latch, 1);
+ h := h + ((a[h] IS NULL)::int | (b[h] IS NULL)::int) * (spare - h);
+ r := eql_v3_internal.compare_ore_block_256_term(a[h], b[h]);
+
+ -- Every pair equal: the shorter array sorts first. Otherwise the
+ -- latched pair's NULL ordering, or the term comparison.
+ RETURN (latch = 0)::int * sign(cardinality(a) - cardinality(b))::integer
+ + (latch > 0)::int * (first_null + (first_null = 0)::int * r);
+ END;
END
$$ LANGUAGE plpgsql;
@@ -59469,7 +59560,7 @@ DROP FUNCTION IF EXISTS eql_v3.version();
--! @brief EQL version reporting (self-contained eql_v3 surface)
--!
--! This file is auto-generated from src/v3/version.template during build.
---! The 3.0.6 placeholder is replaced with the actual release
+--! The 3.1.0 placeholder is replaced with the actual release
--! version (bare semver, e.g. "3.0.0") supplied via `mise run build --version`,
--! or "DEV" for development builds.
@@ -59488,14 +59579,14 @@ CREATE FUNCTION eql_v3.version()
RETURNS text
IMMUTABLE STRICT PARALLEL SAFE
AS $$
- SELECT '3.0.6';
+ SELECT '3.1.0';
$$ LANGUAGE SQL;
--! @brief Schema-level version marker for obj_description() discoverability
--!
--! Mirrors eql_v3.version() as a comment on the schema so the installed
--! version can also be read via obj_description('eql_v3'::regnamespace).
-COMMENT ON SCHEMA eql_v3 IS '3.0.6';
+COMMENT ON SCHEMA eql_v3 IS '3.1.0';
--! @file pin_search_path_v3.sql
--! @brief Post-install: pin search_path on every eql_v3.* function.
--!
diff --git a/packages/eql/crates/eql-bindings/sql/release-manifest.json b/packages/eql/crates/eql-bindings/sql/release-manifest.json
index cd386a01c..f527e842c 100644
--- a/packages/eql/crates/eql-bindings/sql/release-manifest.json
+++ b/packages/eql/crates/eql-bindings/sql/release-manifest.json
@@ -1,6 +1,6 @@
{
- "eqlVersion": "3.0.6",
+ "eqlVersion": "3.1.0",
"schemaVersion": 3,
- "installSqlSha256": "9b6dab789248f911707f7c1c468c5d455987b47aac4a30fc421bc813073fffb1",
+ "installSqlSha256": "c9404de3dccd6702c7addf9fa748402e7de2ac11fb2b0f6a751d7ec3813d870a",
"uninstallSqlSha256": "b1b5131b8175c5d04da9ada108d25c81c5772b15fad79a6c419ebb32d18c60a9"
}
diff --git a/packages/eql/packages/eql/CHANGELOG.md b/packages/eql/packages/eql/CHANGELOG.md
index 6f0c535d2..3f9e8175e 100644
--- a/packages/eql/packages/eql/CHANGELOG.md
+++ b/packages/eql/packages/eql/CHANGELOG.md
@@ -1,5 +1,29 @@
# @cipherstash/eql
+## 3.1.0
+
+### Minor Changes
+
+- e930e5b: Security: the ORE block comparator (`eql_v3_internal.compare_ore_block_256_term`
+ and `compare_ore_block_256_terms`, behind every ordered ORE operator and the ORE
+ operator class) now runs a constant number of operations for operands of the
+ same shape (term count, term width, NULL positions), whether and wherever they
+ differ. Previously its run time depended on where two values first differ, so a
+ party able to time queries could learn how long a prefix their query value
+ shares with stored values. For text the effect was large: a comparison took
+ about 11 µs when the first term differs and 47 µs when only the sixth does.
+ Results are unchanged. Single-term comparisons cost about 6% more when the
+ terms differ and about 36% more (roughly 1 µs) when they are equal, since equal
+ terms now run the hash too; text comparisons now take about 18 µs whatever the
+ operands. Multi-term (text) values now check every paired term, so a
+ malformed or mismatched-width term after the first difference, or one paired
+ with a NULL element, raises instead of being skipped; see
+ [U-001](../../docs/upgrading/v3.1.md#u-001-block-ore-text-comparisons-check-every-term)
+ for how to find such rows before upgrading. A timing harness is in
+ `packages/eql/tests/timing/ore_block_256/`.
+- 0275684: **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. `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.
+- c851db5: **Rust `TextEq` and `TextEqQuery` support Stack Encrypt's target-directed API behind the `stack-encrypt` feature.** The stored table and column identifier supplies the encryption context; native ciphertext and equality terms are transcoded into EQL payloads while Vitamin C owns plaintext encoding. This is a new producer profile with exact string equality, independent of existing cipherstash-client ciphertext and terms. Query operands carry no recoverable ciphertext.
+
## 3.0.6
### Patch Changes
diff --git a/packages/eql/packages/eql/package.json b/packages/eql/packages/eql/package.json
index ae87ef23b..f3be39a88 100644
--- a/packages/eql/packages/eql/package.json
+++ b/packages/eql/packages/eql/package.json
@@ -1,6 +1,6 @@
{
"name": "@cipherstash/eql",
- "version": "3.0.6",
+ "version": "3.1.0",
"description": "Canonical EQL v3 wire types, JSON schemas, and SQL bundle.",
"keywords": [
"eql",
diff --git a/packages/eql/packages/eql/sql/cipherstash-encrypt.sql b/packages/eql/packages/eql/sql/cipherstash-encrypt.sql
index 4b40deee5..39584f55a 100644
--- a/packages/eql/packages/eql/sql/cipherstash-encrypt.sql
+++ b/packages/eql/packages/eql/sql/cipherstash-encrypt.sql
@@ -48285,18 +48285,34 @@ $$ LANGUAGE plpgsql;
--! is itself `IMMUTABLE STRICT PARALLEL SAFE` — so `IMMUTABLE` lets the
--! planner fold/cache these in ordering and index contexts. NOT `STRICT`:
--! the NULL-handling branches below are load-bearing for the array overload.
+--! @note Constant operation count. Past the NULL and length checks (public
+--! properties), every comparison executes the same statements whatever the
+--! operands: one statement per block, which evaluates both of the block's
+--! comparisons (integer `|`, which unlike `OR` does not short-circuit) and
+--! latches the first differing block with arithmetic rather than an `IF`;
+--! then one `encrypt()`, two `get_bit()` and one `get_byte()`, on every
+--! path including for equal terms. An earlier form ran extra statements on
+--! every block after the first difference and skipped a comparison on each
+--! of them, so its time depended on where the operands first differ, which
+--! is the information the constant-time comparator in ore.rs keeps from
+--! anyone timing a query. Measured on PostgreSQL 17 (aarch64) through
+--! `compare_ore_block_256_terms`, 200 k samples of 16 comparisons, operands
+--! differing in the first block against operands differing only in the
+--! last: the old form gave t = +4.9 and +13.9 (its extra statements and its
+--! skipped comparisons mostly cancel on that machine, which no platform
+--! guarantees; with the short-circuit removed it gave t = +388, about
+--! 0.7 µs per comparison). See `tests/timing/ore_block_256/` for this
+--! form's figures. PL/pgSQL is not a constant-time environment, so C-level
+--! effects remain (`memcmp` within 16 bytes, the offset `substr` reads at),
+--! orders of magnitude below one statement.
CREATE FUNCTION eql_v3_internal.compare_ore_block_256_term(a eql_v3_internal.ore_block_256_term, b eql_v3_internal.ore_block_256_term)
RETURNS integer
IMMUTABLE
SET search_path = pg_catalog, extensions, public
AS $$
DECLARE
- eq boolean := true;
- unequal_block smallint := 0;
- hash_key bytea;
- data_block bytea;
- encrypt_block bytea;
- target_block bytea;
+ ab bytea;
+ bb bytea;
left_block_size CONSTANT smallint := 16;
right_block_size CONSTANT smallint := 32;
@@ -48311,7 +48327,9 @@ AS $$
left_offset integer; -- ordinal offset of the first left block (1 + N PRP bytes)
right_offset integer; -- ordinal start of the right CT (= total left CT length = 17*N)
- indicator smallint := 0;
+ latch integer := 0; -- 1 + the first differing block, 0 while none differs
+ first_diff integer; -- first differing block (0 if the terms are equal)
+ indicator integer;
BEGIN
IF a IS NULL AND b IS NULL THEN
RETURN 0;
@@ -48337,64 +48355,77 @@ AS $$
RAISE EXCEPTION 'Malformed ORE term: % bytes', octet_length(a.bytes);
END IF;
- n := (octet_length(a.bytes) - 16) / 49;
+ ab := a.bytes;
+ bb := b.bytes;
+ n := (octet_length(ab) - 16) / 49;
left_offset := 1 + n; -- left blocks begin right after the N PRP bytes
right_offset := 17 * n; -- right CT begins right after the 17*N-byte left CT
+ -- One statement per block, the same for every block: both comparisons
+ -- always evaluated, and the first differing block latched as block + 1
+ -- while the latch is still 0.
FOR block IN 0..n-1 LOOP
- -- Compare each PRP byte (the first N bytes) and its 16-byte left block.
- IF
- substr(a.bytes, 1 + block, 1) != substr(b.bytes, 1 + block, 1)
- OR substr(a.bytes, left_offset + left_block_size * block, left_block_size) != substr(b.bytes, left_offset + left_block_size * block, left_block_size)
- THEN
- IF eq THEN
- unequal_block := block;
- END IF;
- eq = false;
- END IF;
+ latch := latch + (latch = 0)::int * (block + 1) * (
+ (substr(ab, 1 + block, 1) <> substr(bb, 1 + block, 1))::int
+ | (substr(ab, left_offset + left_block_size * block, left_block_size)
+ <> substr(bb, left_offset + left_block_size * block, left_block_size))::int);
END LOOP;
- IF eq THEN
- RETURN 0::integer;
- END IF;
-
- -- Hash key is the IV from the right CT of b.
- hash_key := substr(b.bytes, right_offset + 1, 16);
-
- -- First right block is at right_offset + nonce_size (ordinally indexed).
- target_block := substr(b.bytes, right_offset + 17 + (unequal_block * right_block_size), right_block_size);
-
- data_block := substr(a.bytes, left_offset + (left_block_size * unequal_block), left_block_size);
-
- encrypt_block := encrypt(data_block::bytea, hash_key::bytea, 'aes-ecb');
+ -- Equal terms read block 0.
+ first_diff := greatest(latch - 1, 0);
+ -- One hash and one bit read on every path. For equal terms this reads
+ -- block 0 and the result is discarded below. The hash key is the IV at
+ -- the start of b's right CT; the right blocks follow it.
indicator := (
get_bit(
- encrypt_block,
- 0
- ) + get_bit(target_block, get_byte(a.bytes, unequal_block))) % 2;
-
- IF indicator = 1 THEN
- RETURN 1::integer;
- ELSE
- RETURN -1::integer;
- END IF;
+ encrypt(
+ substr(ab, left_offset + left_block_size * first_diff, left_block_size),
+ substr(bb, right_offset + 1, 16),
+ 'aes-ecb'),
+ 0)
+ + get_bit(
+ substr(bb, right_offset + 17 + right_block_size * first_diff, right_block_size),
+ get_byte(ab, first_diff))) % 2;
+
+ -- 0 when equal, otherwise +1 / -1 from the indicator; no branch on the data.
+ RETURN sign(latch) * (2 * indicator - 1);
END;
$$ LANGUAGE plpgsql;
---! @brief Compare arrays of ORE block terms recursively
+--! @brief Compare arrays of ORE block terms lexicographically
--! @internal
--! @param a eql_v3_internal.ore_block_256_term[] First array
--! @param b eql_v3_internal.ore_block_256_term[] Second array
--! @return integer -1/0/1, or NULL if either array is NULL
+--! @throws Exception if paired terms are different lengths, or any paired
+--! term is malformed
+--! @note Single-term values (every type but text) go straight to
+--! `compare_ore_block_256_term`. Multi-term values (text, up to six terms)
+--! are compared with a constant operation count across the terms. A
+--! term-by-term comparison that stops at the first unequal term takes time
+--! proportional to how many leading terms are equal, which reveals the
+--! length of the shared prefix: measured on PostgreSQL 17, six-term values
+--! took about 11 µs when the first terms differ and 47 µs when only the
+--! sixth does (t below -1500). Instead, every paired term is compared whole
+--! in one statement (its PRP bytes and left blocks, which are deterministic)
+--! and the first unequal one is latched with arithmetic. Then exactly one
+--! call to `compare_ore_block_256_term` runs on every path: on the latched
+--! pair, or, when every paired term is equal or the latched pair has a NULL
+--! element, on a pair whose result is discarded. The result is selected
+--! with arithmetic, so neither a shared prefix of different-length arrays
+--! nor a NULL element after equal terms skips the hash. Term counts and the
+--! positions of NULL elements are public structure and may branch. If every
+--! paired term is equal, the shorter array sorts first. C-level effects
+--! remain: the whole-term `memcmp` stops at the first differing byte, a
+--! few nanoseconds over at most a few hundred bytes. See
+--! `tests/timing/ore_block_256/`.
CREATE FUNCTION eql_v3_internal.compare_ore_block_256_terms(a eql_v3_internal.ore_block_256_term[], b eql_v3_internal.ore_block_256_term[])
RETURNS integer
IMMUTABLE
SET search_path = pg_catalog, extensions, public
AS $$
- DECLARE
- cmp_result integer;
BEGIN
IF a IS NULL OR b IS NULL THEN
RETURN NULL;
@@ -48412,13 +48443,73 @@ AS $$
RETURN 1;
END IF;
- cmp_result := eql_v3_internal.compare_ore_block_256_term(a[1], b[1]);
-
- IF cmp_result = 0 THEN
- RETURN eql_v3_internal.compare_ore_block_256_terms(a[2:array_length(a,1)], b[2:array_length(b,1)]);
+ IF cardinality(a) = 1 AND cardinality(b) = 1 THEN
+ RETURN eql_v3_internal.compare_ore_block_256_term(a[1], b[1]);
END IF;
- RETURN cmp_result;
+ -- Multi-term path. Its variables are declared in this inner block, not at
+ -- the top: PL/pgSQL evaluates every initialised declaration on each call,
+ -- and the single-term path above is the operator-class hot path.
+ DECLARE
+ ab bytea;
+ bb bytea;
+ differs integer; -- 1 if the current pair differs
+ null_order integer; -- the current pair's order if it has a NULL element, else 0
+
+ latch integer := 0; -- the first unequal pair's index, 0 while every pair is equal
+ first_null integer := 0; -- the latched pair's NULL ordering, 0 if it has none
+ spare integer := 0; -- the first pair with no NULL element (structure), 0 if none
+ h integer; -- the pair compare_ore_block_256_term runs on
+ r integer;
+ BEGIN
+ FOR t IN 1..least(cardinality(a), cardinality(b)) LOOP
+ ab := a[t].bytes;
+ bb := b[t].bytes;
+
+ -- The length check comes first, as in compare_ore_block_256_term, so
+ -- both paths report the same error for the same pair.
+ IF octet_length(ab) != octet_length(bb) THEN
+ RAISE EXCEPTION 'Ciphertexts are different lengths';
+ END IF;
+ IF ab IS NOT NULL AND (octet_length(ab) <= 16 OR (octet_length(ab) - 16) % 49 != 0) THEN
+ RAISE EXCEPTION 'Malformed ORE term: % bytes', octet_length(ab);
+ END IF;
+ IF bb IS NOT NULL AND (octet_length(bb) <= 16 OR (octet_length(bb) - 16) % 49 != 0) THEN
+ RAISE EXCEPTION 'Malformed ORE term: % bytes', octet_length(bb);
+ END IF;
+
+ -- NULL elements are structure, not data: they may branch.
+ IF ab IS NULL OR bb IS NULL THEN
+ differs := ((ab IS NULL) <> (bb IS NULL))::int;
+ null_order := (bb IS NULL)::int - (ab IS NULL)::int;
+ ELSE
+ -- The PRP bytes and left blocks: the first 17*N bytes.
+ differs := (substr(ab, 1, 17 * ((octet_length(ab) - 16) / 49))
+ <> substr(bb, 1, 17 * ((octet_length(ab) - 16) / 49)))::int;
+ null_order := 0;
+ IF spare = 0 THEN
+ spare := t;
+ END IF;
+ END IF;
+
+ -- Latch the first unequal pair without a branch.
+ first_null := first_null + (latch = 0)::int * differs * null_order;
+ latch := latch + (latch = 0)::int * differs * t;
+ END LOOP;
+
+ -- One term comparison on every path. It runs on the latched pair, or on
+ -- pair 1 when every pair is equal; if that pair has a NULL element, on
+ -- the first pair with none. Only when no pair is free of NULLs (which
+ -- is structure) does it take its NULL branch.
+ h := greatest(latch, 1);
+ h := h + ((a[h] IS NULL)::int | (b[h] IS NULL)::int) * (spare - h);
+ r := eql_v3_internal.compare_ore_block_256_term(a[h], b[h]);
+
+ -- Every pair equal: the shorter array sorts first. Otherwise the
+ -- latched pair's NULL ordering, or the term comparison.
+ RETURN (latch = 0)::int * sign(cardinality(a) - cardinality(b))::integer
+ + (latch > 0)::int * (first_null + (first_null = 0)::int * r);
+ END;
END
$$ LANGUAGE plpgsql;
@@ -59469,7 +59560,7 @@ DROP FUNCTION IF EXISTS eql_v3.version();
--! @brief EQL version reporting (self-contained eql_v3 surface)
--!
--! This file is auto-generated from src/v3/version.template during build.
---! The 3.0.6 placeholder is replaced with the actual release
+--! The 3.1.0 placeholder is replaced with the actual release
--! version (bare semver, e.g. "3.0.0") supplied via `mise run build --version`,
--! or "DEV" for development builds.
@@ -59488,14 +59579,14 @@ CREATE FUNCTION eql_v3.version()
RETURNS text
IMMUTABLE STRICT PARALLEL SAFE
AS $$
- SELECT '3.0.6';
+ SELECT '3.1.0';
$$ LANGUAGE SQL;
--! @brief Schema-level version marker for obj_description() discoverability
--!
--! Mirrors eql_v3.version() as a comment on the schema so the installed
--! version can also be read via obj_description('eql_v3'::regnamespace).
-COMMENT ON SCHEMA eql_v3 IS '3.0.6';
+COMMENT ON SCHEMA eql_v3 IS '3.1.0';
--! @file pin_search_path_v3.sql
--! @brief Post-install: pin search_path on every eql_v3.* function.
--!
diff --git a/packages/eql/packages/eql/sql/release-manifest.json b/packages/eql/packages/eql/sql/release-manifest.json
index cd386a01c..f527e842c 100644
--- a/packages/eql/packages/eql/sql/release-manifest.json
+++ b/packages/eql/packages/eql/sql/release-manifest.json
@@ -1,6 +1,6 @@
{
- "eqlVersion": "3.0.6",
+ "eqlVersion": "3.1.0",
"schemaVersion": 3,
- "installSqlSha256": "9b6dab789248f911707f7c1c468c5d455987b47aac4a30fc421bc813073fffb1",
+ "installSqlSha256": "c9404de3dccd6702c7addf9fa748402e7de2ac11fb2b0f6a751d7ec3813d870a",
"uninstallSqlSha256": "b1b5131b8175c5d04da9ada108d25c81c5772b15fad79a6c419ebb32d18c60a9"
}
diff --git a/packages/eql/packages/eql/src/generated/release-manifest.ts b/packages/eql/packages/eql/src/generated/release-manifest.ts
index 30e8cd835..8a75afd96 100644
--- a/packages/eql/packages/eql/src/generated/release-manifest.ts
+++ b/packages/eql/packages/eql/src/generated/release-manifest.ts
@@ -1,6 +1,6 @@
export const releaseManifest = {
- eqlVersion: '3.0.6',
+ eqlVersion: '3.1.0',
schemaVersion: 3,
- installSqlSha256: '9b6dab789248f911707f7c1c468c5d455987b47aac4a30fc421bc813073fffb1',
+ installSqlSha256: 'c9404de3dccd6702c7addf9fa748402e7de2ac11fb2b0f6a751d7ec3813d870a',
uninstallSqlSha256: 'b1b5131b8175c5d04da9ada108d25c81c5772b15fad79a6c419ebb32d18c60a9',
} as const
diff --git a/skills/stash-cli/SKILL.md b/skills/stash-cli/SKILL.md
index f45c156bf..159a99f2e 100644
--- a/skills/stash-cli/SKILL.md
+++ b/skills/stash-cli/SKILL.md
@@ -396,7 +396,7 @@ Gets a project from zero to a direct EQL v3 install. It loads an existing `stash
The removed `--eql-version`, `--latest`, `--drizzle`, `--migration`, `--direct`, `--migrations-dir`, and `--exclude-operator-family` options fail clearly instead of being ignored. A request for EQL v2 points dump-recovery users to the upstream EQL 2.3.1 SQL release. New installs are EQL v3 only; its pinned bundle self-adapts when a database role cannot create the optional operator family.
-**`--database-url` is a one-shot.** It installs against that database and leaves the project untouched — no config is loaded, and none is scaffolded, nor is an encryption client. This lets `npx --package=stash@1.2.1 stash eql install --database-url 'postgres://...'` run in a bare project with no CipherStash dependencies while pinning the CLI to this skill's release. It also means the flag always wins: loading a config could pick up a parent-directory `databaseUrl` literal and install against the wrong database.
+**`--database-url` is a one-shot.** It installs against that database and leaves the project untouched — no config is loaded, and none is scaffolded, nor is an encryption client. This lets `npx --package=stash@1.3.0 stash eql install --database-url 'postgres://...'` run in a bare project with no CipherStash dependencies while pinning the CLI to this skill's release. It also means the flag always wins: loading a config could pick up a parent-directory `databaseUrl` literal and install against the wrong database.
**The install verifies the bundle before it runs it.** Before connecting, `eql install` hashes the SQL it resolved from `@cipherstash/eql` and compares it against the `installSqlSha256` that release attests to. A mismatch refuses — naming both digests and the resolved path — rather than executing SQL the version number does not vouch for; nothing is opened and nothing is sent. In practice this only fires on a corrupt or tampered `node_modules`, since the bundle and its manifest are emitted by the same build.
diff --git a/skills/stash-edge/SKILL.md b/skills/stash-edge/SKILL.md
index 9ab0f20df..d3b262712 100644
--- a/skills/stash-edge/SKILL.md
+++ b/skills/stash-edge/SKILL.md
@@ -76,7 +76,7 @@ build step.
```ts
import {
Encryption, encryptedTable, types, isEncrypted,
-} from 'npm:@cipherstash/stack@1.2.1/wasm-inline'
+} from 'npm:@cipherstash/stack@1.3.0/wasm-inline'
```
**Pin an exact version.** Deno caches by specifier, so an unpinned import
@@ -91,7 +91,7 @@ name everywhere:
```jsonc
{
"imports": {
- "@cipherstash/stack/wasm-inline": "npm:@cipherstash/stack@1.2.1/wasm-inline"
+ "@cipherstash/stack/wasm-inline": "npm:@cipherstash/stack@1.3.0/wasm-inline"
}
}
```