Repository navigation
Version Packages - #1054
Open
github-actions[bot] wants to merge 1 commit into
Open
Version Packages#1054github-actions[bot] wants to merge 1 commit into
github-actions[bot] wants to merge 1 commit into
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
5 times, most recently
from
October 5, 2026 05:12
d7a9386 to
1f6ea6a
Compare
coderdan
approved these changes
Oct 5, 2026
Contributor
|
Fix for failing build is #1081. |
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
October 5, 2026 08:24
1f6ea6a to
84953be
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
2 times, most recently
from
October 5, 2026 23:19
ec5ad68 to
a2c616f
Compare
Contributor
|
Important Review skippedBot user detected. To trigger a single review, invoke the
|
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
October 6, 2026 06:03
a2c616f to
5d53c61
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
12 times, most recently
from
October 8, 2026 00:39
b5b5e57 to
f8ad930
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
14 times, most recently
from
October 23, 2026 00:37
d849d64 to
42aa754
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
6 times, most recently
from
October 30, 2026 01:27
c6bb725 to
97d1a34
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
8 times, most recently
from
November 6, 2026 02:58
7b2b6fa to
35c6308
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
November 11, 2026 00:23
35c6308 to
820fb67
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@cipherstash/auth@1.0.0
Minor Changes
fd0ee91:
OidcFederationStrategynow keeps one CTS token per distinct provider JWTinstead of one token for everyone.
getJwtis called on everygetToken()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). Previouslythe 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 (stack-auth
OidcFederationStrategyreuses one user's token for every user until it expires — lock context can bind to the wrong person #1045).getToken()now callsgetJwtin the caller's own async context and hands theJWT to a new
getTokenForJwt(jwt)method, on both the Node andwasm-inlineentries. Previously the Node binding ran
getJwtthrough a napi threadsafefunction, in the async context of
create(), so a callback that read therequest from
AsyncLocalStorage(Clerk'sauth(), Next.jsheaders()) sawno 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.
getTokenForJwtis also public, for a caller that alreadyholds the user's JWT; it shares the strategy's cache with
getToken().A token persisted through
createWithStore(for example a cookie) is nowserved 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_fromfield; tokens stored by earlier releases have none and arere-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.
createandcreateWithStoretake an optional
cacheCapacityafterbaseUrl(thecacheCapacityoption on@cipherstash/auth/wasm-inline); when the cache is full the least recentlyused JWT's token is dropped, that user is exchanged again on their next call,
and the eviction is logged at
debug.0caches nothing. Existing calls areunchanged.
Patch Changes
db76cf4:
@cipherstash/auth'sAutoStrategynow honoursCS_WORKSPACE_CRNwhen you are signed in through the CLI (no access key). The default@cipherstash/stackclient resolves credentials in@cipherstash/protect-ffiand does not pick this up until that package does. Previously the CRN was ignored and the CLI's current workspace was used, so switching workspaces with the CLI silently changed which workspace a process authenticated against. Now the session saved for the workspace named by the CRN is used, and every token is checked against that workspace, failing withWORKSPACE_MISMATCHotherwise. If no session exists for that workspace, the result isNOT_AUTHENTICATED. Without a CRN, behaviour is unchanged.The client key (
secretkey.json) thatstack-encryptreads from the CLI profile follows the same workspace, so the token and the client key never come from different ones.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
fetchhandles 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 gainshelpsaying what to check.INVALID_TOKENfor 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.SERVER_ERRORfor a refused token exchange names the HTTP status and the auth server'serror_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.INVALID_GRANT,INVALID_WORKSPACE_IDandALREADY_CONSUMEDgainhelp, andNOT_AUTHENTICATED's help namesstash auth login.STORE_ERRORcarries the help of the profile failure underneath it, such as logging in again when the profile file is missing.a076649: Sign-in and token refresh no longer trust an unbounded
expires_infrom theauth server. A token lifetime longer than 30 days is treated as 30 days, and a
device code is polled for at most one hour, so an oversized value can no
longer panic the process or wrap to an already-expired token. The buffers that
carry saved tokens and keys to and from
~/.cipherstash, and an access keyread from
CS_CLIENT_ACCESS_KEY, are now wiped from memory once used.A device code's lifetime now runs from when it was issued, and polling stops
at that deadline rather than at the next poll after it.
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.
stash@1.3.0
Minor Changes
bca02b6:
stash decrypt status|plan|backfill|dropmoves encrypted columns back to plaintext — the encrypt lifecycle run in reverse, with the same safety rails.decrypt planadds 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 fordrizzle-kit generate, or a reviewed SQL migration elsewhere.decrypt backfilldecrypts in resumable, checkpointed chunks, onebulkDecrypt(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 planfinds 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--forceto overwrite them from the ciphertext or--confirm-plaintext-currentto keep them. Once--forceis 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, anddecrypt droprefuses while any remain.decrypt dropre-verifies coverage at apply time, drops the indexes built on the column's EQL terms, drops the column, and with--uninstall-eqlremoves EQL v3 when it was the last encrypted column in the database.@cipherstash/migrate:runBackfillnow runs on a shared chunked engine (runChunkedMigration), which also drives the newrunDecryptBackfill. Forward behaviour and recorded event details are unchanged. AddsrunDecryptBackfill(resumes after a failed run;PlaintextWriteErrorkeeps decrypted values out of write errors; chunk size capped atMAX_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.MigrationPhaseandMigrationEventgain those members, so an exhaustiveswitchover them needs the new cases.Patch Changes
db76cf4:
@cipherstash/auth'sAutoStrategynow honoursCS_WORKSPACE_CRNwhen you are signed in through the CLI (no access key). The default@cipherstash/stackclient resolves credentials in@cipherstash/protect-ffiand does not pick this up until that package does. Previously the CRN was ignored and the CLI's current workspace was used, so switching workspaces with the CLI silently changed which workspace a process authenticated against. Now the session saved for the workspace named by the CRN is used, and every token is checked against that workspace, failing withWORKSPACE_MISMATCHotherwise. If no session exists for that workspace, the result isNOT_AUTHENTICATED. Without a CRN, behaviour is unchanged.The client key (
secretkey.json) thatstack-encryptreads from the CLI profile follows the same workspace, so the token and the client key never come from different ones.c2798f0:
stash encrypt dropandstash decrypt dropnow write the Drizzle migration to the directory drizzle-kit reports (theoutindrizzle.config.ts) instead of scanning--migrations-dir. Previously, when the two differed, the SQL could be written over an older migration with the same name while drizzle-kit journaled an empty stub in the realout, so the DROP never shipped. When a scan fallback is needed it matches the exact_<name>.sqlsuffix and refuses to overwrite any file that is not a drizzle-kit scaffold (blank or comment-only), and--migrations-dirnow warns when drizzle-kit wrote elsewhere.bde5b73:
@cipherstash/migrateaddsColumnMigration, 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 fromcipherstash.cs_migrations;confirmDualWrites(),recordDropGenerated()andrecordSourceRemoved()record events, refusing those the rules forbid with aColumnMigrationError.The pure rules are exported as
deriveColumnMigration,openCounterpart,planPassFor,passProgress,deriveTwinNameandmanifestTwin.runBackfillandrunDecryptBackfillnow 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 inforcedBy.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.
runBackfillnow throws aColumnMigrationErrorwith codetwin-mismatchwhenencryptedColumndiffers 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-currentwaiver.Error events store the error's class (
errorName) and safe details instead of its message, which could carry row data.stash decrypt plan,status,backfillanddropresolve every column throughColumnMigration, so they always agree on the twin and the phase.decrypt statusflagsforward-open,twin-has-valuesandforced-pass-owed.decrypt backfill --confirm-plaintext-currentis accepted on a later run as long as no forced pass has started, and refused while a started forced pass is owed.b15c1ad: Comparing an encrypted column with a different domain's query operand now raises instead of silently returning no rows. For example
public.eql_v3_text_ord = $1::eql_v3.query_text_eq, oreql_v3_text_ord < $1::eql_v3.query_text_ord_ope, previously fell through to nativejsonbcomparison and answered false for equal values. Within a type family, every pair of a column domain and another domain's query operand now resolves to a blocker for the operators both sides support, naming both operands and telling the caller to use the query domain that matches the column. Use the query domain of the column's own domain (eql_v3.query_<column domain>). The install bundle grows by about 1,060 functions and the same number of operators, so a single-transaction uninstall now takes about 8,550 locks (up from about 6,400); raisemax_locks_per_transactionon busy clusters.169f985: Fix: the
_ord_orecomparison functions (eql_v3.lt,lte,gt,gte,eq,neq), and the operators on_ord_orecolumns when named with their schema(
col OPERATOR(public.<) $1), no longer give wrong answers whenpublicis noton the
search_path. These functions are kept free of a pinnedsearch_pathso PostgreSQL can inline them into index scans, which means theylook up their comparison operator through the caller's
search_path. The OREcomparison operators live in
public, so a caller without it, such as aSECURITY DEFINERfunction withSET search_path = ''(the hardening Supabaserecommends), got PostgreSQL's built-in record comparison instead. That compares
the ciphertext bytes, so equality and ordering were silently wrong rather than
raising an error. Every generated comparison function now names its operator
with its schema (
OPERATOR(public.<)for ORE,OPERATOR(pg_catalog.<)for theHMAC, bloom filter and OPE terms), so the result no longer depends on the
search_path, and the functions still inline. A barecol < $1still needspublicon thesearch_path, because the operators on encrypted columns arecreated in
public; without it, call the function (eql_v3.lt(col, $1)) orname the operator's schema. Reinstall or upgrade EQL to pick up the fix.
The
stash-postgresskill now explains how to query encrypted columns from afunction or role whose
search_pathleaves outpublic.587d06e: The supply-chain skill names
pipamong 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 withpip install --require-hashes --no-deps.bd7e55f: Fix invalid YAML in
skills/stash-managed-platforms/SKILL.mdfrontmatter: theunquoted
descriptioncontained a colon-space, so any YAML parser rejected thefile (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.
43967d3: Two validation errors no longer quote the caller's search term: a free-text search term that is too short, and a
like/ilikepattern with wildcards the encrypted search cannot honour. Each now reports the term's length instead.The Supabase and DynamoDB adapters' error logs now record a failure's kind instead of its full message, which could quote a plaintext value. The kind is the error class plus any known encryption error type, FFI error code or auth code it carries; anything else is left out. The DynamoDB
options.loggerhook now receives a copy of the error whose message is that description, with the samecode. The full message is still returned to the caller and passed toerrorHandler.@cipherstash/stack/adapter-kitexports the shareddescribeFailureandlikeWildcardErrorhelpers that produce these messages.fd0ee91: Document how
OidcFederationStrategyserves many users through one client: thegetJwtcallback runs on every operation, in the caller's async context, andmust 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
stack-auth
OidcFederationStrategyreuses one user's token for every user until it expires — lock context can bind to the wrong person #1045). One caveat is documented for the@cipherstash/stackclient on the native entry, which calls the strategy from a background task so
a request read from
AsyncLocalStorageis not visible insidegetJwtthereyet: capture the request in the closure or build the client per request until
@cipherstash/stackruns a second encryption engine (protect-ffi) — new major built on stack-encrypt (umbrella) #1065 lands. The docs and thestash-auth,stash-encryption,stash-supabaseandstash-edgeskills say so, and say that on earlier@cipherstash/authreleases a strategy caches one token for everyone, so aclient there must serve one user.
ae63dcd:
skills/stash-prisma: the stale-migrations/cipherstash/table now describeseql-3.1.0. The baseline bakes 3.1.0, 1.2.x shipped 3.0.6, and the table names
the
db initrefusal a stale 1.2.x directory produces.bbb1bfa:
encrypton atypes.Jsoncolumn now types its result asEncrypted | nullwhen the plaintext may benull. Anulldocument has always resolved to{ data: null }at runtime (it is stored as SQL NULL, not encrypted), but the result was typedEncrypted, soresult.data.ccompiled and then threw.encryptis now generic over the plaintext's type: a literalnull, or a value typedJsonDocument, givesdata: Encrypted | null, through.withLockContext()and.audit()too. A value TypeScript knows is non-null, and every scalar column, still givesdata: Encrypted. No runtime change. If your code readsresult.dataafter encrypting a Json value that may benull, it must now check fornullfirst.EncryptOperationandEncryptOperationWithLockContexttake an optional result type parameter (defaultEncrypted), and theEncryptResult<P>helper type is exported from@cipherstash/stack/encryption.@cipherstash/stack-supabase: the per-term query-encryption fallback (used when the client has nobulkEncrypt) now rejects anullenvelope instead of sending the string"null"as a filter operand, matching the bulk path.stash: thestash-encryptionskill now says whatencryptreturns for anullJson document.8280acf: The
stash-dynamodbskill's query examples now narrow theencryptQueryresult before readinghm. The previousresult.data.hmdid not type-check, because the result type also covers ciphertexts, strings andnull.9a7815d: The
stash-dynamodbskill now scopes the no-re-encryption upgrade to the tested top-level scalar equality columns and unindexed text written by@cipherstash/protect-dynamodb@6.0.1. It explains boolean lookup changes and the privacy cost of a plaintext replacement, stale__hmacremoval,downcasenormalization, and the required legacy JSON backfill with its rollback limit. It also clarifies legacy v2 reads and queries using current v3 schemas.ef3765b: The
stash-supabaseskill now says that tables with no encrypted columns areread and written through the same
es.from(), and that an embedded relation toan encrypted table is refused rather than decrypted.
Updated dependencies [db76cf4]
Updated dependencies [a1765f1]
Updated dependencies [fb75f40]
Updated dependencies [a076649]
Updated dependencies [457b28e]
Updated dependencies [b480735]
Updated dependencies [bde5b73]
Updated dependencies [b15c1ad]
Updated dependencies [9412408]
Updated dependencies [37bf4cf]
Updated dependencies [e930e5b]
Updated dependencies [0275684]
Updated dependencies [c851db5]
Updated dependencies [169f985]
Updated dependencies [af28159]
Updated dependencies [fd0ee91]
Updated dependencies [f7f1c76]
Updated dependencies [bca02b6]
@cipherstash/migrate@1.1.0
Minor Changes
bde5b73:
@cipherstash/migrateaddsColumnMigration, 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 fromcipherstash.cs_migrations;confirmDualWrites(),recordDropGenerated()andrecordSourceRemoved()record events, refusing those the rules forbid with aColumnMigrationError.The pure rules are exported as
deriveColumnMigration,openCounterpart,planPassFor,passProgress,deriveTwinNameandmanifestTwin.runBackfillandrunDecryptBackfillnow 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 inforcedBy.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.
runBackfillnow throws aColumnMigrationErrorwith codetwin-mismatchwhenencryptedColumndiffers 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-currentwaiver.Error events store the error's class (
errorName) and safe details instead of its message, which could carry row data.stash decrypt plan,status,backfillanddropresolve every column throughColumnMigration, so they always agree on the twin and the phase.decrypt statusflagsforward-open,twin-has-valuesandforced-pass-owed.decrypt backfill --confirm-plaintext-currentis 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|dropmoves encrypted columns back to plaintext — the encrypt lifecycle run in reverse, with the same safety rails.decrypt planadds 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 fordrizzle-kit generate, or a reviewed SQL migration elsewhere.decrypt backfilldecrypts in resumable, checkpointed chunks, onebulkDecrypt(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 planfinds 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--forceto overwrite them from the ciphertext or--confirm-plaintext-currentto keep them. Once--forceis 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, anddecrypt droprefuses while any remain.decrypt dropre-verifies coverage at apply time, drops the indexes built on the column's EQL terms, drops the column, and with--uninstall-eqlremoves EQL v3 when it was the last encrypted column in the database.@cipherstash/migrate:runBackfillnow runs on a shared chunked engine (runChunkedMigration), which also drives the newrunDecryptBackfill. Forward behaviour and recorded event details are unchanged. AddsrunDecryptBackfill(resumes after a failed run;PlaintextWriteErrorkeeps decrypted values out of write errors; chunk size capped atMAX_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.MigrationPhaseandMigrationEventgain those members, so an exhaustiveswitchover them needs the new cases.Patch Changes
languages/typescript/in cipherstash/stack. The previous release linked toits old folder under
packages/, which no longer exists, so the README'ssource links and the npm page's repository link returned 404.
@cipherstash/stack@1.3.0
Minor Changes
bbb1bfa:
encrypton atypes.Jsoncolumn now types its result asEncrypted | nullwhen the plaintext may benull. Anulldocument has always resolved to{ data: null }at runtime (it is stored as SQL NULL, not encrypted), but the result was typedEncrypted, soresult.data.ccompiled and then threw.encryptis now generic over the plaintext's type: a literalnull, or a value typedJsonDocument, givesdata: Encrypted | null, through.withLockContext()and.audit()too. A value TypeScript knows is non-null, and every scalar column, still givesdata: Encrypted. No runtime change. If your code readsresult.dataafter encrypting a Json value that may benull, it must now check fornullfirst.EncryptOperationandEncryptOperationWithLockContexttake an optional result type parameter (defaultEncrypted), and theEncryptResult<P>helper type is exported from@cipherstash/stack/encryption.@cipherstash/stack-supabase: the per-term query-encryption fallback (used when the client has nobulkEncrypt) now rejects anullenvelope instead of sending the string"null"as a filter operand, matching the bulk path.stash: thestash-encryptionskill now says whatencryptreturns for anullJson document.Patch Changes
ef3765b:
@cipherstash/stack/adapter-kitnow exportsisEncryptedPayload, the checkmodel decryption uses to find an encrypted value in a row. The Supabase adapter
uses it to refuse a row it would otherwise return undecrypted.
43967d3: Two validation errors no longer quote the caller's search term: a free-text search term that is too short, and a
like/ilikepattern with wildcards the encrypted search cannot honour. Each now reports the term's length instead.The Supabase and DynamoDB adapters' error logs now record a failure's kind instead of its full message, which could quote a plaintext value. The kind is the error class plus any known encryption error type, FFI error code or auth code it carries; anything else is left out. The DynamoDB
options.loggerhook now receives a copy of the error whose message is that description, with the samecode. The full message is still returned to the caller and passed toerrorHandler.@cipherstash/stack/adapter-kitexports the shareddescribeFailureandlikeWildcardErrorhelpers that produce these messages.fd0ee91: Document how
OidcFederationStrategyserves many users through one client: thegetJwtcallback runs on every operation, in the caller's async context, andmust 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
stack-auth
OidcFederationStrategyreuses one user's token for every user until it expires — lock context can bind to the wrong person #1045). One caveat is documented for the@cipherstash/stackclient on the native entry, which calls the strategy from a background task so
a request read from
AsyncLocalStorageis not visible insidegetJwtthereyet: capture the request in the closure or build the client per request until
@cipherstash/stackruns a second encryption engine (protect-ffi) — new major built on stack-encrypt (umbrella) #1065 lands. The docs and thestash-auth,stash-encryption,stash-supabaseandstash-edgeskills say so, and say that on earlier@cipherstash/authreleases a strategy caches one token for everyone, so aclient there must serve one user.
4b14ee6: Fix the README's OIDC federation example, which passed the
ResultfromOidcFederationStrategy.create()straight toconfig.authStrategyand failed inEncryption()withopts.strategy.getToken is missing.It now unwraps the
Resultand passesstrategy.data, and so does theLockContextJSDoc example.20ccab3: Internal type cleanup of the encryption client (no runtime change). The exported operation classes
EncryptOperation,EncryptQueryOperation,BatchEncryptQueryOperationandBulkEncryptOperationnow declare that they accept atypes.Jsondocument as plaintext, which they always did at runtime; theirgetOperation()return types widen to match. TheEncryptionClientinterface changes only in whatencryptreturns for atypes.Jsoncolumn; see theEncryptResultchangeset.Updated dependencies [db76cf4]
Updated dependencies [a1765f1]
Updated dependencies [fb75f40]
Updated dependencies [a076649]
Updated dependencies [457b28e]
Updated dependencies [b480735]
Updated dependencies [fd0ee91]
Updated dependencies [457b28e]
Updated dependencies [f7f1c76]
@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 theeql_v3schema comment reads
'3.1.0') and the ORE block comparator behind everyordered 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
_oretext columns before migrating; seeU-001.
No domain, wire format or index term changes.
Two artefacts carry the new bundle:
20261005T0000_upgrade_eql_v3_3_1_0, carrying theinvariant
cipherstash:upgrade-eql-v3-bundle-3.1.0-v1. Existing databasesre-install through it on the next
prisma-next migration planfollowed byprisma-next migrate.migratealone is not enough: onlymigration plancopies the new directory into your repo.20260601T0100_install_eql_v3_bundle, whichnow 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 initkeeps 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
migratewalks only the 3.1.0 edge. 1.0.0 and1.1.x shipped eql-3.0.4, so
migratewalks the 3.0.5, 3.0.6 and 3.1.0 edges inone 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:Your database keeps its markers, so already-applied invariants are not re-run.
If you skip the delete, a fresh
db initfrom a 1.2.x directory refuses withOperation 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.5edge 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-prismahad481 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: nullgenesis edge, would add another permanent ~2.6 MB copy ofthe 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
43967d3: Two validation errors no longer quote the caller's search term: a free-text search term that is too short, and a
like/ilikepattern with wildcards the encrypted search cannot honour. Each now reports the term's length instead.The Supabase and DynamoDB adapters' error logs now record a failure's kind instead of its full message, which could quote a plaintext value. The kind is the error class plus any known encryption error type, FFI error code or auth code it carries; anything else is left out. The DynamoDB
options.loggerhook now receives a copy of the error whose message is that description, with the samecode. The full message is still returned to the caller and passed toerrorHandler.@cipherstash/stack/adapter-kitexports the shareddescribeFailureandlikeWildcardErrorhelpers that produce these messages.Updated dependencies [ef3765b]
Updated dependencies [b15c1ad]
Updated dependencies [9412408]
Updated dependencies [37bf4cf]
Updated dependencies [e930e5b]
Updated dependencies [0275684]
Updated dependencies [c851db5]
Updated dependencies [169f985]
Updated dependencies [43967d3]
Updated dependencies [fd0ee91]
Updated dependencies [4b14ee6]
Updated dependencies [bbb1bfa]
Updated dependencies [20ccab3]
@cipherstash/eql@3.1.0
Minor Changes
eql_v3_internal.compare_ore_block_256_termand
compare_ore_block_256_terms, behind every ordered ORE operator and the OREoperator 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
for how to find such rows before upgrading. A timing harness is in
packages/eql/tests/timing/ore_block_256/.eql-bindingscrate resolves an EQL type named as a string to its own Stack Encrypt plan (stack-encryptfeature).eql_bindings::encryption::targetscarries 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 plaintextValueKindit takes, the indexes it carries, its query twin, and whether the engine can produce it today with the reason when not — andencrypt/decrypt/queryentry points that dispatch on the name and run the type's derivedEncryptFrom/DecryptInto, resolving to the EQL value's JSON bytes through the engine'sPendingso a guest batches it with the rest of a plan. This is the EQL half of EQL types as plan field targets (Go and Rust plans cannot target EQL domains, so Go writes rows EQL cannot query — make domains plan field targets #1062): a Go data plan namesTextEqand the guest returns the finished EQL value instead of assembling one.TextEqis the only producible type; every other name is refused with the plan's reason. The SQL surface and the TypeScript package are unchanged.TextEqandTextEqQuerysupport Stack Encrypt's target-directed API behind thestack-encryptfeature. 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.Patch Changes
b15c1ad: Comparing an encrypted column with a different domain's query operand now raises instead of silently returning no rows. For example
public.eql_v3_text_ord = $1::eql_v3.query_text_eq, oreql_v3_text_ord < $1::eql_v3.query_text_ord_ope, previously fell through to nativejsonbcomparison and answered false for equal values. Within a type family, every pair of a column domain and another domain's query operand now resolves to a blocker for the operators both sides support, naming both operands and telling the caller to use the query domain that matches the column. Use the query domain of the column's own domain (eql_v3.query_<column domain>). The install bundle grows by about 1,060 functions and the same number of operators, so a single-transaction uninstall now takes about 8,550 locks (up from about 6,400); raisemax_locks_per_transactionon busy clusters.9412408: Drop the
CREATE CAST (public.eql_v3_json_search AS eql_v3.query_json)that PostgreSQL always ignored (it warned "cast will be ignored because the source data type is a domain" on every install).doc::eql_v3.query_jsonwas never converting; calleql_v3.to_ste_vec_query(col)instead. Also corrects theeql-bindingswire-format docs:opbacks_ord/_ord_ope(andtext_ord/text_searchcarryhm+op),obonly_ord_ore.37bf4cf: Encrypted JSON arrays now agree between the Rust/TypeScript bindings and the SQL.
SteVecDocumentmodels the rootaarray flag that the SQL root validator accepts and the array accessors require, so an array document the database accepts also parses in the bindings. Theaflag on documents and entries is now optional but not nullable in the generated TypeScript and JSON Schema (a?: boolean), matching the SQL CHECK; anullis rejected instead of failing later with a cast error.eql_v3.jsonb_array_lengthreturns 0 for an empty array (it returned NULL), andeql_v3.jsonb_array_elementsandeql_v3.jsonb_document_contains(jsonb[], jsonb)no longer raise "upper bound of FOR loop cannot be null" on one.169f985: Fix: the
_ord_orecomparison functions (eql_v3.lt,lte,gt,gte,eq,neq), and the operators on_ord_orecolumns when named with their schema(
col OPERATOR(public.<) $1), no longer give wrong answers whenpublicis noton the
search_path. These functions are kept free of a pinnedsearch_pathso PostgreSQL can inline them into index scans, which means theylook up their comparison operator through the caller's
search_path. The OREcomparison operators live in
public, so a caller without it, such as aSECURITY DEFINERfunction withSET search_path = ''(the hardening Supabaserecommends), got PostgreSQL's built-in record comparison instead. That compares
the ciphertext bytes, so equality and ordering were silently wrong rather than
raising an error. Every generated comparison function now names its operator
with its schema (
OPERATOR(public.<)for ORE,OPERATOR(pg_catalog.<)for theHMAC, bloom filter and OPE terms), so the result no longer depends on the
search_path, and the functions still inline. A barecol < $1still needspublicon thesearch_path, because the operators on encrypted columns arecreated in
public; without it, call the function (eql_v3.lt(col, $1)) orname the operator's schema. Reinstall or upgrade EQL to pick up the fix.
The
stash-postgresskill now explains how to query encrypted columns from afunction or role whose
search_pathleaves outpublic.@cipherstash/nextjs@4.4.1
Patch Changes
3bd3d28: Harden the CipherStash session cookie (
__cipherstash_cts_session).httpOnlyandsecure. It holds a bearer token and nothing in this package reads it client-side; if your own client code read it, move that read to the server.secureapplies in every environment: browsers accept it fromhttp://localhost, but a plain-HTTP development server on another host (a LAN address, for example) will no longer keep the session.issandsub) of the OIDC token it was minted from, andprotectMiddlewareandprotectClerkMiddlewarereplace a session left behind by another user (for example after a Clerk multi-sessionsetActiveswitch, or a sign-out on a route the middleware does not match). If a replacement cannot be minted, the stale cookie is cleared. Cookies written by earlier versions carry no owner and are re-minted once. Ownership is read without verifying a signature, so the token you pass toprotectMiddlewareorgetCtsTokenmust be one your application has already authenticated.getCtsToken(oidcToken)only returns the cookie session when it belongs to the issuer and subject ofoidcToken, and otherwise fetches a fresh session for that token.getCtsToken()with no argument is deprecated. It returns the session cookie without checking who it belongs to, because it has no identity to compare it with, so pass the user's token whenever you have one. If a user signs out on a route the middleware does not match, callresetCtsTokenthere.expiryalready past) is reported as a failure instead of being written to the cookie or thrown out of the middleware.GetCtsTokenResponsenow usessuccess: trueandsuccess: falseas literal discriminants, so checkingresult.successnarrowsresult.ctsTokento aCtsToken.NextResponse.next()or a rewrite), the request forwarded to route handlers and server components is updated in the same request, sogetCtsToken()there never sees the previous user's session. Next.js does not do this for route handlers on any release tested, from 14.0.0 to 15.5.29. If the response you pass in already overrides the forwarded request headers, the forwarded session cookie is set to the one the middleware decided on, whether your override replaced the cookie or left it out. A session cookie your response already sets is likewise replaced with the kept session, or cleared when none was kept.getToken()no longer fails the request. It is treated as no token being available, so a stale session is still cleared.x-middleware-set-cookieresponse header whenever the session changes, sohttpOnlydoes not protect it on those responses. Upgrade to at least 14.2.25 or 15.2.3, which also fix a critical Next.js middleware bypass (GHSA-f82v-jwr5-mffw).f7f1c76: The package's source links now point at its folder under
languages/typescript/in cipherstash/stack. The previous release linked toits old folder under
packages/, which no longer exists, so the README'ssource links and the npm page's repository link returned 404.
@cipherstash/protect-ffi@0.33.1
Patch Changes
(affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
languages/typescript/in cipherstash/stack. The previous release linked toits old folder under
packages/, which no longer exists, so the README'ssource links and the npm page's repository link returned 404.
@cipherstash/protect-ffi-darwin-arm64@0.33.1
Patch Changes
(affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
languages/typescript/in cipherstash/stack. The previous release linked toits old folder under
packages/, which no longer exists, so the README'ssource links and the npm page's repository link returned 404.
@cipherstash/protect-ffi-darwin-x64@0.33.1
Patch Changes
(affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
languages/typescript/in cipherstash/stack. The previous release linked toits old folder under
packages/, which no longer exists, so the README'ssource links and the npm page's repository link returned 404.
@cipherstash/protect-ffi-linux-arm64-gnu@0.33.1
Patch Changes
(affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
languages/typescript/in cipherstash/stack. The previous release linked toits old folder under
packages/, which no longer exists, so the README'ssource links and the npm page's repository link returned 404.
@cipherstash/protect-ffi-linux-x64-gnu@0.33.1
Patch Changes
(affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
languages/typescript/in cipherstash/stack. The previous release linked toits old folder under
packages/, which no longer exists, so the README'ssource links and the npm page's repository link returned 404.
@cipherstash/protect-ffi-linux-x64-musl@0.33.1
Patch Changes
(affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
languages/typescript/in cipherstash/stack. The previous release linked toits old folder under
packages/, which no longer exists, so the README'ssource links and the npm page's repository link returned 404.
@cipherstash/protect-ffi-win32-x64-msvc@0.33.1
Patch Changes
(affecting rustls 0.23.13 up to 0.23.44). The package's API is unchanged.
languages/typescript/in cipherstash/stack. The previous release linked toits old folder under
packages/, which no longer exists, so the README'ssource links and the npm page's repository link returned 404.
@cipherstash/stack-drizzle@1.3.0
Patch Changes
@cipherstash/stack-supabase@1.3.0
Patch Changes
43967d3: Two validation errors no longer quote the caller's search term: a free-text search term that is too short, and a
like/ilikepattern with wildcards the encrypted search cannot honour. Each now reports the term's length instead.The Supabase and DynamoDB adapters' error logs now record a failure's kind instead of its full message, which could quote a plaintext value. The kind is the error class plus any known encryption error type, FFI error code or auth code it carries; anything else is left out. The DynamoDB
options.loggerhook now receives a copy of the error whose message is that description, with the samecode. The full message is still returned to the caller and passed toerrorHandler.@cipherstash/stack/adapter-kitexports the shareddescribeFailureandlikeWildcardErrorhelpers that produce these messages.bbb1bfa:
encrypton atypes.Jsoncolumn now types its result asEncrypted | nullwhen the plaintext may benull. Anulldocument has always resolved to{ data: null }at runtime (it is stored as SQL NULL, not encrypted), but the result was typedEncrypted, soresult.data.ccompiled and then threw.encryptis now generic over the plaintext's type: a literalnull, or a value typedJsonDocument, givesdata: Encrypted | null, through.withLockContext()and.audit()too. A value TypeScript knows is non-null, and every scalar column, still givesdata: Encrypted. No runtime change. If your code readsresult.dataafter encrypting a Json value that may benull, it must now check fornullfirst.EncryptOperationandEncryptOperationWithLockContexttake an optional result type parameter (defaultEncrypted), and theEncryptResult<P>helper type is exported from@cipherstash/stack/encryption.@cipherstash/stack-supabase: the per-term query-encryption fallback (used when the client has nobulkEncrypt) now rejects anullenvelope instead of sending the string"null"as a filter operand, matching the bulk path.stash: thestash-encryptionskill now says whatencryptreturns for anullJson document.ab24980: Reading a table that has no encrypted columns no longer fails. Such a table is
not registered with the encryption client, and the adapter still asked the
client to decrypt its rows, so every
select()on it, and every insert, updateor upsert followed by
.select(), returned "decryptModel received a table thisclient was not initialized with" (or threw, with
throwOnError). On thewasm-inlineentry, writes to such a table failed the same way. The adapter nowpasses the rows of a table with no encrypted columns straight through, in both
introspection and declared mode, without calling the encryption client.
A row from such a table that still holds an encrypted value, from an embedded
relation to an encrypted table or from an undeclared
eql_v3_*column indeclared mode, is refused with an error naming the column, rather than returned
as ciphertext.
Updated dependencies [ef3765b]
Updated dependencies [43967d3]
Updated dependencies [fd0ee91]
Updated dependencies [4b14ee6]
Updated dependencies [bbb1bfa]
Updated dependencies [20ccab3]
@cipherstash/wizard@1.3.0
Patch Changes
@modelcontextprotocol/sdk^1.32.0as a direct dependency so theauto-installed peer of
@anthropic-ai/claude-agent-sdkresolves to a releasepatched 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.
@cipherstash/auth-darwin-arm64@1.0.0
@cipherstash/auth-darwin-x64@1.0.0
@cipherstash/auth-linux-arm64-gnu@1.0.0
@cipherstash/auth-linux-x64-gnu@1.0.0
@cipherstash/auth-linux-x64-musl@1.0.0
@cipherstash/auth-win32-x64-msvc@1.0.0
@cipherstash/e2e@0.0.8
Patch Changes
@cipherstash/basic-example@1.2.19
Patch Changes
@cipherstash/prisma-example@0.1.5
Patch Changes
@cipherstash/bench@0.0.10
Patch Changes
@cipherstash/ffi-integration-tests@1.0.3
Patch Changes
@cipherstash/test-kit@0.0.6
Patch Changes