Skip to content

Commit a75513b

Browse files
calvinbrewerclaudecoderdan
authored
Feat/prisma next eql v3 (#655)
* feat(prisma-next): derive v3 domain catalog + codec ids from stack DOMAIN_REGISTRY Codec ids are the DOMAIN_REGISTRY key verbatim (cipherstash/eql-v3/eql_v3_*@1) per the GA eql_v3_* domain naming; json is a first-class exposed domain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(prisma-next): v3 codec-id closed union, traits, invariants, drift test The closed union infers eqlType from getEqlType()'s return type — the stack's bundled d.ts drops the private definition field's type, so the EqlTypeForColumn infer-D path widens to public.${string} downstream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(prisma-next): v3 per-domain authoring constructors + *V2 aliases Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(prisma-next): v3 plain-JSONB cell codecs + EncryptedNumber envelope Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(prisma-next): v3 bulk-encrypt middleware (plain-JSONB payloads) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(prisma-next): v3 query operators — eql_v3.* lowering + capability gating Add cipherstashV3QueryOperations() (src/v3/operators-v3.ts): the EQL v3 operator set registered by the v3 extension descriptor only (decision 1b — same cipherstash* method names as v2, never co-registered; the flat OperationRegistry collision is pinned by test). Lowering matches the stack-drizzle v3 dialect byte-for-byte: operands are ciphertext-free query terms cast to the column domain's eql_v3.query_<domain> type (eql_v3.eq/neq/gt/gte/lt/lte/contains, self-parenthesised (gte AND lte) range, OR-of-eq membership), JSON containment is OPERATOR(public.@>) with the irregular ::eql_v3.query_jsonb cast (cipherstashJsonContains, empty-needle guarded), and ordering extracts eql_v3.ord_term / ord_term_ore by the domain's flavour via the free-standing cipherstashV3Asc/Desc helpers. Operands are bound as pg/text@1 params (bare $N; the template supplies the query cast — the column codec's storage-domain cast would fail the query term's CHECK) and each envelope is routing-key stamped plus marked with its queryType (markV3QueryTerm / v3QueryTermTypeOf), the seam Task 7's SDK adapter uses to route WHERE operands through encryptQuery instead of storage bulkEncrypt. Every operator gates on the concrete domain's catalog capabilities and throws the v3-owned EncryptionOperatorError naming column, domain, operator, and missing capability. v3 codec descriptors are pinned to cipherstash:*-only traits alongside the v2 equality-trait regression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(prisma-next): v3 schema derivation, SDK adapter, runtime descriptor + cipherstashFromStackV3 - deriveStackSchemasV3(contractJson): walks the 0.14 namespace envelope and maps each v3 column's public.eql_v3_* nativeType to its concrete @cipherstash/stack/eql/v3 factory (capabilities are intrinsic to the domain — no typeParams flags), preserving exact domain identity. - createCipherstashV3Sdk(client, schemas): adapts the EncryptionV3 TypedEncryptionClient (structural surface, no casts) to the framework CipherstashSdk. Consumes the Task-6 query-term seam: envelopes marked by markV3QueryTerm route through encryptQuery — one batch crossing per scalar flavour (stack-drizzle's inArray surface), single-call for searchableJson (stack-drizzle's JSON containment path) — while unmarked values take the storage bulkEncrypt path, position-stable. - bulkEncryptMiddlewareV3 now collects pg/text@1-bound marked envelopes (WHERE operands), forwards the envelope itself through the SDK seam, and writes the returned term back as query-term JSONB text without stamping it into the envelope's storage-ciphertext slot. - createCipherstashV3RuntimeDescriptor({ sdk }): the v3 extension descriptor under v3's own id/version (decision 1b — never co-registered with v2; shared method names collide by design). - assertV3SchemasAgree: override validation on EXACT domain identity (integer_ord ≠ integer_ord_ore despite shared cast_as). - cipherstashFromStackV3: the v3-only entry point — hard-errors on v2 cipherstash codec ids and on contracts with no v3 columns; returns { extensions, middleware, encryptionClient } like the v2 factory. - Query-term seam (markV3QueryTerm / v3QueryTermTypeOf + EncryptionOperatorError) extracted to src/v3/query-term.ts so the middleware/adapter don't import the operator registry; operators-v3 re-exports it unchanged. - Exports: v3 surface added to ./runtime and ./stack; new ./v3 subpath (ESM, matching the package's existing ESM-only exports map). - bundling-isolation: allow the v3 catalog chunk (pure domain metadata) as a second cross-plane shared chunk alongside the v2 constants chunk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(prisma-next): EQL v3 bundle baseline migration from @cipherstash/eql Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(prisma-next): property-based suites for v3 wire, catalog, and gating Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(prisma-next): v3 live-PG integration + bundling isolation pins Bundling isolation (test/bundling-isolation.test.ts): - v3.js joins the entry existence check and is pinned against both the contract-space artefacts (RUNTIME_FORBIDDEN) and the v2 wire plane - content-fingerprinted wire markers (encodeEqlV2EncryptedWire / makeCipherstashCellCodec vs CipherstashV3CellCodec / createV3CodecDescriptors / bulkEncryptMiddlewareV3): no code-split chunk may reference both wire planes, the v3 entry graph never reaches the v2 codec-runtime chunk, and middleware.js (v2-only entry) never loads the v3 wire Live-PG suites (test/live/, self-skipping via describeLivePg when CS creds — env vars or ~/.cipherstash profile — or DATABASE_URL are absent; `pnpm test:live` convenience script added): - helpers: live-gate (skip posture), eql-v3 (advisory-locked installEqlV3IfNeeded over the Task-8 migration source SQL), harness (real cipherstashFromStackV3 + real bulkEncryptMiddlewareV3 driven through real execution plans, operator SELECTs lowered by the real postgres adapter, envelope fromInternal().decrypt() read path) - operators-live-pg: eq/ne/inArray, contains (ilike), gt/lte/between, date + timestamp ranges, ord_term ordering, JSON containment (@> with eql_v3.query_jsonb — ruled decision 2), per-family decrypt - operators-null-live-pg: NULL cells round-trip, null operands rejected - boolean-storage-live-pg: storage-only round-trip, operators refuse - bigint-live-pg: lossless bigint beyond MAX_SAFE_INTEGER, eq/range/order - migration-apply-live-pg: applied SQL is byte-identical to the shipped ops.json, invariant id, domains + eql_v3 schema, op postchecks, no add_search_config - bulk-encrypt-live-pg: real EncryptionV3 through the real middleware, JSONB cells, ciphertext stamping, decrypt round-trip - side-by-side-clients-live-pg: v2 and v3 clients coexisting in-process against one database, distinct extension identities and wires Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(prisma-next): changeset for EQL v3 per-domain authoring surface Also a stack changeset for the new eql/v3 barrel exports, and the AGENTS.md repository-layout line updated for the v2/v3 split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(prisma-next): wire the v3 ORM surface end-to-end Gaps surfaced by converting the example app and driving the real CLI + runtime rather than unit fixtures: - CodecTypes/QueryOperationTypes cover all 40 v3 codec ids with trait-accurate operator visibility (cipherstash:v3-* marker traits; cipherstashJsonContains v3-only; storage-only domains surface no operator methods at the type level). - The v3 runtime descriptor presents the pack id ('cipherstash') with v3's own version — the runtime matches contract extensionPacks by descriptor id, so the old distinct id failed startup with RUNTIME.MISSING_EXTENSION_PACK. - Every v3 codec id registers a control-plane expandNativeType hook stripping the public. qualifier (the planner requires the hook for typeParams-carrying columns; bare names match introspected udt_name). No onFieldEvent — v3 emits zero add_search_config ops. - The v3 bundle op is reclassified 'data': the integrity checker rejects self-edges without a data-class op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(prisma-example): convert the example app to EQL v3 Schema: every column is a concrete public.eql_v3_* domain (EncryptedTextSearch / EncryptedDoubleOrd / EncryptedBigIntOrd / EncryptedDateOrd / EncryptedBoolean / EncryptedJson); wiring via cipherstashFromStackV3. Migrations regenerated from the v3 contract — initial app migration carries zero add_search_config ops; the cipherstash space ships both bundle baselines. E2E suites adapted to the v3 surface and green from a wiped database (7 files / 40 tests, live PG + ZeroKMS): equality/range/free-text tokens, order-term sorting, JSON @> containment (positive, negative, multi-key, nested, empty-needle rejection), lossless bigint beyond MAX_SAFE_INTEGER, and eql_v3_boolean's storage-only operator refusal pinned as a feature. README walkthrough e2e (repo-level) also green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(prisma-next): consume @cipherstash/stack/adapter-kit, drop the eql/v3 graduation The branch had graduated DOMAIN_REGISTRY / factoryForDomain / stripDomainSchema onto the public eql/v3 barrel while adapter-kit still exported the same symbols — two doors to one registry. Revert the graduation and import through adapter-kit, the seam the Drizzle and Supabase adapters already consume (PR #655 review, item 3). Also make v3Authored return a fresh descriptor per invocation so a caller mutation cannot alter later contracts (review). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(prisma-next): EQL-derived v3 operator vocabulary with a guarded eqlMatch Rename the v3 query operations to the eql_v3.* function vocabulary they lower to — eqlEq/eqlNeq/eqlIn/eqlNotIn/eqlGt/eqlGte/eqlLt/eqlLte/ eqlBetween/eqlNotBetween/eqlJsonContains, ordering via eqlAsc/eqlDesc. The eql prefix (not bare eq/gt) is required because the framework's native scope-field methods already claim eq/neq/gt/…, which lower to plain SQL comparisons (PR #655 review, items 1+2). The v2 surface keeps its cipherstash* names; the sets are now disjoint. Free-text search is eqlMatch, not ilike: eql_v3.contains is fuzzy bloom token matching, not SQL pattern semantics. Two guards run before encryption: wildcard normalisation (leading/trailing % stripped, interior % or any _ rejected) and the shared adapter-kit matchNeedleError short-needle rejection. cipherstashNotIlike is removed outright — negating a may-false-positive bloom test silently drops matching rows. Type-level dispatch mirrors the split: v3 codec entries carry only cipherstash:v3-* marker traits, so eql* methods never surface on v2 columns and cipherstash* methods never surface on v3 columns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(prisma-next): PR #655 review hardening across the v3 seams - wire-v3: v3ToDriver coerces JSON.stringify's undefined branch to null; v3FromDriver gains overloads so a nullable input yields a nullable result instead of laundering null into T. - sdk-adapter-v3: bulkDecrypt validates response cardinality (a truncated response would silently misalign plaintexts); the deliberate as-never bridges carry the repo's biome suppression. - pack meta: extensionPacks.cipherstash now registers the 40 v3 codec metadata instances + storage rows (derived from the catalog, never hand-listed) and the EncryptedNumber type import, so consumer contracts describe the codecs their fields use. - live suites: direct dotenv/config imports, canonical column-order binding in insertEncryptedRows (a differing key order could bind values to the wrong SQL columns), and migration-apply now uninstalls EQL v3 first so the customer-facing migration SQL genuinely executes on a reused database (test:live is serial now — the reinstall is destructive across files). - bundling-isolation: entry checks scan the complete import graph for every v2 wire/codec-factory marker, not just entry bodies. Verified live: 599 unit + 30 live-PG + 582 integration + 40 example-e2e + 7 README-walkthrough tests green against real ZeroKMS + Postgres. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(prisma-example): adopt the eql* operator surface, regenerate the contract Rename every v3 operator call to the EQL-derived vocabulary (eqlEq, eqlMatch, eqlAsc/eqlDesc, …), fully qualify the README's domain table (public.eql_v3_*), describe free-text search as fuzzy bloom token matching under its real needle semantics, drop the removed cipherstashNotIlike coverage, add a direct dotenv/config import to the str-range suite, and remove the type-erasing probe assertions in the bool suite. pnpm emit regenerates contract.{json,d.ts} — the extensionPacks.cipherstash block now carries the 40 v3 codec instances and storage rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(prisma-next): test-kit family integration suite + CI job Implement makePrismaNextAdapter() behind the shared @cipherstash/test-kit IntegrationAdapter seam and run runFamilySuite over every family — the same derived capability matrix, plaintext oracle, single-vs-bulk insert crossover, ordering assertions, and absent/null handling that pin the Drizzle and Supabase adapters (PR #655 review, item 4). Queries lower through the real v3 operator registry + postgres adapter + bulk-encrypt middleware; inserts run one-row vs multi-row INSERT statements, prisma-next's two batching shapes. New CI workflow mirrors the Drizzle job on both database variants. 582 tests green against real ZeroKMS + live Postgres. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(prisma-next): hermetic typecheck against stack source, gated in CI (#684) * fix(prisma-next): make the JSON encryptQuery cast suppressions effective The `searchableJson` encryptQuery call spreads its two `as never` operands across separate argument lines, so the single `// biome-ignore` above the call reached neither cast — both still tripped the no-type-erasing-assertions plugin, and the ignore itself was flagged as an ineffective suppression. Put one ignore directly above each cast line. Warning-only (CI gates on errors), but resolves the CodeRabbit thread and the dangling ineffective-suppression note. The scalar branch was already correct. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Dan Draper <dan@cipherstash.com>
1 parent 175eeb7 commit a75513b

118 files changed

Lines changed: 15427 additions & 1646 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/eql-v3-prisma-next.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@cipherstash/prisma-next': minor
3+
---
4+
5+
**Breaking:** EQL v3 columns are now authored through **concrete per-domain constructors** — the constructor you choose *is* the capability set. The legacy boolean-option surface (`EncryptedString({ equality, freeTextSearch, orderAndRange })`) is not carried into v3.
6+
7+
- New per-domain constructors, one per exposed `public.eql_v3_*` domain:
8+
- Text: `EncryptedText` (storage), `EncryptedTextEq`, `EncryptedTextOrd` (eq + order/range), `EncryptedTextMatch` (free-text), `EncryptedTextSearch` (eq + free-text + order/range).
9+
- Scalars (Integer, Smallint, BigInt, Numeric, Real, Double, Date, Timestamp): `Encrypted<Fam>` (storage), `Encrypted<Fam>Eq`, `Encrypted<Fam>Ord`.
10+
- `EncryptedBoolean` — storage-only (`public.eql_v3_boolean`); there is no boolean equality constructor.
11+
- `EncryptedJson` — searchable encrypted JSONB (`public.eql_v3_json`, `ste_vec`), queried with `eqlJsonContains` (`@>` containment). Selector querying (comparing the value at a JSONPath) is tracked in #677.
12+
- **Impossible capability combinations have no constructor** (e.g. text equality + free-text without order/range) — they are unrepresentable, not runtime errors.
13+
- **BigInt is a first-class v3 family** (`EncryptedBigInt` / `EncryptedBigIntEq` / `EncryptedBigIntOrd`, JS `bigint` plaintext, backed by `public.eql_v3_bigint*`).
14+
- Use the `*V2` constructors (`EncryptedStringV2`, `EncryptedDoubleV2`, `EncryptedBigIntV2`, `EncryptedDateV2`, `EncryptedBooleanV2`, `EncryptedJsonV2`) to keep EQL v2 columns. A client is v2 or v3 — the two runtime descriptors are never co-registered.
15+
- New `@cipherstash/prisma-next/v3` entry point: `cipherstashFromStackV3({ contractJson })` builds the v3 runtime descriptor, bulk-encrypt middleware, and a stack `EncryptionV3` client from the emitted contract.
16+
- Query operators use an **EQL-derived vocabulary** (`eqlEq`, `eqlNeq`, `eqlIn`, `eqlNotIn`, `eqlGt`, `eqlGte`, `eqlLt`, `eqlLte`, `eqlBetween`, `eqlNotBetween`, `eqlJsonContains`; ordering via `eqlAsc` / `eqlDesc`), lowering to the same-named `eql_v3.*` functions with operands cast to the domain's query type (`$n::eql_v3.query_<domain>`); ordering uses `eql_v3.ord_term` / `eql_v3.ord_term_ore` by the column's ordering flavour. The domains are `public.eql_v3_*`; the operator functions live in the `eql_v3` schema. (The v2 surface keeps its `cipherstash*` names.)
17+
- Free-text search is **`eqlMatch`** — fuzzy bloom token matching (`eql_v3.contains`), deliberately NOT named after SQL `ILIKE`: matching is case-insensitive, order/multiplicity-insensitive, and one-sided (may false-positive). Two guards run before encryption: SQL wildcards are normalised (leading/trailing `%` stripped; interior `%` or any `_` rejected), and needles the column's match index cannot answer (empty / below the tokenizer length) are rejected via the shared `matchNeedleError` guard. There is **no negated match operator** — negating a may-false-positive bloom test would silently drop matching rows.
18+
- A new baseline migration `20260601T0100_install_eql_v3_bundle` (invariant `cipherstash:install-eql-v3-bundle-v1`) installs the `public.eql_v3_*` domains and `eql_v3.*` functions from the pinned `@cipherstash/eql` release. Regenerate contracts and run migrations after changing constructors.
19+
- **The v3 ORM surface is fully wired end-to-end** (proven by converting `examples/prisma`):
20+
- The generated `contract.d.ts` type surface covers every v3 codec id: `CodecTypes` gains all 40 `cipherstash/eql-v3/*@1` entries (envelope outputs — `number`-castAs domains decode to the new `EncryptedNumber` — plus trait-accurate operator visibility), and `QueryOperationTypes` gains the `eql*` operator set, surfaced on v3 columns via type-level `cipherstash:v3-*` marker traits (the v2 `cipherstash*` methods never appear on v3 columns, and vice-versa). Storage-only domains (including `EncryptedBoolean`) surface no operator methods at the type level, matching the runtime gate.
21+
- The v3 runtime descriptor now presents the **pack id** (`cipherstash`) with v3's own version, so `postgres<Contract>({ extensions })` accepts contracts emitted by the cipherstash extension pack instead of failing with `RUNTIME.MISSING_EXTENSION_PACK`.
22+
- Every v3 codec id registers a control-plane `expandNativeType` hook that strips the `public.` qualifier — `prisma-next migration plan` now renders `CREATE TABLE` columns as bare domain names (`eql_v3_bigint_ord`), matching what introspection reports, with **no `add_search_config` ops** (v3 domains carry their own index metadata). No `onFieldEvent` is registered for v3.
23+
- The v3 bundle baseline migration op is reclassified `data` (it is a contract-shape-neutral self-edge; the aggregate integrity checker rejects self-edges without a data-class op), unblocking `prisma-next migration plan` / `migrate` in consuming apps.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cipherstash/prisma-next-example': minor
3+
---
4+
5+
Convert the example app to EQL v3. Every column is now a concrete `public.eql_v3_*` domain authored with the per-domain constructors (`EncryptedTextSearch`, `EncryptedDoubleOrd`, `EncryptedBigIntOrd`, `EncryptedDateOrd`, `EncryptedBoolean`, `EncryptedJson`), wired through `cipherstashFromStackV3({ contractJson })`. The e2e harness runs the full v3 surface against live Postgres + ZeroKMS with no skips: the `eql*` operator vocabulary (equality/range plus `eqlMatch` free-text token search), `eqlAsc`/`eqlDesc` order-term sorting, encrypted JSON containment (`eqlJsonContains` — the v2 `cipherstashJsonb*` helpers do not exist in v3), lossless `bigint` beyond `Number.MAX_SAFE_INTEGER`, and the storage-only `eql_v3_boolean` refusal (`EncryptionOperatorError`) pinned as a feature. Migrations regenerate from the v3 contract: the initial app migration creates the `users` table against the v3 domains with zero `add_search_config` ops, and the cipherstash space carries both bundle baselines (v2 + v3).
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
name: Integration — prisma-next (EQL v3)
2+
3+
# Real ZeroKMS ciphertext against a real Postgres, on BOTH database variants —
4+
# the prisma-next leg of the shared test-kit family driver (the same catalog,
5+
# oracle, and single-vs-bulk crossover as the Drizzle and Supabase jobs).
6+
#
7+
# The prisma-next adapter talks straight to the database, so it does not need
8+
# PostgREST — but it does need to work on managed Postgres, where the `postgres`
9+
# role is not a superuser, the EQL install takes its self-skipping path, and the
10+
# ORE domains cannot hold data. The Supabase compose file brings up PostgREST
11+
# too; this job simply ignores it.
12+
#
13+
# Separate from `tests.yml` on purpose: these suites need CipherStash credentials
14+
# and a database, and they THROW rather than skip when unconfigured. Keeping them
15+
# out of the unit job is what lets `pnpm test` stay runnable with neither.
16+
17+
on:
18+
push:
19+
branches: [main]
20+
paths:
21+
- 'packages/stack/src/eql/v3/**'
22+
- 'packages/prisma-next/**'
23+
# Source layers the adapter's encoding/round-trip rests on: a break here
24+
# (not just under src/eql/v3) can produce wrong rows, so trigger the live
25+
# suite that would catch it.
26+
- 'packages/stack/src/encryption/**'
27+
- 'packages/stack/src/schema/**'
28+
- 'packages/schema/**'
29+
- 'packages/test-kit/**'
30+
- 'packages/cli/src/installer/**'
31+
- 'local/docker-compose.postgres.yml'
32+
- 'local/docker-compose.supabase.yml'
33+
- 'local/supabase-init.sql'
34+
- '.github/workflows/integration-prisma-next.yml'
35+
- '.github/actions/integration-setup/**'
36+
pull_request:
37+
branches: ['**']
38+
# Repeated verbatim: GitHub Actions does not support YAML anchors/aliases.
39+
paths:
40+
- 'packages/stack/src/eql/v3/**'
41+
- 'packages/prisma-next/**'
42+
# Source layers the adapter's encoding/round-trip rests on: a break here
43+
# (not just under src/eql/v3) can produce wrong rows, so trigger the live
44+
# suite that would catch it.
45+
- 'packages/stack/src/encryption/**'
46+
- 'packages/stack/src/schema/**'
47+
- 'packages/schema/**'
48+
- 'packages/test-kit/**'
49+
- 'packages/cli/src/installer/**'
50+
- 'local/docker-compose.postgres.yml'
51+
- 'local/docker-compose.supabase.yml'
52+
- 'local/supabase-init.sql'
53+
- '.github/workflows/integration-prisma-next.yml'
54+
- '.github/actions/integration-setup/**'
55+
56+
jobs:
57+
integration:
58+
name: prisma-next v3 integration (db=${{ matrix.db }})
59+
runs-on: blacksmith-4vcpu-ubuntu-2404
60+
# Serialize every job that brings up the SAME docker-compose stack — same
61+
# rationale and keys as the Drizzle/Supabase workflows, so the jobs queue
62+
# on a shared runner rather than collide on the fixed host ports.
63+
concurrency:
64+
group: integration-live-db-${{ matrix.db }}
65+
cancel-in-progress: false
66+
# Fork PRs have no secrets. Skip cleanly rather than fail on something the
67+
# contributor cannot fix — `tests.yml` still gives them a green signal.
68+
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
69+
70+
strategy:
71+
fail-fast: false
72+
# prisma-next talks straight to Postgres, so it runs against BOTH
73+
# databases. The Supabase variant is not a formality: its `postgres` role
74+
# is not a superuser, so the EQL install takes its self-skipping path. A
75+
# suite that passes on a superuser database can still fail there.
76+
matrix:
77+
include:
78+
- db: postgres
79+
database-url: postgres://cipherstash:password@localhost:55432/cipherstash
80+
pgrest-url: ''
81+
- db: supabase
82+
database-url: postgres://postgres:password@localhost:55433/postgres
83+
pgrest-url: http://localhost:55430
84+
85+
env:
86+
CS_WORKSPACE_CRN: ${{ vars.CS_WORKSPACE_CRN }}
87+
CS_CLIENT_ID: ${{ vars.CS_CLIENT_ID }}
88+
CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }}
89+
CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }}
90+
# Job-level env, not a `.env` file: `dotenv/config` does not override an
91+
# already-set `process.env`, so these win and no secret is written to disk.
92+
DATABASE_URL: ${{ matrix.database-url }}
93+
PGRST_URL: ${{ matrix.pgrest-url }}
94+
# EXPLICIT, never inferred — same rationale as the Drizzle workflow.
95+
CS_IT_DB_VARIANT: ${{ matrix.db }}
96+
97+
steps:
98+
- uses: actions/checkout@v6
99+
- uses: ./.github/actions/integration-setup
100+
101+
# Fast pre-flight: fail in seconds if a secret was rotated or cleared,
102+
# before the docker pull. The in-test `requireIntegrationEnv` is the
103+
# correctness guarantee; this is the cheap one.
104+
- name: Require CipherStash secrets
105+
uses: ./.github/actions/require-cs-secrets
106+
with:
107+
workspace-crn: ${{ vars.CS_WORKSPACE_CRN }}
108+
client-id: ${{ vars.CS_CLIENT_ID }}
109+
client-key: ${{ secrets.CS_CLIENT_KEY }}
110+
client-access-key: ${{ secrets.CS_CLIENT_ACCESS_KEY }}
111+
112+
# Belt-and-braces for the concurrency guard: a run that was hard-killed
113+
# can skip its `down` and leak a container holding the host port onto a
114+
# REUSED runner. `|| true` so a clean runner is not an error.
115+
- name: Clear any leaked containers from a prior run
116+
run: docker compose -f local/docker-compose.${{ matrix.db }}.yml down -v --remove-orphans || true
117+
118+
- name: Start ${{ matrix.db }}
119+
run: docker compose -f local/docker-compose.${{ matrix.db }}.yml up -d --wait
120+
121+
# `globalSetup` installs EQL v3 by shelling out to the real
122+
# `stash eql install --eql-version 3`, so an installer regression fails
123+
# here rather than hiding behind a test-only SQL apply.
124+
- name: prisma-next v3 family suites
125+
run: pnpm --filter @cipherstash/prisma-next run test:integration
126+
127+
- name: Stop ${{ matrix.db }}
128+
if: always()
129+
run: docker compose -f local/docker-compose.${{ matrix.db }}.yml down -v

‎.github/workflows/tests.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ jobs:
127127
- name: Type tests (stack-supabase)
128128
run: pnpm --filter @cipherstash/stack-supabase run test:types
129129

130+
# prisma-next's operator-capability gating is proven by `.test-d.ts`
131+
# `@ts-expect-error` assertions (an unsupported operator on a column
132+
# must be a compile error). Those only fire when tsc processes them,
133+
# so the package `typecheck` must run — and be required — here (#684).
134+
# Its tsconfig resolves stack subpaths to SOURCE, so no build step is
135+
# needed first.
136+
- name: Typecheck (prisma-next — enforces v3 operator-capability gating)
137+
run: pnpm --filter @cipherstash/prisma-next run typecheck
138+
130139
- name: Lint — no hardcoded package-manager runners
131140
run: pnpm run lint:runners
132141

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ If these variables are missing, tests that require live encryption will fail or
8181
- `packages/cli`: The `stash` CLI — auth, init, encryption schema, and database setup (`stash eql install`). Has its own `AGENTS.md`.
8282
- `packages/wizard`: AI-powered encryption setup (`@cipherstash/wizard`)
8383
- `packages/migrate`: Plaintext-to-encrypted column migration (`@cipherstash/migrate`) — resumable backfill, per-column state
84-
- `packages/prisma-next`: Prisma Next integration (`@cipherstash/prisma-next`) — searchable field-level encryption for Postgres
84+
- `packages/prisma-next`: Prisma Next integration (`@cipherstash/prisma-next`) — searchable field-level encryption for Postgres. EQL v2 (`*V2` constructors, `cipherstashFromStack`) and EQL v3 (per-domain constructors, `./v3` entry with `cipherstashFromStackV3`)
8585
- `packages/stack-drizzle`: Drizzle ORM integration (`@cipherstash/stack-drizzle`), depends on `@cipherstash/stack` — EQL v2 (`.`) and EQL v3 (`./v3`). Split out of `@cipherstash/stack`.
8686
- `packages/stack-supabase`: Supabase integration (`@cipherstash/stack-supabase`), depends on `@cipherstash/stack` — `encryptedSupabase` (v2) and `encryptedSupabaseV3` (v3). Split out of `@cipherstash/stack`.
8787
- `packages/schema`: Schema builder utilities and types (`encryptedTable`, `encryptedColumn`, `encryptedField`)

0 commit comments

Comments
 (0)