Repository navigation
Commit a75513b
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
- .changeset
- .github/workflows
- examples/prisma
- migrations
- app
- 20260709T1034_initial
- 20260714T2142_initial
- cipherstash
- 20260601T0100_install_eql_v3_bundle
- refs
- prisma
- src
- prisma
- test/e2e
- packages
- prisma-next
- integration
- migrations
- 20260601T0100_install_eql_v3_bundle
- refs
- src
- exports
- extension-metadata
- middleware
- migration
- stack
- types
- v3
- test
- live
- helpers
- v3
- test-kit/src
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
127 | 127 | | |
128 | 128 | | |
129 | 129 | | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
130 | 139 | | |
131 | 140 | | |
132 | 141 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
84 | | - | |
| 84 | + | |
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| |||
0 commit comments