Skip to content

feat: release prisma next extension#444

Merged
calvinbrewer merged 4 commits into
mainfrom
prisma-next-release
May 14, 2026
Merged

feat: release prisma next extension#444
calvinbrewer merged 4 commits into
mainfrom
prisma-next-release

Conversation

@calvinbrewer
Copy link
Copy Markdown
Contributor

@calvinbrewer calvinbrewer commented May 13, 2026

Summary by CodeRabbit

  • New Features

    • Introduced @cipherstash/prisma-next package for encrypted Prisma Next column support with encrypted search operators
    • Added stash init --prisma-next CLI command for streamlined Prisma Next project setup
    • New encrypted column types: EncryptedString, EncryptedDouble, EncryptedBigInt, EncryptedDate, EncryptedBoolean, EncryptedJson
    • Added full end-to-end example and E2E test suite demonstrating encrypted queries
  • Documentation

    • Package README and developer guide
    • Example project with configuration and setup instructions

Review Change Stack

@calvinbrewer calvinbrewer requested a review from a team as a code owner May 13, 2026 22:05
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 13, 2026

🦋 Changeset detected

Latest commit: 4c2d6d4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@cipherstash/prisma-next Minor
stash Minor
@cipherstash/prisma-next-example Patch
@cipherstash/e2e Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

Warning

Rate limit exceeded

@calvinbrewer has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 34 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a58bd0a-31e0-4a7c-841b-25fb854b6668

📥 Commits

Reviewing files that changed from the base of the PR and between 4c2d6d4 and a5199a0.

📒 Files selected for processing (1)
  • .github/workflows/prisma-next-e2e.yml
📝 Walkthrough

Walkthrough

This PR adds @cipherstash/prisma-next, wires Prisma Next support into stash init, adds control-plane and runtime encryption components, introduces a Prisma example with generated migrations and contracts, and adds unit, type, end-to-end, and CI coverage for encrypted queries, middleware, and stack integration.

Changes

Prisma Next integration and example

Layer / File(s) Summary
Package surface and control plane
packages/prisma-next/package.json, packages/prisma-next/src/extension-metadata/*, packages/prisma-next/src/contract-authoring.ts, packages/prisma-next/src/exports/{control,pack,migration,column-types}.ts, packages/prisma-next/migrations/..., packages/prisma-next/README.md, packages/prisma-next/DEVELOPING.md
Adds the new package manifest, public subpath exports, authoring/type metadata, control-plane descriptor, baseline EQL bundle migration artifacts, migration helper calls, and package docs/build config.
Runtime, middleware, and stack wiring
packages/prisma-next/src/execution/*, packages/prisma-next/src/middleware/bulk-encrypt.ts, packages/prisma-next/src/stack/*, packages/prisma-next/src/exports/{runtime,stack,middleware,codec-types,operation-types}.ts
Adds encrypted envelope types, runtime codecs, query operators, decrypt-all traversal, abort handling, middleware registration and bulk encryption, stack schema derivation, SDK adaptation, and runtime descriptor factories.
CLI init and detection
packages/cli/src/bin/stash.ts, packages/cli/src/commands/db/detect.ts, packages/cli/src/commands/init/..., packages/cli/src/commands/init/providers/__tests__/prisma-next.test.ts, package.json
Adds Prisma Next project detection, a new init provider and help text, and updates init dependency/schema/EQL steps for the Prisma Next flow.
Example app, generated artifacts, and CI
examples/prisma/*, examples/prisma/migrations/..., examples/prisma/test/e2e/*, .github/workflows/prisma-next-e2e.yml, .gitignore
Adds a Prisma Next example project with schema, runtime wiring, demo queries, generated contracts and migrations, E2E harness files, Docker setup, docs, and a workflow that builds and runs the example tests.
Runtime and control test coverage
packages/prisma-next/test/*
Adds unit, type, lowering, descriptor, bundling, stack, middleware, envelope, codec, and control-hook tests covering the new package behavior and exported surfaces.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant PrismaRuntime as Prisma Next Runtime
  participant Cipherstash as `@cipherstash/prisma-next`
  participant Stack as `@cipherstash/stack`
  participant Postgres

  App->>Cipherstash: cipherstashFromStack({ contractJson })
  Cipherstash->>Stack: create EncryptionClient and SDK adapter
  App->>PrismaRuntime: execute query with Encrypted* values
  PrismaRuntime->>Cipherstash: bulkEncryptMiddleware beforeExecute
  Cipherstash->>Stack: bulkEncrypt / bulkDecrypt
  PrismaRuntime->>Postgres: run SQL with eql_v2_encrypted values
  Postgres-->>PrismaRuntime: rows
  PrismaRuntime-->>App: encrypted results ready for decryptAll / field decrypt
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • coderdan
  • auxesis
  • tobyhede

Poem

🐇 I tucked new secrets into Prisma rows,
Through middleware the ciphertext flows.
I hop through queries, range and sort,
With stack and schema lending support.
In moonlit tests the burrow sings—
encrypted paws on silver strings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch prisma-next-release

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
examples/prisma/package.json (1)

1-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add explicit Node and pnpm version constraints for the example.

This manifest is missing runtime/tooling pins required for example apps.

Suggested patch
 {
   "name": "@cipherstash/prisma-next-example",
   "private": true,
   "version": "0.0.0",
+  "engines": {
+    "node": ">=22"
+  },
+  "packageManager": "pnpm@9",
   "description": "End-to-end example of `@cipherstash/prisma-next`: searchable application-layer encryption for Postgres with Prisma Next, using `@cipherstash/stack` as the SDK.",
   "type": "module",

As per coding guidelines, examples/**/package.json: "Use Node.js >= 22 and pnpm 9.x for example apps".

🤖 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/package.json` around lines 1 - 37, The package.json for the
Prisma example lacks Node and pnpm version pins; add an "engines" field
specifying "node": ">=22" and add a "packageManager" field set to "pnpm@9" (or
"pnpm@9.x") at the top level of package.json so the example enforces Node >=22
and pnpm 9.x. Update the top-level JSON object (near "name", "version", etc.) to
include these fields and ensure the file remains valid JSON.
packages/cli/src/commands/init/steps/install-deps.ts (1)

100-117: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-check @cipherstash/prisma-next after install before declaring success.

When Prisma Next is targeted, the post-install verification only checks @cipherstash/stack and stash. A failed @cipherstash/prisma-next install can still report success and skip the missing-package warning.

Suggested fix
-    const stackInstalled = isPackageInstalled(STACK_PACKAGE)
-    const cliInstalled = isPackageInstalled(CLI_PACKAGE)
+    const stackInstalled = isPackageInstalled(STACK_PACKAGE)
+    const cliInstalled = isPackageInstalled(CLI_PACKAGE)
+    const prismaNextInstalled = wantPrismaNext
+      ? isPackageInstalled(PRISMA_NEXT_PACKAGE)
+      : true
 
-    if (stackInstalled && cliInstalled) {
+    if (stackInstalled && cliInstalled && prismaNextInstalled) {
       p.log.success('Stack dependencies installed.')
     } else {
       const stillMissing = [
         ...(stackInstalled ? [] : [`${STACK_PACKAGE} (prod)`]),
+        ...(prismaNextInstalled ? [] : [`${PRISMA_NEXT_PACKAGE} (prod)`]),
         ...(cliInstalled ? [] : [`${CLI_PACKAGE} (dev)`]),
       ]
       p.log.warn(`Still missing: ${stillMissing.join(', ')}.`)
       p.note(
         `You can retry manually:\n  ${(failed.length ? failed : commands).join('\n  ')}`,
         'Manual Installation',
       )
     }
🤖 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 `@packages/cli/src/commands/init/steps/install-deps.ts` around lines 100 - 117,
The post-install verification only re-checks STACK_PACKAGE and CLI_PACKAGE; add
a re-check for the Prisma Next package by calling isPackageInstalled for the
prisma-next constant (e.g. PRISMA_NEXT_PACKAGE or `@cipherstash/prisma-next`)
after installation, include its boolean (prismaNextInstalled) in the
stillMissing array logic and warnings (so missing prisma-next shows up), and
return it in the final state alongside stackInstalled and cliInstalled; update
any use of failed/commands messaging to include prisma-next where appropriate.
🧹 Nitpick comments (6)
packages/prisma-next/src/stack/derive-schemas.ts (1)

140-142: ⚡ Quick win

Use hasOwnProperty for more precise flag validation.

The in operator accepts inherited properties from the prototype chain. While Object.entries() in applyTypeParams prevents prototype keys from reaching this function in practice, using hasOwnProperty is more semantically correct and defensive.

Suggested fix
 function isCipherstashFlag(value: string): value is CipherstashFlag {
-  return value in FLAG_DISPATCH
+  return Object.prototype.hasOwnProperty.call(FLAG_DISPATCH, value)
 }
🤖 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 `@packages/prisma-next/src/stack/derive-schemas.ts` around lines 140 - 142, The
isCipherstashFlag function uses the `in` operator which can match inherited
prototype properties; update it to perform a direct own-property check against
FLAG_DISPATCH (e.g. use Object.prototype.hasOwnProperty.call(FLAG_DISPATCH,
value)) so only FLAG_DISPATCH's own keys are accepted. Locate the
isCipherstashFlag function and replace the `return value in FLAG_DISPATCH` line
with a hasOwnProperty-based check to make flag validation precise and defensive.
packages/prisma-next/test/codec-runtime.test.ts (1)

213-217: ⚡ Quick win

Add explicit undefined coverage in the null/undefined decode test.

The test name promises both cases, but only asserts null. Add a direct undefined assertion to keep this contract pinned.

Suggested patch
-  it('decode passes through null/undefined unchanged', async () => {
+  it('decode handles null and undefined ciphertext payloads', async () => {
     const codec = createCipherstashStringCodec(emptySdk());
     const decoded = await codec.decode(null as unknown as string, ctxWithColumn('user', 'email'));
     expect(decoded.expose().ciphertext).toBeNull();
+
+    const decodedUndefined = await codec.decode(
+      undefined as unknown as string,
+      ctxWithColumn('user', 'email'),
+    );
+    expect(decodedUndefined.expose().ciphertext).toBeUndefined();
   });
🤖 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 `@packages/prisma-next/test/codec-runtime.test.ts` around lines 213 - 217, The
test "decode passes through null/undefined unchanged" only asserts the null
case; add an explicit undefined assertion: call
createCipherstashStringCodec(emptySdk()) then await codec.decode(undefined as
unknown as string, ctxWithColumn('user','email')) and assert
decoded.expose().ciphertext is undefined (or toBeUndefined()) alongside the
existing null check so both null and undefined are covered in the codec.decode
test.
packages/prisma-next/test/envelope-json.test.ts (1)

45-65: ⚡ Quick win

Align this read-side test with its stated signal/call-contract intent.

The test title says decrypt({signal}) behavior, but it never passes a signal or asserts SDK call args (ciphertext/table/column/signal). Please either rename the test or add the same call-shape assertion pattern used in sibling envelope tests.

🤖 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 `@packages/prisma-next/test/envelope-json.test.ts` around lines 45 - 65, The
test title promises to verify decrypt({signal}) but never passes or asserts the
signal or the SDK call shape; update the test that constructs EncryptedJson via
EncryptedJson.fromInternal and then call envelope.decrypt({ signal }) with a
created AbortSignal (e.g., from new AbortController().signal) and assert
decryptMock was called once with an argument object containing the original
ciphertext, table: 'audit', column: 'payload' and the same signal (and keep the
existing expect(result).toBe(decoded)). This mirrors the sibling envelope tests'
call-shape assertions and ensures decryptMock receives
ciphertext/table/column/signal.
packages/prisma-next/test/sdk-adapter.test.ts (2)

10-12: ⚡ Quick win

Inconsistent semicolon usage in imports.

Lines 10-12 are missing semicolons, while the rest of the file consistently uses them (e.g., lines 14, 233, 235). This inconsistency can lead to maintainability issues and potential ASI (Automatic Semicolon Insertion) pitfalls.

✨ Proposed fix
-import { encryptedColumn, encryptedTable } from '@cipherstash/stack/schema'
-import type { EncryptionClient } from '@cipherstash/stack/client'
-import { describe, expect, it, vi } from 'vitest'
+import { encryptedColumn, encryptedTable } from '@cipherstash/stack/schema';
+import type { EncryptionClient } from '@cipherstash/stack/client';
+import { describe, expect, it, vi } from 'vitest';
🤖 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 `@packages/prisma-next/test/sdk-adapter.test.ts` around lines 10 - 12, The
three import statements importing encryptedColumn, encryptedTable,
EncryptionClient and Vitest symbols (describe, expect, it, vi) are missing
trailing semicolons; update the import lines (the lines that import
encryptedColumn/encryptedTable, the line importing type EncryptionClient, and
the line importing from 'vitest') to include semicolons at the end to match the
file's existing style and avoid ASI issues.

67-71: 💤 Low value

Consider extracting the valid envelope to a shared test fixture.

The validEnvelope constant is used across multiple test cases (lines 204, 216, 217, 220). If the envelope structure needs to evolve, having it defined at the top level makes updates easier and clarifies the canonical shape for EQL v2 envelopes in tests.

🤖 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 `@packages/prisma-next/test/sdk-adapter.test.ts` around lines 67 - 71, Extract
the inline validEnvelope constant into a shared test fixture so multiple tests
can import and reuse the canonical EQL v2 envelope shape; create a new exported
fixture (e.g., VALID_ENVELOPE) and replace the local const validEnvelope in
sdk-adapter.test.ts and any other test files (where referenced at lines ~204,
216, 217, 220) with imports from that fixture, keeping the same object shape ({
v: 2, i: { t: 'users', c: 'email' }, c: 'ct-blob' }) so updates are centralized
and tests continue to reference the same symbol.
packages/prisma-next/tsconfig.json (1)

17-18: ⚡ Quick win

Consider enabling unused code checks.

noUnusedLocals and noUnusedParameters are disabled, which can allow dead code to accumulate. Enabling these checks helps catch unused variables and parameters early, improving maintainability.

♻️ Proposed enhancement
-    "noUnusedLocals": false,
-    "noUnusedParameters": false,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
🤖 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 `@packages/prisma-next/tsconfig.json` around lines 17 - 18, Enable TypeScript
unused-code checks by setting "noUnusedLocals" and "noUnusedParameters" to true
in the tsconfig (currently false); after enabling them, run the type checker and
fix reported issues by removing or using unused variables/parameters, renaming
intentionally unused parameters with a leading underscore, or adding explicit /*
eslint-disable *//ts-ignore comments only when necessary; target the settings
"noUnusedLocals" and "noUnusedParameters" in packages/prisma-next/tsconfig.json
and resolve any reported unused symbol warnings across the codebase (functions,
methods, and parameter lists) accordingly.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@examples/prisma/.cipherstash/context.json`:
- Line 4: The generated context JSON contains a stale encryptionClientPath value
("./src/encryption/index.ts") that doesn't match this example's entrypoint;
update the "encryptionClientPath" field in .cipherstash/context.json to point to
the actual module used in this PR (e.g., "src/db.ts" or "./src/db.ts") so
tooling can locate the integration entrypoint (look for the encryptionClientPath
key in .cipherstash/context.json and replace the old path with the correct
src/db.ts path).

In `@examples/prisma/README.md`:
- Around line 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.

In `@examples/prisma/src/index.ts`:
- Line 147: Remove logging of decrypted sensitive plaintext by eliminating
direct calls to decrypt() inside console.log; for occurrences like
row.email.decrypt(), row.salary.decrypt(), or any other .decrypt() usages in
console.log (e.g., the line with console.log(`  ${row.id}: ${await
row.email.decrypt()}`) and the similar calls at lines referenced), replace the
output with non-sensitive indicators such as logging only row.id or a
masked/placeholder value (e.g., "[REDACTED_EMAIL]" or "salary: [REDACTED]") or a
boolean/summary (e.g., "hasEmail: true"), ensuring no decrypted plaintext is
ever written to logs.

In `@packages/prisma-next/package.json`:
- Around line 26-63: The exports map in package.json currently provides only
"import" (ESM) entries for subpaths like "./codec-types", "./column-types",
"./control", "./middleware", "./migration", "./operation-types", "./pack",
"./runtime", and "./stack" but tsup is building only ESM so no .cjs artifacts
exist; fix by updating tsup.config.ts to include 'cjs' in the format array (e.g.
format: ['esm','cjs']) so .cjs outputs are emitted, then add a "require":
"./dist/<name>.cjs" condition to each corresponding subpath in the package.json
"exports" map (for every "./codec-types", "./column-types", "./control",
"./middleware", "./migration", "./operation-types", "./pack", "./runtime",
"./stack") so CommonJS consumers can resolve the CJS bundles.

In `@packages/prisma-next/src/execution/cell-codec-factory.ts`:
- Around line 182-185: The runtime guidance string referencing the wrong package
should be updated: in the error message built in cell-codec-factory (the
template that includes `this.descriptor.codecId` and `this.#typeName`) replace
the incorrect import path `@prisma-next/extension-cipherstash/runtime` with the
package’s actual published runtime package name; keep the surrounding guidance
that mentions `createParameterizedCodecDescriptors(sdk)` and the example
`create${this.#typeName}Codec(sdk)` factory call intact so users are directed to
the correct module to import from.
- Around line 144-174: When encode encounters a pre-encrypt envelope
(handle.ciphertext === undefined) ensure we fail fast if no SDK is attached: add
a guard that if this.sdk === undefined you throw a
runtimeError('RUNTIME.ENCODE_FAILED', ...) describing that the codec was used
without an attached SDK and include this.descriptor.codecId, a reason like
'cipherstash-sdk-not-attached', and envelopeRouting with handle.table and
handle.column; keep the existing middleware-check branch (which uses
isBulkEncryptMiddlewareRegistered(this.sdk) and sets
this.#middlewareCheckPassed) but only run it when this.sdk is present so the
error for missing SDK is deterministic at the codec boundary.

In `@packages/prisma-next/src/exports/control.ts`:
- Around line 38-69: The import of baselineOps (import baselineOps ... ops.json)
referenced by CIPHERSTASH_BASELINE_MIGRATION_NAME is missing and will break
descriptor loading; either add the generated
migrations/20260601T0000_install_eql_bundle/ops.json artifact to the PR so
baselineOps is resolvable, or modify the contract-space wiring where
cipherstashContractSpace is built (the migrations array entry that uses
metadata: baselineMetadata and ops: baselineOps) to point ops to an existing
artifact/source (e.g., replace baselineOps with the correct JSON import or a
fallback loader) so module resolution succeeds.

In `@packages/prisma-next/src/stack/sdk-adapter.ts`:
- Around line 175-179: The validation currently checks typeof (value as { i: {
t?: unknown; c?: unknown } }).i === 'object' then uses 't' in ... and 'c' in ...
which will throw if i is null; update the conditional that builds valid (used in
sdk-adapter.ts) to explicitly ensure value.i is non-null (e.g. value.i !== null)
before doing the 't' in and 'c' in membership checks so a ciphertext with i:
null yields a controlled validation failure instead of a TypeError; keep the
surrounding isEncryptedPayload(value) check and the same property names (i, t,
c).

In `@packages/prisma-next/test/bundling-isolation.test.ts`:
- Around line 34-36: Update the incorrect package name in the top-of-file
comment: replace the reference to `@prisma-next/extension-cipherstash#test` with
the actual package name `@cipherstash/prisma-next#test` (matching the test
message used later, e.g. the string around line 187) so the turbo.json
dependency comment is accurate and not confusing to maintainers.

In `@packages/prisma-next/test/decrypt-all.test.ts`:
- Line 60: Remove the payload interpolation from the mock SDK error strings in
the test so plaintext-like values aren't logged: replace the dynamic message
that references args.ciphertext (the line throwing new Error(`mock SDK: cannot
decrypt: ${JSON.stringify(args.ciphertext)}`)) with a fixed diagnostic string
such as "mock SDK: cannot decrypt" (and make the same change for the similar
occurrence around the other failing throw at lines ~80), leaving the throw
behavior unchanged but eliminating embedding of sensitive payload data.

---

Outside diff comments:
In `@examples/prisma/package.json`:
- Around line 1-37: The package.json for the Prisma example lacks Node and pnpm
version pins; add an "engines" field specifying "node": ">=22" and add a
"packageManager" field set to "pnpm@9" (or "pnpm@9.x") at the top level of
package.json so the example enforces Node >=22 and pnpm 9.x. Update the
top-level JSON object (near "name", "version", etc.) to include these fields and
ensure the file remains valid JSON.

In `@packages/cli/src/commands/init/steps/install-deps.ts`:
- Around line 100-117: The post-install verification only re-checks
STACK_PACKAGE and CLI_PACKAGE; add a re-check for the Prisma Next package by
calling isPackageInstalled for the prisma-next constant (e.g.
PRISMA_NEXT_PACKAGE or `@cipherstash/prisma-next`) after installation, include its
boolean (prismaNextInstalled) in the stillMissing array logic and warnings (so
missing prisma-next shows up), and return it in the final state alongside
stackInstalled and cliInstalled; update any use of failed/commands messaging to
include prisma-next where appropriate.

---

Nitpick comments:
In `@packages/prisma-next/src/stack/derive-schemas.ts`:
- Around line 140-142: The isCipherstashFlag function uses the `in` operator
which can match inherited prototype properties; update it to perform a direct
own-property check against FLAG_DISPATCH (e.g. use
Object.prototype.hasOwnProperty.call(FLAG_DISPATCH, value)) so only
FLAG_DISPATCH's own keys are accepted. Locate the isCipherstashFlag function and
replace the `return value in FLAG_DISPATCH` line with a hasOwnProperty-based
check to make flag validation precise and defensive.

In `@packages/prisma-next/test/codec-runtime.test.ts`:
- Around line 213-217: The test "decode passes through null/undefined unchanged"
only asserts the null case; add an explicit undefined assertion: call
createCipherstashStringCodec(emptySdk()) then await codec.decode(undefined as
unknown as string, ctxWithColumn('user','email')) and assert
decoded.expose().ciphertext is undefined (or toBeUndefined()) alongside the
existing null check so both null and undefined are covered in the codec.decode
test.

In `@packages/prisma-next/test/envelope-json.test.ts`:
- Around line 45-65: The test title promises to verify decrypt({signal}) but
never passes or asserts the signal or the SDK call shape; update the test that
constructs EncryptedJson via EncryptedJson.fromInternal and then call
envelope.decrypt({ signal }) with a created AbortSignal (e.g., from new
AbortController().signal) and assert decryptMock was called once with an
argument object containing the original ciphertext, table: 'audit', column:
'payload' and the same signal (and keep the existing
expect(result).toBe(decoded)). This mirrors the sibling envelope tests'
call-shape assertions and ensures decryptMock receives
ciphertext/table/column/signal.

In `@packages/prisma-next/test/sdk-adapter.test.ts`:
- Around line 10-12: The three import statements importing encryptedColumn,
encryptedTable, EncryptionClient and Vitest symbols (describe, expect, it, vi)
are missing trailing semicolons; update the import lines (the lines that import
encryptedColumn/encryptedTable, the line importing type EncryptionClient, and
the line importing from 'vitest') to include semicolons at the end to match the
file's existing style and avoid ASI issues.
- Around line 67-71: Extract the inline validEnvelope constant into a shared
test fixture so multiple tests can import and reuse the canonical EQL v2
envelope shape; create a new exported fixture (e.g., VALID_ENVELOPE) and replace
the local const validEnvelope in sdk-adapter.test.ts and any other test files
(where referenced at lines ~204, 216, 217, 220) with imports from that fixture,
keeping the same object shape ({ v: 2, i: { t: 'users', c: 'email' }, c:
'ct-blob' }) so updates are centralized and tests continue to reference the same
symbol.

In `@packages/prisma-next/tsconfig.json`:
- Around line 17-18: Enable TypeScript unused-code checks by setting
"noUnusedLocals" and "noUnusedParameters" to true in the tsconfig (currently
false); after enabling them, run the type checker and fix reported issues by
removing or using unused variables/parameters, renaming intentionally unused
parameters with a leading underscore, or adding explicit /* eslint-disable
*//ts-ignore comments only when necessary; target the settings "noUnusedLocals"
and "noUnusedParameters" in packages/prisma-next/tsconfig.json and resolve any
reported unused symbol warnings across the codebase (functions, methods, and
parameter lists) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 22f2d5d4-fc3f-4fb3-b4c2-f235afb8c4bb

📥 Commits

Reviewing files that changed from the base of the PR and between 6e21cc3 and dc02d0b.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (132)
  • .changeset/add-prisma-next-integration.md
  • examples/prisma/.cipherstash/context.json
  • examples/prisma/.env.example
  • examples/prisma/README.md
  • examples/prisma/docker-compose.yml
  • examples/prisma/migrations/app/20260513T1735_initial/end-contract.d.ts
  • examples/prisma/migrations/app/20260513T1735_initial/end-contract.json
  • examples/prisma/migrations/app/20260513T1735_initial/migration.json
  • examples/prisma/migrations/app/20260513T1735_initial/migration.ts
  • examples/prisma/migrations/app/20260513T1735_initial/ops.json
  • examples/prisma/migrations/cipherstash/20260601T0000_install_eql_bundle/contract.json
  • examples/prisma/migrations/cipherstash/20260601T0000_install_eql_bundle/migration.json
  • examples/prisma/migrations/cipherstash/20260601T0000_install_eql_bundle/ops.json
  • examples/prisma/migrations/cipherstash/contract.d.ts
  • examples/prisma/migrations/cipherstash/contract.json
  • examples/prisma/migrations/cipherstash/refs/head.json
  • examples/prisma/package.json
  • examples/prisma/prisma-next.config.ts
  • examples/prisma/prisma/schema.prisma
  • examples/prisma/src/db.ts
  • examples/prisma/src/index.ts
  • examples/prisma/src/prisma/contract.d.ts
  • examples/prisma/src/prisma/contract.json
  • examples/prisma/tsconfig.json
  • package.json
  • packages/cli/src/bin/stash.ts
  • packages/cli/src/commands/db/detect.ts
  • packages/cli/src/commands/init/index.ts
  • packages/cli/src/commands/init/providers/__tests__/prisma-next.test.ts
  • packages/cli/src/commands/init/providers/prisma-next.ts
  • packages/cli/src/commands/init/steps/build-schema.ts
  • packages/cli/src/commands/init/steps/install-deps.ts
  • packages/cli/src/commands/init/steps/install-eql.ts
  • packages/cli/src/commands/init/types.ts
  • packages/prisma-next/DEVELOPING.md
  • packages/prisma-next/README.md
  • packages/prisma-next/migrations/20260601T0000_install_eql_bundle/end-contract.d.ts
  • packages/prisma-next/migrations/20260601T0000_install_eql_bundle/end-contract.json
  • packages/prisma-next/migrations/20260601T0000_install_eql_bundle/migration.json
  • packages/prisma-next/migrations/20260601T0000_install_eql_bundle/migration.ts
  • packages/prisma-next/migrations/20260601T0000_install_eql_bundle/ops.json
  • packages/prisma-next/migrations/refs/head.json
  • packages/prisma-next/package.json
  • packages/prisma-next/prisma-next.config.ts
  • packages/prisma-next/src/contract-authoring.ts
  • packages/prisma-next/src/contract.d.ts
  • packages/prisma-next/src/contract.json
  • packages/prisma-next/src/contract.prisma
  • packages/prisma-next/src/execution/abort.ts
  • packages/prisma-next/src/execution/cell-codec-factory.ts
  • packages/prisma-next/src/execution/codec-runtime.ts
  • packages/prisma-next/src/execution/decrypt-all.ts
  • packages/prisma-next/src/execution/envelope-base.ts
  • packages/prisma-next/src/execution/envelope-bigint.ts
  • packages/prisma-next/src/execution/envelope-boolean.ts
  • packages/prisma-next/src/execution/envelope-date.ts
  • packages/prisma-next/src/execution/envelope-double.ts
  • packages/prisma-next/src/execution/envelope-json.ts
  • packages/prisma-next/src/execution/envelope-string.ts
  • packages/prisma-next/src/execution/helpers.ts
  • packages/prisma-next/src/execution/middleware-registry.ts
  • packages/prisma-next/src/execution/operators.ts
  • packages/prisma-next/src/execution/parameterized.ts
  • packages/prisma-next/src/execution/routing.ts
  • packages/prisma-next/src/execution/sdk.ts
  • packages/prisma-next/src/exports/codec-types.ts
  • packages/prisma-next/src/exports/column-types.ts
  • packages/prisma-next/src/exports/contract-space-typing.ts
  • packages/prisma-next/src/exports/control.ts
  • packages/prisma-next/src/exports/middleware.ts
  • packages/prisma-next/src/exports/migration.ts
  • packages/prisma-next/src/exports/operation-types.ts
  • packages/prisma-next/src/exports/pack.ts
  • packages/prisma-next/src/exports/runtime.ts
  • packages/prisma-next/src/exports/stack.ts
  • packages/prisma-next/src/extension-metadata/codec-metadata.ts
  • packages/prisma-next/src/extension-metadata/constants.ts
  • packages/prisma-next/src/extension-metadata/descriptor-meta.ts
  • packages/prisma-next/src/middleware/bulk-encrypt.ts
  • packages/prisma-next/src/migration/call-classes.ts
  • packages/prisma-next/src/migration/cipherstash-codec.ts
  • packages/prisma-next/src/migration/codec-hooks-factory.ts
  • packages/prisma-next/src/migration/eql-bundle.ts
  • packages/prisma-next/src/migration/eql-install.generated.ts
  • packages/prisma-next/src/stack/derive-schemas.ts
  • packages/prisma-next/src/stack/from-stack.ts
  • packages/prisma-next/src/stack/sdk-adapter.ts
  • packages/prisma-next/src/types/codec-types.ts
  • packages/prisma-next/src/types/operation-types.ts
  • packages/prisma-next/test/abort.test.ts
  • packages/prisma-next/test/authoring.test.ts
  • packages/prisma-next/test/bulk-encrypt-middleware.test.ts
  • packages/prisma-next/test/bundling-isolation.test.ts
  • packages/prisma-next/test/call-classes.test.ts
  • packages/prisma-next/test/call-classes.types.test-d.ts
  • packages/prisma-next/test/cipherstash-codec-numeric.test.ts
  • packages/prisma-next/test/cipherstash-codec-other-codecs.test.ts
  • packages/prisma-next/test/cipherstash-codec-string.test.ts
  • packages/prisma-next/test/cipherstash-codec.test.ts
  • packages/prisma-next/test/codec-runtime.test.ts
  • packages/prisma-next/test/column-types.test.ts
  • packages/prisma-next/test/decrypt-all.test.ts
  • packages/prisma-next/test/derive-schemas.test.ts
  • packages/prisma-next/test/descriptor.test.ts
  • packages/prisma-next/test/envelope-bigint.test.ts
  • packages/prisma-next/test/envelope-boolean.test.ts
  • packages/prisma-next/test/envelope-date.test.ts
  • packages/prisma-next/test/envelope-double.test.ts
  • packages/prisma-next/test/envelope-json.test.ts
  • packages/prisma-next/test/envelope-string.test.ts
  • packages/prisma-next/test/envelope.types.test-d.ts
  • packages/prisma-next/test/equality-trait-removal.test.ts
  • packages/prisma-next/test/from-stack-divergence.test.ts
  • packages/prisma-next/test/helpers.test.ts
  • packages/prisma-next/test/helpers.types.test-d.ts
  • packages/prisma-next/test/operation-types.types.test-d.ts
  • packages/prisma-next/test/operator-lowering-equality.test.ts
  • packages/prisma-next/test/operator-lowering-order-range.test.ts
  • packages/prisma-next/test/operator-lowering-text-search.test.ts
  • packages/prisma-next/test/operator-lowering.helpers.ts
  • packages/prisma-next/test/operator-lowering.test.ts
  • packages/prisma-next/test/psl-interpretation-numeric.test.ts
  • packages/prisma-next/test/psl-interpretation-other-types.test.ts
  • packages/prisma-next/test/psl-interpretation.test.ts
  • packages/prisma-next/test/routing.test.ts
  • packages/prisma-next/test/runtime-descriptor.test.ts
  • packages/prisma-next/test/sdk-adapter.test.ts
  • packages/prisma-next/test/sdk.types.test-d.ts
  • packages/prisma-next/tsconfig.json
  • packages/prisma-next/tsup.config.ts
  • packages/prisma-next/vitest.config.ts
  • pnpm-workspace.yaml

{
"cliVersion": "0.14.0",
"integration": "prisma-next",
"encryptionClientPath": "./src/encryption/index.ts",
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

Fix stale encryptionClientPath in generated context.

"./src/encryption/index.ts" does not match the example layout in this PR (src/db.ts). Keeping this stale path can break tooling that relies on context metadata to locate integration entrypoints.

Suggested fix
-  "encryptionClientPath": "./src/encryption/index.ts",
+  "encryptionClientPath": "./src/db.ts",
📝 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
"encryptionClientPath": "./src/encryption/index.ts",
"encryptionClientPath": "./src/db.ts",
🤖 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/.cipherstash/context.json` at line 4, The generated context
JSON contains a stale encryptionClientPath value ("./src/encryption/index.ts")
that doesn't match this example's entrypoint; update the "encryptionClientPath"
field in .cipherstash/context.json to point to the actual module used in this PR
(e.g., "src/db.ts" or "./src/db.ts") so tooling can locate the integration
entrypoint (look for the encryptionClientPath key in .cipherstash/context.json
and replace the old path with the correct src/db.ts path).

Comment thread examples/prisma/README.md
Comment on lines +46 to +50
Or, to just verify the example typechecks and emits a valid contract (no database, no workspace):

```bash
pnpm install && pnpm emit && pnpm typecheck
```
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.

Comment thread examples/prisma/src/index.ts
Comment on lines +26 to +63
"exports": {
"./codec-types": {
"types": "./dist/codec-types.d.ts",
"import": "./dist/codec-types.js"
},
"./column-types": {
"types": "./dist/column-types.d.ts",
"import": "./dist/column-types.js"
},
"./control": {
"types": "./dist/control.d.ts",
"import": "./dist/control.js"
},
"./middleware": {
"types": "./dist/middleware.d.ts",
"import": "./dist/middleware.js"
},
"./migration": {
"types": "./dist/migration.d.ts",
"import": "./dist/migration.js"
},
"./operation-types": {
"types": "./dist/operation-types.d.ts",
"import": "./dist/operation-types.js"
},
"./pack": {
"types": "./dist/pack.d.ts",
"import": "./dist/pack.js"
},
"./runtime": {
"types": "./dist/runtime.d.ts",
"import": "./dist/runtime.js"
},
"./stack": {
"types": "./dist/stack.d.ts",
"import": "./dist/stack.js"
},
"./package.json": "./package.json"
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 | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify which package manifests have subpath exports with import but no require.
python - <<'PY'
import json, glob
for path in sorted(glob.glob("packages/*/package.json")):
    with open(path, "r", encoding="utf-8") as f:
        data = json.load(f)
    exports = data.get("exports")
    if not isinstance(exports, dict):
        continue
    missing = []
    for key, value in exports.items():
        if isinstance(value, dict) and "import" in value and "require" not in value:
            missing.append(key)
    if missing:
        print(f"{path}: missing require for {', '.join(missing)}")
PY

Repository: cipherstash/stack

Length of output: 231


🏁 Script executed:

# Check build configuration and .cjs file existence in packages/prisma-next
cd packages/prisma-next

echo "=== Contents of packages/prisma-next/ ==="
ls -la

echo -e "\n=== Build script from package.json ==="
cat package.json | grep -A 2 '"scripts"'

echo -e "\n=== Check dist directory for .cjs files ==="
if [ -d "dist" ]; then
  ls -la dist/ | head -20
else
  echo "dist directory does not exist"
fi

echo -e "\n=== Look for build configuration files ==="
ls -la | grep -E "(tsconfig|esbuild|rollup|webpack|vite)"

echo -e "\n=== Check for build config in root ==="
ls -la ../../ | grep -E "(tsconfig|esbuild|rollup|webpack|vite|turbo)"

Repository: cipherstash/stack

Length of output: 1373


🏁 Script executed:

# Check the tsup.config.ts to see what output formats are configured
cat packages/prisma-next/tsup.config.ts

Repository: cipherstash/stack

Length of output: 537


The proposed fix is incomplete—CJS output is not being generated.

The exports map violates the coding guideline (ESM-only breaks CJS compatibility). However, the proposed "require": "./dist/[name].cjs" conditions won't work because the build configuration (tsup.config.ts) is set to format: ['esm'] and does not generate .cjs files.

To restore CJS compatibility, you must:

  1. Update tsup.config.ts to include 'cjs' in the format array.
  2. Add "require" conditions to each subpath export in package.json.
🧰 Tools
🪛 GitHub Actions: Test JS / 0_Run Tests.txt

[error] Test step failed. Command 'pnpm run test' exited with code 1 in /home/runner/_work/stack/stack/packages/prisma-next.

🪛 GitHub Actions: Test JS / Run Tests

[error] Test command failed: pnpm run test (vitest run) exited with code 1 due to 6 failing tests.

🤖 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 `@packages/prisma-next/package.json` around lines 26 - 63, The exports map in
package.json currently provides only "import" (ESM) entries for subpaths like
"./codec-types", "./column-types", "./control", "./middleware", "./migration",
"./operation-types", "./pack", "./runtime", and "./stack" but tsup is building
only ESM so no .cjs artifacts exist; fix by updating tsup.config.ts to include
'cjs' in the format array (e.g. format: ['esm','cjs']) so .cjs outputs are
emitted, then add a "require": "./dist/<name>.cjs" condition to each
corresponding subpath in the package.json "exports" map (for every
"./codec-types", "./column-types", "./control", "./middleware", "./migration",
"./operation-types", "./pack", "./runtime", "./stack") so CommonJS consumers can
resolve the CJS bundles.

Comment on lines +144 to +174
if (handle.ciphertext === undefined) {
// Misconfig diagnostic: when an SDK-bound codec sees a pre-encrypt
// envelope but no `bulkEncryptMiddleware(sdk)` has been
// constructed against that same SDK, the two-pass flow can never
// complete. Throw at the codec boundary with a copy-pasteable
// wiring snippet rather than letting the envelope reach the pg
// driver and produce an opaque serialise error.
if (!this.#middlewareCheckPassed && this.sdk !== undefined) {
if (!isBulkEncryptMiddlewareRegistered(this.sdk)) {
throw runtimeError(
'RUNTIME.ENCODE_FAILED',
`cipherstash ${this.descriptor.codecId}: encrypted column value has not been encrypted, ` +
'and no `bulkEncryptMiddleware(sdk)` has been registered with this SDK. ' +
'Wire it up alongside the extension descriptor:\n\n' +
' postgres<Contract>({\n' +
' contractJson,\n' +
' extensions: [createCipherstashRuntimeDescriptor({ sdk })],\n' +
' middleware: [bulkEncryptMiddleware(sdk)],\n' +
' });\n\n' +
'Both must close over the SAME `sdk` reference. See the @cipherstash/prisma-next README for the full wiring example.',
{
codecId: this.descriptor.codecId,
reason: 'cipherstash-bulk-encrypt-middleware-not-registered',
envelopeRouting: { table: handle.table, column: handle.column },
},
);
}
this.#middlewareCheckPassed = true;
}
return value;
}
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

Fail fast when encode runs without an attached SDK.

decode already guards metadata-only codec instances, but encode currently allows the pre-encrypt envelope sentinel to flow through when this.sdk is undefined. That yields a late/opaque failure path instead of a deterministic runtime error at the codec boundary.

Suggested fix
   const handle = value.expose();
   if (handle.ciphertext === undefined) {
+    if (this.sdk === undefined) {
+      throw runtimeError(
+        'RUNTIME.ENCODE_FAILED',
+        `cipherstash ${this.descriptor.codecId}: encode invoked on a metadata-only codec instance that has no SDK attached.`,
+        {
+          codecId: this.descriptor.codecId,
+          reason: 'cipherstash-sdk-required',
+        },
+      );
+    }
-    if (!this.#middlewareCheckPassed && this.sdk !== undefined) {
+    if (!this.#middlewareCheckPassed) {
       if (!isBulkEncryptMiddlewareRegistered(this.sdk)) {
         throw runtimeError(
📝 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
if (handle.ciphertext === undefined) {
// Misconfig diagnostic: when an SDK-bound codec sees a pre-encrypt
// envelope but no `bulkEncryptMiddleware(sdk)` has been
// constructed against that same SDK, the two-pass flow can never
// complete. Throw at the codec boundary with a copy-pasteable
// wiring snippet rather than letting the envelope reach the pg
// driver and produce an opaque serialise error.
if (!this.#middlewareCheckPassed && this.sdk !== undefined) {
if (!isBulkEncryptMiddlewareRegistered(this.sdk)) {
throw runtimeError(
'RUNTIME.ENCODE_FAILED',
`cipherstash ${this.descriptor.codecId}: encrypted column value has not been encrypted, ` +
'and no `bulkEncryptMiddleware(sdk)` has been registered with this SDK. ' +
'Wire it up alongside the extension descriptor:\n\n' +
' postgres<Contract>({\n' +
' contractJson,\n' +
' extensions: [createCipherstashRuntimeDescriptor({ sdk })],\n' +
' middleware: [bulkEncryptMiddleware(sdk)],\n' +
' });\n\n' +
'Both must close over the SAME `sdk` reference. See the @cipherstash/prisma-next README for the full wiring example.',
{
codecId: this.descriptor.codecId,
reason: 'cipherstash-bulk-encrypt-middleware-not-registered',
envelopeRouting: { table: handle.table, column: handle.column },
},
);
}
this.#middlewareCheckPassed = true;
}
return value;
}
if (handle.ciphertext === undefined) {
// Misconfig diagnostic: when an SDK-bound codec sees a pre-encrypt
// envelope but no `bulkEncryptMiddleware(sdk)` has been
// constructed against that same SDK, the two-pass flow can never
// complete. Throw at the codec boundary with a copy-pasteable
// wiring snippet rather than letting the envelope reach the pg
// driver and produce an opaque serialise error.
if (this.sdk === undefined) {
throw runtimeError(
'RUNTIME.ENCODE_FAILED',
`cipherstash ${this.descriptor.codecId}: encode invoked on a metadata-only codec instance that has no SDK attached.`,
{
codecId: this.descriptor.codecId,
reason: 'cipherstash-sdk-required',
},
);
}
if (!this.#middlewareCheckPassed) {
if (!isBulkEncryptMiddlewareRegistered(this.sdk)) {
throw runtimeError(
'RUNTIME.ENCODE_FAILED',
`cipherstash ${this.descriptor.codecId}: encrypted column value has not been encrypted, ` +
'and no `bulkEncryptMiddleware(sdk)` has been registered with this SDK. ' +
'Wire it up alongside the extension descriptor:\n\n' +
' postgres<Contract>({\n' +
' contractJson,\n' +
' extensions: [createCipherstashRuntimeDescriptor({ sdk })],\n' +
' middleware: [bulkEncryptMiddleware(sdk)],\n' +
' });\n\n' +
'Both must close over the SAME `sdk` reference. See the `@cipherstash/prisma-next` README for the full wiring example.',
{
codecId: this.descriptor.codecId,
reason: 'cipherstash-bulk-encrypt-middleware-not-registered',
envelopeRouting: { table: handle.table, column: handle.column },
},
);
}
this.#middlewareCheckPassed = true;
}
return value;
}
🧰 Tools
🪛 GitHub Actions: Test JS / Run Tests

[error] Test command failed: pnpm run test (vitest run) exited with code 1 due to 6 failing tests.

🤖 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 `@packages/prisma-next/src/execution/cell-codec-factory.ts` around lines 144 -
174, When encode encounters a pre-encrypt envelope (handle.ciphertext ===
undefined) ensure we fail fast if no SDK is attached: add a guard that if
this.sdk === undefined you throw a runtimeError('RUNTIME.ENCODE_FAILED', ...)
describing that the codec was used without an attached SDK and include
this.descriptor.codecId, a reason like 'cipherstash-sdk-not-attached', and
envelopeRouting with handle.table and handle.column; keep the existing
middleware-check branch (which uses isBulkEncryptMiddlewareRegistered(this.sdk)
and sets this.#middlewareCheckPassed) but only run it when this.sdk is present
so the error for missing SDK is deterministic at the codec boundary.

Comment thread packages/prisma-next/src/execution/cell-codec-factory.ts Outdated
Comment on lines +38 to +69
import baselineOps from '../../migrations/20260601T0000_install_eql_bundle/ops.json' with {
type: 'json',
};
import headRef from '../../migrations/refs/head.json' with { type: 'json' };
import contractJson from '../contract.json' with { type: 'json' };
import {
CIPHERSTASH_BASELINE_MIGRATION_NAME,
CIPHERSTASH_BIGINT_CODEC_ID,
CIPHERSTASH_BOOLEAN_CODEC_ID,
CIPHERSTASH_DATE_CODEC_ID,
CIPHERSTASH_DOUBLE_CODEC_ID,
CIPHERSTASH_JSON_CODEC_ID,
CIPHERSTASH_STRING_CODEC_ID,
} from '../extension-metadata/constants';
import { cipherstashPackMeta } from '../extension-metadata/descriptor-meta';
import {
cipherstashBigIntCodecHooks,
cipherstashBooleanCodecHooks,
cipherstashDateCodecHooks,
cipherstashDoubleCodecHooks,
cipherstashJsonCodecHooks,
cipherstashStringCodecHooks,
} from '../migration/cipherstash-codec';

const cipherstashContractSpace = contractSpaceFromJson<Contract<SqlStorage>>({
contractJson,
migrations: [
{
dirName: CIPHERSTASH_BASELINE_MIGRATION_NAME,
metadata: baselineMetadata,
ops: baselineOps,
},
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 | 🔴 Critical | ⚡ Quick win

Missing migration artifact import will break descriptor loading.

Line 38 imports ops.json, but this PR’s migration artifact set for packages/prisma-next/migrations/20260601T0000_install_eql_bundle/ does not include that file. This will fail module resolution when loading the control descriptor.

Please either add the generated ops.json artifact for that migration directory or update the contract-space wiring to use an available source of operations.

🧰 Tools
🪛 GitHub Actions: Test JS / Run Tests

[error] Test command failed: pnpm run test (vitest run) exited with code 1 due to 6 failing tests.

🤖 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 `@packages/prisma-next/src/exports/control.ts` around lines 38 - 69, The import
of baselineOps (import baselineOps ... ops.json) referenced by
CIPHERSTASH_BASELINE_MIGRATION_NAME is missing and will break descriptor
loading; either add the generated
migrations/20260601T0000_install_eql_bundle/ops.json artifact to the PR so
baselineOps is resolvable, or modify the contract-space wiring where
cipherstashContractSpace is built (the migrations array entry that uses
metadata: baselineMetadata and ops: baselineOps) to point ops to an existing
artifact/source (e.g., replace baselineOps with the correct JSON import or a
fallback loader) so module resolution succeeds.

Comment on lines +175 to +179
isEncryptedPayload(value) &&
typeof (value as { i: { t?: unknown; c?: unknown } }).i === 'object' &&
't' in (value as { i: object }).i &&
'c' in (value as { i: object }).i
if (!valid) {
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

🧩 Analysis chain

🏁 Script executed:

cat -n packages/prisma-next/src/stack/sdk-adapter.ts | sed -n '170,185p'

Repository: cipherstash/stack

Length of output: 909


🌐 Web query:

javascript typeof null object in operator property check

💡 Result:

In JavaScript, typeof null returns "object", a longstanding bug from the language's original implementation where null was represented as 0x00 (all zero bits), sharing the type tag 000 with objects.[1][2][3] Despite this, null is a primitive Null type value, not an object.[4][5] The in operator checks if a property exists in an object or its prototype chain, but throws a TypeError if the right-hand side is not an Object (i.e., a primitive like null).[6][7][8] Example: "prop" in null; // TypeError: cannot use 'in' operator to search for 'prop' in null This occurs because the spec requires: if Type(rval) is not Object, throw TypeError.[8] typeof null === "object" does not make null an object; in verifies the actual type internally.[6][7] To safely check properties, first guard against null/undefined: if (value != null && "prop" in value) { // safe } Or use Object.hasOwn(value, "prop") in modern JS, which returns false for null without error.[6]

Citations:


Add null-check for i before property-membership checks.

A ciphertext with i: null will throw a TypeError during the 't' in ... and 'c' in ... operations (since typeof null === "object" but the in operator requires an actual Object) instead of returning your controlled validation error.

Suggested fix
 function ensureEncryptedEnvelope(
   value: unknown,
   kind: 'decrypt' | 'bulkDecrypt',
   index?: number,
 ): Encrypted {
+  const info = (value as { i?: unknown }).i
   const valid =
     isEncryptedPayload(value) &&
-    typeof (value as { i: { t?: unknown; c?: unknown } }).i === 'object' &&
-    't' in (value as { i: object }).i &&
-    'c' in (value as { i: object }).i
+    info !== null &&
+    typeof info === 'object' &&
+    't' in info &&
+    'c' in info
📝 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
isEncryptedPayload(value) &&
typeof (value as { i: { t?: unknown; c?: unknown } }).i === 'object' &&
't' in (value as { i: object }).i &&
'c' in (value as { i: object }).i
if (!valid) {
function ensureEncryptedEnvelope(
value: unknown,
kind: 'decrypt' | 'bulkDecrypt',
index?: number,
): Encrypted {
const info = (value as { i?: unknown }).i
const valid =
isEncryptedPayload(value) &&
info !== null &&
typeof info === 'object' &&
't' in info &&
'c' in info
if (!valid) {
🧰 Tools
🪛 GitHub Actions: Test JS / Run Tests

[error] Test command failed: pnpm run test (vitest run) exited with code 1 due to 6 failing tests.

🤖 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 `@packages/prisma-next/src/stack/sdk-adapter.ts` around lines 175 - 179, The
validation currently checks typeof (value as { i: { t?: unknown; c?: unknown }
}).i === 'object' then uses 't' in ... and 'c' in ... which will throw if i is
null; update the conditional that builds valid (used in sdk-adapter.ts) to
explicitly ensure value.i is non-null (e.g. value.i !== null) before doing the
't' in and 'c' in membership checks so a ciphertext with i: null yields a
controlled validation failure instead of a TypeError; keep the surrounding
isEncryptedPayload(value) check and the same property names (i, t, c).

Comment thread packages/prisma-next/test/bundling-isolation.test.ts Outdated
singleDecryptCalls.push(args);
const ct = args.ciphertext as { c?: string } | null;
if (!ct || typeof ct.c !== 'string' || !ct.c.startsWith('ct:')) {
throw new Error(`mock SDK: cannot decrypt: ${JSON.stringify(args.ciphertext)}`);
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

Remove payload interpolation from mock SDK error strings.

These messages currently include stringified payloads, which can leak plaintext-like test values into logs/output. Prefer fixed diagnostics without embedding payload contents.

Suggested patch
-        throw new Error(`mock SDK: cannot decrypt: ${JSON.stringify(args.ciphertext)}`);
+        throw new Error('mock SDK: cannot decrypt ciphertext payload');
@@
-            throw new Error(`mock SDK: cannot bulk-decrypt: ${JSON.stringify(ciphertext)}`);
+            throw new Error('mock SDK: cannot bulk-decrypt ciphertext payload');
As per coding guidelines, "Never log plaintext data; the library by design never logs plaintext sensitive information".

Also applies to: 80-80

🧰 Tools
🪛 GitHub Actions: Test JS / Run Tests

[error] Test command failed: pnpm run test (vitest run) exited with code 1 due to 6 failing tests.

🤖 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 `@packages/prisma-next/test/decrypt-all.test.ts` at line 60, Remove the payload
interpolation from the mock SDK error strings in the test so plaintext-like
values aren't logged: replace the dynamic message that references
args.ciphertext (the line throwing new Error(`mock SDK: cannot decrypt:
${JSON.stringify(args.ciphertext)}`)) with a fixed diagnostic string such as
"mock SDK: cannot decrypt" (and make the same change for the similar occurrence
around the other failing throw at lines ~80), leaving the throw behavior
unchanged but eliminating embedding of sensitive payload data.

@coderdan
Copy link
Copy Markdown
Contributor

Reviewed end-to-end. Architecture and batching design are solid — cipher-side batching by (table, column), no-equality-trait pinning, envelope redaction defenses, and the structural bundle-isolation test are all great. A few follow-up suggestions, none blocking:

1. Microtask coalescer inside envelope.decrypt()

packages/prisma-next/src/execution/envelope-base.ts:191-218

The single-cell decrypt fallback is correct, but a per-row pattern like for (const r of rows) await r.email.decrypt() silently turns into N ZeroKMS round-trips. We deliberately don't auto-decrypt in middleware (would break ciphertext-passthrough APIs where the caller wants to forward envelopes to a downstream system without materialising plaintext), so the explicit step stays at the user's call site.

Suggestion: add a per-SDK microtask coalescer inside decrypt(). Each call enqueues {envelope, sdk, routingKey} into a pending map and awaits a deferred promise; a microtask drains the map and issues one bulkDecrypt per (sdk, table, column) group. The explicit-decrypt boundary stays user-controlled; ciphertext-passthrough is unaffected (never calls decrypt); decryptAll remains the eager-batch primitive for whole result sets, just no longer the only way to avoid an accidental N+1.

2. Cross-operation batching on the write side

packages/prisma-next/src/middleware/bulk-encrypt.ts:98-150

Each Prisma operation is its own batching boundary, so Promise.all(seeds.map(s => User.create(s))) issues N independent bulk-encrypt calls (one value each). 1000 single-row creates × 6 encrypted cols = 6000 ZeroKMS calls vs ≤ 6 if coalesced.

Either push createMany harder in the README/example, or add a request-scoped DataLoader-style coalescer between the middleware and the SDK that defers each bulkEncrypt by a microtask and merges same-(table, column) calls in the tick. The coalescer would dovetail naturally with (1).

3. Missing test coverage

A few security/correctness properties are unpinned today:

  • Envelope redaction across all six subclasses. String(), JSON.stringify(), and util.inspect() should redact every concrete envelope — only base coverage exists today. Add a parametrised test over the six subclasses.
  • Encrypt failure aborts the write. No test asserts that a thrown bulkEncrypt rejects the write with no replaceValues having run. Mid-batch failure shouldn't be able to leave half the row written. Add encryptImpl: () => { throw new Error('boom') } and assert params.replaceValues was never called.
  • DELETE with cipherstashEq predicate. stampRoutingKeysFromAst only handles INSERT/UPDATE arms (bulk-encrypt.ts:188-200); WHERE-clause envelopes get routing context from the operator path. Today that's fine, but a regression test pins the property so a future AST kind without an operator path fails loudly rather than silently.

4. SDK error stringification

packages/prisma-next/src/stack/sdk-adapter.ts:119

throw new Error(`cipherstash bulkDecrypt entry failed: ${String(entry.error)}`)

If a future SDK build ever stuffs ciphertext or plaintext into its error payload, this propagates it to logs/error reporters. Wrap in something like typeof entry.error === 'string' ? entry.error : '<opaque>' so the trust boundary stays one-way.


Skipping the search-mode-default suggestion — keeping equality/freeTextSearch/orderAndRange defaulted to true is a deliberate usability-vs-leakage tradeoff and the README covers the tradeoff sufficiently.

@@ -0,0 +1,17 @@
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did you intend to commit this file?

Comment on lines +36 to +43
// The CipherStash EQL bundle hits a SQL-injection-style bug in
// `eql_v2.add_encrypted_constraint` when the table or column name
// needs quoting (reserved words, mixed case, etc.). `user` is
// reserved in Postgres, so we map the table to `users`; the
// mixed-case columns are mapped to lowercase column names so the
// bundle's `%I` interpolation renders them unquoted. Drop these
// overrides once the upstream bundle bug is fixed.
@@map("users")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is this referring to? Is there a an issue filed in EQL @calvinbrewer?

Comment thread examples/prisma/src/db.ts Outdated
*
* `cipherstashFromStack({ contractJson })` derives the encryption
* schemas from the contract, constructs the `@cipherstash/stack`
* `EncryptionClient` against your `CS_*` env vars, builds the SDK
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"EncryptionClientagainst yourCS_*` env vars, builds the SDK"

Change to:

"EncryptionClientagainst yourCS_*` env vars or local profile, builds the SDK"

Comment thread examples/prisma/src/db.ts
import type { Contract } from './prisma/contract.d'
import contractJson from './prisma/contract.json' with { type: 'json' }

const cipherstash = await cipherstashFromStack({ contractJson })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I find this function name confusing: cipherstashFromStack implies that its loading/building something from the cipherstash stack but it seems to be some kind of adapter for the Prisma contract.

CipherStash is the stack, not something you get from the stack.

This is also quite different to the example if the draft blog post - how does it relate?

Comment on lines +157 to +160
await decryptAll(rows)
for (const row of rows) {
console.log(` ${row.id}: ${await row.email.decrypt()}`)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This code isn't right. It calls decryptAll, ignores the result and then decrypts each item using a single value call (many ZeroKMS requests). See my general comments about a dataloader type pattern which would avoid this footgun, too.

Comment on lines +168 to +172
console.log(`Found ${rows.length} user(s) with salary > 100,000.`)
await decryptAll(rows)
for (const row of rows) {
console.log(` ${row.id}: salary=${await row.salary.decrypt()}`)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same issue here.

Comment thread examples/prisma/.env.example Outdated

# CipherStash workspace credentials — **deployment only**.
#
# For local development, run `stash auth login` once. The PKCE flow
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing npx in command.

* comparison functions (`eql_v2.eq`, `eql_v2.ilike`) on
* `cipherstash/string@1`-typed columns. The lowering shape mirrors the
* canonical templates in the reference Prisma integration at
* `reference/cipherstash/stack/packages/stack/src/prisma/core/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems to refer to a directory outside of this repo

Comment on lines +279 to +297
function eqlOperator(publicMethod: string, eqlFunction: 'eq' | 'ilike'): SqlOperationDescriptor {
return {
self: { codecId: CIPHERSTASH_STRING_CODEC_ID },
impl: (self: Expression<ScopeField>, value: unknown): Expression<PgBoolReturn> => {
const selfCodec = requireSelfCodec(self, publicMethod);
const selfAst = toExpr(self, selfCodec);
return buildOperation({
method: publicMethod,
args: [selfAst, asEncryptedParam(selfAst, selfCodec, value)],
returns: { codecId: PG_BOOL_CODEC_ID, nullable: false },
lowering: {
targetFamily: 'sql',
strategy: 'function',
template: `eql_v2.${eqlFunction}({{self}}, {{arg0}})`,
},
});
},
};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is this needed? The eql functions don't need to be called directly (and arguably shouldn't).

eql_v2.like and eql_v2.ilike are the only exceptions (like is missing), = should use a standard operator (same as >, <, >= etc).

Comment on lines +35 to +46
* The framework`s built-in `email.eq(...)` is **not reachable** on
* cipherstash columns: the cipherstash codec declares no `equality`
* trait (see `codec-runtime.ts` / `codec-metadata.ts` / `parameterized.ts`),
* and the model-accessor synthesis in `sql-orm-client` gates
* `COMPARISON_METHODS_META.eq` on the `equality` trait being present in
* the column codec`s trait set. Calling `email.eq(...)` on a cipherstash
* column is therefore `undefined` — the wrong-SQL footgun (where the
* built-in `eq` would lower to standard SQL `=` against an
* `eql_v2_encrypted` value, silently returning zero rows because EQL
* ciphers contain randomized nonces) is closed at the codec layer, not
* the operator layer. The trait declaration is regression-pinned by
* `test/equality-trait-removal.test.ts`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This isn't correct. Calling = on an eql_v2_encrypted type is valid and preferred (so long as the argument is correctly encrypted).

I don't have a good understanding of how the codecs work in Prisma Next so I don't know if the operator overrides (cipherstashEq instead of eq) need to stay in place anyway, but technically we don't need to modify the operator itself, just encrypt the argument.

At the very least, the descriptive comment here should be corrected.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, we should use this as an opportunity to start using match instead of like/ilike. EQL 2.3 will move to this as well.

The reason is that the behaviour is different to like as it doesn't correctly support wildcards. Its closer in behaviour to tsearch in postgres.

@coderdan
Copy link
Copy Markdown
Contributor

One more — stale doc references that won't resolve for public consumers.

Several comments link to Prisma Next internal docs that aren't reachable from this repo. DEVELOPING.md:338 spells it out explicitly: ../../../docs/architecture docs/adrs/ADR 212 - Contract spaces.md resolves to a path above the repo root, i.e. a sibling checkout. find against the stack repo confirms no architecture dir, no ADR* files anywhere. Affected sites:

  • packages/prisma-next/src/exports/control.ts:27@see docs/architecture docs/adrs/ADR 212 - Contract spaces.md
  • packages/prisma-next/src/contract.prisma:38 — same kind of reference but cites ADR 211 (different number) for what looks like the same concept; one of the two is stale.
  • packages/prisma-next/migrations/20260601T0000_install_eql_bundle/migration.ts:20-21 — same ADR 212 reference.
  • packages/prisma-next/test/descriptor.test.ts:22 — same.
  • packages/prisma-next/DEVELOPING.md:338 — the explicit ../../../docs/... link.
  • packages/prisma-next/src/exports/control.ts:87-88 — "Mirrors pgvector's wiring at packages/3-extensions/pgvector/src/exports/control.ts" is also a Prisma Next-internal path.

Options: strip the @see lines (the surrounding comments are self-contained), or replace with a link to whatever the public-facing Prisma Next contract-spaces docs page is once one exists. Same treatment for the pgvector cross-reference.

Also worth reconciling ADR 211 vs ADR 212 between contract.prisma and the other sites — either the upstream ADR got renumbered or these are two different ADRs.

Comment on lines +2 to +6
* Public middleware surface for the cipherstash extension.
*
* Consumers register the bulk-encrypt middleware in their runtime so
* `EncryptedString` envelopes embedded in `INSERT` / `UPDATE` plans get
* encrypted in batches before encode runs:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth explaining that encryption is performed automatically by the middleware but that decryption is explicit (using decrypt, decryptAll).

Comment on lines +32 to +33
* @see ADR 195 — Planner IR with two renderers.
* @see ADR 213 — Codec lifecycle hooks.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Refers to Prisma design rules not available in this repo.

* registration without pulling in any runtime code (envelope, SDK,
* codec runtime, middleware).
*
* Mirrors `packages/3-extensions/pgvector/src/exports/pack.ts`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Refers to files in the prisma repo.


export const EQL_INSTALL_VERSION = 'eql-2.2.1' as const;

export const EQL_INSTALL_SQL: string = `--! @file schema.sql
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks like the raw function definitions, not the final "built" version of EQL. Released versions strip docs and perform additional transformations that should be included.

See https://github.com/cipherstash/encrypt-query-language/releases

@@ -0,0 +1,397 @@
/**
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As a follow-up (this PR is already huge), but we should make sure these tests run in GH workflow.

Comment thread packages/prisma-next/DEVELOPING.md Outdated

### `EncryptedEnvelopeBase<T>` — shared envelope superclass

`packages/3-extensions/cipherstash/src/execution/envelope-base.ts` exports an abstract `EncryptedEnvelopeBase<T>` class that holds the `#`-prefixed `EncryptedHandle<T>` slot and ships the five redaction overrides (`toJSON`, `toString`, `valueOf`, `Symbol.toPrimitive`, `Symbol.for('nodejs.util.inspect.custom')`), `expose()`, `decrypt({ signal? })`, and the post-decrypt plaintext cache.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Retains some references to files in the prisma repo.

Comment thread packages/prisma-next/DEVELOPING.md Outdated
Comment on lines +263 to +267
| Linear ticket | Surface |
| --- | --- |
| [TML-2388](https://linear.app/prisma-company/issue/TML-2388) | Codec-SDK binding refactor — pull the per-tenant SDK binding out of the codec factory closure into the descriptor seam so multi-tenant deployments don't re-author the codec per tenant. |
| Polymorphic `CipherstashSdk.decrypt` return type | One-line interface widening from `Promise<string>` to `Promise<unknown>` to mirror the bulk shape; removes a narrowing cast in `EncryptedEnvelopeBase.decrypt`. |
| [TML-2504 — Cipherstash JSONB path-exists predicate: STE-VEC selector hashing](https://linear.app/prisma-company/issue/TML-2504) | `cipherstashJsonbPathExists` against the live EQL bundle expects a hashed STE-VEC selector computed via the CipherStash SDK's `selector(...)` API; the framework currently binds the JSONpath as a plain `pg/text@1` `ParamRef`. Round-trip and the two SELECT-expression helpers (`cipherstashJsonbPathQueryFirst`, `cipherstashJsonbGet`) work; the predicate clause returns zero rows. Resolution requires either a client-side path-hashing middleware or an EQL-side plaintext-path overload. |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some references to Prisma private Linear should be removed/updated.

Comment thread packages/prisma-next/DEVELOPING.md Outdated
Comment on lines +334 to +341
- [pgvector extension](../pgvector/README.md) — the structural precedent for codec, parameterized descriptor, and pack-meta layout.
- [ADR 202 — Codec trait system](../../../docs/architecture%20docs/adrs/ADR%20202%20-%20Codec%20trait%20system.md).
- [ADR 207 — Codec call context per-query AbortSignal and column metadata](../../../docs/architecture%20docs/adrs/ADR%20207%20-%20Codec%20call%20context%20per-query%20AbortSignal%20and%20column%20metadata.md).
- [ADR 208 — Higher-order codecs for parameterized types](../../../docs/architecture%20docs/adrs/ADR%20208%20-%20Higher-order%20codecs%20for%20parameterized%20types.md).
- [ADR 212 — Contract spaces](../../../docs/architecture%20docs/adrs/ADR%20212%20-%20Contract%20spaces.md).
- [ADR 213 — Codec lifecycle hooks](../../../docs/architecture%20docs/adrs/ADR%20213%20-%20Codec%20lifecycle%20hooks.md).
- [ADR 214 — Extension operator surface: namespaced replacement operators and the predicate/helper split](../../../docs/architecture%20docs/adrs/ADR%20214%20-%20Extension%20operator%20surface%20namespaced%20replacement%20operators.md).
- [ADR 215 — Runtime middleware lifecycle: `beforeExecute` fires before `encodeParams`](../../../docs/architecture%20docs/adrs/ADR%20215%20-%20Runtime%20middleware%20lifecycle%20beforeExecute%20before%20encodeParams.md).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Most/all of these should be updated or removed.

Comment thread packages/prisma-next/package.json Outdated
"name": "@cipherstash/prisma-next",
"version": "0.0.0",
"license": "MIT",
"author": "CipherStash <hello@cipherstash.com>",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This email address doesn't exist FYI!

Comment thread packages/prisma-next/README.md Outdated
@@ -0,0 +1,126 @@
# @cipherstash/prisma-next

**Searchable field-level encryption for Postgres with [Prisma Next](https://www.npmjs.com/package/@prisma-next/cli)** — via the [EQL bundle](https://cipherstash.com/docs/stack/platform/eql).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this should point to https://www.npmjs.com/package/prisma-next

Probably should link to @cipherstash/stack before EQL, too.

Comment thread packages/prisma-next/README.md Outdated

## Features

- 🔒 Six encrypted column types — string, double, bigint, date, boolean, JSON
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggest adding backticks around the types. e.g. string.

Comment thread packages/prisma-next/README.md Outdated
## Features

- 🔒 Six encrypted column types — string, double, bigint, date, boolean, JSON
- 🔍 Searchable encryption — equality, free-text search (ILIKE), range, order, JSON path
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion:

Searchable encryption — equality, free-text search, range, order, JSON path and containment

Comment on lines +84 to +85
await decryptAll(rows)
console.log(await rows[0]?.email.decrypt())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This example isn't correct (same issue as the example code earlier).

Comment thread packages/prisma-next/README.md Outdated
```

```bash
stash auth login # one-time, per developer
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

npx stash auth login

Comment thread packages/prisma-next/README.md Outdated
Comment on lines +105 to +108
`stash auth login` runs a PKCE flow and caches credentials in your OS keychain — each developer ends up with their own identity for every encrypt / decrypt against the workspace. No `CS_*` env vars in local development.

The four `CS_*` env vars (`CS_WORKSPACE_CRN`, `CS_CLIENT_ID`, `CS_CLIENT_KEY`, `CS_CLIENT_ACCESS_KEY`) are reserved for production deployments and CI runners. See the [authentication docs](https://cipherstash.com/docs/stack/cipherstash/encryption/prisma-next#authentication) for the full identity story.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested copy:

There are 2 main ways to authenticate to CipherStash:

### Local profile (Dev)

`npx stash auth login` lets you login via the browser and saves credentials in the CipherStash profile (`~/.cipherstash`). A key is automatically generated and granted access to the default keyset.

### Env vars (Production)

The four `CS_*` env vars (`CS_WORKSPACE_CRN`, `CS_CLIENT_ID`, `CS_CLIENT_KEY`, `CS_CLIENT_ACCESS_KEY`) are reserved for production deployments and CI runners. See the [authentication docs](https://cipherstash.com/docs/stack/encryption/prisma-next#authentication) for more information.

The auth stuff should really link to /docs/stack/auth but that section hasn't been created yet.

Comment thread packages/prisma-next/README.md Outdated
- 📖 [**Full docs**](https://cipherstash.com/docs/stack/cipherstash/encryption/prisma-next) — column types, operator reference, security model, known limitations.
- [CipherStash EQL reference](https://cipherstash.com/docs/stack/platform/eql) — encrypted operator semantics and search-config index types.
- [`@cipherstash/stack`](../stack/README.md) — encryption SDK and schema DSL.
- [Prisma Next CLI](https://www.npmjs.com/package/@prisma-next/cli) — the framework this extension plugs into.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

@coderdan coderdan left a comment

Choose a reason for hiding this comment

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

Wowza. Epic! Very exciting (but very large!) PR. I manually reviewed as much as I could and relied on Claude and Coderabbit for the rest.

I've added a number of things that we should address before release but they don't block this PR.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
examples/prisma/test/e2e/global-setup.ts (1)

71-77: ⚡ Quick win

Validate all required CipherStash env vars upfront.

Line 71 currently gates only on CS_WORKSPACE_CRN, but the thrown message says companion credentials are also required. Validate the full required set here so auth/config failures are immediate and deterministic.

Suggested patch
-  if (!process.env['CS_WORKSPACE_CRN']) {
+  const requiredEnv = ['CS_WORKSPACE_CRN', 'CS_CLIENT_ID', 'CS_CLIENT_KEY'] as const;
+  const missing = requiredEnv.filter((k) => !process.env[k]);
+  if (missing.length > 0) {
     throw new Error(
-      'cipherstash e2e harness: `CS_WORKSPACE_CRN` is not set. Populate `.env` ' +
-        '(see `.env.example`) with a ZeroKMS workspace and the three companion ' +
+      `cipherstash e2e harness: missing required env var(s): ${missing.join(', ')}. Populate \`.env\` ` +
+        '(see `.env.example`) with a ZeroKMS workspace and the three companion ' +
         'credentials before running `pnpm test:e2e`.',
     );
   }
🤖 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/test/e2e/global-setup.ts` around lines 71 - 77, Replace the
single-variable guard that only checks process.env['CS_WORKSPACE_CRN'] with a
check of the full required set (e.g.
['CS_WORKSPACE_CRN','CS_KEY_ID','CS_CLIENT_ID','CS_CLIENT_SECRET']) by creating
a REQUIRED_ENV array, computing missing = REQUIRED_ENV.filter(k =>
!process.env[k]), and throwing the same Error if missing.length > 0 that
includes the list of missing variable names and the existing guidance; update
the existing throw site in global-setup.ts (the block that currently tests
process.env['CS_WORKSPACE_CRN']) to use this new check so failures are immediate
and deterministic.
🤖 Prompt for all review comments with 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.

Inline comments:
In @.github/workflows/prisma-next-e2e.yml:
- Around line 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.

In `@examples/prisma/test/e2e/harness.ts`:
- Around line 57-78: The truncateUsers() function's spawnSync('docker', ...)
call should be hardened: pass a reasonable timeout (same pattern used elsewhere,
e.g., pg_isready calls) and include full spawn failure handling by checking
result.error and result.signal in addition to result.status; when any of those
indicate failure, throw an Error that includes the timeout/errno/signal plus
stdout/stderr content to aid debugging. Update the spawnSync invocation in
truncateUsers() to include a timeout option and adjust the subsequent failure
branch to detect and report result.error (process spawn errors), result.signal
(killed by signal), and non-zero result.status with a single detailed error
message containing the error, signal, status, stderr, and stdout.

---

Nitpick comments:
In `@examples/prisma/test/e2e/global-setup.ts`:
- Around line 71-77: Replace the single-variable guard that only checks
process.env['CS_WORKSPACE_CRN'] with a check of the full required set (e.g.
['CS_WORKSPACE_CRN','CS_KEY_ID','CS_CLIENT_ID','CS_CLIENT_SECRET']) by creating
a REQUIRED_ENV array, computing missing = REQUIRED_ENV.filter(k =>
!process.env[k]), and throwing the same Error if missing.length > 0 that
includes the list of missing variable names and the existing guidance; update
the existing throw site in global-setup.ts (the block that currently tests
process.env['CS_WORKSPACE_CRN']) to use this new check so failures are immediate
and deterministic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d9ef9669-95c0-420c-ace6-257b456f9233

📥 Commits

Reviewing files that changed from the base of the PR and between dc02d0b and 4c2d6d4.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (26)
  • .github/workflows/prisma-next-e2e.yml
  • .gitignore
  • examples/prisma/.env.example
  • examples/prisma/package.json
  • examples/prisma/src/db.ts
  • examples/prisma/test/e2e/README.md
  • examples/prisma/test/e2e/bigint.e2e.test.ts
  • examples/prisma/test/e2e/bool.e2e.test.ts
  • examples/prisma/test/e2e/date.e2e.test.ts
  • examples/prisma/test/e2e/docker-compose.yml
  • examples/prisma/test/e2e/global-setup.ts
  • examples/prisma/test/e2e/harness.ts
  • examples/prisma/test/e2e/json.e2e.test.ts
  • examples/prisma/test/e2e/mixed.e2e.test.ts
  • examples/prisma/test/e2e/num.e2e.test.ts
  • examples/prisma/test/e2e/str-range.e2e.test.ts
  • examples/prisma/test/e2e/vitest.config.ts
  • packages/prisma-next/DEVELOPING.md
  • packages/prisma-next/README.md
  • packages/prisma-next/package.json
  • packages/prisma-next/src/execution/cell-codec-factory.ts
  • packages/prisma-next/src/execution/operators.ts
  • packages/prisma-next/src/exports/migration.ts
  • packages/prisma-next/src/exports/pack.ts
  • packages/prisma-next/test/bundling-isolation.test.ts
  • packages/prisma-next/turbo.json
✅ Files skipped from review due to trivial changes (6)
  • examples/prisma/test/e2e/README.md
  • examples/prisma/.env.example
  • .gitignore
  • packages/prisma-next/src/exports/migration.ts
  • packages/prisma-next/DEVELOPING.md
  • packages/prisma-next/README.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • examples/prisma/package.json
  • packages/prisma-next/package.json
  • packages/prisma-next/test/bundling-isolation.test.ts
  • examples/prisma/src/db.ts
  • packages/prisma-next/src/exports/pack.ts
  • packages/prisma-next/src/execution/cell-codec-factory.ts
  • packages/prisma-next/src/execution/operators.ts

Comment on lines +98 to +104
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
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.

Comment on lines +57 to +78
export function truncateUsers(): void {
const result = spawnSync(
'docker',
[
'exec',
'cipherstash-e2e-postgres',
'psql',
'-U',
'cipherstash',
'-d',
'cipherstash_e2e',
'-c',
'TRUNCATE TABLE users',
],
{ stdio: 'pipe' },
);
if (result.status !== 0) {
throw new Error(
`cipherstash e2e harness: TRUNCATE failed (exit ${result.status}):\n` +
`${result.stderr?.toString() ?? ''}\n${result.stdout?.toString() ?? ''}`,
);
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -i -e ts . examples/prisma/test/e2e | xargs rg -n -C3 'spawnSync\('
echo "---- harness truncateUsers() failure checks ----"
rg -n -C6 'export function truncateUsers|spawnSync\(|timeout|result\.error|result\.signal|result\.status' examples/prisma/test/e2e/harness.ts

Repository: cipherstash/stack

Length of output: 3586


Add timeout + comprehensive spawn failure handling in truncateUsers().

Line 58's spawnSync call lacks a timeout (line 71 shows { stdio: 'pipe' } only), creating a risk of indefinite hang if docker exec stalls. Additionally, line 73 checks only result.status !== 0, ignoring result.error and result.signal cases that communicate spawn failures and signals.

Other spawnSync calls in the suite (e.g., pg_isready at global-setup.ts:79) already include timeouts, establishing the pattern to follow.

Suggested patch
 export function truncateUsers(): void {
+  const TRUNCATE_TIMEOUT_MS = 10_000;
   const result = spawnSync(
     'docker',
@@
-    { stdio: 'pipe' },
+    { stdio: 'pipe', timeout: TRUNCATE_TIMEOUT_MS },
   );
-  if (result.status !== 0) {
+  if (result.error || result.signal || result.status !== 0) {
     throw new Error(
-      `cipherstash e2e harness: TRUNCATE failed (exit ${result.status}):\n` +
-        `${result.stderr?.toString() ?? ''}\n${result.stdout?.toString() ?? ''}`,
+      `cipherstash e2e harness: TRUNCATE failed` +
+        `${result.error ? ` (spawn error: ${result.error.message})` : ''}` +
+        `${result.signal ? ` (signal: ${result.signal})` : ''}` +
+        `${typeof result.status === 'number' ? ` (exit ${result.status})` : ''}:\n` +
+        `${result.stderr?.toString() ?? ''}\n${result.stdout?.toString() ?? ''}`,
     );
   }
 }
🤖 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/test/e2e/harness.ts` around lines 57 - 78, The
truncateUsers() function's spawnSync('docker', ...) call should be hardened:
pass a reasonable timeout (same pattern used elsewhere, e.g., pg_isready calls)
and include full spawn failure handling by checking result.error and
result.signal in addition to result.status; when any of those indicate failure,
throw an Error that includes the timeout/errno/signal plus stdout/stderr content
to aid debugging. Update the spawnSync invocation in truncateUsers() to include
a timeout option and adjust the subsequent failure branch to detect and report
result.error (process spawn errors), result.signal (killed by signal), and
non-zero result.status with a single detailed error message containing the
error, signal, status, stderr, and stdout.

@calvinbrewer calvinbrewer merged commit 4e907d0 into main May 14, 2026
7 checks passed
@calvinbrewer calvinbrewer deleted the prisma-next-release branch May 14, 2026 14:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants