Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 36 additions & 0 deletions .changeset/encryption-schema-arrays.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
'@cipherstash/stack': minor
'@cipherstash/prisma-next': patch
---

`Encryption({ schemas })` now accepts any non-empty array of EQL v3 tables, not
only an array literal.

Previously the v3 signature required a non-empty *tuple*, so every indirect form
was rejected at compile time even though it worked at runtime:

```ts
// all of these used to fail with TS2769
export const schemas: AnyV3Table[] = [users, orders]
await Encryption({ schemas })

const readonlySchemas: ReadonlyArray<AnyV3Table> = [users, orders]
await Encryption({ schemas: readonlySchemas })

const built: AnyV3Table[] = []
built.push(users)
await Encryption({ schemas: built })
```

They all compile now. `Encryption({ schemas: [] })` is still a compile error, and
an array literal still gets full per-column typing — passing the wrong plaintext
for a column's domain, or a table the client was not built with, is still
rejected.

`EncryptionClientFor<S>` is widened to match, so it names the typed client for a
loose `readonly AnyV3Table[]` as well as for a tuple. Code that is generic over
its schemas — an adapter that builds a table per request, say — can now write
`EncryptionClientFor<readonly AnyV3Table[]>` and keep the typed surface.

If you narrowed a schema array to `readonly [AnyV3Table, ...AnyV3Table[]]` to
satisfy the old signature, that narrowing is no longer needed.
30 changes: 30 additions & 0 deletions .changeset/reject-v2-wire-over-v3-schemas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
'@cipherstash/stack': minor
---

`Encryption({ schemas, config: { eqlVersion: 2 } })` now throws when every table
in `schemas` is an EQL v3 table, instead of building a client that writes EQL v2
payloads into `eql_v3_*` columns.

`EncryptionV3` used to prevent this by forcing `eqlVersion: 3` over whatever the
caller passed — the override's comment said that was why it existed. Collapsing
`EncryptionV3` into a deprecated alias of `Encryption` removed the override, and
nothing else rejected the combination: `resolveEqlVersion` already threw for a
mixed v2/v3 schema set and for legacy v2 `searchableJson()`, but returned an
explicit version unchanged. So a caller upgrading from
`EncryptionV3({ schemas: [v3Table], config: { eqlVersion: 2 } })` — previously
auto-corrected, and working — silently got a v2-wire client instead, with no
diagnostic at any layer. The type surface agreed with the runtime (both say
"nominal client"), so nothing disagreed loudly enough to notice, and the failure
surfaced later as an `eql_v3_*` domain CHECK rejecting the write, or as v2 wire
landing in a v3 column wherever the check is looser.

The escape hatch itself is unchanged where it is actually used: an explicit
`eqlVersion: 2` over an **EQL v2** schema set still emits v2 wire, which is how
v2 payloads are minted for the read-compatibility suite. Mixed sets still throw
the existing mixing error. Reading v2 payloads is unaffected — `decrypt` and
`decryptModel` continue to read both generations regardless of the client's wire
version.

If you hit the new error, drop `config.eqlVersion` to emit v3, or build the
client from the EQL v2 schema you actually intend to write.
8 changes: 8 additions & 0 deletions .changeset/skills-encryption-client-naming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'stash': patch
---

`skills/stash-encryption` now documents how to name the client's type
(`EncryptionClientFor<S>`, not `Awaited<ReturnType<typeof Encryption>>`, which
always resolves to the untyped nominal client) and states that `schemas` accepts
any non-empty array of v3 tables rather than only an array literal.
10 changes: 4 additions & 6 deletions .changeset/stack-audit-on-decrypt.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@ return type changes, and the two-argument form reconstructs `Date` columns from
`cast_as` instead of leaving them as ISO strings. Code that read those columns as
strings needs updating.

The v3 overload takes a non-empty tuple of tables and a `V3ClientConfig` —
`ClientConfig` without the deprecated `eqlVersion` escape hatch. So
`Encryption({ schemas: [] })` no longer type-checks (it used to compile and then
throw), and `config: { eqlVersion: 2 }` selects the nominal overload, which is
the client you actually get back. Callers passing a plain `AnyV3Table[]` rather
than an array literal must narrow it to `readonly [AnyV3Table, ...AnyV3Table[]]`.
The v3 overload takes a `V3ClientConfig` — `ClientConfig` without the deprecated
`eqlVersion` escape hatch. So `Encryption({ schemas: [] })` no longer type-checks
(it used to compile and then throw), and `config: { eqlVersion: 2 }` selects the
nominal overload, which is the client you actually get back.
`Awaited<ReturnType<typeof Encryption>>` names the nominal client whatever you
pass, because `ReturnType` reads the last overload; use the exported
`EncryptionClientFor<S>` to name the client for a schema tuple.
Expand Down
25 changes: 25 additions & 0 deletions .changeset/typed-client-init-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@cipherstash/stack': patch
---

The typed EQL v3 client no longer throws `TypeError: init is not a function`.

`Encryption` selects its overload from the `config` argument but selects the
client it actually returns by inspecting the `schemas`, so the two can disagree.
Hoisting a config into a `ClientConfig`-typed variable is enough to split them —
`ClientConfig.eqlVersion` is `2 | 3`, which the v3 overload's `eqlVersion?: 3`
rejects — so the call types as the nominal `EncryptionClient` while the runtime
still returns the typed client:

```ts
const config: ClientConfig = { keyset }
const client = await Encryption({ schemas: [users], config })
// type: EncryptionClient · runtime: TypedEncryptionClient
```

`init` was the only member of `EncryptionClient` missing from the typed client,
so any call through the declared type crashed. It is now delegated.

The type still under-reports in this case: you lose the typed surface with no
diagnostic. Pass the config inline, or type the variable as `V3ClientConfig`, to
keep it.
34 changes: 34 additions & 0 deletions .changeset/typed-encrypt-query-dist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
'@cipherstash/stack': patch
---

Fixed: `encryptQuery` on the typed EQL v3 client did not typecheck against the
published package — for any column.

```ts
const users = encryptedTable('users', { email: types.TextSearch('email') })
const client = await Encryption({ schemas: [users] })

client.encrypt('a@b.com', { table: users, column: users.email }) // fine
client.encryptQuery('a@b.com', { table: users, column: users.email })
// TS2345: Argument of type '"a@b.com"' is not assignable to parameter of type 'never'
```

`PlaintextForColumn` and `QueryTypesForColumn` recover a column's domain with
`C extends EncryptedV3Column<infer D>`, which needs `D` to appear bare somewhere
in the instance type. Its only bare occurrence was a **private** field, and
`tsc` strips the types of private members on declaration emit — so in the
shipped `.d.ts` the inference fell back to the `V3DomainDefinition` constraint.
`QueryTypesForColumn` collapsed to `never`, which made `QueryableColumnsOf`
`never`, which typed every query plaintext `never`. `encrypt` was unaffected
because it resolves through `ColumnsOf`.

`EncryptedV3Column` now carries a type-only `declare readonly __domain?: D`.
Nothing is emitted at runtime and no call site changes; the declaration survives
emit and restores the inference site.

This affected every published release with the v3 typed client, including
`1.0.0-rc.4` — the searchable-query recipes in the `stash-encryption` skill did
not compile in a customer's repo. It was invisible in CI because the type tests
import source rather than `dist/`; a new `test:types:dist` suite now typechecks
the emitted declarations and is wired into CI.
33 changes: 33 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,39 @@ jobs:
- name: Typecheck (examples/basic — guards the v3 stack/stack-drizzle importers)
run: pnpm exec turbo run typecheck --filter @cipherstash/basic-example

# The rest of the surfaces that were compiling nowhere. Each of these has
# a `tsconfig.json` that covers its `src` and tests, and each was already
# clean — so they are gated now, before they drift. They resolve their
# workspace dependencies through `dist/*.d.ts`, hence the turbo filters
# (`^build` builds the dependencies first).
#
# NOT gated yet, and deliberately: `@cipherstash/stack` (147 errors under
# its own tsconfig), `@cipherstash/stack-drizzle` (63), `stash` (21) and
# `@cipherstash/stack-supabase` (11). Their `test:types` scripts only
# cover `__tests__/**/*.test-d.ts`, so `src`, the runtime test suites and
# `integration/**` compile nowhere. Most of the drizzle and supabase count
# is one root cause — `V3_MATRIX`'s `indexes` union in
# `@cipherstash/test-kit` — see #778.
- name: Typecheck (migrate)
run: pnpm exec turbo run typecheck --filter @cipherstash/migrate

- name: Typecheck (nextjs)
run: pnpm exec turbo run typecheck --filter @cipherstash/nextjs

- name: Typecheck (examples/prisma — guards the prisma-next importers)
run: pnpm exec turbo run typecheck --filter @cipherstash/prisma-next-example

- name: Typecheck (e2e)
run: pnpm exec turbo run typecheck --filter @cipherstash/e2e

# Everything else typechecks against SOURCE. This one reads the emitted
# `.d.ts`, which is what customers consume — and where typed `encryptQuery`
# sat broken for every column through the whole rc series, because `tsc`
# strips private member types and that was the only place the column's
# domain parameter appeared bare.
- name: Typecheck (stack — emitted declarations, not source)
run: pnpm exec turbo run test:types:dist --filter @cipherstash/stack

- name: Lint — no hardcoded package-manager runners
run: pnpm run lint:runners

Expand Down
Loading