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/.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/.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/.github/workflows/integration-drizzle.yml b/.github/workflows/integration-drizzle.yml index 526c1c1b7..9e5454a3b 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,13 @@ 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 + + # 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 - name: Stop ${{ matrix.db }} diff --git a/.github/workflows/integration-supabase.yml b/.github/workflows/integration-supabase.yml index 6143e7f9c..953e19adb 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,13 @@ 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 + + # 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 - name: Stop ${{ matrix.db }} 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/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/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? 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 87c6c8fd9..8a2f8e242 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -12,6 +12,8 @@ "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/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/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/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/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/packages/stack-drizzle/README.md b/packages/stack-drizzle/README.md new file mode 100644 index 000000000..91f6ec00e --- /dev/null +++ b/packages/stack-drizzle/README.md @@ -0,0 +1,77 @@ +# @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 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 as encryptedTypes, + extractEncryptionSchemaV3, + createEncryptionOperatorsV3, +} from '@cipherstash/stack-drizzle/v3' + +const users = pgTable('users', { + id: integer('id').primaryKey().generatedAlwaysAsIdentity(), + email: encryptedTypes.TextSearch('email'), // equality + order/range + free-text + age: encryptedTypes.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' +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. + +## Docs + +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/__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/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..293c34853 100644 --- a/packages/stack/integration/identity/lock-context.integration.test.ts +++ b/packages/stack-drizzle/integration/lock-context.integration.test.ts @@ -32,20 +32,20 @@ * 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' 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..b5f6762dc --- /dev/null +++ b/packages/stack-drizzle/integration/vitest.config.ts @@ -0,0 +1,22 @@ +import { resolve } from 'node:path' +import { defineConfig } from 'vitest/config' +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 + * (+ PostgREST for supabase). Runs only under `test:integration` (its own CI + * job). Harness + shared `test.*` knobs come from `@cipherstash/test-kit`. + */ +export default defineConfig({ + resolve: { alias: { ...sharedAlias, ...stackSourceAlias } }, + test: { + root: resolve(__dirname, '..'), + include: ['integration/**/*.integration.test.ts'], + ...integrationHarness, + ...integrationTestDefaults, + }, +}) diff --git a/packages/stack-drizzle/package.json b/packages/stack-drizzle/package.json new file mode 100644 index 000000000..540ce71da --- /dev/null +++ b/packages/stack-drizzle/package.json @@ -0,0 +1,98 @@ +{ + "name": "@cipherstash/stack-drizzle", + "version": "0.0.0", + "description": "CipherStash Stack Drizzle ORM integration: searchable, application-layer field-level encryption for PostgreSQL.", + "keywords": [ + "encrypted", + "encryption", + "searchable-encryption", + "security", + "drizzle", + "orm", + "postgres", + "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", + "analyze:complexity": "fta src --score-cap 89" + }, + "dependencies": { + "@cipherstash/stack": "workspace:*", + "@byteslice/result": "0.2.0" + }, + "peerDependencies": { + "drizzle-orm": ">=0.33" + }, + "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", + "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..87838c567 --- /dev/null +++ b/packages/stack-drizzle/tsconfig.json @@ -0,0 +1,40 @@ +{ + "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/integration-clerk": [ + "../test-kit/src/integration/clerk.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/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/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..9fed3fefd --- /dev/null +++ b/packages/stack-drizzle/vitest.config.ts @@ -0,0 +1,18 @@ +import { configDefaults, defineConfig } from 'vitest/config' +import { sharedAlias, stackSourceAlias } from '../../vitest.shared' + +export default defineConfig({ + resolve: { + // See stack-supabase/vitest.config.ts for what these two alias blocks do. + alias: { ...sharedAlias, ...stackSourceAlias }, + }, + test: { + exclude: [...configDefaults.exclude, 'integration/**'], + testTimeout: 30000, + hookTimeout: 30000, + typecheck: { + tsconfig: './tsconfig.typecheck.json', + include: ['__tests__/**/*.test-d.ts'], + }, + }, +}) 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/__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..cb3ef2f88 100644 --- a/packages/stack/__tests__/supabase-helpers.test.ts +++ b/packages/stack-supabase/__tests__/supabase-helpers.test.ts @@ -1,10 +1,6 @@ import { describe, expect, it } from 'vitest' -import { - addJsonbCastsV3, - parseOrString, - rebuildOrString, -} from '@/supabase/helpers' -import type { DbPendingOrCondition } from '@/supabase/types' +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 // 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..006588bc8 100644 --- a/packages/stack/__tests__/supabase-schema-builder.test.ts +++ b/packages/stack-supabase/__tests__/supabase-schema-builder.test.ts @@ -1,11 +1,8 @@ +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' import { describe, expect, it } from 'vitest' -import { encryptedTable, types } from '@/eql/v3' -import type { IntrospectionResult } from '@/supabase/introspect' -import { groupUnmodelledRows } from '@/supabase/introspect' -import { - mergeDeclaredTables, - synthesizeTables, -} from '@/supabase/schema-builder' +import type { IntrospectionResult } from '../src/introspect' +import { groupUnmodelledRows } from '../src/introspect' +import { mergeDeclaredTables, synthesizeTables } 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..684fa1f20 100644 --- a/packages/stack/__tests__/supabase-v3-builder.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-builder.test.ts @@ -1,8 +1,11 @@ +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' +import { + encryptedColumn, + encryptedTable as encryptedTableV2, +} from '@cipherstash/stack/schema' 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 { 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..66fceb524 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 { encryptedTable, types } from '@cipherstash/stack/eql/v3' 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 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,14 @@ 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..cb03776de 100644 --- a/packages/stack/__tests__/supabase-v3-matrix.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-matrix.test.ts @@ -19,22 +19,22 @@ * proves Postgres accepts what this file pins. */ -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 { - createMockEncryptionClient, - createMockSupabase, - isFakeEnvelope, -} from './helpers/supabase-mock' +import type { AnyV3Table } from '@cipherstash/stack/eql/v3' +import { encryptedTable } from '@cipherstash/stack/eql/v3' import { type DomainSpec, type EqlV3TypeName, eqlTypeSlug as slug, typedEntries, V3_MATRIX, -} from './v3-matrix/catalog' +} 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/__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..1fd76f220 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 type { EncryptionClient } from '@cipherstash/stack/encryption' +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' 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 { 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..61953f6a7 100644 --- a/packages/stack/__tests__/supabase-v3-wire.test.ts +++ b/packages/stack-supabase/__tests__/supabase-v3-wire.test.ts @@ -8,9 +8,9 @@ * assert the array handed to `.in()` and cannot see that. */ +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' import { describe, expect, it } from 'vitest' -import { encryptedTable, types } from '@/eql/v3' -import { EncryptedQueryBuilderV3Impl } from '@/supabase/query-builder-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 98% rename from packages/stack/__tests__/supabase-v3.test-d.ts rename to packages/stack-supabase/__tests__/supabase-v3.test-d.ts index 32681f42e..52560e9a7 100644 --- a/packages/stack/__tests__/supabase-v3.test-d.ts +++ b/packages/stack-supabase/__tests__/supabase-v3.test-d.ts @@ -1,13 +1,20 @@ +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 '@/eql/v3' -import { encryptedColumn, encryptedTable as v2EncryptedTable } from '@/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..c0e08260c 100644 --- a/packages/stack/__tests__/supabase-verify.test.ts +++ b/packages/stack-supabase/__tests__/supabase-verify.test.ts @@ -1,7 +1,7 @@ +import { encryptedTable, types } from '@cipherstash/stack/eql/v3' import { describe, expect, it } from 'vitest' -import { encryptedTable, types } from '@/eql/v3' -import type { IntrospectionResult } from '@/supabase/introspect' -import { verifyDeclaredSchemas } from '@/supabase/verify' +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..1023bc387 100644 --- a/packages/stack/__tests__/supabase.test.ts +++ b/packages/stack-supabase/__tests__/supabase.test.ts @@ -1,11 +1,11 @@ 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 '@/index' -import { encryptedColumn, encryptedTable } from '@/schema' -import { encryptedSupabase } from '@/supabase' +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/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 98% rename from packages/stack/integration/helpers/pgrest.ts rename to packages/stack-supabase/integration/helpers/pgrest.ts index 56fcc8681..dfd59799f 100644 --- a/packages/stack/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/__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..4dc30e9c2 --- /dev/null +++ b/packages/stack-supabase/integration/vitest.config.ts @@ -0,0 +1,22 @@ +import { resolve } from 'node:path' +import { defineConfig } from 'vitest/config' +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 + * (+ PostgREST for supabase). Runs only under `test:integration` (its own CI + * job). Harness + shared `test.*` knobs come from `@cipherstash/test-kit`. + */ +export default defineConfig({ + resolve: { alias: { ...sharedAlias, ...stackSourceAlias } }, + test: { + root: resolve(__dirname, '..'), + include: ['integration/**/*.integration.test.ts'], + ...integrationHarness, + ...integrationTestDefaults, + }, +}) 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/package.json b/packages/stack-supabase/package.json new file mode 100644 index 000000000..008358513 --- /dev/null +++ b/packages/stack-supabase/package.json @@ -0,0 +1,92 @@ +{ + "name": "@cipherstash/stack-supabase", + "version": "0.0.0", + "description": "CipherStash Stack Supabase integration: transparent, searchable field-level encryption for Supabase.", + "keywords": [ + "encrypted", + "encryption", + "searchable-encryption", + "security", + "supabase", + "postgres", + "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", + "analyze:complexity": "fta src --score-cap 91" + }, + "dependencies": { + "@cipherstash/stack": "workspace:*" + }, + "peerDependencies": { + "@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:*", + "fta-cli": "3.0.0", + "@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", + "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..9c6d69c68 100644 --- a/packages/stack/src/supabase/helpers.ts +++ b/packages/stack-supabase/src/helpers.ts @@ -1,5 +1,8 @@ -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 90% rename from packages/stack/src/supabase/index.ts rename to packages/stack-supabase/src/index.ts index 8e602fd28..6b3af298c 100644 --- a/packages/stack/src/supabase/index.ts +++ b/packages/stack-supabase/src/index.ts @@ -1,5 +1,8 @@ -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 +32,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', { @@ -165,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/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..b1bb131df 100644 --- a/packages/stack/src/supabase/query-builder-v3.ts +++ b/packages/stack-supabase/src/query-builder-v3.ts @@ -1,19 +1,22 @@ -import type { EncryptionClient } from '@/encryption' -import type { AnyV3Table } from '@/eql/v3' -import { DATE_LIKE_CASTS, EncryptedV3Column } from '@/eql/v3/columns' +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 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 { 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..29b80903d 100644 --- a/packages/stack/src/supabase/query-builder.ts +++ b/packages/stack-supabase/src/query-builder.ts @@ -1,20 +1,23 @@ import type { JsPlaintext } from '@cipherstash/protect-ffi' -import type { EncryptionClient } from '@/encryption' +import type { AuditConfig } from '@cipherstash/stack/adapter-kit' import { bulkModelsToEncryptedPgComposites, + logger, 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 { EncryptionClient } from '@cipherstash/stack/encryption' +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 { 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..ff6dec8b2 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 { 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/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..41cf4ca98 100644 --- a/packages/stack/src/supabase/types.ts +++ b/packages/stack-supabase/src/types.ts @@ -1,15 +1,18 @@ -import type { EncryptionClient } from '@/encryption' -import type { AuditConfig } from '@/encryption/operations/base-operation' +import type { AuditConfig } from '@cipherstash/stack/adapter-kit' +import type { EncryptionClient } from '@cipherstash/stack/encryption' 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 +631,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 { AuditConfig } from '@cipherstash/stack/adapter-kit' +export type { EncryptionClient } from '@cipherstash/stack/encryption' +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..f4f2dacc5 --- /dev/null +++ b/packages/stack-supabase/tsconfig.json @@ -0,0 +1,36 @@ +{ + "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"], + "@/*": ["../stack/src/*"] + } + }, + "include": ["src/**/*.ts", "__tests__/**/*.ts", "integration/**/*.ts"], + "exclude": ["dist", "node_modules"] +} 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/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..9931d316c --- /dev/null +++ b/packages/stack-supabase/vitest.config.ts @@ -0,0 +1,22 @@ +import { configDefaults, defineConfig } from 'vitest/config' +import { sharedAlias, stackSourceAlias } from '../../vitest.shared' + +export default defineConfig({ + resolve: { + // `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 + // when unconfigured, so `pnpm test` must never collect them. + exclude: [...configDefaults.exclude, 'integration/**'], + testTimeout: 30000, + hookTimeout: 30000, + typecheck: { + tsconfig: './tsconfig.typecheck.json', + include: ['__tests__/**/*.test-d.ts'], + }, + }, +}) 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/packages/stack/__tests__/clerk-provider.test.ts b/packages/stack/__tests__/clerk-provider.test.ts index 597e2e8af..df02618a2 100644 --- a/packages/stack/__tests__/clerk-provider.test.ts +++ b/packages/stack/__tests__/clerk-provider.test.ts @@ -1,5 +1,5 @@ +import { clerkJwtProvider } from '@cipherstash/test-kit/integration-clerk' import { afterEach, describe, expect, it } from 'vitest' -import { clerkJwtProvider } from '../integration/helpers/clerk' /** * `clerkJwtProvider`'s "FAIL rather than skip" guard is the load-bearing half of 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/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..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,66 +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, '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, 'no-skips-reporter.ts')], + ...integrationHarness, + ...integrationTestDefaults, }, }) diff --git a/packages/stack/package.json b/packages/stack/package.json index 856ea5513..1106aaaad 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" ], @@ -54,24 +57,15 @@ "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" ], - "supabase": [ - "./dist/supabase/index.d.ts" - ], "wasm-inline": [ "./dist/wasm-inline.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", @@ -178,16 +152,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", @@ -208,6 +172,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", @@ -268,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/src/adapter-kit.ts b/packages/stack/src/adapter-kit.ts new file mode 100644 index 000000000..8dcf65041 --- /dev/null +++ b/packages/stack/src/adapter-kit.ts @@ -0,0 +1,49 @@ +/** + * 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. + */ + +// 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' + +// 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' +// Shared structured logger (adapters log rejections through the same instance). +export { logger } from './utils/logger/index.js' diff --git a/packages/stack/tsconfig.json b/packages/stack/tsconfig.json index 122bb3e96..d0a2140d5 100644 --- a/packages/stack/tsconfig.json +++ b/packages/stack/tsconfig.json @@ -45,12 +45,14 @@ // 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"], "@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 64a6152da..413934516 100644 --- a/packages/stack/tsup.config.ts +++ b/packages/stack/tsup.config.ts @@ -15,13 +15,11 @@ 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/supabase/index.ts', 'src/encryption/index.ts', 'src/encryption/v3.ts', 'src/errors/index.ts', + 'src/adapter-kit.ts', ], format: ['cjs', 'esm'], sourcemap: true, @@ -29,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 @@ -51,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/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/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/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/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/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/packages/stack/__tests__/v3-matrix/needle-for.ts b/packages/test-kit/src/needle-for.ts similarity index 57% rename from packages/stack/__tests__/v3-matrix/needle-for.ts rename to packages/test-kit/src/needle-for.ts index cfde47563..13f62b86c 100644 --- a/packages/stack/__tests__/v3-matrix/needle-for.ts +++ b/packages/test-kit/src/needle-for.ts @@ -1,3 +1,12 @@ +// 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' @@ -15,7 +24,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), ) diff --git a/packages/test-kit/tsconfig.json b/packages/test-kit/tsconfig.json index 0524347d6..0730f54fb 100644 --- a/packages/test-kit/tsconfig.json +++ b/packages/test-kit/tsconfig.json @@ -30,11 +30,11 @@ "@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 // 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 c5ee35c1e..ea152ac6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,12 @@ 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 dotenv: specifier: ^17.4.2 version: 17.4.2 @@ -208,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) @@ -673,12 +682,98 @@ 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) + fta-cli: + specifier: 3.0.0 + version: 3.0.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/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 + 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 + 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': 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 @@ -2130,277 +2225,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 +2516,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 +3805,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 +5438,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 +5680,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: @@ -6043,6 +5912,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 @@ -6301,7 +6178,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 +6491,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 +6827,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 +7123,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/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 diff --git a/vitest.shared.ts b/vitest.shared.ts index 7c66d8dfa..068aaec06 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, @@ -42,9 +46,42 @@ 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/adapter-kit.ts', + ), + '@cipherstash/stack/encryption': resolve( repoRoot, - 'packages/stack/src/supabase/index.ts', + '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 @@ -53,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', + ), +}