From bd58f13975fdbf702606e5f9ef6c20bb17419958 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 00:42:38 +1000 Subject: [PATCH 01/17] feat(stack): add @cipherstash/stack/adapter-kit internal support surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Foundation for splitting the Supabase and Drizzle integrations into their own packages (#627). When the adapters leave this package they can no longer reach core internals via `@/` imports across the package boundary, so the exact symbols they consume — logger, AuditConfig, the v3 column model + DATE_LIKE_CASTS, the domain registry, matchNeedleError, and the model→composite helpers — are re-exported here from one narrow, versioned entry point rather than leaking six internal module paths. Not a general-purpose public API; the seam between core and first-party adapters. Verified: builds, and all re-exported values resolve from the built entry. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- packages/stack/package.json | 13 ++++++++ packages/stack/src/adapter-kit.ts | 50 +++++++++++++++++++++++++++++++ packages/stack/tsup.config.ts | 1 + 3 files changed, 64 insertions(+) create mode 100644 packages/stack/src/adapter-kit.ts diff --git a/packages/stack/package.json b/packages/stack/package.json index 856ea5513..1fc57c87c 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -39,6 +39,9 @@ "errors": [ "./dist/errors/index.d.ts" ], + "adapter-kit": [ + "./dist/adapter-kit.d.ts" + ], "client": [ "./dist/client.d.ts" ], @@ -208,6 +211,16 @@ "default": "./dist/errors/index.cjs" } }, + "./adapter-kit": { + "import": { + "types": "./dist/adapter-kit.d.ts", + "default": "./dist/adapter-kit.js" + }, + "require": { + "types": "./dist/adapter-kit.d.cts", + "default": "./dist/adapter-kit.cjs" + } + }, "./wasm-inline": { "import": { "types": "./dist/wasm-inline.d.ts", diff --git a/packages/stack/src/adapter-kit.ts b/packages/stack/src/adapter-kit.ts new file mode 100644 index 000000000..4d8d20027 --- /dev/null +++ b/packages/stack/src/adapter-kit.ts @@ -0,0 +1,50 @@ +/** + * Internal support surface for FIRST-PARTY adapter packages + * (`@cipherstash/stack-supabase`, `@cipherstash/stack-drizzle`). + * + * These symbols were previously reached through deep `@/` internal imports while + * the Supabase and Drizzle adapters lived INSIDE this package. Splitting them into + * their own packages (see issue #627) means those imports must resolve across a + * package boundary — so exactly the symbols the adapters consume are re-exported + * here, from one narrow, versioned entry point, rather than leaking six internal + * module paths into the public surface. + * + * NOT a general-purpose public API. This is the seam between core and the + * first-party adapters; treat it as `@internal`-adjacent. Anything an END USER + * should reach has a dedicated subpath (`./schema`, `./eql/v3`, `./types`, + * `./identity`, `./errors`). If a symbol here graduates to genuine public use, + * promote it to one of those instead of widening this file's contract. + * + * The full member list is small on purpose; every addition is an audit decision. + */ + +// Shared structured logger (adapters log rejections through the same instance). +export { logger } from './utils/logger/index.js' + +// Audit config carried by chainable operations. +export type { AuditConfig } from './encryption/operations/base-operation.js' + +// v3 column model + the date-like cast set the Supabase builder uses to +// reconstruct `Date` values from PostgREST select aliases. +export { + type AnyEncryptedV3Column, + DATE_LIKE_CASTS, + EncryptedV3Column, +} from './eql/v3/columns.js' + +// Domain registry: the Supabase adapter classifies introspected columns by their +// Postgres domain and rebuilds each column's encryption config from it. +export { + DOMAIN_REGISTRY, + factoryForDomain, + stripDomainSchema, +} from './eql/v3/domain-registry.js' + +// Shared match-index guard (short-needle rejection), reused by both adapters. +export { matchNeedleError } from './schema/match-defaults.js' + +// Model → encrypted-PG-composite helpers used by the v2 Supabase mutation path. +export { + bulkModelsToEncryptedPgComposites, + modelToEncryptedPgComposites, +} from './encryption/helpers/index.js' diff --git a/packages/stack/tsup.config.ts b/packages/stack/tsup.config.ts index 64a6152da..e542cd5cd 100644 --- a/packages/stack/tsup.config.ts +++ b/packages/stack/tsup.config.ts @@ -22,6 +22,7 @@ export default defineConfig([ 'src/encryption/index.ts', 'src/encryption/v3.ts', 'src/errors/index.ts', + 'src/adapter-kit.ts', ], format: ['cjs', 'esm'], sourcemap: true, From 5411a13abdb574e37fce27732a1a9cdb3dbe432d Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 00:45:21 +1000 Subject: [PATCH 02/17] docs(plan): adapter package split execution plan + adapter-kit changeset (#627) Captures the verified import surface, the expose-vs-relocate decision (defaulted to the single adapter-kit subpath), the file inventory, phased move steps, and the verification checklist. The mechanical move is staged for an attended session. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .changeset/stack-adapter-kit.md | 12 ++ .../plans/2026-07-13-adapter-package-split.md | 115 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 .changeset/stack-adapter-kit.md create mode 100644 docs/plans/2026-07-13-adapter-package-split.md diff --git a/.changeset/stack-adapter-kit.md b/.changeset/stack-adapter-kit.md new file mode 100644 index 000000000..43bc5ccd0 --- /dev/null +++ b/.changeset/stack-adapter-kit.md @@ -0,0 +1,12 @@ +--- +'@cipherstash/stack': minor +--- + +Add the `@cipherstash/stack/adapter-kit` subpath — a narrow support surface for +the first-party adapter packages (`@cipherstash/stack-supabase`, +`@cipherstash/stack-drizzle`) being split out of this package (#627). It +re-exports exactly the core internals the adapters consume (the logger, +`AuditConfig`, the v3 column model + `DATE_LIKE_CASTS`, the domain registry, the +match-index guard, and the model→composite helpers) so those imports resolve +across the package boundary without leaking six internal module paths. This is the +core↔adapter seam, not general-purpose public API. diff --git a/docs/plans/2026-07-13-adapter-package-split.md b/docs/plans/2026-07-13-adapter-package-split.md new file mode 100644 index 000000000..f3cd9701d --- /dev/null +++ b/docs/plans/2026-07-13-adapter-package-split.md @@ -0,0 +1,115 @@ +# Adapter package split — execution plan (#627) + +Extract the Supabase and Drizzle integrations out of `@cipherstash/stack` into +their own packages (`@cipherstash/stack-supabase`, `@cipherstash/stack-drizzle`), +each depending on stack via `workspace:*`, following the `@cipherstash/prisma-next` +precedent. Tracked in #627; PR1's integration harness (#616) is merged, which is +the safety net this refactor wanted behind it. + +## Status + +- **DONE (committed on this branch):** the enabling support surface + `@cipherstash/stack/adapter-kit` — a single narrow entry re-exporting exactly the + core internals the adapters consume. Builds; all re-exported values resolve. +- **STAGED (this plan):** the mechanical move of the adapter code + tests into the + two new packages. Deliberately left for an **attended** session — it touches + ~35 files across a package boundary and each import rewrite / test move is a live + breakage point best caught interactively, not overnight. + +## The design decision (needs sign-off) + +The plan's own risk note said the "expose internal modules vs relocate them" call +is *"a design call to make before coding."* Made here, defaulted to **expose**, and +kept as small and reversible as possible: + +> Rather than leak six internal module paths (`@/utils/logger`, +> `@/encryption/helpers`, `@/encryption/operations/base-operation`, +> `@/eql/v3/columns`, `@/eql/v3/domain-registry`, `@/schema/match-defaults`) into +> the public surface, **one** new subpath — `@cipherstash/stack/adapter-kit` — +> re-exports exactly the symbols the adapters use. It is documented as the +> core↔first-party-adapter seam, not general-purpose public API. + +The internal `@/types` symbols the adapters need (`BulkEncryptedData`, +`ClientConfig`, `Encrypted`, `QueryTypeName`, `queryTypes`) are **already** in the +public `./types` — so they need no new surface; the move repoints those imports to +`@cipherstash/stack/types`. `AnyEncryptedV3Column` is already in `./eql/v3`. + +**If you'd rather relocate** (e.g. move `logger` to `@cipherstash/utils`, the v3 +column/registry pieces to `@cipherstash/schema`) than expose `adapter-kit`, say so +and the move repoints accordingly — the mechanical steps below are unchanged. + +## Verified import surface (what the adapters pull from core) + +| Import (was `@/…`) | Move to | Symbols | +|---|---|---| +| `@/types` | `@cipherstash/stack/types` | `BulkEncryptedData`, `ClientConfig`, `Encrypted`, `QueryTypeName`, `queryTypes` | +| `@/errors` | `@cipherstash/stack/errors` | (errors) | +| `@/identity` | `@cipherstash/stack/identity` | `LockContext` | +| `@/schema` | `@cipherstash/stack/schema` | `EncryptedColumn`, `EncryptedTable`, … | +| `@/eql/v3` | `@cipherstash/stack/eql/v3` | v3 table/column API, `AnyEncryptedV3Column` | +| `@/encryption`, `@/encryption/index.js` | `@cipherstash/stack` | `Encryption`, `EncryptionClient` | +| `@/utils/logger` | `@cipherstash/stack/adapter-kit` | `logger` | +| `@/encryption/helpers` | `@cipherstash/stack/adapter-kit` | `bulkModelsToEncryptedPgComposites`, `modelToEncryptedPgComposites` | +| `@/encryption/operations/base-operation` | `@cipherstash/stack/adapter-kit` | `AuditConfig` | +| `@/eql/v3/columns` | `@cipherstash/stack/adapter-kit` | `EncryptedV3Column`, `DATE_LIKE_CASTS` (+ `AnyEncryptedV3Column` via `./eql/v3`) | +| `@/eql/v3/domain-registry` | `@cipherstash/stack/adapter-kit` | `DOMAIN_REGISTRY`, `factoryForDomain`, `stripDomainSchema` | +| `@/schema/match-defaults` | `@cipherstash/stack/adapter-kit` | `matchNeedleError` | + +The adapters are **leaf modules** — nothing in `packages/stack/src` outside the +adapter dirs imports them — so removing them does not break core. + +## File inventory to move + +- **stack-supabase:** `packages/stack/src/supabase/*` (8 files: `helpers`, + `index`, `introspect`, `query-builder`, `query-builder-v3`, `schema-builder`, + `types`, `verify`). Unit tests: the `supabase*` files under + `packages/stack/__tests__/` (~13). Integration: `packages/stack/integration/supabase/`. +- **stack-drizzle:** `packages/stack/src/drizzle/*` (v2) **and** + `packages/stack/src/eql/v3/drizzle/*`. Unit tests: `drizzle*` under + `packages/stack/__tests__/` + `__tests__/drizzle-v3/`. Integration: + `packages/stack/integration/drizzle-v3/`. + +## Phased steps (per package; do Supabase first as the proven slice) + +1. Scaffold the package (`package.json` with `@cipherstash/stack: workspace:*` + + `@cipherstash/protect-ffi` at stack's pinned version, `pg`/`@types/pg` if + introspection needs them; `tsconfig.json`, `tsup.config.ts`, `vitest.config.ts` + — copy prisma-next's shape). Export `.` (the adapter entry). +2. `git mv` the src dir(s) into the package. +3. Rewrite imports per the table above (`@/…` → `@cipherstash/stack[/subpath]`). + Relative imports within the moved tree stay as-is. +4. Remove the adapter's subpath from stack: `exports`, `typesVersions`, + `tsup.config.ts` entry. (`./supabase`, `./drizzle`, `./eql/v3/drizzle`.) +5. Build stack, then the new package (src only). Get green before touching tests. +6. Move the unit + integration test files; rewrite their imports (watch for tests + that reach stack `__tests__/helpers/**` — those helpers may need to move to + `@cipherstash/test-kit` or be duplicated). Wire the package's vitest config + + integration config against `@cipherstash/test-kit`. +7. Update the `fta-v3.yml` complexity gate: its `analyze:complexity` scans + `packages/stack/src/eql/v3`; update the path filter and add an equivalent gate + in stack-drizzle so the moved code keeps its budget. +8. Update doc/skill references (~13 for `@cipherstash/stack/drizzle`, plus the + Supabase reference docs and `skills/stash-supabase` / `skills/stash-drizzle`) — + these ship in the `stash` tarball. +9. Changesets: breaking (removed subpaths from stack) + new-package changesets. + +## Verification checklist + +- `pnpm --filter @cipherstash/stack build` + `test:types` green (core still builds + with the subpaths removed). +- `pnpm --filter @cipherstash/stack-supabase build && … test` green; same for + stack-drizzle. +- `pnpm -w build` green (no dependency cycle: stack must not import the adapters). +- `pnpm install --frozen-lockfile` clean. +- The two integration workflows point at the moved suites. +- `stash manifest --json` unaffected (CLI surface unchanged by this refactor). + +## Open questions for review + +1. Approve the `adapter-kit` expose approach, or prefer relocation for specific + symbols (esp. `logger` → `@cipherstash/utils`)? +2. Package names: `@cipherstash/stack-supabase` / `@cipherstash/stack-drizzle` + (matches the plan) — confirm. +3. Version/`publishConfig`: start both at `0.1.0`, `access: public`? +4. Test-helper coupling: move shared `__tests__/helpers/**` into + `@cipherstash/test-kit`, or duplicate the few the adapters need? From c10e7a88633180b139eff5f4a0d2d09e2d9d9f26 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 10:30:05 +1000 Subject: [PATCH 03/17] fix(lint): bump biome.json $schema to 2.5.2; sort adapter-kit exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The main-merge floated @biomejs/biome to ^2.5.2 but biome.json's $schema still pinned 2.4.15, so biome rejected the config (deserialize error) and fell back to defaults — failing `code:check` across the branch. Match the schema to the CLI. Also sort the adapter-kit re-exports (biome 2.5.2 organizeImports). Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- biome.json | 2 +- packages/stack/src/adapter-kit.ts | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/biome.json b/biome.json index 17d9c3f47..11e6da5bd 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json", + "$schema": "https://biomejs.dev/schemas/2.5.2/schema.json", "files": { "includes": [ "**", diff --git a/packages/stack/src/adapter-kit.ts b/packages/stack/src/adapter-kit.ts index 4d8d20027..8dcf65041 100644 --- a/packages/stack/src/adapter-kit.ts +++ b/packages/stack/src/adapter-kit.ts @@ -18,8 +18,11 @@ * The full member list is small on purpose; every addition is an audit decision. */ -// Shared structured logger (adapters log rejections through the same instance). -export { logger } from './utils/logger/index.js' +// Model → encrypted-PG-composite helpers used by the v2 Supabase mutation path. +export { + bulkModelsToEncryptedPgComposites, + modelToEncryptedPgComposites, +} from './encryption/helpers/index.js' // Audit config carried by chainable operations. export type { AuditConfig } from './encryption/operations/base-operation.js' @@ -42,9 +45,5 @@ export { // Shared match-index guard (short-needle rejection), reused by both adapters. export { matchNeedleError } from './schema/match-defaults.js' - -// Model → encrypted-PG-composite helpers used by the v2 Supabase mutation path. -export { - bulkModelsToEncryptedPgComposites, - modelToEncryptedPgComposites, -} from './encryption/helpers/index.js' +// Shared structured logger (adapters log rejections through the same instance). +export { logger } from './utils/logger/index.js' From ed91460673ae2d0747d2bb6d34908a0a32460235 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 10:37:40 +1000 Subject: [PATCH 04/17] refactor(supabase): extract src into @cipherstash/stack-supabase (#627) Moves packages/stack/src/supabase into the new package; rewrites its `@/` imports to `@cipherstash/stack` public subpaths + `@cipherstash/stack/adapter-kit`. Drops `./supabase` from stack's exports/typesVersions/tsup entry and tsconfig path; repoints the shared vitest alias. Updates examples/basic and removes the stale test-kit tsconfig path. Tests move in the next commit. Both packages build (JS + DTS). Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- examples/basic/package.json | 1 + examples/basic/src/lib/supabase/encrypted.ts | 2 +- packages/stack-supabase/package.json | 88 +++++ .../src}/helpers.ts | 4 +- .../supabase => stack-supabase/src}/index.ts | 6 +- .../src}/introspect.ts | 2 +- .../src}/query-builder-v3.ts | 12 +- .../src}/query-builder.ts | 16 +- .../src}/schema-builder.ts | 6 +- .../supabase => stack-supabase/src}/types.ts | 22 +- .../supabase => stack-supabase/src}/verify.ts | 2 +- packages/stack-supabase/tsconfig.json | 35 ++ packages/stack-supabase/tsup.config.ts | 21 ++ packages/stack-supabase/vitest.config.ts | 30 ++ packages/stack/package.json | 13 - packages/stack/tsconfig.json | 3 +- packages/stack/tsup.config.ts | 1 - packages/test-kit/tsconfig.json | 1 - pnpm-lock.yaml | 310 +++--------------- vitest.shared.ts | 15 +- 20 files changed, 273 insertions(+), 317 deletions(-) create mode 100644 packages/stack-supabase/package.json rename packages/{stack/src/supabase => stack-supabase/src}/helpers.ts (99%) rename packages/{stack/src/supabase => stack-supabase/src}/index.ts (98%) rename packages/{stack/src/supabase => stack-supabase/src}/introspect.ts (99%) rename packages/{stack/src/supabase => stack-supabase/src}/query-builder-v3.ts (98%) rename packages/{stack/src/supabase => stack-supabase/src}/query-builder.ts (99%) rename packages/{stack/src/supabase => stack-supabase/src}/schema-builder.ts (94%) rename packages/{stack/src/supabase => stack-supabase/src}/types.ts (97%) rename packages/{stack/src/supabase => stack-supabase/src}/verify.ts (97%) create mode 100644 packages/stack-supabase/tsconfig.json create mode 100644 packages/stack-supabase/tsup.config.ts create mode 100644 packages/stack-supabase/vitest.config.ts diff --git a/examples/basic/package.json b/examples/basic/package.json index 87c6c8fd9..45b60a977 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -12,6 +12,7 @@ "description": "", "dependencies": { "@cipherstash/stack": "workspace:*", + "@cipherstash/stack-supabase": "workspace:*", "dotenv": "^17.4.2", "pg": "8.20.0" }, diff --git a/examples/basic/src/lib/supabase/encrypted.ts b/examples/basic/src/lib/supabase/encrypted.ts index 816b0cd65..147930987 100644 --- a/examples/basic/src/lib/supabase/encrypted.ts +++ b/examples/basic/src/lib/supabase/encrypted.ts @@ -1,4 +1,4 @@ -import { encryptedSupabase } from '@cipherstash/stack/supabase' +import { encryptedSupabase } from '@cipherstash/stack-supabase' import { contactsTable, encryptionClient } from '../../encryption/index' import { createServerClient } from './server' diff --git a/packages/stack-supabase/package.json b/packages/stack-supabase/package.json new file mode 100644 index 000000000..4dbf81279 --- /dev/null +++ b/packages/stack-supabase/package.json @@ -0,0 +1,88 @@ +{ + "name": "@cipherstash/stack-supabase", + "version": "0.1.0", + "description": "CipherStash Stack Supabase integration: transparent field-level encryption for Supabase with EQL v3 (encryptedSupabaseV3, connect-time introspection) and v2 (encryptedSupabase).", + "keywords": [ + "encrypted", + "supabase", + "postgres", + "eql", + "cipherstash", + "stack" + ], + "bugs": { + "url": "https://github.com/cipherstash/stack/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/cipherstash/stack.git", + "directory": "packages/stack-supabase" + }, + "license": "MIT", + "author": "CipherStash ", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "dist", + "README.md" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "scripts": { + "prebuild": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run", + "test:types": "vitest --run --typecheck.only", + "test:integration": "vitest run --config integration/vitest.config.ts" + }, + "dependencies": { + "@cipherstash/stack": "workspace:*" + }, + "peerDependencies": { + "@cipherstash/protect-ffi": "0.29.0", + "@supabase/supabase-js": ">=2", + "pg": ">=8" + }, + "peerDependenciesMeta": { + "@supabase/supabase-js": { + "optional": true + }, + "pg": { + "optional": true + } + }, + "devDependencies": { + "@cipherstash/protect-ffi": "0.29.0", + "@cipherstash/test-kit": "workspace:*", + "@supabase/postgrest-js": "2.105.4", + "@supabase/supabase-js": "^2.110.0", + "@types/pg": "^8.20.0", + "dotenv": "17.4.2", + "pg": "8.20.0", + "postgres": "^3.4.8", + "tsup": "catalog:repo", + "typescript": "catalog:repo", + "vitest": "catalog:repo" + }, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=22" + } +} diff --git a/packages/stack/src/supabase/helpers.ts b/packages/stack-supabase/src/helpers.ts similarity index 99% rename from packages/stack/src/supabase/helpers.ts rename to packages/stack-supabase/src/helpers.ts index b3856bd60..014c37fb1 100644 --- a/packages/stack/src/supabase/helpers.ts +++ b/packages/stack-supabase/src/helpers.ts @@ -1,5 +1,5 @@ -import type { EncryptedTable, EncryptedTableColumn } from '@/schema' -import type { QueryTypeName } from '@/types' +import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/stack/schema' +import type { QueryTypeName } from '@cipherstash/stack/types' import type { DbFilterString, DbPendingOrCondition, diff --git a/packages/stack/src/supabase/index.ts b/packages/stack-supabase/src/index.ts similarity index 98% rename from packages/stack/src/supabase/index.ts rename to packages/stack-supabase/src/index.ts index 8e602fd28..4255a2a22 100644 --- a/packages/stack/src/supabase/index.ts +++ b/packages/stack-supabase/src/index.ts @@ -1,5 +1,5 @@ -import { Encryption } from '@/encryption' -import type { EncryptedTable, EncryptedTableColumn } from '@/schema' +import { Encryption } from '@cipherstash/stack' +import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/stack/schema' import type { UnmodelledColumn } from './introspect' import { introspect } from './introspect' import { EncryptedQueryBuilderImpl } from './query-builder' @@ -29,7 +29,7 @@ import { verifyDeclaredSchemas } from './verify' * @example * ```typescript * import { Encryption } from '@cipherstash/stack' - * import { encryptedSupabase } from '@cipherstash/stack/supabase' + * import { encryptedSupabase } from '@cipherstash/stack-supabase' * import { encryptedTable, encryptedColumn } from '@cipherstash/stack/schema' * * const users = encryptedTable('users', { diff --git a/packages/stack/src/supabase/introspect.ts b/packages/stack-supabase/src/introspect.ts similarity index 99% rename from packages/stack/src/supabase/introspect.ts rename to packages/stack-supabase/src/introspect.ts index 4b58ea919..9c126d6ab 100644 --- a/packages/stack/src/supabase/introspect.ts +++ b/packages/stack-supabase/src/introspect.ts @@ -1,4 +1,4 @@ -import { DOMAIN_REGISTRY } from '@/eql/v3/domain-registry' +import { DOMAIN_REGISTRY } from '@cipherstash/stack/adapter-kit' /** One introspected column: its DB name and its `public` EQL v3 domain (or `null`). */ export interface IntrospectedColumn { diff --git a/packages/stack/src/supabase/query-builder-v3.ts b/packages/stack-supabase/src/query-builder-v3.ts similarity index 98% rename from packages/stack/src/supabase/query-builder-v3.ts rename to packages/stack-supabase/src/query-builder-v3.ts index 8b11cb8aa..c814a9ec2 100644 --- a/packages/stack/src/supabase/query-builder-v3.ts +++ b/packages/stack-supabase/src/query-builder-v3.ts @@ -1,19 +1,19 @@ -import type { EncryptionClient } from '@/encryption' -import type { AnyV3Table } from '@/eql/v3' -import { DATE_LIKE_CASTS, EncryptedV3Column } from '@/eql/v3/columns' +import type { EncryptionClient } from '@cipherstash/stack/encryption' +import type { AnyV3Table } from '@cipherstash/stack/eql/v3' +import { DATE_LIKE_CASTS, EncryptedV3Column } from '@cipherstash/stack/adapter-kit' import type { ColumnSchema, EncryptedTable, EncryptedTableColumn, -} from '@/schema' +} from '@cipherstash/stack/schema' import type { BuildableQueryColumn, Encrypted, EncryptedQueryResult, QueryTypeName, ScalarQueryTerm, -} from '@/types' -import { logger } from '@/utils/logger' +} from '@cipherstash/stack/types' +import { logger } from '@cipherstash/stack/adapter-kit' import { addJsonbCastsV3, selectKeyToDbV3 } from './helpers' import { EncryptedQueryBuilderImpl, diff --git a/packages/stack/src/supabase/query-builder.ts b/packages/stack-supabase/src/query-builder.ts similarity index 99% rename from packages/stack/src/supabase/query-builder.ts rename to packages/stack-supabase/src/query-builder.ts index 890cfc483..e58f3ece0 100644 --- a/packages/stack/src/supabase/query-builder.ts +++ b/packages/stack-supabase/src/query-builder.ts @@ -1,20 +1,20 @@ import type { JsPlaintext } from '@cipherstash/protect-ffi' -import type { EncryptionClient } from '@/encryption' +import type { EncryptionClient } from '@cipherstash/stack/encryption' import { bulkModelsToEncryptedPgComposites, modelToEncryptedPgComposites, -} from '@/encryption/helpers' -import type { AuditConfig } from '@/encryption/operations/base-operation' -import type { LockContext } from '@/identity' -import type { EncryptedTable, EncryptedTableColumn } from '@/schema' -import { EncryptedColumn } from '@/schema' +} from '@cipherstash/stack/adapter-kit' +import type { AuditConfig } from '@cipherstash/stack/adapter-kit' +import type { LockContext } from '@cipherstash/stack/identity' +import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/stack/schema' +import { EncryptedColumn } from '@cipherstash/stack/schema' import type { BuildableQueryColumn, EncryptedQueryResult, QueryTypeName, ScalarQueryTerm, -} from '@/types' -import { logger } from '@/utils/logger' +} from '@cipherstash/stack/types' +import { logger } from '@cipherstash/stack/adapter-kit' import { addJsonbCasts, formatContainmentOperand, diff --git a/packages/stack/src/supabase/schema-builder.ts b/packages/stack-supabase/src/schema-builder.ts similarity index 94% rename from packages/stack/src/supabase/schema-builder.ts rename to packages/stack-supabase/src/schema-builder.ts index ba5f22f9f..c2eb8466b 100644 --- a/packages/stack/src/supabase/schema-builder.ts +++ b/packages/stack-supabase/src/schema-builder.ts @@ -1,6 +1,6 @@ -import { type AnyV3Table, EncryptedTable } from '@/eql/v3' -import type { AnyEncryptedV3Column } from '@/eql/v3/columns' -import { factoryForDomain } from '@/eql/v3/domain-registry' +import { type AnyV3Table, EncryptedTable } from '@cipherstash/stack/eql/v3' +import type { AnyEncryptedV3Column } from '@cipherstash/stack/adapter-kit' +import { factoryForDomain } from '@cipherstash/stack/adapter-kit' import type { IntrospectionResult } from './introspect' /** A record of declared v3 tables, keyed by table name. */ diff --git a/packages/stack/src/supabase/types.ts b/packages/stack-supabase/src/types.ts similarity index 97% rename from packages/stack/src/supabase/types.ts rename to packages/stack-supabase/src/types.ts index 869eb9b47..d55f06cf9 100644 --- a/packages/stack/src/supabase/types.ts +++ b/packages/stack-supabase/src/types.ts @@ -1,15 +1,15 @@ -import type { EncryptionClient } from '@/encryption' -import type { AuditConfig } from '@/encryption/operations/base-operation' +import type { EncryptionClient } from '@cipherstash/stack/encryption' +import type { AuditConfig } from '@cipherstash/stack/adapter-kit' import type { AnyV3Table, EqlTypeForColumn, InferPlaintext, QueryTypesForColumn, -} from '@/eql/v3' -import type { EncryptionError } from '@/errors' -import type { LockContext } from '@/identity' -import type { EncryptedTable, EncryptedTableColumn } from '@/schema' -import type { ClientConfig } from '@/types' +} from '@cipherstash/stack/eql/v3' +import type { EncryptionError } from '@cipherstash/stack/errors' +import type { LockContext } from '@cipherstash/stack/identity' +import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/stack/schema' +import type { ClientConfig } from '@cipherstash/stack/types' import type { V3Schemas } from './schema-builder' // --------------------------------------------------------------------------- @@ -628,14 +628,14 @@ export interface SupabaseClientLike { // Re-export for convenience // --------------------------------------------------------------------------- -export type { EncryptionClient } from '@/encryption' -export type { AuditConfig } from '@/encryption/operations/base-operation' -export type { LockContext } from '@/identity' +export type { EncryptionClient } from '@cipherstash/stack/encryption' +export type { AuditConfig } from '@cipherstash/stack/adapter-kit' +export type { LockContext } from '@cipherstash/stack/identity' export type { EncryptedColumn, EncryptedTable, EncryptedTableColumn, -} from '@/schema' +} from '@cipherstash/stack/schema' // --------------------------------------------------------------------------- // Forward declaration for query builder (avoids circular) diff --git a/packages/stack/src/supabase/verify.ts b/packages/stack-supabase/src/verify.ts similarity index 97% rename from packages/stack/src/supabase/verify.ts rename to packages/stack-supabase/src/verify.ts index b20226fd8..b82371fa7 100644 --- a/packages/stack/src/supabase/verify.ts +++ b/packages/stack-supabase/src/verify.ts @@ -1,4 +1,4 @@ -import { stripDomainSchema } from '@/eql/v3/domain-registry' +import { stripDomainSchema } from '@cipherstash/stack/adapter-kit' import type { IntrospectionResult } from './introspect' import type { V3Schemas } from './schema-builder' diff --git a/packages/stack-supabase/tsconfig.json b/packages/stack-supabase/tsconfig.json new file mode 100644 index 000000000..79db12189 --- /dev/null +++ b/packages/stack-supabase/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "lib": ["ES2022", "DOM"], + "target": "ES2022", + "module": "ESNext", + "moduleDetection": "force", + "allowJs": true, + "esModuleInterop": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "customConditions": ["node"], + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "paths": { + "@cipherstash/stack": ["../stack/src/index.ts"], + "@cipherstash/stack/adapter-kit": ["../stack/src/adapter-kit.ts"], + "@cipherstash/stack/encryption": ["../stack/src/encryption/index.ts"], + "@cipherstash/stack/schema": ["../stack/src/schema/index.ts"], + "@cipherstash/stack/identity": ["../stack/src/identity/index.ts"], + "@cipherstash/stack/errors": ["../stack/src/errors/index.ts"], + "@cipherstash/stack/types": ["../stack/src/types-public.ts"], + "@cipherstash/stack/eql/v3": ["../stack/src/eql/v3/index.ts"], + "@cipherstash/test-kit": ["../test-kit/src/index.ts"], + "@cipherstash/test-kit/suite": ["../test-kit/src/run-family-suite.ts"], + "@cipherstash/test-kit/catalog": ["../test-kit/src/catalog.ts"] + } + }, + "include": ["src/**/*.ts", "__tests__/**/*.ts", "integration/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/stack-supabase/tsup.config.ts b/packages/stack-supabase/tsup.config.ts new file mode 100644 index 000000000..2cdd511e4 --- /dev/null +++ b/packages/stack-supabase/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: 'dist', + format: ['cjs', 'esm'], + sourcemap: true, + dts: true, + clean: false, + target: 'es2022', + tsconfig: './tsconfig.json', + // Core + the Postgres/Supabase clients stay external — they are the + // consumer's (peer) dependencies, not bundled into the adapter. + external: [ + '@cipherstash/stack', + '@cipherstash/protect-ffi', + '@supabase/supabase-js', + '@supabase/postgrest-js', + 'pg', + ], +}) diff --git a/packages/stack-supabase/vitest.config.ts b/packages/stack-supabase/vitest.config.ts new file mode 100644 index 000000000..abc8b09b4 --- /dev/null +++ b/packages/stack-supabase/vitest.config.ts @@ -0,0 +1,30 @@ +import { resolve } from 'node:path' +import { configDefaults, defineConfig } from 'vitest/config' +import { sharedAlias } from '../../vitest.shared' + +export default defineConfig({ + resolve: { + alias: { + // Resolve `@cipherstash/stack` + its subpaths (and `@cipherstash/test-kit`) + // to SOURCE, so `pnpm test` here is not coupled to a prior stack build. + ...sharedAlias, + // adapter-kit is the core↔adapter seam; not in the shared block because + // only the adapter packages consume it. + '@cipherstash/stack/adapter-kit': resolve( + __dirname, + '../stack/src/adapter-kit.ts', + ), + }, + }, + test: { + // Integration suites require credentials + a database + PostgREST and THROW + // when unconfigured, so `pnpm test` must never collect them. + exclude: [...configDefaults.exclude, 'integration/**'], + testTimeout: 30000, + hookTimeout: 30000, + typecheck: { + tsconfig: './tsconfig.json', + include: ['__tests__/**/*.test-d.ts'], + }, + }, +}) diff --git a/packages/stack/package.json b/packages/stack/package.json index 1fc57c87c..da40d220b 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -72,9 +72,6 @@ "dynamodb": [ "./dist/dynamodb/index.d.ts" ], - "supabase": [ - "./dist/supabase/index.d.ts" - ], "wasm-inline": [ "./dist/wasm-inline.d.ts" ] @@ -181,16 +178,6 @@ "default": "./dist/dynamodb/index.cjs" } }, - "./supabase": { - "import": { - "types": "./dist/supabase/index.d.ts", - "default": "./dist/supabase/index.js" - }, - "require": { - "types": "./dist/supabase/index.d.cts", - "default": "./dist/supabase/index.cjs" - } - }, "./encryption": { "import": { "types": "./dist/encryption/index.d.ts", diff --git a/packages/stack/tsconfig.json b/packages/stack/tsconfig.json index 122bb3e96..fe9857436 100644 --- a/packages/stack/tsconfig.json +++ b/packages/stack/tsconfig.json @@ -49,8 +49,7 @@ "@cipherstash/stack": ["./src/index.ts"], "@cipherstash/stack/eql/v3": ["./src/eql/v3/index.ts"], "@cipherstash/stack/schema": ["./src/schema/index.ts"], - "@cipherstash/stack/v3": ["./src/encryption/v3.ts"], - "@cipherstash/stack/supabase": ["./src/supabase/index.ts"] + "@cipherstash/stack/v3": ["./src/encryption/v3.ts"] } } } diff --git a/packages/stack/tsup.config.ts b/packages/stack/tsup.config.ts index e542cd5cd..67ef65716 100644 --- a/packages/stack/tsup.config.ts +++ b/packages/stack/tsup.config.ts @@ -18,7 +18,6 @@ export default defineConfig([ 'src/eql/v3/drizzle/index.ts', 'src/drizzle/index.ts', 'src/dynamodb/index.ts', - 'src/supabase/index.ts', 'src/encryption/index.ts', 'src/encryption/v3.ts', 'src/errors/index.ts', diff --git a/packages/test-kit/tsconfig.json b/packages/test-kit/tsconfig.json index 0524347d6..0bd5cf053 100644 --- a/packages/test-kit/tsconfig.json +++ b/packages/test-kit/tsconfig.json @@ -30,7 +30,6 @@ "@cipherstash/stack/eql/v3": ["../stack/src/eql/v3/index.ts"], "@cipherstash/stack/schema": ["../stack/src/schema/index.ts"], "@cipherstash/stack/v3": ["../stack/src/encryption/v3.ts"], - "@cipherstash/stack/supabase": ["../stack/src/supabase/index.ts"], // Resolving stack to source means we also inherit stack's OWN internal // alias: `src/eql/v3/columns.ts` imports `@/schema`. Without this the diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5ee35c1e..767ac4a82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: '@cipherstash/stack': specifier: workspace:* version: link:../../packages/stack + '@cipherstash/stack-supabase': + specifier: workspace:* + version: link:../../packages/stack-supabase dotenv: specifier: ^17.4.2 version: 17.4.2 @@ -673,6 +676,46 @@ importers: specifier: catalog:repo version: 0.41.0 + packages/stack-supabase: + dependencies: + '@cipherstash/stack': + specifier: workspace:* + version: link:../stack + devDependencies: + '@cipherstash/protect-ffi': + specifier: 0.29.0 + version: 0.29.0 + '@cipherstash/test-kit': + specifier: workspace:* + version: link:../test-kit + '@supabase/postgrest-js': + specifier: 2.105.4 + version: 2.105.4 + '@supabase/supabase-js': + specifier: ^2.110.0 + version: 2.110.0 + '@types/pg': + specifier: ^8.20.0 + version: 8.20.0 + dotenv: + specifier: 17.4.2 + version: 17.4.2 + pg: + specifier: 8.20.0 + version: 8.20.0 + postgres: + specifier: ^3.4.8 + version: 3.4.9 + tsup: + specifier: catalog:repo + version: 8.5.1(jiti@2.7.0)(postcss@8.5.14)(tsx@4.22.1)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: catalog:repo + version: 5.9.3 + vitest: + specifier: catalog:repo + version: 3.2.6(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.22.1)(yaml@2.9.0) + packages/test-kit: dependencies: '@cipherstash/stack': @@ -2130,277 +2173,139 @@ packages: typescript: optional: true - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} - cpu: [arm] - os: [android] - '@rollup/rollup-android-arm-eabi@4.60.4': resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} - cpu: [arm64] - os: [android] - '@rollup/rollup-android-arm64@4.60.4': resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} - cpu: [arm64] - os: [darwin] - '@rollup/rollup-darwin-arm64@4.60.4': resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} - cpu: [x64] - os: [darwin] - '@rollup/rollup-darwin-x64@4.60.4': resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} - cpu: [arm64] - os: [freebsd] - '@rollup/rollup-freebsd-arm64@4.60.4': resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} - cpu: [x64] - os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.4': resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} - cpu: [arm] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} - cpu: [arm] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm-musleabihf@4.60.4': resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-arm64-gnu@4.60.4': resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} - cpu: [arm64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-arm64-musl@4.60.4': resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} - cpu: [loong64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-loong64-gnu@4.60.4': resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} - cpu: [loong64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-loong64-musl@4.60.4': resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-ppc64-gnu@4.60.4': resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} - cpu: [ppc64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-ppc64-musl@4.60.4': resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.60.4': resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} - cpu: [riscv64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-riscv64-musl@4.60.4': resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} - cpu: [s390x] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-s390x-gnu@4.60.4': resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} - cpu: [x64] - os: [linux] - libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.4': resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} - cpu: [x64] - os: [linux] - libc: [musl] - '@rollup/rollup-linux-x64-musl@4.60.4': resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} - cpu: [x64] - os: [openbsd] - '@rollup/rollup-openbsd-x64@4.60.4': resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} - cpu: [arm64] - os: [openharmony] - '@rollup/rollup-openharmony-arm64@4.60.4': resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} - cpu: [arm64] - os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.60.4': resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} - cpu: [ia32] - os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.4': resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} - cpu: [x64] - os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.4': resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} - cpu: [x64] - os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.4': resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} cpu: [x64] @@ -2559,11 +2464,6 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} @@ -3853,11 +3753,6 @@ packages: engines: {node: 20 || >=22} hasBin: true - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - rollup@4.60.4: resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -5491,153 +5386,78 @@ snapshots: optionalDependencies: typescript: 5.9.3 - '@rollup/rollup-android-arm-eabi@4.59.0': - optional: true - '@rollup/rollup-android-arm-eabi@4.60.4': optional: true - '@rollup/rollup-android-arm64@4.59.0': - optional: true - '@rollup/rollup-android-arm64@4.60.4': optional: true - '@rollup/rollup-darwin-arm64@4.59.0': - optional: true - '@rollup/rollup-darwin-arm64@4.60.4': optional: true - '@rollup/rollup-darwin-x64@4.59.0': - optional: true - '@rollup/rollup-darwin-x64@4.60.4': optional: true - '@rollup/rollup-freebsd-arm64@4.59.0': - optional: true - '@rollup/rollup-freebsd-arm64@4.60.4': optional: true - '@rollup/rollup-freebsd-x64@4.59.0': - optional: true - '@rollup/rollup-freebsd-x64@4.60.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': - optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.59.0': - optional: true - '@rollup/rollup-linux-arm64-musl@4.60.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.59.0': - optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-loong64-musl@4.59.0': - optional: true - '@rollup/rollup-linux-loong64-musl@4.60.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-ppc64-musl@4.59.0': - optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.59.0': - optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.59.0': - optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.59.0': - optional: true - '@rollup/rollup-linux-x64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-x64-musl@4.59.0': - optional: true - '@rollup/rollup-linux-x64-musl@4.60.4': optional: true - '@rollup/rollup-openbsd-x64@4.59.0': - optional: true - '@rollup/rollup-openbsd-x64@4.60.4': optional: true - '@rollup/rollup-openharmony-arm64@4.59.0': - optional: true - '@rollup/rollup-openharmony-arm64@4.60.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.59.0': - optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.59.0': - optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.59.0': - optional: true - '@rollup/rollup-win32-x64-gnu@4.60.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.59.0': - optional: true - '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true @@ -5808,10 +5628,7 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn@8.16.0: {} - - acorn@8.17.0: - optional: true + acorn@8.17.0: {} ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: @@ -6301,7 +6118,7 @@ snapshots: dependencies: magic-string: 0.30.21 mlly: 1.8.2 - rollup: 4.59.0 + rollup: 4.60.4 forwarded@0.2.0: {} @@ -6614,7 +6431,7 @@ snapshots: mlly@1.8.2: dependencies: - acorn: 8.16.0 + acorn: 8.17.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.4 @@ -6950,37 +6767,6 @@ snapshots: glob: 13.0.6 package-json-from-dist: 1.0.1 - rollup@4.59.0: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 - fsevents: 2.3.3 - rollup@4.60.4: dependencies: '@types/estree': 1.0.8 @@ -7277,11 +7063,11 @@ snapshots: picocolors: 1.1.1 postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.14)(tsx@4.22.1)(yaml@2.9.0) resolve-from: 5.0.0 - rollup: 4.59.0 + rollup: 4.60.4 source-map: 0.7.6 sucrase: 3.35.1 tinyexec: 0.3.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 tree-kill: 1.2.2 optionalDependencies: postcss: 8.5.14 diff --git a/vitest.shared.ts b/vitest.shared.ts index 7c66d8dfa..9818364e7 100644 --- a/vitest.shared.ts +++ b/vitest.shared.ts @@ -42,9 +42,20 @@ export const sharedAlias: Record = { repoRoot, 'packages/stack/src/encryption/v3.ts', ), - '@cipherstash/stack/supabase': resolve( + // The core↔adapter seam, consumed by the split adapter packages. + '@cipherstash/stack/adapter-kit': resolve( repoRoot, - 'packages/stack/src/supabase/index.ts', + 'packages/stack/src/adapter-kit.ts', + ), + '@cipherstash/stack/encryption': resolve( + repoRoot, + 'packages/stack/src/encryption/index.ts', + ), + // The Supabase adapter now lives in its own package (was + // `@cipherstash/stack/supabase`); resolve it to source too. + '@cipherstash/stack-supabase': resolve( + repoRoot, + 'packages/stack-supabase/src/index.ts', ), // Bare entry LAST: it is a prefix of every subpath above, and Vite matches in // order, so the subpaths must win first. Both sibling tsconfigs map bare From ebbb90d3d54f0ed587600dcd53cac6af19542d3e Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 10:43:25 +1000 Subject: [PATCH 05/17] test(supabase): move Supabase unit tests into stack-supabase (#627) Moves the 11 supabase* unit suites + the 2 test helpers (supabase-mock, postgrest-wire) into the new package; rewrites @/ imports (including vi.mock and dynamic import() targets) to relative ../src paths and @cipherstash/stack public subpaths; repoints the v3 matrix catalog to @cipherstash/test-kit/catalog. Adds fast-check + the @/ / wasm-inline aliases the source-resolved stack needs. stack-supabase: 442 unit tests + type tests green. stack: 1172 pass (only the pre-existing DATABASE_URL live suite fails locally). Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .../__tests__/helpers/postgrest-wire.ts | 2 +- .../__tests__/helpers/supabase-mock.ts | 2 +- .../__tests__/supabase-helpers.test.ts | 4 ++-- .../__tests__/supabase-introspect.test.ts | 8 ++++---- .../__tests__/supabase-schema-builder.test.ts | 8 ++++---- .../__tests__/supabase-v3-builder.test.ts | 8 ++++---- .../__tests__/supabase-v3-factory.test.ts | 18 +++++++++--------- .../__tests__/supabase-v3-matrix.test.ts | 8 ++++---- .../__tests__/supabase-v3-select-star.test.ts | 6 +++--- .../__tests__/supabase-v3-wire.test.ts | 4 ++-- .../__tests__/supabase-v3.test-d.ts | 6 +++--- .../__tests__/supabase-verify.test.ts | 6 +++--- .../__tests__/supabase.test.ts | 6 +++--- packages/stack-supabase/package.json | 1 + packages/stack-supabase/vitest.config.ts | 15 +++++++++++++++ pnpm-lock.yaml | 3 +++ 16 files changed, 62 insertions(+), 43 deletions(-) rename packages/{stack => stack-supabase}/__tests__/helpers/postgrest-wire.ts (97%) rename packages/{stack => stack-supabase}/__tests__/helpers/supabase-mock.ts (98%) rename packages/{stack => stack-supabase}/__tests__/supabase-helpers.test.ts (99%) rename packages/{stack => stack-supabase}/__tests__/supabase-introspect.test.ts (96%) rename packages/{stack => stack-supabase}/__tests__/supabase-schema-builder.test.ts (97%) rename packages/{stack => stack-supabase}/__tests__/supabase-v3-builder.test.ts (99%) rename packages/{stack => stack-supabase}/__tests__/supabase-v3-factory.test.ts (93%) rename packages/{stack => stack-supabase}/__tests__/supabase-v3-matrix.test.ts (97%) rename packages/{stack => stack-supabase}/__tests__/supabase-v3-select-star.test.ts (97%) rename packages/{stack => stack-supabase}/__tests__/supabase-v3-wire.test.ts (98%) rename packages/{stack => stack-supabase}/__tests__/supabase-v3.test-d.ts (99%) rename packages/{stack => stack-supabase}/__tests__/supabase-verify.test.ts (94%) rename packages/{stack => stack-supabase}/__tests__/supabase.test.ts (98%) diff --git a/packages/stack/__tests__/helpers/postgrest-wire.ts b/packages/stack-supabase/__tests__/helpers/postgrest-wire.ts similarity index 97% rename from packages/stack/__tests__/helpers/postgrest-wire.ts rename to packages/stack-supabase/__tests__/helpers/postgrest-wire.ts index d08088987..b535606eb 100644 --- a/packages/stack/__tests__/helpers/postgrest-wire.ts +++ b/packages/stack-supabase/__tests__/helpers/postgrest-wire.ts @@ -17,7 +17,7 @@ */ import { PostgrestClient } from '@supabase/postgrest-js' -import type { SupabaseQueryBuilder } from '@/supabase/types' +import type { SupabaseQueryBuilder } from '../../src/types' export type WirePostgrest = { /** Structurally a supabase client: `.from(table)` → a query builder. */ diff --git a/packages/stack/__tests__/helpers/supabase-mock.ts b/packages/stack-supabase/__tests__/helpers/supabase-mock.ts similarity index 98% rename from packages/stack/__tests__/helpers/supabase-mock.ts rename to packages/stack-supabase/__tests__/helpers/supabase-mock.ts index e799170a9..a72efd619 100644 --- a/packages/stack/__tests__/helpers/supabase-mock.ts +++ b/packages/stack-supabase/__tests__/helpers/supabase-mock.ts @@ -13,7 +13,7 @@ * same doubles, and a change to either mock moves both suites together. */ -import type { EncryptionClient } from '@/encryption' +import type { EncryptionClient } from '@cipherstash/stack/encryption' export type FakeEnvelope = { v: 2 diff --git a/packages/stack/__tests__/supabase-helpers.test.ts b/packages/stack-supabase/__tests__/supabase-helpers.test.ts similarity index 99% rename from packages/stack/__tests__/supabase-helpers.test.ts rename to packages/stack-supabase/__tests__/supabase-helpers.test.ts index bb5b870b8..8ba7ded63 100644 --- a/packages/stack/__tests__/supabase-helpers.test.ts +++ b/packages/stack-supabase/__tests__/supabase-helpers.test.ts @@ -3,8 +3,8 @@ import { addJsonbCastsV3, parseOrString, rebuildOrString, -} from '@/supabase/helpers' -import type { DbPendingOrCondition } from '@/supabase/types' +} from '../src/helpers' +import type { DbPendingOrCondition } from '../src/types' // `createdAt` is a renamed property (DB column `created_at`); `email` is a // property whose name already equals its DB column. diff --git a/packages/stack/__tests__/supabase-introspect.test.ts b/packages/stack-supabase/__tests__/supabase-introspect.test.ts similarity index 96% rename from packages/stack/__tests__/supabase-introspect.test.ts rename to packages/stack-supabase/__tests__/supabase-introspect.test.ts index 2fd9ccc52..9259da720 100644 --- a/packages/stack/__tests__/supabase-introspect.test.ts +++ b/packages/stack-supabase/__tests__/supabase-introspect.test.ts @@ -1,6 +1,6 @@ import fc from 'fast-check' import { afterEach, describe, expect, it, vi } from 'vitest' -import { groupIntrospectionRows, loadPg } from '@/supabase/introspect' +import { groupIntrospectionRows, loadPg } from '../src/introspect' describe('groupIntrospectionRows', () => { it('groups rows by table, preserving row order as column order', () => { @@ -115,7 +115,7 @@ describe('introspect happy path', () => { return { default: { Client } } }) - const { introspect } = await import('@/supabase/introspect') + const { introspect } = await import('../src/introspect') const { tables, unmodelled } = await introspect('postgres://ok') expect(queries).toHaveLength(2) @@ -158,7 +158,7 @@ describe('introspect happy path', () => { return { default: { Client } } }) - const { introspect } = await import('@/supabase/introspect') + const { introspect } = await import('../src/introspect') await expect(introspect('postgres://ok')).rejects.toThrow( 'relation does not exist', ) @@ -188,7 +188,7 @@ describe('introspect connection error handling', () => { return { default: { Client } } }) - const { introspect } = await import('@/supabase/introspect') + const { introspect } = await import('../src/introspect') await expect(introspect('postgres://unreachable')).rejects.toThrow( 'ECONNREFUSED', ) diff --git a/packages/stack/__tests__/supabase-schema-builder.test.ts b/packages/stack-supabase/__tests__/supabase-schema-builder.test.ts similarity index 97% rename from packages/stack/__tests__/supabase-schema-builder.test.ts rename to packages/stack-supabase/__tests__/supabase-schema-builder.test.ts index 725023da4..4ef404d90 100644 --- a/packages/stack/__tests__/supabase-schema-builder.test.ts +++ b/packages/stack-supabase/__tests__/supabase-schema-builder.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it } from 'vitest' -import { encryptedTable, types } from '@/eql/v3' -import type { IntrospectionResult } from '@/supabase/introspect' -import { groupUnmodelledRows } from '@/supabase/introspect' +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import type { IntrospectionResult } from '../src/introspect' +import { groupUnmodelledRows } from '../src/introspect' import { mergeDeclaredTables, synthesizeTables, -} from '@/supabase/schema-builder' +} from '../src/schema-builder' const introspection: IntrospectionResult = [ { diff --git a/packages/stack/__tests__/supabase-v3-builder.test.ts b/packages/stack-supabase/__tests__/supabase-v3-builder.test.ts similarity index 99% rename from packages/stack/__tests__/supabase-v3-builder.test.ts rename to packages/stack-supabase/__tests__/supabase-v3-builder.test.ts index b2e8cc3ab..f56512971 100644 --- a/packages/stack/__tests__/supabase-v3-builder.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-builder.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, vi } from 'vitest' -import { encryptedTable, types } from '@/eql/v3' -import { encryptedColumn, encryptedTable as encryptedTableV2 } from '@/schema' -import { encryptedSupabase } from '@/supabase' -import { EncryptedQueryBuilderV3Impl } from '@/supabase/query-builder-v3' +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import { encryptedColumn, encryptedTable as encryptedTableV2 } from '@cipherstash/stack/schema' +import { encryptedSupabase } from '../src/index.js' +import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' import { createMockEncryptionClient, createMockSupabase, diff --git a/packages/stack/__tests__/supabase-v3-factory.test.ts b/packages/stack-supabase/__tests__/supabase-v3-factory.test.ts similarity index 93% rename from packages/stack/__tests__/supabase-v3-factory.test.ts rename to packages/stack-supabase/__tests__/supabase-v3-factory.test.ts index 542a236f4..9fbc37e12 100644 --- a/packages/stack/__tests__/supabase-v3-factory.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-factory.test.ts @@ -1,9 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { encryptedTable, types } from '@/eql/v3' -import type { SupabaseClientLike } from '@/supabase' -import { encryptedSupabaseV3 } from '@/supabase' -import type { IntrospectionData } from '@/supabase/introspect' -import { EncryptedQueryBuilderV3Impl } from '@/supabase/query-builder-v3' +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import type { SupabaseClientLike } from '../src/index.js' +import { encryptedSupabaseV3 } from '../src/index.js' +import type { IntrospectionData } from '../src/introspect' +import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' // --- Mocks ----------------------------------------------------------------- // @@ -17,13 +17,13 @@ const { introspectMock, encryptionMock, createClientMock } = vi.hoisted(() => ({ createClientMock: vi.fn(() => ({ from: () => ({}) })), })) -vi.mock('@/supabase/introspect', async (importActual) => { - const actual = await importActual() +vi.mock('../src/introspect', async (importActual) => { + const actual = await importActual() return { ...actual, introspect: (url: string) => introspectMock(url) } }) -vi.mock('@/encryption', async (importActual) => { - const actual = await importActual() +vi.mock('@cipherstash/stack/encryption', async (importActual) => { + const actual = await importActual() return { ...actual, Encryption: (cfg: unknown) => encryptionMock(cfg) } }) diff --git a/packages/stack/__tests__/supabase-v3-matrix.test.ts b/packages/stack-supabase/__tests__/supabase-v3-matrix.test.ts similarity index 97% rename from packages/stack/__tests__/supabase-v3-matrix.test.ts rename to packages/stack-supabase/__tests__/supabase-v3-matrix.test.ts index 3ecdb3d99..917cc8215 100644 --- a/packages/stack/__tests__/supabase-v3-matrix.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-matrix.test.ts @@ -20,9 +20,9 @@ */ import { describe, expect, it } from 'vitest' -import type { AnyV3Table } from '@/eql/v3' -import { encryptedTable } from '@/eql/v3' -import { EncryptedQueryBuilderV3Impl } from '@/supabase/query-builder-v3' +import type { AnyV3Table } from '@cipherstash/stack/eql/v3' +import { encryptedTable } from '@cipherstash/stack/eql/v3' +import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' import { createMockEncryptionClient, createMockSupabase, @@ -34,7 +34,7 @@ import { eqlTypeSlug as slug, typedEntries, V3_MATRIX, -} from './v3-matrix/catalog' +} from '@cipherstash/test-kit/catalog' const matrixEntries = typedEntries(V3_MATRIX) diff --git a/packages/stack/__tests__/supabase-v3-select-star.test.ts b/packages/stack-supabase/__tests__/supabase-v3-select-star.test.ts similarity index 97% rename from packages/stack/__tests__/supabase-v3-select-star.test.ts rename to packages/stack-supabase/__tests__/supabase-v3-select-star.test.ts index 3e1b3522d..edf207c41 100644 --- a/packages/stack/__tests__/supabase-v3-select-star.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-select-star.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' -import type { EncryptionClient } from '@/encryption' -import { encryptedTable, types } from '@/eql/v3' -import { EncryptedQueryBuilderV3Impl } from '@/supabase/query-builder-v3' +import type { EncryptionClient } from '@cipherstash/stack/encryption' +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' /** * Supabase double that records the select string AND simulates the part of diff --git a/packages/stack/__tests__/supabase-v3-wire.test.ts b/packages/stack-supabase/__tests__/supabase-v3-wire.test.ts similarity index 98% rename from packages/stack/__tests__/supabase-v3-wire.test.ts rename to packages/stack-supabase/__tests__/supabase-v3-wire.test.ts index f9f0716db..4bc7ea6c1 100644 --- a/packages/stack/__tests__/supabase-v3-wire.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-wire.test.ts @@ -9,8 +9,8 @@ */ import { describe, expect, it } from 'vitest' -import { encryptedTable, types } from '@/eql/v3' -import { EncryptedQueryBuilderV3Impl } from '@/supabase/query-builder-v3' +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' import { createWirePostgrest } from './helpers/postgrest-wire' import { createMockEncryptionClient } from './helpers/supabase-mock' diff --git a/packages/stack/__tests__/supabase-v3.test-d.ts b/packages/stack-supabase/__tests__/supabase-v3.test-d.ts similarity index 99% rename from packages/stack/__tests__/supabase-v3.test-d.ts rename to packages/stack-supabase/__tests__/supabase-v3.test-d.ts index 32681f42e..1f0fc6e9f 100644 --- a/packages/stack/__tests__/supabase-v3.test-d.ts +++ b/packages/stack-supabase/__tests__/supabase-v3.test-d.ts @@ -1,13 +1,13 @@ import { describe, expectTypeOf, it } from 'vitest' -import { encryptedTable, type InferPlaintext, types } from '@/eql/v3' -import { encryptedColumn, encryptedTable as v2EncryptedTable } from '@/schema' +import { encryptedTable, type InferPlaintext, types } from '@cipherstash/stack/eql/v3' +import { encryptedColumn, encryptedTable as v2EncryptedTable } from '@cipherstash/stack/schema' import { type EncryptedQueryBuilderV3, type EncryptedSupabaseResponse, encryptedSupabase, encryptedSupabaseV3, type SupabaseClientLike, -} from '@/supabase' +} from '../src/index.js' declare const supabaseClient: SupabaseClientLike diff --git a/packages/stack/__tests__/supabase-verify.test.ts b/packages/stack-supabase/__tests__/supabase-verify.test.ts similarity index 94% rename from packages/stack/__tests__/supabase-verify.test.ts rename to packages/stack-supabase/__tests__/supabase-verify.test.ts index 4762c7ed5..72948f63a 100644 --- a/packages/stack/__tests__/supabase-verify.test.ts +++ b/packages/stack-supabase/__tests__/supabase-verify.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' -import { encryptedTable, types } from '@/eql/v3' -import type { IntrospectionResult } from '@/supabase/introspect' -import { verifyDeclaredSchemas } from '@/supabase/verify' +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import type { IntrospectionResult } from '../src/introspect' +import { verifyDeclaredSchemas } from '../src/verify' const introspection: IntrospectionResult = [ { diff --git a/packages/stack/__tests__/supabase.test.ts b/packages/stack-supabase/__tests__/supabase.test.ts similarity index 98% rename from packages/stack/__tests__/supabase.test.ts rename to packages/stack-supabase/__tests__/supabase.test.ts index 175c90604..928869683 100644 --- a/packages/stack/__tests__/supabase.test.ts +++ b/packages/stack-supabase/__tests__/supabase.test.ts @@ -3,9 +3,9 @@ import 'dotenv/config' import { createClient } from '@supabase/supabase-js' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { Encryption } from '@/index' -import { encryptedColumn, encryptedTable } from '@/schema' -import { encryptedSupabase } from '@/supabase' +import { Encryption } from '@cipherstash/stack' +import { encryptedColumn, encryptedTable } from '@cipherstash/stack/schema' +import { encryptedSupabase } from '../src/index.js' // supabase.test.ts needs a live Supabase project, so the suite is skipped // when the Supabase environment is not configured (e.g. in CI, pending a diff --git a/packages/stack-supabase/package.json b/packages/stack-supabase/package.json index 4dbf81279..e7d1f6c1d 100644 --- a/packages/stack-supabase/package.json +++ b/packages/stack-supabase/package.json @@ -72,6 +72,7 @@ "@supabase/postgrest-js": "2.105.4", "@supabase/supabase-js": "^2.110.0", "@types/pg": "^8.20.0", + "fast-check": "^4.8.0", "dotenv": "17.4.2", "pg": "8.20.0", "postgres": "^3.4.8", diff --git a/packages/stack-supabase/vitest.config.ts b/packages/stack-supabase/vitest.config.ts index abc8b09b4..356568b5b 100644 --- a/packages/stack-supabase/vitest.config.ts +++ b/packages/stack-supabase/vitest.config.ts @@ -14,6 +14,21 @@ export default defineConfig({ __dirname, '../stack/src/adapter-kit.ts', ), + // We resolve `@cipherstash/stack` to its SOURCE (via sharedAlias), and that + // source uses stack's internal `@/` alias — so it must resolve here too. + // This package's own code never uses `@/`, so there is no collision. + '@/': resolve(__dirname, '../stack/src') + '/', + // stack's `src/wasm-inline.ts` imports the `/wasm-inline` subpaths of + // protect-ffi/auth, which aren't resolvable by Vitest; alias to stack's + // stubs so loading stack source in tests doesn't fail on them. + '@cipherstash/protect-ffi/wasm-inline': resolve( + __dirname, + '../stack/__tests__/helpers/stub-protect-ffi-wasm-inline.ts', + ), + '@cipherstash/auth/wasm-inline': resolve( + __dirname, + '../stack/__tests__/helpers/stub-auth-wasm-inline.ts', + ), }, }, test: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 767ac4a82..a95dbaa93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -700,6 +700,9 @@ importers: dotenv: specifier: 17.4.2 version: 17.4.2 + fast-check: + specifier: ^4.8.0 + version: 4.8.0 pg: specifier: 8.20.0 version: 8.20.0 From c434cc138807cca32942957bd6e1be881e5fd8ca Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 11:00:31 +1000 Subject: [PATCH 06/17] refactor(drizzle): extract into @cipherstash/stack-drizzle (v2 + v3) (#627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves src/drizzle (v2 → package root) and src/eql/v3/drizzle (v3 → ./v3) into the new package; rewrites @/ imports to @cipherstash/stack public subpaths + adapter-kit. Drops ./drizzle and ./eql/v3/drizzle from stack. Moves the drizzle unit + type suites; relocates the shared needle-for helper into @cipherstash/test-kit (used by both a stack test and the drizzle suite). Adds the @/* → stack source mapping to the adapter/test-kit tsconfigs (source- resolved stack needs its own internal alias) and the missing stack subpath aliases (types/errors/identity) to vitest.shared for value imports like queryTypes. Green: stack-drizzle 356 unit + type tests; stack-supabase 442; stack 812 (only the pre-existing DATABASE_URL live suite fails locally). All three build (JS+DTS); biome code:check passes. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .../drizzle-operators-bigint.test.ts | 4 +- .../__tests__/drizzle-operators-jsonb.test.ts | 4 +- .../__tests__/v3}/bigint.test.ts | 6 +- .../__tests__/v3}/codec.test.ts | 6 +- .../__tests__/v3}/column.test.ts | 16 ++-- .../__tests__/v3}/exports.test.ts | 2 +- .../__tests__/v3}/operators.test-d.ts | 14 +-- .../__tests__/v3}/operators.test.ts | 20 ++-- .../__tests__/v3}/schema-extraction.test.ts | 6 +- .../__tests__/v3}/sql-dialect.test.ts | 2 +- .../__tests__/v3}/types.test-d.ts | 6 +- .../__tests__/v3}/types.test.ts | 6 +- packages/stack-drizzle/package.json | 95 +++++++++++++++++++ .../drizzle => stack-drizzle/src}/index.ts | 8 +- .../src}/operators.ts | 14 +-- .../src}/schema-extraction.ts | 4 +- .../drizzle => stack-drizzle/src/v3}/codec.ts | 2 +- .../src/v3}/column.ts | 7 +- .../drizzle => stack-drizzle/src/v3}/index.ts | 0 .../src/v3}/operators.ts | 19 ++-- .../src/v3}/schema-extraction.ts | 4 +- .../src/v3}/sql-dialect.ts | 0 .../drizzle => stack-drizzle/src/v3}/types.ts | 2 +- packages/stack-drizzle/tsconfig.json | 37 ++++++++ packages/stack-drizzle/tsup.config.ts | 13 +++ packages/stack-drizzle/vitest.config.ts | 34 +++++++ .../__tests__/supabase-helpers.test.ts | 6 +- .../__tests__/supabase-schema-builder.test.ts | 7 +- .../__tests__/supabase-v3-builder.test.ts | 7 +- .../__tests__/supabase-v3-factory.test.ts | 5 +- .../__tests__/supabase-v3-matrix.test.ts | 14 +-- .../__tests__/supabase-v3-select-star.test.ts | 2 +- .../__tests__/supabase-v3-wire.test.ts | 2 +- .../__tests__/supabase-v3.test-d.ts | 11 ++- .../__tests__/supabase-verify.test.ts | 2 +- .../stack-supabase/__tests__/supabase.test.ts | 4 +- packages/stack-supabase/src/helpers.ts | 5 +- packages/stack-supabase/src/index.ts | 5 +- .../stack-supabase/src/query-builder-v3.ts | 7 +- packages/stack-supabase/src/query-builder.ts | 11 ++- packages/stack-supabase/src/schema-builder.ts | 2 +- packages/stack-supabase/src/types.ts | 9 +- packages/stack-supabase/tsconfig.json | 3 +- .../__tests__/v3-matrix/needle-for.test.ts | 2 +- packages/stack/package.json | 26 ----- packages/stack/tsup.config.ts | 2 - packages/test-kit/src/index.ts | 1 + .../v3-matrix => test-kit/src}/needle-for.ts | 2 +- packages/test-kit/tsconfig.json | 1 + pnpm-lock.yaml | 42 ++++++++ vitest.shared.ts | 22 +++++ 51 files changed, 387 insertions(+), 144 deletions(-) rename packages/{stack => stack-drizzle}/__tests__/drizzle-operators-bigint.test.ts (93%) rename packages/{stack => stack-drizzle}/__tests__/drizzle-operators-jsonb.test.ts (98%) rename packages/{stack/__tests__/drizzle-v3 => stack-drizzle/__tests__/v3}/bigint.test.ts (96%) rename packages/{stack/__tests__/drizzle-v3 => stack-drizzle/__tests__/v3}/codec.test.ts (98%) rename packages/{stack/__tests__/drizzle-v3 => stack-drizzle/__tests__/v3}/column.test.ts (98%) rename packages/{stack/__tests__/drizzle-v3 => stack-drizzle/__tests__/v3}/exports.test.ts (96%) rename packages/{stack/__tests__/drizzle-v3 => stack-drizzle/__tests__/v3}/operators.test-d.ts (86%) rename packages/{stack/__tests__/drizzle-v3 => stack-drizzle/__tests__/v3}/operators.test.ts (98%) rename packages/{stack/__tests__/drizzle-v3 => stack-drizzle/__tests__/v3}/schema-extraction.test.ts (94%) rename packages/{stack/__tests__/drizzle-v3 => stack-drizzle/__tests__/v3}/sql-dialect.test.ts (98%) rename packages/{stack/__tests__/drizzle-v3 => stack-drizzle/__tests__/v3}/types.test-d.ts (92%) rename packages/{stack/__tests__/drizzle-v3 => stack-drizzle/__tests__/v3}/types.test.ts (80%) create mode 100644 packages/stack-drizzle/package.json rename packages/{stack/src/drizzle => stack-drizzle/src}/index.ts (98%) rename packages/{stack/src/drizzle => stack-drizzle/src}/operators.ts (99%) rename packages/{stack/src/drizzle => stack-drizzle/src}/schema-extraction.ts (99%) rename packages/{stack/src/eql/v3/drizzle => stack-drizzle/src/v3}/codec.ts (98%) rename packages/{stack/src/eql/v3/drizzle => stack-drizzle/src/v3}/column.ts (96%) rename packages/{stack/src/eql/v3/drizzle => stack-drizzle/src/v3}/index.ts (100%) rename packages/{stack/src/eql/v3/drizzle => stack-drizzle/src/v3}/operators.ts (97%) rename packages/{stack/src/eql/v3/drizzle => stack-drizzle/src/v3}/schema-extraction.ts (98%) rename packages/{stack/src/eql/v3/drizzle => stack-drizzle/src/v3}/sql-dialect.ts (100%) rename packages/{stack/src/eql/v3/drizzle => stack-drizzle/src/v3}/types.ts (89%) create mode 100644 packages/stack-drizzle/tsconfig.json create mode 100644 packages/stack-drizzle/tsup.config.ts create mode 100644 packages/stack-drizzle/vitest.config.ts rename packages/{stack/__tests__/v3-matrix => test-kit/src}/needle-for.ts (94%) diff --git a/packages/stack/__tests__/drizzle-operators-bigint.test.ts b/packages/stack-drizzle/__tests__/drizzle-operators-bigint.test.ts similarity index 93% rename from packages/stack/__tests__/drizzle-operators-bigint.test.ts rename to packages/stack-drizzle/__tests__/drizzle-operators-bigint.test.ts index 6e042a6b9..e71e47622 100644 --- a/packages/stack/__tests__/drizzle-operators-bigint.test.ts +++ b/packages/stack-drizzle/__tests__/drizzle-operators-bigint.test.ts @@ -1,7 +1,7 @@ +import type { EncryptionClient } from '@cipherstash/stack/encryption' import { PgDialect, pgTable } from 'drizzle-orm/pg-core' import { describe, expect, it, vi } from 'vitest' -import { createEncryptionOperators, encryptedType } from '@/drizzle' -import type { EncryptionClient } from '@/encryption' +import { createEncryptionOperators, encryptedType } from '../src/index.js' // Regression coverage for the `bigint` (int8) plaintext path through the v3 // Drizzle operators. Before the fix, `toPlaintext` fell through to diff --git a/packages/stack/__tests__/drizzle-operators-jsonb.test.ts b/packages/stack-drizzle/__tests__/drizzle-operators-jsonb.test.ts similarity index 98% rename from packages/stack/__tests__/drizzle-operators-jsonb.test.ts rename to packages/stack-drizzle/__tests__/drizzle-operators-jsonb.test.ts index 2f3f5658e..47ab6bce0 100644 --- a/packages/stack/__tests__/drizzle-operators-jsonb.test.ts +++ b/packages/stack-drizzle/__tests__/drizzle-operators-jsonb.test.ts @@ -1,11 +1,11 @@ +import type { EncryptionClient } from '@cipherstash/stack/encryption' import { PgDialect, pgTable } from 'drizzle-orm/pg-core' import { describe, expect, it, vi } from 'vitest' import { createEncryptionOperators, EncryptionOperatorError, encryptedType, -} from '@/drizzle' -import type { EncryptionClient } from '@/encryption' +} from '../src/index.js' const ENCRYPTED_VALUE = '{"v":"encrypted-value"}' diff --git a/packages/stack/__tests__/drizzle-v3/bigint.test.ts b/packages/stack-drizzle/__tests__/v3/bigint.test.ts similarity index 96% rename from packages/stack/__tests__/drizzle-v3/bigint.test.ts rename to packages/stack-drizzle/__tests__/v3/bigint.test.ts index 7901955e1..8db51f376 100644 --- a/packages/stack/__tests__/drizzle-v3/bigint.test.ts +++ b/packages/stack-drizzle/__tests__/v3/bigint.test.ts @@ -1,12 +1,12 @@ import { type SQL, sql } from 'drizzle-orm' import { integer, PgDialect, pgTable } from 'drizzle-orm/pg-core' import { describe, expect, it, vi } from 'vitest' -import { getEqlV3Column, isEqlV3Column } from '@/eql/v3/drizzle/column' +import { getEqlV3Column, isEqlV3Column } from '../../src/v3/column' import { createEncryptionOperatorsV3, EncryptionOperatorError, -} from '@/eql/v3/drizzle/operators' -import { types } from '@/eql/v3/drizzle/types' +} from '../../src/v3/operators' +import { types } from '../../src/v3/types' // A representative encrypted envelope — what `client.encrypt` actually returns // and what a bigint column stores. Deliberately NOT a plaintext bigint. diff --git a/packages/stack/__tests__/drizzle-v3/codec.test.ts b/packages/stack-drizzle/__tests__/v3/codec.test.ts similarity index 98% rename from packages/stack/__tests__/drizzle-v3/codec.test.ts rename to packages/stack-drizzle/__tests__/v3/codec.test.ts index 4519ee0a0..3049c2c2a 100644 --- a/packages/stack/__tests__/drizzle-v3/codec.test.ts +++ b/packages/stack-drizzle/__tests__/v3/codec.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from 'vitest' -import { - EqlV3CodecError, - v3FromDriver, - v3ToDriver, -} from '@/eql/v3/drizzle/codec' +import { EqlV3CodecError, v3FromDriver, v3ToDriver } from '../../src/v3/codec' // A realistic `public.eql_v3_text_eq` envelope: schema version, table/column // identifier, mp_base85 ciphertext, HMAC term. The trivial `{v:1,c:'ct'}` diff --git a/packages/stack/__tests__/drizzle-v3/column.test.ts b/packages/stack-drizzle/__tests__/v3/column.test.ts similarity index 98% rename from packages/stack/__tests__/drizzle-v3/column.test.ts rename to packages/stack-drizzle/__tests__/v3/column.test.ts index f9397dae3..f812a3bcf 100644 --- a/packages/stack/__tests__/drizzle-v3/column.test.ts +++ b/packages/stack-drizzle/__tests__/v3/column.test.ts @@ -1,18 +1,18 @@ +import { types as v3Types } from '@cipherstash/stack/eql/v3' +import { + EQL_V3_DOMAIN_SCHEMA, + eqlTypeSlug as slug, + typedEntries, + V3_MATRIX, +} from '@cipherstash/test-kit/catalog' import { pgTable } from 'drizzle-orm/pg-core' import { describe, expect, it } from 'vitest' -import { types as v3Types } from '@/eql/v3' import { EQL_V3_DOMAINS, getEqlV3Column, isEqlV3Column, makeEqlV3Column, -} from '@/eql/v3/drizzle/column' -import { - EQL_V3_DOMAIN_SCHEMA, - eqlTypeSlug as slug, - typedEntries, - V3_MATRIX, -} from '../v3-matrix/catalog' +} from '../../src/v3/column' describe('makeEqlV3Column', () => { it('sets dataType() to the concrete eql_v3 domain', () => { diff --git a/packages/stack/__tests__/drizzle-v3/exports.test.ts b/packages/stack-drizzle/__tests__/v3/exports.test.ts similarity index 96% rename from packages/stack/__tests__/drizzle-v3/exports.test.ts rename to packages/stack-drizzle/__tests__/v3/exports.test.ts index 26fbb1ae9..4ad6d98e3 100644 --- a/packages/stack/__tests__/drizzle-v3/exports.test.ts +++ b/packages/stack-drizzle/__tests__/v3/exports.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import * as barrel from '@/eql/v3/drizzle' +import * as barrel from '../../src/v3/index.js' // Exhaustive, so ADDING an export fails here too. The previous version asserted // only four names, leaving the codec/column re-exports unpinned and letting a diff --git a/packages/stack/__tests__/drizzle-v3/operators.test-d.ts b/packages/stack-drizzle/__tests__/v3/operators.test-d.ts similarity index 86% rename from packages/stack/__tests__/drizzle-v3/operators.test-d.ts rename to packages/stack-drizzle/__tests__/v3/operators.test-d.ts index bb6cd1861..3d3de6589 100644 --- a/packages/stack/__tests__/drizzle-v3/operators.test-d.ts +++ b/packages/stack-drizzle/__tests__/v3/operators.test-d.ts @@ -1,12 +1,12 @@ import type { Result } from '@byteslice/result' +import type { AuditConfig } from '@cipherstash/stack/adapter-kit' +import type { EncryptionClient } from '@cipherstash/stack/encryption' +import type { EncryptionError } from '@cipherstash/stack/errors' +import type { LockContext } from '@cipherstash/stack/identity' +import type { Encrypted } from '@cipherstash/stack/types' +import type { EncryptionV3 } from '@cipherstash/stack/v3' import { describe, expectTypeOf, it } from 'vitest' -import type { EncryptionClient } from '@/encryption' -import type { AuditConfig } from '@/encryption/operations/base-operation' -import type { EncryptionV3 } from '@/encryption/v3' -import { createEncryptionOperatorsV3 } from '@/eql/v3/drizzle' -import type { EncryptionError } from '@/errors' -import type { LockContext } from '@/identity' -import type { Encrypted } from '@/types' +import { createEncryptionOperatorsV3 } from '../../src/v3/index.js' /** * Static regression guard for M1: `createEncryptionOperatorsV3` must accept the diff --git a/packages/stack/__tests__/drizzle-v3/operators.test.ts b/packages/stack-drizzle/__tests__/v3/operators.test.ts similarity index 98% rename from packages/stack/__tests__/drizzle-v3/operators.test.ts rename to packages/stack-drizzle/__tests__/v3/operators.test.ts index d96b7e801..1cd9ede4c 100644 --- a/packages/stack/__tests__/drizzle-v3/operators.test.ts +++ b/packages/stack-drizzle/__tests__/v3/operators.test.ts @@ -1,3 +1,9 @@ +import { needleFor } from '@cipherstash/test-kit' +import { + eqlTypeSlug as slug, + typedEntries, + V3_MATRIX, +} from '@cipherstash/test-kit/catalog' import { eq as drizzleEq, exists, @@ -11,19 +17,13 @@ import { } from 'drizzle-orm' import { integer, PgDialect, pgTable } from 'drizzle-orm/pg-core' import { describe, expect, it, vi } from 'vitest' -import { makeEqlV3Column } from '@/eql/v3/drizzle/column' +import { makeEqlV3Column } from '../../src/v3/column' import { createEncryptionOperatorsV3, EncryptionOperatorError, -} from '@/eql/v3/drizzle/operators' -import { extractEncryptionSchemaV3 } from '@/eql/v3/drizzle/schema-extraction' -import { types } from '@/eql/v3/drizzle/types' -import { - eqlTypeSlug as slug, - typedEntries, - V3_MATRIX, -} from '../v3-matrix/catalog' -import { needleFor } from '../v3-matrix/needle-for' +} from '../../src/v3/operators' +import { extractEncryptionSchemaV3 } from '../../src/v3/schema-extraction' +import { types } from '../../src/v3/types' const TERM = { c: 'ct', v: 1 } const TERM_JSON = JSON.stringify(TERM) diff --git a/packages/stack/__tests__/drizzle-v3/schema-extraction.test.ts b/packages/stack-drizzle/__tests__/v3/schema-extraction.test.ts similarity index 94% rename from packages/stack/__tests__/drizzle-v3/schema-extraction.test.ts rename to packages/stack-drizzle/__tests__/v3/schema-extraction.test.ts index c87790fc3..e3767fcc0 100644 --- a/packages/stack/__tests__/drizzle-v3/schema-extraction.test.ts +++ b/packages/stack-drizzle/__tests__/v3/schema-extraction.test.ts @@ -1,8 +1,8 @@ +import { encryptedTable, types as v3Types } from '@cipherstash/stack/eql/v3' import { integer, pgTable } from 'drizzle-orm/pg-core' import { describe, expect, it } from 'vitest' -import { encryptedTable, types as v3Types } from '@/eql/v3' -import { extractEncryptionSchemaV3 } from '@/eql/v3/drizzle/schema-extraction' -import { types } from '@/eql/v3/drizzle/types' +import { extractEncryptionSchemaV3 } from '../../src/v3/schema-extraction' +import { types } from '../../src/v3/types' describe('extractEncryptionSchemaV3', () => { it('rebuilds an equivalent eql/v3 encryptedTable from every drizzle v3 factory', () => { diff --git a/packages/stack/__tests__/drizzle-v3/sql-dialect.test.ts b/packages/stack-drizzle/__tests__/v3/sql-dialect.test.ts similarity index 98% rename from packages/stack/__tests__/drizzle-v3/sql-dialect.test.ts rename to packages/stack-drizzle/__tests__/v3/sql-dialect.test.ts index 142b79e58..3120bcd17 100644 --- a/packages/stack/__tests__/drizzle-v3/sql-dialect.test.ts +++ b/packages/stack-drizzle/__tests__/v3/sql-dialect.test.ts @@ -1,7 +1,7 @@ import { not, sql } from 'drizzle-orm' import { PgDialect } from 'drizzle-orm/pg-core' import { describe, expect, it } from 'vitest' -import { EQL_V3_FN_SCHEMA, v3Dialect } from '@/eql/v3/drizzle/sql-dialect' +import { EQL_V3_FN_SCHEMA, v3Dialect } from '../../src/v3/sql-dialect' const dialect = new PgDialect() const render = (s: ReturnType) => dialect.sqlToQuery(s).sql diff --git a/packages/stack/__tests__/drizzle-v3/types.test-d.ts b/packages/stack-drizzle/__tests__/v3/types.test-d.ts similarity index 92% rename from packages/stack/__tests__/drizzle-v3/types.test-d.ts rename to packages/stack-drizzle/__tests__/v3/types.test-d.ts index e33b4f7a0..55b8022c2 100644 --- a/packages/stack/__tests__/drizzle-v3/types.test-d.ts +++ b/packages/stack-drizzle/__tests__/v3/types.test-d.ts @@ -1,7 +1,7 @@ +import type { types as v3Types } from '@cipherstash/stack/eql/v3' +import type { Encrypted } from '@cipherstash/stack/types' import { describe, expectTypeOf, it } from 'vitest' -import type { types as v3Types } from '@/eql/v3' -import { types } from '@/eql/v3/drizzle/types' -import type { Encrypted } from '@/types' +import { types } from '../../src/v3/types' describe('v3 drizzle types - type-level', () => { it('exposes exactly the same factory keys as @/eql/v3 types', () => { diff --git a/packages/stack/__tests__/drizzle-v3/types.test.ts b/packages/stack-drizzle/__tests__/v3/types.test.ts similarity index 80% rename from packages/stack/__tests__/drizzle-v3/types.test.ts rename to packages/stack-drizzle/__tests__/v3/types.test.ts index c143fc17c..face747e1 100644 --- a/packages/stack/__tests__/drizzle-v3/types.test.ts +++ b/packages/stack-drizzle/__tests__/v3/types.test.ts @@ -1,7 +1,7 @@ +import { types as v3Types } from '@cipherstash/stack/eql/v3' import { describe, expect, it } from 'vitest' -import { types as v3Types } from '@/eql/v3' -import { getEqlV3Column } from '@/eql/v3/drizzle/column' -import { types } from '@/eql/v3/drizzle/types' +import { getEqlV3Column } from '../../src/v3/column' +import { types } from '../../src/v3/types' describe('v3 drizzle types namespace', () => { it('exposes the same factory names as @/eql/v3 types', () => { diff --git a/packages/stack-drizzle/package.json b/packages/stack-drizzle/package.json new file mode 100644 index 000000000..f744ebc74 --- /dev/null +++ b/packages/stack-drizzle/package.json @@ -0,0 +1,95 @@ +{ + "name": "@cipherstash/stack-drizzle", + "version": "0.1.0", + "description": "CipherStash Stack Drizzle ORM integration: encrypted column types and query operators for EQL v2 (./) and EQL v3 (./v3).", + "keywords": [ + "encrypted", + "drizzle", + "orm", + "postgres", + "eql", + "cipherstash", + "stack" + ], + "bugs": { + "url": "https://github.com/cipherstash/stack/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/cipherstash/stack.git", + "directory": "packages/stack-drizzle" + }, + "license": "MIT", + "author": "CipherStash ", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "dist", + "README.md" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./v3": { + "import": { + "types": "./dist/v3/index.d.ts", + "default": "./dist/v3/index.js" + }, + "require": { + "types": "./dist/v3/index.d.cts", + "default": "./dist/v3/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "v3": [ + "./dist/v3/index.d.ts" + ] + } + }, + "scripts": { + "prebuild": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run", + "test:types": "vitest --run --typecheck.only", + "test:integration": "vitest run --config integration/vitest.config.ts" + }, + "dependencies": { + "@cipherstash/stack": "workspace:*", + "@byteslice/result": "0.2.0" + }, + "peerDependencies": { + "@cipherstash/protect-ffi": "0.29.0", + "drizzle-orm": ">=0.33" + }, + "devDependencies": { + "@cipherstash/protect-ffi": "0.29.0", + "@cipherstash/test-kit": "workspace:*", + "dotenv": "17.4.2", + "drizzle-orm": "^0.45.2", + "postgres": "^3.4.8", + "tsup": "catalog:repo", + "typescript": "catalog:repo", + "vitest": "catalog:repo" + }, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=22" + } +} diff --git a/packages/stack/src/drizzle/index.ts b/packages/stack-drizzle/src/index.ts similarity index 98% rename from packages/stack/src/drizzle/index.ts rename to packages/stack-drizzle/src/index.ts index 6284e45b3..ff3f12bb0 100644 --- a/packages/stack/src/drizzle/index.ts +++ b/packages/stack-drizzle/src/index.ts @@ -1,5 +1,9 @@ +import type { + CastAs, + MatchIndexOpts, + TokenFilter, +} from '@cipherstash/stack/schema' import { customType } from 'drizzle-orm/pg-core' -import type { CastAs, MatchIndexOpts, TokenFilter } from '@/schema' export type { CastAs, MatchIndexOpts, TokenFilter } @@ -81,7 +85,7 @@ const columnConfigMap = new Map< * * ```typescript * import { pgTable, integer, timestamp } from 'drizzle-orm/pg-core' - * import { encryptedType } from '@cipherstash/stack/drizzle' + * import { encryptedType } from '@cipherstash/stack-drizzle' * * const users = pgTable('users', { * email: encryptedType('email', { diff --git a/packages/stack/src/drizzle/operators.ts b/packages/stack-drizzle/src/operators.ts similarity index 99% rename from packages/stack/src/drizzle/operators.ts rename to packages/stack-drizzle/src/operators.ts index c31e2ff07..674bb3ba3 100644 --- a/packages/stack/src/drizzle/operators.ts +++ b/packages/stack-drizzle/src/operators.ts @@ -1,3 +1,10 @@ +import type { EncryptionClient } from '@cipherstash/stack/encryption' +import type { + EncryptedColumn, + EncryptedTable, + EncryptedTableColumn, +} from '@cipherstash/stack/schema' +import { type QueryTypeName, queryTypes } from '@cipherstash/stack/types' import { and, arrayContained, @@ -30,13 +37,6 @@ import { sql, } from 'drizzle-orm' import type { PgTable } from 'drizzle-orm/pg-core' -import type { EncryptionClient } from '@/encryption/index.js' -import type { - EncryptedColumn, - EncryptedTable, - EncryptedTableColumn, -} from '@/schema' -import { type QueryTypeName, queryTypes } from '@/types' import type { EncryptedColumnConfig } from './index.js' import { getEncryptedColumnConfig } from './index.js' import { extractEncryptionSchema } from './schema-extraction.js' diff --git a/packages/stack/src/drizzle/schema-extraction.ts b/packages/stack-drizzle/src/schema-extraction.ts similarity index 99% rename from packages/stack/src/drizzle/schema-extraction.ts rename to packages/stack-drizzle/src/schema-extraction.ts index 824234148..90bb3ec1e 100644 --- a/packages/stack/src/drizzle/schema-extraction.ts +++ b/packages/stack-drizzle/src/schema-extraction.ts @@ -1,10 +1,10 @@ -import type { PgCustomColumn, PgTable } from 'drizzle-orm/pg-core' import { type EncryptedColumn, type EncryptedTable, encryptedColumn, encryptedTable, -} from '@/schema' +} from '@cipherstash/stack/schema' +import type { PgCustomColumn, PgTable } from 'drizzle-orm/pg-core' import { getEncryptedColumnConfig } from './index.js' /** diff --git a/packages/stack/src/eql/v3/drizzle/codec.ts b/packages/stack-drizzle/src/v3/codec.ts similarity index 98% rename from packages/stack/src/eql/v3/drizzle/codec.ts rename to packages/stack-drizzle/src/v3/codec.ts index 0db6f55b8..07c822ee9 100644 --- a/packages/stack/src/eql/v3/drizzle/codec.ts +++ b/packages/stack-drizzle/src/v3/codec.ts @@ -5,7 +5,7 @@ * distinct from v2's composite-literal parser. */ -import type { Encrypted } from '@/types' +import type { Encrypted } from '@cipherstash/stack/types' /** Thrown when a driver value cannot be read back as an EQL v3 envelope. */ export class EqlV3CodecError extends Error { diff --git a/packages/stack/src/eql/v3/drizzle/column.ts b/packages/stack-drizzle/src/v3/column.ts similarity index 96% rename from packages/stack/src/eql/v3/drizzle/column.ts rename to packages/stack-drizzle/src/v3/column.ts index abc2be4c2..3c2d184a1 100644 --- a/packages/stack/src/eql/v3/drizzle/column.ts +++ b/packages/stack-drizzle/src/v3/column.ts @@ -1,6 +1,9 @@ +import { + type AnyEncryptedV3Column, + types as v3Types, +} from '@cipherstash/stack/eql/v3' +import type { Encrypted } from '@cipherstash/stack/types' import { customType } from 'drizzle-orm/pg-core' -import { type AnyEncryptedV3Column, types as v3Types } from '@/eql/v3' -import type { Encrypted } from '@/types' import { v3FromDriver, v3ToDriver } from './codec.js' const buildersByDomain: ReadonlyMap< diff --git a/packages/stack/src/eql/v3/drizzle/index.ts b/packages/stack-drizzle/src/v3/index.ts similarity index 100% rename from packages/stack/src/eql/v3/drizzle/index.ts rename to packages/stack-drizzle/src/v3/index.ts diff --git a/packages/stack/src/eql/v3/drizzle/operators.ts b/packages/stack-drizzle/src/v3/operators.ts similarity index 97% rename from packages/stack/src/eql/v3/drizzle/operators.ts rename to packages/stack-drizzle/src/v3/operators.ts index dc85db026..ca9437ecd 100644 --- a/packages/stack/src/eql/v3/drizzle/operators.ts +++ b/packages/stack-drizzle/src/v3/operators.ts @@ -1,4 +1,14 @@ import type { Result } from '@byteslice/result' +import type { AuditConfig } from '@cipherstash/stack/adapter-kit' +import { matchNeedleError } from '@cipherstash/stack/adapter-kit' +import type { + AnyEncryptedV3Column, + AnyV3Table, +} from '@cipherstash/stack/eql/v3' +import type { EncryptionError } from '@cipherstash/stack/errors' +import type { LockContext } from '@cipherstash/stack/identity' +import type { ColumnSchema } from '@cipherstash/stack/schema' +import type { BulkEncryptedData, Encrypted } from '@cipherstash/stack/types' import { and, asc, @@ -16,13 +26,6 @@ import { sql, } from 'drizzle-orm' import type { PgTable } from 'drizzle-orm/pg-core' -import type { AuditConfig } from '@/encryption/operations/base-operation' -import type { AnyEncryptedV3Column, AnyV3Table } from '@/eql/v3' -import type { EncryptionError } from '@/errors' -import type { LockContext } from '@/identity' -import type { ColumnSchema } from '@/schema' -import { matchNeedleError } from '@/schema/match-defaults' -import type { BulkEncryptedData, Encrypted } from '@/types' import { getEqlV3Column } from './column.js' import { extractEncryptionSchemaV3, @@ -387,7 +390,7 @@ export function createEncryptionOperatorsV3( // `result.data` is `BulkEncryptedData` — `{ id?, data: Encrypted | null }[]` // — not `unknown`. The length check stays: a position-stable contract // violation must not silently truncate the predicate. - const encrypted = result.data + const encrypted: BulkEncryptedData = result.data if (encrypted.length !== values.length) { throw operandFailure( ctx, diff --git a/packages/stack/src/eql/v3/drizzle/schema-extraction.ts b/packages/stack-drizzle/src/v3/schema-extraction.ts similarity index 98% rename from packages/stack/src/eql/v3/drizzle/schema-extraction.ts rename to packages/stack-drizzle/src/v3/schema-extraction.ts index 623804ba9..68f8796bb 100644 --- a/packages/stack/src/eql/v3/drizzle/schema-extraction.ts +++ b/packages/stack-drizzle/src/v3/schema-extraction.ts @@ -1,9 +1,9 @@ -import type { PgTable } from 'drizzle-orm/pg-core' import { type AnyEncryptedV3Column, type AnyV3Table, encryptedTable, -} from '@/eql/v3' +} from '@cipherstash/stack/eql/v3' +import type { PgTable } from 'drizzle-orm/pg-core' import { getEqlV3Column } from './column.js' /** Drizzle stashes the SQL table name on this well-known symbol key. */ diff --git a/packages/stack/src/eql/v3/drizzle/sql-dialect.ts b/packages/stack-drizzle/src/v3/sql-dialect.ts similarity index 100% rename from packages/stack/src/eql/v3/drizzle/sql-dialect.ts rename to packages/stack-drizzle/src/v3/sql-dialect.ts diff --git a/packages/stack/src/eql/v3/drizzle/types.ts b/packages/stack-drizzle/src/v3/types.ts similarity index 89% rename from packages/stack/src/eql/v3/drizzle/types.ts rename to packages/stack-drizzle/src/v3/types.ts index b52fe83f3..443382bc9 100644 --- a/packages/stack/src/eql/v3/drizzle/types.ts +++ b/packages/stack-drizzle/src/v3/types.ts @@ -1,4 +1,4 @@ -import { types as v3Types } from '@/eql/v3' +import { types as v3Types } from '@cipherstash/stack/eql/v3' import { makeEqlV3Column } from './column.js' type V3Types = typeof v3Types diff --git a/packages/stack-drizzle/tsconfig.json b/packages/stack-drizzle/tsconfig.json new file mode 100644 index 000000000..2b181374d --- /dev/null +++ b/packages/stack-drizzle/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "lib": ["ES2022", "DOM"], + "target": "ES2022", + "module": "ESNext", + "moduleDetection": "force", + "allowJs": true, + "esModuleInterop": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "customConditions": ["node"], + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "paths": { + "@cipherstash/stack": ["../stack/src/index.ts"], + "@cipherstash/stack/adapter-kit": ["../stack/src/adapter-kit.ts"], + "@cipherstash/stack/encryption": ["../stack/src/encryption/index.ts"], + "@cipherstash/stack/schema": ["../stack/src/schema/index.ts"], + "@cipherstash/stack/identity": ["../stack/src/identity/index.ts"], + "@cipherstash/stack/errors": ["../stack/src/errors/index.ts"], + "@cipherstash/stack/types": ["../stack/src/types-public.ts"], + "@cipherstash/stack/eql/v3": ["../stack/src/eql/v3/index.ts"], + "@cipherstash/stack/v3": ["../stack/src/encryption/v3.ts"], + "@cipherstash/test-kit": ["../test-kit/src/index.ts"], + "@cipherstash/test-kit/suite": ["../test-kit/src/run-family-suite.ts"], + "@cipherstash/test-kit/catalog": ["../test-kit/src/catalog.ts"], + "@/*": ["../stack/src/*"] + } + }, + "include": ["src/**/*.ts", "__tests__/**/*.ts", "integration/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/stack-drizzle/tsup.config.ts b/packages/stack-drizzle/tsup.config.ts new file mode 100644 index 000000000..19ce5967e --- /dev/null +++ b/packages/stack-drizzle/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts', 'src/v3/index.ts'], + outDir: 'dist', + format: ['cjs', 'esm'], + sourcemap: true, + dts: true, + clean: false, + target: 'es2022', + tsconfig: './tsconfig.json', + external: ['@cipherstash/stack', '@cipherstash/protect-ffi', 'drizzle-orm'], +}) diff --git a/packages/stack-drizzle/vitest.config.ts b/packages/stack-drizzle/vitest.config.ts new file mode 100644 index 000000000..6e50a87be --- /dev/null +++ b/packages/stack-drizzle/vitest.config.ts @@ -0,0 +1,34 @@ +import { resolve } from 'node:path' +import { configDefaults, defineConfig } from 'vitest/config' +import { sharedAlias } from '../../vitest.shared' + +export default defineConfig({ + resolve: { + alias: { + ...sharedAlias, + '@cipherstash/stack/adapter-kit': resolve( + __dirname, + '../stack/src/adapter-kit.ts', + ), + // stack source uses its internal `@/`; this package's code never does. + '@/': resolve(__dirname, '../stack/src') + '/', + '@cipherstash/protect-ffi/wasm-inline': resolve( + __dirname, + '../stack/__tests__/helpers/stub-protect-ffi-wasm-inline.ts', + ), + '@cipherstash/auth/wasm-inline': resolve( + __dirname, + '../stack/__tests__/helpers/stub-auth-wasm-inline.ts', + ), + }, + }, + test: { + exclude: [...configDefaults.exclude, 'integration/**'], + testTimeout: 30000, + hookTimeout: 30000, + typecheck: { + tsconfig: './tsconfig.json', + include: ['__tests__/**/*.test-d.ts'], + }, + }, +}) diff --git a/packages/stack-supabase/__tests__/supabase-helpers.test.ts b/packages/stack-supabase/__tests__/supabase-helpers.test.ts index 8ba7ded63..cb3ef2f88 100644 --- a/packages/stack-supabase/__tests__/supabase-helpers.test.ts +++ b/packages/stack-supabase/__tests__/supabase-helpers.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from 'vitest' -import { - addJsonbCastsV3, - parseOrString, - rebuildOrString, -} from '../src/helpers' +import { addJsonbCastsV3, parseOrString, rebuildOrString } from '../src/helpers' import type { DbPendingOrCondition } from '../src/types' // `createdAt` is a renamed property (DB column `created_at`); `email` is a diff --git a/packages/stack-supabase/__tests__/supabase-schema-builder.test.ts b/packages/stack-supabase/__tests__/supabase-schema-builder.test.ts index 4ef404d90..006588bc8 100644 --- a/packages/stack-supabase/__tests__/supabase-schema-builder.test.ts +++ b/packages/stack-supabase/__tests__/supabase-schema-builder.test.ts @@ -1,11 +1,8 @@ -import { describe, expect, it } from 'vitest' import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import { describe, expect, it } from 'vitest' import type { IntrospectionResult } from '../src/introspect' import { groupUnmodelledRows } from '../src/introspect' -import { - mergeDeclaredTables, - synthesizeTables, -} from '../src/schema-builder' +import { mergeDeclaredTables, synthesizeTables } from '../src/schema-builder' const introspection: IntrospectionResult = [ { diff --git a/packages/stack-supabase/__tests__/supabase-v3-builder.test.ts b/packages/stack-supabase/__tests__/supabase-v3-builder.test.ts index f56512971..684fa1f20 100644 --- a/packages/stack-supabase/__tests__/supabase-v3-builder.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-builder.test.ts @@ -1,6 +1,9 @@ -import { describe, expect, it, vi } from 'vitest' import { encryptedTable, types } from '@cipherstash/stack/eql/v3' -import { encryptedColumn, encryptedTable as encryptedTableV2 } from '@cipherstash/stack/schema' +import { + encryptedColumn, + encryptedTable as encryptedTableV2, +} from '@cipherstash/stack/schema' +import { describe, expect, it, vi } from 'vitest' import { encryptedSupabase } from '../src/index.js' import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' import { diff --git a/packages/stack-supabase/__tests__/supabase-v3-factory.test.ts b/packages/stack-supabase/__tests__/supabase-v3-factory.test.ts index 9fbc37e12..66fceb524 100644 --- a/packages/stack-supabase/__tests__/supabase-v3-factory.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-factory.test.ts @@ -1,5 +1,5 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { SupabaseClientLike } from '../src/index.js' import { encryptedSupabaseV3 } from '../src/index.js' import type { IntrospectionData } from '../src/introspect' @@ -23,7 +23,8 @@ vi.mock('../src/introspect', async (importActual) => { }) vi.mock('@cipherstash/stack/encryption', async (importActual) => { - const actual = await importActual() + const actual = + await importActual() return { ...actual, Encryption: (cfg: unknown) => encryptionMock(cfg) } }) diff --git a/packages/stack-supabase/__tests__/supabase-v3-matrix.test.ts b/packages/stack-supabase/__tests__/supabase-v3-matrix.test.ts index 917cc8215..cb03776de 100644 --- a/packages/stack-supabase/__tests__/supabase-v3-matrix.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-matrix.test.ts @@ -19,15 +19,8 @@ * proves Postgres accepts what this file pins. */ -import { describe, expect, it } from 'vitest' import type { AnyV3Table } from '@cipherstash/stack/eql/v3' import { encryptedTable } from '@cipherstash/stack/eql/v3' -import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' -import { - createMockEncryptionClient, - createMockSupabase, - isFakeEnvelope, -} from './helpers/supabase-mock' import { type DomainSpec, type EqlV3TypeName, @@ -35,6 +28,13 @@ import { typedEntries, V3_MATRIX, } from '@cipherstash/test-kit/catalog' +import { describe, expect, it } from 'vitest' +import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' +import { + createMockEncryptionClient, + createMockSupabase, + isFakeEnvelope, +} from './helpers/supabase-mock' const matrixEntries = typedEntries(V3_MATRIX) diff --git a/packages/stack-supabase/__tests__/supabase-v3-select-star.test.ts b/packages/stack-supabase/__tests__/supabase-v3-select-star.test.ts index edf207c41..1fd76f220 100644 --- a/packages/stack-supabase/__tests__/supabase-v3-select-star.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-select-star.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it } from 'vitest' import type { EncryptionClient } from '@cipherstash/stack/encryption' import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import { describe, expect, it } from 'vitest' import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' /** diff --git a/packages/stack-supabase/__tests__/supabase-v3-wire.test.ts b/packages/stack-supabase/__tests__/supabase-v3-wire.test.ts index 4bc7ea6c1..61953f6a7 100644 --- a/packages/stack-supabase/__tests__/supabase-v3-wire.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-wire.test.ts @@ -8,8 +8,8 @@ * assert the array handed to `.in()` and cannot see that. */ -import { describe, expect, it } from 'vitest' import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import { describe, expect, it } from 'vitest' import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' import { createWirePostgrest } from './helpers/postgrest-wire' import { createMockEncryptionClient } from './helpers/supabase-mock' diff --git a/packages/stack-supabase/__tests__/supabase-v3.test-d.ts b/packages/stack-supabase/__tests__/supabase-v3.test-d.ts index 1f0fc6e9f..52560e9a7 100644 --- a/packages/stack-supabase/__tests__/supabase-v3.test-d.ts +++ b/packages/stack-supabase/__tests__/supabase-v3.test-d.ts @@ -1,6 +1,13 @@ +import { + encryptedTable, + type InferPlaintext, + types, +} from '@cipherstash/stack/eql/v3' +import { + encryptedColumn, + encryptedTable as v2EncryptedTable, +} from '@cipherstash/stack/schema' import { describe, expectTypeOf, it } from 'vitest' -import { encryptedTable, type InferPlaintext, types } from '@cipherstash/stack/eql/v3' -import { encryptedColumn, encryptedTable as v2EncryptedTable } from '@cipherstash/stack/schema' import { type EncryptedQueryBuilderV3, type EncryptedSupabaseResponse, diff --git a/packages/stack-supabase/__tests__/supabase-verify.test.ts b/packages/stack-supabase/__tests__/supabase-verify.test.ts index 72948f63a..c0e08260c 100644 --- a/packages/stack-supabase/__tests__/supabase-verify.test.ts +++ b/packages/stack-supabase/__tests__/supabase-verify.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest' import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import { describe, expect, it } from 'vitest' import type { IntrospectionResult } from '../src/introspect' import { verifyDeclaredSchemas } from '../src/verify' diff --git a/packages/stack-supabase/__tests__/supabase.test.ts b/packages/stack-supabase/__tests__/supabase.test.ts index 928869683..1023bc387 100644 --- a/packages/stack-supabase/__tests__/supabase.test.ts +++ b/packages/stack-supabase/__tests__/supabase.test.ts @@ -1,10 +1,10 @@ import 'dotenv/config' +import { Encryption } from '@cipherstash/stack' +import { encryptedColumn, encryptedTable } from '@cipherstash/stack/schema' import { createClient } from '@supabase/supabase-js' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { Encryption } from '@cipherstash/stack' -import { encryptedColumn, encryptedTable } from '@cipherstash/stack/schema' import { encryptedSupabase } from '../src/index.js' // supabase.test.ts needs a live Supabase project, so the suite is skipped diff --git a/packages/stack-supabase/src/helpers.ts b/packages/stack-supabase/src/helpers.ts index 014c37fb1..9c6d69c68 100644 --- a/packages/stack-supabase/src/helpers.ts +++ b/packages/stack-supabase/src/helpers.ts @@ -1,4 +1,7 @@ -import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/stack/schema' +import type { + EncryptedTable, + EncryptedTableColumn, +} from '@cipherstash/stack/schema' import type { QueryTypeName } from '@cipherstash/stack/types' import type { DbFilterString, diff --git a/packages/stack-supabase/src/index.ts b/packages/stack-supabase/src/index.ts index 4255a2a22..4dbb70dff 100644 --- a/packages/stack-supabase/src/index.ts +++ b/packages/stack-supabase/src/index.ts @@ -1,5 +1,8 @@ import { Encryption } from '@cipherstash/stack' -import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/stack/schema' +import type { + EncryptedTable, + EncryptedTableColumn, +} from '@cipherstash/stack/schema' import type { UnmodelledColumn } from './introspect' import { introspect } from './introspect' import { EncryptedQueryBuilderImpl } from './query-builder' diff --git a/packages/stack-supabase/src/query-builder-v3.ts b/packages/stack-supabase/src/query-builder-v3.ts index c814a9ec2..b1bb131df 100644 --- a/packages/stack-supabase/src/query-builder-v3.ts +++ b/packages/stack-supabase/src/query-builder-v3.ts @@ -1,6 +1,10 @@ +import { + DATE_LIKE_CASTS, + EncryptedV3Column, + logger, +} from '@cipherstash/stack/adapter-kit' import type { EncryptionClient } from '@cipherstash/stack/encryption' import type { AnyV3Table } from '@cipherstash/stack/eql/v3' -import { DATE_LIKE_CASTS, EncryptedV3Column } from '@cipherstash/stack/adapter-kit' import type { ColumnSchema, EncryptedTable, @@ -13,7 +17,6 @@ import type { QueryTypeName, ScalarQueryTerm, } from '@cipherstash/stack/types' -import { logger } from '@cipherstash/stack/adapter-kit' import { addJsonbCastsV3, selectKeyToDbV3 } from './helpers' import { EncryptedQueryBuilderImpl, diff --git a/packages/stack-supabase/src/query-builder.ts b/packages/stack-supabase/src/query-builder.ts index e58f3ece0..29b80903d 100644 --- a/packages/stack-supabase/src/query-builder.ts +++ b/packages/stack-supabase/src/query-builder.ts @@ -1,12 +1,16 @@ import type { JsPlaintext } from '@cipherstash/protect-ffi' -import type { EncryptionClient } from '@cipherstash/stack/encryption' +import type { AuditConfig } from '@cipherstash/stack/adapter-kit' import { bulkModelsToEncryptedPgComposites, + logger, modelToEncryptedPgComposites, } from '@cipherstash/stack/adapter-kit' -import type { AuditConfig } from '@cipherstash/stack/adapter-kit' +import type { EncryptionClient } from '@cipherstash/stack/encryption' import type { LockContext } from '@cipherstash/stack/identity' -import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/stack/schema' +import type { + EncryptedTable, + EncryptedTableColumn, +} from '@cipherstash/stack/schema' import { EncryptedColumn } from '@cipherstash/stack/schema' import type { BuildableQueryColumn, @@ -14,7 +18,6 @@ import type { QueryTypeName, ScalarQueryTerm, } from '@cipherstash/stack/types' -import { logger } from '@cipherstash/stack/adapter-kit' import { addJsonbCasts, formatContainmentOperand, diff --git a/packages/stack-supabase/src/schema-builder.ts b/packages/stack-supabase/src/schema-builder.ts index c2eb8466b..ff6dec8b2 100644 --- a/packages/stack-supabase/src/schema-builder.ts +++ b/packages/stack-supabase/src/schema-builder.ts @@ -1,6 +1,6 @@ -import { type AnyV3Table, EncryptedTable } from '@cipherstash/stack/eql/v3' import type { AnyEncryptedV3Column } from '@cipherstash/stack/adapter-kit' import { factoryForDomain } from '@cipherstash/stack/adapter-kit' +import { type AnyV3Table, EncryptedTable } from '@cipherstash/stack/eql/v3' import type { IntrospectionResult } from './introspect' /** A record of declared v3 tables, keyed by table name. */ diff --git a/packages/stack-supabase/src/types.ts b/packages/stack-supabase/src/types.ts index d55f06cf9..41cf4ca98 100644 --- a/packages/stack-supabase/src/types.ts +++ b/packages/stack-supabase/src/types.ts @@ -1,5 +1,5 @@ -import type { EncryptionClient } from '@cipherstash/stack/encryption' import type { AuditConfig } from '@cipherstash/stack/adapter-kit' +import type { EncryptionClient } from '@cipherstash/stack/encryption' import type { AnyV3Table, EqlTypeForColumn, @@ -8,7 +8,10 @@ import type { } from '@cipherstash/stack/eql/v3' import type { EncryptionError } from '@cipherstash/stack/errors' import type { LockContext } from '@cipherstash/stack/identity' -import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/stack/schema' +import type { + EncryptedTable, + EncryptedTableColumn, +} from '@cipherstash/stack/schema' import type { ClientConfig } from '@cipherstash/stack/types' import type { V3Schemas } from './schema-builder' @@ -628,8 +631,8 @@ export interface SupabaseClientLike { // Re-export for convenience // --------------------------------------------------------------------------- -export type { EncryptionClient } from '@cipherstash/stack/encryption' export type { AuditConfig } from '@cipherstash/stack/adapter-kit' +export type { EncryptionClient } from '@cipherstash/stack/encryption' export type { LockContext } from '@cipherstash/stack/identity' export type { EncryptedColumn, diff --git a/packages/stack-supabase/tsconfig.json b/packages/stack-supabase/tsconfig.json index 79db12189..f4f2dacc5 100644 --- a/packages/stack-supabase/tsconfig.json +++ b/packages/stack-supabase/tsconfig.json @@ -27,7 +27,8 @@ "@cipherstash/stack/eql/v3": ["../stack/src/eql/v3/index.ts"], "@cipherstash/test-kit": ["../test-kit/src/index.ts"], "@cipherstash/test-kit/suite": ["../test-kit/src/run-family-suite.ts"], - "@cipherstash/test-kit/catalog": ["../test-kit/src/catalog.ts"] + "@cipherstash/test-kit/catalog": ["../test-kit/src/catalog.ts"], + "@/*": ["../stack/src/*"] } }, "include": ["src/**/*.ts", "__tests__/**/*.ts", "integration/**/*.ts"], diff --git a/packages/stack/__tests__/v3-matrix/needle-for.test.ts b/packages/stack/__tests__/v3-matrix/needle-for.test.ts index 21b2a6e6f..87f68e365 100644 --- a/packages/stack/__tests__/v3-matrix/needle-for.test.ts +++ b/packages/stack/__tests__/v3-matrix/needle-for.test.ts @@ -1,7 +1,7 @@ +import { needleFor } from '@cipherstash/test-kit' import { describe, expect, it } from 'vitest' import { matchNeedleError } from '@/schema/match-defaults' import { V3_MATRIX } from './catalog' -import { needleFor } from './needle-for' const MATCH_BLOCK = { tokenizer: { kind: 'ngram', token_length: 3 } } as const diff --git a/packages/stack/package.json b/packages/stack/package.json index da40d220b..7b0713905 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -57,18 +57,12 @@ "eql/v3": [ "./dist/eql/v3/index.d.ts" ], - "eql/v3/drizzle": [ - "./dist/eql/v3/drizzle/index.d.ts" - ], "v3": [ "./dist/encryption/v3.d.ts" ], "types": [ "./dist/types-public.d.ts" ], - "drizzle": [ - "./dist/drizzle/index.d.ts" - ], "dynamodb": [ "./dist/dynamodb/index.d.ts" ], @@ -128,16 +122,6 @@ "default": "./dist/eql/v3/index.cjs" } }, - "./eql/v3/drizzle": { - "import": { - "types": "./dist/eql/v3/drizzle/index.d.ts", - "default": "./dist/eql/v3/drizzle/index.js" - }, - "require": { - "types": "./dist/eql/v3/drizzle/index.d.cts", - "default": "./dist/eql/v3/drizzle/index.cjs" - } - }, "./v3": { "import": { "types": "./dist/encryption/v3.d.ts", @@ -158,16 +142,6 @@ "default": "./dist/types-public.cjs" } }, - "./drizzle": { - "import": { - "types": "./dist/drizzle/index.d.ts", - "default": "./dist/drizzle/index.js" - }, - "require": { - "types": "./dist/drizzle/index.d.cts", - "default": "./dist/drizzle/index.cjs" - } - }, "./dynamodb": { "import": { "types": "./dist/dynamodb/index.d.ts", diff --git a/packages/stack/tsup.config.ts b/packages/stack/tsup.config.ts index 67ef65716..c482fda6e 100644 --- a/packages/stack/tsup.config.ts +++ b/packages/stack/tsup.config.ts @@ -15,8 +15,6 @@ export default defineConfig([ 'src/identity/index.ts', 'src/schema/index.ts', 'src/eql/v3/index.ts', - 'src/eql/v3/drizzle/index.ts', - 'src/drizzle/index.ts', 'src/dynamodb/index.ts', 'src/encryption/index.ts', 'src/encryption/v3.ts', diff --git a/packages/test-kit/src/index.ts b/packages/test-kit/src/index.ts index 7575c1a6a..ecdc27562 100644 --- a/packages/test-kit/src/index.ts +++ b/packages/test-kit/src/index.ts @@ -42,6 +42,7 @@ export { type FamilyName, } from './families.ts' export { type DbVariant, dbVariant, installEqlV3 } from './install.ts' +export { needleFor } from './needle-for.ts' export { negativeOps, type Plain, diff --git a/packages/stack/__tests__/v3-matrix/needle-for.ts b/packages/test-kit/src/needle-for.ts similarity index 94% rename from packages/stack/__tests__/v3-matrix/needle-for.ts rename to packages/test-kit/src/needle-for.ts index cfde47563..ebec96e7f 100644 --- a/packages/stack/__tests__/v3-matrix/needle-for.ts +++ b/packages/test-kit/src/needle-for.ts @@ -1,4 +1,4 @@ -import { matchNeedleError } from '@/schema/match-defaults' +import { matchNeedleError } from '@cipherstash/stack/adapter-kit' import type { V3_MATRIX } from './catalog' type MatrixSpec = (typeof V3_MATRIX)[keyof typeof V3_MATRIX] diff --git a/packages/test-kit/tsconfig.json b/packages/test-kit/tsconfig.json index 0bd5cf053..0730f54fb 100644 --- a/packages/test-kit/tsconfig.json +++ b/packages/test-kit/tsconfig.json @@ -34,6 +34,7 @@ // Resolving stack to source means we also inherit stack's OWN internal // alias: `src/eql/v3/columns.ts` imports `@/schema`. Without this the // kit's typecheck fails inside stack's sources rather than in the kit. + "@cipherstash/stack/adapter-kit": ["../stack/src/adapter-kit.ts"], "@/*": ["../stack/src/*"] } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a95dbaa93..d93da5629 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -676,6 +676,40 @@ importers: specifier: catalog:repo version: 0.41.0 + packages/stack-drizzle: + dependencies: + '@byteslice/result': + specifier: 0.2.0 + version: 0.2.0 + '@cipherstash/stack': + specifier: workspace:* + version: link:../stack + devDependencies: + '@cipherstash/protect-ffi': + specifier: 0.29.0 + version: 0.29.0 + '@cipherstash/test-kit': + specifier: workspace:* + version: link:../test-kit + dotenv: + specifier: 17.4.2 + version: 17.4.2 + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(@types/pg@8.20.0)(gel@2.2.0)(mysql2@3.16.0)(pg@8.21.0)(postgres@3.4.9) + postgres: + specifier: ^3.4.8 + version: 3.4.9 + tsup: + specifier: catalog:repo + version: 8.5.1(jiti@2.7.0)(postcss@8.5.14)(tsx@4.22.1)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: catalog:repo + version: 5.9.3 + vitest: + specifier: catalog:repo + version: 3.2.6(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.22.1)(yaml@2.9.0) + packages/stack-supabase: dependencies: '@cipherstash/stack': @@ -5863,6 +5897,14 @@ snapshots: pg: 8.20.0 postgres: 3.4.9 + drizzle-orm@0.45.2(@types/pg@8.20.0)(gel@2.2.0)(mysql2@3.16.0)(pg@8.21.0)(postgres@3.4.9): + optionalDependencies: + '@types/pg': 8.20.0 + gel: 2.2.0 + mysql2: 3.16.0 + pg: 8.21.0 + postgres: 3.4.9 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 diff --git a/vitest.shared.ts b/vitest.shared.ts index 9818364e7..1fe3f455a 100644 --- a/vitest.shared.ts +++ b/vitest.shared.ts @@ -51,12 +51,34 @@ export const sharedAlias: Record = { repoRoot, 'packages/stack/src/encryption/index.ts', ), + '@cipherstash/stack/types': resolve( + repoRoot, + 'packages/stack/src/types-public.ts', + ), + '@cipherstash/stack/errors': resolve( + repoRoot, + 'packages/stack/src/errors/index.ts', + ), + '@cipherstash/stack/identity': resolve( + repoRoot, + 'packages/stack/src/identity/index.ts', + ), // The Supabase adapter now lives in its own package (was // `@cipherstash/stack/supabase`); resolve it to source too. '@cipherstash/stack-supabase': resolve( repoRoot, 'packages/stack-supabase/src/index.ts', ), + // The Drizzle adapter package (was `@cipherstash/stack/drizzle` + + // `@cipherstash/stack/eql/v3/drizzle`). `/v3` first — longest prefix wins. + '@cipherstash/stack-drizzle/v3': resolve( + repoRoot, + 'packages/stack-drizzle/src/v3/index.ts', + ), + '@cipherstash/stack-drizzle': resolve( + repoRoot, + 'packages/stack-drizzle/src/index.ts', + ), // Bare entry LAST: it is a prefix of every subpath above, and Vite matches in // order, so the subpaths must win first. Both sibling tsconfigs map bare // `@cipherstash/stack` → `src/index.ts`; without the runtime match here a From bd91e9335bd935df2fc023a8d5a8f31247396322 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 11:22:17 +1000 Subject: [PATCH 07/17] fix(test-kit): keep needle-for out of the encryption/helpers typecheck chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moving needle-for into test-kit subjected it to test-kit's strict `tsc -p` (stricter than stack's scoped typecheck, which never checked this file). Import matchNeedleError directly from @/schema/match-defaults instead of via @cipherstash/stack/adapter-kit — the latter re-exports encryption/helpers, pulling jsonb.ts's pre-existing loose types into the graph. Also guard the `.match` index access, which strict tsc rejects on the domain-index union. Fixes the Run Tests (Node 22/24) job. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- packages/test-kit/src/needle-for.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/test-kit/src/needle-for.ts b/packages/test-kit/src/needle-for.ts index ebec96e7f..2ca881ee5 100644 --- a/packages/test-kit/src/needle-for.ts +++ b/packages/test-kit/src/needle-for.ts @@ -1,4 +1,6 @@ -import { matchNeedleError } from '@cipherstash/stack/adapter-kit' +// Import directly from stack's schema helper (not `@cipherstash/stack/adapter-kit`) +// so test-kit's strict `tsc` graph does not pull in the encryption/helpers chain. +import { matchNeedleError } from '@/schema/match-defaults' import type { V3_MATRIX } from './catalog' type MatrixSpec = (typeof V3_MATRIX)[keyof typeof V3_MATRIX] @@ -15,7 +17,7 @@ type MatrixSpec = (typeof V3_MATRIX)[keyof typeof V3_MATRIX] * line. It would also ignore a domain that raises its own `token_length`. */ export const needleFor = (spec: MatrixSpec): string => { - const match = spec.indexes.match ?? {} + const match = 'match' in spec.indexes ? (spec.indexes.match ?? {}) : {} const needle = spec.samples.find( (sample) => typeof sample === 'string' && !matchNeedleError(sample, match), ) From 41d9210d8cd8c244d2d96709ca031cc9ac17abcb Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 11:33:13 +1000 Subject: [PATCH 08/17] test(adapters): relocate integration suites + shared harness (#627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the shared integration harness (global-setup, no-skips-reporter, clerk helper) into @cipherstash/test-kit/src/integration, so both adapter packages share one copy. Relocates the Supabase integration suite (+ pgrest/v3-envelope helpers) into stack-supabase/integration, and the Drizzle suites + the Clerk-federated lock-context suite into stack-drizzle/integration, each with a vitest config wiring the test-kit harness. stack keeps the adapter-agnostic shared/ suites and the model-path identity/matrix-identity suite, repointed at the test-kit harness. Rewires integration-{supabase,drizzle}.yml to run each package's test:integration plus stack's shared/(+identity) suites, and updates their path triggers to the new packages. Scopes each adapter package's vitest typecheck to a tsconfig.typecheck.json (.test-d.ts only) so the intentionally loose .test.ts/integration files — now precisely typed via the @/* mapping — don't fail the type gate. Verified locally: all three packages build (JS+DTS), unit + type tests green, code:check green, frozen-lockfile clean; integration suites resolve their imports (reach requireIntegrationEnv). Integration execution itself runs in CI (needs creds/DB/PostgREST). Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .github/workflows/integration-drizzle.yml | 11 ++++- .github/workflows/integration-supabase.yml | 14 ++++-- .../integration}/adapter.ts | 6 +-- .../integration}/families.integration.test.ts | 0 .../json-contains.integration.test.ts | 7 +-- .../lock-context.integration.test.ts | 8 ++-- .../null-persistence.integration.test.ts | 7 +-- .../relational.integration.test.ts | 6 +-- .../integration/vitest.config.ts | 48 +++++++++++++++++++ packages/stack-drizzle/tsconfig.json | 3 ++ .../stack-drizzle/tsconfig.typecheck.json | 4 ++ packages/stack-drizzle/vitest.config.ts | 2 +- .../integration}/adapter.ts | 6 +-- .../integration}/families.integration.test.ts | 0 .../integration}/grants.integration.test.ts | 2 +- .../integration/helpers/pgrest.ts | 0 .../integration}/helpers/v3-envelope.ts | 0 .../introspect.integration.test.ts | 6 +-- .../select-alias.integration.test.ts | 6 +-- .../integration/vitest.config.ts | 48 +++++++++++++++++++ .../integration}/wire.integration.test.ts | 13 ++--- .../stack-supabase/tsconfig.typecheck.json | 4 ++ packages/stack-supabase/vitest.config.ts | 2 +- .../matrix-identity.integration.test.ts | 2 +- packages/stack/integration/vitest.config.ts | 9 +++- packages/stack/tsconfig.json | 3 ++ packages/test-kit/package.json | 2 + .../src/integration}/clerk.ts | 0 .../src}/integration/global-setup.ts | 2 +- .../src}/integration/no-skips-reporter.ts | 0 pnpm-lock.yaml | 3 ++ vitest.shared.ts | 4 ++ 32 files changed, 183 insertions(+), 45 deletions(-) rename packages/{stack/integration/drizzle-v3 => stack-drizzle/integration}/adapter.ts (98%) rename packages/{stack/integration/drizzle-v3 => stack-drizzle/integration}/families.integration.test.ts (100%) rename packages/{stack/integration/drizzle-v3 => stack-drizzle/integration}/json-contains.integration.test.ts (96%) rename packages/{stack/integration/identity => stack-drizzle/integration}/lock-context.integration.test.ts (98%) rename packages/{stack/integration/drizzle-v3 => stack-drizzle/integration}/null-persistence.integration.test.ts (97%) rename packages/{stack/integration/drizzle-v3 => stack-drizzle/integration}/relational.integration.test.ts (99%) create mode 100644 packages/stack-drizzle/integration/vitest.config.ts create mode 100644 packages/stack-drizzle/tsconfig.typecheck.json rename packages/{stack/integration/supabase => stack-supabase/integration}/adapter.ts (97%) rename packages/{stack/integration/supabase => stack-supabase/integration}/families.integration.test.ts (100%) rename packages/{stack/integration/supabase => stack-supabase/integration}/grants.integration.test.ts (99%) rename packages/{stack => stack-supabase}/integration/helpers/pgrest.ts (100%) rename packages/{stack/__tests__ => stack-supabase/integration}/helpers/v3-envelope.ts (100%) rename packages/{stack/integration/supabase => stack-supabase/integration}/introspect.integration.test.ts (97%) rename packages/{stack/integration/supabase => stack-supabase/integration}/select-alias.integration.test.ts (95%) create mode 100644 packages/stack-supabase/integration/vitest.config.ts rename packages/{stack/integration/supabase => stack-supabase/integration}/wire.integration.test.ts (98%) create mode 100644 packages/stack-supabase/tsconfig.typecheck.json rename packages/{stack/integration/helpers => test-kit/src/integration}/clerk.ts (100%) rename packages/{stack => test-kit/src}/integration/global-setup.ts (96%) rename packages/{stack => test-kit/src}/integration/no-skips-reporter.ts (100%) diff --git a/.github/workflows/integration-drizzle.yml b/.github/workflows/integration-drizzle.yml index 526c1c1b7..9bb406c02 100644 --- a/.github/workflows/integration-drizzle.yml +++ b/.github/workflows/integration-drizzle.yml @@ -18,6 +18,7 @@ on: branches: [main] paths: - 'packages/stack/src/eql/v3/**' + - 'packages/stack-drizzle/**' # Source layers the adapter's encoding/round-trip rests on: a break here # (not just under src/eql/v3) can produce wrong rows, so trigger the live # suite that would catch it. @@ -37,6 +38,7 @@ on: # Repeated verbatim: GitHub Actions does not support YAML anchors/aliases. paths: - 'packages/stack/src/eql/v3/**' + - 'packages/stack-drizzle/**' # Source layers the adapter's encoding/round-trip rests on: a break here # (not just under src/eql/v3) can produce wrong rows, so trigger the live # suite that would catch it. @@ -108,9 +110,13 @@ jobs: # (they need CLERK_MACHINE_TOKEN + a workspace with the Clerk issuer # registered); the Supabase adapter suites are not run here — they have # their own job. + # The Drizzle adapter suites (incl. the Clerk-federated lock-context one) + # now live in @cipherstash/stack-drizzle (run below via its own + # test:integration). This glob scopes what still lives in @cipherstash/stack: + # the adapter-agnostic `shared/` core suites and the model-path + # `identity/matrix-identity` suite. CS_IT_SUITE: >- integration/shared/**/*.integration.test.ts, - integration/drizzle-v3/**/*.integration.test.ts, integration/identity/**/*.integration.test.ts steps: @@ -135,6 +141,9 @@ jobs: # `stash eql install --eql-version 3`, so an installer regression fails # here rather than hiding behind a test-only SQL apply. - name: Drizzle v3 integration suites + run: pnpm --filter @cipherstash/stack-drizzle run test:integration + + - name: Shared core + identity integration suites run: pnpm --filter @cipherstash/stack run test:integration - name: Stop ${{ matrix.db }} diff --git a/.github/workflows/integration-supabase.yml b/.github/workflows/integration-supabase.yml index 6143e7f9c..7100e08db 100644 --- a/.github/workflows/integration-supabase.yml +++ b/.github/workflows/integration-supabase.yml @@ -12,7 +12,7 @@ on: push: branches: [main] paths: - - 'packages/stack/src/supabase/**' + - 'packages/stack-supabase/**' - 'packages/stack/src/eql/v3/**' # Source layers the adapter's encoding/round-trip rests on: a break here # (not just under src/supabase) can produce wrong wire output or rows, so @@ -30,7 +30,7 @@ on: pull_request: branches: ['**'] paths: - - 'packages/stack/src/supabase/**' + - 'packages/stack-supabase/**' - 'packages/stack/src/eql/v3/**' # Source layers the adapter's encoding/round-trip rests on: a break here # (not just under src/supabase) can produce wrong wire output or rows, so @@ -84,9 +84,12 @@ jobs: # # `integration/identity/` runs on the Drizzle job (it needs Postgres + # Drizzle, not PostgREST), so it is intentionally not listed here. + # The Supabase adapter suites now live in @cipherstash/stack-supabase (run + # below via its own test:integration). This glob scopes the adapter-agnostic + # `shared/` core suites that still live in @cipherstash/stack and run against + # this job's Postgres too. CS_IT_SUITE: >- - integration/shared/**/*.integration.test.ts, - integration/supabase/**/*.integration.test.ts + integration/shared/**/*.integration.test.ts steps: - uses: actions/checkout@v6 @@ -110,6 +113,9 @@ jobs: # `stash eql install --eql-version 3 --supabase --direct`, so an installer # regression fails here rather than hiding behind a test-only SQL apply. - name: Supabase v3 integration suites + run: pnpm --filter @cipherstash/stack-supabase run test:integration + + - name: Shared core integration suites (against Supabase Postgres) run: pnpm --filter @cipherstash/stack run test:integration - name: Stop ${{ matrix.db }} diff --git a/packages/stack/integration/drizzle-v3/adapter.ts b/packages/stack-drizzle/integration/adapter.ts similarity index 98% rename from packages/stack/integration/drizzle-v3/adapter.ts rename to packages/stack-drizzle/integration/adapter.ts index 6cc35a172..496c49ff2 100644 --- a/packages/stack/integration/drizzle-v3/adapter.ts +++ b/packages/stack-drizzle/integration/adapter.ts @@ -1,3 +1,4 @@ +import { EncryptionV3 } from '@cipherstash/stack/v3' import { databaseUrl, type IntegrationAdapter, @@ -10,12 +11,11 @@ import { asc, type SQL } from 'drizzle-orm' import { type PgTable, pgTable, text } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' -import { EncryptionV3 } from '@/encryption/v3' +import { makeEqlV3Column } from '../src/v3/column' import { createEncryptionOperatorsV3, extractEncryptionSchemaV3, -} from '@/eql/v3/drizzle' -import { makeEqlV3Column } from '@/eql/v3/drizzle/column' +} from '../src/v3/index.js' /** * The Drizzle v3 adapter under real ZeroKMS ciphertext. diff --git a/packages/stack/integration/drizzle-v3/families.integration.test.ts b/packages/stack-drizzle/integration/families.integration.test.ts similarity index 100% rename from packages/stack/integration/drizzle-v3/families.integration.test.ts rename to packages/stack-drizzle/integration/families.integration.test.ts diff --git a/packages/stack/integration/drizzle-v3/json-contains.integration.test.ts b/packages/stack-drizzle/integration/json-contains.integration.test.ts similarity index 96% rename from packages/stack/integration/drizzle-v3/json-contains.integration.test.ts rename to packages/stack-drizzle/integration/json-contains.integration.test.ts index ce409bace..c955f7d6b 100644 --- a/packages/stack/integration/drizzle-v3/json-contains.integration.test.ts +++ b/packages/stack-drizzle/integration/json-contains.integration.test.ts @@ -7,19 +7,20 @@ * `encryptQuery` — asserting it returns exactly the rows whose document contains * the sub-object (jsonb `@>` semantics), and excludes the rest. */ + +import type { JsonDocument } from '@cipherstash/stack/eql/v3' +import { EncryptionV3 } from '@cipherstash/stack/v3' import { databaseUrl, unwrapResult } from '@cipherstash/test-kit' import { and, asc, eq, type SQL } from 'drizzle-orm' import { integer, pgTable, text } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { EncryptionV3 } from '@/encryption/v3' -import type { JsonDocument } from '@/eql/v3' import { createEncryptionOperatorsV3, extractEncryptionSchemaV3, types, -} from '@/eql/v3/drizzle' +} from '../src/v3/index.js' const sqlClient = postgres(databaseUrl(), { prepare: false }) diff --git a/packages/stack/integration/identity/lock-context.integration.test.ts b/packages/stack-drizzle/integration/lock-context.integration.test.ts similarity index 98% rename from packages/stack/integration/identity/lock-context.integration.test.ts rename to packages/stack-drizzle/integration/lock-context.integration.test.ts index ad4fc52d5..312bcefde 100644 --- a/packages/stack/integration/identity/lock-context.integration.test.ts +++ b/packages/stack-drizzle/integration/lock-context.integration.test.ts @@ -33,19 +33,19 @@ * control. */ import { OidcFederationStrategy } from '@cipherstash/auth' +import { EncryptionV3 } from '@cipherstash/stack/v3' import { databaseUrl, unwrapResult, V3_MATRIX } from '@cipherstash/test-kit' +import { clerkJwtProvider } from '@cipherstash/test-kit/integration-clerk' import { and, asc as drizzleAsc, eq as drizzleEq, type SQL } from 'drizzle-orm' import { integer, pgTable, text } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { EncryptionV3 } from '@/encryption/v3' +import { makeEqlV3Column } from '../src/v3/column' import { createEncryptionOperatorsV3, extractEncryptionSchemaV3, -} from '@/eql/v3/drizzle' -import { makeEqlV3Column } from '@/eql/v3/drizzle/column' -import { clerkJwtProvider } from '../helpers/clerk' +} from '../src/v3/index.js' const sqlClient = postgres(databaseUrl(), { prepare: false }) diff --git a/packages/stack/integration/drizzle-v3/null-persistence.integration.test.ts b/packages/stack-drizzle/integration/null-persistence.integration.test.ts similarity index 97% rename from packages/stack/integration/drizzle-v3/null-persistence.integration.test.ts rename to packages/stack-drizzle/integration/null-persistence.integration.test.ts index 84e96158e..6534c60ac 100644 --- a/packages/stack/integration/drizzle-v3/null-persistence.integration.test.ts +++ b/packages/stack-drizzle/integration/null-persistence.integration.test.ts @@ -12,18 +12,19 @@ * the NULL row, `isNotNull` selects the present row, the NULL cell reads back * as SQL NULL, and the present cell still decrypts to its plaintext. */ + +import { EncryptionV3 } from '@cipherstash/stack/v3' import { databaseUrl, V3_MATRIX } from '@cipherstash/test-kit' import { and, asc as drizzleAsc, eq as drizzleEq, type SQL } from 'drizzle-orm' import { integer, pgTable, text } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { EncryptionV3 } from '@/encryption/v3' +import { makeEqlV3Column } from '../src/v3/column' import { createEncryptionOperatorsV3, extractEncryptionSchemaV3, -} from '@/eql/v3/drizzle' -import { makeEqlV3Column } from '@/eql/v3/drizzle/column' +} from '../src/v3/index.js' const sqlClient = postgres(databaseUrl(), { prepare: false }) diff --git a/packages/stack/integration/drizzle-v3/relational.integration.test.ts b/packages/stack-drizzle/integration/relational.integration.test.ts similarity index 99% rename from packages/stack/integration/drizzle-v3/relational.integration.test.ts rename to packages/stack-drizzle/integration/relational.integration.test.ts index 09c713428..933c9bc21 100644 --- a/packages/stack/integration/drizzle-v3/relational.integration.test.ts +++ b/packages/stack-drizzle/integration/relational.integration.test.ts @@ -19,6 +19,7 @@ * `stash eql install`. This suite throws rather than skips when unconfigured. */ +import { EncryptionV3 } from '@cipherstash/stack/v3' import { type DomainSpec, databaseUrl, @@ -42,14 +43,13 @@ import { integer, pgTable, text } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { EncryptionV3 } from '@/encryption/v3' +import { makeEqlV3Column } from '../src/v3/column' import { createEncryptionOperatorsV3, EncryptionOperatorError, extractEncryptionSchemaV3, types as v3drizzle, -} from '@/eql/v3/drizzle' -import { makeEqlV3Column } from '@/eql/v3/drizzle/column' +} from '../src/v3/index.js' const sqlClient = postgres(databaseUrl(), { prepare: false }) diff --git a/packages/stack-drizzle/integration/vitest.config.ts b/packages/stack-drizzle/integration/vitest.config.ts new file mode 100644 index 000000000..7ec6aebf1 --- /dev/null +++ b/packages/stack-drizzle/integration/vitest.config.ts @@ -0,0 +1,48 @@ +import { resolve } from 'node:path' +import { defineConfig } from 'vitest/config' +import { sharedAlias } from '../../../vitest.shared' + +/** + * Integration suites for the Drizzle adapter: real ZeroKMS, real Postgres, real + * PostgREST. Runs only under `test:integration` (its own CI job). The shared + * harness — `global-setup` (installs EQL v3) and the no-skips reporter — lives in + * `@cipherstash/test-kit/src/integration` so both adapter packages share one copy. + */ +const TEST_KIT_INT = resolve(__dirname, '../../test-kit/src/integration') + +export default defineConfig({ + resolve: { + alias: { + ...sharedAlias, + '@cipherstash/stack/adapter-kit': resolve( + __dirname, + '../../stack/src/adapter-kit.ts', + ), + '@/': resolve(__dirname, '../../stack/src') + '/', + '@cipherstash/protect-ffi/wasm-inline': resolve( + __dirname, + '../../stack/__tests__/helpers/stub-protect-ffi-wasm-inline.ts', + ), + '@cipherstash/auth/wasm-inline': resolve( + __dirname, + '../../stack/__tests__/helpers/stub-auth-wasm-inline.ts', + ), + }, + }, + test: { + root: resolve(__dirname, '..'), + include: ['integration/**/*.integration.test.ts'], + globalSetup: [resolve(TEST_KIT_INT, 'global-setup.ts')], + server: { + deps: { + inline: [/packages\/test-kit/], + }, + }, + testTimeout: 60_000, + hookTimeout: 180_000, + fileParallelism: false, + silent: 'passed-only', + passWithNoTests: false, + reporters: ['default', resolve(TEST_KIT_INT, 'no-skips-reporter.ts')], + }, +}) diff --git a/packages/stack-drizzle/tsconfig.json b/packages/stack-drizzle/tsconfig.json index 2b181374d..87838c567 100644 --- a/packages/stack-drizzle/tsconfig.json +++ b/packages/stack-drizzle/tsconfig.json @@ -28,6 +28,9 @@ "@cipherstash/stack/v3": ["../stack/src/encryption/v3.ts"], "@cipherstash/test-kit": ["../test-kit/src/index.ts"], "@cipherstash/test-kit/suite": ["../test-kit/src/run-family-suite.ts"], + "@cipherstash/test-kit/integration-clerk": [ + "../test-kit/src/integration/clerk.ts" + ], "@cipherstash/test-kit/catalog": ["../test-kit/src/catalog.ts"], "@/*": ["../stack/src/*"] } diff --git a/packages/stack-drizzle/tsconfig.typecheck.json b/packages/stack-drizzle/tsconfig.typecheck.json new file mode 100644 index 000000000..9d06679f0 --- /dev/null +++ b/packages/stack-drizzle/tsconfig.typecheck.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["__tests__/**/*.test-d.ts"] +} diff --git a/packages/stack-drizzle/vitest.config.ts b/packages/stack-drizzle/vitest.config.ts index 6e50a87be..810e666a1 100644 --- a/packages/stack-drizzle/vitest.config.ts +++ b/packages/stack-drizzle/vitest.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ testTimeout: 30000, hookTimeout: 30000, typecheck: { - tsconfig: './tsconfig.json', + tsconfig: './tsconfig.typecheck.json', include: ['__tests__/**/*.test-d.ts'], }, }, diff --git a/packages/stack/integration/supabase/adapter.ts b/packages/stack-supabase/integration/adapter.ts similarity index 97% rename from packages/stack/integration/supabase/adapter.ts rename to packages/stack-supabase/integration/adapter.ts index cb549fd97..4df3689bb 100644 --- a/packages/stack/integration/supabase/adapter.ts +++ b/packages/stack-supabase/integration/adapter.ts @@ -1,3 +1,4 @@ +import { encryptedTable } from '@cipherstash/stack/eql/v3' import { databaseUrl, type IntegrationAdapter, @@ -7,9 +8,8 @@ import { type TableSpec, } from '@cipherstash/test-kit' import postgres from 'postgres' -import { encryptedTable } from '@/eql/v3' -import { encryptedSupabaseV3 } from '@/supabase' -import { makePostgrestClient, reloadSchemaCache } from '../helpers/pgrest' +import { encryptedSupabaseV3 } from '../src/index.js' +import { makePostgrestClient, reloadSchemaCache } from './helpers/pgrest' /** * The Supabase v3 adapter under real ZeroKMS ciphertext. diff --git a/packages/stack/integration/supabase/families.integration.test.ts b/packages/stack-supabase/integration/families.integration.test.ts similarity index 100% rename from packages/stack/integration/supabase/families.integration.test.ts rename to packages/stack-supabase/integration/families.integration.test.ts diff --git a/packages/stack/integration/supabase/grants.integration.test.ts b/packages/stack-supabase/integration/grants.integration.test.ts similarity index 99% rename from packages/stack/integration/supabase/grants.integration.test.ts rename to packages/stack-supabase/integration/grants.integration.test.ts index 74bae3421..43bfa13e0 100644 --- a/packages/stack/integration/supabase/grants.integration.test.ts +++ b/packages/stack-supabase/integration/grants.integration.test.ts @@ -35,7 +35,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { SUPABASE_PERMISSIONS_SQL_V3, supabaseInternalPermissionsSql, -} from '../../../cli/src/installer/grants' +} from '../../cli/src/installer/grants' const sql = postgres(databaseUrl(), { prepare: false }) diff --git a/packages/stack/integration/helpers/pgrest.ts b/packages/stack-supabase/integration/helpers/pgrest.ts similarity index 100% rename from packages/stack/integration/helpers/pgrest.ts rename to packages/stack-supabase/integration/helpers/pgrest.ts diff --git a/packages/stack/__tests__/helpers/v3-envelope.ts b/packages/stack-supabase/integration/helpers/v3-envelope.ts similarity index 100% rename from packages/stack/__tests__/helpers/v3-envelope.ts rename to packages/stack-supabase/integration/helpers/v3-envelope.ts diff --git a/packages/stack/integration/supabase/introspect.integration.test.ts b/packages/stack-supabase/integration/introspect.integration.test.ts similarity index 97% rename from packages/stack/integration/supabase/introspect.integration.test.ts rename to packages/stack-supabase/integration/introspect.integration.test.ts index 11e70d16c..b5b81b3ff 100644 --- a/packages/stack/integration/supabase/introspect.integration.test.ts +++ b/packages/stack-supabase/integration/introspect.integration.test.ts @@ -1,9 +1,9 @@ +import { factoryForDomain } from '@cipherstash/stack/adapter-kit' import { databaseUrl } from '@cipherstash/test-kit' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { factoryForDomain } from '@/eql/v3/domain-registry' -import { introspect } from '@/supabase/introspect' -import { synthesizeTables } from '@/supabase/schema-builder' +import { introspect } from '../src/introspect' +import { synthesizeTables } from '../src/schema-builder' const sql = postgres(databaseUrl(), { prepare: false }) diff --git a/packages/stack/integration/supabase/select-alias.integration.test.ts b/packages/stack-supabase/integration/select-alias.integration.test.ts similarity index 95% rename from packages/stack/integration/supabase/select-alias.integration.test.ts rename to packages/stack-supabase/integration/select-alias.integration.test.ts index dcb2d05a2..0e62523a3 100644 --- a/packages/stack/integration/supabase/select-alias.integration.test.ts +++ b/packages/stack-supabase/integration/select-alias.integration.test.ts @@ -1,9 +1,9 @@ +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' import { databaseUrl } from '@cipherstash/test-kit' import postgres from 'postgres' import { afterAll, beforeAll, expect, it } from 'vitest' -import { encryptedTable, types } from '@/eql/v3' -import { encryptedSupabaseV3 } from '@/supabase' -import { makePostgrestClient, reloadSchemaCache } from '../helpers/pgrest' +import { encryptedSupabaseV3 } from '../src/index.js' +import { makePostgrestClient, reloadSchemaCache } from './helpers/pgrest' /** * `Date` reconstruction across every way PostgREST can key a result row. diff --git a/packages/stack-supabase/integration/vitest.config.ts b/packages/stack-supabase/integration/vitest.config.ts new file mode 100644 index 000000000..5c18db824 --- /dev/null +++ b/packages/stack-supabase/integration/vitest.config.ts @@ -0,0 +1,48 @@ +import { resolve } from 'node:path' +import { defineConfig } from 'vitest/config' +import { sharedAlias } from '../../../vitest.shared' + +/** + * Integration suites for the Supabase adapter: real ZeroKMS, real Postgres, real + * PostgREST. Runs only under `test:integration` (its own CI job). The shared + * harness — `global-setup` (installs EQL v3) and the no-skips reporter — lives in + * `@cipherstash/test-kit/src/integration` so both adapter packages share one copy. + */ +const TEST_KIT_INT = resolve(__dirname, '../../test-kit/src/integration') + +export default defineConfig({ + resolve: { + alias: { + ...sharedAlias, + '@cipherstash/stack/adapter-kit': resolve( + __dirname, + '../../stack/src/adapter-kit.ts', + ), + '@/': resolve(__dirname, '../../stack/src') + '/', + '@cipherstash/protect-ffi/wasm-inline': resolve( + __dirname, + '../../stack/__tests__/helpers/stub-protect-ffi-wasm-inline.ts', + ), + '@cipherstash/auth/wasm-inline': resolve( + __dirname, + '../../stack/__tests__/helpers/stub-auth-wasm-inline.ts', + ), + }, + }, + test: { + root: resolve(__dirname, '..'), + include: ['integration/**/*.integration.test.ts'], + globalSetup: [resolve(TEST_KIT_INT, 'global-setup.ts')], + server: { + deps: { + inline: [/packages\/test-kit/], + }, + }, + testTimeout: 60_000, + hookTimeout: 180_000, + fileParallelism: false, + silent: 'passed-only', + passWithNoTests: false, + reporters: ['default', resolve(TEST_KIT_INT, 'no-skips-reporter.ts')], + }, +}) diff --git a/packages/stack/integration/supabase/wire.integration.test.ts b/packages/stack-supabase/integration/wire.integration.test.ts similarity index 98% rename from packages/stack/integration/supabase/wire.integration.test.ts rename to packages/stack-supabase/integration/wire.integration.test.ts index 70230b83d..56be15602 100644 --- a/packages/stack/integration/supabase/wire.integration.test.ts +++ b/packages/stack-supabase/integration/wire.integration.test.ts @@ -28,17 +28,14 @@ * `permission denied for schema eql_v3_internal` (see `supabase-v3-grants-pg`). */ +import type { EncryptionClient } from '@cipherstash/stack/encryption' +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' import { databaseUrl } from '@cipherstash/test-kit' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import type { EncryptionClient } from '@/encryption' -import { encryptedTable, types } from '@/eql/v3' -import { EncryptedQueryBuilderV3Impl } from '@/supabase/query-builder-v3' -import { - narrowedQueryTerm, - storageEnvelope, -} from '../../__tests__/helpers/v3-envelope' -import { makePostgrestClient, reloadSchemaCache } from '../helpers/pgrest' +import { EncryptedQueryBuilderV3Impl } from '../src/query-builder-v3' +import { makePostgrestClient, reloadSchemaCache } from './helpers/pgrest' +import { narrowedQueryTerm, storageEnvelope } from './helpers/v3-envelope' const TABLE = 'protect_ci_v3_pgrest' diff --git a/packages/stack-supabase/tsconfig.typecheck.json b/packages/stack-supabase/tsconfig.typecheck.json new file mode 100644 index 000000000..9d06679f0 --- /dev/null +++ b/packages/stack-supabase/tsconfig.typecheck.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["__tests__/**/*.test-d.ts"] +} diff --git a/packages/stack-supabase/vitest.config.ts b/packages/stack-supabase/vitest.config.ts index 356568b5b..92b4ac85e 100644 --- a/packages/stack-supabase/vitest.config.ts +++ b/packages/stack-supabase/vitest.config.ts @@ -38,7 +38,7 @@ export default defineConfig({ testTimeout: 30000, hookTimeout: 30000, typecheck: { - tsconfig: './tsconfig.json', + tsconfig: './tsconfig.typecheck.json', include: ['__tests__/**/*.test-d.ts'], }, }, diff --git a/packages/stack/integration/identity/matrix-identity.integration.test.ts b/packages/stack/integration/identity/matrix-identity.integration.test.ts index a20893929..5009a4a74 100644 --- a/packages/stack/integration/identity/matrix-identity.integration.test.ts +++ b/packages/stack/integration/identity/matrix-identity.integration.test.ts @@ -16,9 +16,9 @@ */ import { OidcFederationStrategy } from '@cipherstash/auth' import { unwrapResult } from '@cipherstash/test-kit' +import { clerkJwtProvider } from '@cipherstash/test-kit/integration-clerk' import { beforeAll, describe, expect, it } from 'vitest' import { EncryptionV3, encryptedTable, types } from '@/encryption/v3' -import { clerkJwtProvider } from '../helpers/clerk' const users = encryptedTable('v3_identity_live_users', { email: types.TextEq('email'), diff --git a/packages/stack/integration/vitest.config.ts b/packages/stack/integration/vitest.config.ts index c252f1e47..62c40056d 100644 --- a/packages/stack/integration/vitest.config.ts +++ b/packages/stack/integration/vitest.config.ts @@ -78,7 +78,9 @@ export default defineConfig({ test: { root: resolve(__dirname, '..'), include: SUITE_GLOBS, - globalSetup: [resolve(__dirname, 'global-setup.ts')], + globalSetup: [ + resolve(__dirname, '../../test-kit/src/integration/global-setup.ts'), + ], server: { deps: { // `@cipherstash/test-kit` resolves to source in ANOTHER package, i.e. @@ -121,6 +123,9 @@ export default defineConfig({ // Fail the run if anything is skipped. A skipped test reads exactly like a // passing one, and every silent hole this suite has found took that shape. - reporters: ['default', resolve(__dirname, 'no-skips-reporter.ts')], + reporters: [ + 'default', + resolve(__dirname, '../../test-kit/src/integration/no-skips-reporter.ts'), + ], }, }) diff --git a/packages/stack/tsconfig.json b/packages/stack/tsconfig.json index fe9857436..d0a2140d5 100644 --- a/packages/stack/tsconfig.json +++ b/packages/stack/tsconfig.json @@ -45,6 +45,9 @@ // and `packages/test-kit/tsconfig.json`; keep the three in step. "@cipherstash/test-kit": ["../test-kit/src/index.ts"], "@cipherstash/test-kit/suite": ["../test-kit/src/run-family-suite.ts"], + "@cipherstash/test-kit/integration-clerk": [ + "../test-kit/src/integration/clerk.ts" + ], "@cipherstash/test-kit/catalog": ["../test-kit/src/catalog.ts"], "@cipherstash/stack": ["./src/index.ts"], "@cipherstash/stack/eql/v3": ["./src/eql/v3/index.ts"], diff --git a/packages/test-kit/package.json b/packages/test-kit/package.json index 9709ecc7d..f12c580cc 100644 --- a/packages/test-kit/package.json +++ b/packages/test-kit/package.json @@ -5,6 +5,7 @@ "description": "Shared EQL v3 test harness: the domain catalog, the plaintext oracle, and the integration-suite driver. Consumed as TypeScript source \u2014 no build step.", "type": "module", "exports": { + "./integration-clerk": "./src/integration/clerk.ts", ".": "./src/index.ts", "./catalog": "./src/catalog.ts", "./suite": "./src/run-family-suite.ts" @@ -16,6 +17,7 @@ "@cipherstash/stack": "workspace:*" }, "devDependencies": { + "@clerk/backend": "3.4.9", "typescript": "catalog:repo", "vitest": "catalog:repo" } diff --git a/packages/stack/integration/helpers/clerk.ts b/packages/test-kit/src/integration/clerk.ts similarity index 100% rename from packages/stack/integration/helpers/clerk.ts rename to packages/test-kit/src/integration/clerk.ts diff --git a/packages/stack/integration/global-setup.ts b/packages/test-kit/src/integration/global-setup.ts similarity index 96% rename from packages/stack/integration/global-setup.ts rename to packages/test-kit/src/integration/global-setup.ts index de61d79c2..33b838289 100644 --- a/packages/stack/integration/global-setup.ts +++ b/packages/test-kit/src/integration/global-setup.ts @@ -4,7 +4,7 @@ import { installEqlV3, type Requirement, requireIntegrationEnv, -} from '@cipherstash/test-kit' +} from '../index.ts' /** * Fail fast, before a single container second is spent, and install EQL v3 once diff --git a/packages/stack/integration/no-skips-reporter.ts b/packages/test-kit/src/integration/no-skips-reporter.ts similarity index 100% rename from packages/stack/integration/no-skips-reporter.ts rename to packages/test-kit/src/integration/no-skips-reporter.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d93da5629..d4b657881 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -759,6 +759,9 @@ importers: specifier: workspace:* version: link:../stack devDependencies: + '@clerk/backend': + specifier: 3.4.9 + version: 3.4.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) typescript: specifier: catalog:repo version: 5.9.3 diff --git a/vitest.shared.ts b/vitest.shared.ts index 1fe3f455a..02c3f27bf 100644 --- a/vitest.shared.ts +++ b/vitest.shared.ts @@ -43,6 +43,10 @@ export const sharedAlias: Record = { 'packages/stack/src/encryption/v3.ts', ), // The core↔adapter seam, consumed by the split adapter packages. + '@cipherstash/test-kit/integration-clerk': resolve( + repoRoot, + 'packages/test-kit/src/integration/clerk.ts', + ), '@cipherstash/stack/adapter-kit': resolve( repoRoot, 'packages/stack/src/adapter-kit.ts', From ca9d576a1885032163ec8a20dc102007e651bb2f Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 11:35:43 +1000 Subject: [PATCH 09/17] fix(stack-drizzle): import OidcFederationStrategy from @cipherstash/stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The relocated lock-context integration suite imported it from @cipherstash/auth, which stack-drizzle doesn't depend on directly (CI: "Cannot find package '@cipherstash/auth'"). @cipherstash/stack re-exports it, and stack is already a dependency — resolves through stack's own module context. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .../stack-drizzle/integration/lock-context.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stack-drizzle/integration/lock-context.integration.test.ts b/packages/stack-drizzle/integration/lock-context.integration.test.ts index 312bcefde..293c34853 100644 --- a/packages/stack-drizzle/integration/lock-context.integration.test.ts +++ b/packages/stack-drizzle/integration/lock-context.integration.test.ts @@ -32,7 +32,7 @@ * a distinct `sub`) and asserts B cannot read A's row, with A reading it as the * control. */ -import { OidcFederationStrategy } from '@cipherstash/auth' +import { OidcFederationStrategy } from '@cipherstash/stack' import { EncryptionV3 } from '@cipherstash/stack/v3' import { databaseUrl, unwrapResult, V3_MATRIX } from '@cipherstash/test-kit' import { clerkJwtProvider } from '@cipherstash/test-kit/integration-clerk' From 46717a666458d0fd131a3341ed11b68e516a8c9c Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 11:39:50 +1000 Subject: [PATCH 10/17] fix: repoint clerk-provider unit test + order the integration-clerk alias clerk-provider.test.ts (a stack unit test) imported the clerk helper from ../integration/helpers/clerk, which moved to @cipherstash/test-kit; repoint it to @cipherstash/test-kit/integration-clerk. Also move that alias ahead of the bare @cipherstash/test-kit entry in vitest.shared so Vite's prefix match resolves the subpath (it was ordered after, so it never matched). Fixes the Run Tests (Node 22/24) job. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- packages/stack/__tests__/clerk-provider.test.ts | 2 +- vitest.shared.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/stack/__tests__/clerk-provider.test.ts b/packages/stack/__tests__/clerk-provider.test.ts index 597e2e8af..8849ac571 100644 --- a/packages/stack/__tests__/clerk-provider.test.ts +++ b/packages/stack/__tests__/clerk-provider.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from 'vitest' -import { clerkJwtProvider } from '../integration/helpers/clerk' +import { clerkJwtProvider } from '@cipherstash/test-kit/integration-clerk' /** * `clerkJwtProvider`'s "FAIL rather than skip" guard is the load-bearing half of diff --git a/vitest.shared.ts b/vitest.shared.ts index 02c3f27bf..6fe89d665 100644 --- a/vitest.shared.ts +++ b/vitest.shared.ts @@ -29,6 +29,10 @@ export const sharedAlias: Record = { repoRoot, 'packages/test-kit/src/catalog.ts', ), + '@cipherstash/test-kit/integration-clerk': resolve( + repoRoot, + 'packages/test-kit/src/integration/clerk.ts', + ), '@cipherstash/test-kit': resolve(repoRoot, 'packages/test-kit/src/index.ts'), '@cipherstash/stack/eql/v3': resolve( repoRoot, @@ -43,10 +47,6 @@ export const sharedAlias: Record = { 'packages/stack/src/encryption/v3.ts', ), // The core↔adapter seam, consumed by the split adapter packages. - '@cipherstash/test-kit/integration-clerk': resolve( - repoRoot, - 'packages/test-kit/src/integration/clerk.ts', - ), '@cipherstash/stack/adapter-kit': resolve( repoRoot, 'packages/stack/src/adapter-kit.ts', From 1fc94dae0d20ea24de3c6b206e8d657857ea6213 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 11:40:13 +1000 Subject: [PATCH 11/17] style: sort clerk-provider.test imports (biome) Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- packages/stack/__tests__/clerk-provider.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stack/__tests__/clerk-provider.test.ts b/packages/stack/__tests__/clerk-provider.test.ts index 8849ac571..df02618a2 100644 --- a/packages/stack/__tests__/clerk-provider.test.ts +++ b/packages/stack/__tests__/clerk-provider.test.ts @@ -1,5 +1,5 @@ -import { afterEach, describe, expect, it } from 'vitest' import { clerkJwtProvider } from '@cipherstash/test-kit/integration-clerk' +import { afterEach, describe, expect, it } from 'vitest' /** * `clerkJwtProvider`'s "FAIL rather than skip" guard is the load-bearing half of From bff9a2e4e8de2a022b5ce3167db19dd8f20d70e2 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 11:51:14 +1000 Subject: [PATCH 12/17] ci(fta): extend the complexity gate to the split adapter packages (#627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate ran only `fta src/eql/v3` in stack, so relocating the Drizzle v3 code out to stack-drizzle silently dropped it from coverage — and the v2 adapter monoliths (supabase query-builder.ts, drizzle operators.ts) were never gated at all. Adds `analyze:complexity` to both new packages and runs all three from fta-v3.yml (one step each, so a failure names the package), triggered on the new package paths. Caps are ratchets at each package's current worst file: stack 69 (columns.ts 68.46), stack-drizzle 89 (operators.ts 88.70), stack-supabase 91 (query-builder.ts 90.41). Lower a cap as a file is refactored below the next threshold; the two v2 monoliths are the debt to whittle toward stack's 69. Kept the job name "Analyze v3 complexity" so any required-check config still matches. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .github/workflows/fta-v3.yml | 25 +++++++++++++++++++++---- packages/stack-drizzle/package.json | 4 +++- packages/stack-supabase/package.json | 4 +++- pnpm-lock.yaml | 6 ++++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/fta-v3.yml b/.github/workflows/fta-v3.yml index 3a130d9fc..95659a4f9 100644 --- a/.github/workflows/fta-v3.yml +++ b/.github/workflows/fta-v3.yml @@ -1,7 +1,8 @@ -name: "FTA Complexity (EQL v3)" +name: "FTA Complexity" -# Blocking complexity gate for the EQL v3 text-search schema. Runs the Fast -# TypeScript Analyzer (fta-cli) against the v3 source directory only and fails +# Blocking complexity gate for the EQL v3 core (stack) and the split adapter +# packages (stack-drizzle, stack-supabase). Runs the Fast TypeScript Analyzer +# (fta-cli) against each package's source and fails # the check when any file exceeds the score cap (`--score-cap` in the # `analyze:complexity` script). FTA is pure static source analysis, so this job # needs no build step, database, or credentials. @@ -12,6 +13,8 @@ on: - 'main' paths: - 'packages/stack/src/eql/v3/**' + - 'packages/stack-drizzle/**' + - 'packages/stack-supabase/**' # Shared match-index defaults live outside src/eql/v3 but shape every # emitted v3 match block (load-bearing `k`/`m` ciphertext params), so edits # here must trigger the v3 gate too. @@ -23,6 +26,8 @@ on: - "**" paths: - 'packages/stack/src/eql/v3/**' + - 'packages/stack-drizzle/**' + - 'packages/stack-supabase/**' - 'packages/stack/src/schema/match-defaults.ts' - 'packages/stack/package.json' - '.github/workflows/fta-v3.yml' @@ -61,5 +66,17 @@ jobs: # Non-zero exit (score above the cap) fails the check — this is the # blocking gate. No `continue-on-error`. - - name: Analyze v3 complexity + # One step per package so a failure names the offending package. Each caps + # at its current worst file (a ratchet, not an aspiration): stack src/eql/v3 + # at 69, and the split adapter packages at their monolith maxima — drizzle + # 89 (operators.ts), supabase 91 (query-builder.ts). Lower a cap whenever a + # file is refactored below the next threshold; the v2 query-builder/operators + # monoliths are the debt to whittle down toward stack's 69. + - name: Analyze stack (eql/v3) complexity run: pnpm --filter @cipherstash/stack run analyze:complexity + + - name: Analyze stack-drizzle complexity + run: pnpm --filter @cipherstash/stack-drizzle run analyze:complexity + + - name: Analyze stack-supabase complexity + run: pnpm --filter @cipherstash/stack-supabase run analyze:complexity diff --git a/packages/stack-drizzle/package.json b/packages/stack-drizzle/package.json index f744ebc74..8c7b05b9b 100644 --- a/packages/stack-drizzle/package.json +++ b/packages/stack-drizzle/package.json @@ -66,7 +66,8 @@ "dev": "tsup --watch", "test": "vitest run", "test:types": "vitest --run --typecheck.only", - "test:integration": "vitest run --config integration/vitest.config.ts" + "test:integration": "vitest run --config integration/vitest.config.ts", + "analyze:complexity": "fta src --score-cap 89" }, "dependencies": { "@cipherstash/stack": "workspace:*", @@ -79,6 +80,7 @@ "devDependencies": { "@cipherstash/protect-ffi": "0.29.0", "@cipherstash/test-kit": "workspace:*", + "fta-cli": "3.0.0", "dotenv": "17.4.2", "drizzle-orm": "^0.45.2", "postgres": "^3.4.8", diff --git a/packages/stack-supabase/package.json b/packages/stack-supabase/package.json index e7d1f6c1d..e35ff5b4f 100644 --- a/packages/stack-supabase/package.json +++ b/packages/stack-supabase/package.json @@ -48,7 +48,8 @@ "dev": "tsup --watch", "test": "vitest run", "test:types": "vitest --run --typecheck.only", - "test:integration": "vitest run --config integration/vitest.config.ts" + "test:integration": "vitest run --config integration/vitest.config.ts", + "analyze:complexity": "fta src --score-cap 91" }, "dependencies": { "@cipherstash/stack": "workspace:*" @@ -69,6 +70,7 @@ "devDependencies": { "@cipherstash/protect-ffi": "0.29.0", "@cipherstash/test-kit": "workspace:*", + "fta-cli": "3.0.0", "@supabase/postgrest-js": "2.105.4", "@supabase/supabase-js": "^2.110.0", "@types/pg": "^8.20.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4b657881..b4e85ef18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -697,6 +697,9 @@ importers: drizzle-orm: specifier: ^0.45.2 version: 0.45.2(@types/pg@8.20.0)(gel@2.2.0)(mysql2@3.16.0)(pg@8.21.0)(postgres@3.4.9) + fta-cli: + specifier: 3.0.0 + version: 3.0.0 postgres: specifier: ^3.4.8 version: 3.4.9 @@ -737,6 +740,9 @@ importers: fast-check: specifier: ^4.8.0 version: 4.8.0 + fta-cli: + specifier: 3.0.0 + version: 3.0.0 pg: specifier: 8.20.0 version: 8.20.0 From 31ca318496650f6efd8c8c46de57d8667e4c4098 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 11:58:40 +1000 Subject: [PATCH 13/17] docs(627): update skills, docs, meta-files, and changesets for the adapter split - skills/stash-{drizzle,supabase,encryption}: import from @cipherstash/stack-drizzle (+/v3) and @cipherstash/stack-supabase, installed alongside @cipherstash/stack; corrected the "included in @cipherstash/stack" framing to separate first-party packages. - docs/reference/supabase-sdk.md + packages/stack/README.md: new package paths; README's subpath-export table now lists the adapters as separate packages, not stack subpaths. - AGENTS.md: dropped the three removed subpaths from the export list, added the two packages to Repository Layout, updated the integrations section. - SECURITY.md: added both packages; scoped stack's description. - examples/basic: import + deps updated. - Changesets: @cipherstash/stack minor (breaking subpath removal + adapter-kit), the two new packages at first release, and a stash patch for the bundled skills. Historical design docs under docs/superpowers/specs and the separate @cipherstash/drizzle (protect-based) package are intentionally left unchanged. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .changeset/adapter-package-split.md | 28 +++++++++++++++++++++++ .changeset/adapter-split-skills.md | 11 +++++++++ AGENTS.md | 8 ++++--- SECURITY.md | 4 +++- docs/reference/supabase-sdk.md | 6 ++--- examples/basic/package.json | 1 + examples/basic/src/encryption/index.ts | 2 +- packages/stack-drizzle/package.json | 2 +- packages/stack-supabase/package.json | 2 +- packages/stack/README.md | 18 ++++++++++----- pnpm-lock.yaml | 3 +++ skills/stash-drizzle/SKILL.md | 31 ++++++++++++++------------ skills/stash-encryption/SKILL.md | 8 +++---- skills/stash-supabase/SKILL.md | 17 ++++++++------ 14 files changed, 101 insertions(+), 40 deletions(-) create mode 100644 .changeset/adapter-package-split.md create mode 100644 .changeset/adapter-split-skills.md diff --git a/.changeset/adapter-package-split.md b/.changeset/adapter-package-split.md new file mode 100644 index 000000000..13d871f4c --- /dev/null +++ b/.changeset/adapter-package-split.md @@ -0,0 +1,28 @@ +--- +'@cipherstash/stack': minor +'@cipherstash/stack-drizzle': minor +'@cipherstash/stack-supabase': minor +--- + +Split the Drizzle and Supabase integrations into their own packages. + +The adapters now ship as first-party packages that depend on `@cipherstash/stack`, +following the `@cipherstash/prisma-next` precedent: + +- **`@cipherstash/stack-drizzle`** — Drizzle ORM integration. EQL v2 on the package + root (`@cipherstash/stack-drizzle`: `encryptedType`, `extractEncryptionSchema`, + `createEncryptionOperators`) and EQL v3 on `@cipherstash/stack-drizzle/v3` + (`types` factories, `createEncryptionOperatorsV3`, `extractEncryptionSchemaV3`, …). +- **`@cipherstash/stack-supabase`** — Supabase integration: `encryptedSupabase` (v2) + and `encryptedSupabaseV3` (v3, connect-time introspection). + +**Breaking (`@cipherstash/stack`):** the `./drizzle`, `./supabase`, and +`./eql/v3/drizzle` subpath exports are removed. Migrate imports: + +- `@cipherstash/stack/drizzle` → `@cipherstash/stack-drizzle` +- `@cipherstash/stack/eql/v3/drizzle` → `@cipherstash/stack-drizzle/v3` +- `@cipherstash/stack/supabase` → `@cipherstash/stack-supabase` + +Add the relevant package to your dependencies alongside `@cipherstash/stack`. A new +`@cipherstash/stack/adapter-kit` subpath exposes the narrow core internals the +first-party adapters consume; it is the core↔adapter seam, not general-purpose API. diff --git a/.changeset/adapter-split-skills.md b/.changeset/adapter-split-skills.md new file mode 100644 index 000000000..772eeb21d --- /dev/null +++ b/.changeset/adapter-split-skills.md @@ -0,0 +1,11 @@ +--- +'stash': patch +--- + +Update the bundled `stash-drizzle`, `stash-supabase`, and `stash-encryption` agent +skills (and the stack README / Supabase reference doc) for the adapter package +split: the Drizzle and Supabase integrations import from `@cipherstash/stack-drizzle` +(+ `/v3`) and `@cipherstash/stack-supabase` respectively, installed alongside +`@cipherstash/stack`, rather than from `@cipherstash/stack/{drizzle,supabase,eql/v3/drizzle}` +subpaths. Skills ship inside the `stash` tarball, so the stale import paths would +otherwise become wrong guidance in a user's project. diff --git a/AGENTS.md b/AGENTS.md index 6c6f128b3..0b0b4a023 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,7 +72,7 @@ If these variables are missing, tests that require live encryption will fail or ## Repository Layout - `packages/stack`: Main package (`@cipherstash/stack`) containing the encryption client and all integrations - - Subpath exports: `@cipherstash/stack`, `@cipherstash/stack/client`, `@cipherstash/stack/identity`, `@cipherstash/stack/schema`, `@cipherstash/stack/eql/v3`, `@cipherstash/stack/eql/v3/drizzle`, `@cipherstash/stack/v3`, `@cipherstash/stack/types`, `@cipherstash/stack/drizzle`, `@cipherstash/stack/dynamodb`, `@cipherstash/stack/supabase`, `@cipherstash/stack/encryption`, `@cipherstash/stack/errors`, `@cipherstash/stack/wasm-inline` + - Subpath exports: `@cipherstash/stack`, `@cipherstash/stack/client`, `@cipherstash/stack/identity`, `@cipherstash/stack/schema`, `@cipherstash/stack/eql/v3`, `@cipherstash/stack/v3`, `@cipherstash/stack/types`, `@cipherstash/stack/dynamodb`, `@cipherstash/stack/encryption`, `@cipherstash/stack/errors`, `@cipherstash/stack/adapter-kit`, `@cipherstash/stack/wasm-inline` (the Drizzle and Supabase integrations moved to their own packages — see below) - `packages/protect`: Core encryption library (internal, re-exported via `@cipherstash/stack`) - `src/index.ts`: Public API (`Encryption`, exports) - `src/ffi/index.ts`: `EncryptionClient` implementation, bridges to `@cipherstash/protect-ffi` @@ -82,6 +82,8 @@ If these variables are missing, tests that require live encryption will fail or - `packages/wizard`: AI-powered encryption setup (`@cipherstash/wizard`) - `packages/migrate`: Plaintext-to-encrypted column migration (`@cipherstash/migrate`) — resumable backfill, per-column state - `packages/prisma-next`: Prisma Next integration (`@cipherstash/prisma-next`) — searchable field-level encryption for Postgres +- `packages/stack-drizzle`: Drizzle ORM integration (`@cipherstash/stack-drizzle`), depends on `@cipherstash/stack` — EQL v2 (`.`) and EQL v3 (`./v3`). Split out of `@cipherstash/stack`. +- `packages/stack-supabase`: Supabase integration (`@cipherstash/stack-supabase`), depends on `@cipherstash/stack` — `encryptedSupabase` (v2) and `encryptedSupabaseV3` (v3). Split out of `@cipherstash/stack`. - `packages/schema`: Schema builder utilities and types (`encryptedTable`, `encryptedColumn`, `encryptedField`) - `packages/drizzle`: Drizzle ORM integration (`encryptedType`, `extractEncryptionSchema`, `createEncryptionOperators`) - `packages/nextjs`: Next.js helpers and Clerk integration (`./clerk` export) @@ -157,8 +159,8 @@ Three rules to remember when editing CI or pnpm config: - `encryptQuery(terms[])` for batch query encryption - **Identity-aware encryption**: Authenticate the client as the end user with `OidcFederationStrategy` (`config.strategy`, re-exported from `@cipherstash/stack`), then chain `.withLockContext({ identityClaim })` on operations to bind the data key to a claim. The same claim must be used for encrypt and decrypt. (`LockContext.identify()` from `@cipherstash/stack/identity` is deprecated — the strategy now handles token acquisition; `.withLockContext()` also accepts a `LockContext`.) - **Integrations**: - - **Drizzle ORM**: `encryptedType`, `extractEncryptionSchema`, `createEncryptionOperators` from `@cipherstash/stack/drizzle` - - **Supabase**: `encryptedSupabase` from `@cipherstash/stack/supabase` + - **Drizzle ORM**: `encryptedType`, `extractEncryptionSchema`, `createEncryptionOperators` from `@cipherstash/stack-drizzle` (EQL v3 factories from `@cipherstash/stack-drizzle/v3`) + - **Supabase**: `encryptedSupabase` (v2) / `encryptedSupabaseV3` (v3) from `@cipherstash/stack-supabase` - **DynamoDB**: `encryptedDynamoDB` from `@cipherstash/stack/dynamodb` ## Critical Gotchas (read before coding) diff --git a/SECURITY.md b/SECURITY.md index cbbb1e2f6..742c2f272 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,7 +9,7 @@ This repository is the CipherStash Stack monorepo for JavaScript/TypeScript. It | Package | Description | | ------- | ----------- | -| `@cipherstash/stack` | Main package: encryption client and all integrations | +| `@cipherstash/stack` | Main package: encryption client, schema, EQL v3 typed client | | `stash` | CipherStash CLI | | `@cipherstash/protect` | Core encryption library (re-exported via `@cipherstash/stack`) | | `@cipherstash/schema` | Schema builder utilities | @@ -18,6 +18,8 @@ This repository is the CipherStash Stack monorepo for JavaScript/TypeScript. It | `@cipherstash/protect-dynamodb` | DynamoDB helpers | | `@cipherstash/migrate` | Plaintext-to-encrypted column migration tooling | | `@cipherstash/prisma-next` | Prisma Next integration (searchable field-level encryption for Postgres) | +| `@cipherstash/stack-drizzle` | Drizzle ORM integration for `@cipherstash/stack` (EQL v2 + v3) | +| `@cipherstash/stack-supabase` | Supabase integration for `@cipherstash/stack` (EQL v2 + v3) | | `@cipherstash/wizard` | AI-powered encryption setup | **Security fixes are released for the latest release line of each package.** Security reports are welcome for any version, but fixes land in the latest release — if you are running an older major version, plan to upgrade to receive them. diff --git a/docs/reference/supabase-sdk.md b/docs/reference/supabase-sdk.md index 5017c3511..e84b3f6e6 100644 --- a/docs/reference/supabase-sdk.md +++ b/docs/reference/supabase-sdk.md @@ -1,6 +1,6 @@ # Supabase SDK reference -`@cipherstash/stack/supabase` wraps a supabase-js client so encrypted columns +`@cipherstash/stack-supabase` wraps a supabase-js client so encrypted columns are transparently encrypted on mutations, `::jsonb`-cast on selects, encrypted in filter terms, and decrypted in results. @@ -27,7 +27,7 @@ client internally. Introspection needs a direct Postgres connection run in a Worker or the browser. ```typescript -import { encryptedSupabaseV3 } from '@cipherstash/stack/supabase' +import { encryptedSupabaseV3 } from '@cipherstash/stack-supabase' // Introspects the database via options.databaseUrl or DATABASE_URL const es = await encryptedSupabaseV3( @@ -66,7 +66,7 @@ tables against the database at construction: ```typescript import { encryptedTable, types } from '@cipherstash/stack/eql/v3' -import { encryptedSupabaseV3 } from '@cipherstash/stack/supabase' +import { encryptedSupabaseV3 } from '@cipherstash/stack-supabase' const users = encryptedTable('users', { email: types.TextSearch('email'), // public.eql_v3_text_search diff --git a/examples/basic/package.json b/examples/basic/package.json index 45b60a977..8a2f8e242 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -12,6 +12,7 @@ "description": "", "dependencies": { "@cipherstash/stack": "workspace:*", + "@cipherstash/stack-drizzle": "workspace:*", "@cipherstash/stack-supabase": "workspace:*", "dotenv": "^17.4.2", "pg": "8.20.0" diff --git a/examples/basic/src/encryption/index.ts b/examples/basic/src/encryption/index.ts index c7290f7a6..4f78be08e 100644 --- a/examples/basic/src/encryption/index.ts +++ b/examples/basic/src/encryption/index.ts @@ -2,7 +2,7 @@ import { Encryption } from '@cipherstash/stack' import { encryptedType, extractEncryptionSchema, -} from '@cipherstash/stack/drizzle' +} from '@cipherstash/stack-drizzle' import { integer, pgTable, timestamp } from 'drizzle-orm/pg-core' export const usersTable = pgTable('users', { diff --git a/packages/stack-drizzle/package.json b/packages/stack-drizzle/package.json index 8c7b05b9b..ed702ac47 100644 --- a/packages/stack-drizzle/package.json +++ b/packages/stack-drizzle/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/stack-drizzle", - "version": "0.1.0", + "version": "0.0.0", "description": "CipherStash Stack Drizzle ORM integration: encrypted column types and query operators for EQL v2 (./) and EQL v3 (./v3).", "keywords": [ "encrypted", diff --git a/packages/stack-supabase/package.json b/packages/stack-supabase/package.json index e35ff5b4f..e7430f4b2 100644 --- a/packages/stack-supabase/package.json +++ b/packages/stack-supabase/package.json @@ -1,6 +1,6 @@ { "name": "@cipherstash/stack-supabase", - "version": "0.1.0", + "version": "0.0.0", "description": "CipherStash Stack Supabase integration: transparent field-level encryption for Supabase with EQL v3 (encryptedSupabaseV3, connect-time introspection) and v2 (encryptedSupabase).", "keywords": [ "encrypted", diff --git a/packages/stack/README.md b/packages/stack/README.md index 8bd9b8b3d..7482e7946 100644 --- a/packages/stack/README.md +++ b/packages/stack/README.md @@ -307,11 +307,11 @@ Operator family support for Supabase is being developed in collaboration with th Encrypted data is stored as an [EQL](https://github.com/cipherstash/encrypt-query-language) JSON payload. Install the EQL extension in PostgreSQL to enable searchable queries, then store encrypted data in `eql_v2_encrypted` columns. -The `@cipherstash/stack/drizzle` module provides `encryptedType` for defining encrypted columns and `createEncryptionOperators` for querying them: +The `@cipherstash/stack-drizzle` module provides `encryptedType` for defining encrypted columns and `createEncryptionOperators` for querying them: ```typescript import { pgTable, integer, timestamp } from "drizzle-orm/pg-core" -import { encryptedType, extractEncryptionSchema, createEncryptionOperators } from "@cipherstash/stack/drizzle" +import { encryptedType, extractEncryptionSchema, createEncryptionOperators } from "@cipherstash/stack-drizzle" import { Encryption } from "@cipherstash/stack" // Define schema with encrypted columns @@ -366,7 +366,7 @@ These operators encrypt the JSON path selector using the `steVecSelector` query ### EQL v3 Concrete-Type Columns (Drizzle) -The `@cipherstash/stack/eql/v3/drizzle` module targets EQL v3, where each encrypted column is a **concrete Postgres domain type** (`eql_v3.text_eq`, `eql_v3.integer_ord`, …). Instead of toggling capability flags, you pick a concrete type from the `types` namespace and its query capabilities are fixed by that choice. The v2 `@cipherstash/stack/drizzle` module above is unchanged — this is an additive export. +The `@cipherstash/stack-drizzle/v3` module targets EQL v3, where each encrypted column is a **concrete Postgres domain type** (`eql_v3.text_eq`, `eql_v3.integer_ord`, …). Instead of toggling capability flags, you pick a concrete type from the `types` namespace and its query capabilities are fixed by that choice. It is the `/v3` subpath of the same `@cipherstash/stack-drizzle` package whose v2 surface is shown above. Declare a Drizzle table using the `types` factories. The suffix encodes the capability: `*Eq` (equality), `*Ord` (order + range, which also covers equality), `*Match` / `TextSearch` (free-text search), and the bare name (e.g. `Text`, `Bigint`) is storage-only. @@ -377,7 +377,7 @@ import { types, createEncryptionOperatorsV3, extractEncryptionSchemaV3, -} from "@cipherstash/stack/eql/v3/drizzle" +} from "@cipherstash/stack-drizzle/v3" import { EncryptionV3 } from "@cipherstash/stack/v3" // Capabilities come from the concrete type — no flags to configure. @@ -674,7 +674,15 @@ csValue(valueName) // returns ProtectValue (for nested values) | `@cipherstash/stack/client` | Client-safe exports (schema builders and types only - no native FFI) | | `@cipherstash/stack/types` | All TypeScript types (`Encrypted`, `Decrypted`, `ClientConfig`, `EncryptionClientConfig`, query types, etc.) | | `@cipherstash/stack/v3` | `EncryptionV3` typed client plus the EQL v3 authoring DSL (`encryptedTable`, `types`, v3 type helpers) | -| `@cipherstash/stack/eql/v3/drizzle` | EQL v3 Drizzle integration: `types` column factories, `createEncryptionOperatorsV3`, `extractEncryptionSchemaV3`, `makeEqlV3Column`, `EncryptionOperatorError` | + +The Drizzle and Supabase integrations are **separate first-party packages** that +depend on `@cipherstash/stack` (they are no longer subpaths of it): + +| Package | Provides | +|-------|-----| +| `@cipherstash/stack-drizzle` | Drizzle ORM integration (EQL v2): `encryptedType`, `extractEncryptionSchema`, `createEncryptionOperators` | +| `@cipherstash/stack-drizzle/v3` | EQL v3 Drizzle integration: `types` column factories, `createEncryptionOperatorsV3`, `extractEncryptionSchemaV3`, `makeEqlV3Column`, `EncryptionOperatorError` | +| `@cipherstash/stack-supabase` | Supabase integration: `encryptedSupabase` (v2) and `encryptedSupabaseV3` (v3) | ## Migration from @cipherstash/protect diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4e85ef18..3fb128d10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: '@cipherstash/stack': specifier: workspace:* version: link:../../packages/stack + '@cipherstash/stack-drizzle': + specifier: workspace:* + version: link:../../packages/stack-drizzle '@cipherstash/stack-supabase': specifier: workspace:* version: link:../../packages/stack-supabase diff --git a/skills/stash-drizzle/SKILL.md b/skills/stash-drizzle/SKILL.md index 856e06894..f5cecde05 100644 --- a/skills/stash-drizzle/SKILL.md +++ b/skills/stash-drizzle/SKILL.md @@ -1,11 +1,11 @@ --- name: stash-drizzle -description: Integrate CipherStash encryption with Drizzle ORM using @cipherstash/stack/drizzle. Covers the encryptedType column type, encrypted query operators (eq, like, ilike, gt/gte/lt/lte, between, inArray, asc/desc), schema extraction, batched and/or conditions, EQL migration generation, the EQL v3 integration (@cipherstash/stack/eql/v3/drizzle), and the complete Drizzle integration workflow. Use when adding encryption to a Drizzle ORM project, defining encrypted Drizzle schemas, or querying encrypted columns with Drizzle. +description: Integrate CipherStash encryption with Drizzle ORM using @cipherstash/stack-drizzle. Covers the encryptedType column type, encrypted query operators (eq, like, ilike, gt/gte/lt/lte, between, inArray, asc/desc), schema extraction, batched and/or conditions, EQL migration generation, the EQL v3 integration (@cipherstash/stack-drizzle/v3), and the complete Drizzle integration workflow. Use when adding encryption to a Drizzle ORM project, defining encrypted Drizzle schemas, or querying encrypted columns with Drizzle. --- # CipherStash Stack - Drizzle ORM Integration -Guide for integrating CipherStash field-level encryption with Drizzle ORM using `@cipherstash/stack/drizzle`. Provides a custom column type for encrypted fields and query operators that transparently encrypt search values. +Guide for integrating CipherStash field-level encryption with Drizzle ORM using `@cipherstash/stack-drizzle`. Provides a custom column type for encrypted fields and query operators that transparently encrypt search values. ## When to Use This Skill @@ -20,10 +20,13 @@ Guide for integrating CipherStash field-level encryption with Drizzle ORM using ## Installation ```bash -npm install @cipherstash/stack drizzle-orm +npm install @cipherstash/stack @cipherstash/stack-drizzle drizzle-orm ``` -The Drizzle integration is included in `@cipherstash/stack` and imports from `@cipherstash/stack/drizzle`. +The Drizzle integration ships as its own first-party package, +`@cipherstash/stack-drizzle`, which depends on `@cipherstash/stack`. Install both. +It is distinct from the older, separate `@cipherstash/drizzle` package (which is +`@cipherstash/protect`-based, with different symbol names). ## Database Setup @@ -63,7 +66,7 @@ Use `encryptedType()` to define encrypted columns in Drizzle table schemas: ```typescript import { pgTable, integer, timestamp, varchar } from "drizzle-orm/pg-core" -import { encryptedType } from "@cipherstash/stack/drizzle" +import { encryptedType } from "@cipherstash/stack-drizzle" const usersTable = pgTable("users", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), @@ -111,7 +114,7 @@ The generic type parameter `` sets the TypeScript type for the decrypted ### 1. Extract Schema from Drizzle Table ```typescript -import { extractEncryptionSchema, createEncryptionOperators } from "@cipherstash/stack/drizzle" +import { extractEncryptionSchema, createEncryptionOperators } from "@cipherstash/stack-drizzle" import { Encryption } from "@cipherstash/stack" // Convert Drizzle table definition to CipherStash schema @@ -367,7 +370,7 @@ Add an `email_encrypted` column **alongside** `email`. Crucially, the encrypted ```typescript // src/db/schema.ts -import { encryptedType } from '@cipherstash/stack/drizzle' +import { encryptedType } from '@cipherstash/stack-drizzle' export const users = pgTable('users', { id: integer('id').primaryKey().generatedAlwaysAsIdentity(), @@ -384,7 +387,7 @@ Update the encryption client to harvest the encrypted columns from the table: ```typescript // src/encryption/index.ts import { Encryption } from '@cipherstash/stack' -import { extractEncryptionSchema } from '@cipherstash/stack/drizzle' +import { extractEncryptionSchema } from '@cipherstash/stack-drizzle' import { users } from '../db/schema' const usersEncryptionSchema = extractEncryptionSchema(users) @@ -624,7 +627,7 @@ import { eq } from "drizzle-orm" import { drizzle } from "drizzle-orm/postgres-js" import postgres from "postgres" import { pgTable, integer, timestamp, varchar } from "drizzle-orm/pg-core" -import { encryptedType, extractEncryptionSchema, createEncryptionOperators, EncryptionOperatorError, EncryptionConfigError } from "@cipherstash/stack/drizzle" +import { encryptedType, extractEncryptionSchema, createEncryptionOperators, EncryptionOperatorError, EncryptionConfigError } from "@cipherstash/stack-drizzle" import { Encryption } from "@cipherstash/stack" // Schema @@ -691,7 +694,7 @@ Individual operators (e.g., `eq()`, `gte()`, `like()`) throw errors when invoked - **`EncryptionConfigError`** — thrown for configuration issues (e.g., using `like` on a column without `freeTextSearch: true`). ```typescript -import { createEncryptionOperators, EncryptionOperatorError, EncryptionConfigError } from "@cipherstash/stack/drizzle" +import { createEncryptionOperators, EncryptionOperatorError, EncryptionConfigError } from "@cipherstash/stack-drizzle" class EncryptionOperatorError extends Error { context?: { @@ -712,9 +715,9 @@ class EncryptionConfigError extends Error { Encryption client operations return `Result` objects with `data` or `failure`. -## EQL v3 Integration (`@cipherstash/stack/eql/v3/drizzle`) +## EQL v3 Integration (`@cipherstash/stack-drizzle/v3`) -Everything above covers the v2 integration (`@cipherstash/stack/drizzle`). The **EQL v3** typed schema has its own Drizzle integration on the `@cipherstash/stack/eql/v3/drizzle` subpath. In v3 every encrypted column is a concrete Postgres domain (`public.eql_v3_text_search`, `public.eql_v3_integer_ord`, ...) whose query capabilities are fixed by the type — there is no `equality: true` / `freeTextSearch: true` config object. See the `stash-encryption` skill's "EQL v3 Typed Schema" section for the full `types` catalog and capability suffixes (`Eq`, `Ord`/`OrdOre`, `Match`, `Search`). +Everything above covers the v2 integration (`@cipherstash/stack-drizzle`). The **EQL v3** typed schema has its own Drizzle integration on the `@cipherstash/stack-drizzle/v3` subpath. In v3 every encrypted column is a concrete Postgres domain (`public.eql_v3_text_search`, `public.eql_v3_integer_ord`, ...) whose query capabilities are fixed by the type — there is no `equality: true` / `freeTextSearch: true` config object. See the `stash-encryption` skill's "EQL v3 Typed Schema" section for the full `types` catalog and capability suffixes (`Eq`, `Ord`/`OrdOre`, `Match`, `Search`). Exports: `types` (Drizzle-native column factories mirroring the `@cipherstash/stack/eql/v3` namespace), `makeEqlV3Column`, `getEqlV3Column`, `isEqlV3Column`, `extractEncryptionSchemaV3`, `createEncryptionOperatorsV3`, `EncryptionOperatorError`, and the codec helpers `v3ToDriver` / `v3FromDriver` / `EqlV3CodecError`. @@ -727,7 +730,7 @@ import { types, extractEncryptionSchemaV3, createEncryptionOperatorsV3, -} from "@cipherstash/stack/eql/v3/drizzle" +} from "@cipherstash/stack-drizzle/v3" const users = pgTable("users", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), @@ -786,5 +789,5 @@ Differences from the v2 operators to know about: - A `null` operand throws — use `isNull()` / `isNotNull()` for NULL checks. - `inArray` / `notInArray` reject an empty list, and encrypt the whole list in a single `bulkEncrypt` crossing when the client exposes one. - `contains` rejects a needle shorter than the match tokenizer's token length (it would otherwise silently match every row). -- Operators gate on the column's capabilities and throw `EncryptionOperatorError` (with `context.columnName` / `tableName` / `operator`) when the domain can't answer the operator. This `EncryptionOperatorError` is exported from `@cipherstash/stack/eql/v3/drizzle` and is deliberately separate from the v2 class of the same name; there is no `EncryptionConfigError` on the v3 path. +- Operators gate on the column's capabilities and throw `EncryptionOperatorError` (with `context.columnName` / `tableName` / `operator`) when the domain can't answer the operator. This `EncryptionOperatorError` is exported from `@cipherstash/stack-drizzle/v3` and is deliberately separate from the v2 class of the same name; there is no `EncryptionConfigError` on the v3 path. - Every operator takes an optional trailing `{ lockContext, audit }` argument; `createEncryptionOperatorsV3(client, { lockContext, audit })` sets defaults applied to every operand encryption. diff --git a/skills/stash-encryption/SKILL.md b/skills/stash-encryption/SKILL.md index 794e2116b..a90b25b7d 100644 --- a/skills/stash-encryption/SKILL.md +++ b/skills/stash-encryption/SKILL.md @@ -118,8 +118,8 @@ The SDK never logs plaintext data. | `@cipherstash/stack` | `Encryption` function, `encryptedTable`, `encryptedColumn`, `encryptedField` (convenience re-exports) | | `@cipherstash/stack/schema` | `encryptedTable`, `encryptedColumn`, `encryptedField`, schema types | | `@cipherstash/stack/identity` | `LockContext` class and identity types | -| `@cipherstash/stack/drizzle` | `encryptedType`, `extractEncryptionSchema`, `createEncryptionOperators` for Drizzle ORM | -| `@cipherstash/stack/supabase` | `encryptedSupabase` wrapper for Supabase | +| `@cipherstash/stack-drizzle` | `encryptedType`, `extractEncryptionSchema`, `createEncryptionOperators` for Drizzle ORM | +| `@cipherstash/stack-supabase` | `encryptedSupabase` wrapper for Supabase | | `@cipherstash/stack/dynamodb` | `encryptedDynamoDB` helper for DynamoDB | | `@cipherstash/stack/encryption` | `EncryptionClient` class, `Encryption` function | | `@cipherstash/stack/errors` | `EncryptionErrorTypes`, `StackError`, error subtypes, `getErrorMessage` | @@ -127,7 +127,7 @@ The SDK never logs plaintext data. | `@cipherstash/stack/types` | All TypeScript types | | `@cipherstash/stack/eql/v3` | EQL v3 typed schema: `encryptedTable`, `types` namespace, `buildEncryptConfig`, inference types (see "EQL v3 Typed Schema" below) | | `@cipherstash/stack/v3` | `EncryptionV3` factory, `typedClient`, `TypedEncryptionClient` — plus re-exports of everything in `@cipherstash/stack/eql/v3` | -| `@cipherstash/stack/eql/v3/drizzle` | Drizzle ORM integration for EQL v3 schemas (see the `stash-drizzle` skill) | +| `@cipherstash/stack-drizzle/v3` | Drizzle ORM integration for EQL v3 schemas (see the `stash-drizzle` skill) | ## Schema Definition @@ -776,7 +776,7 @@ type UserEncrypted = InferEncrypted ### Drizzle ORM -`@cipherstash/stack/eql/v3/drizzle` provides Drizzle-native v3 column factories, schema extraction, and auto-encrypting query operators. See the `stash-drizzle` skill for the full guide. +`@cipherstash/stack-drizzle/v3` provides Drizzle-native v3 column factories, schema extraction, and auto-encrypting query operators. See the `stash-drizzle` skill for the full guide. ## Rolling Encryption Out to Production diff --git a/skills/stash-supabase/SKILL.md b/skills/stash-supabase/SKILL.md index ff783cfca..847a2108d 100644 --- a/skills/stash-supabase/SKILL.md +++ b/skills/stash-supabase/SKILL.md @@ -1,6 +1,6 @@ --- name: stash-supabase -description: Integrate CipherStash encryption with Supabase using @cipherstash/stack/supabase. Covers the encryptedSupabase wrapper, transparent encryption/decryption on insert/update/select, encrypted query filters (eq, like, ilike, gt/gte/lt/lte, in, or, match), identity-aware encryption, and the complete query builder API. Use when adding encryption to a Supabase project, querying encrypted columns, or building secure Supabase applications. +description: Integrate CipherStash encryption with Supabase using @cipherstash/stack-supabase. Covers the encryptedSupabase wrapper, transparent encryption/decryption on insert/update/select, encrypted query filters (eq, like, ilike, gt/gte/lt/lte, in, or, match), identity-aware encryption, and the complete query builder API. Use when adding encryption to a Supabase project, querying encrypted columns, or building secure Supabase applications. --- # CipherStash Stack - Supabase Integration @@ -18,9 +18,12 @@ Guide for integrating CipherStash field-level encryption with Supabase using the ## Installation ```bash -npm install @cipherstash/stack @supabase/supabase-js +npm install @cipherstash/stack @cipherstash/stack-supabase @supabase/supabase-js ``` +The Supabase integration ships as its own first-party package, +`@cipherstash/stack-supabase`, which depends on `@cipherstash/stack`. Install both. + ## Database Schema Encrypted columns must be stored as JSONB in your Supabase database: @@ -70,7 +73,7 @@ const users = encryptedTable("users", { ```typescript import { createClient } from "@supabase/supabase-js" import { Encryption } from "@cipherstash/stack" -import { encryptedSupabase } from "@cipherstash/stack/supabase" +import { encryptedSupabase } from "@cipherstash/stack-supabase" const supabase = createClient( process.env.SUPABASE_URL!, @@ -322,7 +325,7 @@ const { data, error } = await eSupabase ```typescript import { createClient } from "@supabase/supabase-js" import { Encryption } from "@cipherstash/stack" -import { encryptedSupabase } from "@cipherstash/stack/supabase" +import { encryptedSupabase } from "@cipherstash/stack-supabase" import { encryptedTable, encryptedColumn } from "@cipherstash/stack/schema" // Schema @@ -394,7 +397,7 @@ type EncryptedSupabaseError = { ## Exported Types -`@cipherstash/stack/supabase` also exports the following types: +`@cipherstash/stack-supabase` also exports the following types: - `EncryptedSupabaseConfig` - `EncryptedSupabaseInstance` @@ -420,7 +423,7 @@ The query surface matches v2 — same filter methods, `withLockContext`, ### Setup ```typescript -import { encryptedSupabaseV3 } from "@cipherstash/stack/supabase" +import { encryptedSupabaseV3 } from "@cipherstash/stack-supabase" // Introspects the database via options.databaseUrl or DATABASE_URL const es = await encryptedSupabaseV3(supabaseUrl, supabaseKey) @@ -444,7 +447,7 @@ tables against the database at construction: ```typescript import { encryptedTable, types } from "@cipherstash/stack/eql/v3" -import { encryptedSupabaseV3 } from "@cipherstash/stack/supabase" +import { encryptedSupabaseV3 } from "@cipherstash/stack-supabase" const users = encryptedTable("users", { email: types.TextSearch("email"), // public.eql_v3_text_search — eq + range + free-text From 5e5adc934a58373ab9297b0730653c5f38095abe Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 12:14:07 +1000 Subject: [PATCH 14/17] fix(627): update CLI scaffolding, benches, and example for the adapter split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot flagged remaining @cipherstash/stack/drizzle references outside the docs: - CLI `init` scaffolding (utils.ts) generates user Drizzle schema files — now imports from @cipherstash/stack-drizzle, not the removed @cipherstash/stack/drizzle subpath. `encrypt/context.ts`'s optional runtime import updated to match. - packages/bench: drizzle bench/test/setup import @cipherstash/stack-drizzle; added it as a workspace dependency, and the bench CI job now builds it alongside @cipherstash/stack (the db-only smoke doesn't need it, but the full bench does). - examples/basic already updated in the prior commit. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .github/workflows/tests-bench.yml | 12 +++++++----- .../bench/__benches__/drizzle/operators.bench.ts | 2 +- .../__tests__/drizzle/operators.explain.test.ts | 2 +- packages/bench/package.json | 1 + packages/bench/src/drizzle/setup.ts | 6 +++--- packages/cli/src/commands/encrypt/context.ts | 6 +++--- packages/cli/src/commands/init/utils.ts | 8 ++++---- pnpm-lock.yaml | 3 +++ 8 files changed, 23 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tests-bench.yml b/.github/workflows/tests-bench.yml index eb793707b..829b0bd94 100644 --- a/.github/workflows/tests-bench.yml +++ b/.github/workflows/tests-bench.yml @@ -46,11 +46,13 @@ jobs: run: pnpm recursive install --frozen-lockfile # `@cipherstash/stack` ships dist/-based `exports`; bench imports - # from it (and `@cipherstash/stack/drizzle`) so the package needs - # to be built before vitest can resolve it. Going through turbo - # also picks up `^build` deps if any are added later. - - name: Build @cipherstash/stack - run: pnpm exec turbo run build --filter @cipherstash/stack + # from `@cipherstash/stack` and `@cipherstash/stack-drizzle` (the Drizzle + # adapter split out into its own package), so both must be built before + # vitest can resolve them — the drizzle bench/test files import + # `@cipherstash/stack-drizzle`. Going through turbo also picks up `^build` + # deps (stack-drizzle depends on stack). + - name: Build stack + adapter packages + run: pnpm exec turbo run build --filter @cipherstash/stack --filter @cipherstash/stack-drizzle # Starts the pinned postgres-eql container (PostgreSQL 17 + EQL # pre-installed) via local/docker-compose.yml; waits for healthcheck. diff --git a/packages/bench/__benches__/drizzle/operators.bench.ts b/packages/bench/__benches__/drizzle/operators.bench.ts index 7c931bc6c..4dd7c99af 100644 --- a/packages/bench/__benches__/drizzle/operators.bench.ts +++ b/packages/bench/__benches__/drizzle/operators.bench.ts @@ -1,4 +1,4 @@ -import { createEncryptionOperators } from '@cipherstash/stack/drizzle' +import { createEncryptionOperators } from '@cipherstash/stack-drizzle' import type { SQL } from 'drizzle-orm' import { afterAll, beforeAll, bench, describe } from 'vitest' import { diff --git a/packages/bench/__tests__/drizzle/operators.explain.test.ts b/packages/bench/__tests__/drizzle/operators.explain.test.ts index 1c2bac899..9aec76d4f 100644 --- a/packages/bench/__tests__/drizzle/operators.explain.test.ts +++ b/packages/bench/__tests__/drizzle/operators.explain.test.ts @@ -1,7 +1,7 @@ import { mkdirSync, writeFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import { createEncryptionOperators } from '@cipherstash/stack/drizzle' +import { createEncryptionOperators } from '@cipherstash/stack-drizzle' import type { SQL } from 'drizzle-orm' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { diff --git a/packages/bench/package.json b/packages/bench/package.json index 7dc4d94e0..62318993f 100644 --- a/packages/bench/package.json +++ b/packages/bench/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@cipherstash/stack": "workspace:*", + "@cipherstash/stack-drizzle": "workspace:*", "drizzle-orm": "0.45.2", "pg": "^8.20.0" }, diff --git a/packages/bench/src/drizzle/setup.ts b/packages/bench/src/drizzle/setup.ts index 35490df43..944ece227 100644 --- a/packages/bench/src/drizzle/setup.ts +++ b/packages/bench/src/drizzle/setup.ts @@ -1,9 +1,9 @@ import { Encryption } from '@cipherstash/stack' +import type { EncryptionClient } from '@cipherstash/stack/encryption' import { encryptedType, extractEncryptionSchema, -} from '@cipherstash/stack/drizzle' -import type { EncryptionClient } from '@cipherstash/stack/encryption' +} from '@cipherstash/stack-drizzle' import { drizzle } from 'drizzle-orm/node-postgres' import { pgTable, serial } from 'drizzle-orm/pg-core' import pg from 'pg' @@ -13,7 +13,7 @@ import { getDatabaseUrl } from '../harness/db.js' * Drizzle schema for the bench table. Mirrors `sql/schema.sql`. * * `id` is `serial`; the encrypted columns are `eql_v2_encrypted` composites - * driven by `@cipherstash/stack/drizzle`'s `encryptedType`. + * driven by `@cipherstash/stack-drizzle`'s `encryptedType`. * * Index config flags (`equality`, `freeTextSearch`, `orderAndRange`, * `searchableJson`) are deliberately all on — the bench needs to exercise diff --git a/packages/cli/src/commands/encrypt/context.ts b/packages/cli/src/commands/encrypt/context.ts index c9a80ce46..1975e2d96 100644 --- a/packages/cli/src/commands/encrypt/context.ts +++ b/packages/cli/src/commands/encrypt/context.ts @@ -104,11 +104,11 @@ export async function loadEncryptionContext(): Promise { // Second pass: auto-derive EncryptedTable schemas from drizzle pgTable // exports so users don't have to manually export the result of - // extractEncryptionSchema(). Silently no-op if @cipherstash/stack/drizzle + // extractEncryptionSchema(). Silently no-op if @cipherstash/stack-drizzle // isn't installed (e.g. a Supabase-only project). if (drizzleCandidates.length > 0) { try { - const drizzleModule = (await import('@cipherstash/stack/drizzle')) as { + const drizzleModule = (await import('@cipherstash/stack-drizzle')) as { extractEncryptionSchema?: (t: unknown) => EncryptedTableLike } const extract = drizzleModule.extractEncryptionSchema @@ -127,7 +127,7 @@ export async function loadEncryptionContext(): Promise { } } } catch { - // @cipherstash/stack/drizzle not installed; skip drizzle fallback. + // @cipherstash/stack-drizzle not installed; skip drizzle fallback. } } diff --git a/packages/cli/src/commands/init/utils.ts b/packages/cli/src/commands/init/utils.ts index 7d8e3c4e4..3ccb3a277 100644 --- a/packages/cli/src/commands/init/utils.ts +++ b/packages/cli/src/commands/init/utils.ts @@ -231,7 +231,7 @@ function generateDrizzleFromSchema(schema: SchemaDef): string { }) return `import { pgTable, integer, timestamp } from 'drizzle-orm/pg-core' -import { encryptedType, extractEncryptionSchema } from '@cipherstash/stack/drizzle' +import { encryptedType, extractEncryptionSchema } from '@cipherstash/stack-drizzle' import { Encryption } from '@cipherstash/stack' export const ${varName} = pgTable('${schema.tableName}', { @@ -330,7 +330,7 @@ const ${schemaVarName} = extractEncryptionSchema(${varName})` const schemaVarNames = schemas.map((s) => `${toCamelCase(s.tableName)}Schema`) return `import { pgTable, integer, timestamp } from 'drizzle-orm/pg-core' -import { encryptedType, extractEncryptionSchema } from '@cipherstash/stack/drizzle' +import { encryptedType, extractEncryptionSchema } from '@cipherstash/stack-drizzle' import { Encryption } from '@cipherstash/stack' ${tableDefs.join('\n\n')} @@ -437,7 +437,7 @@ const DRIZZLE_PLACEHOLDER = `/** * * Encrypted twin column for an existing populated column (path 3 — lifecycle): * - * import { encryptedType } from '@cipherstash/stack/drizzle' + * import { encryptedType } from '@cipherstash/stack-drizzle' * * export const users = pgTable('users', { * id: integer('id').primaryKey().generatedAlwaysAsIdentity(), @@ -457,7 +457,7 @@ const DRIZZLE_PLACEHOLDER = `/** * * Once you have encrypted tables declared, harvest them and pass to Encryption(): * - * import { extractEncryptionSchema } from '@cipherstash/stack/drizzle' + * import { extractEncryptionSchema } from '@cipherstash/stack-drizzle' * import { users, orders } from './db/schema' * * export const encryptionClient = await Encryption({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3fb128d10..ea152ac6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -214,6 +214,9 @@ importers: '@cipherstash/stack': specifier: workspace:* version: link:../stack + '@cipherstash/stack-drizzle': + specifier: workspace:* + version: link:../stack-drizzle drizzle-orm: specifier: 0.45.2 version: 0.45.2(@types/pg@8.20.0)(gel@2.2.0)(mysql2@3.16.0)(pg@8.20.0)(postgres@3.4.9) From 67e90b0dd1749ab5f974cf67955f8d3479054d69 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 12:49:49 +1000 Subject: [PATCH 15/17] fix(627): address code-review findings on the adapter split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correctness / packaging: 1. CLI `install-deps` now installs the integration's adapter package (@cipherstash/stack-drizzle / -supabase / prisma-next), not just @cipherstash/stack — a Drizzle `stash init` was scaffolding an import of a package it never installed. 2. Fix the missed rewrite in stack-supabase/integration/helpers/pgrest.ts (`@/supabase/types` → `../../src/types`, a deleted path). 3-4. Drop the spurious required `@cipherstash/protect-ffi` peer from both new packages (never imported in drizzle; type-only in supabase) — kept as devDep; avoids unmet-peer warnings + version conflict on stack's next ffi bump. 5. Remove the now-dead `drizzle-orm`/`@supabase/supabase-js`/`pg` peers + peerDependenciesMeta from `@cipherstash/stack` and the inert tsup `external`. 6. Add README.md to both new packages (they list it in `files`). 7. Guard `encryptedSupabaseV3(url, key)`'s dynamic `@supabase/supabase-js` import with the same friendly-message remap as the `pg` (`loadPg`) path. Efficiency / altitude / cleanup: 8. Document that the second per-job integration vitest run's EQL install self- skips via `isInstalled` (fast no-op, not a re-apply). 9. Document why test-kit's needle-for imports `matchNeedleError` via `@/` rather than the adapter-kit barrel (avoids dragging the encryption/helpers chain into test-kit's strict tsc). 10. De-duplicate the vitest configs: a shared `stackSourceAlias` (the `@/` + wasm stubs) in vitest.shared, and shared `integrationHarness`/`integrationTestDefaults` in @cipherstash/test-kit, so the five near-identical configs stop drifting; removed the redundant per-config adapter-kit override + its false comment. Verified: all three adapter/core packages + cli build (JS+DTS), unit + type tests green, fta gates pass, code:check + frozen-lockfile clean, integration configs load (reach requireIntegrationEnv), and full `tsc` over the packages no longer flags pgrest.ts or the vitest configs. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- .github/workflows/integration-drizzle.yml | 4 + .github/workflows/integration-supabase.yml | 4 + .../src/commands/init/steps/install-deps.ts | 57 ++++++++++---- packages/stack-drizzle/README.md | 34 +++++++++ .../integration/vitest.config.ts | 48 +++--------- packages/stack-drizzle/package.json | 1 - packages/stack-drizzle/vitest.config.ts | 22 +----- packages/stack-supabase/README.md | 33 +++++++++ .../integration/helpers/pgrest.ts | 2 +- .../integration/vitest.config.ts | 48 +++--------- packages/stack-supabase/package.json | 1 - packages/stack-supabase/src/index.ts | 20 ++++- packages/stack-supabase/vitest.config.ts | 33 ++------- packages/stack/integration/vitest.config.ts | 74 +++---------------- packages/stack/package.json | 16 ---- packages/stack/tsup.config.ts | 2 - packages/stack/vitest.config.ts | 30 ++------ packages/test-kit/src/integration/config.ts | 38 ++++++++++ packages/test-kit/src/needle-for.ts | 11 ++- vitest.shared.ts | 22 ++++++ 20 files changed, 256 insertions(+), 244 deletions(-) create mode 100644 packages/stack-drizzle/README.md create mode 100644 packages/stack-supabase/README.md create mode 100644 packages/test-kit/src/integration/config.ts diff --git a/.github/workflows/integration-drizzle.yml b/.github/workflows/integration-drizzle.yml index 9bb406c02..9e5454a3b 100644 --- a/.github/workflows/integration-drizzle.yml +++ b/.github/workflows/integration-drizzle.yml @@ -143,6 +143,10 @@ jobs: - name: Drizzle v3 integration suites run: pnpm --filter @cipherstash/stack-drizzle run test:integration + # A second vitest invocation (stack's shared/ + identity suites live in a + # different package now). Its globalSetup calls the same EQL v3 install, but + # `isInstalled` short-circuits against the DB the first invocation already + # provisioned — a fast no-op check, not a second schema apply. - name: Shared core + identity integration suites run: pnpm --filter @cipherstash/stack run test:integration diff --git a/.github/workflows/integration-supabase.yml b/.github/workflows/integration-supabase.yml index 7100e08db..953e19adb 100644 --- a/.github/workflows/integration-supabase.yml +++ b/.github/workflows/integration-supabase.yml @@ -115,6 +115,10 @@ jobs: - name: Supabase v3 integration suites run: pnpm --filter @cipherstash/stack-supabase run test:integration + # A second vitest invocation (stack's shared/ suites live in a different + # package now). Its globalSetup calls the same EQL v3 install, but + # `isInstalled` short-circuits against the DB the first invocation already + # provisioned — so this is a fast no-op check, not a second schema apply. - name: Shared core integration suites (against Supabase Postgres) run: pnpm --filter @cipherstash/stack run test:integration diff --git a/packages/cli/src/commands/init/steps/install-deps.ts b/packages/cli/src/commands/init/steps/install-deps.ts index afc6a379c..574ee92ab 100644 --- a/packages/cli/src/commands/init/steps/install-deps.ts +++ b/packages/cli/src/commands/init/steps/install-deps.ts @@ -12,16 +12,41 @@ import { const STACK_PACKAGE = '@cipherstash/stack' const CLI_PACKAGE = 'stash' const PRISMA_NEXT_PACKAGE = '@cipherstash/prisma-next' +const DRIZZLE_PACKAGE = '@cipherstash/stack-drizzle' +const SUPABASE_PACKAGE = '@cipherstash/stack-supabase' + +/** + * The integration adapter is its OWN package (depends on `@cipherstash/stack`), + * not a subpath of it — so whichever integration the user picked, its adapter + * package must be installed too, or the scaffolded client code (which imports + * e.g. `@cipherstash/stack-drizzle`) fails to resolve. + */ +function integrationPackageFor(integration?: string): string | null { + switch (integration) { + case 'prisma-next': + return PRISMA_NEXT_PACKAGE + case 'drizzle': + return DRIZZLE_PACKAGE + case 'supabase': + return SUPABASE_PACKAGE + default: + return null + } +} /** * Install the runtime + dev npm packages the user needs to run encryption: * - * - `@cipherstash/stack` (prod) — the encryption client and per-integration - * helpers (drizzle, supabase, schema). + * - `@cipherstash/stack` (prod) — the encryption client, schema builders, and + * EQL v3 typed client. + * - the integration adapter package (prod), if the chosen integration has one: + * `@cipherstash/stack-drizzle`, `@cipherstash/stack-supabase`, or + * `@cipherstash/prisma-next`. These are separate packages that depend on + * `@cipherstash/stack`. * - `stash` (dev) — the CLI itself, so the user can run `stash eql install`, * `stash wizard`, etc. as a project script without the global install. * - * Skips silently when both are already present. Prompts before running the + * Skips silently when everything is already present. Prompts before running the * install commands so the user sees the package manager invocation that's * about to execute. */ @@ -29,18 +54,19 @@ export const installDepsStep: InitStep = { id: 'install-deps', name: 'Install dependencies', async run(state: InitState, provider: InitProvider): Promise { - const wantPrismaNext = - state.integration === 'prisma-next' || provider.name === 'prisma-next' + const integrationPkg = + integrationPackageFor(state.integration) ?? + integrationPackageFor(provider.name) const stackPresent = isPackageInstalled(STACK_PACKAGE) const cliPresent = isPackageInstalled(CLI_PACKAGE) - const prismaNextPresent = wantPrismaNext - ? isPackageInstalled(PRISMA_NEXT_PACKAGE) + const integrationPresent = integrationPkg + ? isPackageInstalled(integrationPkg) : true // Everything already there — silent success, no prompts. - if (stackPresent && cliPresent && prismaNextPresent) { - const installed = wantPrismaNext - ? `${STACK_PACKAGE}, ${PRISMA_NEXT_PACKAGE} and ${CLI_PACKAGE}` + if (stackPresent && cliPresent && integrationPresent) { + const installed = integrationPkg + ? `${STACK_PACKAGE}, ${integrationPkg} and ${CLI_PACKAGE}` : `${STACK_PACKAGE} and ${CLI_PACKAGE}` p.log.success(`${installed} are already installed.`) return { ...state, stackInstalled: true, cliInstalled: true } @@ -49,8 +75,7 @@ export const installDepsStep: InitStep = { const pm = detectPackageManager() const prodPackages: string[] = [] if (!stackPresent) prodPackages.push(STACK_PACKAGE) - if (wantPrismaNext && !prismaNextPresent) - prodPackages.push(PRISMA_NEXT_PACKAGE) + if (integrationPkg && !integrationPresent) prodPackages.push(integrationPkg) const devPackages = cliPresent ? [] : [CLI_PACKAGE] const commands = combinedInstallCommands(pm, prodPackages, devPackages) @@ -109,12 +134,18 @@ export const installDepsStep: InitStep = { // per-package tracking, not a composite flag. const stackInstalled = isPackageInstalled(STACK_PACKAGE) const cliInstalled = isPackageInstalled(CLI_PACKAGE) + const integrationInstalled = integrationPkg + ? isPackageInstalled(integrationPkg) + : true - if (stackInstalled && cliInstalled) { + if (stackInstalled && cliInstalled && integrationInstalled) { p.log.success('Stack dependencies installed.') } else { const stillMissing = [ ...(stackInstalled ? [] : [`${STACK_PACKAGE} (prod)`]), + ...(integrationPkg && !integrationInstalled + ? [`${integrationPkg} (prod)`] + : []), ...(cliInstalled ? [] : [`${CLI_PACKAGE} (dev)`]), ] p.log.warn(`Still missing: ${stillMissing.join(', ')}.`) diff --git a/packages/stack-drizzle/README.md b/packages/stack-drizzle/README.md new file mode 100644 index 000000000..210c33323 --- /dev/null +++ b/packages/stack-drizzle/README.md @@ -0,0 +1,34 @@ +# @cipherstash/stack-drizzle + +Drizzle ORM integration for [CipherStash Stack](https://www.npmjs.com/package/@cipherstash/stack) — +searchable, application-layer field-level encryption for PostgreSQL. + +Depends on `@cipherstash/stack`; install both: + +```bash +npm install @cipherstash/stack @cipherstash/stack-drizzle drizzle-orm +``` + +## EQL v2 (package root) + +```ts +import { encryptedType, extractEncryptionSchema, createEncryptionOperators } from '@cipherstash/stack-drizzle' +import { Encryption } from '@cipherstash/stack' +``` + +`encryptedType` defines an `eql_v2_encrypted` column; `createEncryptionOperators` +returns query operators (`eq`, `like`, `gt`, `inArray`, …) that transparently +encrypt search values. + +## EQL v3 (`/v3` subpath) + +```ts +import { types, createEncryptionOperatorsV3, extractEncryptionSchemaV3, makeEqlV3Column } from '@cipherstash/stack-drizzle/v3' +``` + +Each encrypted column is a concrete `public.eql_v3_*` Postgres domain; query +capabilities are fixed by the chosen `types.*` factory. + +See the `stash-drizzle` agent skill and https://cipherstash.com/docs for the full guide. + +> Not to be confused with `@cipherstash/drizzle`, the older `@cipherstash/protect`-based package. diff --git a/packages/stack-drizzle/integration/vitest.config.ts b/packages/stack-drizzle/integration/vitest.config.ts index 7ec6aebf1..b5f6762dc 100644 --- a/packages/stack-drizzle/integration/vitest.config.ts +++ b/packages/stack-drizzle/integration/vitest.config.ts @@ -1,48 +1,22 @@ import { resolve } from 'node:path' import { defineConfig } from 'vitest/config' -import { sharedAlias } from '../../../vitest.shared' +import { sharedAlias, stackSourceAlias } from '../../../vitest.shared' +import { + integrationHarness, + integrationTestDefaults, +} from '../../test-kit/src/integration/config' /** - * Integration suites for the Drizzle adapter: real ZeroKMS, real Postgres, real - * PostgREST. Runs only under `test:integration` (its own CI job). The shared - * harness — `global-setup` (installs EQL v3) and the no-skips reporter — lives in - * `@cipherstash/test-kit/src/integration` so both adapter packages share one copy. + * Integration suites for the drizzle adapter: real ZeroKMS + real Postgres + * (+ PostgREST for supabase). Runs only under `test:integration` (its own CI + * job). Harness + shared `test.*` knobs come from `@cipherstash/test-kit`. */ -const TEST_KIT_INT = resolve(__dirname, '../../test-kit/src/integration') - export default defineConfig({ - resolve: { - alias: { - ...sharedAlias, - '@cipherstash/stack/adapter-kit': resolve( - __dirname, - '../../stack/src/adapter-kit.ts', - ), - '@/': resolve(__dirname, '../../stack/src') + '/', - '@cipherstash/protect-ffi/wasm-inline': resolve( - __dirname, - '../../stack/__tests__/helpers/stub-protect-ffi-wasm-inline.ts', - ), - '@cipherstash/auth/wasm-inline': resolve( - __dirname, - '../../stack/__tests__/helpers/stub-auth-wasm-inline.ts', - ), - }, - }, + resolve: { alias: { ...sharedAlias, ...stackSourceAlias } }, test: { root: resolve(__dirname, '..'), include: ['integration/**/*.integration.test.ts'], - globalSetup: [resolve(TEST_KIT_INT, 'global-setup.ts')], - server: { - deps: { - inline: [/packages\/test-kit/], - }, - }, - testTimeout: 60_000, - hookTimeout: 180_000, - fileParallelism: false, - silent: 'passed-only', - passWithNoTests: false, - reporters: ['default', resolve(TEST_KIT_INT, 'no-skips-reporter.ts')], + ...integrationHarness, + ...integrationTestDefaults, }, }) diff --git a/packages/stack-drizzle/package.json b/packages/stack-drizzle/package.json index ed702ac47..8a7139c47 100644 --- a/packages/stack-drizzle/package.json +++ b/packages/stack-drizzle/package.json @@ -74,7 +74,6 @@ "@byteslice/result": "0.2.0" }, "peerDependencies": { - "@cipherstash/protect-ffi": "0.29.0", "drizzle-orm": ">=0.33" }, "devDependencies": { diff --git a/packages/stack-drizzle/vitest.config.ts b/packages/stack-drizzle/vitest.config.ts index 810e666a1..9fed3fefd 100644 --- a/packages/stack-drizzle/vitest.config.ts +++ b/packages/stack-drizzle/vitest.config.ts @@ -1,26 +1,10 @@ -import { resolve } from 'node:path' import { configDefaults, defineConfig } from 'vitest/config' -import { sharedAlias } from '../../vitest.shared' +import { sharedAlias, stackSourceAlias } from '../../vitest.shared' export default defineConfig({ resolve: { - alias: { - ...sharedAlias, - '@cipherstash/stack/adapter-kit': resolve( - __dirname, - '../stack/src/adapter-kit.ts', - ), - // stack source uses its internal `@/`; this package's code never does. - '@/': resolve(__dirname, '../stack/src') + '/', - '@cipherstash/protect-ffi/wasm-inline': resolve( - __dirname, - '../stack/__tests__/helpers/stub-protect-ffi-wasm-inline.ts', - ), - '@cipherstash/auth/wasm-inline': resolve( - __dirname, - '../stack/__tests__/helpers/stub-auth-wasm-inline.ts', - ), - }, + // See stack-supabase/vitest.config.ts for what these two alias blocks do. + alias: { ...sharedAlias, ...stackSourceAlias }, }, test: { exclude: [...configDefaults.exclude, 'integration/**'], diff --git a/packages/stack-supabase/README.md b/packages/stack-supabase/README.md new file mode 100644 index 000000000..4e5a3e063 --- /dev/null +++ b/packages/stack-supabase/README.md @@ -0,0 +1,33 @@ +# @cipherstash/stack-supabase + +Supabase integration for [CipherStash Stack](https://www.npmjs.com/package/@cipherstash/stack) — +transparent, searchable field-level encryption on top of a Supabase (PostgREST) client. + +Depends on `@cipherstash/stack`; install both: + +```bash +npm install @cipherstash/stack @cipherstash/stack-supabase @supabase/supabase-js +``` + +## EQL v3 (recommended) + +`encryptedSupabaseV3` introspects the database at connect time (native +`public.eql_v3_*` column domains) — no schema argument, `select('*')` support, +`contains()` free-text search, and encrypted `order()` on OPE columns. + +```ts +import { encryptedSupabaseV3 } from '@cipherstash/stack-supabase' + +const es = await encryptedSupabaseV3(supabaseUrl, supabaseKey) +await es.from('users').select('id, email').eq('email', 'a@b.com') +``` + +Introspection needs a direct Postgres connection (`DATABASE_URL`), so `pg` is an +optional peer and the factory cannot run in a Worker or the browser. + +## EQL v2 (legacy) + +`encryptedSupabase` wraps a supabase-js client with a v2 schema; still shipped for +existing v2 deployments. + +See the `stash-supabase` agent skill and https://cipherstash.com/docs for the full guide. diff --git a/packages/stack-supabase/integration/helpers/pgrest.ts b/packages/stack-supabase/integration/helpers/pgrest.ts index 56fcc8681..dfd59799f 100644 --- a/packages/stack-supabase/integration/helpers/pgrest.ts +++ b/packages/stack-supabase/integration/helpers/pgrest.ts @@ -12,7 +12,7 @@ import { PostgrestClient } from '@supabase/postgrest-js' import type postgres from 'postgres' -import type { SupabaseClientLike } from '@/supabase/types' +import type { SupabaseClientLike } from '../../src/types' export function makePostgrestClient(): SupabaseClientLike { const url = process.env.PGRST_URL diff --git a/packages/stack-supabase/integration/vitest.config.ts b/packages/stack-supabase/integration/vitest.config.ts index 5c18db824..4dc30e9c2 100644 --- a/packages/stack-supabase/integration/vitest.config.ts +++ b/packages/stack-supabase/integration/vitest.config.ts @@ -1,48 +1,22 @@ import { resolve } from 'node:path' import { defineConfig } from 'vitest/config' -import { sharedAlias } from '../../../vitest.shared' +import { sharedAlias, stackSourceAlias } from '../../../vitest.shared' +import { + integrationHarness, + integrationTestDefaults, +} from '../../test-kit/src/integration/config' /** - * Integration suites for the Supabase adapter: real ZeroKMS, real Postgres, real - * PostgREST. Runs only under `test:integration` (its own CI job). The shared - * harness — `global-setup` (installs EQL v3) and the no-skips reporter — lives in - * `@cipherstash/test-kit/src/integration` so both adapter packages share one copy. + * Integration suites for the supabase adapter: real ZeroKMS + real Postgres + * (+ PostgREST for supabase). Runs only under `test:integration` (its own CI + * job). Harness + shared `test.*` knobs come from `@cipherstash/test-kit`. */ -const TEST_KIT_INT = resolve(__dirname, '../../test-kit/src/integration') - export default defineConfig({ - resolve: { - alias: { - ...sharedAlias, - '@cipherstash/stack/adapter-kit': resolve( - __dirname, - '../../stack/src/adapter-kit.ts', - ), - '@/': resolve(__dirname, '../../stack/src') + '/', - '@cipherstash/protect-ffi/wasm-inline': resolve( - __dirname, - '../../stack/__tests__/helpers/stub-protect-ffi-wasm-inline.ts', - ), - '@cipherstash/auth/wasm-inline': resolve( - __dirname, - '../../stack/__tests__/helpers/stub-auth-wasm-inline.ts', - ), - }, - }, + resolve: { alias: { ...sharedAlias, ...stackSourceAlias } }, test: { root: resolve(__dirname, '..'), include: ['integration/**/*.integration.test.ts'], - globalSetup: [resolve(TEST_KIT_INT, 'global-setup.ts')], - server: { - deps: { - inline: [/packages\/test-kit/], - }, - }, - testTimeout: 60_000, - hookTimeout: 180_000, - fileParallelism: false, - silent: 'passed-only', - passWithNoTests: false, - reporters: ['default', resolve(TEST_KIT_INT, 'no-skips-reporter.ts')], + ...integrationHarness, + ...integrationTestDefaults, }, }) diff --git a/packages/stack-supabase/package.json b/packages/stack-supabase/package.json index e7430f4b2..0872f5fdb 100644 --- a/packages/stack-supabase/package.json +++ b/packages/stack-supabase/package.json @@ -55,7 +55,6 @@ "@cipherstash/stack": "workspace:*" }, "peerDependencies": { - "@cipherstash/protect-ffi": "0.29.0", "@supabase/supabase-js": ">=2", "pg": ">=8" }, diff --git a/packages/stack-supabase/src/index.ts b/packages/stack-supabase/src/index.ts index 4dbb70dff..6b3af298c 100644 --- a/packages/stack-supabase/src/index.ts +++ b/packages/stack-supabase/src/index.ts @@ -168,7 +168,25 @@ export async function encryptedSupabaseV3( const url = clientOrUrl const key = keyOrOptions as string options = maybeOptions ?? {} - const { createClient } = await import('@supabase/supabase-js') + // `@supabase/supabase-js` is an optional peer: the url+key overload needs it + // to construct a client, but the (client) overload does not. Remap a missing + // install to an actionable message, matching `loadPg`. Guard on `err.code` + // (CJS `MODULE_NOT_FOUND`, ESM `ERR_MODULE_NOT_FOUND`), not message text. + let createClient: (url: string, key: string) => unknown + try { + ;({ createClient } = await import('@supabase/supabase-js')) + } catch (err) { + const code = (err as { code?: string }).code + if (code !== 'MODULE_NOT_FOUND' && code !== 'ERR_MODULE_NOT_FOUND') + throw err + throw new Error( + "[supabase v3]: encryptedSupabaseV3(url, key) needs '@supabase/supabase-js' " + + 'to build the client, but that optional peer dependency is not installed. ' + + 'Install it (`npm install @supabase/supabase-js`), or pass an existing ' + + 'client: encryptedSupabaseV3(supabaseClient, options).', + { cause: err }, + ) + } supabaseClient = createClient(url, key) as unknown as SupabaseClientLike } else { supabaseClient = clientOrUrl diff --git a/packages/stack-supabase/vitest.config.ts b/packages/stack-supabase/vitest.config.ts index 92b4ac85e..9931d316c 100644 --- a/packages/stack-supabase/vitest.config.ts +++ b/packages/stack-supabase/vitest.config.ts @@ -1,35 +1,12 @@ -import { resolve } from 'node:path' import { configDefaults, defineConfig } from 'vitest/config' -import { sharedAlias } from '../../vitest.shared' +import { sharedAlias, stackSourceAlias } from '../../vitest.shared' export default defineConfig({ resolve: { - alias: { - // Resolve `@cipherstash/stack` + its subpaths (and `@cipherstash/test-kit`) - // to SOURCE, so `pnpm test` here is not coupled to a prior stack build. - ...sharedAlias, - // adapter-kit is the core↔adapter seam; not in the shared block because - // only the adapter packages consume it. - '@cipherstash/stack/adapter-kit': resolve( - __dirname, - '../stack/src/adapter-kit.ts', - ), - // We resolve `@cipherstash/stack` to its SOURCE (via sharedAlias), and that - // source uses stack's internal `@/` alias — so it must resolve here too. - // This package's own code never uses `@/`, so there is no collision. - '@/': resolve(__dirname, '../stack/src') + '/', - // stack's `src/wasm-inline.ts` imports the `/wasm-inline` subpaths of - // protect-ffi/auth, which aren't resolvable by Vitest; alias to stack's - // stubs so loading stack source in tests doesn't fail on them. - '@cipherstash/protect-ffi/wasm-inline': resolve( - __dirname, - '../stack/__tests__/helpers/stub-protect-ffi-wasm-inline.ts', - ), - '@cipherstash/auth/wasm-inline': resolve( - __dirname, - '../stack/__tests__/helpers/stub-auth-wasm-inline.ts', - ), - }, + // `sharedAlias` resolves `@cipherstash/stack` + its subpaths (incl. + // adapter-kit) and `@cipherstash/test-kit` to SOURCE; `stackSourceAlias` adds + // stack's internal `@/` alias + the wasm-inline stubs that source needs. + alias: { ...sharedAlias, ...stackSourceAlias }, }, test: { // Integration suites require credentials + a database + PostgREST and THROW diff --git a/packages/stack/integration/vitest.config.ts b/packages/stack/integration/vitest.config.ts index 62c40056d..df1a71441 100644 --- a/packages/stack/integration/vitest.config.ts +++ b/packages/stack/integration/vitest.config.ts @@ -1,7 +1,11 @@ import { existsSync } from 'node:fs' import { resolve } from 'node:path' import { defineConfig } from 'vitest/config' -import { sharedAlias } from '../../../vitest.shared' +import { sharedAlias, stackSourceAlias } from '../../../vitest.shared' +import { + integrationHarness, + integrationTestDefaults, +} from '../../test-kit/src/integration/config' /** * Integration suites: real ZeroKMS, real Postgres, real PostgREST. @@ -61,71 +65,13 @@ for (const glob of SUITE_GLOBS) { } export default defineConfig({ - resolve: { - alias: { - ...sharedAlias, - '@/': resolve(__dirname, '../src') + '/', - '@cipherstash/protect-ffi/wasm-inline': resolve( - __dirname, - '../__tests__/helpers/stub-protect-ffi-wasm-inline.ts', - ), - '@cipherstash/auth/wasm-inline': resolve( - __dirname, - '../__tests__/helpers/stub-auth-wasm-inline.ts', - ), - }, - }, + resolve: { alias: { ...sharedAlias, ...stackSourceAlias } }, test: { root: resolve(__dirname, '..'), + // Unlike the adapter packages (fixed glob), stack's integration job scopes to + // its shared/ + identity/ suites via CS_IT_SUITE — hence the guard above. include: SUITE_GLOBS, - globalSetup: [ - resolve(__dirname, '../../test-kit/src/integration/global-setup.ts'), - ], - server: { - deps: { - // `@cipherstash/test-kit` resolves to source in ANOTHER package, i.e. - // outside this config's root, so Vitest externalizes it and loads it - // through Node rather than the transform pipeline. Its driver imports - // `vitest`, and a `vitest` imported outside a worker cannot reach the - // runner's state: "Vitest failed to access its internal state". - inline: [/packages\/test-kit/], - }, - }, - // Real crypto round-trips over the network. The unit config uses 30s for the - // same reason; seeding a family table encrypts every sample, so hooks need - // more headroom than tests do. - testTimeout: 60_000, - hookTimeout: 180_000, - // One database, shared tables. File-level parallelism would have two family - // suites installing EQL and reloading the PostgREST schema cache at once. - fileParallelism: false, - - /** - * Show console output only for tests that FAIL. - * - * The capability-rejection tests are the bulk of the suite, and each one - * makes the adapter refuse an operation. `EncryptedQueryBuilderImpl.execute` - * logs `logger.error(...)` before returning its `Result` error, so every - * passing rejection emits an ERROR block with a stack trace. A CI run - * printed 213 of them — all from passing tests — which is enough noise to - * bury a real failure and enough to make a green job look broken. - * - * `'passed-only'` suppresses those and keeps the logs of any test that - * actually fails, which is when they are worth reading. The stack logger has - * no silent level (`STASH_STACK_LOG` bottoms out at `error`), so this is the - * only place to do it without changing product logging behaviour. - */ - silent: 'passed-only', - - // A fully-empty run is a failure, not a pass: paired with the per-glob - // directory guard above, a mistyped/renamed suite path can never go green. - passWithNoTests: false, - - // Fail the run if anything is skipped. A skipped test reads exactly like a - // passing one, and every silent hole this suite has found took that shape. - reporters: [ - 'default', - resolve(__dirname, '../../test-kit/src/integration/no-skips-reporter.ts'), - ], + ...integrationHarness, + ...integrationTestDefaults, }, }) diff --git a/packages/stack/package.json b/packages/stack/package.json index 7b0713905..1106aaaad 100644 --- a/packages/stack/package.json +++ b/packages/stack/package.json @@ -242,22 +242,6 @@ "@cipherstash/auth-linux-x64-musl": "catalog:repo", "@cipherstash/auth-win32-x64-msvc": "catalog:repo" }, - "peerDependencies": { - "@supabase/supabase-js": ">=2", - "drizzle-orm": ">=0.33", - "pg": ">=8" - }, - "peerDependenciesMeta": { - "drizzle-orm": { - "optional": true - }, - "@supabase/supabase-js": { - "optional": true - }, - "pg": { - "optional": true - } - }, "engines": { "node": ">=22" } diff --git a/packages/stack/tsup.config.ts b/packages/stack/tsup.config.ts index c482fda6e..413934516 100644 --- a/packages/stack/tsup.config.ts +++ b/packages/stack/tsup.config.ts @@ -27,7 +27,6 @@ export default defineConfig([ clean: false, target: 'es2022', tsconfig: './tsconfig.json', - external: ['drizzle-orm', '@supabase/supabase-js', 'pg'], // zod + @byteslice/result are bundled so dist/wasm-inline.js carries // no bare-specifier transitive imports — important for Deno / Edge / // browser consumers whose runtime won't resolve npm names without an @@ -49,7 +48,6 @@ export default defineConfig([ clean: false, target: 'es2022', tsconfig: './tsconfig.json', - external: ['drizzle-orm', '@supabase/supabase-js', 'pg'], noExternal: ['evlog', 'uuid', 'zod', '@byteslice/result'], }, ]) diff --git a/packages/stack/vitest.config.ts b/packages/stack/vitest.config.ts index b2aeac8c1..06413417f 100644 --- a/packages/stack/vitest.config.ts +++ b/packages/stack/vitest.config.ts @@ -1,29 +1,15 @@ -import { resolve } from 'node:path' import { configDefaults, defineConfig } from 'vitest/config' -import { sharedAlias } from '../../vitest.shared' +import { sharedAlias, stackSourceAlias } from '../../vitest.shared' export default defineConfig({ resolve: { - alias: { - // `@cipherstash/test-kit` + the stack subpaths its catalog imports, all - // resolved to source. Must precede `@/` only in spirit — the keys do not - // overlap — but keep it first so the shared block is obvious. - ...sharedAlias, - '@/': resolve(__dirname, './src') + '/', - // The installed `@cipherstash/{protect-ffi,auth}` only export `.`; their - // `/wasm-inline` subpaths (imported by `src/wasm-inline.ts`) are not - // resolvable by Vitest. Alias them to local stubs so unit tests that only - // exercise pure helpers can load the module. Tests needing real WASM - // behaviour mock these specifiers explicitly. - '@cipherstash/protect-ffi/wasm-inline': resolve( - __dirname, - './__tests__/helpers/stub-protect-ffi-wasm-inline.ts', - ), - '@cipherstash/auth/wasm-inline': resolve( - __dirname, - './__tests__/helpers/stub-auth-wasm-inline.ts', - ), - }, + // `sharedAlias` resolves `@cipherstash/test-kit` + the stack subpaths its + // catalog imports to source; `stackSourceAlias` adds stack's own `@/` alias + // and the `/wasm-inline` stubs (`src/wasm-inline.ts` imports subpaths Vitest + // can't resolve; unit tests that only touch pure helpers load the stub, tests + // needing real WASM mock the specifiers explicitly). Both point at fixed + // locations under packages/stack, so they live in vitest.shared. + alias: { ...sharedAlias, ...stackSourceAlias }, }, test: { // Integration suites live in `integration/` and require credentials, a diff --git a/packages/test-kit/src/integration/config.ts b/packages/test-kit/src/integration/config.ts new file mode 100644 index 000000000..4e224c87e --- /dev/null +++ b/packages/test-kit/src/integration/config.ts @@ -0,0 +1,38 @@ +import { resolve } from 'node:path' + +/** + * Shared integration-suite vitest wiring, so the adapter packages' near-identical + * `integration/vitest.config.ts` files don't drift. The harness (`global-setup`, + * the no-skips reporter) lives beside this file in `@cipherstash/test-kit`. + * + * Each package still owns its own `defineConfig`, `resolve.alias` + * (`...sharedAlias, ...stackSourceAlias`), and `test.root` — only the harness + * paths and the shared `test.*` knobs come from here. + */ +const HERE = __dirname + +// Not `as const`: vitest's `test` config wants mutable `string[]` for +// globalSetup/reporters and the `'passed-only'` literal for `silent`; a blanket +// `as const` makes the arrays `readonly` and fails `defineConfig`'s overload. + +/** `globalSetup` (installs EQL v3) + the fail-on-skip reporter. */ +export const integrationHarness = { + globalSetup: [resolve(HERE, 'global-setup.ts')], + reporters: ['default', resolve(HERE, 'no-skips-reporter.ts')], +} + +/** + * The `test.*` knobs every integration suite shares: `@cipherstash/test-kit` must + * be inlined (it imports `vitest` and is externalized as out-of-root source); + * real crypto over the network needs generous timeouts; suites share one DB so no + * file parallelism; suppress passing-rejection ERROR noise; a fully-empty run is a + * failure, not a pass. + */ +export const integrationTestDefaults = { + server: { deps: { inline: [/packages\/test-kit/] } }, + testTimeout: 60_000, + hookTimeout: 180_000, + fileParallelism: false, + silent: 'passed-only' as const, + passWithNoTests: false, +} diff --git a/packages/test-kit/src/needle-for.ts b/packages/test-kit/src/needle-for.ts index 2ca881ee5..13f62b86c 100644 --- a/packages/test-kit/src/needle-for.ts +++ b/packages/test-kit/src/needle-for.ts @@ -1,5 +1,12 @@ -// Import directly from stack's schema helper (not `@cipherstash/stack/adapter-kit`) -// so test-kit's strict `tsc` graph does not pull in the encryption/helpers chain. +// test-kit is an internal, source-only package that consumes stack through the +// `@/` alias (like `catalog.ts` does) — not through the published adapter seam. +// So we import `matchNeedleError` straight from `@/schema/match-defaults` rather +// than `@cipherstash/stack/adapter-kit`: the adapter-kit BARREL also re-exports +// `bulkModelsToEncryptedPgComposites` from `encryption/helpers`, and importing +// anything from that barrel drags the whole encryption/helpers → jsonb chain into +// test-kit's strict `tsc` graph (whose stricter options flag pre-existing loose +// types there). The published adapter packages, which have no `@/` alias, use the +// adapter-kit seam as intended; test-kit does not need it. import { matchNeedleError } from '@/schema/match-defaults' import type { V3_MATRIX } from './catalog' diff --git a/vitest.shared.ts b/vitest.shared.ts index 6fe89d665..068aaec06 100644 --- a/vitest.shared.ts +++ b/vitest.shared.ts @@ -90,3 +90,25 @@ export const sharedAlias: Record = { // `packages/stack/dist`, re-coupling `pnpm test` to a prior `pnpm build`. '@cipherstash/stack': resolve(repoRoot, 'packages/stack/src/index.ts'), } + +/** + * The aliases needed to load stack's SOURCE (not just its public subpaths) from a + * vitest config: stack's own internal `@/` alias — which its source uses and which + * therefore leaks into any package that source-resolves it — plus the wasm-inline + * stubs for the two `/wasm-inline` subpaths Vitest cannot resolve. All resolve to + * fixed locations under `packages/stack`, so they are shared here rather than + * copy-pasted into every package's vitest config. + * + * Spread AFTER `sharedAlias`: `resolve: { alias: { ...sharedAlias, ...stackSourceAlias } }`. + */ +export const stackSourceAlias: Record = { + '@/': `${resolve(repoRoot, 'packages/stack/src')}/`, + '@cipherstash/protect-ffi/wasm-inline': resolve( + repoRoot, + 'packages/stack/__tests__/helpers/stub-protect-ffi-wasm-inline.ts', + ), + '@cipherstash/auth/wasm-inline': resolve( + repoRoot, + 'packages/stack/__tests__/helpers/stub-auth-wasm-inline.ts', + ), +} From 83d5c8a9b3041d16c5d7f4a3624dffff724050f1 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 13:49:44 +1000 Subject: [PATCH 16/17] docs(627): address review feedback on adapter package metadata + README - stack-drizzle/stack-supabase package.json: drop EQL from the descriptions (say "searchable field-level encryption"), remove the `eql` keyword, and add `encryption`, `searchable-encryption`, and `security` keywords. - stack-drizzle README: lead with EQL v3 (with an insert/query/decrypt money-shot), mark v2 as legacy, and link full docs to cipherstash.com/docs/integrations/drizzle. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- packages/stack-drizzle/README.md | 63 +++++++++++++++++++++++----- packages/stack-drizzle/package.json | 6 ++- packages/stack-supabase/package.json | 6 ++- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/packages/stack-drizzle/README.md b/packages/stack-drizzle/README.md index 210c33323..097bb9f73 100644 --- a/packages/stack-drizzle/README.md +++ b/packages/stack-drizzle/README.md @@ -9,7 +9,56 @@ Depends on `@cipherstash/stack`; install both: npm install @cipherstash/stack @cipherstash/stack-drizzle drizzle-orm ``` -## EQL v2 (package root) +## EQL v3 (`/v3` subpath) + +Each encrypted column is a concrete `public.eql_v3_*` Postgres domain whose query +capabilities are fixed by the `types.*` factory you choose — no per-column config +object. Install the domains once with `stash eql install --eql-version 3`. + +```ts +import { pgTable, integer } from 'drizzle-orm/pg-core' +import { EncryptionV3 } from '@cipherstash/stack/v3' +import { + types, + extractEncryptionSchemaV3, + createEncryptionOperatorsV3, +} from '@cipherstash/stack-drizzle/v3' + +const users = pgTable('users', { + id: integer('id').primaryKey().generatedAlwaysAsIdentity(), + email: types.TextSearch('email'), // equality + order/range + free-text + age: types.IntegerOrd('age'), // equality + order/range +}) + +const schema = extractEncryptionSchemaV3(users) +const client = await EncryptionV3({ schemas: [schema] }) +const ops = createEncryptionOperatorsV3(client) + +// Insert — encrypt models first +const enc = await client.bulkEncryptModels( + [{ email: 'alice@example.com', age: 30 }], + schema, +) +if (!enc.failure) await db.insert(users).values(enc.data) + +// Query — operators auto-encrypt their plaintext operands +const rows = await db + .select() + .from(users) + .where(await ops.and( + ops.contains(users.email, 'alice'), // free-text containment over ciphertext + ops.between(users.age, 18, 65), + )) + .orderBy(ops.asc(users.age)) + +// Decrypt after select +const dec = await client.bulkDecryptModels(rows, schema) +``` + +## EQL v2 (package root) — legacy + +The v2 integration predates the typed v3 domains and is kept for existing +projects. New projects should use v3 above. ```ts import { encryptedType, extractEncryptionSchema, createEncryptionOperators } from '@cipherstash/stack-drizzle' @@ -20,15 +69,9 @@ import { Encryption } from '@cipherstash/stack' returns query operators (`eq`, `like`, `gt`, `inArray`, …) that transparently encrypt search values. -## EQL v3 (`/v3` subpath) - -```ts -import { types, createEncryptionOperatorsV3, extractEncryptionSchemaV3, makeEqlV3Column } from '@cipherstash/stack-drizzle/v3' -``` - -Each encrypted column is a concrete `public.eql_v3_*` Postgres domain; query -capabilities are fixed by the chosen `types.*` factory. +## Docs -See the `stash-drizzle` agent skill and https://cipherstash.com/docs for the full guide. +Full guide: https://cipherstash.com/docs/integrations/drizzle — see also the +bundled `stash-drizzle` agent skill. > Not to be confused with `@cipherstash/drizzle`, the older `@cipherstash/protect`-based package. diff --git a/packages/stack-drizzle/package.json b/packages/stack-drizzle/package.json index 8a7139c47..540ce71da 100644 --- a/packages/stack-drizzle/package.json +++ b/packages/stack-drizzle/package.json @@ -1,13 +1,15 @@ { "name": "@cipherstash/stack-drizzle", "version": "0.0.0", - "description": "CipherStash Stack Drizzle ORM integration: encrypted column types and query operators for EQL v2 (./) and EQL v3 (./v3).", + "description": "CipherStash Stack Drizzle ORM integration: searchable, application-layer field-level encryption for PostgreSQL.", "keywords": [ "encrypted", + "encryption", + "searchable-encryption", + "security", "drizzle", "orm", "postgres", - "eql", "cipherstash", "stack" ], diff --git a/packages/stack-supabase/package.json b/packages/stack-supabase/package.json index 0872f5fdb..008358513 100644 --- a/packages/stack-supabase/package.json +++ b/packages/stack-supabase/package.json @@ -1,12 +1,14 @@ { "name": "@cipherstash/stack-supabase", "version": "0.0.0", - "description": "CipherStash Stack Supabase integration: transparent field-level encryption for Supabase with EQL v3 (encryptedSupabaseV3, connect-time introspection) and v2 (encryptedSupabase).", + "description": "CipherStash Stack Supabase integration: transparent, searchable field-level encryption for Supabase.", "keywords": [ "encrypted", + "encryption", + "searchable-encryption", + "security", "supabase", "postgres", - "eql", "cipherstash", "stack" ], From 053041c6a1574e6eec15c12e8e26bbea5828e71b Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Mon, 13 Jul 2026 14:11:03 +1000 Subject: [PATCH 17/17] docs(627): alias v3 types as encryptedTypes in the drizzle README example Reads as encryptedTypes.TextSearch('email') / encryptedTypes.IntegerOrd('age'), making the schema clearer at a glance (review feedback on #630). Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w --- packages/stack-drizzle/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stack-drizzle/README.md b/packages/stack-drizzle/README.md index 097bb9f73..91f6ec00e 100644 --- a/packages/stack-drizzle/README.md +++ b/packages/stack-drizzle/README.md @@ -19,15 +19,15 @@ object. Install the domains once with `stash eql install --eql-version 3`. import { pgTable, integer } from 'drizzle-orm/pg-core' import { EncryptionV3 } from '@cipherstash/stack/v3' import { - types, + types as encryptedTypes, extractEncryptionSchemaV3, createEncryptionOperatorsV3, } from '@cipherstash/stack-drizzle/v3' const users = pgTable('users', { id: integer('id').primaryKey().generatedAlwaysAsIdentity(), - email: types.TextSearch('email'), // equality + order/range + free-text - age: types.IntegerOrd('age'), // equality + order/range + email: encryptedTypes.TextSearch('email'), // equality + order/range + free-text + age: encryptedTypes.IntegerOrd('age'), // equality + order/range }) const schema = extractEncryptionSchemaV3(users)