diff --git a/.changeset/adapter-split-skills.md b/.changeset/adapter-split-skills.md
index 772eeb21d..06806839d 100644
--- a/.changeset/adapter-split-skills.md
+++ b/.changeset/adapter-split-skills.md
@@ -5,7 +5,7 @@
Update the bundled `stash-drizzle`, `stash-supabase`, and `stash-encryption` agent
skills (and the stack README / Supabase reference doc) for the adapter package
split: the Drizzle and Supabase integrations import from `@cipherstash/stack-drizzle`
-(+ `/v3`) and `@cipherstash/stack-supabase` respectively, installed alongside
+and `@cipherstash/stack-supabase` respectively, installed alongside
`@cipherstash/stack`, rather than from `@cipherstash/stack/{drizzle,supabase,eql/v3/drizzle}`
subpaths. Skills ship inside the `stash` tarball, so the stale import paths would
otherwise become wrong guidance in a user's project.
diff --git a/.changeset/cli-eql-v3-single-bundle.md b/.changeset/cli-eql-v3-single-bundle.md
index 25db56023..438b5e91c 100644
--- a/.changeset/cli-eql-v3-single-bundle.md
+++ b/.changeset/cli-eql-v3-single-bundle.md
@@ -2,7 +2,7 @@
'stash': minor
---
-`stash eql install --eql-version 3` now installs the eql-3.0.0 GA bundle,
+`stash eql install` now installs the eql-3.0.0 GA bundle,
vendored from the pinned `@cipherstash/eql` package (sha256-verified).
Since eql-3.0.0 one artifact installs everywhere: the operator-class
diff --git a/.changeset/cli-v2-cutover-prompt-correction.md b/.changeset/cli-v2-cutover-prompt-correction.md
deleted file mode 100644
index 4a90e2f6b..000000000
--- a/.changeset/cli-v2-cutover-prompt-correction.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-'stash': patch
----
-
-`stash init`'s setup prompt no longer tells agents to declare an EQL v2 cutover
-column with a `types.*` domain.
-
-The `types.*` factories are EQL v3 only — a v2 column is an `eql_v2_encrypted`
-composite — so an agent following the old step-4 guidance would author a schema
-that cannot describe the column it just cut over to. The prompt now says
-explicitly not to use `types.*` for a v2 column, and points at the deprecated
-`@cipherstash/stack/schema` builders with decryption through
-`@cipherstash/stack`, which is the actual read path for legacy v2 rows.
-
-Superseded later in this release: `stash encrypt cutover` and all v2 mutation guidance are removed; legacy v2 remains read-only.
diff --git a/.changeset/decrypt-chaining-docs.md b/.changeset/decrypt-chaining-docs.md
index 36afc506b..2b7d6a230 100644
--- a/.changeset/decrypt-chaining-docs.md
+++ b/.changeset/decrypt-chaining-docs.md
@@ -49,15 +49,11 @@ other two to guess:
object-shorthand that read as three required strings — `queryType` is inferred
from the column's configured indexes.
-The cutover and complete-rollout **plan templates** now split EQL v3 from v2.
-Both described the v2 rename swap (`
` → `_plaintext`, twin → ``)
-as the only cutover path, so on the default v3 install `stash plan` drafted a
-plan built around `stash encrypt cutover` — a command that refuses v3 columns
-outright ("there is no rename step") and refuses entirely on a v3-only
-database, where `eql_v2_configuration` does not exist. The implement prompt
-already carried this split; the plan templates did not. The version is
-per-column, so the templates tell the agent to establish it per column rather
-than deciding once for the whole plan.
+The cutover and complete-rollout **plan templates** now describe the EQL v3
+rollout. Both described a rename swap (`` → `_plaintext`, twin →
+``) as the only cutover path, which EQL v3 does not have — the application
+switches to the encrypted column by name. The implement prompt already carried
+the v3 story; the plan templates did not.
The "already encrypted" stop-and-ask now recognises `eql_v3_*` domains
alongside the legacy `eql_v2_encrypted` udt, so it can fire on the default
diff --git a/.changeset/encrypt-client-guard-parity.md b/.changeset/encrypt-client-guard-parity.md
index 63c3a2664..68578a39b 100644
--- a/.changeset/encrypt-client-guard-parity.md
+++ b/.changeset/encrypt-client-guard-parity.md
@@ -6,9 +6,9 @@
initialized encrypt config, instead of reporting a missing table.
The guard that refuses an unusable client file existed twice — once in
-`loadEncryptConfig` (`stash db push` / `db validate`) and once, hand-copied, in
+`loadEncryptConfig` (`stash db validate`) and once, hand-copied, in
`loadEncryptionContext` (`stash encrypt backfill`). The copies had already
-drifted: for a client whose `getEncryptConfig()` returns nothing, `db push`
+drifted: for a client whose `getEncryptConfig()` returns nothing, `db validate`
exited 1 with `Encryption client in has no initialized encrypt config`,
while `encrypt backfill` fell through to `Table "users" was not found in the
encryption client exports. Available: (none)` — naming the symptom rather than
diff --git a/.changeset/encrypt-lifecycle-mixed-table.md b/.changeset/encrypt-lifecycle-mixed-table.md
deleted file mode 100644
index f6b6273bd..000000000
--- a/.changeset/encrypt-lifecycle-mixed-table.md
+++ /dev/null
@@ -1,61 +0,0 @@
----
-'stash': patch
----
-
-`stash encrypt cutover` and `stash encrypt drop` no longer act on — or report
-success for — an encrypted column they only guessed at.
-
-On a table holding both a legacy EQL v2 pair (`ssn` / `ssn_encrypted`) and one
-unrelated EQL v3 column, the v2 ciphertext column is not classified as an EQL
-column at all, so column resolution fell through to the "this is the table's
-only EQL column" rule and claimed the unrelated v3 column. Three consequences,
-all now fixed:
-
-- **`cutover` reported success for work it never did.** Its EQL v3 branch had no
- guard on how the column was resolved, and returned without setting an exit
- code — so it printed "point your application at `email_enc`" and exited 0
- while the v2 rename never ran. A scripted rollout read that as complete. It
- now refuses, and exits 1, exactly as `drop` already did.
-
-- **The recorded pairing was discarded.** `encrypt backfill` writes the true
- `encryptedColumn` to `.cipherstash/migrations.json`, so the answer was already
- on disk — but a hint that failed to resolve was dropped entirely and the
- re-resolution reached the guess. A hint naming a column that still exists but
- is not an EQL v3 column is now reported as what it is (most often a legacy
- `eql_v2_encrypted` counterpart) instead of being replaced by a guess. A
- genuinely stale hint — one naming a column that is gone — still falls back to
- the naming convention as before.
-
-- **`drop`'s refusal message prescribed the guess.** It told the user to re-run
- `backfill --encrypted-column `. Following it recorded the
- guess as fact, so the next run resolved "by hint", walked past the refusal,
- and passed the coverage check vacuously — an unrelated but legitimately
- backfilled column is non-NULL on every row — then generated a live
- `DROP COLUMN` on the plaintext and exited 0. The message now asks for the
- column that actually encrypts the named one, and says explicitly not to record
- the guess.
-
-The `unresolvedHint` refusal is scoped to tables that actually hold EQL v3
-columns a guess could wrongly claim. A **pure-v2** table has none, so it still
-falls through to the EQL v2 lifecycle exactly as before — including when
-`encrypt backfill` recorded an `encryptedColumn` for it, which it does for v2
-columns too.
-
-Two cases DO newly exit 1, both deliberately:
-
-- Any table with at least one EQL v3 column where the manifest records an
- `encryptedColumn` that exists but is not one of them — not only the
- v2-pair-plus-one-v3 shape. The recorded pairing is authoritative and
- disagrees with every candidate, so guessing past it is the bug.
-
-- A column whose encrypted counterpart could only be identified **by
- elimination** — no recorded `encryptedColumn`, no `_encrypted` name
- match, one EQL column left once the plaintext column itself is excluded.
- `cutover` now refuses this as `drop` already did. Note `.cipherstash/` is
- gitignored, so `migrations.json` is machine-local: a fresh clone or CI runner
- can hit this on a **pure-v3** table whose encrypted column is named
- unconventionally, where `cutover` previously exited 0 with "not applicable".
- Re-run `stash encrypt backfill --table T --column C --encrypted-column `
- to record the pairing.
-
-Superseded later in this release: no v2 lifecycle can be driven by `stash encrypt`; mixed and pure-v2 state now fail with migration/recovery guidance.
diff --git a/.changeset/eql-v3-adapter-type-robustness.md b/.changeset/eql-v3-adapter-type-robustness.md
index f7cddd986..e2954a322 100644
--- a/.changeset/eql-v3-adapter-type-robustness.md
+++ b/.changeset/eql-v3-adapter-type-robustness.md
@@ -1,38 +1,35 @@
---
-'@cipherstash/stack': minor
+'@cipherstash/stack-drizzle': minor
+'@cipherstash/stack-supabase': minor
---
Restore the EQL v3 envelope and `Result` types the adapters were erasing.
-Both v3 adapters typed their operand-encryption paths as `unknown` and dropped
-the `Result` wrapper, so the query-type encoding and the failure channel were
+Both adapters typed their operand-encryption paths as `unknown` and dropped the
+`Result` wrapper, so the query-type encoding and the failure channel were
invisible to the type system:
-- `eql/v3/drizzle/operators.ts` typed the client's `encrypt`/`bulkEncrypt` as
+- The Drizzle operator module typed the client's `encrypt`/`bulkEncrypt` as
returning `unknown`, collapsed the operation's `Result` to
`{ data?: unknown; failure?: { message } }`, and cast the bulk response to
`Array<{ data: unknown }>`.
-- `supabase/query-builder-v3.ts` returned `Promise` from
- `encryptCollectedTerms`, `bulkEncryptGroup` and `encryptGroupPerTerm`, and the
- base `query-builder.ts` did the same.
+- The Supabase query builder returned `Promise` from
+ `encryptCollectedTerms`, `bulkEncryptGroup` and `encryptGroupPerTerm`.
These now carry the SDK's real types — `Encrypted` (the storage envelope union,
which includes every v3 per-domain payload), `BulkEncryptedData`, and
`EncryptedQueryResult` — threaded through a properly-typed operation surface that
-resolves `Result`. The Supabase divergence the erasure hid is
-now explicit: the v2 path yields `encryptQuery` composite literals and the v3
-path yields `JSON.stringify`'d envelope strings, and both are `EncryptedQueryResult`.
+resolves `Result`.
-Bumped `minor`, not `patch`: `createEncryptionOperatorsV3` is a public export
-(`@cipherstash/stack/eql/v3/drizzle`), and tightening its client contract from
-`unknown` to a typed operation surface is a compile-time breaking change — a
-downstream consumer passing a loosely-typed (`unknown`-returning) client double
-will now fail `tsc`. That tightening has teeth: `operators.test-d.ts` pins it
-with a negative type-test asserting an `unknown`-returning `{ encrypt }` double
-is rejected (a positive "correctly-typed double is accepted" assertion cannot
-catch a re-erasure, since a correct value is assignable to `unknown`).
+Tightening `createEncryptionOperators`' client contract from `unknown` to a typed
+operation surface is a compile-time breaking change for a downstream consumer
+passing a loosely-typed (`unknown`-returning) client double: it will now fail
+`tsc`. That tightening has teeth — `operators.test-d.ts` pins it with a negative
+type-test asserting an `unknown`-returning `{ encrypt }` double is rejected (a
+positive "correctly-typed double is accepted" assertion cannot catch a
+re-erasure, since a correct value is assignable to `unknown`).
-Behaviour is otherwise unchanged, with one addition: the Supabase v3 bulk path
-now rejects a `null` envelope returned by `bulkEncrypt` (the restored
+Behaviour is otherwise unchanged, with one addition: the Supabase bulk path now
+rejects a `null` envelope returned by `bulkEncrypt` (the restored
`Encrypted | null` type makes that arm reachable, and a `null` would otherwise
be `JSON.stringify`'d to the literal `"null"` and sent as a filter operand).
diff --git a/.changeset/eql-v3-cli-install.md b/.changeset/eql-v3-cli-install.md
deleted file mode 100644
index 264f449d9..000000000
--- a/.changeset/eql-v3-cli-install.md
+++ /dev/null
@@ -1,15 +0,0 @@
----
-"stash": minor
----
-
-Add an EQL v3 install path to `stash eql install` via a new `--eql-version <2|3>`
-flag (default `2`). v3 installs the native concrete-domain schema (`public.*`
-type domains, `eql_v3` operators, `eql_v3_internal` constructors) from bundles
-vendored into `packages/cli/src/sql` by `scripts/build-eql-v3-sql.mjs` (full
-bundle + a Supabase variant with the two superuser-only operator-class chunks
-stripped). v3 currently supports the direct install path only —
-`--drizzle`/`--migration`/`--migrations-dir`/`--latest` are rejected — and the
-installer keys `isInstalled`/version checks and Supabase grants to the `eql_v3`
-schema.
-
-Superseded later in this release: `--eql-version` and the v2 installer are removed; installs and upgrades are v3-only.
diff --git a/.changeset/eql-v3-drizzle-encrypt-query.md b/.changeset/eql-v3-drizzle-encrypt-query.md
index 9fb894b91..075e6d887 100644
--- a/.changeset/eql-v3-drizzle-encrypt-query.md
+++ b/.changeset/eql-v3-drizzle-encrypt-query.md
@@ -5,8 +5,8 @@
EQL v3 Drizzle: encrypt every query operand with `encryptQuery`, not `encrypt` (#622).
-The v3 Drizzle operators (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`notBetween`/
-`inArray`/`notInArray`/`contains`) previously encrypted their operands with
+The Drizzle operators (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`notBetween`/
+`inArray`/`notInArray`/`matches`/`contains`) previously encrypted their operands with
`client.encrypt`, producing a full storage envelope (including the ciphertext `c`)
cast to `::jsonb`. A WHERE-clause operand should be a query *term*, not a value to
store. Every operator now uses `client.encryptQuery`, which yields a
diff --git a/.changeset/eql-v3-drizzle-fail-open-guards.md b/.changeset/eql-v3-drizzle-fail-open-guards.md
index 990508580..ee7fb142e 100644
--- a/.changeset/eql-v3-drizzle-fail-open-guards.md
+++ b/.changeset/eql-v3-drizzle-fail-open-guards.md
@@ -1,10 +1,10 @@
---
-'@cipherstash/stack': minor
+'@cipherstash/stack-drizzle': minor
---
-Close two fail-open paths in the EQL v3 Drizzle adapter.
+Close two fail-open paths in the Drizzle adapter.
-`ops.contains()` now throws `EncryptionOperatorError` for a search term that
+`ops.matches()` now throws `EncryptionOperatorError` for a search term that
tokenizes to nothing: the empty string, or a term shorter than the match index
tokenizer's `token_length` (3 by default). Such a term produces an empty bloom
filter, and `stored_bf @> '{}'` is true for every row — so a user searching
@@ -16,8 +16,9 @@ The floor counts Unicode codepoints, matching the tokenizer. A UTF-16 length
check would wave through an astral-plane term — `"👍👍"` is 4 code units but
only 2 codepoints, yields no trigram, and matched every row.
-**Breaking for callers passing short terms:** `contains()` calls that previously
-returned every row now throw. Terms of 3+ codepoints are unaffected.
+**Breaking for callers passing short terms:** free-text calls that previously
+returned every row now throw. Terms at or above the configured `token_length`
+are unaffected.
`v3FromDriver()` now throws the new `EqlV3CodecError` on a payload that is not
an EQL envelope, instead of surfacing a raw `SyntaxError` for malformed JSON and
@@ -27,12 +28,7 @@ accepts both scalar envelopes (ciphertext at `c`) and SteVec documents
(ciphertext at `sv[0].c`). A SteVec's `sv` must be a non-empty array: `sv[0]` is
the decryption root, so `sv: []` carries a ciphertext key but no ciphertext, and
is now rejected rather than passed to `decrypt`. `EqlV3CodecError` is exported
-from `@cipherstash/stack/eql/v3/drizzle` so callers can catch it.
+from the `@cipherstash/stack-drizzle` package root so callers can catch it.
Also removes an unreachable branch in `inArray`/`notInArray`, whose empty-list
guard already throws before it.
-
-Note: the v2 Drizzle adapter's `like`/`ilike` path builds the same bloom filters
-and has the same short-term fail-open. It is **not** fixed here — v2 terms carry
-SQL wildcards, so the floor must be measured against what its tokenizer actually
-receives before the shared guard can be reused. Tracked separately.
diff --git a/.changeset/eql-v3-drizzle.md b/.changeset/eql-v3-drizzle.md
index 0ec9d099a..7b1c8a8f0 100644
--- a/.changeset/eql-v3-drizzle.md
+++ b/.changeset/eql-v3-drizzle.md
@@ -1,17 +1,18 @@
---
-"@cipherstash/stack": minor
+'@cipherstash/stack-drizzle': minor
---
-Add EQL v3 Drizzle support at `@cipherstash/stack/eql/v3/drizzle`. A Drizzle-native
-`types` namespace (same PascalCase names as `@cipherstash/stack/eql/v3`) declares
-encrypted columns whose Postgres type is the semantic `public.`; the concrete
-type drives the legal query operators. `createEncryptionOperatorsV3` provides
-capability-checked `eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`contains`/`inArray`/
-`asc`/`desc`/`and`/`or` that emit the latest two-argument `eql_v3` SQL functions with
-full-envelope operands, and `extractEncryptionSchemaV3` rebuilds the schema for
-`Encryption`. This surface was subsequently consolidated at the
-`@cipherstash/stack-drizzle` package root.
+EQL v3 Drizzle support on the `@cipherstash/stack-drizzle` package root. A
+Drizzle-native `types` namespace (same PascalCase names as
+`@cipherstash/stack/eql/v3`) declares encrypted columns whose Postgres type is
+the semantic `public.eql_v3_`; the concrete type drives the legal query
+operators. `createEncryptionOperators` provides capability-checked
+`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`matches`/`contains`/`inArray`/
+`asc`/`desc`/`and`/`or` that emit the two-argument `eql_v3` SQL functions with
+full-envelope operands, and `extractEncryptionSchema` rebuilds the schema for
+`Encryption`.
-The v3 text-search helper is `contains`; obsolete `like`/`ilike` helpers are not
-exposed because v3 free-text search is token containment rather than SQL wildcard
-matching.
+The encrypted free-text helper is `matches`; obsolete `like`/`ilike` helpers are
+not exposed, because encrypted free-text search is bloom-filter token matching
+rather than SQL wildcard matching. `contains` is genuine encrypted-JSON
+containment (`@>` against a `types.Json` column), not free-text.
diff --git a/.changeset/eql-v3-ga-rebaseline.md b/.changeset/eql-v3-ga-rebaseline.md
index b394d1741..68a6c1022 100644
--- a/.changeset/eql-v3-ga-rebaseline.md
+++ b/.changeset/eql-v3-ga-rebaseline.md
@@ -8,7 +8,7 @@ Re-baseline EQL v3 on the eql-3.0.0 GA release and protect-ffi 0.29.
eql-3.0.0 naming convention — flat, prefixed names in `public`
(`public.eql_v3_text_search`, `public.eql_v3_integer_ord`, …) instead of the
alpha-era bare names. Databases installed from an alpha bundle must be
- re-installed (`stash eql install --eql-version 3` replaces the schema).
+ re-installed (`stash eql install` replaces the schema).
- `encryptQuery` on the EQL v3 client now returns EQL v3 query operands
(protect-ffi 0.29): term-only scalar operands for the `eql_v3.query_`
domains, the `eql_v3.query_jsonb` containment needle, and bare selector
diff --git a/.changeset/eql-v3-rename-contains-to-matches.md b/.changeset/eql-v3-rename-contains-to-matches.md
index 2e576b464..119a8214d 100644
--- a/.changeset/eql-v3-rename-contains-to-matches.md
+++ b/.changeset/eql-v3-rename-contains-to-matches.md
@@ -11,7 +11,7 @@ containment. The name `contains()` promised substring/containment semantics it
never had. It is renamed to `matches()` on the encrypted surface; `contains()` is
kept for genuine, exact containment:
-- **Drizzle** (`@cipherstash/stack-drizzle/v3`): `matches()` = bloom free-text on
+- **Drizzle** (`@cipherstash/stack-drizzle`): `matches()` = bloom free-text on
`text_match`/`text_search` columns; `contains()` = exact encrypted-JSON `@>` on
`types.Json` (ste_vec) columns.
- **Supabase** (`@cipherstash/stack-supabase`): `.matches()` = encrypted free-text;
diff --git a/.changeset/eql-v3-sole-docs.md b/.changeset/eql-v3-sole-docs.md
index baa9edf10..4b3b14019 100644
--- a/.changeset/eql-v3-sole-docs.md
+++ b/.changeset/eql-v3-sole-docs.md
@@ -15,14 +15,13 @@ stored EQL v2 data is still reachable there:
corresponding v3 table and `{ storedEqlVersion: 2 }`, so the
`stash-dynamodb` skill documents that explicit compatibility path (#657).
- **The encrypt rollout lifecycle.** `stash encrypt *` and `@cipherstash/migrate`
- detect a column's generation from its Postgres domain type, with EQL v3 as the
- recognised and default generation; a legacy `eql_v2_encrypted` column does not
- classify and falls through to the v2 lifecycle, which ends in
- `stash encrypt cutover` rather than the v3 `stash encrypt drop`. That
- difference is kept under a version callout (#648).
+ classify a column from its Postgres domain type: a `public.eql_v3_*` domain is
+ recognised as v3, and anything else — including a legacy `eql_v2_encrypted`
+ column — does not classify. The documented lifecycle is the v3 one
+ (backfill → switch the application to the encrypted column → drop the
+ plaintext); legacy v2 columns are read-only, covered under a version callout
+ (#648).
Also corrects the legacy `@cipherstash/drizzle` README's pointer to the removed
`@cipherstash/stack/drizzle` subpath (now the separate `@cipherstash/stack-drizzle`
package).
-
-Superseded later in this release: CLI/migrate v2 mutation guidance is removed; only legacy ciphertext reads and status diagnostics remain.
diff --git a/.changeset/eql-v3-supabase-adapter.md b/.changeset/eql-v3-supabase-adapter.md
index 418d410f0..f8d6a61fe 100644
--- a/.changeset/eql-v3-supabase-adapter.md
+++ b/.changeset/eql-v3-supabase-adapter.md
@@ -1,68 +1,46 @@
---
-'@cipherstash/stack': minor
+'@cipherstash/stack-supabase': minor
---
-Add `encryptedSupabaseV3` — the EQL v3 dialect of the Supabase adapter. It is
-now a connect-time-async factory: `await encryptedSupabaseV3(url, key)` (or
-`(client)`) introspects the database over `DATABASE_URL`, detects EQL v3 columns
-by their Postgres domain (`information_schema.columns.domain_name`), and derives
-each column's encryption config from its domain — callers no longer pass a
-schema to `from()`. `select('*')` is supported (expanded from the introspected
-column list, and aliased back to each declared column's JS property name so a
-property→DB rename round-trips). A column using an EQL v3 domain this SDK version does not model
-(e.g. `public.json`, `*_ord_ope`) throws at construction rather than silently
-passing through. Supplying `schemas` remains optional and adds compile-time
-types plus startup verification of the declared tables against the database.
-Requires a Postgres connection for introspection (`pg` is a new optional peer),
-so it cannot run in a Worker or the browser.
+`encryptedSupabase` is a connect-time-async, introspecting EQL v3 factory:
+`await encryptedSupabase(url, key)` (or `(client)`) introspects the database over
+`DATABASE_URL`, detects EQL v3 columns by their Postgres domain
+(`information_schema.columns.domain_name`), and derives each column's encryption
+config from its domain — callers no longer pass a schema to `from()`.
+`select('*')` is supported (expanded from the introspected column list, and
+aliased back to each declared column's JS property name so a property→DB rename
+round-trips). A column using a `public.eql_v3_*` domain this SDK version does not
+model throws when its table is named via `from()` rather than silently passing
+through. Supplying `schemas` remains optional and adds compile-time types plus
+eager construction-time verification of the declared tables against the
+database — including that same unmodelled-column check. Requires a Postgres connection for
+introspection (`pg` is an optional peer), so it cannot run in a Worker or the
+browser.
Every column name a query carries — filters, `match`, `not`, raw `filter`,
-`or()`, `order()`, and the `onConflict` option — is now resolved from its JS
-property name to its DB column name in a single pass before the query is built,
-so a declared rename round-trips everywhere rather than only on the paths that
+`or()`, `order()`, and the `onConflict` option — is resolved from its JS property
+name to its DB column name in a single pass before the query is built, so a
+declared rename round-trips everywhere rather than only on the paths that
remembered to translate.
-`order()` on ANY encrypted v3 column is now rejected — at compile time when
-`schemas` is supplied, and at runtime otherwise. The EQL v3 domains are
-`DOMAIN … AS jsonb` and the bundle declares no btree operator class on them, so
-`ORDER BY col` resolves through jsonb's default `jsonb_cmp` and sorts by the
-envelope's byte structure: a stable, plausible-looking, meaningless row order,
-with no error. Correct ordering is `ORDER BY eql_v3.ord_term(col)`, which
-PostgREST's `order=` cannot express. Order by a plaintext column, expose
-`eql_v3.ord_term()` as a generated column or view, or use the EQL v3 Drizzle
-integration, which emits `ord_term` directly. Note `gte`/`lte` filters remain
-correct: the comparison operators *are* declared on the ord domains, and only
-sorting resolves through the missing operator class.
-
-`.or()` now understands PostgREST's `column.not..` negation. It was
+`.or()` understands PostgREST's `column.not..` negation. It was
previously parsed as `{ op: 'not', value: '.' }`, so on an encrypted
column `or('nickname.not.in.(ada,grace)')` encrypted the literal string
`in.(ada,grace)` as a single plaintext and produced a filter that silently
matched nothing.
-Free-text search on the v3 builder is `contains(column, value)`. `like`/`ilike`
-are not exposed, because EQL v3 free-text search is token containment over a
-bloom filter (`@>`, backed by `eql_v3.contains`) rather than SQL wildcard
-matching — `%` is tokenized like any other character, so a `like` pattern is a
-category error. This matches the v3 Drizzle integration, which omits them for
-the same reason. On an encrypted column `like`/`ilike` now throw and name
-`contains`; on a plaintext column they remain ordinary PostgREST filters.
-
-`contains` is narrowed at compile time to columns whose domain carries the
-`freeTextSearch` capability (`public.text_match`, `public.text_search`), and
-guarded at runtime for the untyped surface. A raw `filter(column, operator, …)`
-on an encrypted v3 column now derives its query type from the operator instead
-of always encrypting an equality term, so `filter('bio', 'cs', …)` on a
-`public.text_match` column works rather than being rejected, and an unsupported
-operator throws instead of silently encrypting the wrong term.
-
-Substring `contains` matches any needle whose trigrams are all present in the
-stored value; needles shorter than the tokenizer's window (3 characters) bloom to
-nothing and are rejected rather than silently matching every row. The v3 match
-index now emits `include_original: false` — the flag is inert in protect-ffi (the
-bloom is trigram-only either way), so this moves no ciphertext and only pins the
-value a substring-search domain wants.
-
-This surface was subsequently consolidated as the unsuffixed
-`encryptedSupabase` EQL v3 factory. The legacy v2 Supabase authoring wrapper has
-been removed; core native decrypt remains compatible with stored v2 payloads.
+Encrypted free-text search is `matches(column, value)`, narrowed at compile time
+to columns whose domain carries the `freeTextSearch` capability
+(`public.eql_v3_text_match`, `public.eql_v3_text_search`) and guarded at runtime
+for the untyped surface. It matches any needle whose trigrams are all present in
+the stored value; needles shorter than the tokenizer's window (3 characters)
+bloom to nothing and are rejected rather than silently matching every row. A raw
+`filter(column, operator, …)` on an encrypted column derives its query type from
+the operator instead of always encrypting an equality term, so
+`filter('bio', 'cs', …)` on a `public.eql_v3_text_match` column works rather than
+being rejected, and an unsupported operator throws instead of silently encrypting
+the wrong term.
+
+The v3 match index emits `include_original: false` — the flag is inert in
+protect-ffi (the bloom is trigram-only either way), so this moves no ciphertext
+and only pins the value a substring-search domain wants.
diff --git a/.changeset/eql-v3-wasm-inline.md b/.changeset/eql-v3-wasm-inline.md
index c67e659cc..9b1e2c88f 100644
--- a/.changeset/eql-v3-wasm-inline.md
+++ b/.changeset/eql-v3-wasm-inline.md
@@ -9,7 +9,7 @@ created a client pinned to the FFI's EQL v2 wire format, so a v3 schema
(concrete `eql_v3_*` domains) failed every encrypt on the edge. It now targets
EQL v3 exclusively:
-- The factory constructs the WASM client with `eqlVersion: 3`, so v3 schemas
+- The factory builds an EQL v3 WASM client, so v3 schemas
encrypt/decrypt correctly on the edge.
- The entry re-exports the **v3** authoring surface (`types`, `encryptedTable`,
the column classes, `buildEncryptConfig`, and the inference helpers) — the
diff --git a/.changeset/init-drizzle-eql-v3.md b/.changeset/init-drizzle-eql-v3.md
index 5e54b75e2..184409944 100644
--- a/.changeset/init-drizzle-eql-v3.md
+++ b/.changeset/init-drizzle-eql-v3.md
@@ -2,16 +2,13 @@
'stash': patch
---
-`stash init --drizzle` now installs EQL v3 instead of v2.
+`stash init --drizzle` installs EQL v3.
-The Drizzle init flow pinned `--eql-version 2`, because `stash eql install
---drizzle` (the only migration-generating install path at the time) was
-v2-only. That made `stash init --drizzle` the single flow that provisioned a v2
-database — a bare `stash eql install`, and init for every other integration,
-already defaulted to v3. It also contradicted the `stash-drizzle` skill init
-copies into the same project, which documents the v3 `@cipherstash/stack-drizzle`
-surface (`types.*` domains, `Encryption`) and would have the user's agent
-author v3 code against a v2 database.
+The Drizzle init flow used to provision a v2 database — the only
+migration-generating install path at the time was v2-only — while the
+`stash-drizzle` skill init copies into the same project documents the v3
+`@cipherstash/stack-drizzle` surface (`types.*` domains, `Encryption`). The
+user's agent would have authored v3 code against a v2 database.
Init's Drizzle flow now routes through `stash eql migration --drizzle`, so it
stays migration-first (the install lands in your Drizzle migration history and
@@ -21,7 +18,6 @@ The generated migration also carries the `cs_migrations` tracking schema, so one
isn't installed or configured, init now reports EQL as not installed and points
at `stash eql migration --drizzle` rather than aborting the run.
-The final CLI installation and mutation surface is v3-only: the explicit v2
-Drizzle install path is removed. Legacy v2 remains readable and visible in
-diagnostics. Generate a checked-in install migration with `stash eql migration
---drizzle`.
+The CLI installation and mutation surface is v3-only. Legacy v2 remains readable
+and visible in diagnostics. Generate a checked-in install migration with
+`stash eql migration --drizzle`.
diff --git a/.changeset/init-placeholder-eql-v3.md b/.changeset/init-placeholder-eql-v3.md
index 6c75a89c1..28e4bcf3d 100644
--- a/.changeset/init-placeholder-eql-v3.md
+++ b/.changeset/init-placeholder-eql-v3.md
@@ -13,9 +13,6 @@ customer's coding agent v2 guidance against a v3 schema (follow-up to #732 /
Scaffolds now teach the v3 surface: `Encryption` from `@cipherstash/stack/v3`,
the concrete-domain `types.*` factories (`types.TextSearch`, `types.IntegerOrd`,
-`types.Text`, `types.Json`, …), and the `@cipherstash/stack-drizzle/v3` entry
-(`extractEncryptionSchemaV3`) for Drizzle. The `encryptionClient` export shape
-and the empty-schema "no schemas yet" error path are unchanged.
-
-The scaffolds emit `extractEncryptionSchema` from the collapsed
-`@cipherstash/stack-drizzle` root.
+`types.Text`, `types.Json`, …), and `extractEncryptionSchema` from the
+`@cipherstash/stack-drizzle` package root for Drizzle. The `encryptionClient`
+export shape and the empty-schema "no schemas yet" error path are unchanged.
diff --git a/.changeset/init-scaffold-compiles.md b/.changeset/init-scaffold-compiles.md
index 3de825aea..cf6b76338 100644
--- a/.changeset/init-scaffold-compiles.md
+++ b/.changeset/init-scaffold-compiles.md
@@ -13,16 +13,13 @@ the non-empty schema requirement.
The scaffold now declares a single sentinel table, `__stash_placeholder__`, so
the file typechecks as written. Every command that reads the encryption client
-— `stash db push`, `stash db validate`, and `stash encrypt backfill` — refuses
-to run while that table is still the only one declared, and names it, rather
-than failing later with a confusing "table not found". (`stash encrypt cutover`
-and `stash encrypt drop` do not read the client file at all; they resolve
-against the database.)
+— `stash db validate` and `stash encrypt backfill` — refuses to run while that
+table is still the only one declared, and names it, rather than failing later
+with a confusing "table not found". (`stash encrypt drop` does not read the
+client file at all; it resolves against the database.)
Nothing in the repo compiled this output before: `packages/cli` has no
typecheck step, the codegen tests only string-match fragments of the template,
and the step test stubs the generator out entirely. Both templates are now
committed as fixtures that CI typechecks, pinned byte-for-byte to the generator
so they cannot drift.
-
-Superseded later in this release: the generated guidance no longer references removed `db push` or `encrypt cutover` commands.
diff --git a/.changeset/migrate-column-exists.md b/.changeset/migrate-column-exists.md
index d88867430..33870eaae 100644
--- a/.changeset/migrate-column-exists.md
+++ b/.changeset/migrate-column-exists.md
@@ -11,11 +11,11 @@ Callers need that difference to tell a STALE column reference (it is gone) from
a live one the domain classifier simply does not recognise — most often a legacy
`eql_v2_encrypted` counterpart.
-`stash encrypt cutover` / `drop` had a private copy of this probe built on a bare
+`stash encrypt drop` had a private copy of this probe built on a bare
`to_regclass($1)`. That form *parses* its argument and case-folds unquoted
identifiers, so on a Prisma-style `"User"` table it resolved `user`, reported the
column missing, and treated a valid recorded pairing as stale — silently skipping
-the fail-closed that stops those commands acting on a guessed encrypted column.
+the fail-closed that stops the command acting on a guessed encrypted column.
The shared implementation quotes with `format('%I')` first, like every other
catalog probe in this package, so the lookup is case-exact while still honouring
`search_path` for unqualified names.
diff --git a/.changeset/migrate-eql-v3.md b/.changeset/migrate-eql-v3.md
index e43ea380e..97afadbba 100644
--- a/.changeset/migrate-eql-v3.md
+++ b/.changeset/migrate-eql-v3.md
@@ -16,17 +16,11 @@ right lifecycle, no new flags:
database, including the domain CHECK and a decrypt round-trip). The
manifest records the detected version, the encrypted column's name, and the
v3 target phase, and the command prints v3-appropriate next steps.
-- **`encrypt cutover`** on a backfilled v3 column reports "not applicable"
- (exit 0) with guidance: v3 has no rename cut-over — the application
- switches to the encrypted column by name. Before backfill completes it
- exits 1 and says to finish the backfill instead of instructing the switch.
- On a database with no `eql_v2_configuration` table (a v3-only install) the
- v2 path now explains that instead of surfacing a raw Postgres error.
- **`encrypt drop`** is version-aware: v3 runs from the `backfilled` phase,
**verifies live coverage** (refuses to generate the migration while any row
still has the plaintext set and the encrypted column NULL — the
`countUnencrypted` check), and drops the ORIGINAL plaintext column (there
- is no `_plaintext` under v3); v2 behaviour is unchanged. The generated
+ is no `_plaintext` under v3). The generated
v3 migration **re-verifies coverage at apply time** — it locks the table,
re-counts, and aborts without dropping if plaintext-only rows appeared
after generation. And because dropping is the one irreversible step, it
@@ -34,7 +28,7 @@ right lifecycle, no new flags:
manifest's recorded `encryptedColumn` or the naming convention): a match
found only by being the table's sole EQL column is refused with
instructions, and an ambiguous table (several EQL columns, none
- identifiable) fails closed listing the candidates — as does `cutover`.
+ identifiable) fails closed listing the candidates.
- **`encrypt status`** classifies each column from the observed domain type
(manifest as fallback), shows `v3` in the EQL column, and no longer raises
the v2-only `not-registered` / `plaintext-col-missing` drift flags for v3
@@ -49,14 +43,11 @@ right lifecycle, no new flags:
matching the manifest and the installer. Resolved columns carry `via:
'hint' | 'convention' | 'sole'` so callers can tell a positively asserted
pairing from a by-elimination guess.
-- Fixed: `encrypt cutover`/`encrypt drop` precondition failures now actually
- exit 1 — the early-return guards previously skipped the exit-code path
- entirely, so failed preconditions exited 0. (This also applies to v2
- preconditions: scripted pipelines that relied on the erroneous exit 0 will
- now see the documented exit 1.)
+- Fixed: `encrypt drop` precondition failures now actually exit 1 — the
+ early-return guards previously skipped the exit-code path entirely, so failed
+ preconditions exited 0. Scripted pipelines that relied on the erroneous exit 0
+ will now see the documented exit 1.
The `stash-cli` and `stash-encryption` skills and the `@cipherstash/migrate`
-README document the two lifecycles (v2: backfill → cutover → drop;
-v3: backfill → switch-by-name → drop).
-
-Superseded later in this release: `@cipherstash/migrate` and the CLI now author and mutate v3 only; legacy v2 manifest fields remain readable.
+README document the v3 lifecycle: backfill → switch the application to the
+encrypted column by name → drop the plaintext column.
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 89dab5375..4bc0470ae 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -1,5 +1,5 @@
{
- "mode": "pre",
+ "mode": "exit",
"tag": "rc",
"initialVersions": {
"@cipherstash/e2e": "0.0.2",
diff --git a/.changeset/remove-eql-v2-drizzle-root-consumers.md b/.changeset/remove-eql-v2-drizzle-root-consumers.md
new file mode 100644
index 000000000..a9001d0a7
--- /dev/null
+++ b/.changeset/remove-eql-v2-drizzle-root-consumers.md
@@ -0,0 +1,13 @@
+---
+'stash': patch
+'@cipherstash/stack': patch
+---
+
+Follow the `@cipherstash/stack-drizzle` package-root collapse in the packages that
+document it.
+
+- **`stash`:** `stash init --drizzle` emits the package-root
+ `extractEncryptionSchema` import, and the bundled `stash-drizzle` and
+ `stash-encryption` skills match.
+- **`@cipherstash/stack`:** README only — its Drizzle section documents the
+ package-root exports.
diff --git a/.changeset/remove-eql-v2-drizzle-root.md b/.changeset/remove-eql-v2-drizzle-root.md
index eb06bd693..c713ad68a 100644
--- a/.changeset/remove-eql-v2-drizzle-root.md
+++ b/.changeset/remove-eql-v2-drizzle-root.md
@@ -1,7 +1,5 @@
---
'@cipherstash/stack-drizzle': major
-'@cipherstash/stack': patch
-'stash': patch
---
Remove the EQL v2 authoring surface from `@cipherstash/stack-drizzle` and collapse the EQL v3 `./v3` subpath into the package root.
@@ -17,10 +15,3 @@ Remove the EQL v2 authoring surface from `@cipherstash/stack-drizzle` and collap
The `types.*` column factories, `makeEqlV3Column` / `getEqlV3Column` / `isEqlV3Column`, the codec helpers (`v3ToDriver` / `v3FromDriver` / `EqlV3CodecError`), and `EncryptionOperatorError` are unchanged apart from moving to the root.
Existing EQL v2 ciphertext remains decryptable via `@cipherstash/stack` — only the Drizzle-side v2 authoring and query-building is removed.
-
-**`stash` (patch):** `stash init --drizzle` now emits the package-root
-`extractEncryptionSchema` import. The bundled `stash-drizzle` and
-`stash-encryption` skills are updated to match.
-
-**`@cipherstash/stack` (patch):** README only — its Drizzle section now
-documents the final package-root exports.
diff --git a/.changeset/rewriter-never-drops-ciphertext.md b/.changeset/rewriter-never-drops-ciphertext.md
index e32890e4c..25bbee069 100644
--- a/.changeset/rewriter-never-drops-ciphertext.md
+++ b/.changeset/rewriter-never-drops-ciphertext.md
@@ -36,11 +36,8 @@ rather than rewritten, so the ADD+DROP+RENAME no longer drops a column that the
migration corpus itself shows already holds ciphertext. That is a guarantee
about what the corpus says, not about the database: the sweep reasons entirely
from migration files, and a database that has drifted from its migration
-history is outside what it can see. `stash encrypt cutover` is the sharpest
-example — it renames columns directly in the database and never writes drizzle
-SQL, so the corpus can still describe a column as plaintext after cutover has
-made it ciphertext; the same is true of any change made by hand via psql or the
-Supabase dashboard. If your migration history is squashed, the column's
+history is outside what it can see: a column encrypted by hand via psql or the
+Supabase dashboard is still described as plaintext by the corpus. If your migration history is squashed, the column's
`CREATE TABLE` lives outside the directory being swept, or the database has
simply drifted from what the migrations describe, you will see the statement
flagged instead of repaired: check the column's current type in the database
diff --git a/.changeset/skills-eql-v3-accuracy.md b/.changeset/skills-eql-v3-accuracy.md
index 75c9f55de..b892b60a0 100644
--- a/.changeset/skills-eql-v3-accuracy.md
+++ b/.changeset/skills-eql-v3-accuracy.md
@@ -10,16 +10,15 @@ rollout tooling was v2-only. Since these skills are copied into customer repos,
stale text steered users away from v3 and toward workarounds they no longer need.
- **`stash-drizzle`, `stash-supabase`** — replaced the "v3 not supported end-to-end"
- callouts with an accurate EQL version note: the tooling auto-detects a column's
- generation from its Postgres domain type, and the two lifecycles differ at the end.
- v3 is `backfill → switch the app to the encrypted column by name → drop` with no
- cut-over rename; v2 keeps the `stash encrypt cutover` rename plus config promotion.
+ callouts with an accurate EQL version note: the tooling classifies a column from
+ its Postgres domain type, and the documented lifecycle is
+ `backfill → switch the app to the encrypted column by name → drop` — there is no
+ cut-over rename.
- **`stash-supabase`** — removed the "Interim path until #648: the v2 encrypted twin"
section; a v2 twin is no longer needed to get CLI-managed backfill.
- **`stash-drizzle`, `stash-supabase`** — the drop step now documents that
- `stash encrypt drop` targets the *original* column under v3 (there is no
- `_plaintext`, since nothing was renamed) and `_plaintext` under v2.
-- **`stash-cli`** — corrected the documented `EQLInstaller` default: `eqlVersion`
- defaults to `3`, not `2`, matching the `--eql-version` CLI default. Also reworded
- the v2 cut-over known-gap note, which cited cipherstash/stack#585 as open tracking
- when it was resolved by making v3 the default.
+ `stash encrypt drop` targets the *original* column (there is no
+ `_plaintext`, since nothing is renamed).
+- **`stash-cli`** — corrected the documented `EQLInstaller` default (EQL v3) and
+ removed the v2 cut-over known-gap note, which cited cipherstash/stack#585 as open
+ tracking when it was resolved by making v3 the default.
diff --git a/.changeset/skills-pin-stable-1-0.md b/.changeset/skills-pin-stable-1-0.md
new file mode 100644
index 000000000..cfe7a9c6a
--- /dev/null
+++ b/.changeset/skills-pin-stable-1-0.md
@@ -0,0 +1,21 @@
+---
+'stash': patch
+---
+
+The bundled skills pin `1.0.0`, not a release candidate (#791).
+
+`skills/stash-edge` hardcoded `@cipherstash/stack@1.0.0-rc.4` in its Deno `npm:`
+import and its `deno.json` import map, and `skills/stash-cli` pinned the
+bare-project `npx --package=stash@…` one-shot the same way. Nothing in the build
+rewrites those literals — `tsup.config.ts` copies `skills/` verbatim, and the
+`__STASH_RUNTIME_VERSIONS__` embed only reaches compiled CLI code — so the
+published skill would have told Deno and Supabase Edge Function users to pin a
+release candidate in production, in their own repo, indefinitely.
+
+The prerelease-semver paragraph that explained why `@^1.0.0` does not match
+`1.0.0-rc.4` is gone with the rc pin; "pin exactly, Deno caches by specifier"
+stands on its own. The `supabase-worker` example pins the same way.
+
+A test now guards every `skills/*/SKILL.md`: an exact pin of a release-train
+package must name a stable version on the current major, so a stale rc pin fails
+on the version-bump PR instead of shipping.
diff --git a/.changeset/skills-v3-lifecycle-honesty.md b/.changeset/skills-v3-lifecycle-honesty.md
deleted file mode 100644
index 2c29c1d0e..000000000
--- a/.changeset/skills-v3-lifecycle-honesty.md
+++ /dev/null
@@ -1,29 +0,0 @@
----
-'stash': patch
----
-
-Correct the EQL v2/v3 rollout lifecycle in the bundled `stash-encryption`,
-`stash-supabase` and `stash-drizzle` agent skills. Each described the **v2**
-lifecycle as the unqualified default even though v3 is the default generation,
-so an agent following the prose would run steps that do not apply — and, in one
-case, expect the wrong column to be dropped.
-
-- `stash encrypt drop` was documented as removing `_plaintext`. That is the
- **v2** target. On a v3 column there is no `_plaintext`: the command drops
- the **original ``**, guarded by a `DO` block that takes `ACCESS EXCLUSIVE`
- and re-counts unencrypted rows at apply time, raising instead of dropping if
- any remain. Each step in the cutover table is now marked v2-only or v3, and the
- drop preconditions (`cut-over` for v2, `backfilled` for v3) are stated.
-- "The pending row will be promoted to active by `stash encrypt cutover`" was
- false for v3, where cutover short-circuits before touching any configuration.
- `stash db activate` is the only promotion path there.
-- The CipherStash Proxy call-outs told every reader to run `stash db push`.
- `db push`/`db activate` manage `eql_v2_configuration`, which EQL v3 does not
- ship — on a v3-only database `db push` reports "Nothing to do." and exits 0,
- and `db activate` errors. The call-outs are now scoped to the EQL v2 + Proxy
- path.
-
-Skills ship inside the `stash` tarball and are copied into user projects at
-`stash init`, so this guidance was being installed into customer repos.
-
-Superseded later in this release: the bundled skills no longer document v2/Proxy mutation commands because those CLI paths are removed.
diff --git a/.changeset/stack-1-0-0-rc.md b/.changeset/stack-1-0-0-rc.md
index 555ae5c8e..aba65318a 100644
--- a/.changeset/stack-1-0-0-rc.md
+++ b/.changeset/stack-1-0-0-rc.md
@@ -5,7 +5,7 @@
'stash': major
---
-CipherStash Stack 1.0 (release candidate).
+CipherStash Stack 1.0.
This is the first 1.0-line release of `@cipherstash/stack`, the first published
release of the split-out EQL v3 adapters `@cipherstash/stack-drizzle` and
diff --git a/.changeset/stash-cli-eql-v3-default.md b/.changeset/stash-cli-eql-v3-default.md
deleted file mode 100644
index b6eb6da19..000000000
--- a/.changeset/stash-cli-eql-v3-default.md
+++ /dev/null
@@ -1,14 +0,0 @@
----
-"stash": minor
----
-
-Default EQL to v3 and stop the CLI recommending `stash db push` (#585).
-
-- **EQL v3 is now the default.** `stash eql install` and `stash eql upgrade` target v3 (the native `eql_v3.*` domain schema) without `--eql-version 3`. The v2-only paths — `--drizzle`, `--migration`, `--migrations-dir`, and `--latest` — now require an explicit `--eql-version 2` and error with clear guidance otherwise (v3 installs via the direct path only). `stash init` pins v2 automatically when it drives the Drizzle migration flow. **Note:** for a Supabase project, `stash init` now runs a v3 direct install rather than offering the v2 migration-file flow; run `stash eql install --supabase --migration --eql-version 2` if you want a checked-in migration file.
-- **`stash db push` is no longer recommended in CLI output.** `db push` writes the `public.eql_v2_configuration` table, which is a v2 + CipherStash Proxy artifact — EQL v3 has no configuration table (config lives in each column's `eql_v3.*` type) and nothing in the v3 stack reads it. The push recommendations are removed from `eql status`, the help banner, and the init/plan/cutover guidance. `db push` (and `db activate`) remain available for EQL v2 + Proxy users; they're now labelled as such.
-- **`eql status` is v3-aware.** On a v3-only database it reports that encrypt config lives in the column types instead of hitting a "table not found" dead-end that told users to run `db push` (which neither creates that table nor applies to v3).
-- **`stash db push` guards a v3-only database** with a clear "not needed under EQL v3" message instead of a raw `relation "public.eql_v2_configuration" does not exist` error.
-
-**Superseded later in this release:** the CLI no longer installs or mutates EQL
-v2 state; `db push`, `db activate`, the Proxy choice, and the v2 install flags
-described above are removed. Legacy state remains visible through status only.
diff --git a/.changeset/stash-cli-skill-refresh.md b/.changeset/stash-cli-skill-refresh.md
index a2e62330b..7d524448f 100644
--- a/.changeset/stash-cli-skill-refresh.md
+++ b/.changeset/stash-cli-skill-refresh.md
@@ -20,14 +20,9 @@ handoff time, so a stale skill becomes stale guidance in the user's project.
entirely, plus the non-interactive interface (per-command escape hatches, exit codes, the
`DATABASE_URL` resolution order, the `auth login --json` NDJSON event contract).
- **Corrects the `db` → `eql` move.** `db install`, `db upgrade`, and `db status` are
- deprecated aliases that warn and forward; `db push`, `db activate`, `db validate`,
- `db test-connection`, and `db migrate` remain in the `db` group.
-- **Scopes `db push` / `db activate` as EQL v2 + CipherStash Proxy only**, in both the skill
- and the README's recommended flow. SDK users hold their encryption config in application
- code and don't need them.
-- Adds the missing `--database-url`, `--eql-version`, `--prisma-next`, `--proxy`/`--no-proxy`,
- and `--region` flags; corrects six programmatic API signatures; fixes the README's claim
+ deprecated aliases that warn and forward; `db validate`, `db test-connection`, and
+ `db migrate` remain in the `db` group.
+- Adds the missing `--database-url`, `--prisma-next`, and `--region` flags; corrects
+ six programmatic API signatures; fixes the README's claim
that `stash init` ends in an agent-handoff menu (that belongs to `stash plan` / `stash impl`);
and marks `stash env` as the non-functional stub it currently is.
-
-Superseded later in this release: the v2/Proxy commands and flags listed above are removed from both the CLI and bundled skill.
diff --git a/.changeset/stash-supabase-contains-substrings.md b/.changeset/stash-supabase-contains-substrings.md
index ca0df909a..c184442ca 100644
--- a/.changeset/stash-supabase-contains-substrings.md
+++ b/.changeset/stash-supabase-contains-substrings.md
@@ -2,8 +2,8 @@
"stash": patch
---
-Correct the bundled `stash-supabase` agent skill: EQL v3 `contains()` matches
-substrings. The skill previously carried the reverse — that `contains()` matched
+Correct the bundled `stash-supabase` agent skill: encrypted free-text search
+matches substrings. The skill previously carried the reverse — that it matched
only exact values because the query's bloom filter appended the whole search term
as an extra token. That was never true: `include_original` is inert in
protect-ffi (the match bloom is trigram-only either way), so any substring of at
diff --git a/.changeset/supabase-in-list-operands.md b/.changeset/supabase-in-list-operands.md
index 360e23370..f4b5fa1b0 100644
--- a/.changeset/supabase-in-list-operands.md
+++ b/.changeset/supabase-in-list-operands.md
@@ -1,5 +1,5 @@
---
-'@cipherstash/stack': minor
+'@cipherstash/stack-supabase': minor
---
Fix encrypted `in`-list operands in the Supabase adapter, and widen the `is` /
@@ -16,9 +16,7 @@ in.("{"v":1,"c":"…"}",…)
```
Encrypted lists are now emitted through `filter(col, 'in', …)` with each element
-quoted and escaped, matching what the `.or()` path already did. This affects
-**v2 as well as v3** — v2's `("a@b.com")` composite literal is itself
-quote-bearing and was equally broken.
+quoted and escaped, matching what the `.or()` path already did.
**`not(col, 'in', […])` encrypted the whole list as a single ciphertext**, so
the filter silently matched nothing, and emitted an unparenthesized
@@ -30,11 +28,8 @@ array.
**`filter(col, 'in', […])` encrypted the whole list as a single ciphertext.**
The raw `.filter()` path reached `in` with none of the element-splitting the
`in()`, `not(…, 'in', …)` and `.or()` paths perform, so the entire list operand
-was encrypted as one equality term. The two wire formats then failed
-differently, which is why this went unnoticed: **v2**'s `("json")` composite
-literal is already parenthesized, so PostgREST parsed it as a one-element list
-and answered `200 []` — a filter that silently matched nothing. **v3**'s bare
-`{…}` envelope is not, so PostgREST rejected the request outright with
+was encrypted as one equality term. A bare `{…}` envelope is not parenthesized,
+so PostgREST rejected the request outright with
`PGRST100 (failed to parse filter)`.
Each element is now encrypted separately and the operand rendered as a quoted
@@ -56,7 +51,8 @@ plaintext jsonb/array column falls through to PostgREST's native containment, so
`contains('tags', ['vip'])` and `contains('meta', { plan: 'pro' })` now
typecheck. A plaintext SCALAR column does not: `@>` is undefined on `text`, so
the operand type follows the column's own shape and a scalar rejects every
-containment operand. Encrypted match columns still take a `string` token.
+containment operand. Encrypted free-text search is `matches()`, which takes a
+`string` token; `contains()` on an encrypted non-JSON column throws and names it.
Relatedly, `.or([{ op: 'contains' }])` now emits PostgREST's `cs` operator for
plaintext columns too — previously only encrypted conditions were translated, so
a plaintext containment reached the wire as `.contains.` and failed to parse.
@@ -87,6 +83,6 @@ ciphertext.
**In-list operands encrypt in one crossing per column.** The element-wise `in` /
`not.in` encoding above spent one ZeroKMS round-trip per element; terms are now
grouped by column and each group takes a single `bulkEncrypt` call, matching the
-Drizzle v3 path. Falls back to per-term encryption for clients without
+Drizzle path. Falls back to per-term encryption for clients without
`bulkEncrypt`, and rejects a bulk response whose length does not match the list
rather than silently truncating the predicate.
diff --git a/.changeset/supabase-is-null-operands.md b/.changeset/supabase-is-null-operands.md
index 46478aa24..c4a2e19b8 100644
--- a/.changeset/supabase-is-null-operands.md
+++ b/.changeset/supabase-is-null-operands.md
@@ -1,5 +1,5 @@
---
-'@cipherstash/stack': patch
+'@cipherstash/stack-supabase': patch
---
Fix the Supabase adapter encrypting `is` and `null` filter operands.
@@ -14,10 +14,9 @@ NULL column rather than ciphertext, so it is found with an unencrypted
`IS NULL`; encrypting the operand could never match.
A single `isEncryptableTerm(operator, value)` predicate now guards every term
-collector. Affects both `encryptedSupabase` (v2) and `encryptedSupabaseV3`. On
-v3 this additionally removes a spurious `does not support equality queries`
-error, which `is` raised because it maps to the `equality` query type and so hit
-the column-capability guard — `or('active.is.null')` on a storage-only column
+collector. This additionally removes a spurious `does not support equality
+queries` error, which `is` raised because it maps to the `equality` query type
+and so hit the column-capability guard — `or('active.is.null')` on a storage-only column
threw rather than querying.
Relatedly, an `or()` string is now rebuilt whenever a condition *references* an
diff --git a/.changeset/supabase-or-string-parser.md b/.changeset/supabase-or-string-parser.md
index 25db66e67..84fd89975 100644
--- a/.changeset/supabase-or-string-parser.md
+++ b/.changeset/supabase-or-string-parser.md
@@ -1,8 +1,8 @@
---
-'@cipherstash/stack': patch
+'@cipherstash/stack-supabase': patch
---
-Fix the Supabase adapter's `.or()` string parser mis-splitting conditions, and pin `contains()` on a mixed union column key to the encrypted operand.
+Fix the Supabase adapter's `.or()` string parser mis-splitting conditions.
An `.or()` string is only rebuilt from its parse when it references an encrypted column — otherwise the caller's string is forwarded verbatim — so each of these corrupts precisely the mixed encrypted/plaintext case.
@@ -15,5 +15,3 @@ An `.or()` string is only rebuilt from its parse when it references an encrypted
**A parenthesized operand was read as a list for every operator.** Only `in` and the range operators (`ov`, `sl`, `sr`, `nxr`, `nxl`, `adj`) take a paren-delimited operand; elsewhere `(` is an ordinary character. `email.eq.(foo)` parsed as `['foo']` and encrypted a JS array rather than the string, matching nothing.
**A string operand spelling `null`, `true` or `false` is now quoted.** PostgREST reads a bare `null` as SQL NULL, so `.or([{ column: 'name', op: 'eq', value: 'null' }])` emitted `name.eq.null` and compared against NULL instead of the three-character string.
-
-**`contains(col, …)` where `col` is a union spanning an encrypted and a plaintext column** accepted an array or object operand. The union is now only as permissive as its strictest member: any declared encrypted column in the union pins the operand to `string`. A literal column argument was never affected.
diff --git a/.changeset/supabase-v3-order-by-ope-term.md b/.changeset/supabase-v3-order-by-ope-term.md
index 33d450239..ad7d612db 100644
--- a/.changeset/supabase-v3-order-by-ope-term.md
+++ b/.changeset/supabase-v3-order-by-ope-term.md
@@ -1,34 +1,33 @@
---
-'@cipherstash/stack': minor
+'@cipherstash/stack-supabase': minor
---
-**`order()` now works on EQL v3 encrypted ordering columns in the Supabase
-adapter.** It was rejected outright on every encrypted column.
+**`order()` works on EQL v3 encrypted ordering columns.**
-A bare `ORDER BY col` on an EQL v3 domain really is wrong — the bundle declares
-no btree operator class on any domain, so the sort falls through to jsonb's
-default `jsonb_cmp` and compares the envelope's keys in storage order, starting
-at the random ciphertext `c`. Measured over ten rows it returns
-`r00,r04,r08,r01,…` where the plaintext order is `r00..r09`. No error, a stable
-and plausible-looking meaningless order.
+A bare `ORDER BY col` on an EQL v3 domain is wrong — the bundle declares no btree
+operator class on any domain, so the sort falls through to jsonb's default
+`jsonb_cmp` and compares the envelope's keys in storage order, starting at the
+random ciphertext `c`. Measured over ten rows it returns `r00,r04,r08,r01,…`
+where the plaintext order is `r00..r09`. No error, a stable and
+plausible-looking meaningless order.
But the correct sort key is reachable without a function call. `eql_v3.ord_term`
returns the domain's `op` term, and OPE is order-preserving, so ordering by the
term reproduces the plaintext order. PostgREST cannot emit
-`ORDER BY eql_v3.ord_term(col)`, but it can emit a jsonb path. The builder now
-emits `order=col->op` for an encrypted ordering column, verified against a live
-PostgREST for `integer_ord` and `text_search` in both directions.
+`ORDER BY eql_v3.ord_term(col)`, but it can emit a jsonb path. The builder emits
+`order=col->op` for an encrypted ordering column, verified against a live
+PostgREST for `eql_v3_integer_ord` and `eql_v3_text_search` in both directions.
-The guard is now on the ordering FLAVOUR, not on encryption:
+The guard is on the ordering FLAVOUR, not on encryption:
-- **`ope` present → supported.** Every plain `*_ord` domain, plus `text_ord` and
- `text_search`.
+- **`ope` present → supported.** Every plain `eql_v3_*_ord` domain, plus
+ `eql_v3_text_ord` and `eql_v3_text_search`.
- **`ore` present → rejected.** The `ob` term is an array of ORE blocks whose
comparison needs the superuser-only operator class, which no jsonb path can
reach. (Such a column cannot hold data on managed Postgres anyway: its domain
- CHECK raises `ore_domain_unavailable`.) ORE columns are now excluded from
- `order()` at COMPILE time too, not only at runtime — `.order(oreColumn)` is a
- type error, matching the rejection.
+ CHECK raises `ore_domain_unavailable`.) ORE columns are excluded from `order()`
+ at COMPILE time as well as at runtime — `.order(oreColumn)` is a type error,
+ matching the rejection.
- **neither → rejected.** Storage-only, equality-only and match-only columns
carry no ordering term.
@@ -40,8 +39,8 @@ numeric and date domains, and per-character (16 hex chars each) for text, so
lexicographic order reproduces plaintext order including the prefix case
(`ada` < `adam`). `ope-term.integration.test.ts` pins that shape.
-`V3OrderableKeys` widens to admit OPE-backed ordering columns (`*_ord`,
-`text_ord`, `text_search`) while still excluding ORE (`*_ord_ore`) columns, so
-`order()` typechecks exactly where it works. `is(col, true)` is unaffected — it
-stays plaintext-only, and now has its own `V3PlaintextKeys` rather than
-borrowing the orderable set.
+`OrderableKeys` admits OPE-backed ordering columns (`eql_v3_*_ord`,
+`eql_v3_text_ord`, `eql_v3_text_search`) while excluding ORE
+(`eql_v3_*_ord_ore`) columns, so `order()` typechecks exactly where it works.
+`is(col, true)` is unaffected — it stays plaintext-only, with its own
+`PlaintextKeys` rather than borrowing the orderable set.
diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md
index 04c61a87f..f3f433eb8 100644
--- a/CONTRIBUTE.md
+++ b/CONTRIBUTE.md
@@ -104,24 +104,37 @@ We use [**Changesets**](https://github.com/changesets/changesets) to manage vers
- Follow the prompts to indicate the type of version bump (patch, minor, major).
- The [GitHub Actions workflows](./.github/workflows/) handle the **publish** step to npm once your PR is merged and the changeset is committed to `main`.
-## Pre release process
+Releases are **stable** — Changesets is not in pre mode. Merging a changeset to
+`main` opens (or updates) a "Version Packages" PR with plain semver versions, and
+merging that PR publishes to npm under the `latest` dist-tag.
-We currently use [changesets to manage pre-releasing](https://github.com/changesets/changesets/blob/main/docs/prereleases.md) the `next` version of the package, and the process is executed manually.
+The `stash` / `@cipherstash/stack` / `@cipherstash/stack-drizzle` /
+`@cipherstash/stack-supabase` / `@cipherstash/stack-prisma` / `@cipherstash/wizard`
+packages are a `fixed` group in [`.changeset/config.json`](./.changeset/config.json):
+they always version together, so a bump to any one of them bumps all six.
-To do so, you need to:
+## Pre-release process
-1. Check out the `next` branch
-2. Run `pnpm changeset pre enter next`
-3. Run `pnpm changeset version`
-4. Run `git add .`
-5. Run `git commit -m "Enter prerelease mode and version packages"`
-6. Run `pnpm changeset publish --tag next`
-7. Run `git push --follow-tags`
+The 1.0 line published its `1.0.0-rc.*` series through
+[changesets pre mode](https://github.com/changesets/changesets/blob/main/docs/prereleases.md).
+That mode is **exited** — do not re-enter it for ordinary work; a stable release
+is the default.
-When you are ready to release, you can run `pnpm changeset pre exit` to exit prerelease mode and commit the changes.
-When you merge the PR, the `next` branch will be merged into `main`, and the package will be published to npm without the prerelease tag.
+If a future line genuinely needs a prerelease series:
+
+1. Run `pnpm changeset pre enter rc` (this writes `.changeset/pre.json`)
+2. Commit that file — from then on, every "Version Packages" PR produces
+ `x.y.z-rc.N` versions and publishes under the `rc` dist-tag
+3. When the line is ready to go stable, run `pnpm changeset pre exit` and commit
+ the change. The next `changeset version` graduates every package to its final
+ version, consumes the accumulated changesets into `CHANGELOG.md`, and deletes
+ `.changeset/pre.json`
> [!IMPORTANT]
+> Pre mode retains every consumed changeset until exit, so its markdown is what
+> lands in the stable changelog — an entry that a later change made wrong stays
+> wrong until someone edits it. Review `.changeset/*.md` as a set before exiting.
+>
> This process can be dangerous, so please be careful when using it as it's difficult to undo mistakes.
> If you are unfamiliar with the process, please reach out to the maintainers for help.
diff --git a/examples/supabase-worker/supabase/functions/cipherstash-roundtrip/index.ts b/examples/supabase-worker/supabase/functions/cipherstash-roundtrip/index.ts
index ada5f2d07..db3a49f4d 100644
--- a/examples/supabase-worker/supabase/functions/cipherstash-roundtrip/index.ts
+++ b/examples/supabase-worker/supabase/functions/cipherstash-roundtrip/index.ts
@@ -18,7 +18,7 @@ import {
encryptedTable,
isEncrypted,
types,
-} from 'npm:@cipherstash/stack@^1.0.0-rc.3/wasm-inline'
+} from 'npm:@cipherstash/stack@1.0.0/wasm-inline'
// EQL v3: the WASM entry exports the v3 authoring surface only (`types.*`),
// not the v2 chainable builders — `encryptedColumn('email').equality()` is
diff --git a/packages/cli/src/__tests__/release-train.test.ts b/packages/cli/src/__tests__/release-train.test.ts
index 64dc1da48..4a322eac1 100644
--- a/packages/cli/src/__tests__/release-train.test.ts
+++ b/packages/cli/src/__tests__/release-train.test.ts
@@ -1,12 +1,34 @@
-import { readFileSync } from 'node:fs'
+import { readdirSync, readFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { INTEGRATION_ADAPTER_PACKAGES } from '../commands/init/steps/install-deps.js'
-import { RELEASE_TRAIN_MANIFESTS } from '../release-train.js'
+import {
+ RELEASE_TRAIN_MANIFESTS,
+ type ReleaseTrainPackage,
+} from '../release-train.js'
const CLI_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
const REPO_ROOT = resolve(CLI_ROOT, '../..')
+const SKILLS_ROOT = resolve(REPO_ROOT, 'skills')
+
+/** The stable version a workspace manifest belongs to: `1.0.0-rc.4` → `1.0.0`. */
+function stableVersion(version: string): string {
+ return version.split('-')[0] as string
+}
+
+function isReleaseTrainPackage(pkg: string): pkg is ReleaseTrainPackage {
+ return Object.hasOwn(RELEASE_TRAIN_MANIFESTS, pkg)
+}
+
+function manifestVersion(pkg: ReleaseTrainPackage): string {
+ const rel = RELEASE_TRAIN_MANIFESTS[pkg]
+ if (!rel) throw new Error(`${pkg} is not on the release train`)
+ const manifest = JSON.parse(readFileSync(resolve(CLI_ROOT, rel), 'utf8')) as {
+ version: string
+ }
+ return manifest.version
+}
// The growth guard for #661: `RELEASE_TRAIN_MANIFESTS` is the single source
// the build embeds versions from, and `INTEGRATION_ADAPTER_PACKAGES` is what
@@ -34,19 +56,71 @@ describe('release train coverage', () => {
})
it('pins the bare-project stash invocation in the published CLI skill', () => {
- const cliManifest = JSON.parse(
- readFileSync(resolve(CLI_ROOT, 'package.json'), 'utf8'),
- ) as { version: string }
const skill = readFileSync(
- resolve(REPO_ROOT, 'skills/stash-cli/SKILL.md'),
+ resolve(SKILLS_ROOT, 'stash-cli/SKILL.md'),
'utf8',
)
+ // The pin names the STABLE version of this release line, not the raw
+ // manifest version: the skill is read in a customer's repo against what is
+ // published, and a prerelease build's own `1.0.0-rc.4` would tell them to
+ // run a release candidate in production (#791).
expect(skill).toContain(
- `npx --package=stash@${cliManifest.version} stash eql install --database-url 'postgres://...'`,
+ `npx --package=stash@${stableVersion(manifestVersion('stash'))} stash eql install --database-url 'postgres://...'`,
)
})
+ // #791: `skills/` ships inside the `stash` tarball and is copied into
+ // customer repos by `stash init`, and NOTHING in the build rewrites the
+ // version literals inside it — `tsup.config.ts` copies the directory
+ // verbatim, and the `__STASH_RUNTIME_VERSIONS__` embed only reaches compiled
+ // CLI code. So a hardcoded `@cipherstash/stack@1.0.0-rc.4` would keep telling
+ // Deno and Supabase Edge users to pin a release candidate long after the
+ // stable release. This is the guard: it fires on the version-bump PR, when
+ // someone is in a position to update the surrounding prose too.
+ describe('skill version pins', () => {
+ const skillFiles = readdirSync(SKILLS_ROOT, { withFileTypes: true })
+ .filter((entry) => entry.isDirectory())
+ .map((entry) => ({
+ skill: entry.name,
+ body: readFileSync(
+ resolve(SKILLS_ROOT, entry.name, 'SKILL.md'),
+ 'utf8',
+ ),
+ }))
+
+ it('finds skills to check (a moved directory must not silently pass)', () => {
+ expect(skillFiles.length).toBeGreaterThan(0)
+ })
+
+ it.each(
+ skillFiles,
+ )('$skill pins release-train packages at a stable version on the current major', ({
+ body,
+ }) => {
+ // Exact pins only (`pkg@1.2.3`, optionally `npm:`-prefixed and with a
+ // trailing subpath). A range spec (`^1.0.0`) carries its own semantics
+ // and is not a pin to check.
+ const PIN =
+ /(?:npm:)?(stash|@cipherstash\/[a-z0-9-]+)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g
+
+ for (const [, pkg, pinned] of body.matchAll(PIN)) {
+ if (!isReleaseTrainPackage(pkg)) continue
+ const current = manifestVersion(pkg)
+
+ expect(
+ pinned,
+ `${pkg}@${pinned} pins a prerelease — skills ship to customers, so pin the stable release (${stableVersion(current)})`,
+ ).not.toContain('-')
+
+ expect(
+ pinned.split('.')[0],
+ `${pkg}@${pinned} is off this release line (${current}) — update the pin and the prose around it`,
+ ).toBe(stableVersion(current).split('.')[0])
+ }
+ })
+ })
+
it('every train manifest exists and carries a version (what tsup will embed)', () => {
// Exercises the exact inputs tsup.config.ts reads at build time, so a
// renamed/moved workspace package fails HERE in source-mode tests, not
diff --git a/skills/stash-cli/SKILL.md b/skills/stash-cli/SKILL.md
index 0b8e3f567..cf354bd8c 100644
--- a/skills/stash-cli/SKILL.md
+++ b/skills/stash-cli/SKILL.md
@@ -354,7 +354,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.0.0-rc.4 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.0.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.
#### `eql migration`
diff --git a/skills/stash-edge/SKILL.md b/skills/stash-edge/SKILL.md
index ae4a11efb..ca68d3c7c 100644
--- a/skills/stash-edge/SKILL.md
+++ b/skills/stash-edge/SKILL.md
@@ -56,15 +56,12 @@ build step.
```ts
import {
Encryption, encryptedTable, types, isEncrypted,
-} from 'npm:@cipherstash/stack@1.0.0-rc.4/wasm-inline'
+} from 'npm:@cipherstash/stack@1.0.0/wasm-inline'
```
**Pin an exact version.** Deno caches by specifier, so an unpinned import
-drifts between deploys. And while the package is on a prerelease line, a
-caret range does not do what it looks like: `@^1.0.0` will **not** match
-`1.0.0-rc.4`, because semver ranges exclude prereleases unless the range
-itself names one. Use the exact version, or a prerelease-bearing range
-(`@^1.0.0-rc.4`). Check what is current with `npm view @cipherstash/stack dist-tags`.
+drifts between deploys — pin, and bump the pin deliberately. Check what is
+current with `npm view @cipherstash/stack dist-tags`.
### Deno with an import map
@@ -74,7 +71,7 @@ name everywhere:
```jsonc
{
"imports": {
- "@cipherstash/stack/wasm-inline": "npm:@cipherstash/stack@1.0.0-rc.4/wasm-inline"
+ "@cipherstash/stack/wasm-inline": "npm:@cipherstash/stack@1.0.0/wasm-inline"
}
}
```