Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a11f0c8
feat(prisma-next): derive v3 domain catalog + codec ids from stack DO…
calvinbrewer Jul 14, 2026
1b1afb0
feat(prisma-next): v3 codec-id closed union, traits, invariants, drif…
calvinbrewer Jul 14, 2026
0161834
feat(prisma-next): v3 per-domain authoring constructors + *V2 aliases
calvinbrewer Jul 14, 2026
c91f63e
feat(prisma-next): v3 plain-JSONB cell codecs + EncryptedNumber envelope
calvinbrewer Jul 14, 2026
d59764d
feat(prisma-next): v3 bulk-encrypt middleware (plain-JSONB payloads)
calvinbrewer Jul 14, 2026
faa3976
feat(prisma-next): v3 query operators — eql_v3.* lowering + capabilit…
calvinbrewer Jul 14, 2026
c46f2ae
feat(prisma-next): v3 schema derivation, SDK adapter, runtime descrip…
calvinbrewer Jul 14, 2026
887088b
feat(prisma-next): EQL v3 bundle baseline migration from @cipherstash…
calvinbrewer Jul 14, 2026
a3fac7c
test(prisma-next): property-based suites for v3 wire, catalog, and ga…
calvinbrewer Jul 14, 2026
7bad0b9
test(prisma-next): v3 live-PG integration + bundling isolation pins
calvinbrewer Jul 14, 2026
5391dee
docs(prisma-next): changeset for EQL v3 per-domain authoring surface
calvinbrewer Jul 14, 2026
872a0b4
fix(prisma-next): wire the v3 ORM surface end-to-end
calvinbrewer Jul 14, 2026
a0f5ee0
feat(prisma-example): convert the example app to EQL v3
calvinbrewer Jul 14, 2026
467a385
Merge remote-tracking branch 'origin/main' into feat/prisma-next-eql-v3
calvinbrewer Jul 16, 2026
3f56667
refactor(prisma-next): consume @cipherstash/stack/adapter-kit, drop t…
calvinbrewer Jul 16, 2026
94b54fe
feat(prisma-next): EQL-derived v3 operator vocabulary with a guarded …
calvinbrewer Jul 16, 2026
1a8d0da
fix(prisma-next): PR #655 review hardening across the v3 seams
calvinbrewer Jul 16, 2026
69a2ef1
feat(prisma-example): adopt the eql* operator surface, regenerate the…
calvinbrewer Jul 16, 2026
06df347
test(prisma-next): test-kit family integration suite + CI job
calvinbrewer Jul 16, 2026
5a8e6da
fix(cli): stop init/plan/impl crashing on Prisma Next; ship stash-pri…
coderdan Jul 17, 2026
82796a7
docs(skills): fix three factual errors in stash-prisma-next flagged o…
coderdan Jul 17, 2026
29d0ea5
feat(cli): refuse `stash eql install` in a Prisma Next project (#683)
coderdan Jul 17, 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
23 changes: 23 additions & 0 deletions .changeset/eql-v3-prisma-next.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@cipherstash/prisma-next': minor
---

**Breaking:** EQL v3 columns are now authored through **concrete per-domain constructors** — the constructor you choose *is* the capability set. The legacy boolean-option surface (`EncryptedString({ equality, freeTextSearch, orderAndRange })`) is not carried into v3.

- New per-domain constructors, one per exposed `public.eql_v3_*` domain:
- Text: `EncryptedText` (storage), `EncryptedTextEq`, `EncryptedTextOrd` (eq + order/range), `EncryptedTextMatch` (free-text), `EncryptedTextSearch` (eq + free-text + order/range).
- Scalars (Integer, Smallint, BigInt, Numeric, Real, Double, Date, Timestamp): `Encrypted<Fam>` (storage), `Encrypted<Fam>Eq`, `Encrypted<Fam>Ord`.
- `EncryptedBoolean` — storage-only (`public.eql_v3_boolean`); there is no boolean equality constructor.
- `EncryptedJson` — searchable encrypted JSONB (`public.eql_v3_json`, `ste_vec`), queried with `eqlJsonContains` (`@>` containment). Selector querying (comparing the value at a JSONPath) is tracked in #677.
- **Impossible capability combinations have no constructor** (e.g. text equality + free-text without order/range) — they are unrepresentable, not runtime errors.
- **BigInt is a first-class v3 family** (`EncryptedBigInt` / `EncryptedBigIntEq` / `EncryptedBigIntOrd`, JS `bigint` plaintext, backed by `public.eql_v3_bigint*`).
- Use the `*V2` constructors (`EncryptedStringV2`, `EncryptedDoubleV2`, `EncryptedBigIntV2`, `EncryptedDateV2`, `EncryptedBooleanV2`, `EncryptedJsonV2`) to keep EQL v2 columns. A client is v2 or v3 — the two runtime descriptors are never co-registered.
- New `@cipherstash/prisma-next/v3` entry point: `cipherstashFromStackV3({ contractJson })` builds the v3 runtime descriptor, bulk-encrypt middleware, and a stack `EncryptionV3` client from the emitted contract.
- Query operators use an **EQL-derived vocabulary** (`eqlEq`, `eqlNeq`, `eqlIn`, `eqlNotIn`, `eqlGt`, `eqlGte`, `eqlLt`, `eqlLte`, `eqlBetween`, `eqlNotBetween`, `eqlJsonContains`; ordering via `eqlAsc` / `eqlDesc`), lowering to the same-named `eql_v3.*` functions with operands cast to the domain's query type (`$n::eql_v3.query_<domain>`); ordering uses `eql_v3.ord_term` / `eql_v3.ord_term_ore` by the column's ordering flavour. The domains are `public.eql_v3_*`; the operator functions live in the `eql_v3` schema. (The v2 surface keeps its `cipherstash*` names.)
- Free-text search is **`eqlMatch`** — fuzzy bloom token matching (`eql_v3.contains`), deliberately NOT named after SQL `ILIKE`: matching is case-insensitive, order/multiplicity-insensitive, and one-sided (may false-positive). Two guards run before encryption: SQL wildcards are normalised (leading/trailing `%` stripped; interior `%` or any `_` rejected), and needles the column's match index cannot answer (empty / below the tokenizer length) are rejected via the shared `matchNeedleError` guard. There is **no negated match operator** — negating a may-false-positive bloom test would silently drop matching rows.
- A new baseline migration `20260601T0100_install_eql_v3_bundle` (invariant `cipherstash:install-eql-v3-bundle-v1`) installs the `public.eql_v3_*` domains and `eql_v3.*` functions from the pinned `@cipherstash/eql` release. Regenerate contracts and run migrations after changing constructors.
- **The v3 ORM surface is fully wired end-to-end** (proven by converting `examples/prisma`):
- The generated `contract.d.ts` type surface covers every v3 codec id: `CodecTypes` gains all 40 `cipherstash/eql-v3/*@1` entries (envelope outputs — `number`-castAs domains decode to the new `EncryptedNumber` — plus trait-accurate operator visibility), and `QueryOperationTypes` gains the `eql*` operator set, surfaced on v3 columns via type-level `cipherstash:v3-*` marker traits (the v2 `cipherstash*` methods never appear on v3 columns, and vice-versa). Storage-only domains (including `EncryptedBoolean`) surface no operator methods at the type level, matching the runtime gate.
- The v3 runtime descriptor now presents the **pack id** (`cipherstash`) with v3's own version, so `postgres<Contract>({ extensions })` accepts contracts emitted by the cipherstash extension pack instead of failing with `RUNTIME.MISSING_EXTENSION_PACK`.
- Every v3 codec id registers a control-plane `expandNativeType` hook that strips the `public.` qualifier — `prisma-next migration plan` now renders `CREATE TABLE` columns as bare domain names (`eql_v3_bigint_ord`), matching what introspection reports, with **no `add_search_config` ops** (v3 domains carry their own index metadata). No `onFieldEvent` is registered for v3.
- The v3 bundle baseline migration op is reclassified `data` (it is a contract-shape-neutral self-edge; the aggregate integrity checker rejects self-edges without a data-class op), unblocking `prisma-next migration plan` / `migrate` in consuming apps.
5 changes: 5 additions & 0 deletions .changeset/prisma-example-eql-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cipherstash/prisma-next-example': minor
---

Convert the example app to EQL v3. Every column is now a concrete `public.eql_v3_*` domain authored with the per-domain constructors (`EncryptedTextSearch`, `EncryptedDoubleOrd`, `EncryptedBigIntOrd`, `EncryptedDateOrd`, `EncryptedBoolean`, `EncryptedJson`), wired through `cipherstashFromStackV3({ contractJson })`. The e2e harness runs the full v3 surface against live Postgres + ZeroKMS with no skips: the `eql*` operator vocabulary (equality/range plus `eqlMatch` free-text token search), `eqlAsc`/`eqlDesc` order-term sorting, encrypted JSON containment (`eqlJsonContains` — the v2 `cipherstashJsonb*` helpers do not exist in v3), lossless `bigint` beyond `Number.MAX_SAFE_INTEGER`, and the storage-only `eql_v3_boolean` refusal (`EncryptionOperatorError`) pinned as a feature. Migrations regenerate from the v3 contract: the initial app migration creates the `users` table against the v3 domains with zero `add_search_config` ops, and the cipherstash space carries both bundle baselines (v2 + v3).
24 changes: 24 additions & 0 deletions .changeset/stash-prisma-next-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'stash': minor
---

`stash init`, `stash plan`, and `stash impl` no longer crash on a Prisma Next
project. `SKILL_MAP` was missing a `prisma-next` entry, so the skills-install
and AGENTS.md-builder steps hit `SKILL_MAP[integration]` → `undefined` and threw
"not iterable" for any repo the CLI detected as Prisma Next. The entry is added
and both consumers now resolve skills through a `skillsFor()` helper that
degrades an unmapped integration to the base skill set instead of crashing
(`tsup` ships without type-checking, so the `Record<Integration>` type alone
didn't protect the build).

Ships a new **`stash-prisma-next`** agent skill documenting the EQL v3 Prisma
Next surface — the domain-named encrypted column types (`EncryptedTextSearch`,
`EncryptedDoubleOrd`, …), `cipherstashFromStackV3` wiring, the runtime value
envelopes, the `eql*` query operators, and EQL installation via
`prisma-next migration apply`. It is installed for Prisma Next projects and
inlined into `AGENTS.md` for editor agents.

`stash eql install` now refuses to run in a Prisma Next project (pointing you
at `prisma-next migration apply`, which owns EQL installation) unless you pass
`--force` — closing the manual-invocation hole that `stash init --prisma-next`
already avoided.
129 changes: 129 additions & 0 deletions .github/workflows/integration-prisma-next.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: Integration — prisma-next (EQL v3)

# Real ZeroKMS ciphertext against a real Postgres, on BOTH database variants —
# the prisma-next leg of the shared test-kit family driver (the same catalog,
# oracle, and single-vs-bulk crossover as the Drizzle and Supabase jobs).
#
# The prisma-next adapter talks straight to the database, so it does not need
# PostgREST — but it does need to work on managed Postgres, where the `postgres`
# role is not a superuser, the EQL install takes its self-skipping path, and the
# ORE domains cannot hold data. The Supabase compose file brings up PostgREST
# too; this job simply ignores it.
#
# Separate from `tests.yml` on purpose: these suites need CipherStash credentials
# and a database, and they THROW rather than skip when unconfigured. Keeping them
# out of the unit job is what lets `pnpm test` stay runnable with neither.

on:
push:
branches: [main]
paths:
- 'packages/stack/src/eql/v3/**'
- 'packages/prisma-next/**'
# Source layers the adapter's encoding/round-trip rests on: a break here
# (not just under src/eql/v3) can produce wrong rows, so trigger the live
# suite that would catch it.
- 'packages/stack/src/encryption/**'
- 'packages/stack/src/schema/**'
- 'packages/schema/**'
- 'packages/test-kit/**'
- 'packages/cli/src/installer/**'
- 'local/docker-compose.postgres.yml'
- 'local/docker-compose.supabase.yml'
- 'local/supabase-init.sql'
- '.github/workflows/integration-prisma-next.yml'
- '.github/actions/integration-setup/**'
pull_request:
branches: ['**']
# Repeated verbatim: GitHub Actions does not support YAML anchors/aliases.
paths:
- 'packages/stack/src/eql/v3/**'
- 'packages/prisma-next/**'
# Source layers the adapter's encoding/round-trip rests on: a break here
# (not just under src/eql/v3) can produce wrong rows, so trigger the live
# suite that would catch it.
- 'packages/stack/src/encryption/**'
- 'packages/stack/src/schema/**'
- 'packages/schema/**'
- 'packages/test-kit/**'
- 'packages/cli/src/installer/**'
- 'local/docker-compose.postgres.yml'
- 'local/docker-compose.supabase.yml'
- 'local/supabase-init.sql'
- '.github/workflows/integration-prisma-next.yml'
- '.github/actions/integration-setup/**'

jobs:
integration:
name: prisma-next v3 integration (db=${{ matrix.db }})
runs-on: blacksmith-4vcpu-ubuntu-2404
# Serialize every job that brings up the SAME docker-compose stack — same
# rationale and keys as the Drizzle/Supabase workflows, so the jobs queue
# on a shared runner rather than collide on the fixed host ports.
concurrency:
group: integration-live-db-${{ matrix.db }}
cancel-in-progress: false
# Fork PRs have no secrets. Skip cleanly rather than fail on something the
# contributor cannot fix — `tests.yml` still gives them a green signal.
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}

strategy:
fail-fast: false
# prisma-next talks straight to Postgres, so it runs against BOTH
# databases. The Supabase variant is not a formality: its `postgres` role
# is not a superuser, so the EQL install takes its self-skipping path. A
# suite that passes on a superuser database can still fail there.
matrix:
include:
- db: postgres
database-url: postgres://cipherstash:password@localhost:55432/cipherstash
pgrest-url: ''
- db: supabase
database-url: postgres://postgres:password@localhost:55433/postgres
pgrest-url: http://localhost:55430

env:
CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }}
CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }}
CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }}
CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }}
# Job-level env, not a `.env` file: `dotenv/config` does not override an
# already-set `process.env`, so these win and no secret is written to disk.
DATABASE_URL: ${{ matrix.database-url }}
PGRST_URL: ${{ matrix.pgrest-url }}
# EXPLICIT, never inferred — same rationale as the Drizzle workflow.
CS_IT_DB_VARIANT: ${{ matrix.db }}

steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/integration-setup

# Fast pre-flight: fail in seconds if a secret was rotated or cleared,
# before the docker pull. The in-test `requireIntegrationEnv` is the
# correctness guarantee; this is the cheap one.
- name: Require CipherStash secrets
uses: ./.github/actions/require-cs-secrets
with:
workspace-crn: ${{ vars.CS_WORKSPACE_CRN }}
client-id: ${{ vars.CS_CLIENT_ID }}
client-key: ${{ secrets.CS_CLIENT_KEY }}
client-access-key: ${{ secrets.CS_CLIENT_ACCESS_KEY }}

# Belt-and-braces for the concurrency guard: a run that was hard-killed
# can skip its `down` and leak a container holding the host port onto a
# REUSED runner. `|| true` so a clean runner is not an error.
- name: Clear any leaked containers from a prior run
run: docker compose -f local/docker-compose.${{ matrix.db }}.yml down -v --remove-orphans || true

- name: Start ${{ matrix.db }}
run: docker compose -f local/docker-compose.${{ matrix.db }}.yml up -d --wait

# `globalSetup` installs EQL v3 by shelling out to the real
# `stash eql install --eql-version 3`, so an installer regression fails
# here rather than hiding behind a test-only SQL apply.
- name: prisma-next v3 family suites
run: pnpm --filter @cipherstash/prisma-next run test:integration

- name: Stop ${{ matrix.db }}
if: always()
run: docker compose -f local/docker-compose.${{ matrix.db }}.yml down -v
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ If these variables are missing, tests that require live encryption will fail or
- `packages/cli`: The `stash` CLI — auth, init, encryption schema, and database setup (`stash eql install`). Has its own `AGENTS.md`.
- `packages/wizard`: AI-powered encryption setup (`@cipherstash/wizard`)
- `packages/migrate`: Plaintext-to-encrypted column migration (`@cipherstash/migrate`) — resumable backfill, per-column state
- `packages/prisma-next`: Prisma Next integration (`@cipherstash/prisma-next`) — searchable field-level encryption for Postgres
- `packages/prisma-next`: Prisma Next integration (`@cipherstash/prisma-next`) — searchable field-level encryption for Postgres. EQL v2 (`*V2` constructors, `cipherstashFromStack`) and EQL v3 (per-domain constructors, `./v3` entry with `cipherstashFromStackV3`)
- `packages/stack-drizzle`: Drizzle ORM integration (`@cipherstash/stack-drizzle`), depends on `@cipherstash/stack` — EQL v2 (`.`) and EQL v3 (`./v3`). Split out of `@cipherstash/stack`.
- `packages/stack-supabase`: Supabase integration (`@cipherstash/stack-supabase`), depends on `@cipherstash/stack` — `encryptedSupabase` (v2) and `encryptedSupabaseV3` (v3). Split out of `@cipherstash/stack`.
- `packages/schema`: Schema builder utilities and types (`encryptedTable`, `encryptedColumn`, `encryptedField`)
Expand All @@ -92,7 +92,7 @@ If these variables are missing, tests that require live encryption will fail or
- `e2e/*`: Cross-package end-to-end tests (package managers, supply chain, Prisma example README)
- `examples/*`: Working apps (basic, prisma, supabase-worker)
- `docs/plans/*`: Internal design plans. User-facing documentation lives at https://cipherstash.com/docs (not in this repo).
- `skills/*`: Agent skills (`stash-cli`, `stash-encryption`, `stash-drizzle`, `stash-dynamodb`, `stash-supabase`, `stash-supply-chain-security`)
- `skills/*`: Agent skills (`stash-cli`, `stash-encryption`, `stash-drizzle`, `stash-dynamodb`, `stash-supabase`, `stash-prisma-next`, `stash-supply-chain-security`)

## Agent Skills — these ship to customers

Expand All @@ -115,7 +115,7 @@ nothing type-checks them, and the damage lands in a customer's repo, not ours.
|---|---|
| `packages/cli` commands, flags, or prompts | `skills/stash-cli` |
| `packages/stack` encryption API, schema builders, subpath exports | `skills/stash-encryption` |
| Drizzle / Supabase / DynamoDB integrations | `skills/stash-drizzle`, `skills/stash-supabase`, `skills/stash-dynamodb` |
| Drizzle / Supabase / Prisma Next / DynamoDB integrations | `skills/stash-drizzle`, `skills/stash-supabase`, `skills/stash-prisma-next`, `skills/stash-dynamodb` |
| The rollout/cutover lifecycle (`packages/migrate`, `stash encrypt *`) | `skills/stash-encryption` and `skills/stash-cli` |
| pnpm config, CI workflows, dependency policy | `skills/stash-supply-chain-security` |
| The durable agent rules themselves | `packages/cli/src/commands/init/doctrine/AGENTS-doctrine.md` |
Expand Down
Loading
Loading