Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4b14ee6
docs(stack): unwrap the OidcFederationStrategy Result in the README e…
coderdan Oct 23, 2026
c311f82
Merge pull request #1342 from cipherstash/docs/unwrap-oidc-federation…
coderdan Oct 23, 2026
b480735
fix(stack-auth)!: finish token refreshes after caller cancellation
coderdan Oct 12, 2026
6052281
test(auth): run native guest exchange under Tokio
coderdan Oct 12, 2026
6a16ca4
test(auth): cover token expiry boundaries in refresh
coderdan Oct 12, 2026
e71d061
fix(stack-auth): give the initiating caller the token its refresh ins…
coderdan Oct 18, 2026
f1b6a7c
test(stack-auth): run the cancellation soak on a multi-thread runtime
coderdan Oct 18, 2026
0b57ae7
docs(golang): say why the auth guest's block_on is wasm32-only
coderdan Oct 18, 2026
d0bdaec
ci(release-plz): run the auth refresh soak only on an armed crates line
coderdan Oct 18, 2026
e930e5b
fix(eql): constant operation count for the ORE block comparator
coderdan Oct 10, 2026
4f2a15d
test(eql): timing harness for the ORE block comparator
coderdan Oct 10, 2026
e3ca82f
fix(eql): close the remaining ORE comparator timing paths
coderdan Oct 11, 2026
e0cea1a
test(eql): ORE comparator ordering test, harness classes for the clos…
coderdan Oct 11, 2026
609eeef
docs(eql): say the timing harness samples pairs with replacement
coderdan Oct 11, 2026
19945c4
fix(eql): check term widths before well-formedness in the ORE array c…
coderdan Oct 13, 2026
bf9ad90
test(eql): equal-term timing class, and report a failed database probe
coderdan Oct 13, 2026
38246d2
docs(eql): minor bump and an upgrade note for the comparator's new er…
coderdan Oct 13, 2026
195eaf6
fix(stack-prisma): re-bake the eql-3.1.0 migrations with the new ORE …
coderdan Oct 28, 2026
b26ed8d
Merge pull request #1172 from cipherstash/fix/1170-cancel-safe-auth-r…
coderdan Oct 28, 2026
9ff5185
Merge pull request #1148 from cipherstash/fix/eql-ore-block-256-const…
coderdan Oct 28, 2026
5521be2
chore(deps): bump golang.org/x/tools in /languages/golang
dependabot[bot] Oct 28, 2026
8ae0c70
chore(deps): bump the cargo-minor-patch group across 1 directory with…
dependabot[bot] Oct 28, 2026
f705a08
chore(deps): bump the cargo-minor-patch group across 1 directory with…
dependabot[bot] Oct 28, 2026
65c5581
chore(deps-dev): bump the dev-dependencies group across 1 directory w…
dependabot[bot] Oct 28, 2026
2a533ac
chore(deps): bump the cargo-minor-patch group across 4 directories wi…
dependabot[bot] Oct 28, 2026
b04dce0
Merge pull request #1375 from cipherstash/dependabot/npm_and_yarn/dev…
freshtonic Oct 30, 2026
aa53596
Merge pull request #1413 from cipherstash/dependabot/cargo/cargo-mino…
freshtonic Oct 30, 2026
7b5b873
Merge pull request #1411 from cipherstash/dependabot/cargo/packages/e…
freshtonic Oct 30, 2026
a376696
Merge pull request #1410 from cipherstash/dependabot/cargo/languages/…
freshtonic Oct 30, 2026
3a923ef
Merge pull request #1409 from cipherstash/dependabot/go_modules/langu…
freshtonic Oct 30, 2026
13bab61
Merge main into next
coderdan Oct 30, 2026
5697451
test(stack-encrypt): import proptest's Rng in the JSON leakage tests
coderdan Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cargo/mutants.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ exclude_re = [
# `a_builder_without_a_transport_is_refused_when_there_is_no_bundled_one`.
'stack-auth/src/error\.rs:\d+:\d+: replace RequestError::is_no_transport -> bool with false$',
'stack-auth/src/auto_strategy\.rs:150:9: replace AutoStrategy::detect_inner -> Result<Self, AuthError> with Ok\(Default::default\(\)\)$',
'stack-auth/src/token_store\.rs:(258|266):9: replace <impl TokenStore for TokenStoreFn<L, S>>::(load|save)',
'stack-auth/src/token_store\.rs:(261|269):9: replace <impl TokenStore for TokenStoreFn<L, S>>::(load|save)',
# stack-auth — equivalent.
'stack-auth/src/(access_key|oidc)_refresher\.rs:\d+:\d+: replace <impl Refresher for \w+(<P>)?>::try_credential -> Option<Self::Credential> with Some\(Default::default\(\)\)$',
'stack-auth/src/error\.rs:\d+:\d+: replace AuthErrorKind::payload -> serde_json::Map<String, serde_json::Value> with Default::default\(\)$',
Expand Down
5 changes: 5 additions & 0 deletions .changeset/cancelled-auth-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cipherstash/auth': patch
---

Keep native token refreshes running when their caller is cancelled, so rotated device-session credentials are saved and subsequent requests can still authenticate. Close a refresh-waiter notification race, and give the caller that started a refresh the new token rather than the expiring one it replaced.
19 changes: 13 additions & 6 deletions .changeset/eql-3-1-0-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
'@cipherstash/stack-prisma': minor
---

Move the bundled EQL v3 migrations to **eql-3.1.0**. EQL 3.1.0 is a minor
release for the Rust `eql-bindings` crate. Its install SQL changes only the
version stamp: `eql_v3.version()` returns `'3.1.0'` and the `eql_v3` schema
comment reads `'3.1.0'`. No function, operator, domain or index behaviour
changes.
Move the bundled EQL v3 migrations to **eql-3.1.0**. Its install SQL changes
the version stamp (`eql_v3.version()` returns `'3.1.0'` and the `eql_v3`
schema comment reads `'3.1.0'`) and the ORE block comparator behind every
ordered ORE operator and the ORE operator class. The comparator now runs a
constant number of operations whatever two values share, closing a timing
leak. Ordering results are unchanged for well-formed values, but a block-ORE
text value with a malformed or mismatched-width term now raises on every
comparison against it. Check your `_ore` text columns before migrating; see
[U-004](../../../../packages/eql/docs/upgrading/v3.1.md#u-004-block-ore-text-comparisons-check-every-term).
No domain, wire format or index term changes.

Two artefacts carry the new bundle:

Expand Down Expand Up @@ -52,4 +57,6 @@ baseline. The team chose it on these numbers: `@cipherstash/stack-prisma` had
481 npm downloads in the 30 days to 3 October 2026, and 384 in the last 7 of
those days, 305 of them on the 1.2.0 release day. The append-only alternative,
a second `from: null` genesis edge, would add another permanent ~2.6 MB copy of
the bundle for a release that changes no SQL behaviour.
the bundle. That choice was made when 3.1.0 changed only the version stamp; the
comparator change came after it and does not alter the trade, since every
database reaches the same bundle either way.
21 changes: 21 additions & 0 deletions .changeset/eql-ore-block-256-constant-op-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@cipherstash/eql': minor
---

Security: the ORE block comparator (`eql_v3_internal.compare_ore_block_256_term`
and `compare_ore_block_256_terms`, behind every ordered ORE operator and the ORE
operator class) now runs a constant number of operations for operands of the
same shape (term count, term width, NULL positions), whether and wherever they
differ. Previously its run time depended on where two values first differ, so a
party able to time queries could learn how long a prefix their query value
shares with stored values. For text the effect was large: a comparison took
about 11 µs when the first term differs and 47 µs when only the sixth does.
Results are unchanged. Single-term comparisons cost about 6% more when the
terms differ and about 36% more (roughly 1 µs) when they are equal, since equal
terms now run the hash too; text comparisons now take about 18 µs whatever the
operands. Multi-term (text) values now check every paired term, so a
malformed or mismatched-width term after the first difference, or one paired
with a NULL element, raises instead of being skipped; see
[U-004](../../docs/upgrading/v3.1.md#u-004-block-ore-text-comparisons-check-every-term)
for how to find such rows before upgrading. A timing harness is in
`packages/eql/tests/timing/ore_block_256/`.
2 changes: 1 addition & 1 deletion .changeset/eql-v4-block-ore-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@cipherstash/eql': minor
---

**EQL v4 compares ore-rs's 6-bit block ORE, and Stack Encrypt produces the block ORE types `IntegerOrdOre`, `SmallintOrdOre`, `BigintOrdOre`, `RealOrdOre`, `DoubleOrdOre`, `TextOrdOre` and `TextSearchOre`.** A v4 `ob` term is one ore-rs ciphertext: the fixed-width scheme for numbers, the chained scheme for text (padded to a power-of-two length bucket of at least 16 bytes). The v4 bundle names its block ORE objects `ore_block_v2` (`eql_v4_internal.ore_block_v2_term`, `compare_ore_block_v2_term`) and compares them with a new comparator that checks each term's shape, raises on malformed or mismatched terms, and runs a constant operation count for terms of one shape. The EQL v3 bundle is unchanged, byte for byte: its `ob` is still cipherstash-client's 8-bit block ORE under `ore_block_256`. See [U-002](../../docs/upgrading/v3.1.md#u-002-the-block-ore-term-is-ore-rss-6-bit-scheme): the comparator refuses the 8-bit terms the previous `eql_v4` bundle compared, so rows or operands holding them must be written again through Stack Encrypt.
**EQL v4 compares ore-rs's 6-bit block ORE, and Stack Encrypt produces the block ORE types `IntegerOrdOre`, `SmallintOrdOre`, `BigintOrdOre`, `RealOrdOre`, `DoubleOrdOre`, `TextOrdOre` and `TextSearchOre`.** A v4 `ob` term is one ore-rs ciphertext: the fixed-width scheme for numbers, the chained scheme for text (padded to a power-of-two length bucket of at least 16 bytes). The v4 bundle names its block ORE objects `ore_block_v2` (`eql_v4_internal.ore_block_v2_term`, `compare_ore_block_v2_term`) and compares them with a new comparator that checks each term's shape, raises on malformed or mismatched terms, and runs a constant operation count for terms of one shape. This change leaves the EQL v3 bundle as it was: its `ob` is still cipherstash-client's 8-bit block ORE under `ore_block_256`. See [U-002](../../docs/upgrading/v3.1.md#u-002-the-block-ore-term-is-ore-rss-6-bit-scheme): the comparator refuses the 8-bit terms the previous `eql_v4` bundle compared, so rows or operands holding them must be written again through Stack Encrypt.
2 changes: 1 addition & 1 deletion .changeset/eql-v4-order-term-shape.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@cipherstash/eql': minor
---

**An `eql_v4` order term is one hex string, and every `eql_v4` domain checks its shape when it is written.** A block ORE term (`ob`, and the `lo` and `hi` bounds of a block ORE range) is the lowercase hex of the whole ore-rs ciphertext, `"ob": "0202…"`, where it was a one-element array. Every `eql_v4` domain carrying `ob` must now hold a string starting `0202` or `0203`, and one carrying `op` a string starting `00` or `01`: an EQL v3 term or a term of the wrong scheme is refused with `check_violation` at the write or cast, not at the first comparison. The check reads only the prefix, so its cost does not grow with the term. In `eql-bindings`, a v4 `ob` field is Stack Encrypt's `OreBlockTerm` and an `op` field its `OpeTerm`, typed by the plaintext they order (`DateOrdOre::ob` is an `OreBlockTerm<Date>`), and reading a payload checks the term's structure. The `eql_v3` bundle is unchanged, byte for byte. A database that installed an earlier `eql_v4` bundle keeps its stored domains across an install, so it must drop its stored order domains first: see [U-003](../../docs/upgrading/v3.1.md#u-003-an-order-term-is-one-hex-string-checked-when-it-is-written).
**An `eql_v4` order term is one hex string, and every `eql_v4` domain checks its shape when it is written.** A block ORE term (`ob`, and the `lo` and `hi` bounds of a block ORE range) is the lowercase hex of the whole ore-rs ciphertext, `"ob": "0202…"`, where it was a one-element array. Every `eql_v4` domain carrying `ob` must now hold a string starting `0202` or `0203`, and one carrying `op` a string starting `00` or `01`: an EQL v3 term or a term of the wrong scheme is refused with `check_violation` at the write or cast, not at the first comparison. The check reads only the prefix, so its cost does not grow with the term. In `eql-bindings`, a v4 `ob` field is Stack Encrypt's `OreBlockTerm` and an `op` field its `OpeTerm`, typed by the plaintext they order (`DateOrdOre::ob` is an `OreBlockTerm<Date>`), and reading a payload checks the term's structure. This change leaves the `eql_v3` bundle as it was. A database that installed an earlier `eql_v4` bundle keeps its stored domains across an install, so it must drop its stored order domains first: see [U-003](../../docs/upgrading/v3.1.md#u-003-an-order-term-is-one-hex-string-checked-when-it-is-written).
2 changes: 1 addition & 1 deletion .changeset/eql-v4-text-search-optional-terms.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
'@cipherstash/eql': minor
---

**An EQL v4 text search query carries only the terms its operator reads.** `eql_v4.query_text_search` and `eql_v4.query_text_search_ore` now require at least one of their terms (`hm`, `op` or `ob`, `bf`) instead of all of them, so `col = $1` and `col < $1` work on text too short to cut a 3-gram, which has no match term. Each extractor over such a query raises (`invalid_parameter_value`) wherever it reads the operand when the query lacks the term it reads, or holds it as JSON `null` or an empty array, naming the domain, the term and the operators that compare it: `=` without `hm`, `<` without `op`, `@@` without `bf`. So a missing term raises rather than matching no row silently, or every row; the raise is at evaluation, so a generic plan that never reaches the predicate returns no row and no error. Stored `eql_v4_text_search` values still carry every term. The `eql_v3` domains are unchanged: the v3 bundle is byte for byte what it was. With it, **Stack Encrypt produces `TextSearch`** (`eql-bindings`, `stack-encrypt` feature): `TextSearchQuery`'s terms are `Option`s, built per index through `TextSearchEqualityQuery`, `TextSearchOrderQuery` and `TextSearchMatchQuery`, and the stored value carries every term: text too short for a 3-gram stores the empty filter (`"bf": []`, the new `v4::terms::SearchBloomFilter` field type), so `=` and `<` find it and no `@@` query does, where `TextMatch` still refuses it. `TextSearchQuery`'s `Option` terms and `TextSearch`'s `SearchBloomFilter` change the fields of the unreleased `v4` module only; no published `eql-bindings` has it. See [U-001](../../docs/upgrading/v3.1.md#u-001-a-text-search-query-carries-only-the-terms-its-operator-reads) for what a hand-built query must carry, the raise, and the rollback.
**An EQL v4 text search query carries only the terms its operator reads.** `eql_v4.query_text_search` and `eql_v4.query_text_search_ore` now require at least one of their terms (`hm`, `op` or `ob`, `bf`) instead of all of them, so `col = $1` and `col < $1` work on text too short to cut a 3-gram, which has no match term. Each extractor over such a query raises (`invalid_parameter_value`) wherever it reads the operand when the query lacks the term it reads, or holds it as JSON `null` or an empty array, naming the domain, the term and the operators that compare it: `=` without `hm`, `<` without `op`, `@@` without `bf`. So a missing term raises rather than matching no row silently, or every row; the raise is at evaluation, so a generic plan that never reaches the predicate returns no row and no error. Stored `eql_v4_text_search` values still carry every term. This change leaves the `eql_v3` domains as they were. With it, **Stack Encrypt produces `TextSearch`** (`eql-bindings`, `stack-encrypt` feature): `TextSearchQuery`'s terms are `Option`s, built per index through `TextSearchEqualityQuery`, `TextSearchOrderQuery` and `TextSearchMatchQuery`, and the stored value carries every term: text too short for a 3-gram stores the empty filter (`"bf": []`, the new `v4::terms::SearchBloomFilter` field type), so `=` and `<` find it and no `@@` query does, where `TextMatch` still refuses it. `TextSearchQuery`'s `Option` terms and `TextSearch`'s `SearchBloomFilter` change the fields of the unreleased `v4` module only; no published `eql-bindings` has it. See [U-001](../../docs/upgrading/v3.1.md#u-001-a-text-search-query-carries-only-the-terms-its-operator-reads) for what a hand-built query must carry, the raise, and the rollback.
6 changes: 6 additions & 0 deletions .changeset/readme-unwrap-oidc-federation-result.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cipherstash/stack": patch
---

Fix the README's OIDC federation example, which passed the `Result` from `OidcFederationStrategy.create()` straight to `config.authStrategy` and failed in `Encryption()` with `opts.strategy.getToken is missing`.
It now unwraps the `Result` and passes `strategy.data`, and so does the `LockContext` JSDoc example.
8 changes: 5 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,10 @@ updates:

# ── Cargo (the root workspace — the stack-* crates) ─────────────
# The root workspace (the six stack-* crates and the three node binding
# crates) and the five detached workspaces beside it: the three cargo-fuzz
# crates and the two Go WASI guests. Each has its own Cargo.lock, and each
# is its own workspace root, so each is listed.
# crates) and the six detached workspaces beside it: the three cargo-fuzz
# crates, the two Go WASI guests and the EQL ORE timing harness's ciphertext
# generator. Each has its own Cargo.lock, and each is its own workspace
# root, so each is listed.
- package-ecosystem: cargo
directories:
- /
Expand All @@ -260,6 +261,7 @@ updates:
- /packages/stack-encrypt/fuzz
- /languages/golang/encrypt/guest
- /languages/golang/auth/guest
- /packages/eql/tests/timing/ore_block_256/ctgen
# Lockfile only, like the other two cargo entries. Every one of these
# workspaces shares crates by path with another: the fuzz crates and both
# guests depend on root-workspace crates, the encrypt guest also on
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/_test-auth-refresh-soak.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Auth refresh cancellation soak

on:
workflow_call:
inputs:
long:
type: boolean
default: false

permissions:
contents: read

jobs:
soak:
runs-on: blacksmith-16vcpu-ubuntu-2204
timeout-minutes: 30
env:
MISE_ENV: test
NEXTEST_PROFILE: ci
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
with:
version: 2026.4.0
install: true
working_directory: .
cache: true
cache_key_prefix: mise-v1-ubuntu-2204
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Short seeded soak
if: ${{ !inputs.long }}
run: cargo nextest run --locked -p stack-auth --all-features -E 'test(cancellation_soak_short)' --success-output final
- name: Long seeded soak
if: inputs.long
run: mise run test:soak:stack-auth
30 changes: 29 additions & 1 deletion .github/workflows/release-plz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,9 +225,37 @@ jobs:
id: armed
run: node scripts/eql-pipeline-armed.mjs crates

# Gated on the switch so an unarmed line spends nothing on the soak. It
# still runs on every armed push, before release-crates' preflight decides
# whether any crate is unpublished, so a failed soak holds stack-kms and
# stack-encrypt too.
auth-refresh-soak:
name: Auth refresh cancellation soak before publishing
needs: [crates-armed]
if: needs.crates-armed.outputs.armed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
env:
MISE_ENV: test
NEXTEST_PROFILE: ci
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
# Release workflows never restore caches. The PR workflow runs the
# same mise task with caches; only toolchain setup differs here.
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
with:
version: 2026.4.0
install: true
working_directory: .
cache: false
- name: Long seeded soak
run: mise run test:soak:stack-auth

release-crates:
name: "Release the stack-* crates"
needs: [crates-armed]
needs: [crates-armed, auth-refresh-soak]
if: needs.crates-armed.outputs.armed == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down
50 changes: 50 additions & 0 deletions .github/workflows/tests-auth-refresh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Tests (auth refresh cancellation)

# The short run covers every PR, including Version Packages. The longer run
# also covers release PRs and changes to the engine, refreshers or toolchain.
on:
pull_request: {}
workflow_dispatch: {}

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
short:
uses: ./.github/workflows/_test-auth-refresh-soak.yml

changes:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
refresh: ${{ steps.paths.outputs.refresh }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: dorny/paths-filter@0e4a8c6effa4802afeda77dc8d303f8176d7dfad # v3
id: paths
with:
filters: |
refresh:
- 'packages/stack-auth/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'mise*.toml'
- '.config/nextest.toml'
- '.github/workflows/*auth-refresh*.yml'
- '.github/workflows/release-plz.yml'

long:
needs: changes
if: github.event_name == 'workflow_dispatch' || github.head_ref == 'changeset-release/main' || needs.changes.outputs.refresh == 'true'
uses: ./.github/workflows/_test-auth-refresh-soak.yml
with:
long: true
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,11 @@ exact pin would stop the suite sharing one `cts-common` with it.
`mutants.yml`, `fuzz.yml`, `miri.yml` and `udeps.yml`.
`scripts/__tests__/crates-ci.test.mjs` fails when a mise task in the root
`mise.toml` or the five crate `tasks.toml` files loses its last CI caller.
- **Auth refresh cancellation soak:** `tests-auth-refresh.yml` runs a short,
seeded, virtual-time soak on every PR and the long run on auth changes and
Version Packages PRs. `release-plz.yml` also requires the long run before
`release-crates` can publish. Run it locally with `mise run
test:soak:stack-auth`; it needs no credentials or live services.

### Fuzzing

Expand Down
Loading
Loading