Skip to content
Merged
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
24 changes: 24 additions & 0 deletions .changeset/add-prisma-next-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@cipherstash/prisma-next": minor
"stash": minor
---

Add `@cipherstash/prisma-next` — searchable application-layer encryption for Postgres with Prisma Next. The framework's migration system installs the EQL bundle in the same `prisma-next migration apply` sweep that creates the application schema; no separate `stash db install` step.

**`@cipherstash/prisma-next` (new package, initial release)**

- **Six encrypted column types** — `EncryptedString`, `EncryptedDouble`, `EncryptedBigInt`, `EncryptedDate`, `EncryptedBoolean`, `EncryptedJson` — declared via PSL constructors (`cipherstash.Encrypted*()`) or TS factories (`encryptedString()`, etc.).
- **17 query operators** — 13 predicate operators surfaced as column methods (`cipherstashEq`, `cipherstashIlike`, `cipherstashGt`, `cipherstashBetween`, `cipherstashInArray`, `cipherstashJsonbPathExists`, …) and 4 free-standing helpers (`cipherstashAsc`, `cipherstashDesc`, `cipherstashJsonbPathQueryFirst`, `cipherstashJsonbGet`).
- **Per-codec search-mode flags** (`equality`, `freeTextSearch`, `orderAndRange`, `searchableJson`) drive the EQL search-config indices the codec lifecycle hook emits at migration time. Defaults to `true` across the board.
- **One-call setup** via `cipherstashFromStack({ contractJson })` from `@cipherstash/prisma-next/stack` — derives the stack `encryptedTable` / `encryptedColumn` schemas from `contract.json` (single source of truth, no duplicate hand-written declarations), constructs the `@cipherstash/stack` `EncryptionClient`, builds the framework-native `CipherstashSdk` adapter, and returns ready-to-spread `{ extensions, middleware, encryptionClient }` for `postgres<Contract>({...})`.
- **Layered API** — `deriveStackSchemas(contractJson)` and `createCipherstashSdk(client, schemas)` exposed as primitives for advanced users (custom keysets, multi-tenant routing, non-stack KMS).
- **Bulk-encrypt middleware** (`bulkEncryptMiddleware(sdk)`) coalesces every plaintext placeholder across a query into one `bulkEncrypt` SDK round-trip per `(table, column)` group. `decryptAll(rows)` does the symmetric coalescing on the read side.
- **Misconfig diagnostic** — if the user constructs the runtime descriptor but forgets to register `bulkEncryptMiddleware(sdk)` against the same SDK, the codec's encode throws a `RUNTIME.ENCODE_FAILED` envelope with a copy-pasteable wiring snippet at the first encrypted write.
- **Subpath exports** — `./stack`, `./control`, `./runtime`, `./middleware`, `./pack`, `./column-types`; tree-shakable along the control / runtime / middleware seams.
- **Contributes an EQL contract space** — installs the `eql_v2` schema, `eql_v2_encrypted` composite type, `ore_*` types, EQL functions / operators / casts via the cipherstash extension's baseline migration. Runs in the same control-plane sweep as the application schema.
- **Full docs**: https://cipherstash.com/docs/stack/cipherstash/encryption/prisma-next.

**`stash` (new feature)**

- **`stash init --prisma-next`** — new init provider for Prisma Next projects. Reuses `authenticate` + `resolve-database` + `install-deps` (additionally installs `@cipherstash/prisma-next`), skips `install-eql` (the framework handles it via `prisma-next migration apply`) and `build-schema` (`cipherstashFromStack` derives schemas from the contract — no hand-written encryption client file). Detected automatically when a `prisma-next.config.*` or `@cipherstash/prisma-next` dependency is present in the project.
- **`detectPrismaNext(cwd)`** — new export from `commands/db/detect.ts` mirroring the existing `detectDrizzle` / `detectSupabase` helpers.
118 changes: 118 additions & 0 deletions .github/workflows/prisma-next-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Prisma Next E2E

# End-to-end tests for `@cipherstash/prisma-next`: spins up a real
# Postgres container, applies the cipherstash baseline migration
# (EQL bundle install) + the example app's schema, then runs the
# suite at `examples/prisma/test/e2e/` against a live ZeroKMS
# workspace.
#
# Triggers only on changes that affect the package or the example
# (the unit-test suite in `tests.yml` covers everything that doesn't
# need a live workspace).

on:
push:
branches:
- main
paths:
- 'packages/prisma-next/**'
- 'examples/prisma/**'
- '.github/workflows/prisma-next-e2e.yml'
pull_request:
branches:
- '**'
paths:
- 'packages/prisma-next/**'
- 'examples/prisma/**'
- '.github/workflows/prisma-next-e2e.yml'

jobs:
e2e:
name: Run Prisma Next E2E
runs-on: blacksmith-4vcpu-ubuntu-2404

# Skip cleanly on fork PRs where secrets aren't available. The
# global-setup hook in the suite hard-errors when `CS_WORKSPACE_CRN`
# is unset; gating at the job level produces a clean "skipped"
# status instead of a noisy failure.
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}

env:
CS_WORKSPACE_CRN: ${{ secrets.CS_WORKSPACE_CRN }}
CS_CLIENT_ID: ${{ secrets.CS_CLIENT_ID }}
CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }}
CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }}

steps:
- name: Checkout Repo
uses: actions/checkout@v6

- uses: pnpm/action-setup@v6.0.3
name: Install pnpm
with:
run_install: false

- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'

# node-pty's install hook falls back to `node-gyp rebuild` when no
# linux-x64 prebuild matches. pnpm/action-setup v6 no longer ships
# node-gyp on PATH, so install it explicitly.
- name: Install node-gyp
run: npm install -g node-gyp

- name: Install dependencies
run: pnpm install --frozen-lockfile

# Write the CS_* credentials and the harness DATABASE_URL into the
# example app's .env so the runtime + the `prisma-next migration
# apply` invocation in global-setup both pick them up. The harness
# also overrides DATABASE_URL inside the test process to point at
# the container, but the migration:apply subprocess relies on
# prisma-next.config.ts → process.env['DATABASE_URL'] being set
# before the test runner spawns it.
- name: Create .env file in examples/prisma
run: |
touch ./examples/prisma/.env
echo "DATABASE_URL=postgres://cipherstash:cipherstash@localhost:54329/cipherstash_e2e" >> ./examples/prisma/.env
echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./examples/prisma/.env
echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./examples/prisma/.env
echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./examples/prisma/.env
echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./examples/prisma/.env

# Build via turbo so the `^build` dependency on
# `@cipherstash/stack` (which `@cipherstash/prisma-next` imports
# `/schema` from) is honoured. A bare
# `pnpm --filter @cipherstash/prisma-next build` bypasses the
# task graph and leaves the upstream dist/ empty, surfacing as
# `Cannot find module '@cipherstash/stack/schema'` from tsc.
- name: Build @cipherstash/prisma-next
run: pnpm exec turbo run build --filter @cipherstash/prisma-next

- name: Emit example contract
run: pnpm --filter @cipherstash/prisma-next-example emit

- name: Start E2E Postgres container
working-directory: examples/prisma
run: |
docker compose -f test/e2e/docker-compose.yml up -d
# Wait for pg_isready before handing off to the suite — the
# global-setup hook expects the container to already be up.
for i in {1..60}; do
if docker exec cipherstash-e2e-postgres pg_isready -U cipherstash -d cipherstash_e2e >/dev/null 2>&1; then
echo "Postgres ready"
break
fi
sleep 1
done
Comment on lines +98 to +104
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail fast when Postgres never becomes ready.

The readiness loop can timeout silently and continue to the test step, which delays and obscures the root failure. Add an explicit timeout check and exit non-zero here.

Proposed fix
       - name: Start E2E Postgres container
         working-directory: examples/prisma
         run: |
           docker compose -f test/e2e/docker-compose.yml up -d
           # Wait for pg_isready before handing off to the suite — the
           # global-setup hook expects the container to already be up.
+          ready=0
           for i in {1..60}; do
             if docker exec cipherstash-e2e-postgres pg_isready -U cipherstash -d cipherstash_e2e >/dev/null 2>&1; then
               echo "Postgres ready"
+              ready=1
               break
             fi
             sleep 1
           done
+          if [ "$ready" -ne 1 ]; then
+            echo "Postgres did not become ready within 60s"
+            docker logs cipherstash-e2e-postgres || true
+            exit 1
+          fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for i in {1..60}; do
if docker exec cipherstash-e2e-postgres pg_isready -U cipherstash -d cipherstash_e2e >/dev/null 2>&1; then
echo "Postgres ready"
break
fi
sleep 1
done
ready=0
for i in {1..60}; do
if docker exec cipherstash-e2e-postgres pg_isready -U cipherstash -d cipherstash_e2e >/dev/null 2>&1; then
echo "Postgres ready"
ready=1
break
fi
sleep 1
done
if [ "$ready" -ne 1 ]; then
echo "Postgres did not become ready within 60s"
docker logs cipherstash-e2e-postgres || true
exit 1
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/prisma-next-e2e.yml around lines 98 - 104, The readiness
loop using "docker exec cipherstash-e2e-postgres pg_isready -U cipherstash -d
cipherstash_e2e" can silently time out; modify the loop so after the for i in
{1..60} attempts you check whether the container became ready and if not print a
clear error (e.g., "Postgres did not become ready within timeout") and exit
non-zero (exit 1) to fail the workflow fast; ensure the final check references
the same readiness command/container name and returns a non-zero exit status
when readiness was not achieved.


- name: Run E2E suite
run: pnpm --filter @cipherstash/prisma-next-example test:e2e

- name: Stop E2E Postgres container
if: always()
working-directory: examples/prisma
run: docker compose -f test/e2e/docker-compose.yml down -v
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@ mise.local.toml
cipherstash.toml
cipherstash.secret.toml
sql/cipherstash-*.sql
.cipherstash/

notes/
25 changes: 25 additions & 0 deletions examples/prisma/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Postgres connection. The database must have the EQL bundle
# installed; `pnpm migration:apply` (after `pnpm migration:plan`)
# installs it for you alongside the application schema.
#
# Defaults match the bundled `docker-compose.yml`. Run
# `docker compose up -d` from this directory to start a Postgres on
# port 5544 with these credentials.
DATABASE_URL=postgres://postgres:postgres@localhost:5544/cipherstash_prisma_example

# CipherStash workspace credentials — **deployment only**.
#
# For local development, run `npx stash auth login` once. The PKCE flow
# stores per-developer credentials in your OS keychain, and the
# `@cipherstash/stack` `EncryptionClient` picks them up automatically.
# No CS_* env vars needed.
#
# Set the four values below only when you're deploying — production
# servers and CI runners are machine accounts with no human at the
# keyboard, so they use static credentials provisioned via the
# CipherStash dashboard (Settings → Access Keys).
#
# CS_WORKSPACE_CRN=
# CS_CLIENT_ID=
# CS_CLIENT_KEY=
# CS_CLIENT_ACCESS_KEY=
92 changes: 92 additions & 0 deletions examples/prisma/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# @cipherstash/prisma-next example

End-to-end demo of [`@cipherstash/prisma-next`](../../packages/prisma-next/README.md): searchable application-layer encryption for Postgres with [Prisma Next](https://www.npmjs.com/package/@prisma-next/cli), using [`@cipherstash/stack`](../../packages/stack/README.md) as the encryption SDK.

A single `User` model with one column per cipherstash codec (string, double, bigint, date, boolean, JSON), exercised end-to-end: insert, equality, free-text search, range, between, in-array, sort, and `decryptAll`-amortised read.

📖 See the [Prisma Next encryption docs](https://cipherstash.com/docs/stack/cipherstash/encryption/prisma-next) for the full operator reference, security model, and known limitations.

## Layout

| Path | Purpose |
| -------------------------- | --------------------------------------------------------------------------------------------- |
| `docker-compose.yml` | Local Postgres 16 on port 5544. |
| `prisma/schema.prisma` | Application schema (one `User` model exercising all six cipherstash codecs). |
| `prisma-next.config.ts` | Wires `cipherstash` into `extensionPacks`. |
| `src/db.ts` | One-call setup via `cipherstashFromStack({ contractJson })`. |
| `src/index.ts` | The demo flow. |
| `src/prisma/contract.*` | Emitted by `pnpm emit`. |
| `migrations/` | Emitted by `pnpm migration:plan`. |

## Prerequisites

1. **Docker** for the bundled Postgres on port 5544 (or any Postgres 16+).
2. **A CipherStash workspace** — sign up at [cipherstash.com](https://cipherstash.com), then run `stash auth login` (PKCE; caches credentials in your OS keychain — no `CS_*` env vars needed in local dev).

## Run it

```bash
cp .env.example .env # DATABASE_URL points at the bundled Postgres
stash auth login # one-time, per developer

docker compose up -d
pnpm install
pnpm emit # PSL → contract.{json,d.ts}
pnpm migration:plan --name initial
pnpm migration:apply # installs EQL bundle + your app schema in one sweep
pnpm start # runs the demo
```

Teardown:

```bash
docker compose down -v
```

Or, to just verify the example typechecks and emits a valid contract (no database, no workspace):

```bash
pnpm install && pnpm emit && pnpm typecheck
```
Comment on lines +46 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add required README sections for native-module externalization and test execution.

This README still misses two required items: a note that @cipherstash/protect-ffi must be externalized and loaded via runtime require, plus explicit “how to run tests” instructions.

As per coding guidelines, "Each example app must include a README covering: setup (env vars, install, run commands), notes on native module externalization, and how to run tests".

Also applies to: 89-93

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/prisma/README.md` around lines 46 - 50, Update the README to include
a short "Native module externalization" note stating that the native package
"@cipherstash/protect-ffi" must be externalized from bundlers and loaded at
runtime via require (e.g., require('@cipherstash/protect-ffi')) and any ENV or
platform notes needed; also add an explicit "Running tests" section with the
exact commands to run the example's tests (install, emit/build, and the test
runner commands such as pnpm install && pnpm emit && pnpm test or the equivalent
used in CI), and ensure these sections appear alongside the existing setup/usage
instructions referenced near the pnpm install && pnpm emit && pnpm typecheck
snippet.


## Expected output

```text
--- Insert (mixed-codec round-trip) ---
Inserted 4 rows across six cipherstash codecs.

--- cipherstashEq (string equality) ---
Found 1 row(s) for alice@example.com.
user-0: alice@example.com

--- cipherstashIlike (string free-text-search) ---
Found 3 row(s) matching %@example.com.
user-0: alice@example.com
user-1: bob@example.com
user-2: carol@example.com

--- cipherstashGt (double order-and-range) ---
Found 2 user(s) with salary > 100,000.
user-1: salary=110000
user-3: salary=145000

--- cipherstashBetween (date order-and-range) ---
Found 3 user(s) born between 1985 and 1995.

--- cipherstashInArray (bigint equality) ---
Found 2 user(s) whose accountId is in the supplied array.

--- cipherstashInArray (boolean equality-only) ---
Found 3 user(s) with emailVerified = true.

--- cipherstashAsc (bare-column ORDER BY) ---
user-0: email=alice@example.com
user-1: email=bob@example.com
user-2: email=carol@example.com
user-3: email=dave@otherorg.test
```

## References

- 📖 [Prisma Next encryption docs](https://cipherstash.com/docs/stack/cipherstash/encryption/prisma-next) — the canonical reference.
- [`@cipherstash/prisma-next` package README](../../packages/prisma-next/README.md) — install, subpath exports, quick start.
33 changes: 33 additions & 0 deletions examples/prisma/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Local Postgres for the @cipherstash/prisma-next example.
#
# Usage:
# docker compose up -d # start
# docker compose down -v # stop + delete volume (fresh state)
#
# The DATABASE_URL in .env.example matches the values below:
# postgres://postgres:postgres@localhost:5544/cipherstash_prisma_example
#
# Port 5544 (not 5432) is used to avoid colliding with any host-side
# Postgres / other example containers.

services:
postgres:
image: postgres:16
container_name: cipherstash-prisma-example-pg
restart: unless-stopped
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: cipherstash_prisma_example
ports:
- "5544:5432"
volumes:
- cipherstash-prisma-example-pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d cipherstash_prisma_example"]
interval: 2s
timeout: 5s
retries: 30

volumes:
cipherstash-prisma-example-pg-data:
Loading
Loading