From b0ff3fbb1af0cf4d2d8e4e47ea88fffa38ce5e95 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Wed, 11 Feb 2026 14:16:05 -0700 Subject: [PATCH 1/9] refactor: rename Protect branding to Stack/Encryption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename public API from Protect naming to Stack/Encryption naming: - protect() → Encryption() - csTable/csColumn → encryptedTable/encryptedColumn - ProtectClient → EncryptionClient - ProtectError → EncryptionError - @cipherstash/protect → @cipherstash/stack All old names kept as deprecated aliases for backward compat. Updates examples, docs, drizzle, dynamodb, and schema packages. Adds packages/stack as the new core package. Adds CLAUDE.md and MIGRATION.md. Co-Authored-By: Claude Opus 4.6 --- .cursor/commands/create-example-app.md | 18 +- .cursorrules | 6 +- .github/workflows/rebuild-docs.yml | 2 +- AGENTS.md | 29 +- CLAUDE.md | 69 + CONTRIBUTE.md | 22 +- MIGRATION.md | 188 +++ README.md | 40 +- SECURITY.md | 6 +- docs/README.md | 6 +- .../aws-kms-vs-cipherstash-comparison.md | 58 +- docs/concepts/searchable-encryption.md | 10 +- docs/getting-started.md | 64 +- docs/how-to/lock-contexts-with-clerk.md | 4 +- docs/how-to/nextjs-external-packages.md | 6 +- docs/how-to/npm-lockfile-v3.md | 4 +- docs/how-to/sst-external-packages.md | 6 +- docs/prompts/init-protect.md | 28 +- docs/reference/configuration.md | 22 +- docs/reference/drizzle/DRIFT-TESTING.md | 2 +- docs/reference/drizzle/drizzle-protect.md | 12 +- docs/reference/drizzle/drizzle.md | 10 +- docs/reference/model-operations.md | 14 +- docs/reference/schema.md | 60 +- .../searchable-encryption-postgres.md | 34 +- docs/reference/supabase-sdk.md | 62 +- examples/basic/README.md | 12 +- examples/basic/package.json | 2 +- examples/basic/protect.ts | 18 +- examples/drizzle/README.md | 14 +- examples/drizzle/package.json | 2 +- examples/drizzle/src/protect/config.ts | 8 +- examples/dynamo/README.md | 4 +- examples/dynamo/package.json | 2 +- examples/dynamo/src/common/protect.ts | 8 +- examples/dynamo/src/encrypted-key-in-gsi.ts | 7 +- .../dynamo/src/encrypted-partition-key.ts | 7 +- examples/dynamo/src/encrypted-sort-key.ts | 7 +- examples/hono-supabase/README.md | 6 +- examples/hono-supabase/package.json | 2 +- examples/hono-supabase/src/index.ts | 20 +- examples/nest/README.md | 4 +- examples/nest/package.json | 2 +- examples/nest/src/app.controller.spec.ts | 2 +- examples/nest/src/app.service.ts | 2 +- .../protect/decorators/decrypt.decorator.ts | 12 +- .../protect/decorators/encrypt.decorator.ts | 12 +- .../interceptors/decrypt.interceptor.ts | 12 +- .../interceptors/encrypt.interceptor.ts | 12 +- .../interfaces/protect-config.interface.ts | 4 +- examples/nest/src/protect/protect.module.ts | 32 +- .../nest/src/protect/protect.service.spec.ts | 6 +- examples/nest/src/protect/protect.service.ts | 16 +- examples/nest/src/protect/schema.ts | 16 +- examples/nest/test/app.e2e-spec.ts | 2 +- examples/next-drizzle-mysql/README.md | 10 +- examples/next-drizzle-mysql/next.config.ts | 2 +- examples/next-drizzle-mysql/package.json | 2 +- .../next-drizzle-mysql/src/protect/index.ts | 6 +- .../next-drizzle-mysql/src/protect/schema.ts | 8 +- examples/nextjs-clerk/.env.example | 2 +- examples/nextjs-clerk/README.md | 22 +- examples/nextjs-clerk/next.config.ts | 2 +- examples/nextjs-clerk/package.json | 2 +- examples/nextjs-clerk/src/app/layout.tsx | 4 +- examples/nextjs-clerk/src/app/page.tsx | 2 +- .../nextjs-clerk/src/components/Header.tsx | 2 +- .../nextjs-clerk/src/core/protect/index.ts | 18 +- examples/typeorm/README.md | 24 +- examples/typeorm/package.json | 4 +- examples/typeorm/src/data-source.ts | 2 +- examples/typeorm/src/entity/User.ts | 2 +- .../typeorm/src/helpers/protect-entity.ts | 12 +- examples/typeorm/src/index.ts | 6 +- examples/typeorm/src/protect.ts | 16 +- .../typeorm/src/utils/encrypted-column.ts | 2 +- package.json | 19 +- packages/drizzle/README.md | 25 +- packages/drizzle/__tests__/docs.test.ts | 6 +- packages/drizzle/__tests__/drizzle.test.ts | 12 +- packages/drizzle/package.json | 6 +- packages/drizzle/src/pg/operators.ts | 145 +- packages/drizzle/src/pg/schema-extraction.ts | 40 +- .../{protect-dynamodb => dynamodb}/.npmignore | 0 .../CHANGELOG.md | 0 .../{protect-dynamodb => dynamodb}/README.md | 21 +- .../__tests__/audit.test.ts | 20 +- .../__tests__/dynamodb.test.ts | 26 +- .../package.json | 6 +- .../src/helpers.ts | 8 +- .../src/index.ts | 14 +- .../src/operations/base-operation.ts | 0 .../src/operations/bulk-decrypt-models.ts | 12 +- .../src/operations/bulk-encrypt-models.ts | 12 +- .../src/operations/decrypt-model.ts | 12 +- .../src/operations/encrypt-model.ts | 12 +- .../src/operations/search-terms.ts | 6 +- .../src/types.ts | 18 +- .../tsconfig.json | 0 .../tsup.config.ts | 0 packages/jseql/README.md | 4 - packages/nextjs/README.md | 4 +- packages/nextjs/package.json | 2 +- packages/protect/README.md | 1091 +-------------- .../protect/__tests__/encrypt-query.test.ts | 299 +++- packages/protect/__tests__/fixtures/index.ts | 14 +- .../__tests__/infer-index-type.test.ts | 15 +- .../protect/__tests__/jsonb-helpers.test.ts | 48 +- packages/protect/__tests__/supabase.test.ts | 8 +- .../src/ffi/helpers/infer-index-type.ts | 13 +- .../protect/src/ffi/helpers/type-guards.ts | 2 +- .../protect/src/ffi/helpers/validation.ts | 10 +- packages/protect/src/ffi/index.ts | 21 +- .../src/ffi/operations/batch-encrypt-query.ts | 65 +- .../ffi/operations/deprecated/search-terms.ts | 26 +- .../src/ffi/operations/encrypt-query.ts | 35 +- packages/protect/src/helpers/index.ts | 9 +- packages/protect/src/helpers/jsonb.ts | 2 +- packages/protect/src/index.ts | 15 +- packages/protect/src/types.ts | 2 +- packages/schema/README.md | 108 +- .../schema/__tests__/searchable-json.test.ts | 14 +- packages/schema/package.json | 11 +- packages/schema/src/index.ts | 84 +- packages/stack/.npmignore | 5 + packages/stack/README.md | 1094 +++++++++++++++ packages/stack/__tests__/audit.test.ts | 472 +++++++ .../stack/__tests__/backward-compat.test.ts | 65 + .../stack/__tests__/basic-protect.test.ts | 44 + packages/stack/__tests__/bulk-protect.test.ts | 597 ++++++++ .../__tests__/deprecated/search-terms.test.ts | 140 ++ .../stack/__tests__/encrypt-query.test.ts | 872 ++++++++++++ packages/stack/__tests__/fixtures/index.ts | 127 ++ packages/stack/__tests__/helpers.test.ts | 148 ++ .../stack/__tests__/infer-index-type.test.ts | 57 + packages/stack/__tests__/json-protect.test.ts | 1223 +++++++++++++++++ .../stack/__tests__/jsonb-helpers.test.ts | 203 +++ packages/stack/__tests__/keysets.test.ts | 89 ++ packages/stack/__tests__/lock-context.test.ts | 208 +++ .../stack/__tests__/nested-models.test.ts | 962 +++++++++++++ .../stack/__tests__/number-protect.test.ts | 835 +++++++++++ packages/stack/__tests__/protect-ops.test.ts | 843 ++++++++++++ packages/stack/__tests__/supabase.test.ts | 307 +++++ packages/stack/package.json | 80 ++ packages/stack/src/bin/stash.ts | 499 +++++++ packages/stack/src/client.ts | 31 + .../stack/src/ffi/helpers/infer-index-type.ts | 70 + packages/stack/src/ffi/helpers/type-guards.ts | 18 + packages/stack/src/ffi/helpers/validation.ts | 94 ++ packages/stack/src/ffi/index.ts | 438 ++++++ packages/stack/src/ffi/model-helpers.ts | 952 +++++++++++++ .../src/ffi/operations/base-operation.ts | 55 + .../src/ffi/operations/batch-encrypt-query.ts | 233 ++++ .../src/ffi/operations/bulk-decrypt-models.ts | 109 ++ .../stack/src/ffi/operations/bulk-decrypt.ts | 175 +++ .../src/ffi/operations/bulk-encrypt-models.ts | 128 ++ .../stack/src/ffi/operations/bulk-encrypt.ts | 210 +++ .../stack/src/ffi/operations/decrypt-model.ts | 106 ++ packages/stack/src/ffi/operations/decrypt.ts | 127 ++ .../ffi/operations/deprecated/search-terms.ts | 132 ++ .../stack/src/ffi/operations/encrypt-model.ts | 125 ++ .../stack/src/ffi/operations/encrypt-query.ts | 162 +++ packages/stack/src/ffi/operations/encrypt.ts | 155 +++ .../stack/src/ffi/operations/search-terms.ts | 63 + packages/stack/src/helpers/index.ts | 139 ++ packages/stack/src/helpers/jsonb.ts | 99 ++ packages/stack/src/identify/index.ts | 130 ++ packages/stack/src/index.ts | 177 +++ packages/stack/src/stash/index.ts | 474 +++++++ packages/stack/src/types.ts | 193 +++ packages/stack/tsconfig.json | 28 + packages/stack/tsup.config.ts | 30 + pnpm-lock.yaml | 117 +- 173 files changed, 15129 insertions(+), 2018 deletions(-) create mode 100644 CLAUDE.md create mode 100644 MIGRATION.md rename packages/{protect-dynamodb => dynamodb}/.npmignore (100%) rename packages/{protect-dynamodb => dynamodb}/CHANGELOG.md (100%) rename packages/{protect-dynamodb => dynamodb}/README.md (90%) rename packages/{protect-dynamodb => dynamodb}/__tests__/audit.test.ts (94%) rename packages/{protect-dynamodb => dynamodb}/__tests__/dynamodb.test.ts (92%) rename packages/{protect-dynamodb => dynamodb}/package.json (87%) rename packages/{protect-dynamodb => dynamodb}/src/helpers.ts (97%) rename packages/{protect-dynamodb => dynamodb}/src/index.ts (85%) rename packages/{protect-dynamodb => dynamodb}/src/operations/base-operation.ts (100%) rename packages/{protect-dynamodb => dynamodb}/src/operations/bulk-decrypt-models.ts (86%) rename packages/{protect-dynamodb => dynamodb}/src/operations/bulk-encrypt-models.ts (86%) rename packages/{protect-dynamodb => dynamodb}/src/operations/decrypt-model.ts (85%) rename packages/{protect-dynamodb => dynamodb}/src/operations/encrypt-model.ts (85%) rename packages/{protect-dynamodb => dynamodb}/src/operations/search-terms.ts (92%) rename packages/{protect-dynamodb => dynamodb}/src/types.ts (83%) rename packages/{protect-dynamodb => dynamodb}/tsconfig.json (100%) rename packages/{protect-dynamodb => dynamodb}/tsup.config.ts (100%) delete mode 100644 packages/jseql/README.md create mode 100644 packages/stack/.npmignore create mode 100644 packages/stack/README.md create mode 100644 packages/stack/__tests__/audit.test.ts create mode 100644 packages/stack/__tests__/backward-compat.test.ts create mode 100644 packages/stack/__tests__/basic-protect.test.ts create mode 100644 packages/stack/__tests__/bulk-protect.test.ts create mode 100644 packages/stack/__tests__/deprecated/search-terms.test.ts create mode 100644 packages/stack/__tests__/encrypt-query.test.ts create mode 100644 packages/stack/__tests__/fixtures/index.ts create mode 100644 packages/stack/__tests__/helpers.test.ts create mode 100644 packages/stack/__tests__/infer-index-type.test.ts create mode 100644 packages/stack/__tests__/json-protect.test.ts create mode 100644 packages/stack/__tests__/jsonb-helpers.test.ts create mode 100644 packages/stack/__tests__/keysets.test.ts create mode 100644 packages/stack/__tests__/lock-context.test.ts create mode 100644 packages/stack/__tests__/nested-models.test.ts create mode 100644 packages/stack/__tests__/number-protect.test.ts create mode 100644 packages/stack/__tests__/protect-ops.test.ts create mode 100644 packages/stack/__tests__/supabase.test.ts create mode 100644 packages/stack/package.json create mode 100644 packages/stack/src/bin/stash.ts create mode 100644 packages/stack/src/client.ts create mode 100644 packages/stack/src/ffi/helpers/infer-index-type.ts create mode 100644 packages/stack/src/ffi/helpers/type-guards.ts create mode 100644 packages/stack/src/ffi/helpers/validation.ts create mode 100644 packages/stack/src/ffi/index.ts create mode 100644 packages/stack/src/ffi/model-helpers.ts create mode 100644 packages/stack/src/ffi/operations/base-operation.ts create mode 100644 packages/stack/src/ffi/operations/batch-encrypt-query.ts create mode 100644 packages/stack/src/ffi/operations/bulk-decrypt-models.ts create mode 100644 packages/stack/src/ffi/operations/bulk-decrypt.ts create mode 100644 packages/stack/src/ffi/operations/bulk-encrypt-models.ts create mode 100644 packages/stack/src/ffi/operations/bulk-encrypt.ts create mode 100644 packages/stack/src/ffi/operations/decrypt-model.ts create mode 100644 packages/stack/src/ffi/operations/decrypt.ts create mode 100644 packages/stack/src/ffi/operations/deprecated/search-terms.ts create mode 100644 packages/stack/src/ffi/operations/encrypt-model.ts create mode 100644 packages/stack/src/ffi/operations/encrypt-query.ts create mode 100644 packages/stack/src/ffi/operations/encrypt.ts create mode 100644 packages/stack/src/ffi/operations/search-terms.ts create mode 100644 packages/stack/src/helpers/index.ts create mode 100644 packages/stack/src/helpers/jsonb.ts create mode 100644 packages/stack/src/identify/index.ts create mode 100644 packages/stack/src/index.ts create mode 100644 packages/stack/src/stash/index.ts create mode 100644 packages/stack/src/types.ts create mode 100644 packages/stack/tsconfig.json create mode 100644 packages/stack/tsup.config.ts diff --git a/.cursor/commands/create-example-app.md b/.cursor/commands/create-example-app.md index 1125be0b..480824b2 100644 --- a/.cursor/commands/create-example-app.md +++ b/.cursor/commands/create-example-app.md @@ -1,10 +1,10 @@ -# Cursor Super-Prompt: Protect.js Example Apps (Framework/ORM-Agnostic, No-Cheese) +# Cursor Super-Prompt: Stash Encryption Example Apps (Framework/ORM-Agnostic, No-Cheese) ROLE -You are a senior systems engineer focused on developer experience and a core maintainer of `@cipherstash/protect`. Your mission: create a polished set of runnable **Protect.js example apps** across multiple stacks. Each example must be minimal, factual, and runnable in minutes. +You are a senior systems engineer focused on developer experience and a core maintainer of `@cipherstash/stack`. Your mission: create a polished set of runnable **Stash Encryption example apps** across multiple stacks. Each example must be minimal, factual, and runnable in minutes. GROUNDING & SOURCES (use @ref; do not guess) -- Protect.js APIs: the Protect.js main README is the single source of truth. If not accessible, STOP and ask for the exact snippet/repo path. Do not invent APIs. +- Stash Encryption APIs: the Stash Encryption main README is the single source of truth. If not accessible, STOP and ask for the exact snippet/repo path. Do not invent APIs. - ORM/DB docs (pick per stack): @ref https://www.prisma.io/docs @ref https://typeorm.io @@ -26,7 +26,7 @@ STACK MATRIX (generate now) Optional (time-permitting): nextjs-prisma (App Router), fastify-knex. NO-CHEESE RULES (hard requirements) -Goal: smallest possible working example that clearly demonstrates Protect.js. Clarity > patterns > abstractions. +Goal: smallest possible working example that clearly demonstrates Stash Encryption. Clarity > patterns > abstractions. DON'TS - No Singletons/Factories/Service-Locators/DI frameworks. - No ports & adapters/custom repo abstractions for tiny demos—call the ORM/client directly. @@ -41,7 +41,7 @@ README tone --- Simplicity budget (per example) - ≤ 8 TS source files (excluding migrations). -- Deps: ORM/client + `@cipherstash/protect` + dev tooling (ts-node or tsx). Nothing else unless required by the stack. +- Deps: ORM/client + `@cipherstash/stack` + dev tooling (ts-node or tsx). Nothing else unless required by the stack. - One `.env.example`; use `dotenv`. No layered config. CODE STYLE @@ -61,7 +61,7 @@ REQUIRED DX & SCRIPTS (per example) - `seed` → seed sensible data - `demo` → prints proof of encryption & queries - `typecheck` → `tsc --noEmit` -- `.env.example` includes DB vars and **exact** Protect.js env names from the Protect README (do not invent): +- `.env.example` includes DB vars and **exact** Stash Encryption env names from the Stack README (do not invent): {{PROTECT_ENV_VARS := "e.g., PROTECT_PROJECT_ID, PROTECT_CLIENT_KEY, PROTECT_SERVER_URL (replace with real names from README)"}} PROJECT LAYOUT (monorepo, minimal) @@ -80,10 +80,10 @@ README REQUIREMENTS (every example) - AI banner (exact text) at the very top with {{BOOK_CHAT_URL}}. - 90-second Quickstart (copy/paste only). - "What this shows" checklist (encrypted fields, CRUD, query). -- "How encryption works here" (short, accurate, tied to Protect.js). +- "How encryption works here" (short, accurate, tied to Stash Encryption). - Config notes for the stack (e.g., why CJS for TypeORM). - Troubleshooting (ESM/CJS, ts-node/tsx, migration pitfalls). -- `@ref` links to stack docs + Protect README. +- `@ref` links to stack docs + Stack README. DELIVERABLES (return in one message/PR) - Root `README.md` + `docker-compose.yml`. @@ -118,4 +118,4 @@ OUTPUT FORMAT VARIABLES TO FILL BEFORE RUN - {{BOOK_CHAT_URL}} = your booking link - {{STACKS}} = list of stacks to generate -- {{PROTECT_ENV_VARS}} = exact names from Protect.js README +- {{PROTECT_ENV_VARS}} = exact names from Stash Encryption README diff --git a/.cursorrules b/.cursorrules index 205bcb70..3aa6e40f 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,11 +1,11 @@ -## Protect.js Cursor Rules +## Stash Encryption Cursor Rules These rules guide agents when creating or updating example apps under `examples/*` in this repository. ### Example App Prompt (for agents) - **Goals** - - Show end-to-end usage of Protect.js with clear, minimal code. + - Show end-to-end usage of Stash Encryption with clear, minimal code. - Demonstrate schema, encrypt/decrypt, and (when relevant) searchable encryption on PostgreSQL. - **Hard guardrails (do not violate)** @@ -33,7 +33,7 @@ These rules guide agents when creating or updating example apps under `examples/ - `docs/concepts/searchable-encryption.md` - **Deliverables checklist for a new example** - - A `protect.ts` (or equivalent) that initializes `protect({ schemas })` using `csTable`/`csColumn`. + - A `protect.ts` (or equivalent) that initializes `Encryption({ schemas })` using `encryptedTable`/`encryptedColumn`. - If targeting Postgres searchable encryption, include `.freeTextSearch().equality().orderAndRange()` on appropriate columns. - A minimal script or route/handler that encrypts and decrypts at least one value. - A README covering: diff --git a/.github/workflows/rebuild-docs.yml b/.github/workflows/rebuild-docs.yml index 27695b63..04086878 100644 --- a/.github/workflows/rebuild-docs.yml +++ b/.github/workflows/rebuild-docs.yml @@ -3,7 +3,7 @@ name: Rebuild Docs on: push: tags: - - '@cipherstash/protect@*' + - '@cipherstash/stack@*' - '@cipherstash/drizzle@*' jobs: diff --git a/AGENTS.md b/AGENTS.md index a4d2b0c6..a25033ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -This is the Protect.js repository - End-to-end, per-value encryption for JavaScript/TypeScript with zero‑knowledge key management (via CipherStash ZeroKMS). Encrypted data is stored as EQL JSON payloads; searchable encryption is currently supported for PostgreSQL. +This is the Stash Encryption repository (protectjs) - End-to-end, per-value encryption for JavaScript/TypeScript with zero‑knowledge key management (via CipherStash ZeroKMS). Encrypted data is stored as EQL JSON payloads; searchable encryption is currently supported for PostgreSQL. ## Prerequisites @@ -43,7 +43,7 @@ pnpm test - Filter to a single package (recommended for fast iteration): ```bash -pnpm --filter @cipherstash/protect test +pnpm --filter @cipherstash/stack test pnpm --filter @cipherstash/nextjs test ``` @@ -71,22 +71,23 @@ If these variables are missing, tests that require live encryption will fail or ## Repository Layout -- `packages/protect`: Core library - - `src/index.ts`: Public API (`protect`, exports) - - `src/ffi/index.ts`: `ProtectClient` implementation, bridges to `@cipherstash/protect-ffi` +- `packages/stack`: Core library (published as `@cipherstash/stack`) + - `src/index.ts`: Public API (`Encryption`, exports) + - `src/ffi/index.ts`: `EncryptionClient` implementation, bridges to `@cipherstash/protect-ffi` - `src/ffi/operations/*`: Encrypt/decrypt/model/bulk/search-terms operations (thenable pattern with optional `.withLockContext()`) - `__tests__/*`: End-to-end and API contract tests (Vitest) -- `packages/schema`: Schema builder utilities and types (`csTable`, `csColumn`, `buildEncryptConfig`) +- `packages/protect`: Deprecated — re-exports from `stack` for backward compatibility +- `packages/schema`: Schema builder utilities and types (`encryptedTable`, `encryptedColumn`, `buildEncryptConfig`) - `packages/nextjs`: Next.js helpers and Clerk integration (`./clerk` export) -- `packages/protect-dynamodb`: DynamoDB helpers for Protect.js +- `packages/dynamodb`: DynamoDB helpers (published as `@cipherstash/protect-dynamodb`) - `packages/utils`: Shared config (`utils/config`) and logger (`utils/logger`) - `examples/*`: Working apps (basic, drizzle, nextjs-clerk, next-drizzle-mysql, dynamo, hono-supabase) - `docs/*`: Concepts, how-to guides (Next.js bundling, SST, npm lockfile v3), reference ## Key Concepts and APIs -- **Initialization**: `protect({ schemas })` returns an initialized `ProtectClient`. Provide at least one `csTable`. -- **Schema**: Define tables/columns with `csTable` and `csColumn`. Add `.freeTextSearch().equality().orderAndRange()` to enable searchable encryption on PostgreSQL. +- **Initialization**: `Encryption({ schemas })` returns an initialized `EncryptionClient`. Provide at least one `encryptedTable`. +- **Schema**: Define tables/columns with `encryptedTable` and `encryptedColumn`. Add `.freeTextSearch().equality().orderAndRange()` to enable searchable encryption on PostgreSQL. - **Operations** (all return Result-like objects and support chaining `.withLockContext(lockContext)` when applicable): - `encrypt(plaintext, { table, column })` - `decrypt(encryptedPayload)` @@ -94,17 +95,17 @@ If these variables are missing, tests that require live encryption will fail or - `bulkEncrypt(plaintexts[], { table, column })` / `bulkDecrypt(encrypted[])` - `bulkEncryptModels(models[], table)` / `bulkDecryptModels(models[])` - `createSearchTerms(terms)` for searchable queries -- **Identity-aware encryption**: Use `LockContext` from `@cipherstash/protect/identify` and chain `.withLockContext()` on operations. Same context must be used for both encrypt and decrypt. +- **Identity-aware encryption**: Use `LockContext` from `@cipherstash/stack/identity` and chain `.withLockContext()` on operations. Same context must be used for both encrypt and decrypt. ## Critical Gotchas (read before coding) -- **Native Node.js module**: Protect.js relies on `@cipherstash/protect-ffi` (Node-API). It must be loaded via native Node.js `require`. Do NOT bundle this module; configure bundlers to externalize it. +- **Native Node.js module**: Stash Encryption relies on `@cipherstash/protect-ffi` (Node-API). It must be loaded via native Node.js `require`. Do NOT bundle this module; configure bundlers to externalize it. - Next.js: see `docs/how-to/nextjs-external-packages.md` - SST/Serverless: see `docs/how-to/sst-external-packages.md` - npm lockfile v3 on Linux: see `docs/how-to/npm-lockfile-v3.md` - **Bun is not supported**: Due to Node-API compatibility gaps. Use Node.js. - **Do not log plaintext**: The library never logs plaintext by design. Don’t add logs that risk leaking sensitive data. -- **Result shape is contract**: Operations return `{ data }` or `{ failure }`. Preserve this shape and error `type` values in `ProtectErrorTypes`. +- **Result shape is contract**: Operations return `{ data }` or `{ failure }`. Preserve this shape and error `type` values in `EncryptionErrorTypes`. - **Encrypted payload shape is contract**: Keys like `c` in the EQL payload are validated by tests and downstream tools. Don’t change them. - **Exports must support ESM and CJS**: Each package’s `exports` maps must keep both `import` and `require` fields. Don’t remove CJS. @@ -142,11 +143,11 @@ pnpm changeset:publish ## Adding Features Safely (LLM checklist) 1. Identify the target package(s) in `packages/*` and confirm whether changes affect public APIs or payload shapes. -2. If modifying `packages/protect` operations or `ProtectClient`, ensure: +2. If modifying `packages/stack` operations or `EncryptionClient`, ensure: - The Result contract and error type strings remain stable. - `.withLockContext()` remains available for affected operations. - ESM/CJS exports continue to work (don’t break `require`). -3. If changing schema behavior (`packages/schema`), update type definitions and ensure `buildEncryptConfig` still validates with Zod in `ProtectClient.init`. +3. If changing schema behavior (`packages/schema`), update type definitions and ensure `buildEncryptConfig` still validates with Zod in `EncryptionClient.init`. 4. Add/extend tests in the same package. For features that require live credentials, guard with env checks or provide mock-friendly paths. 5. Run: - `pnpm run code:fix` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..7d06d2a0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md — Stash Encryption (protectjs) + +End-to-end, per-value encryption for JS/TS with zero-knowledge key management (CipherStash ZeroKMS). Encrypted data stored as EQL JSON payloads; searchable encryption supported for PostgreSQL. + +## Quick reference + +```bash +pnpm install # install (requires pnpm 10.x, Node >= 22) +pnpm run build # build all packages (Turborepo + tsup) +pnpm run dev # watch mode +pnpm test # run all package tests (Vitest) +pnpm --filter @cipherstash/stack test # test a single package +pnpm run code:fix # lint + format (Biome) +pnpm changeset # create a changeset for release +``` + +## Monorepo structure + +pnpm workspaces with Turborepo orchestration. Packages under `packages/*`, examples under `examples/*`. + +| Package | Purpose | +|---------|---------| +| `protect` | Deprecated — re-exports from `stack` for backward compatibility | +| `stack` | Core library — encrypt/decrypt, FFI bridge to `@cipherstash/protect-ffi` | +| `schema` | Schema builder (`encryptedTable`, `encryptedColumn`, `buildEncryptConfig`) | +| `nextjs` | Next.js helpers and Clerk integration | +| `dynamodb` | DynamoDB helpers | +| `drizzle` | Drizzle ORM integration | +| `utils` | Shared config and logger | + +Build order is managed by Turborepo (`^build` dependency). Each package uses `tsup` for bundling. + +## Critical constraints + +1. **Native FFI** — `@cipherstash/protect-ffi` is a Node-API native module loaded via `require`. Bundlers **must** externalize it (Next.js: `serverExternalPackages`; SST/serverless: see `docs/how-to/`). +2. **No plaintext logging** — The library never logs plaintext by design. Never add logs that could leak sensitive data. +3. **Result contract** — All operations return `{ data }` or `{ failure }` with stable error `type` strings from `EncryptionErrorTypes`. Do not change this shape. +4. **EQL payload shape is contract** — Keys like `c` in encrypted payloads are validated by tests and downstream tools. Do not alter. +5. **ESM + CJS** — Every package's `exports` map must keep both `import` and `require` fields. Do not remove CJS support. +6. **Bun is not supported** — Node-API compatibility gaps. Use Node.js only. + +## Environment variables (for tests/examples) + +```bash +CS_WORKSPACE_CRN= # required for live encryption tests +CS_CLIENT_ID= +CS_CLIENT_KEY= +CS_CLIENT_ACCESS_KEY= +USER_JWT= # optional — identity-aware encryption tests +USER_2_JWT= +PROTECT_LOG_LEVEL=debug|info|error +``` + +Tests requiring credentials will fail or be skipped without these. + +## Code style + +- Biome for formatting and linting (single quotes, no semicolons, 2-space indent) +- `noThenProperty` lint rule is disabled (operations use thenable pattern) +- Tests use Vitest with `.test.ts` files under each package's `__tests__/` +- Test via public API; avoid reaching into private internals + +## Making changes checklist + +1. Identify target package(s); check if changes affect public APIs or payload shapes +2. Preserve Result contract, `.withLockContext()` chaining, and ESM/CJS exports +3. Add/extend tests in the same package +4. Run: `pnpm run code:fix && pnpm --filter build && pnpm --filter test` +5. Create a changeset (`pnpm changeset`) if the change affects published packages diff --git a/CONTRIBUTE.md b/CONTRIBUTE.md index 4b37ed61..9c5bc53c 100644 --- a/CONTRIBUTE.md +++ b/CONTRIBUTE.md @@ -1,4 +1,4 @@ -# How to contribute to @cipherstash/protect +# How to contribute to @cipherstash/stack ## I want to report a bug, or make a feature request @@ -8,9 +8,9 @@ Please use the GitHub issue tracker to report bugs, suggest features, or documen --- -# Contributing to @cipherstash/protect +# Contributing to @cipherstash/stack -Thank you for your interest in contributing to **@cipherstash/protect**! This document will walk you through the repository’s structure, how to build and run the project locally, and how to make contributions effectively. +Thank you for your interest in contributing to **@cipherstash/stack**! This document will walk you through the repository's structure, how to build and run the project locally, and how to make contributions effectively. ## Repository Structure @@ -21,7 +21,7 @@ Thank you for your interest in contributing to **@cipherstash/protect**! This do │ └── example-app-2/ │ ├── packages/ -│ └── protect/ <-- Main package published to npm +│ └── stack/ <-- Main package published to npm │ ├── .changeset/ ├── .turbo/ @@ -34,13 +34,13 @@ Thank you for your interest in contributing to **@cipherstash/protect**! This do This repo uses [Turborepo](https://turbo.build/) to manage multiple packages and examples in a monorepo structure. Turborepo orchestrates tasks (build, test, lint, etc.) across the different packages in a consistent and efficient manner. -### `packages/protect` +### `packages/stack` -The **@cipherstash/protect** package is the core library that is published to npm under the `@cipherstash/protect` namespace. This is likely where you’ll spend most of your time if you’re contributing new features or bug fixes related to JSEQL’s core functionality. +The **@cipherstash/stack** package is the core library that is published to npm under the `@cipherstash/stack` namespace. This is likely where you'll spend most of your time if you're contributing new features or bug fixes related to the core functionality. ### `examples/` Directory -Within the `examples/` directory, you’ll find example applications that demonstrate how to use **@cipherstash/protect**. These examples reference the local `@cipherstash/protect` package, allowing you to test and verify your changes to **@cipherstash/protect** in a real-world application scenario. +Within the `examples/` directory, you'll find example applications that demonstrate how to use **@cipherstash/stack**. These examples reference the local `@cipherstash/stack` package, allowing you to test and verify your changes to **@cipherstash/stack** in a real-world application scenario. ## Setup Instructions @@ -59,13 +59,13 @@ pnpm install ### 4. Build the Main Package -Before you can run any example, you need to build the `@cipherstash/protect` package: +Before you can run any example, you need to build the `@cipherstash/stack` package: ```bash pnpm run build ``` -This command triggers Turborepo’s build pipeline, compiling the **@cipherstash/protect** package in `packages/protect` and linking it locally so the example can reference it. +This command triggers Turborepo's build pipeline, compiling the **@cipherstash/stack** package in `packages/stack` and linking it locally so the example can reference it. ### 5. Run an Example App @@ -77,7 +77,7 @@ pnpm run dev Navigate to one of the examples in `examples/` and follow the instructions for the corresponding example. -Now, you can view the running application (if it’s a web or server app) or otherwise test the example’s output. This will help confirm your local build of **@cipherstash/protect** is working correctly. +Now, you can view the running application (if it's a web or server app) or otherwise test the example's output. This will help confirm your local build of **@cipherstash/stack** is working correctly. ## Making Changes @@ -86,7 +86,7 @@ Now, you can view the running application (if it’s a web or server app) or oth git checkout -b feat/my-new-feature ``` -2. **Implement your changes** in the relevant package (most likely in `packages/protect`). +2. **Implement your changes** in the relevant package (most likely in `packages/stack`). 3. **Write tests** to cover any new functionality or bug fixes. diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..17caca88 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,188 @@ +# Migration Guide: `@cipherstash/protect` to `@cipherstash/stack` + +This guide covers migrating from `@cipherstash/protect` to `@cipherstash/stack`. All old names are preserved as deprecated aliases, so your existing code will continue to work. However, we recommend updating to the new names to stay current. + +## 1. Update your dependencies + +Replace `@cipherstash/protect` with `@cipherstash/stack`: + +```bash +# Remove the old package +npm uninstall @cipherstash/protect + +# Install the new package +npm install @cipherstash/stack +``` + +If you use the DynamoDB helpers, update the peer dependency: + +```bash +# @cipherstash/protect-dynamodb now expects @cipherstash/stack +npm install @cipherstash/stack @cipherstash/protect-dynamodb +``` + +If you use the Drizzle integration, update the peer dependency: + +```bash +npm install @cipherstash/stack @cipherstash/drizzle +``` + +## 2. Update your imports + +### Package imports + +| Before | After | +|--------|-------| +| `from '@cipherstash/protect'` | `from '@cipherstash/stack'` | +| `from '@cipherstash/protect/client'` | `from '@cipherstash/stack/client'` | +| `from '@cipherstash/protect/identify'` | `from '@cipherstash/stack/identity'` | + +### Named imports + +| Before | After | +|--------|-------| +| `protect` | `Encryption` | +| `csTable` | `encryptedTable` | +| `csColumn` | `encryptedColumn` | +| `csValue` | `encryptedValue` | + +### Type imports + +| Before | After | +|--------|-------| +| `ProtectClient` | `EncryptionClient` | +| `ProtectClientConfig` | `EncryptionClientConfig` | +| `ProtectError` | `EncryptionError` | +| `ProtectErrorTypes` | `EncryptionErrorTypes` | +| `ProtectTable` | `EncryptedTable` | +| `ProtectColumn` | `EncryptedColumn` | +| `ProtectValue` | `EncryptedValue` | +| `ProtectTableColumn` | `EncryptedTableColumn` | +| `ProtectOperation` | `EncryptionOperation` | + +## 3. Update your code + +### Schema definition + +```diff +-import { csTable, csColumn } from '@cipherstash/protect' ++import { encryptedTable, encryptedColumn } from '@cipherstash/stack' + +-const users = csTable('users', { +- email: csColumn('email').equality().freeTextSearch(), +- age: csColumn('age').dataType('number').orderAndRange(), ++const users = encryptedTable('users', { ++ email: encryptedColumn('email').equality().freeTextSearch(), ++ age: encryptedColumn('age').dataType('number').orderAndRange(), + }) +``` + +### Client initialization + +```diff +-import { protect, type ProtectClientConfig } from '@cipherstash/protect' ++import { Encryption, type EncryptionClientConfig } from '@cipherstash/stack' + +-const config: ProtectClientConfig = { ++const config: EncryptionClientConfig = { + schemas: [users], + } + +-const client = await protect(config) ++const client = await Encryption(config) +``` + +### Identity / Lock context + +```diff +-import { LockContext } from '@cipherstash/protect/identify' ++import { LockContext } from '@cipherstash/stack/identity' +``` + +### Error handling + +```diff +-import { ProtectErrorTypes } from '@cipherstash/protect' ++import { EncryptionErrorTypes } from '@cipherstash/stack' + + if (result.failure) { +- if (result.failure.type === ProtectErrorTypes.EncryptionError) { ++ if (result.failure.type === EncryptionErrorTypes.EncryptionError) { + // handle encryption error + } + } +``` + +### Drizzle integration + +```diff +-import { protect } from '@cipherstash/protect' ++import { Encryption } from '@cipherstash/stack' + import { extractProtectSchema, createProtectOperators } from '@cipherstash/drizzle/pg' + + const users = extractProtectSchema(usersTable) +-const client = await protect({ schemas: [users] }) ++const client = await Encryption({ schemas: [users] }) + const ops = createProtectOperators(client) +``` + +> Note: `extractProtectSchema` and `createProtectOperators` retain their names in the Drizzle package. + +### DynamoDB integration + +```diff +-import { protect, csTable, csColumn } from '@cipherstash/protect' ++import { Encryption, encryptedTable, encryptedColumn } from '@cipherstash/stack' + import { protectDynamoDB } from '@cipherstash/protect-dynamodb' + +-const users = csTable('users', { +- email: csColumn('email').equality(), ++const users = encryptedTable('users', { ++ email: encryptedColumn('email').equality(), + }) + +-const client = await protect({ schemas: [users] }) ++const client = await Encryption({ schemas: [users] }) + const dynamo = protectDynamoDB({ protectClient: client }) +``` + +## 4. Deprecated aliases + +All old names continue to work as deprecated aliases. Your IDE will show strikethrough on deprecated names, and TypeScript will emit deprecation warnings. There is no runtime behavior change when using deprecated aliases. + +You can migrate incrementally — old and new names can coexist in the same codebase. + +## 5. What hasn't changed + +- The `Result` contract (`{ data }` or `{ failure }`) is unchanged +- EQL payload shapes are unchanged +- The `.withLockContext()` chaining pattern is unchanged +- All encryption, decryption, and search operations work identically +- Environment variables (`CS_WORKSPACE_CRN`, `CS_CLIENT_ID`, etc.) are unchanged +- The `@cipherstash/protect-ffi` native module is still used internally + +## 6. Find and replace cheat sheet + +For a quick migration, run these find-and-replace operations across your codebase: + +``` +@cipherstash/protect/identify → @cipherstash/stack/identity +@cipherstash/protect/client → @cipherstash/stack/client +@cipherstash/protect → @cipherstash/stack +csTable( → encryptedTable( +csColumn( → encryptedColumn( +csValue( → encryptedValue( +protect({ → Encryption({ +await protect( → await Encryption( +ProtectClientConfig → EncryptionClientConfig +ProtectClient → EncryptionClient +ProtectErrorTypes → EncryptionErrorTypes +ProtectError → EncryptionError +ProtectTable → EncryptedTable +ProtectColumn → EncryptedColumn +ProtectValue → EncryptedValue +ProtectTableColumn → EncryptedTableColumn +ProtectOperation → EncryptionOperation +``` + +> **Important**: Run the more specific replacements first (e.g., `@cipherstash/protect/identify` before `@cipherstash/protect`) to avoid partial matches. diff --git a/README.md b/README.md index 14df6a49..843f35cb 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,41 @@ CipherStash Logo -

Protect.js

+

The CipherStash data security stack

Built by CipherStash -NPM version -npm downloads -License +NPM version +npm downloads +License Join the community on Discord ## Getting Started -Protect.js lets you encrypt every value with its own key—without sacrificing performance or usability. Encryption happens in your app; ciphertext is stored in your database. +CipherStash is the new standard for data security that feels invisible. Encrypt, control, and audit access to sensitive data directly in your TypeScript applications. -Per‑value unique keys are powered by CipherStash [ZeroKMS](https://cipherstash.com/products/zerokms) bulk key operations, backed by a root key in [AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html). +What is the Stash Stack? +Stash Stack is a collection of packages that provide a unified way to manage data security: -Visit the [documentation](#documentation) below to get started with Protect.js and explore related products. +- **Encryption** - Application-level encryption. Encrypt sensitive fields (names, emails, health records, etc.) while retaining search and filtering. Built for claim-based access control and identity-bound encryption. +- **Secrets** - Secrets management. A secure vault for secrets and sensitive config. Manage your secrets easily with a zero-trust architecture and full audit trail. +- **KMS** - Distributed key management. KMS is the key management system designed for both security and speed. + +Visit the [documentation](#documentation) below to get started. ## Documentation Visit the documentation for our products to get started: -- **[Protect.js](https://cipherstash.com/docs/protect-js)** - End-to-end field level encryption for JavaScript/TypeScript apps with zero‑knowledge key management -- **[Stash - Secrets Manager](https://getstash.sh/docs)** - Store and manage secrets like API keys and database credentials with zero-trust encryption -- **[Protect.js for Drizzle ORM](https://cipherstash.com/docs/drizzle)** - Seamlessly integrate Protect.js with Drizzle ORM and PostgreSQL +- **[Encryption](https://cipherstash.com/docs/encryption)** - End-to-end field level encryption for JavaScript/TypeScript apps with zero‑knowledge key management +- **[Secrets](https://cipherstash.com/docs/secrets)** - Store and manage secrets like API keys and database credentials with zero-trust encryption +- **[KMS](https://cipherstash.com/docs/kms)** - Distributed key management. KMS is the key management system designed for both security and speed. +- **[Stash + Drizzle](https://cipherstash.com/docs/encryption/drizzle)** - Seamlessly integrate Stash Encryption with Drizzle ORM and PostgreSQL ## Features -Protect.js protects data using industry-standard AES encryption and [ZeroKMS](https://cipherstash.com/products/zerokms) for bulk encryption and decryption operations and is up to 14x faster than AWS KMS or Hashicorp Vault. This enables every encrypted value, in every column, in every row in your database to have a unique key, without sacrificing performance. +Stash Encryption protects data using industry-standard AES encryption and [Stash KMS](https://cipherstash.com/stack/kms) for bulk encryption and decryption operations and is up to 14x faster than AWS KMS or Hashicorp Vault. This enables every encrypted value, in every column, in every row in your database to have a unique key, without sacrificing performance. **Features:** @@ -47,26 +53,26 @@ Protect.js protects data using industry-standard AES encryption and [ZeroKMS](ht - **Reduce the blast radius of data breaches**: Limit the impact of exploited vulnerabilities to only the data your end-users can decrypt > [!IMPORTANT] -> **You need to opt-out of bundling when using Protect.js.** Protect.js uses Node.js specific features and requires the use of the native Node.js `require`. See the [documentation](https://cipherstash.com/docs/protect-js) for bundling configuration guides. +> **You need to opt-out of bundling when using `@cipherstash/stack`.** This package uses Node.js specific features and requires the use of the native Node.js `require`. See the [documentation](https://cipherstash.com/docs/protect-js) for bundling configuration guides. ## Community -The Protect.js community can be found on [Discord](https://discord.gg/5qwXUFb6PB) where you can ask questions, voice ideas, and share your projects with other people. +The CipherStash community can be found on [Discord](https://discord.gg/5qwXUFb6PB) where you can ask questions, voice ideas, and share your projects with other people. -Do note that our [Code of Conduct](CODE_OF_CONDUCT.md) applies to all Protect.js community channels. Users are **highly encouraged** to read and adhere to it to avoid repercussions. +Do note that our [Code of Conduct](CODE_OF_CONDUCT.md) applies to all CipherStash community channels. Users are **highly encouraged** to read and adhere to it to avoid repercussions. ## Contributing -Contributions to Protect.js are welcome and highly appreciated. However, before you jump right into it, we would like you to review our [Contribution Guidelines](CONTRIBUTE.md) to make sure you have a smooth experience contributing to Protect.js. +Contributions are welcome and highly appreciated. However, before you jump right into it, we would like you to review our [Contribution Guidelines](CONTRIBUTE.md) to make sure you have a smooth experience. --- ## Security -If you believe you have found a security vulnerability in Protect.js, we encourage you to **_responsibly disclose this and NOT open a public issue_**. +If you believe you have found a security vulnerability, we encourage you to **_responsibly disclose this and NOT open a public issue_**. Please email [security@cipherstash.com](mailto:security@cipherstash.com) with details about the vulnerability. We will review your report and provide further instructions for submitting your report. ## License -Protect.js is [MIT licensed](./LICENSE.md). +This project is [MIT licensed](./LICENSE.md). diff --git a/SECURITY.md b/SECURITY.md index 466993a5..6516f076 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ This document describes the security posture, reporting process, and guidelines ## Supported Packages -This repository contains the JavaScript/TypeScript SDK for CipherStash Protect and related packages. +This repository contains the JavaScript/TypeScript SDK for CipherStash Stash Encryption and related packages. The below tables list each package along with the currently supported (receiving security updates). @@ -91,8 +91,8 @@ We will never take legal action against good-faith security researchers who foll The following are **in scope**: - The `cipherstash/protectjs` GitHub repository -- All published NPM packages under the `@cipherstash/protect*` namespace -- Protect.js cryptographic implementations, configuration layers, and CLI tooling +- All published NPM packages under the `@cipherstash/*` namespace +- Stash Encryption cryptographic implementations, configuration layers, and CLI tooling - Key-handling, authenticated encryption behaviour, JSON/JSONB field-level encryption flows - Documentation or code examples that could lead to insecure usage - CipherStash’s internal infrastructure diff --git a/docs/README.md b/docs/README.md index 0e1192c9..6d8bb059 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# Protect.js documentation +# Stash Encryption documentation -The documentation for Protect.js is organized into the following sections: +The documentation for Stash Encryption is organized into the following sections: - [Getting started](./getting-started.md) @@ -12,7 +12,7 @@ The documentation for Protect.js is organized into the following sections: - [Configuration and production deployment](./reference/configuration.md) - [Searchable encryption with PostgreSQL](./reference/searchable-encryption-postgres.md) -- [Protect.js schemas](./reference/schema.md) +- [Stash Encryption schemas](./reference/schema.md) - [Model operations with bulk crypto functions](./reference/model-operations.md) ### ORMs and frameworks diff --git a/docs/concepts/aws-kms-vs-cipherstash-comparison.md b/docs/concepts/aws-kms-vs-cipherstash-comparison.md index d0a52f19..e1a26a95 100644 --- a/docs/concepts/aws-kms-vs-cipherstash-comparison.md +++ b/docs/concepts/aws-kms-vs-cipherstash-comparison.md @@ -1,6 +1,6 @@ -# Why Protect.js Makes Encryption Simple: A Comparison with AWS KMS +# Why Stash Encryption Makes Encryption Simple: A Comparison with AWS KMS -Encrypting data shouldn't require managing binary buffers, base64 encoding, key ARNs, or building custom search solutions. Protect.js eliminates these complexities, giving you encryption that "just works" with a developer-friendly API. +Encrypting data shouldn't require managing binary buffers, base64 encoding, key ARNs, or building custom search solutions. Stash Encryption eliminates these complexities, giving you encryption that "just works" with a developer-friendly API. ## The Simple Truth: Encrypting a Value @@ -55,18 +55,18 @@ const encrypted = await encryptWithKMS('secret@squirrel.example'); - ❌ Region configuration - ❌ AWS credential setup -### Protect.js: One Simple Call +### Stash Encryption: One Simple Call ```typescript -import { protect, csTable, csColumn } from '@cipherstash/protect'; +import { Encryption, encryptedTable, encryptedColumn } from '@cipherstash/stack'; // One-time setup: Define your schema -const users = csTable('users', { - email: csColumn('email'), +const users = encryptedTable('users', { + email: encryptedColumn('email'), }); // One-time setup: Initialize client -const protectClient = await protect({ +const protectClient = await Encryption({ schemas: [users], }); @@ -125,7 +125,7 @@ async function decryptWithKMS(base64Ciphertext: string): Promise { } ``` -### Protect.js: One Line +### Stash Encryption: One Line ```typescript // Decrypt: One call, returns typed value @@ -147,12 +147,12 @@ const plaintext = decryptResult.data; // Already a string, typed correctly - Storing plaintext indexes (defeats the purpose of encryption) - Building a custom searchable encryption solution (months of work) -**Protect.js:** Searchable encryption is built-in and works with PostgreSQL: +**Stash Encryption:** Searchable encryption is built-in and works with PostgreSQL: ```typescript // Just add search capabilities to your schema -const users = csTable('users', { - email: csColumn('email') +const users = encryptedTable('users', { + email: encryptedColumn('email') .freeTextSearch() // Full-text search .equality() // WHERE email = ? .orderAndRange(), // ORDER BY, range queries @@ -196,10 +196,10 @@ const command = new EncryptCommand({ // You must manually ensure the same context is used for decryption ``` -**Protect.js:** Built-in identity-aware encryption with `LockContext`: +**Stash Encryption:** Built-in identity-aware encryption with `LockContext`: ```typescript -import { LockContext } from '@cipherstash/protect/identify'; +import { LockContext } from '@cipherstash/stack/identity'; // Create lock context from user JWT (one line) const lc = new LockContext(); @@ -211,7 +211,7 @@ const encryptResult = await protectClient.encrypt( { column: users.email, table: users } ).withLockContext(lockContext); -// Decrypt requires the same lock context (enforced by Protect.js) +// Decrypt requires the same lock context (enforced by Stash Encryption) const decryptResult = await protectClient.decrypt(ciphertext) .withLockContext(lockContext); ``` @@ -234,10 +234,10 @@ const encryptedItems = await Promise.all( // Hope you don't hit rate limits or need to retry ``` -**Protect.js:** Native bulk encryption optimized for performance: +**Stash Encryption:** Native bulk encryption optimized for performance: ```typescript -// Protect.js: One call for bulk encryption +// Stash Encryption: One call for bulk encryption const bulkPlaintexts = [ { id: '1', plaintext: 'Alice' }, { id: '2', plaintext: 'Bob' }, @@ -273,7 +273,7 @@ try { } ``` -**Protect.js:** Type-safe Result pattern: +**Stash Encryption:** Type-safe Result pattern: ```typescript const result = await protectClient.encrypt(plaintext, options); @@ -300,7 +300,7 @@ if (result.failure) { const plaintext: string = Buffer.from(response.Plaintext).toString('utf-8'); ``` -**Protect.js:** Full TypeScript support with inferred types: +**Stash Encryption:** Full TypeScript support with inferred types: ```typescript // TypeScript infers the return type automatically @@ -317,7 +317,7 @@ const base64 = Buffer.from(ciphertext).toString('base64'); // Store in database as TEXT or BLOB ``` -**Protect.js:** JSON payload ready for database: +**Stash Encryption:** JSON payload ready for database: ```typescript // Returns JSON payload ready for JSONB storage @@ -364,18 +364,18 @@ const decrypted = await decrypt(encrypted); **Lines of code:** ~25 lines for basic encrypt/decrypt **What you manage:** Key ARNs, binary conversions, base64 encoding, error handling, AWS credentials, regions -### Protect.js: Full Implementation +### Stash Encryption: Full Implementation ```typescript -import { protect, csTable, csColumn } from '@cipherstash/protect'; +import { Encryption, encryptedTable, encryptedColumn } from '@cipherstash/stack'; // One-time schema definition -const users = csTable('users', { - email: csColumn('email'), +const users = encryptedTable('users', { + email: encryptedColumn('email'), }); // One-time initialization -const protectClient = await protect({ +const protectClient = await Encryption({ schemas: [users], }); @@ -402,11 +402,11 @@ const plaintext = decryptResult.data; ``` **Lines of code:** ~20 lines including setup -**What you manage:** Nothing—Protect.js handles it all +**What you manage:** Nothing—Stash Encryption handles it all ## Feature Comparison -| Feature | AWS KMS | Protect.js | +| Feature | AWS KMS | Stash Encryption | |---------|---------|------------| | **Basic Encryption** | ✅ Requires manual buffer/base64 handling | ✅ One-line API, JSON payload | | **Key Management** | ❌ You manage key ARNs | ✅ Zero-knowledge, automatic | @@ -430,7 +430,7 @@ const plaintext = decryptResult.data; - Implementing identity-aware encryption yourself - Managing key ARNs and AWS configuration -**Protect.js** gives you: +**Stash Encryption** gives you: - A simple, type-safe API - Built-in searchable encryption - Built-in identity-aware encryption @@ -448,7 +448,7 @@ const plaintext = decryptResult.data; - You don't need to search encrypted data - You're comfortable with manual buffer/base64 handling -### Use Protect.js when: +### Use Stash Encryption when: - You're building applications with databases - You need to search encrypted data - You want a developer-friendly API @@ -461,6 +461,6 @@ const plaintext = decryptResult.data; ## References - [AWS KMS Documentation](https://docs.aws.amazon.com/kms/) -- [CipherStash Protect.js Getting Started](./getting-started.md) +- [CipherStash Stash Encryption Getting Started](./getting-started.md) - [CipherStash Schema Reference](./reference/schema.md) - [Searchable Encryption Concepts](./concepts/searchable-encryption.md) diff --git a/docs/concepts/searchable-encryption.md b/docs/concepts/searchable-encryption.md index 56ca41fa..499a80a1 100644 --- a/docs/concepts/searchable-encryption.md +++ b/docs/concepts/searchable-encryption.md @@ -1,6 +1,6 @@ # Searchable encryption -Protect.js supports searching encrypted data, which enables trusted data access so that you can: +Stash Encryption supports searching encrypted data, which enables trusted data access so that you can: 1. Prove to your customers that you can track exactly what data is being accessed in your application. 2. Provide evidence for compliance requirements, such as [SOC 2](https://cipherstash.com/compliance/soc2) and [BDSG](https://cipherstash.com/compliance/bdsg). @@ -63,7 +63,7 @@ CipherStash's approach to searchable encryption solves the performance problem w ### Using Encrypt Query Language (EQL) -CipherStash uses [EQL](https://github.com/cipherstash/encrypt-query-language) to perform queries on encrypted data, and Protect.js makes it easy to use EQL with any TypeScipt application. +CipherStash uses [EQL](https://github.com/cipherstash/encrypt-query-language) to perform queries on encrypted data, and Stash Encryption makes it easy to use EQL with any TypeScipt application. ```ts // 1) Encrypt the search term @@ -92,7 +92,7 @@ const equalitySQL = ` const result = await client.query(equalitySQL, [ protectedUser.email.getName(), encryptedParam.data ]) ``` -Using the above approach, Protect.js is generating the EQL payloads and which means you never have to drop down to writing complex SQL queries. +Using the above approach, Stash Encryption is generating the EQL payloads and which means you never have to drop down to writing complex SQL queries. So does this solve the original problem of searching on encrypted data? @@ -103,7 +103,7 @@ So does this solve the original problem of searching on encrypted data? 1 | Alice Johnson | mBbKmsMMkbKBSN... ``` -The answer is yes! And you can use Protect.js to [decrypt the results in your application](../../README.md#decrypting-data). +The answer is yes! And you can use Stash Encryption to [decrypt the results in your application](../../README.md#decrypting-data). ## How fast is CipherStash's searchable encryption? @@ -133,7 +133,7 @@ With searchable encryption: - Data can be encrypted, stored, and searched in your existing PostgreSQL database. - Encrypted data can be searched using equality, free text search, and range queries. -- Data remains encrypted, and will be decrypted using the Protect.js library in your application. +- Data remains encrypted, and will be decrypted using the Stash Encryption library in your application. - Queries are blazing fast, and won't slow down your application experience. - Every decryption event is logged, giving you an audit trail of data access events. diff --git a/docs/getting-started.md b/docs/getting-started.md index 84c154c0..bc1a8b9a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -2,7 +2,7 @@ This getting started guide steps you through: -1. Installing and configuring Protect.js in a standalone project +1. Installing and configuring Stash Encryption in a standalone project 2. Encrypting, searching, and decrypting data in a PostgreSQL database > [!IMPORTANT] @@ -15,10 +15,10 @@ This getting started guide steps you through: ## Table of contents - [Step 0: Basic file structure](#step-0-basic-file-structure) -- [Step 1: Install Protect.js](#step-1-install-protectjs) +- [Step 1: Install Stash Encryption](#step-1-install-stash-encryption) - [Step 2: Set up credentials](#step-2-set-up-credentials) - [Step 3: Define your schema](#step-3-define-your-schema) -- [Step 4: Initialize the Protect client](#step-4-initialize-the-protect-client) +- [Step 4: Initialize the Encryption client](#step-4-initialize-the-encryption-client) - [Step 5: Encrypt data](#step-5-encrypt-data) - [Step 6: Decrypt data](#step-6-decrypt-data) - [Step 7: Store encrypted data in a database](#step-7-store-encrypted-data-in-a-database) @@ -26,7 +26,7 @@ This getting started guide steps you through: ## Step 0: Basic file structure The following is the basic file structure of the standalone project for this getting started guide. -In the `src/protect/` directory, we have the table definition in `schema.ts` and the Protect.js client in `index.ts`. +In the `src/protect/` directory, we have the table definition in `schema.ts` and the Stash Encryption client in `index.ts`. ``` 📦 @@ -42,7 +42,7 @@ In the `src/protect/` directory, we have the table definition in `schema.ts` and └ 📜 tsconfig.json ``` -If you're following this getting started guide with an existing app, skip to [the next step](#step-1-install-protectjs). +If you're following this getting started guide with an existing app, skip to [the next step](#step-1-install-stash-encryption). If you're following this getting started guide with a clean slate, create a basic structure by running: @@ -53,36 +53,36 @@ git init npm init -y ``` -## Step 1: Install Protect.js +## Step 1: Install Stash Encryption -Install the [`@cipherstash/protect` package](https://www.npmjs.com/package/@cipherstash/protect) with your package manager of choice: +Install the [`@cipherstash/stack` package](https://www.npmjs.com/package/@cipherstash/stack) with your package manager of choice: ```bash -npm install @cipherstash/protect +npm install @cipherstash/stack # or -yarn add @cipherstash/protect +yarn add @cipherstash/stack # or -pnpm add @cipherstash/protect +pnpm add @cipherstash/stack ``` > [!TIP] -> [Bun](https://bun.sh/) is not currently supported due to a lack of [Node-API compatibility](https://github.com/oven-sh/bun/issues/158). Under the hood, Protect.js uses [CipherStash Client](#cipherstash-client) which is written in Rust and embedded using [Neon](https://github.com/neon-bindings/neon). +> [Bun](https://bun.sh/) is not currently supported due to a lack of [Node-API compatibility](https://github.com/oven-sh/bun/issues/158). Under the hood, Stash Encryption uses [CipherStash Client](#cipherstash-client) which is written in Rust and embedded using [Neon](https://github.com/neon-bindings/neon). > [!NOTE] -> **You need to opt out of bundling when using Protect.js.** +> **You need to opt out of bundling when using Stash Encryption.** > -> Protect.js uses Node.js specific features and requires the use of the [native Node.js `require`](https://nodejs.org/api/modules.html#requireid). +> Stash Encryption uses Node.js specific features and requires the use of the [native Node.js `require`](https://nodejs.org/api/modules.html#requireid). > > You need to opt out of bundling for tools like [Webpack](https://webpack.js.org/configuration/externals/), [esbuild](https://webpack.js.org/configuration/externals/), or [Next.js](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages). > -> Read more about [building and bundling with Protect.js](#builds-and-bundling). +> Read more about [building and bundling with Stash Encryption](#builds-and-bundling). ## Step 2: Set up credentials If you haven't already, sign up for a [CipherStash account](https://cipherstash.com/signup). Once you have an account, you will create a Workspace which is scoped to your application environment. -Follow the onboarding steps to get your first set of credentials required to use Protect.js. +Follow the onboarding steps to get your first set of credentials required to use Stash Encryption. By the end of the onboarding, you will have the following environment variables: ```bash @@ -96,19 +96,19 @@ Save these environment variables to a `.env` file in your project. ## Step 3: Define your schema -Protect.js uses a schema to define the tables and columns that you want to encrypt and decrypt. +Stash Encryption uses a schema to define the tables and columns that you want to encrypt and decrypt. To define your tables and columns, add the following to `src/protect/schema.ts`: ```ts -import { csTable, csColumn } from "@cipherstash/protect"; +import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; -export const users = csTable("users", { - email: csColumn("email"), +export const users = encryptedTable("users", { + email: encryptedColumn("email"), }); -export const orders = csTable("orders", { - address: csColumn("address"), +export const orders = encryptedTable("orders", { + address: encryptedColumn("address"), }); ``` @@ -117,37 +117,37 @@ export const orders = csTable("orders", { If you want to search encrypted data in your PostgreSQL database, you must declare the indexes in schema in `src/protect/schema.ts`: ```ts -import { csTable, csColumn } from "@cipherstash/protect"; +import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; -export const users = csTable("users", { - email: csColumn("email").freeTextSearch().equality().orderAndRange(), +export const users = encryptedTable("users", { + email: encryptedColumn("email").freeTextSearch().equality().orderAndRange(), }); -export const orders = csTable("orders", { - address: csColumn("address"), +export const orders = encryptedTable("orders", { + address: encryptedColumn("address"), }); ``` Read more about [defining your schema](./docs/reference/schema.md). -## Step 4: Initialize the Protect client +## Step 4: Initialize the Encryption client -To import the `protect` function and initialize a client with your defined schema, add the following to `src/protect/index.ts`: +To import the `Encryption` function and initialize a client with your defined schema, add the following to `src/protect/index.ts`: ```ts -import { protect, type ProtectClientConfig } from "@cipherstash/protect"; +import { Encryption, type EncryptionClientConfig } from "@cipherstash/stack"; import { users, orders } from "./schema"; -const config: ProtectClientConfig = { +const config: EncryptionClientConfig = { schemas: [users, orders], } -export const protectClient = await protect(config); +export const protectClient = await Encryption(config); ``` ## Step 5: Encrypt data -Protect.js provides the `encrypt` function on `protectClient` to encrypt data. +Stash Encryption provides the `encrypt` function on `protectClient` to encrypt data. `encrypt` takes a plaintext string, and an object with the table and column as parameters. Start encrypting data by adding this to `src/index.ts`: diff --git a/docs/how-to/lock-contexts-with-clerk.md b/docs/how-to/lock-contexts-with-clerk.md index 7e267525..ad5f088f 100644 --- a/docs/how-to/lock-contexts-with-clerk.md +++ b/docs/how-to/lock-contexts-with-clerk.md @@ -68,7 +68,7 @@ export default async function Page() { Since the CTS token is already available, you can construct a `LockContext` object with the existing CTS token. ```typescript -import { LockContext } from '@cipherstash/protect/identify' +import { LockContext } from '@cipherstash/stack/identity' import { getCtsToken } from '@cipherstash/nextjs' export default async function Page() { @@ -95,7 +95,7 @@ export default async function Page() { If you want to override the default context, you can pass a custom context to the `LockContext` constructor. ```typescript -import { LockContext } from '@cipherstash/protect/identify' +import { LockContext } from '@cipherstash/stack/identity' // protectClient from the previous steps const lc = new LockContext({ diff --git a/docs/how-to/nextjs-external-packages.md b/docs/how-to/nextjs-external-packages.md index 0b4655c9..f342d5f5 100644 --- a/docs/how-to/nextjs-external-packages.md +++ b/docs/how-to/nextjs-external-packages.md @@ -1,6 +1,6 @@ # Next.js -Using `@cipherstash/protect` with Next.js? You need to opt-out from the Server Components bundling and use native Node.js `require` instead. +Using `@cipherstash/stack` with Next.js? You need to opt-out from the Server Components bundling and use native Node.js `require` instead. ## Using version 15 or later @@ -9,7 +9,7 @@ Using `@cipherstash/protect` with Next.js? You need to opt-out from the Server C ```js const nextConfig = { ... - serverExternalPackages: ['@cipherstash/protect'], + serverExternalPackages: ['@cipherstash/stack'], } ``` @@ -21,7 +21,7 @@ const nextConfig = { const nextConfig = { ... experimental: { - serverComponentsExternalPackages: ['@cipherstash/protect'], + serverComponentsExternalPackages: ['@cipherstash/stack'], }, } ``` diff --git a/docs/how-to/npm-lockfile-v3.md b/docs/how-to/npm-lockfile-v3.md index 58559169..9f999a66 100644 --- a/docs/how-to/npm-lockfile-v3.md +++ b/docs/how-to/npm-lockfile-v3.md @@ -2,7 +2,7 @@ Some npm users see deployments fail on Linux (e.g., AWS Lambda) when their `package-lock.json` was created on macOS or Windows. -This happens with `package-lock.json` version 3, where npm only records certain optional native pieces for the platform that created the lockfile. As a result, Linux builds can miss the native engine that Protect.js needs at runtime. +This happens with `package-lock.json` version 3, where npm only records certain optional native pieces for the platform that created the lockfile. As a result, Linux builds can miss the native engine that Stash Encryption needs at runtime. ## Who is affected @@ -12,7 +12,7 @@ This happens with `package-lock.json` version 3, where npm only records certain ## What you might see -- Build succeeds, but the app fails to start on Linux with an error like “failed to load native addon” or “module not found” related to the Protect.js engine +- Build succeeds, but the app fails to start on Linux with an error like "failed to load native addon" or "module not found" related to the Stash Encryption engine ## Fixes (pick one) diff --git a/docs/how-to/sst-external-packages.md b/docs/how-to/sst-external-packages.md index 714a795c..24e965f0 100644 --- a/docs/how-to/sst-external-packages.md +++ b/docs/how-to/sst-external-packages.md @@ -1,6 +1,6 @@ # SST and esbuild -Using `@cipherstash/protect` in a serverless function deployed with [SST](https://sst.dev/)? +Using `@cipherstash/stack` in a serverless function deployed with [SST](https://sst.dev/)? You need to configure the `nodejs.esbuild.external` and `nodejs.install` options in your `sst.config.ts` file as documented [here](https://sst.dev/docs/component/aws/function/#nodejs): @@ -8,9 +8,9 @@ You need to configure the `nodejs.esbuild.external` and `nodejs.install` options ... nodejs: { esbuild: { - external: ['@cipherstash/protect'], + external: ['@cipherstash/stack'], }, - install: ['@cipherstash/protect'], + install: ['@cipherstash/stack'], }, ... ``` diff --git a/docs/prompts/init-protect.md b/docs/prompts/init-protect.md index df45ea33..3e46ab50 100644 --- a/docs/prompts/init-protect.md +++ b/docs/prompts/init-protect.md @@ -1,44 +1,44 @@ -# Implementing Protect.js into a Node.js application +# Implementing Stash Encryption into a Node.js application -Your task is to introduce Protect.js into a Node.js application. Protect.js requires the Node.js runtime so it will still work with framweworks like Next.js and Tanstack Start, since they support running code that only executes on the server. +Your task is to introduce Stash Encryption into a Node.js application. Stash Encryption requires the Node.js runtime so it will still work with framweworks like Next.js and Tanstack Start, since they support running code that only executes on the server. --- -## Installing Protect.js +## Installing Stash Encryption -Determine what package manager the application is using. This will either be `pnpm`, `npm`, or `bun` and then use the appropriate package manager to add Protect.js to the application: +Determine what package manager the application is using. This will either be `pnpm`, `npm`, or `bun` and then use the appropriate package manager to add Stash Encryption to the application: ```bash -npm install @cipehrstash/protect +npm install @cipherstash/stack # or -pnpm add @cipehrstash/protect +pnpm add @cipherstash/stack # or -bun add @cipehrstash/protect +bun add @cipherstash/stack ``` -If you detect a mono repo, you need to ask the user which application they want Protect.js installed in. +If you detect a mono repo, you need to ask the user which application they want Stash Encryption installed in. --- -## Adding scafolding for the Protect.js client, schemas, and example code +## Adding scafolding for the Stash Encryption client, schemas, and example code In the rool of the application (if the application is configred to use something like `src` then this is where these operations will occur), you need to add a `protect` directory with the following files/content. If the application uses TypeScript use the `.ts` extension, else use the `.js` extension. `protect/schemas.(ts/js)` ```js -import { csTable, csColumn } from '@cipherstash/protect` +import { encryptedTable, encryptedColumn } from '@cipherstash/stack' -export const protectedExample = csTable('example_table', { - sensitiveData: csColumn('sensitiveData'), +export const protectedExample = encryptedTable('example_table', { + sensitiveData: encryptedColumn('sensitiveData'), } ``` `protect/index.(ts/js)` ```js -import { protect } from '@cipehrstash/protect' +import { Encryption } from '@cipherstash/stack' import { * as protectSchemas } from './schemas' -export const protectClient = protect({ +export const protectClient = Encryption({ schemas: [...protectSchemas] }) ``` diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 40f0c813..87122f5d 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1,6 +1,6 @@ -# Protect.js configuration +# Stash Encryption configuration -Protect.js is configured with [toml](https://toml.io/en/) files or environment variables, and is used to initialize the client when Protect.js is initialized. +Stash Encryption is configured with [toml](https://toml.io/en/) files or environment variables, and is used to initialize the client when Stash Encryption is initialized. Environment variables will take precedence over configuration files and it's recommented to use them for sensitive values. @@ -12,7 +12,7 @@ Environment variables will take precedence over configuration files and it's rec - [`[auth]` section](#auth-section) - [cipherstash.secret.toml](#cipherstashsecrettoml) - [Environment variables](#environment-variables) -- [Configuring the Protect client directly](#configuring-the-protect-client-directly) +- [Configuring the Encryption client directly](#configuring-the-encryption-client-directly) - [Deploying to production](#deploying-to-production) - [Region configuration](#region-configuration) - [File system write permissions](#file-system-write-permissions) @@ -86,7 +86,7 @@ This is critical for encrypting/decrypting data. ## Environment variables -You can also use environment variables to configure Protect.js. +You can also use environment variables to configure Stash Encryption. The following environment variables are supported: | Variable name | Description | Required | Default | @@ -97,16 +97,16 @@ The following environment variables are supported: | `CS_CLIENT_ACCESS_KEY` | The access key for your CipherStash account. | Yes | | | `CS_CONFIG_PATH` | A temporary path to store the CipherStash client configuration. | No | `/home/{username}/.cipherstash` | -## Configuring the Protect client directly +## Configuring the Encryption client directly -You can also configure the Protect client directly by passing a `ProtectClientConfig` object to the `protect` function during initialization. -This is useful if you want to configure the Protect client specific to your application. +You can also configure the Encryption client directly by passing an `EncryptionClientConfig` object to the `Encryption` function during initialization. +This is useful if you want to configure the Encryption client specific to your application. An exmaple of this might be if you want to use a secret manager to store your client key and access key rather than relying on environment variables or configuration files. ```ts -import { protect, type ProtectClientConfig } from "@cipherstash/protect"; +import { Encryption, type EncryptionClientConfig } from "@cipherstash/stack"; -const config: ProtectClientConfig = { +const config: EncryptionClientConfig = { schemas: [users, orders], workspaceCrn: "your-workspace-crn", accessKey: "your-access-key", @@ -114,13 +114,13 @@ const config: ProtectClientConfig = { clientKey: "your-client-key", } -const protectClient = await protect(config); +const protectClient = await Encryption(config); ``` ## Deploying to production > [!TIP] -> There are some configuration details you should take note of when deploying `@cipherstash/protect` in your production examples. +> There are some configuration details you should take note of when deploying `@cipherstash/stack` in your production examples. ### File system write permissions diff --git a/docs/reference/drizzle/DRIFT-TESTING.md b/docs/reference/drizzle/DRIFT-TESTING.md index 7d70de93..d1375d4f 100644 --- a/docs/reference/drizzle/DRIFT-TESTING.md +++ b/docs/reference/drizzle/DRIFT-TESTING.md @@ -1,6 +1,6 @@ # Documentation Drift Testing -This document describes the documentation drift detection system for the Drizzle + Protect.js integration. The system ensures that code examples in documentation remain executable and accurate as the codebase evolves. +This document describes the documentation drift detection system for the Drizzle + Stash Encryption integration. The system ensures that code examples in documentation remain executable and accurate as the codebase evolves. ## Overview diff --git a/docs/reference/drizzle/drizzle-protect.md b/docs/reference/drizzle/drizzle-protect.md index 8afc55a8..3d9ce7f7 100644 --- a/docs/reference/drizzle/drizzle-protect.md +++ b/docs/reference/drizzle/drizzle-protect.md @@ -1,7 +1,7 @@ -# Drizzle + Protect.js Query Examples +# Drizzle + Stash Encryption Query Examples ## Manual Encryption Pattern (Verbose) -This page demonstrates how to perform queries on encrypted data using **Drizzle ORM** with **explicit Protect.js encryption calls** for full control. +This page demonstrates how to perform queries on encrypted data using **Drizzle ORM** with **explicit Stash Encryption encryption calls** for full control. **Pattern:** Manually encrypt query values before passing them to standard Drizzle operators. @@ -33,7 +33,7 @@ This gives you explicit visibility into the encryption/decryption workflow at th ✅ **Use the manual encryption pattern when:** - You need fine-grained control over encryption timing -- You want to understand how Protect.js works internally +- You want to understand how Stash Encryption works internally - You're building custom abstractions or utilities - You need to cache encrypted values for performance - You're implementing batch operations with encryption @@ -348,7 +348,7 @@ return decrypted.data ## Order by encrypted data -Protect.js supports ordering on encrypted fields using Order-Revealing Encryption (ORE). This allows the database to sort encrypted values without decrypting them, while preserving the original sort order. +Stash Encryption supports ordering on encrypted fields using Order-Revealing Encryption (ORE). This allows the database to sort encrypted values without decrypting them, while preserving the original sort order. ### Order by encrypted number @@ -477,7 +477,7 @@ When using the manual encryption pattern instead of protect operators, you have ✅ **Use manual encryption when:** - You need fine-grained control over encryption timing -- You want to understand how Protect.js works internally +- You want to understand how Stash Encryption works internally - You're building custom abstractions - You need to cache encrypted values - You're implementing batch operations @@ -559,7 +559,7 @@ protectTransactions.created_at // Note: snake_case - **Compare patterns**: Try the same query with both protect operators and manual encryption - **Explore the code**: Check out the source code in the repository - **Try different queries**: Modify the examples above and run them -- **Read the docs**: Visit [CipherStash Protect.js documentation](https://docs.cipherstash.com/) +- **Read the docs**: Visit [CipherStash Stash Encryption documentation](https://docs.cipherstash.com/) - **Integrate into your app**: Use these patterns in your own applications --- diff --git a/docs/reference/drizzle/drizzle.md b/docs/reference/drizzle/drizzle.md index f250bab0..e32f38bb 100644 --- a/docs/reference/drizzle/drizzle.md +++ b/docs/reference/drizzle/drizzle.md @@ -1,7 +1,7 @@ -# Drizzle + Protect.js Query Examples +# Drizzle + Stash Encryption Query Examples ## Protect Operators Pattern (Recommended) -This page demonstrates how to perform queries on encrypted data using **Drizzle ORM** with **CipherStash Protect.js** using the **protect operators pattern**. +This page demonstrates how to perform queries on encrypted data using **Drizzle ORM** with **CipherStash Stash Encryption** using the **protect operators pattern**. **Pattern:** Auto-encrypting operators from `createProtectOperators()` provide clean syntax with automatic encryption. @@ -234,7 +234,7 @@ return results ## Order by encrypted data -Protect.js supports ordering on encrypted fields using Order-Revealing Encryption (ORE). This allows the database to sort encrypted values without decrypting them, while preserving the original sort order. +Stash Encryption supports ordering on encrypted fields using Order-Revealing Encryption (ORE). This allows the database to sort encrypted values without decrypting them, while preserving the original sort order. ### Order by encrypted number @@ -313,7 +313,7 @@ return result ## Understanding the results -All results are automatically **decrypted** by Protect.js before being returned to you. The data remains encrypted in the database at all times. +All results are automatically **decrypted** by Stash Encryption before being returned to you. The data remains encrypted in the database at all times. ### What's happening behind the scenes @@ -337,7 +337,7 @@ All results are automatically **decrypted** by Protect.js before being returned - **Explore the code**: Check out the source code in the repository - **Try different queries**: Modify the examples above and run them -- **Read the docs**: Visit [CipherStash Protect.js documentation](https://docs.cipherstash.com/) +- **Read the docs**: Visit [CipherStash Stash Encryption documentation](https://docs.cipherstash.com/) - **Integrate into your app**: Use these patterns in your own applications --- diff --git a/docs/reference/model-operations.md b/docs/reference/model-operations.md index 5a241214..865c576b 100644 --- a/docs/reference/model-operations.md +++ b/docs/reference/model-operations.md @@ -1,6 +1,6 @@ # Model operations -Model operations in Protect.js provide a high-level interface for encrypting and decrypting entire objects. +Model operations in Stash Encryption provide a high-level interface for encrypting and decrypting entire objects. These operations automatically handle the encryption of fields defined in your schema while preserving other fields. ## Table of contents @@ -115,7 +115,7 @@ const decryptedUsers = decryptedResult.data; ### Using type parameters -Protect.js provides strong TypeScript support through generic type parameters: +Stash Encryption provides strong TypeScript support through generic type parameters: ```typescript // Define your model type @@ -157,9 +157,9 @@ The type system ensures: The model operations can infer types from your schema definition: ```typescript -const users = csTable("users", { - email: csColumn("email").freeTextSearch(), - address: csColumn("address"), +const users = encryptedTable("users", { + email: encryptedColumn("email").freeTextSearch(), + address: encryptedColumn("address"), }); // Types are inferred from the schema @@ -201,10 +201,10 @@ const result = await protectClient.encryptModel(user, users); if (result.failure) { // Handle specific error types switch (result.failure.type) { - case ProtectErrorTypes.EncryptionError: + case EncryptionErrorTypes.EncryptionError: console.error("Encryption failed:", result.failure.message); break; - case ProtectErrorTypes.ClientInitError: + case EncryptionErrorTypes.ClientInitError: console.error("Client not initialized:", result.failure.message); break; default: diff --git a/docs/reference/schema.md b/docs/reference/schema.md index b828bdf4..bd040712 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -1,6 +1,6 @@ -# Protect.js schema +# Stash Encryption schema -Protect.js lets you define a schema in TypeScript with properties that map to your database columns, and define indexes and casting for each column which are used when searching on encrypted data. +Stash Encryption lets you define a schema in TypeScript with properties that map to your database columns, and define indexes and casting for each column which are used when searching on encrypted data. ## Table of contents @@ -10,11 +10,11 @@ Protect.js lets you define a schema in TypeScript with properties that map to yo - [Searchable encryption](#searchable-encryption) - [Nested objects](#nested-objects) - [Available index options](#available-index-options) -- [Initializing the Protect client](#initializing-the-protect-client) +- [Initializing the Encryption client](#initializing-the-encryption-client) ## Creating schema files -You can declare your Protect.js schema directly in TypeScript either in a single `schema.ts` file, or you can split your schema into multiple files. It's up to you. +You can declare your Stash Encryption schema directly in TypeScript either in a single `schema.ts` file, or you can split your schema into multiple files. It's up to you. Example in a single file: @@ -38,15 +38,15 @@ or in multiple files: ## Understanding schema files -A schema represents a mapping of your database, and which columns you want to encrypt and index. Thus, it's a collection of tables and columns represented with `csTable` and `csColumn`. +A schema represents a mapping of your database, and which columns you want to encrypt and index. Thus, it's a collection of tables and columns represented with `encryptedTable` and `encryptedColumn`. The below is pseudo-code for how these mappings are defined: ```ts -import { csTable, csColumn } from "@cipherstash/protect"; +import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; -export const tableNameInTypeScript = csTable("tableNameInDatabase", { - columnNameInTypeScript: csColumn("columnNameInDatabase"), +export const tableNameInTypeScript = encryptedTable("tableNameInDatabase", { + columnNameInTypeScript: encryptedColumn("columnNameInDatabase"), }); ``` @@ -54,13 +54,13 @@ export const tableNameInTypeScript = csTable("tableNameInDatabase", { Now that you understand how your schema is defined, let's dive into how you can configure your schema. -Start by importing the `csTable` and `csColumn` functions from `@cipherstash/protect` and create a new table with a column. +Start by importing the `encryptedTable` and `encryptedColumn` functions from `@cipherstash/stack` and create a new table with a column. ```ts -import { csTable, csColumn } from "@cipherstash/protect"; +import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; -export const protectedUsers = csTable("users", { - email: csColumn("email"), +export const protectedUsers = encryptedTable("users", { + email: encryptedColumn("email"), }); ``` @@ -69,35 +69,35 @@ export const protectedUsers = csTable("users", { If you are looking to enable searchable encryption in a PostgreSQL database, you must declaratively enable the indexes in your schema by chaining the index options to the column. ```ts -import { csTable, csColumn } from "@cipherstash/protect"; +import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; -export const protectedUsers = csTable("users", { - email: csColumn("email").freeTextSearch().equality().orderAndRange(), +export const protectedUsers = encryptedTable("users", { + email: encryptedColumn("email").freeTextSearch().equality().orderAndRange(), }); ``` ### Nested objects -Protect.js supports nested objects in your schema, allowing you to encrypt **but not search on** nested properties. You can define nested objects up to 3 levels deep. +Stash Encryption supports nested objects in your schema, allowing you to encrypt **but not search on** nested properties. You can define nested objects up to 3 levels deep. This is useful for data stores that have less structured data, like NoSQL databases. -You can define nested objects by using the `csValue` function to define a value in a nested object. The value naming convention of the `csValue` function is a dot-separated string of the nested object path, e.g. `profile.name` or `profile.address.street`. +You can define nested objects by using the `encryptedValue` function to define a value in a nested object. The value naming convention of the `encryptedValue` function is a dot-separated string of the nested object path, e.g. `profile.name` or `profile.address.street`. > [!NOTE] > Using nested objects is not recommended for SQL databases, as it will not be searchable. > You should either use a JSON data type and encrypt the entire object, or use a separate column for each nested property. ```ts -import { csTable, csColumn, csValue } from "@cipherstash/protect"; +import { encryptedTable, encryptedColumn, encryptedValue } from "@cipherstash/stack"; -export const protectedUsers = csTable("users", { - email: csColumn("email").freeTextSearch().equality().orderAndRange(), +export const protectedUsers = encryptedTable("users", { + email: encryptedColumn("email").freeTextSearch().equality().orderAndRange(), profile: { - name: csValue("profile.name"), + name: encryptedValue("profile.name"), address: { - street: csValue("profile.address.street"), + street: encryptedValue("profile.address.street"), location: { - coordinates: csValue("profile.address.location.coordinates"), + coordinates: encryptedValue("profile.address.location.coordinates"), }, }, }, @@ -112,7 +112,7 @@ When working with nested objects: - Optional nested objects are supported > [!WARNING] -> TODO: The schema builder does not validate the values you supply to the `csValue` or `csColumn` functions. +> TODO: The schema builder does not validate the values you supply to the `encryptedValue` or `encryptedColumn` functions. > These values are meant to be unique, and and cause unexpected behavior if they are not defined correctly. ## Available index options @@ -127,20 +127,20 @@ The following index options are available for your schema: You can chain these methods to your column to configure them in any combination. -## Initializing the Protect client +## Initializing the Encryption client You will use your defined schemas to initialize the EQL client. -Simply import your schemas and pass them to the `protect` function. +Simply import your schemas and pass them to the `Encryption` function. ```ts -import { protect, type ProtectClientConfig } from "@cipherstash/protect"; +import { Encryption, type EncryptionClientConfig } from "@cipherstash/stack"; import { protectedUsers } from "./schemas/users"; -const config: ProtectClientConfig = { - schemas: [protectedUsers], // At least one csTable is required +const config: EncryptionClientConfig = { + schemas: [protectedUsers], // At least one encryptedTable is required } -const protectClient = await protect(config); +const protectClient = await Encryption(config); ``` --- diff --git a/docs/reference/searchable-encryption-postgres.md b/docs/reference/searchable-encryption-postgres.md index 74ead6a4..c348b3e9 100644 --- a/docs/reference/searchable-encryption-postgres.md +++ b/docs/reference/searchable-encryption-postgres.md @@ -1,6 +1,6 @@ -# Searchable encryption with Protect.js and PostgreSQL +# Searchable encryption with Stash Encryption and PostgreSQL -This reference guide outlines the different query patterns you can use to search encrypted data with Protect.js. +This reference guide outlines the different query patterns you can use to search encrypted data with Stash Encryption. ## Table of contents @@ -22,11 +22,11 @@ This reference guide outlines the different query patterns you can use to search Before you can use searchable encryption with PostgreSQL, you need to: 1. Install the [EQL custom types and functions](https://github.com/cipherstash/encrypt-query-language?tab=readme-ov-file#installation) -2. Set up your Protect.js schema with the appropriate search capabilities +2. Set up your Stash Encryption schema with the appropriate search capabilities > [!WARNING] > The formal EQL repo documentation is heavily focused on the underlying custom function implementation. -> It also has a bias towards the [CipherStash Proxy](https://github.com/cipherstash/proxy) product, so this guide is the best place to get started when using Protect.js. +> It also has a bias towards the [CipherStash Proxy](https://github.com/cipherstash/proxy) product, so this guide is the best place to get started when using Stash Encryption. ## What is EQL? @@ -36,26 +36,26 @@ EQL (Encrypt Query Language) is a set of PostgreSQL extensions that enable searc - Functions for comparing and searching encrypted values - Support for range queries and sorting on encrypted data -When you install EQL, it adds these capabilities to your PostgreSQL database, allowing Protect.js to perform operations on encrypted data without decrypting it first. +When you install EQL, it adds these capabilities to your PostgreSQL database, allowing Stash Encryption to perform operations on encrypted data without decrypting it first. > [!IMPORTANT] > Any column that is encrypted with EQL must be of type `eql_v2_encrypted` which is included in the EQL extension. ## Setting up your schema -Define your Protect.js schema using `csTable` and `csColumn` to specify how each field should be encrypted and searched: +Define your Stash Encryption schema using `encryptedTable` and `encryptedColumn` to specify how each field should be encrypted and searched: ```typescript -import { protect, csTable, csColumn } from '@cipherstash/protect' +import { Encryption, encryptedTable, encryptedColumn } from '@cipherstash/stack' -const schema = csTable('users', { - email: csColumn('email_encrypted') +const schema = encryptedTable('users', { + email: encryptedColumn('email_encrypted') .equality() // Enables exact matching .freeTextSearch() // Enables text search .orderAndRange(), // Enables sorting and range queries - phone: csColumn('phone_encrypted') + phone: encryptedColumn('phone_encrypted') .equality(), // Only exact matching - age: csColumn('age_encrypted') + age: encryptedColumn('age_encrypted') .orderAndRange() // Only sorting and range queries }) ``` @@ -174,10 +174,10 @@ const result = await client.query( ```typescript import { Client } from 'pg' -import { protect, csTable, csColumn } from '@cipherstash/protect' +import { Encryption, encryptedTable, encryptedColumn } from '@cipherstash/stack' -const schema = csTable('users', { - email: csColumn('email_encrypted') +const schema = encryptedTable('users', { + email: encryptedColumn('email_encrypted') .equality() .freeTextSearch() .orderAndRange() @@ -187,7 +187,7 @@ const client = new Client({ // your connection details }) -const protectClient = await protect({ +const protectClient = await Encryption({ schemas: [schema] }) @@ -228,7 +228,7 @@ const decryptedData = await protectClient.bulkDecryptModels(result.rows) ### Using Supabase SDK -For Supabase users, we provide a specific implementation guide. [Read more about using Protect.js with Supabase](./supabase-sdk.md). +For Supabase users, we provide a specific implementation guide. [Read more about using Stash Encryption with Supabase](./supabase-sdk.md). ## Best practices @@ -253,7 +253,7 @@ For Supabase users, we provide a specific implementation guide. [Read more about - Cache frequently accessed data 4. **Error Handling** - - Always check for failures with any Protect.js method + - Always check for failures with any Stash Encryption method - Handle encryption errors aggressively - Handle decryption errors gracefully diff --git a/docs/reference/supabase-sdk.md b/docs/reference/supabase-sdk.md index 594c3122..da32fc8d 100644 --- a/docs/reference/supabase-sdk.md +++ b/docs/reference/supabase-sdk.md @@ -1,10 +1,10 @@ -# Using CipherStash Protect.js with Supabase SDK +# Using CipherStash Stash Encryption with Supabase SDK -You can encrypt data [in-use](../concepts/searchable-encryption.md) with Protect.js and store it in your Supabase project all while maintaining the ability to search the data without decryption. +You can encrypt data [in-use](../concepts/searchable-encryption.md) with Stash Encryption and store it in your Supabase project all while maintaining the ability to search the data without decryption. This reference guide will show you how to do this with the Supabase SDK. > [!NOTE] -> The following assumes you have installed the [latest version of the EQL v2 extension](https://github.com/cipherstash/encrypt-query-language/releases) which has a specific release for Supabase, and gone through the [Protect.js setup guide](https://github.com/cipherstash/protectjs). +> The following assumes you have installed the [latest version of the EQL v2 extension](https://github.com/cipherstash/encrypt-query-language/releases) which has a specific release for Supabase, and gone through the [Stash Encryption setup guide](https://github.com/cipherstash/protectjs). ## Defining your column types @@ -22,27 +22,27 @@ Under the hood, the EQL payload is a JSON object that is stored as a composite t ## Inserting data -You can insert encrypted data into the table using Protect.js and the Supabase SDK. Since the `eql_v2_encrypted` column is a composite type, you'll need to use the `encryptedToPgComposite` helper to properly format the data: +You can insert encrypted data into the table using Stash Encryption and the Supabase SDK. Since the `eql_v2_encrypted` column is a composite type, you'll need to use the `encryptedToPgComposite` helper to properly format the data: ```typescript -import { - protect, - csTable, - csColumn, - encryptedToPgComposite, - type ProtectClientConfig -} from '@cipherstash/protect' - -const users = csTable('users', { - name: csColumn('name').freeTextSearch().equality(), - email: csColumn('email').freeTextSearch().equality() +import { + Encryption, + encryptedTable, + encryptedColumn, + encryptedToPgComposite, + type EncryptionClientConfig +} from '@cipherstash/stack' + +const users = encryptedTable('users', { + name: encryptedColumn('name').freeTextSearch().equality(), + email: encryptedColumn('email').freeTextSearch().equality() }) -const config: ProtectClientConfig = { +const config: EncryptionClientConfig = { schemas: [users], } -const protectClient = await protect(config) +const protectClient = await Encryption(config) const encryptedResult = await protectClient.encryptModel( { @@ -71,7 +71,7 @@ const { data, error } = await supabase .select('id, email::jsonb, name::jsonb') ``` -Without the `::jsonb` cast, the encrypted payload would be wrapped in an object with a `data` key, which would require additional handling before decryption. The cast ensures you get the raw encrypted payload that can be directly used with Protect.js for decryption: +Without the `::jsonb` cast, the encrypted payload would be wrapped in an object with a `data` key, which would require additional handling before decryption. The cast ensures you get the raw encrypted payload that can be directly used with Stash Encryption for decryption: ```typescript const decryptedResult = await protectClient.decryptModel(data[0]) @@ -88,24 +88,24 @@ console.log('Decrypted user:', decryptedResult.data) When working with models that contain multiple encrypted fields, you can use the `modelToEncryptedPgComposites` helper to handle the conversion to PostgreSQL composite types: ```typescript -import { - protect, - csTable, - csColumn, - modelToEncryptedPgComposites, - type ProtectClientConfig -} from '@cipherstash/protect' - -const users = csTable('users', { - name: csColumn('name').freeTextSearch().equality(), - email: csColumn('email').freeTextSearch().equality() +import { + Encryption, + encryptedTable, + encryptedColumn, + modelToEncryptedPgComposites, + type EncryptionClientConfig +} from '@cipherstash/stack' + +const users = encryptedTable('users', { + name: encryptedColumn('name').freeTextSearch().equality(), + email: encryptedColumn('email').freeTextSearch().equality() }) -const config: ProtectClientConfig = { +const config: EncryptionClientConfig = { schemas: [users], } -const protectClient = await protect(config) +const protectClient = await Encryption(config) const model = { name: 'John Doe', diff --git a/examples/basic/README.md b/examples/basic/README.md index c25c6de6..8e22b72a 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -1,6 +1,6 @@ -# Basic example of using @cipherstash/protect +# Basic example of using @cipherstash/stack -This basic example demonstrates how to use the `@cipherstash/protect` package to encrypt arbitrary input. +This basic example demonstrates how to use the `@cipherstash/stack` package to encrypt arbitrary input. ## Installing the basic example @@ -43,7 +43,7 @@ Lastly, install the CipherStash CLI: > [!IMPORTANT] > Make sure you have [installed the CipherStash CLI](#installation) before following these steps. -Set up all the configuration and credentials required for Protect.js: +Set up all the configuration and credentials required for Stash Encryption: ```bash stash setup @@ -53,8 +53,8 @@ If you have not already signed up for a CipherStash account, this will prompt yo At the end of `stash setup`, you will have two files in your project: -- `cipherstash.toml` which contains the configuration for Protect.js -- `cipherstash.secret.toml` which contains the credentials for Protect.js +- `cipherstash.toml` which contains the configuration for Stash Encryption +- `cipherstash.secret.toml` which contains the credentials for Stash Encryption > [!WARNING] > Do not commit `cipherstash.secret.toml` to git, because it contains sensitive credentials. @@ -72,4 +72,4 @@ The application will log the plaintext to the console that has been encrypted us ## Next steps -Check out the [Protect.js + Next.js + Clerk example app](../nextjs-clerk) to see how to add end-user identity as an extra control when encrypting data. +Check out the [Stash Encryption + Next.js + Clerk example app](../nextjs-clerk) to see how to add end-user identity as an extra control when encrypting data. diff --git a/examples/basic/package.json b/examples/basic/package.json index 9a23f43c..bfd574f3 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -11,7 +11,7 @@ "license": "ISC", "description": "", "dependencies": { - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "dotenv": "^16.4.7" }, "devDependencies": { diff --git a/examples/basic/protect.ts b/examples/basic/protect.ts index 0feb8f63..17b18962 100644 --- a/examples/basic/protect.ts +++ b/examples/basic/protect.ts @@ -1,17 +1,17 @@ import 'dotenv/config' import { - type ProtectClientConfig, - csColumn, - csTable, - protect, -} from '@cipherstash/protect' + type EncryptionClientConfig, + encryptedColumn, + encryptedTable, + Encryption, +} from '@cipherstash/stack' -export const users = csTable('users', { - name: csColumn('name'), +export const users = encryptedTable('users', { + name: encryptedColumn('name'), }) -const config: ProtectClientConfig = { +const config: EncryptionClientConfig = { schemas: [users], } -export const protectClient = await protect(config) +export const protectClient = await Encryption(config) diff --git a/examples/drizzle/README.md b/examples/drizzle/README.md index 6138e78b..e6983cfd 100644 --- a/examples/drizzle/README.md +++ b/examples/drizzle/README.md @@ -1,6 +1,6 @@ -# Express REST API with Drizzle ORM and Protect.js +# Express REST API with Drizzle ORM and Stash Encryption -This example demonstrates a FinTech REST API built with Express.js, Drizzle ORM, and Protect.js. It showcases how to encrypt sensitive financial data (account numbers, amounts, transaction descriptions) while maintaining the ability to search and query encrypted fields. +This example demonstrates a FinTech REST API built with Express.js, Drizzle ORM, and Stash Encryption. It showcases how to encrypt sensitive financial data (account numbers, amounts, transaction descriptions) while maintaining the ability to search and query encrypted fields. ## Prerequisites @@ -12,7 +12,7 @@ This example demonstrates a FinTech REST API built with Express.js, Drizzle ORM, - [Express](https://expressjs.com/) - Web framework - [Drizzle ORM](https://orm.drizzle.team/) - TypeScript ORM -- [Protect.js](https://github.com/cipherstash/protectjs) - End-to-end encryption +- [Stash Encryption](https://github.com/cipherstash/protectjs) - End-to-end encryption - [PostgreSQL](https://www.postgresql.org/) - Database ## Setup @@ -291,7 +291,7 @@ The `transactions` table has the following structure: ### Encryption -- Sensitive fields (`accountNumber`, `amount`, `description`) are encrypted using Protect.js before being stored in the database +- Sensitive fields (`accountNumber`, `amount`, `description`) are encrypted using Stash Encryption before being stored in the database - The `@cipherstash/drizzle` package provides `encryptedType` helper to define encrypted columns in Drizzle schemas - Data is automatically encrypted when inserting/updating and decrypted when reading @@ -301,7 +301,7 @@ The `transactions` table has the following structure: - **Text search** on `accountNumber` and `description` using `ilike` operator - **Range queries** on `amount` using `gte` and `lte` operators - **Equality queries** on `accountNumber` and `amount` -- All encrypted field queries use Protect.js operators that automatically handle encryption +- All encrypted field queries use Stash Encryption operators that automatically handle encryption ### Type Safety @@ -310,8 +310,8 @@ The `transactions` table has the following structure: ## Notes -- **Native Module**: Protect.js uses `@cipherstash/protect-ffi`, a native Node-API module. Express doesn't bundle code, so no special configuration is needed. If deploying to serverless platforms, ensure the native module is properly externalized. -- **Error Handling**: All Protect.js operations return a Result type (`{ data }` or `{ failure }`). The API properly handles these results and returns appropriate HTTP status codes. +- **Native Module**: Stash Encryption uses `@cipherstash/protect-ffi`, a native Node-API module. Express doesn't bundle code, so no special configuration is needed. If deploying to serverless platforms, ensure the native module is properly externalized. +- **Error Handling**: All Stash Encryption operations return a Result type (`{ data }` or `{ failure }`). The API properly handles these results and returns appropriate HTTP status codes. - **Bulk Operations**: The API uses `bulkEncryptModels` and `bulkDecryptModels` for efficient batch operations when querying multiple transactions. ## License diff --git a/examples/drizzle/package.json b/examples/drizzle/package.json index 13bf1e46..63c39234 100644 --- a/examples/drizzle/package.json +++ b/examples/drizzle/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@cipherstash/drizzle": "workspace:*", - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "drizzle-orm": "^0.44.7", "express": "^5.2.1", "pg": "^8.16.3", diff --git a/examples/drizzle/src/protect/config.ts b/examples/drizzle/src/protect/config.ts index 05482ba0..1e3d8a12 100644 --- a/examples/drizzle/src/protect/config.ts +++ b/examples/drizzle/src/protect/config.ts @@ -3,14 +3,14 @@ import { createProtectOperators, extractProtectSchema, } from '@cipherstash/drizzle/pg' -import { protect } from '@cipherstash/protect' +import { Encryption } from '@cipherstash/stack' import { transactions } from '../db/schema' -// Extract Protect.js schema from Drizzle table +// Extract Stash Encryption schema from Drizzle table export const transactionsSchema = extractProtectSchema(transactions) -// Initialize Protect.js client -export const protectClient = await protect({ +// Initialize Stash Encryption client +export const protectClient = await Encryption({ schemas: [transactionsSchema], }) diff --git a/examples/dynamo/README.md b/examples/dynamo/README.md index 6b5f8d63..33cbc534 100644 --- a/examples/dynamo/README.md +++ b/examples/dynamo/README.md @@ -1,6 +1,6 @@ # DynamoDB Examples -Examples of using Protect.js with DynamoDB. +Examples of using Stash Encryption with DynamoDB. ## Prereqs - [Node.js](https://nodejs.org/en) (tested with v22.11.0) @@ -10,7 +10,7 @@ Examples of using Protect.js with DynamoDB. ## Setup -Install the workspace dependencies and build Protect.js: +Install the workspace dependencies and build Stash Encryption: ``` # change to the workspace root directory cd ../.. diff --git a/examples/dynamo/package.json b/examples/dynamo/package.json index 3d8830a9..b518007c 100644 --- a/examples/dynamo/package.json +++ b/examples/dynamo/package.json @@ -21,7 +21,7 @@ "@aws-sdk/client-dynamodb": "^3.817.0", "@aws-sdk/lib-dynamodb": "^3.817.0", "@aws-sdk/util-dynamodb": "^3.817.0", - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "@cipherstash/protect-dynamodb": "workspace:*", "pg": "^8.13.1" }, diff --git a/examples/dynamo/src/common/protect.ts b/examples/dynamo/src/common/protect.ts index 78441f07..6e5edefa 100644 --- a/examples/dynamo/src/common/protect.ts +++ b/examples/dynamo/src/common/protect.ts @@ -1,9 +1,9 @@ -import { csColumn, csTable, protect } from '@cipherstash/protect' +import { encryptedColumn, encryptedTable, Encryption } from '@cipherstash/stack' -export const users = csTable('users', { - email: csColumn('email').equality(), +export const users = encryptedTable('users', { + email: encryptedColumn('email').equality(), }) -export const protectClient = await protect({ +export const protectClient = await Encryption({ schemas: [users], }) diff --git a/examples/dynamo/src/encrypted-key-in-gsi.ts b/examples/dynamo/src/encrypted-key-in-gsi.ts index 6fa9e356..9d50d949 100644 --- a/examples/dynamo/src/encrypted-key-in-gsi.ts +++ b/examples/dynamo/src/encrypted-key-in-gsi.ts @@ -68,7 +68,12 @@ const main = async () => { // Use encryptQuery to create the search term for GSI query const encryptedResult = await protectClient.encryptQuery([ - { value: 'abc@example.com', column: users.email, table: users, queryType: 'equality' }, + { + value: 'abc@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, ]) if (encryptedResult.failure) { diff --git a/examples/dynamo/src/encrypted-partition-key.ts b/examples/dynamo/src/encrypted-partition-key.ts index a3fd6eb0..183f119f 100644 --- a/examples/dynamo/src/encrypted-partition-key.ts +++ b/examples/dynamo/src/encrypted-partition-key.ts @@ -51,7 +51,12 @@ const main = async () => { // Use encryptQuery to create the search term for partition key lookup const encryptedResult = await protectClient.encryptQuery([ - { value: 'abc@example.com', column: users.email, table: users, queryType: 'equality' }, + { + value: 'abc@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, ]) if (encryptedResult.failure) { diff --git a/examples/dynamo/src/encrypted-sort-key.ts b/examples/dynamo/src/encrypted-sort-key.ts index e8e08175..b868ea45 100644 --- a/examples/dynamo/src/encrypted-sort-key.ts +++ b/examples/dynamo/src/encrypted-sort-key.ts @@ -60,7 +60,12 @@ const main = async () => { // Use encryptQuery to create the search term for sort key range query const encryptedResult = await protectClient.encryptQuery([ - { value: 'abc@example.com', column: users.email, table: users, queryType: 'equality' }, + { + value: 'abc@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, ]) if (encryptedResult.failure) { diff --git a/examples/hono-supabase/README.md b/examples/hono-supabase/README.md index 0093c327..de7240df 100644 --- a/examples/hono-supabase/README.md +++ b/examples/hono-supabase/README.md @@ -1,6 +1,6 @@ # CipherStash JSEQL + Supabase + Hono Example -This project demonstrates how to encrypt data using [@cipherstash/protect](https://www.npmjs.com/package/@cipherstash/protect) before storing it in a [Supabase](https://supabase.com/) Postgres database. It uses [Hono](https://hono.dev/) to create a minimal RESTful API, showcasing how to seamlessly integrate field-level encryption into a typical web application workflow. +This project demonstrates how to encrypt data using [@cipherstash/stack](https://www.npmjs.com/package/@cipherstash/stack) before storing it in a [Supabase](https://supabase.com/) Postgres database. It uses [Hono](https://hono.dev/) to create a minimal RESTful API, showcasing how to seamlessly integrate field-level encryption into a typical web application workflow. ## Table of Contents - [Overview](#overview) @@ -22,7 +22,7 @@ This project demonstrates how to encrypt data using [@cipherstash/protect](https ## Overview **What does this example show?** -1. **Encrypting data** with [@cipherstash/protect](https://www.npmjs.com/package/@cipherstash/protect). +1. **Encrypting data** with [@cipherstash/stack](https://www.npmjs.com/package/@cipherstash/stack). 2. **Storing encrypted data** in a Postgres database (using Supabase). 3. **Retrieving and decrypting** that data in a minimal Hono-based REST API. @@ -157,6 +157,6 @@ Creates a new user with an **encrypted** email field. ## Additional Resources -- [@cipherstash/protect Documentation](https://github.com/cipherstash/protectjs) +- [@cipherstash/stack Documentation](https://github.com/cipherstash/protectjs) - [Hono Framework](https://hono.dev/) - [Supabase Documentation](https://supabase.com/docs) \ No newline at end of file diff --git a/examples/hono-supabase/package.json b/examples/hono-supabase/package.json index 741079b6..563b47fd 100644 --- a/examples/hono-supabase/package.json +++ b/examples/hono-supabase/package.json @@ -6,7 +6,7 @@ "dev": "tsx watch src/index.ts" }, "dependencies": { - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "@hono/node-server": "^1.13.7", "@supabase/supabase-js": "^2.47.10", "dotenv": "^16.4.7", diff --git a/examples/hono-supabase/src/index.ts b/examples/hono-supabase/src/index.ts index 8f5bef13..32f32c28 100644 --- a/examples/hono-supabase/src/index.ts +++ b/examples/hono-supabase/src/index.ts @@ -5,21 +5,21 @@ import { Hono } from 'hono' // Consolidated protect and it's schemas into a single file import { - type ProtectClientConfig, - csColumn, - csTable, - protect, -} from '@cipherstash/protect' - -export const users = csTable('users', { - email: csColumn('email'), + type EncryptionClientConfig, + encryptedColumn, + encryptedTable, + Encryption, +} from '@cipherstash/stack' + +export const users = encryptedTable('users', { + email: encryptedColumn('email'), }) -const config: ProtectClientConfig = { +const config: EncryptionClientConfig = { schemas: [users], } -export const protectClient = await protect(config) +export const protectClient = await Encryption(config) // Create a single supabase client for interacting with the database const supabaseUrl = process.env.SUPABASE_URL diff --git a/examples/nest/README.md b/examples/nest/README.md index 7235ce70..2f54cc35 100644 --- a/examples/nest/README.md +++ b/examples/nest/README.md @@ -1,4 +1,4 @@ -# Protect.js Example with NestJS +# Stash Encryption Example with NestJS > ⚠️ **Heads-up:** This example was generated with AI with some very specific prompting to make it as useful as possible for you :) > If you find any issues, think this example is absolutely terrible, or would like to speak with a human, book a call with the [CipherStash solutions engineering team](https://calendly.com/cipherstash-gtm/cipherstash-discovery-call?month=2025-09) @@ -43,7 +43,7 @@ CS_CLIENT_ACCESS_KEY= - If you integrate bundlers, externalize `@cipherstash/protect-ffi` (native module). ### References -- Protect.js: see repo root `README.md` +- Stash Encryption: see repo root `README.md` - NestJS docs: `https://docs.nestjs.com/` - Next.js external packages: `docs/how-to/nextjs-external-packages.md` - SST external packages: `docs/how-to/sst-external-packages.md` diff --git a/examples/nest/package.json b/examples/nest/package.json index bdb34f76..7727e77f 100644 --- a/examples/nest/package.json +++ b/examples/nest/package.json @@ -20,7 +20,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "@nestjs/common": "^11.0.1", "@nestjs/config": "^3.2.0", "@nestjs/core": "^11.0.1", diff --git a/examples/nest/src/app.controller.spec.ts b/examples/nest/src/app.controller.spec.ts index b61e370f..b2deea6d 100644 --- a/examples/nest/src/app.controller.spec.ts +++ b/examples/nest/src/app.controller.spec.ts @@ -1,4 +1,4 @@ -import type { Decrypted, EncryptedPayload } from '@cipherstash/protect' +import type { Decrypted, EncryptedPayload } from '@cipherstash/stack' import { Test, type TestingModule } from '@nestjs/testing' import { AppController } from './app.controller' import { AppService, type CreateUserDto, type User } from './app.service' diff --git a/examples/nest/src/app.service.ts b/examples/nest/src/app.service.ts index e67dad28..7e4ddc4b 100644 --- a/examples/nest/src/app.service.ts +++ b/examples/nest/src/app.service.ts @@ -1,4 +1,4 @@ -import type { Decrypted, EncryptedPayload } from '@cipherstash/protect' +import type { Decrypted, EncryptedPayload } from '@cipherstash/stack' import { Injectable } from '@nestjs/common' import type { ProtectService } from './protect' import { users } from './protect' diff --git a/examples/nest/src/protect/decorators/decrypt.decorator.ts b/examples/nest/src/protect/decorators/decrypt.decorator.ts index c6572fe5..bb79589f 100644 --- a/examples/nest/src/protect/decorators/decrypt.decorator.ts +++ b/examples/nest/src/protect/decorators/decrypt.decorator.ts @@ -3,14 +3,14 @@ import { getProtectService } from '../utils/get-protect-service.util' import type { ProtectColumn, - ProtectTable, - ProtectTableColumn, - ProtectValue, -} from '@cipherstash/protect' + EncryptedTable, + EncryptedTableColumn, + EncryptedValue, +} from '@cipherstash/stack' export interface DecryptOptions { - table: ProtectTable - column: ProtectColumn | ProtectValue + table: EncryptedTable + column: ProtectColumn | EncryptedValue lockContext?: unknown // JWT or LockContext } diff --git a/examples/nest/src/protect/decorators/encrypt.decorator.ts b/examples/nest/src/protect/decorators/encrypt.decorator.ts index 9abdb352..ca23d626 100644 --- a/examples/nest/src/protect/decorators/encrypt.decorator.ts +++ b/examples/nest/src/protect/decorators/encrypt.decorator.ts @@ -5,14 +5,14 @@ import { getProtectService } from '../utils/get-protect-service.util' import type { ProtectColumn, - ProtectTable, - ProtectTableColumn, - ProtectValue, -} from '@cipherstash/protect' + EncryptedTable, + EncryptedTableColumn, + EncryptedValue, +} from '@cipherstash/stack' export interface EncryptOptions { - table: ProtectTable - column: ProtectColumn | ProtectValue + table: EncryptedTable + column: ProtectColumn | EncryptedValue lockContext?: unknown // JWT or LockContext } diff --git a/examples/nest/src/protect/interceptors/decrypt.interceptor.ts b/examples/nest/src/protect/interceptors/decrypt.interceptor.ts index 1078c3c8..30c8ddb3 100644 --- a/examples/nest/src/protect/interceptors/decrypt.interceptor.ts +++ b/examples/nest/src/protect/interceptors/decrypt.interceptor.ts @@ -11,15 +11,15 @@ import { getProtectService } from '../utils/get-protect-service.util' import type { ProtectColumn, - ProtectTable, - ProtectTableColumn, - ProtectValue, -} from '@cipherstash/protect' + EncryptedTable, + EncryptedTableColumn, + EncryptedValue, +} from '@cipherstash/stack' export interface DecryptInterceptorOptions { fields?: string[] - table: ProtectTable - column: ProtectColumn | ProtectValue + table: EncryptedTable + column: ProtectColumn | EncryptedValue lockContext?: unknown } diff --git a/examples/nest/src/protect/interceptors/encrypt.interceptor.ts b/examples/nest/src/protect/interceptors/encrypt.interceptor.ts index d1a2fa6c..fa5dd44a 100644 --- a/examples/nest/src/protect/interceptors/encrypt.interceptor.ts +++ b/examples/nest/src/protect/interceptors/encrypt.interceptor.ts @@ -11,15 +11,15 @@ import { getProtectService } from '../utils/get-protect-service.util' import type { ProtectColumn, - ProtectTable, - ProtectTableColumn, - ProtectValue, -} from '@cipherstash/protect' + EncryptedTable, + EncryptedTableColumn, + EncryptedValue, +} from '@cipherstash/stack' export interface EncryptInterceptorOptions { fields?: string[] - table: ProtectTable - column: ProtectColumn | ProtectValue + table: EncryptedTable + column: ProtectColumn | EncryptedValue lockContext?: unknown } diff --git a/examples/nest/src/protect/interfaces/protect-config.interface.ts b/examples/nest/src/protect/interfaces/protect-config.interface.ts index 2a7ef944..0fc2494c 100644 --- a/examples/nest/src/protect/interfaces/protect-config.interface.ts +++ b/examples/nest/src/protect/interfaces/protect-config.interface.ts @@ -1,4 +1,4 @@ -import type { ProtectTable, ProtectTableColumn } from '@cipherstash/protect' +import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/stack' export interface ProtectConfig { workspaceCrn: string @@ -6,5 +6,5 @@ export interface ProtectConfig { clientKey: string clientAccessKey: string logLevel?: 'debug' | 'info' | 'error' - schemas?: ProtectTable[] + schemas?: EncryptedTable[] } diff --git a/examples/nest/src/protect/protect.module.ts b/examples/nest/src/protect/protect.module.ts index 6aca610f..5722197f 100644 --- a/examples/nest/src/protect/protect.module.ts +++ b/examples/nest/src/protect/protect.module.ts @@ -1,10 +1,10 @@ import { - type ProtectClient, - type ProtectClientConfig, - type ProtectTable, - type ProtectTableColumn, - protect, -} from '@cipherstash/protect' + type EncryptionClient, + type EncryptionClientConfig, + type EncryptedTable, + type EncryptedTableColumn, + Encryption, +} from '@cipherstash/stack' import { type DynamicModule, Global, Module } from '@nestjs/common' import { ConfigModule, ConfigService } from '@nestjs/config' import type { ProtectConfig } from './interfaces/protect-config.interface' @@ -63,17 +63,17 @@ export class ProtectModule { }, { provide: PROTECT_CLIENT, - useFactory: async (config: ProtectConfig): Promise => { - const protectConfig: ProtectClientConfig = { + useFactory: async (config: ProtectConfig): Promise => { + const protectConfig: EncryptionClientConfig = { schemas: (config.schemas && config.schemas.length > 0 ? config.schemas : [users]) as [ - ProtectTable, - ...ProtectTable[], + EncryptedTable, + ...EncryptedTable[], ], } - return await protect(protectConfig) + return await Encryption(protectConfig) }, inject: [PROTECT_CONFIG], }, @@ -98,17 +98,17 @@ export class ProtectModule { }, { provide: PROTECT_CLIENT, - useFactory: async (config: ProtectConfig): Promise => { - const protectConfig: ProtectClientConfig = { + useFactory: async (config: ProtectConfig): Promise => { + const protectConfig: EncryptionClientConfig = { schemas: (config.schemas && config.schemas.length > 0 ? config.schemas : [users]) as [ - ProtectTable, - ...ProtectTable[], + EncryptedTable, + ...EncryptedTable[], ], } - return await protect(protectConfig) + return await Encryption(protectConfig) }, inject: [PROTECT_CONFIG], }, diff --git a/examples/nest/src/protect/protect.service.spec.ts b/examples/nest/src/protect/protect.service.spec.ts index fb4a4d96..be8bcd84 100644 --- a/examples/nest/src/protect/protect.service.spec.ts +++ b/examples/nest/src/protect/protect.service.spec.ts @@ -1,4 +1,4 @@ -import type { EncryptedPayload, ProtectClient } from '@cipherstash/protect' +import type { EncryptedPayload, EncryptionClient } from '@cipherstash/stack' import { Test, type TestingModule } from '@nestjs/testing' import { PROTECT_CLIENT } from './protect.constants' import { ProtectService } from './protect.service' @@ -6,7 +6,7 @@ import { users } from './schema' describe('ProtectService', () => { let service: ProtectService - let mockClient: jest.Mocked + let mockClient: jest.Mocked const mockEncryptedPayload: EncryptedPayload = { c: 'mock-encrypted-data', @@ -23,7 +23,7 @@ describe('ProtectService', () => { bulkDecrypt: jest.fn(), bulkEncryptModels: jest.fn(), bulkDecryptModels: jest.fn(), - } as jest.Mocked + } as jest.Mocked const module: TestingModule = await Test.createTestingModule({ providers: [ diff --git a/examples/nest/src/protect/protect.service.ts b/examples/nest/src/protect/protect.service.ts index be3a8a12..ce900602 100644 --- a/examples/nest/src/protect/protect.service.ts +++ b/examples/nest/src/protect/protect.service.ts @@ -3,10 +3,10 @@ import type { EncryptOptions, EncryptedPayload, LockContext, - ProtectClient, - ProtectTable, - ProtectTableColumn, -} from '@cipherstash/protect' + EncryptionClient, + EncryptedTable, + EncryptedTableColumn, +} from '@cipherstash/stack' import { Inject, Injectable } from '@nestjs/common' import { PROTECT_CLIENT } from './protect.constants' @@ -14,7 +14,7 @@ import { PROTECT_CLIENT } from './protect.constants' export class ProtectService { constructor( @Inject(PROTECT_CLIENT) - private readonly client: ProtectClient, + private readonly client: EncryptionClient, ) {} async encrypt(plaintext: string, options: EncryptOptions) { @@ -27,7 +27,7 @@ export class ProtectService { async encryptModel>( model: Decrypted, - table: ProtectTable, + table: EncryptedTable, ) { return this.client.encryptModel(model, table) } @@ -51,7 +51,7 @@ export class ProtectService { async bulkEncryptModels>( models: Decrypted[], - table: ProtectTable, + table: EncryptedTable, ) { return this.client.bulkEncryptModels(models, table) } @@ -78,7 +78,7 @@ export class ProtectService { async encryptModelWithLockContext>( model: Decrypted, - table: ProtectTable, + table: EncryptedTable, lockContext: LockContext, ) { return this.client diff --git a/examples/nest/src/protect/schema.ts b/examples/nest/src/protect/schema.ts index bf104f79..cce7ba9e 100644 --- a/examples/nest/src/protect/schema.ts +++ b/examples/nest/src/protect/schema.ts @@ -1,17 +1,17 @@ -import { csColumn, csTable } from '@cipherstash/protect' +import { encryptedColumn, encryptedTable } from '@cipherstash/stack' -export const users = csTable('users', { - email_encrypted: csColumn('email_encrypted') +export const users = encryptedTable('users', { + email_encrypted: encryptedColumn('email_encrypted') .equality() .orderAndRange() .freeTextSearch(), - phone_encrypted: csColumn('phone_encrypted').equality().orderAndRange(), - ssn_encrypted: csColumn('ssn_encrypted').equality(), + phone_encrypted: encryptedColumn('phone_encrypted').equality().orderAndRange(), + ssn_encrypted: encryptedColumn('ssn_encrypted').equality(), }) -export const orders = csTable('orders', { - address_encrypted: csColumn('address_encrypted').freeTextSearch(), - creditCard_encrypted: csColumn('creditCard_encrypted').equality(), +export const orders = encryptedTable('orders', { + address_encrypted: encryptedColumn('address_encrypted').freeTextSearch(), + creditCard_encrypted: encryptedColumn('creditCard_encrypted').equality(), }) // Export all schemas for easy import diff --git a/examples/nest/test/app.e2e-spec.ts b/examples/nest/test/app.e2e-spec.ts index 948e3e0d..5a7a65c6 100644 --- a/examples/nest/test/app.e2e-spec.ts +++ b/examples/nest/test/app.e2e-spec.ts @@ -1,4 +1,4 @@ -import type { EncryptedPayload } from '@cipherstash/protect' +import type { EncryptedPayload } from '@cipherstash/stack' import type { INestApplication } from '@nestjs/common' import { Test, type TestingModule } from '@nestjs/testing' import request from 'supertest' diff --git a/examples/next-drizzle-mysql/README.md b/examples/next-drizzle-mysql/README.md index d11fe529..acaca4e1 100644 --- a/examples/next-drizzle-mysql/README.md +++ b/examples/next-drizzle-mysql/README.md @@ -1,16 +1,16 @@ -# Next.js + Drizzle ORM + MySQL + Protect.js Example +# Next.js + Drizzle ORM + MySQL + Stash Encryption Example This example demonstrates how to build a modern web application using: - [Next.js](https://nextjs.org/) - React framework for production - [Drizzle ORM](https://orm.drizzle.team/) - TypeScript ORM for SQL databases - [MySQL](https://www.mysql.com/) - Popular open-source relational database -- [Protect.js](https://cipherstash.com/protect) - Data protection and encryption library +- [Stash Encryption](https://cipherstash.com/protect) - Data protection and encryption library ## Features - Full-stack TypeScript application - Database migrations and schema management with Drizzle -- Data protection and encryption with Protect.js +- Data protection and encryption with Stash Encryption - Modern UI with Tailwind CSS - Form handling with React Hook Form and Zod validation - Docker-based MySQL database setup @@ -33,7 +33,7 @@ This example demonstrates how to build a modern web application using: ```bash cp .env.example .env.local ``` - Then update the environment variables in `.env.local` with your Protect.js configuration values. + Then update the environment variables in `.env.local` with your Stash Encryption configuration values. 3. Start the MySQL database using Docker: ```bash @@ -73,5 +73,5 @@ The application will be available at `http://localhost:3000`. - [Next.js Documentation](https://nextjs.org/docs) - [Drizzle ORM Documentation](https://orm.drizzle.team/docs/overview) -- [Protect.js Documentation](https://cipherstash.com/protect/docs) +- [Stash Encryption Documentation](https://cipherstash.com/protect/docs) - [MySQL Documentation](https://dev.mysql.com/doc/) diff --git a/examples/next-drizzle-mysql/next.config.ts b/examples/next-drizzle-mysql/next.config.ts index 0786faa6..c2c60b7f 100644 --- a/examples/next-drizzle-mysql/next.config.ts +++ b/examples/next-drizzle-mysql/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from 'next' const nextConfig: NextConfig = { - serverExternalPackages: ['@cipherstash/protect', 'mysql2'], + serverExternalPackages: ['@cipherstash/stack', 'mysql2'], } export default nextConfig diff --git a/examples/next-drizzle-mysql/package.json b/examples/next-drizzle-mysql/package.json index a64f84c5..3b624020 100644 --- a/examples/next-drizzle-mysql/package.json +++ b/examples/next-drizzle-mysql/package.json @@ -10,7 +10,7 @@ "db:migrate": "drizzle-kit migrate" }, "dependencies": { - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "@hookform/resolvers": "^5.0.1", "drizzle-orm": "^0.44.0", "mysql2": "^3.14.1", diff --git a/examples/next-drizzle-mysql/src/protect/index.ts b/examples/next-drizzle-mysql/src/protect/index.ts index 339a8cb6..2864f7ce 100644 --- a/examples/next-drizzle-mysql/src/protect/index.ts +++ b/examples/next-drizzle-mysql/src/protect/index.ts @@ -1,8 +1,8 @@ -import { type ProtectClientConfig, protect } from '@cipherstash/protect' +import { type EncryptionClientConfig, Encryption } from '@cipherstash/stack' import { users } from './schema' -const config: ProtectClientConfig = { +const config: EncryptionClientConfig = { schemas: [users], } -export const protectClient = await protect(config) +export const protectClient = await Encryption(config) diff --git a/examples/next-drizzle-mysql/src/protect/schema.ts b/examples/next-drizzle-mysql/src/protect/schema.ts index a9591051..8b62b46d 100644 --- a/examples/next-drizzle-mysql/src/protect/schema.ts +++ b/examples/next-drizzle-mysql/src/protect/schema.ts @@ -1,6 +1,6 @@ -import { csColumn, csTable } from '@cipherstash/protect' +import { encryptedColumn, encryptedTable } from '@cipherstash/stack' -export const users = csTable('users', { - email: csColumn('email'), - name: csColumn('name'), +export const users = encryptedTable('users', { + email: encryptedColumn('email'), + name: encryptedColumn('name'), }) diff --git a/examples/nextjs-clerk/.env.example b/examples/nextjs-clerk/.env.example index dbb3cfa2..fa9da01b 100644 --- a/examples/nextjs-clerk/.env.example +++ b/examples/nextjs-clerk/.env.example @@ -5,7 +5,7 @@ CLERK_SECRET_KEY=your_clerk_secret_key # Postres - Try out Supabase for free https://supabase.com/ POSTGRES_URL=your_postgres_url -# CipherStash Protect.js +# CipherStash Stash Encryption CS_WORKSPACE_ID=your_workspace_id CS_CLIENT_ID=your_client_id CS_CLIENT_KEY=your_client_secret diff --git a/examples/nextjs-clerk/README.md b/examples/nextjs-clerk/README.md index 273c900b..99b78736 100644 --- a/examples/nextjs-clerk/README.md +++ b/examples/nextjs-clerk/README.md @@ -1,6 +1,6 @@ -# Protect.js + Next.js + Clerk example +# Stash Encryption + Next.js + Clerk example -This example demonstrates how to use Protect.js with Next.js. It also demonstrates how to use Lock Contexts to ensure that only the intended users can access sensitive data, by using Clerk for authentication. +This example demonstrates how to use Stash Encryption with Next.js. It also demonstrates how to use Lock Contexts to ensure that only the intended users can access sensitive data, by using Clerk for authentication. This project uses the following technologies: @@ -49,7 +49,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the The database is hosted on Supabase and has the following schema which is defined using the Drizzle ORM: ```ts -// Data that is encrypted using protect.js is stored as jsonb in postgres +// Data that is encrypted using Stash Encryption is stored as jsonb in postgres export const users = pgTable("users", { id: serial("id").primaryKey(), @@ -65,20 +65,20 @@ export const users = pgTable("users", { > The EQL library ships with custom types that are used to define encrypted fields. > See the [EQL documentation](https://github.com/cipherstash/encrypted-query-language) for more information. -## @cipherstash/protect +## @cipherstash/stack -All the email data is encrypted using Protect.js. +All the email data is encrypted using Stash Encryption. The cipherstext is stored in the `email` column of the `users` table. The application is configured to only decrypt the data when the user is signed in, otherwise it will display the encrypted data. ### Npm package -`@cipherstash/protect` uses custom Rust bindings to the CipherStash Client in order to perform encryptions and decryptions. +`@cipherstash/stack` uses custom Rust bindings to the CipherStash Client in order to perform encryptions and decryptions. We leverage the [Neon project](https://neon-rs.dev/) to provide a JavaScript API for these bindings. ### Encryption -When a user is added to the database, the email address is encrypted using Protect.js. +When a user is added to the database, the email address is encrypted using Stash Encryption. To view the encryption implementation, see the `addUser` function in [src/lib/actions.ts](src/lib/actions.ts). ### Decryption @@ -87,7 +87,7 @@ To view the decrpytion implementation, see the `getUsers` function in [src/app/p ### Next.js -Since `@cipherstash/protect` is a native Node.js module, you need to opt-out from the Server Components bundling and use native Node.js `require` instead. +Since `@cipherstash/stack` is a native Node.js module, you need to opt-out from the Server Components bundling and use native Node.js `require` instead. #### Using version 15 or later @@ -96,7 +96,7 @@ Since `@cipherstash/protect` is a native Node.js module, you need to opt-out fro ```js const nextConfig = { ... - serverExternalPackages: ['@cipherstash/protect'], + serverExternalPackages: ['@cipherstash/stack'], } ``` @@ -108,7 +108,7 @@ const nextConfig = { const nextConfig = { ... experimental: { - serverComponentsExternalPackages: ['@cipherstash/protect'], + serverComponentsExternalPackages: ['@cipherstash/stack'], }, } ``` @@ -118,4 +118,4 @@ const nextConfig = { `serverExternalPackages` does not work with workspace packages and the issues is being tracked [here](https://github.com/vercel/next.js/issues/43433). Once this is fixed upstream, this application can use the workspace package for development. -For the time being, it used `@cipherstash/protect` from the npm registry. \ No newline at end of file +For the time being, it used `@cipherstash/stack` from the npm registry. \ No newline at end of file diff --git a/examples/nextjs-clerk/next.config.ts b/examples/nextjs-clerk/next.config.ts index 69f6bd42..087316a4 100644 --- a/examples/nextjs-clerk/next.config.ts +++ b/examples/nextjs-clerk/next.config.ts @@ -13,7 +13,7 @@ const nextConfig: NextConfig = { // https://github.com/vercel/next.js/issues/43433 // --- // TODO: Once this is fixed upstream, we can use the workspace packages - serverExternalPackages: ['@cipherstash/protect'], + serverExternalPackages: ['@cipherstash/stack'], } export default nextConfig diff --git a/examples/nextjs-clerk/package.json b/examples/nextjs-clerk/package.json index 502ca3b2..c50cdee0 100644 --- a/examples/nextjs-clerk/package.json +++ b/examples/nextjs-clerk/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@cipherstash/nextjs": "workspace:*", - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "@clerk/nextjs": "catalog:security", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-select": "^2.1.4", diff --git a/examples/nextjs-clerk/src/app/layout.tsx b/examples/nextjs-clerk/src/app/layout.tsx index b835533a..584628e0 100644 --- a/examples/nextjs-clerk/src/app/layout.tsx +++ b/examples/nextjs-clerk/src/app/layout.tsx @@ -4,8 +4,8 @@ import type { Metadata } from 'next' import './globals.css' export const metadata: Metadata = { - title: 'Protect.js + Next.js + Clerk', - description: 'An example of using Protect.js with Next.js and Clerk', + title: 'Stash Encryption + Next.js + Clerk', + description: 'An example of using Stash Encryption with Next.js and Clerk', } export default function Layout({ children }: { children: React.ReactNode }) { diff --git a/examples/nextjs-clerk/src/app/page.tsx b/examples/nextjs-clerk/src/app/page.tsx index 0008947c..e883f685 100644 --- a/examples/nextjs-clerk/src/app/page.tsx +++ b/examples/nextjs-clerk/src/app/page.tsx @@ -2,7 +2,7 @@ import { db } from '@/core/db' import { users } from '@/core/db/schema' import { getLockContext, protectClient } from '@/core/protect' import { getCtsToken } from '@cipherstash/nextjs' -import type { EncryptedData } from '@cipherstash/protect' +import type { EncryptedData } from '@cipherstash/stack' import { auth, currentUser } from '@clerk/nextjs/server' import Header from '../components/Header' import UserTable from '../components/UserTable' diff --git a/examples/nextjs-clerk/src/components/Header.tsx b/examples/nextjs-clerk/src/components/Header.tsx index 3163b87a..fb2a0e82 100644 --- a/examples/nextjs-clerk/src/components/Header.tsx +++ b/examples/nextjs-clerk/src/components/Header.tsx @@ -27,7 +27,7 @@ export default function Header() { / -

protect.js

+

stash encryption

/ diff --git a/examples/nextjs-clerk/src/core/protect/index.ts b/examples/nextjs-clerk/src/core/protect/index.ts index 05303ea9..ecd2040a 100644 --- a/examples/nextjs-clerk/src/core/protect/index.ts +++ b/examples/nextjs-clerk/src/core/protect/index.ts @@ -2,21 +2,21 @@ import 'dotenv/config' import { type CtsToken, LockContext, - type ProtectClientConfig, - csColumn, - csTable, - protect, -} from '@cipherstash/protect' + type EncryptionClientConfig, + encryptedColumn, + encryptedTable, + Encryption, +} from '@cipherstash/stack' -export const users = csTable('users', { - email: csColumn('email'), +export const users = encryptedTable('users', { + email: encryptedColumn('email'), }) -const config: ProtectClientConfig = { +const config: EncryptionClientConfig = { schemas: [users], } -export const protectClient = await protect(config) +export const protectClient = await Encryption(config) export const getLockContext = (cts_token?: CtsToken) => { if (!cts_token) { diff --git a/examples/typeorm/README.md b/examples/typeorm/README.md index efa6731f..dd905a3c 100644 --- a/examples/typeorm/README.md +++ b/examples/typeorm/README.md @@ -1,4 +1,4 @@ -# Protect.js Example with TypeORM +# Stash Encryption Example with TypeORM > ⚠️ **Heads-up:** This example was generated with AI with some very specific prompting to make it as useful as possible for you :) > If you find any issues, think this example is absolutely terrible, or would like to speak with a human, book a call with the [CipherStash solutions engineering team](https://calendly.com/cipherstash-gtm/cipherstash-discovery-call?month=2025-09) @@ -33,7 +33,7 @@ DB_DATABASE=cipherstash ```typescript // src/entity/User.ts import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm' -import type { EncryptedData } from '@cipherstash/protect' +import type { EncryptedData } from '@cipherstash/stack' import { EncryptedColumn } from '../decorators/encrypted-column' @Entity() @@ -68,19 +68,19 @@ export class User { } ``` -### 4. Configure Protect.js +### 4. Configure Stash Encryption ```typescript // src/protect.ts -import { protect, csTable, csColumn } from '@cipherstash/protect' +import { Encryption, encryptedTable, encryptedColumn } from '@cipherstash/stack' -export const protectedUser = csTable('user', { - email: csColumn('email').equality().orderAndRange(), - ssn: csColumn('ssn').equality(), - phone: csColumn('phone').equality(), +export const protectedUser = encryptedTable('user', { + email: encryptedColumn('email').equality().orderAndRange(), + ssn: encryptedColumn('ssn').equality(), + phone: encryptedColumn('phone').equality(), }) -export const protectClient = await protect({ +export const protectClient = await Encryption({ schemas: [protectedUser], }) ``` @@ -298,7 +298,7 @@ if (result.failure) { ### 3. Environment Configuration ```typescript // Use environment variables for all sensitive data -export const protectClient = await protect({ +export const protectClient = await Encryption({ schemas: [protectedUser], }) ``` @@ -358,7 +358,7 @@ The demo will show: ## 🔗 Next Steps - **Explore the demo**: Run `npm start` to see all features in action -- **Read the docs**: Check out [Protect.js documentation](https://github.com/cipherstash/protectjs/tree/main/docs) +- **Read the docs**: Check out [Stash Encryption documentation](https://github.com/cipherstash/protectjs/tree/main/docs) - **Learn concepts**: Understand [searchable encryption](https://github.com/cipherstash/protectjs/blob/main/docs/concepts/searchable-encryption.md) - **See other examples**: Browse the [examples directory](https://github.com/cipherstash/protectjs/tree/main/examples) @@ -380,7 +380,7 @@ The demo will show: ### Getting Help -- 📚 [Protect.js Documentation](https://github.com/cipherstash/protectjs/tree/main/docs) +- 📚 [Stash Encryption Documentation](https://github.com/cipherstash/protectjs/tree/main/docs) - 🐛 [GitHub Issues](https://github.com/cipherstash/protectjs/issues) - 💬 [Community Support](https://cipherstash.com) diff --git a/examples/typeorm/package.json b/examples/typeorm/package.json index bdfbb63c..d0f98e65 100644 --- a/examples/typeorm/package.json +++ b/examples/typeorm/package.json @@ -2,7 +2,7 @@ "name": "@cipherstash/typeorm-example", "version": "0.1.9", "private": true, - "description": "Protect.js with TypeORM example", + "description": "Stash Encryption with TypeORM example", "type": "commonjs", "devDependencies": { "@types/node": "^22.13.10", @@ -11,7 +11,7 @@ "typescript": "^5.8.2" }, "dependencies": { - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "dotenv": "^16.4.7", "pg": "^8.14.1", "reflect-metadata": "^0.2.2", diff --git a/examples/typeorm/src/data-source.ts b/examples/typeorm/src/data-source.ts index 7e315bd0..df7412bd 100644 --- a/examples/typeorm/src/data-source.ts +++ b/examples/typeorm/src/data-source.ts @@ -3,7 +3,7 @@ import { User } from './entity/User' const originalConnectionConnectFunction = DataSource.prototype.initialize -// Patch DataSource to support custom column type for Protect.js +// Patch DataSource to support custom column type for Stash Encryption DataSource.prototype.initialize = async function (...params) { // TypeORM's supportedDataTypes is typed as ColumnType[], but we need to add our custom type. // Use 'as any' to bypass the type error for custom types. diff --git a/examples/typeorm/src/entity/User.ts b/examples/typeorm/src/entity/User.ts index 24ecceda..040725a5 100644 --- a/examples/typeorm/src/entity/User.ts +++ b/examples/typeorm/src/entity/User.ts @@ -1,4 +1,4 @@ -import type { EncryptedData } from '@cipherstash/protect' +import type { EncryptedData } from '@cipherstash/stack' import { Column, CreateDateColumn, diff --git a/examples/typeorm/src/helpers/protect-entity.ts b/examples/typeorm/src/helpers/protect-entity.ts index 4880998c..eca0c805 100644 --- a/examples/typeorm/src/helpers/protect-entity.ts +++ b/examples/typeorm/src/helpers/protect-entity.ts @@ -1,7 +1,7 @@ import { - type ProtectClient, + type EncryptionClientConfig, encryptedToPgComposite, -} from '@cipherstash/protect' +} from '@cipherstash/stack' import type { EntityTarget } from 'typeorm' import { AppDataSource } from '../data-source' @@ -9,7 +9,7 @@ import { AppDataSource } from '../data-source' * Helper functions for working with encrypted entities in TypeORM */ export class ProtectEntityHelper { - constructor(private protectClient: ProtectClient) {} + constructor(private protectClient: EncryptionClientConfig) {} /** * Bulk encrypt and save entities to the database @@ -33,7 +33,7 @@ export class ProtectEntityHelper { entityClass: EntityTarget, // biome-ignore lint/suspicious/noExplicitAny: Required for dynamic entity types entities: Array>, - // biome-ignore lint/suspicious/noExplicitAny: Required for Protect.js schema types + // biome-ignore lint/suspicious/noExplicitAny: Required for Stash Encryption schema types encryptFields: Record, ): Promise { // First, prepare all entities for encryption @@ -108,7 +108,7 @@ export class ProtectEntityHelper { // biome-ignore lint/suspicious/noExplicitAny: Required for dynamic entity types async bulkDecrypt>( entities: T[], - // biome-ignore lint/suspicious/noExplicitAny: Required for Protect.js schema types + // biome-ignore lint/suspicious/noExplicitAny: Required for Stash Encryption schema types decryptFields: Record, ): Promise { // Prepare encrypted data for bulk decryption @@ -199,7 +199,7 @@ export class ProtectEntityHelper { entityClass: EntityTarget, fieldName: string, searchValue: string, - // biome-ignore lint/suspicious/noExplicitAny: Required for Protect.js schema types + // biome-ignore lint/suspicious/noExplicitAny: Required for Stash Encryption schema types fieldConfig: { table: any; column: any }, ): Promise { // Use encryptQuery instead of deprecated createSearchTerms diff --git a/examples/typeorm/src/index.ts b/examples/typeorm/src/index.ts index 24c0da7c..34a31d51 100644 --- a/examples/typeorm/src/index.ts +++ b/examples/typeorm/src/index.ts @@ -11,14 +11,14 @@ async function main() { await AppDataSource.initialize() console.log('✅ Database connection established') - // Initialize the Protect client + // Initialize the Encryption client const protectClient = await initializeProtectClient() - console.log('✅ Protect client initialized') + console.log('✅ Encryption client initialized') // Initialize the helper for streamlined operations const helper = new ProtectEntityHelper(protectClient) - console.log('\n🔐 Protect.js TypeORM Integration Demo') + console.log('\n🔐 Stash Encryption TypeORM Integration Demo') console.log('=====================================') // Example 1: Single user encryption and saving diff --git a/examples/typeorm/src/protect.ts b/examples/typeorm/src/protect.ts index 491067f0..af979019 100644 --- a/examples/typeorm/src/protect.ts +++ b/examples/typeorm/src/protect.ts @@ -1,24 +1,24 @@ -import { csColumn, csTable, protect } from '@cipherstash/protect' +import { encryptedColumn, encryptedTable, Encryption } from '@cipherstash/stack' /** * Define the protected schema for the User entity * This maps to the encrypted fields in your TypeORM entity */ -export const protectedUser = csTable('user', { - email: csColumn('email').equality().orderAndRange(), - ssn: csColumn('ssn').equality(), - phone: csColumn('phone').equality(), +export const protectedUser = encryptedTable('user', { + email: encryptedColumn('email').equality().orderAndRange(), + ssn: encryptedColumn('ssn').equality(), + phone: encryptedColumn('phone').equality(), }) /** - * Initialize the Protect client with the defined schema + * Initialize the Encryption client with the defined schema * This will be used throughout the application for encryption/decryption operations */ -let protectClient: Awaited> +let protectClient: Awaited> export async function initializeProtectClient() { if (!protectClient) { - protectClient = await protect({ + protectClient = await Encryption({ schemas: [protectedUser], }) } diff --git a/examples/typeorm/src/utils/encrypted-column.ts b/examples/typeorm/src/utils/encrypted-column.ts index 50ab5eb1..c47a487a 100644 --- a/examples/typeorm/src/utils/encrypted-column.ts +++ b/examples/typeorm/src/utils/encrypted-column.ts @@ -1,4 +1,4 @@ -import type { EncryptedData } from '@cipherstash/protect' +import type { EncryptedData } from '@cipherstash/stack' import type { ColumnOptions } from 'typeorm' /** diff --git a/package.json b/package.json index 20258f73..31565e24 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,18 @@ { - "name": "@cipherstash/protectjs", - "description": "CipherStash Protect for JavaScript/TypeScript", + "name": "@cipherstash/stack", + "description": "CipherStash Stack", "author": "CipherStash ", "keywords": [ - "encrypted", - "query", - "language", - "typescript", - "ts", - "eql" + "encryption", + "secrets", + "typescript" ], "bugs": { - "url": "https://github.com/cipherstash/protectjs/issues" + "url": "https://github.com/cipherstash/stack/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/cipherstash/protectjs.git" + "url": "git+https://github.com/cipherstash/stack.git" }, "license": "MIT", "workspaces": [ @@ -24,7 +21,7 @@ ], "scripts": { "build": "turbo build --filter './packages/*'", - "build:js": "turbo build --filter './packages/protect' --filter './packages/nextjs'", + "build:js": "turbo build --filter './packages/stack' --filter './packages/nextjs'", "changeset": "changeset", "changeset:version": "changeset version", "changeset:publish": "changeset publish", diff --git a/packages/drizzle/README.md b/packages/drizzle/README.md index 2f4e82ff..d6b09514 100644 --- a/packages/drizzle/README.md +++ b/packages/drizzle/README.md @@ -1,8 +1,8 @@ -# Protect.js Drizzle ORM Integration +# Stash Encryption — Drizzle ORM Integration **Type-safe encryption for Drizzle ORM with searchable queries** -Seamlessly integrate Protect.js with Drizzle ORM and PostgreSQL to encrypt your data while maintaining full query capabilities—equality, range queries, text search, and sorting—all with complete TypeScript type safety. +Seamlessly integrate Stash Encryption with Drizzle ORM and PostgreSQL to encrypt your data while maintaining full query capabilities—equality, range queries, text search, and sorting—all with complete TypeScript type safety. ## Features @@ -14,9 +14,12 @@ Seamlessly integrate Protect.js with Drizzle ORM and PostgreSQL to encrypt your ## Installation ```bash -npm install @cipherstash/protect @cipherstash/drizzle drizzle-orm +npm install @cipherstash/stack @cipherstash/drizzle drizzle-orm ``` +> [!NOTE] +> **Migrating from `@cipherstash/protect`?** Replace `@cipherstash/protect` with `@cipherstash/stack` in your imports. The `protect()` function is now `Encryption()`. All old names remain available as deprecated aliases. + ## Database Setup Before using encrypted columns, you need to install the CipherStash EQL (Encrypt Query Language) functions in your PostgreSQL database. @@ -109,19 +112,19 @@ export const usersTable = pgTable('users', { > > This is because the database only stores and returns encrypted ciphertext, so it doesn't know the underlying original type. You must specify the decrypted type in your ORM schema for full type safety. -### 2. Initialize Protect.js +### 2. Initialize Stash Encryption ```typescript // protect/config.ts -import { protect } from '@cipherstash/protect' +import { Encryption } from '@cipherstash/stack' import { extractProtectSchema } from '@cipherstash/drizzle/pg' import { usersTable } from '../db/schema' -// Extract Protect.js schema from Drizzle table +// Extract Stash Encryption schema from Drizzle table export const users = extractProtectSchema(usersTable) -// Initialize Protect.js client -export const protectClient = await protect({ +// Initialize Stash Encryption client +export const protectClient = await Encryption({ schemas: [users] }) ``` @@ -307,18 +310,18 @@ Creates an encrypted column type for Drizzle schemas. ### `extractProtectSchema(table)` -Extracts a Protect.js schema from a Drizzle table definition. +Extracts a Stash Encryption schema from a Drizzle table definition. **Parameters:** - `table` - Drizzle table definition with encrypted columns -**Returns:** Protect.js schema object +**Returns:** Stash Encryption schema object ### `createProtectOperators(protectClient)` Creates Drizzle-compatible operators that automatically handle encryption. **Parameters:** -- `protectClient` - Initialized Protect.js client +- `protectClient` - Initialized Stash Encryption client **Returns:** Object with all operator functions diff --git a/packages/drizzle/__tests__/docs.test.ts b/packages/drizzle/__tests__/docs.test.ts index fec07e06..1e379048 100644 --- a/packages/drizzle/__tests__/docs.test.ts +++ b/packages/drizzle/__tests__/docs.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config' import { existsSync, readFileSync } from 'node:fs' import { join } from 'node:path' -import { protect } from '@cipherstash/protect' +import { Encryption } from '@cipherstash/stack' import * as drizzleOrm from 'drizzle-orm' import { integer, pgTable } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/postgres-js' @@ -67,14 +67,14 @@ const protectTransactions = extractProtectSchema(transactions) describe('Documentation Drift Tests', () => { let db: ReturnType let client: ReturnType - let protectClient: Awaited> + let protectClient: Awaited> let protectOps: ReturnType let seedDataIds: number[] = [] beforeAll(async () => { client = postgres(process.env.DATABASE_URL as string) db = drizzle({ client }) - protectClient = await protect({ schemas: [protectTransactions] }) + protectClient = await Encryption({ schemas: [protectTransactions] }) protectOps = createProtectOperators(protectClient) // Create test table with EQL encrypted columns (drop if exists for clean state) diff --git a/packages/drizzle/__tests__/drizzle.test.ts b/packages/drizzle/__tests__/drizzle.test.ts index 5c42ea81..d9ebc0af 100644 --- a/packages/drizzle/__tests__/drizzle.test.ts +++ b/packages/drizzle/__tests__/drizzle.test.ts @@ -1,5 +1,5 @@ import 'dotenv/config' -import { protect } from '@cipherstash/protect' +import { Encryption } from '@cipherstash/stack' import { and, eq, inArray, sql } from 'drizzle-orm' import { integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/postgres-js' @@ -56,7 +56,7 @@ const drizzleUsersTable = pgTable('protect-ci', { testRunId: text('test_run_id'), }) -// Extract Protect.js schema from Drizzle table +// Extract Stash Encryption schema from Drizzle table const users = extractProtectSchema(drizzleUsersTable) // Hard code this as the CI database doesn't support order by on encrypted columns @@ -78,14 +78,14 @@ interface DecryptedUser { } } -let protectClient: Awaited> +let protectClient: Awaited> let protectOps: ReturnType let db: ReturnType const testData: TestUser[] = [] beforeAll(async () => { - // Initialize Protect.js client using schema extracted from Drizzle table - protectClient = await protect({ schemas: [users] }) + // Initialize Stash Encryption client using schema extracted from Drizzle table + protectClient = await Encryption({ schemas: [users] }) protectOps = createProtectOperators(protectClient) const client = postgres(process.env.DATABASE_URL as string) @@ -180,7 +180,7 @@ afterAll(async () => { .where(eq(drizzleUsersTable.testRunId, TEST_RUN_ID)) }, 30000) -describe('Drizzle ORM Integration with Protect.js', () => { +describe('Drizzle ORM Integration with Stash Encryption', () => { it('should perform equality search using Protect operators', async () => { const searchEmail = 'jane.smith@example.com' diff --git a/packages/drizzle/package.json b/packages/drizzle/package.json index bd5d3c5a..72e09009 100644 --- a/packages/drizzle/package.json +++ b/packages/drizzle/package.json @@ -1,7 +1,7 @@ { "name": "@cipherstash/drizzle", "version": "2.3.0", - "description": "CipherStash Protect.js Drizzle ORM integration for TypeScript", + "description": "CipherStash Stash Encryption Drizzle ORM integration for TypeScript", "keywords": [ "encrypted", "drizzle", @@ -43,7 +43,7 @@ "release": "tsup" }, "peerDependencies": { - "@cipherstash/protect": ">=10", + "@cipherstash/stack": ">=0", "@cipherstash/schema": ">=2", "@types/pg": "*", "drizzle-kit": ">=0.20", @@ -66,7 +66,7 @@ } }, "devDependencies": { - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "@cipherstash/schema": "workspace:*", "dotenv": "^16.4.7", "fast-check": "^4.3.0", diff --git a/packages/drizzle/src/pg/operators.ts b/packages/drizzle/src/pg/operators.ts index 9addf834..0b54ab36 100644 --- a/packages/drizzle/src/pg/operators.ts +++ b/packages/drizzle/src/pg/operators.ts @@ -1,10 +1,10 @@ +import type { QueryTypeName } from '@cipherstash/stack' import type { - ProtectClient, - ProtectColumn, - ProtectTable, - ProtectTableColumn, -} from '@cipherstash/protect/client' -import type { QueryTypeName } from '@cipherstash/protect' + EncryptedColumn, + EncryptedTable, + EncryptedTableColumn, + EncryptionClient, +} from '@cipherstash/stack/client' import { type SQL, type SQLWrapper, @@ -131,8 +131,8 @@ function getDrizzleTableFromColumn(drizzleColumn: SQLWrapper): unknown { */ function getProtectTableFromColumn( drizzleColumn: SQLWrapper, - protectTableCache: Map>, -): ProtectTable | undefined { + protectTableCache: Map>, +): EncryptedTable | undefined { const drizzleTable = getDrizzleTableFromColumn(drizzleColumn) if (!drizzleTable) { return undefined @@ -162,12 +162,12 @@ function getProtectTableFromColumn( } /** - * Helper to get the ProtectColumn for a Drizzle column from the ProtectTable + * Helper to get the EncryptedColumn for a Drizzle column from the ProtectTable */ -function getProtectColumn( +function getEncryptedColumn( drizzleColumn: SQLWrapper, - protectTable: ProtectTable, -): ProtectColumn | undefined { + protectTable: EncryptedTable, +): EncryptedColumn | undefined { const column = drizzleColumn as unknown as Record const columnName = column.name as string | undefined if (!columnName) { @@ -175,28 +175,28 @@ function getProtectColumn( } const protectTableAny = protectTable as unknown as Record - return protectTableAny[columnName] as ProtectColumn | undefined + return protectTableAny[columnName] as EncryptedColumn | undefined } /** * Column metadata extracted from a Drizzle column */ interface ColumnInfo { - readonly protectColumn: ProtectColumn | undefined + readonly protectColumn: EncryptedColumn | undefined readonly config: (EncryptedColumnConfig & { name: string }) | undefined - readonly protectTable: ProtectTable | undefined + readonly protectTable: EncryptedTable | undefined readonly columnName: string readonly tableName: string | undefined } /** - * Helper to get the ProtectColumn and column config for a Drizzle column + * Helper to get the EncryptedColumn and column config for a Drizzle column * If protectTable is not provided, it will be derived from the column */ function getColumnInfo( drizzleColumn: SQLWrapper, - protectTable: ProtectTable | undefined, - protectTableCache: Map>, + protectTable: EncryptedTable | undefined, + protectTableCache: Map>, ): ColumnInfo { const column = drizzleColumn as unknown as Record const columnName = (column.name as string | undefined) || 'unknown' @@ -225,7 +225,7 @@ function getColumnInfo( } } - const protectColumn = getProtectColumn(drizzleColumn, resolvedProtectTable) + const protectColumn = getEncryptedColumn(drizzleColumn, resolvedProtectTable) const config = getEncryptedColumnConfig(columnName, drizzleColumn) return { @@ -268,10 +268,14 @@ interface ValueToEncrypt { * Returns an array of encrypted search terms or original values if not encrypted */ async function encryptValues( - protectClient: ProtectClient, - values: Array<{ value: unknown; column: SQLWrapper; queryType?: QueryTypeName }>, - protectTable: ProtectTable | undefined, - protectTableCache: Map>, + protectClient: EncryptionClient, + values: Array<{ + value: unknown + column: SQLWrapper + queryType?: QueryTypeName + }>, + protectTable: EncryptedTable | undefined, + protectTableCache: Map>, ): Promise { if (values.length === 0) { return [] @@ -312,9 +316,13 @@ async function encryptValues( const columnGroups = new Map< string, { - column: ProtectColumn - table: ProtectTable - values: Array<{ value: string | number; index: number; queryType?: QueryTypeName }> + column: EncryptedColumn + table: EncryptedTable + values: Array<{ + value: string | number + index: number + queryType?: QueryTypeName + }> resultIndices: number[] } >() @@ -402,11 +410,11 @@ async function encryptValues( * Returns the encrypted search term or the original value if not encrypted */ async function encryptValue( - protectClient: ProtectClient, + protectClient: EncryptionClient, value: unknown, drizzleColumn: SQLWrapper, - protectTable: ProtectTable | undefined, - protectTableCache: Map>, + protectTable: EncryptedTable | undefined, + protectTableCache: Map>, queryType?: QueryTypeName, ): Promise { const results = await encryptValues( @@ -468,9 +476,9 @@ function createLazyOperator( ) => SQL, needsEncryption: boolean, columnInfo: ColumnInfo, - protectClient: ProtectClient, - defaultProtectTable: ProtectTable | undefined, - protectTableCache: Map>, + protectClient: EncryptionClient, + defaultProtectTable: EncryptedTable | undefined, + protectTableCache: Map>, min?: unknown, max?: unknown, queryType?: QueryTypeName, @@ -602,9 +610,9 @@ async function executeLazyOperator( */ async function executeLazyOperatorDirect( lazyOp: LazyOperator, - protectClient: ProtectClient, - defaultProtectTable: ProtectTable | undefined, - protectTableCache: Map>, + protectClient: EncryptionClient, + defaultProtectTable: EncryptedTable | undefined, + protectTableCache: Map>, ): Promise { if (!lazyOp.needsEncryption) { return lazyOp.execute(lazyOp.right) @@ -649,9 +657,9 @@ function createComparisonOperator( left: SQLWrapper, right: unknown, columnInfo: ColumnInfo, - protectClient: ProtectClient, - defaultProtectTable: ProtectTable | undefined, - protectTableCache: Map>, + protectClient: EncryptionClient, + defaultProtectTable: EncryptedTable | undefined, + protectTableCache: Map>, ): Promise | SQL { const { config } = columnInfo @@ -698,8 +706,8 @@ function createComparisonOperator( protectClient, defaultProtectTable, protectTableCache, - undefined, // min - undefined, // max + undefined, // min + undefined, // max 'orderAndRange', ) as Promise } @@ -732,8 +740,8 @@ function createComparisonOperator( protectClient, defaultProtectTable, protectTableCache, - undefined, // min - undefined, // max + undefined, // min + undefined, // max 'equality', ) as Promise } @@ -751,9 +759,9 @@ function createRangeOperator( min: unknown, max: unknown, columnInfo: ColumnInfo, - protectClient: ProtectClient, - defaultProtectTable: ProtectTable | undefined, - protectTableCache: Map>, + protectClient: EncryptionClient, + defaultProtectTable: EncryptedTable | undefined, + protectTableCache: Map>, ): Promise | SQL { const { config } = columnInfo @@ -810,9 +818,9 @@ function createTextSearchOperator( left: SQLWrapper, right: unknown, columnInfo: ColumnInfo, - protectClient: ProtectClient, - defaultProtectTable: ProtectTable | undefined, - protectTableCache: Map>, + protectClient: EncryptionClient, + defaultProtectTable: EncryptedTable | undefined, + protectTableCache: Map>, ): Promise | SQL { const { config } = columnInfo @@ -855,8 +863,8 @@ function createTextSearchOperator( protectClient, defaultProtectTable, protectTableCache, - undefined, // min - undefined, // max + undefined, // min + undefined, // max 'freeTextSearch', ) as Promise } @@ -866,7 +874,7 @@ function createTextSearchOperator( // ============================================================================ /** - * Creates a set of Protect.js-aware operators that automatically encrypt values + * Creates a set of Stash Encryption-aware operators that automatically encrypt values * for encrypted columns before using them with Drizzle operators. * * For equality and text search operators (eq, ne, like, ilike, inArray, etc.): @@ -877,7 +885,7 @@ function createTextSearchOperator( * Values are encrypted and then use eql_v2.* functions (eql_v2.gt(), eql_v2.gte(), etc.) * which are required for ORE (Order-Revealing Encryption) comparisons. * - * @param protectClient - The Protect.js client instance + * @param protectClient - The Stash Encryption client instance * @returns An object with all Drizzle operators wrapped for encrypted columns * * @example @@ -898,7 +906,7 @@ function createTextSearchOperator( * .where(await protectOps.gte(usersTable.age, 25)) * ``` */ -export function createProtectOperators(protectClient: ProtectClient): { +export function createProtectOperators(protectClient: EncryptionClient): { // Comparison operators /** * Equality operator - encrypts value for encrypted columns. @@ -1066,8 +1074,11 @@ export function createProtectOperators(protectClient: ProtectClient): { arrayOverlaps: typeof arrayOverlaps } { // Create a cache for protect tables keyed by table name - const protectTableCache = new Map>() - const defaultProtectTable: ProtectTable | undefined = + const protectTableCache = new Map< + string, + EncryptedTable + >() + const defaultProtectTable: EncryptedTable | undefined = undefined /** @@ -1334,7 +1345,11 @@ export function createProtectOperators(protectClient: ProtectClient): { // Encrypt all values in the array in a single batch const encryptedValues = await encryptValues( protectClient, - right.map((value) => ({ value, column: left, queryType: 'equality' as const })), + right.map((value) => ({ + value, + column: left, + queryType: 'equality' as const, + })), defaultProtectTable, protectTableCache, ) @@ -1380,7 +1395,11 @@ export function createProtectOperators(protectClient: ProtectClient): { // Encrypt all values in the array in a single batch const encryptedValues = await encryptValues( protectClient, - right.map((value) => ({ value, column: left, queryType: 'equality' as const })), + right.map((value) => ({ + value, + column: left, + queryType: 'equality' as const, + })), defaultProtectTable, protectTableCache, ) @@ -1519,7 +1538,11 @@ export function createProtectOperators(protectClient: ProtectClient): { // Batch encrypt all values const encryptedResults = await encryptValues( protectClient, - valuesToEncrypt.map((v) => ({ value: v.value, column: v.column, queryType: v.queryType })), + valuesToEncrypt.map((v) => ({ + value: v.value, + column: v.column, + queryType: v.queryType, + })), defaultProtectTable, protectTableCache, ) @@ -1674,7 +1697,11 @@ export function createProtectOperators(protectClient: ProtectClient): { const encryptedResults = await encryptValues( protectClient, - valuesToEncrypt.map((v) => ({ value: v.value, column: v.column, queryType: v.queryType })), + valuesToEncrypt.map((v) => ({ + value: v.value, + column: v.column, + queryType: v.queryType, + })), defaultProtectTable, protectTableCache, ) diff --git a/packages/drizzle/src/pg/schema-extraction.ts b/packages/drizzle/src/pg/schema-extraction.ts index a655e07c..00dcd511 100644 --- a/packages/drizzle/src/pg/schema-extraction.ts +++ b/packages/drizzle/src/pg/schema-extraction.ts @@ -1,18 +1,18 @@ import { - type ProtectColumn, - csColumn, - csTable, -} from '@cipherstash/protect/client' + type EncryptedColumn, + encryptedColumn, + encryptedTable, +} from '@cipherstash/stack/client' import type { PgTable } from 'drizzle-orm/pg-core' import { getEncryptedColumnConfig } from './index.js' /** - * Extracts a Protect.js schema from a Drizzle table definition. + * Extracts an encryption schema from a Drizzle table definition. * This function identifies columns created with `encryptedType` and - * builds a corresponding `ProtectTable` with `csColumn` definitions. + * builds a corresponding table schema with `encryptedColumn` definitions. * * @param table - The Drizzle table definition - * @returns A ProtectTable that can be used with `protect()` initialization + * @returns An EncryptedTable that can be used with `Encryption()` initialization * * @example * ```ts @@ -22,14 +22,14 @@ import { getEncryptedColumnConfig } from './index.js' * }) * * const protectSchema = extractProtectSchema(drizzleUsersTable) - * const protectClient = await protect({ schemas: [protectSchema.build()] }) + * const encryptionClient = await Encryption({ schemas: [protectSchema.build()] }) * ``` */ // We use any for the PgTable generic because we need to access Drizzle's internal properties // biome-ignore lint/suspicious/noExplicitAny: Drizzle table types don't expose Symbol properties export function extractProtectSchema>( table: T, -): ReturnType>> { +): ReturnType>> { // Drizzle tables store the name in a Symbol property // biome-ignore lint/suspicious/noExplicitAny: Drizzle tables don't expose Symbol properties in types const tableName = (table as any)[Symbol.for('drizzle:Name')] as @@ -41,7 +41,7 @@ export function extractProtectSchema>( ) } - const columns: Record = {} + const columns: Record = {} // Iterate through table columns for (const [columnName, column] of Object.entries(table)) { @@ -58,40 +58,40 @@ export function extractProtectSchema>( // Drizzle columns have a 'name' property that contains the actual database column name const actualColumnName = column.name || config.name - // This is an encrypted column - build csColumn using the actual column name - const csCol = csColumn(actualColumnName) + // This is an encrypted column - build encryptedColumn using the actual column name + const col = encryptedColumn(actualColumnName) // Apply data type if (config.dataType && config.dataType !== 'string') { - csCol.dataType(config.dataType) + col.dataType(config.dataType) } // Apply indexes based on configuration if (config.orderAndRange) { - csCol.orderAndRange() + col.orderAndRange() } if (config.equality) { if (Array.isArray(config.equality)) { // Custom token filters - csCol.equality(config.equality) + col.equality(config.equality) } else { // Default equality (boolean true) - csCol.equality() + col.equality() } } if (config.freeTextSearch) { if (typeof config.freeTextSearch === 'object') { // Custom match options - csCol.freeTextSearch(config.freeTextSearch) + col.freeTextSearch(config.freeTextSearch) } else { // Default freeTextSearch (boolean true) - csCol.freeTextSearch() + col.freeTextSearch() } } - columns[actualColumnName] = csCol + columns[actualColumnName] = col } } @@ -101,5 +101,5 @@ export function extractProtectSchema>( ) } - return csTable(tableName, columns) + return encryptedTable(tableName, columns) } diff --git a/packages/protect-dynamodb/.npmignore b/packages/dynamodb/.npmignore similarity index 100% rename from packages/protect-dynamodb/.npmignore rename to packages/dynamodb/.npmignore diff --git a/packages/protect-dynamodb/CHANGELOG.md b/packages/dynamodb/CHANGELOG.md similarity index 100% rename from packages/protect-dynamodb/CHANGELOG.md rename to packages/dynamodb/CHANGELOG.md diff --git a/packages/protect-dynamodb/README.md b/packages/dynamodb/README.md similarity index 90% rename from packages/protect-dynamodb/README.md rename to packages/dynamodb/README.md index e52ffe66..cf154749 100644 --- a/packages/protect-dynamodb/README.md +++ b/packages/dynamodb/README.md @@ -1,6 +1,6 @@ -# Protect.js DynamoDB Helpers +# Stash Encryption — DynamoDB Helpers -Helpers for using CipherStash [Protect.js](https://github.com/cipherstash/protectjs) with DynamoDB. +Helpers for using CipherStash [Stash Encryption](https://github.com/cipherstash/protectjs) with DynamoDB. [![Built by CipherStash](https://raw.githubusercontent.com/cipherstash/meta/refs/heads/main/csbadge.svg)](https://cipherstash.com) [![NPM version](https://img.shields.io/npm/v/@cipherstash/protect-dynamodb.svg?style=for-the-badge&labelColor=000000)](https://www.npmjs.com/package/@cipherstash/protect-dynamodb) @@ -16,20 +16,23 @@ yarn add @cipherstash/protect-dynamodb pnpm add @cipherstash/protect-dynamodb ``` +> [!NOTE] +> **Migrating from `@cipherstash/protect`?** Replace `@cipherstash/protect` with `@cipherstash/stack` in your imports. `csTable` is now `encryptedTable`, `csColumn` is now `encryptedColumn`, and `protect()` is now `Encryption()`. All old names remain available as deprecated aliases. + ## Quick Start ```typescript import { protectDynamoDB } from '@cipherstash/protect-dynamodb' -import { protect, csColumn, csTable } from '@cipherstash/protect' +import { Encryption, encryptedColumn, encryptedTable } from '@cipherstash/stack' import { PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb' // Define your protected table schema -const users = csTable('users', { - email: csColumn('email').equality(), +const users = encryptedTable('users', { + email: encryptedColumn('email').equality(), }) -// Initialize the Protect client -const protectClient = await protect({ +// Initialize the Encryption client +const protectClient = await Encryption({ schemas: [users], }) @@ -150,8 +153,8 @@ The package automatically handles: ### Simple Table with Encrypted Fields ```typescript -const users = csTable('users', { - email: csColumn('email').equality(), +const users = encryptedTable('users', { + email: encryptedColumn('email').equality(), }) // Encrypt and store diff --git a/packages/protect-dynamodb/__tests__/audit.test.ts b/packages/dynamodb/__tests__/audit.test.ts similarity index 94% rename from packages/protect-dynamodb/__tests__/audit.test.ts rename to packages/dynamodb/__tests__/audit.test.ts index 619ff875..80da59bf 100644 --- a/packages/protect-dynamodb/__tests__/audit.test.ts +++ b/packages/dynamodb/__tests__/audit.test.ts @@ -1,27 +1,27 @@ import 'dotenv/config' -import { csColumn, csTable, csValue, protect } from '@cipherstash/protect' +import { encryptedColumn, encryptedTable, encryptedValue, Encryption } from '@cipherstash/stack' import { beforeAll, describe, expect, it } from 'vitest' import { protectDynamoDB } from '../src' -const schema = csTable('dynamo_cipherstash_test', { - email: csColumn('email').equality(), - firstName: csColumn('firstName').equality(), - lastName: csColumn('lastName').equality(), - phoneNumber: csColumn('phoneNumber'), +const schema = encryptedTable('dynamo_cipherstash_test', { + email: encryptedColumn('email').equality(), + firstName: encryptedColumn('firstName').equality(), + lastName: encryptedColumn('lastName').equality(), + phoneNumber: encryptedColumn('phoneNumber'), example: { - protected: csValue('example.protected'), + protected: encryptedValue('example.protected'), deep: { - protected: csValue('example.deep.protected'), + protected: encryptedValue('example.deep.protected'), }, }, }) describe('protect dynamodb helpers', () => { - let protectClient: Awaited> + let protectClient: Awaited> let protectDynamo: ReturnType beforeAll(async () => { - protectClient = await protect({ + protectClient = await Encryption({ schemas: [schema], }) diff --git a/packages/protect-dynamodb/__tests__/dynamodb.test.ts b/packages/dynamodb/__tests__/dynamodb.test.ts similarity index 92% rename from packages/protect-dynamodb/__tests__/dynamodb.test.ts rename to packages/dynamodb/__tests__/dynamodb.test.ts index 0b8ee27a..36e891ea 100644 --- a/packages/protect-dynamodb/__tests__/dynamodb.test.ts +++ b/packages/dynamodb/__tests__/dynamodb.test.ts @@ -1,21 +1,21 @@ import 'dotenv/config' -import { csColumn, csTable, csValue, protect } from '@cipherstash/protect' +import { encryptedColumn, encryptedTable, encryptedValue, Encryption } from '@cipherstash/stack' import { beforeAll, describe, expect, it } from 'vitest' import { protectDynamoDB } from '../src' -const schema = csTable('dynamo_cipherstash_test', { - email: csColumn('email').equality(), - firstName: csColumn('firstName').equality(), - lastName: csColumn('lastName').equality(), - phoneNumber: csColumn('phoneNumber'), - json: csColumn('json').dataType('json'), - jsonSearchable: csColumn('jsonSearchable').dataType('json'), +const schema = encryptedTable('dynamo_cipherstash_test', { + email: encryptedColumn('email').equality(), + firstName: encryptedColumn('firstName').equality(), + lastName: encryptedColumn('lastName').equality(), + phoneNumber: encryptedColumn('phoneNumber'), + json: encryptedColumn('json').dataType('json'), + jsonSearchable: encryptedColumn('jsonSearchable').dataType('json'), //.searchableJson('users/jsonSearchable'), example: { - protected: csValue('example.protected'), + protected: encryptedValue('example.protected'), deep: { - protected: csValue('example.deep.protected'), - protectNestedJson: csValue('example.deep.protectNestedJson').dataType( + protected: encryptedValue('example.deep.protected'), + protectNestedJson: encryptedValue('example.deep.protectNestedJson').dataType( 'json', ), }, @@ -23,11 +23,11 @@ const schema = csTable('dynamo_cipherstash_test', { }) describe('protect dynamodb helpers', () => { - let protectClient: Awaited> + let protectClient: Awaited> let protectDynamo: ReturnType beforeAll(async () => { - protectClient = await protect({ + protectClient = await Encryption({ schemas: [schema], }) diff --git a/packages/protect-dynamodb/package.json b/packages/dynamodb/package.json similarity index 87% rename from packages/protect-dynamodb/package.json rename to packages/dynamodb/package.json index d3b266d9..a47ba624 100644 --- a/packages/protect-dynamodb/package.json +++ b/packages/dynamodb/package.json @@ -1,7 +1,7 @@ { "name": "@cipherstash/protect-dynamodb", "version": "8.0.0", - "description": "Protect.js DynamoDB Helpers", + "description": "Stash Encryption DynamoDB Helpers", "keywords": [ "dynamodb", "cipherstash", @@ -34,7 +34,7 @@ "release": "tsup" }, "devDependencies": { - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "dotenv": "^16.4.7", "tsup": "catalog:repo", "tsx": "catalog:repo", @@ -42,7 +42,7 @@ "vitest": "catalog:repo" }, "peerDependencies": { - "@cipherstash/protect": "workspace:*" + "@cipherstash/stack": "workspace:*" }, "publishConfig": { "access": "public" diff --git a/packages/protect-dynamodb/src/helpers.ts b/packages/dynamodb/src/helpers.ts similarity index 97% rename from packages/protect-dynamodb/src/helpers.ts rename to packages/dynamodb/src/helpers.ts index c52d0a15..57df11aa 100644 --- a/packages/protect-dynamodb/src/helpers.ts +++ b/packages/dynamodb/src/helpers.ts @@ -1,8 +1,8 @@ import type { Encrypted, - ProtectTable, - ProtectTableColumn, -} from '@cipherstash/protect' + EncryptedTable, + EncryptedTableColumn, +} from '@cipherstash/stack' import type { ProtectDynamoDBError } from './types' export const ciphertextAttrSuffix = '__source' export const searchTermAttrSuffix = '__hmac' @@ -134,7 +134,7 @@ export function toEncryptedDynamoItem( export function toItemWithEqlPayloads( decrypted: Record, - encryptSchemas: ProtectTable, + encryptSchemas: EncryptedTable, ): Record { function processValue( attrName: string, diff --git a/packages/protect-dynamodb/src/index.ts b/packages/dynamodb/src/index.ts similarity index 85% rename from packages/protect-dynamodb/src/index.ts rename to packages/dynamodb/src/index.ts index fe18ddfa..b5c75cca 100644 --- a/packages/protect-dynamodb/src/index.ts +++ b/packages/dynamodb/src/index.ts @@ -1,9 +1,9 @@ import type { Encrypted, - ProtectTable, - ProtectTableColumn, + EncryptedTable, + EncryptedTableColumn, SearchTerm, -} from '@cipherstash/protect' +} from '@cipherstash/stack' import { BulkDecryptModelsOperation } from './operations/bulk-decrypt-models' import { BulkEncryptModelsOperation } from './operations/bulk-encrypt-models' import { DecryptModelOperation } from './operations/decrypt-model' @@ -19,7 +19,7 @@ export function protectDynamoDB( return { encryptModel>( item: T, - protectTable: ProtectTable, + protectTable: EncryptedTable, ) { return new EncryptModelOperation( protectClient, @@ -31,7 +31,7 @@ export function protectDynamoDB( bulkEncryptModels>( items: T[], - protectTable: ProtectTable, + protectTable: EncryptedTable, ) { return new BulkEncryptModelsOperation( protectClient, @@ -43,7 +43,7 @@ export function protectDynamoDB( decryptModel>( item: Record, - protectTable: ProtectTable, + protectTable: EncryptedTable, ) { return new DecryptModelOperation( protectClient, @@ -55,7 +55,7 @@ export function protectDynamoDB( bulkDecryptModels>( items: Record[], - protectTable: ProtectTable, + protectTable: EncryptedTable, ) { return new BulkDecryptModelsOperation( protectClient, diff --git a/packages/protect-dynamodb/src/operations/base-operation.ts b/packages/dynamodb/src/operations/base-operation.ts similarity index 100% rename from packages/protect-dynamodb/src/operations/base-operation.ts rename to packages/dynamodb/src/operations/base-operation.ts diff --git a/packages/protect-dynamodb/src/operations/bulk-decrypt-models.ts b/packages/dynamodb/src/operations/bulk-decrypt-models.ts similarity index 86% rename from packages/protect-dynamodb/src/operations/bulk-decrypt-models.ts rename to packages/dynamodb/src/operations/bulk-decrypt-models.ts index e72e231c..4460332e 100644 --- a/packages/protect-dynamodb/src/operations/bulk-decrypt-models.ts +++ b/packages/dynamodb/src/operations/bulk-decrypt-models.ts @@ -2,10 +2,10 @@ import { type Result, withResult } from '@byteslice/result' import type { Decrypted, Encrypted, - ProtectClient, + EncryptionClient, ProtectTable, ProtectTableColumn, -} from '@cipherstash/protect' +} from '@cipherstash/stack' import { handleError, toItemWithEqlPayloads } from '../helpers' import type { ProtectDynamoDBError } from '../types' import { @@ -16,14 +16,14 @@ import { export class BulkDecryptModelsOperation< T extends Record, > extends DynamoDBOperation[]> { - private protectClient: ProtectClient + private protectClient: EncryptionClient private items: Record[] - private protectTable: ProtectTable + private protectTable: EncryptedTable constructor( - protectClient: ProtectClient, + protectClient: EncryptionClient, items: Record[], - protectTable: ProtectTable, + protectTable: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) diff --git a/packages/protect-dynamodb/src/operations/bulk-encrypt-models.ts b/packages/dynamodb/src/operations/bulk-encrypt-models.ts similarity index 86% rename from packages/protect-dynamodb/src/operations/bulk-encrypt-models.ts rename to packages/dynamodb/src/operations/bulk-encrypt-models.ts index 10fd5398..6afe2dde 100644 --- a/packages/protect-dynamodb/src/operations/bulk-encrypt-models.ts +++ b/packages/dynamodb/src/operations/bulk-encrypt-models.ts @@ -1,9 +1,9 @@ import { type Result, withResult } from '@byteslice/result' import type { - ProtectClient, + EncryptionClient, ProtectTable, ProtectTableColumn, -} from '@cipherstash/protect' +} from '@cipherstash/stack' import { deepClone, handleError, toEncryptedDynamoItem } from '../helpers' import type { ProtectDynamoDBError } from '../types' import { @@ -14,14 +14,14 @@ import { export class BulkEncryptModelsOperation< T extends Record, > extends DynamoDBOperation { - private protectClient: ProtectClient + private protectClient: EncryptionClient private items: T[] - private protectTable: ProtectTable + private protectTable: EncryptedTable constructor( - protectClient: ProtectClient, + protectClient: EncryptionClient, items: T[], - protectTable: ProtectTable, + protectTable: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) diff --git a/packages/protect-dynamodb/src/operations/decrypt-model.ts b/packages/dynamodb/src/operations/decrypt-model.ts similarity index 85% rename from packages/protect-dynamodb/src/operations/decrypt-model.ts rename to packages/dynamodb/src/operations/decrypt-model.ts index 862e434d..73a7f8a3 100644 --- a/packages/protect-dynamodb/src/operations/decrypt-model.ts +++ b/packages/dynamodb/src/operations/decrypt-model.ts @@ -2,10 +2,10 @@ import { type Result, withResult } from '@byteslice/result' import type { Decrypted, Encrypted, - ProtectClient, + EncryptionClient, ProtectTable, ProtectTableColumn, -} from '@cipherstash/protect' +} from '@cipherstash/stack' import { handleError, toItemWithEqlPayloads } from '../helpers' import type { ProtectDynamoDBError } from '../types' import { @@ -16,14 +16,14 @@ import { export class DecryptModelOperation< T extends Record, > extends DynamoDBOperation> { - private protectClient: ProtectClient + private protectClient: EncryptionClient private item: Record - private protectTable: ProtectTable + private protectTable: EncryptedTable constructor( - protectClient: ProtectClient, + protectClient: EncryptionClient, item: Record, - protectTable: ProtectTable, + protectTable: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) diff --git a/packages/protect-dynamodb/src/operations/encrypt-model.ts b/packages/dynamodb/src/operations/encrypt-model.ts similarity index 85% rename from packages/protect-dynamodb/src/operations/encrypt-model.ts rename to packages/dynamodb/src/operations/encrypt-model.ts index 7cf374f7..1158bd84 100644 --- a/packages/protect-dynamodb/src/operations/encrypt-model.ts +++ b/packages/dynamodb/src/operations/encrypt-model.ts @@ -1,9 +1,9 @@ import { type Result, withResult } from '@byteslice/result' import type { - ProtectClient, + EncryptionClient, ProtectTable, ProtectTableColumn, -} from '@cipherstash/protect' +} from '@cipherstash/stack' import { deepClone, handleError, toEncryptedDynamoItem } from '../helpers' import type { ProtectDynamoDBError } from '../types' import { @@ -14,14 +14,14 @@ import { export class EncryptModelOperation< T extends Record, > extends DynamoDBOperation { - private protectClient: ProtectClient + private protectClient: EncryptionClient private item: T - private protectTable: ProtectTable + private protectTable: EncryptedTable constructor( - protectClient: ProtectClient, + protectClient: EncryptionClient, item: T, - protectTable: ProtectTable, + protectTable: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) diff --git a/packages/protect-dynamodb/src/operations/search-terms.ts b/packages/dynamodb/src/operations/search-terms.ts similarity index 92% rename from packages/protect-dynamodb/src/operations/search-terms.ts rename to packages/dynamodb/src/operations/search-terms.ts index 22537ae4..afa6ac50 100644 --- a/packages/protect-dynamodb/src/operations/search-terms.ts +++ b/packages/dynamodb/src/operations/search-terms.ts @@ -1,5 +1,5 @@ import { type Result, withResult } from '@byteslice/result' -import type { ProtectClient, SearchTerm } from '@cipherstash/protect' +import type { EncryptionClient, SearchTerm } from '@cipherstash/stack' import { handleError } from '../helpers' import type { ProtectDynamoDBError } from '../types' import { @@ -22,11 +22,11 @@ import { * ``` */ export class SearchTermsOperation extends DynamoDBOperation { - private protectClient: ProtectClient + private protectClient: EncryptionClient private terms: SearchTerm[] constructor( - protectClient: ProtectClient, + protectClient: EncryptionClient, terms: SearchTerm[], options?: DynamoDBOperationOptions, ) { diff --git a/packages/protect-dynamodb/src/types.ts b/packages/dynamodb/src/types.ts similarity index 83% rename from packages/protect-dynamodb/src/types.ts rename to packages/dynamodb/src/types.ts index 32c18de6..36855d09 100644 --- a/packages/protect-dynamodb/src/types.ts +++ b/packages/dynamodb/src/types.ts @@ -1,10 +1,10 @@ import type { Encrypted, - ProtectClient, - ProtectTable, - ProtectTableColumn, + EncryptedTable, + EncryptedTableColumn, + EncryptionClient, SearchTerm, -} from '@cipherstash/protect' +} from '@cipherstash/stack' import type { BulkDecryptModelsOperation } from './operations/bulk-decrypt-models' import type { BulkEncryptModelsOperation } from './operations/bulk-encrypt-models' import type { DecryptModelOperation } from './operations/decrypt-model' @@ -12,7 +12,7 @@ import type { EncryptModelOperation } from './operations/encrypt-model' import type { SearchTermsOperation } from './operations/search-terms' export interface ProtectDynamoDBConfig { - protectClient: ProtectClient + protectClient: EncryptionClient options?: { logger?: { error: (message: string, error: Error) => void @@ -29,22 +29,22 @@ export interface ProtectDynamoDBError extends Error { export interface ProtectDynamoDBInstance { encryptModel>( item: T, - protectTable: ProtectTable, + protectTable: EncryptedTable, ): EncryptModelOperation bulkEncryptModels>( items: T[], - protectTable: ProtectTable, + protectTable: EncryptedTable, ): BulkEncryptModelsOperation decryptModel>( item: Record, - protectTable: ProtectTable, + protectTable: EncryptedTable, ): DecryptModelOperation bulkDecryptModels>( items: Record[], - protectTable: ProtectTable, + protectTable: EncryptedTable, ): BulkDecryptModelsOperation /** diff --git a/packages/protect-dynamodb/tsconfig.json b/packages/dynamodb/tsconfig.json similarity index 100% rename from packages/protect-dynamodb/tsconfig.json rename to packages/dynamodb/tsconfig.json diff --git a/packages/protect-dynamodb/tsup.config.ts b/packages/dynamodb/tsup.config.ts similarity index 100% rename from packages/protect-dynamodb/tsup.config.ts rename to packages/dynamodb/tsup.config.ts diff --git a/packages/jseql/README.md b/packages/jseql/README.md deleted file mode 100644 index 6a65f652..00000000 --- a/packages/jseql/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# @cipherstash/jseql - -The `@cipherstash/jseql` package has been deprecated in favor of the `@cipherstash/protect` package. -Please refer to the [main README](https://github.com/cipherstash/protectjs) for more information. \ No newline at end of file diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md index 4a26fb46..65720cce 100644 --- a/packages/nextjs/README.md +++ b/packages/nextjs/README.md @@ -1,4 +1,4 @@ -# @cipherstash/protect +# @cipherstash/nextjs -This is the main package for the CipherStash Protect JavaScript Package. +Next.js helpers and Clerk integration for the CipherStash Stash Encryption stack. Please refer to the [main README](https://github.com/cipherstash/protectjs) for more information. \ No newline at end of file diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 376db8d4..79230346 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,7 +1,7 @@ { "name": "@cipherstash/nextjs", "version": "4.1.0", - "description": "Nextjs package for use with @cipherstash/protect", + "description": "Nextjs package for use with @cipherstash/stack", "keywords": [ "encrypted", "typescript", diff --git a/packages/protect/README.md b/packages/protect/README.md index fab455d0..72d7b1f1 100644 --- a/packages/protect/README.md +++ b/packages/protect/README.md @@ -1,1090 +1,3 @@ -

- CipherStash Logo -
- Protect.js -

-

- End-to-end field level encryption for JavaScript/TypeScript apps with zero‑knowledge key management. Search encrypted data without decrypting it. -
-

-

-
⭐ Please star this repo if you find it useful!
-
+# CipherStash Protect.js - - -Protect.js lets you encrypt every value with its own key—without sacrificing performance or usability. Encryption happens in your app; ciphertext is stored in your database. - -Per‑value unique keys are powered by CipherStash [ZeroKMS](https://cipherstash.com/products/zerokms) bulk key operations, backed by a root key in [AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html). - -Encrypted data is structured as an [EQL](https://github.com/cipherstash/encrypt-query-language) JSON payload and can be stored in any database that supports JSONB. - -> [!IMPORTANT] -> Searching, sorting, and filtering on encrypted data is currently only supported when storing encrypted data in PostgreSQL. -> Read more about [searching encrypted data](./docs/concepts/searchable-encryption.md). - -Looking for DynamoDB support? Check out the [Protect.js for DynamoDB helper library](https://www.npmjs.com/package/@cipherstash/protect-dynamodb). - -## Quick start (60 seconds) - -Create an account and workspace in the [CipherStash dashboard](https://cipherstash.com/signup), then follow the onboarding guide to generate your client credentials and store them in your `.env` file. - -Install the package: - -```bash -npm install @cipherstash/protect -``` - -Start encrypting data: - -```ts -import { protect } from "@cipherstash/protect"; -import { csTable, csColumn } from "@cipherstash/protect"; - -// 1) Define a schema -const users = csTable("users", { email: csColumn("email") }); - -// 2) Create a client (requires CS_* env vars) -const client = await protect({ schemas: [users] }); - -// 3) Encrypt → store JSONB payload -const encrypted = await client.encrypt("alice@example.com", { - table: users, - column: users.email, -}); - -if (encrypted.failure) { - // You decide how to handle the failure and the user experience -} - -// 4) Decrypt later -const decrypted = await client.decrypt(encrypted.data); -``` - -## Architecture (high level) - -![Protect.js Architecture Diagram](https://github.com/cipherstash/protectjs/blob/main/docs/images/protectjs-architecture.png) - -## Table of contents - -- [Quick start (60 seconds)](#quick-start-60-seconds) -- [Architecture (high level)](#architecture-high-level) -- [Features](#features) -- [Installing Protect.js](#installing-protectjs) -- [Getting started](#getting-started) -- [Identity-aware encryption](#identity-aware-encryption) -- [Supported data types](#supported-data-types) -- [Searchable encryption](#searchable-encryption) -- [Multi-tenant encryption](#multi-tenant-encryption) -- [Logging](#logging) -- [CipherStash Client](#cipherstash-client) -- [Example applications](#example-applications) -- [Builds and bundling](#builds-and-bundling) -- [Contributing](#contributing) -- [License](#license) - -For more specific documentation, refer to the [docs](https://github.com/cipherstash/protectjs/tree/main/docs). - -## Features - -Protect.js protects data in using industry-standard AES encryption. -Protect.js uses [ZeroKMS](https://cipherstash.com/products/zerokms) for bulk encryption and decryption operations. -This enables every encrypted value, in every column, in every row in your database to have a unique key — without sacrificing performance. - -**Features:** - -- **Bulk encryption and decryption**: Protect.js uses [ZeroKMS](https://cipherstash.com/products/zerokms) for encrypting and decrypting thousands of records at once, while using a unique key for every value. -- **Single item encryption and decryption**: Just looking for a way to encrypt and decrypt single values? Protect.js has you covered. -- **Really fast:** ZeroKMS's performance makes using millions of unique keys feasible and performant for real-world applications built with Protect.js. -- **Identity-aware encryption**: Lock down access to sensitive data by requiring a valid JWT to perform a decryption. -- **Audit trail**: Every decryption event will be logged in ZeroKMS to help you prove compliance. -- **Searchable encryption**: Protect.js supports searching encrypted data in PostgreSQL. -- **TypeScript support**: Strongly typed with TypeScript interfaces and types. - -**Use cases:** - -- **Trusted data access**: make sure only your end-users can access their sensitive data stored in your product. -- **Meet compliance requirements faster:** meet and exceed the data encryption requirements of SOC2 and ISO27001. -- **Reduce the blast radius of data breaches:** limit the impact of exploited vulnerabilities to only the data your end-users can decrypt. - -## Installing Protect.js - -Install the [`@cipherstash/protect` package](https://www.npmjs.com/package/@cipherstash/protect) with your package manager of choice: - -```bash -npm install @cipherstash/protect -# or -yarn add @cipherstash/protect -# or -pnpm add @cipherstash/protect -``` - -> [!TIP] -> [Bun](https://bun.sh/) is not currently supported due to a lack of [Node-API compatibility](https://github.com/oven-sh/bun/issues/158). Under the hood, Protect.js uses [CipherStash Client](#cipherstash-client) which is written in Rust and embedded using [Neon](https://github.com/neon-bindings/neon). - -### Opt-out of bundling - -> [!IMPORTANT] -> **You need to opt-out of bundling when using Protect.js.** - -Protect.js uses Node.js specific features and requires the use of the [native Node.js `require`](https://nodejs.org/api/modules.html#requireid). - -When using Protect.js, you need to opt-out of bundling for tools like [Webpack](https://webpack.js.org/configuration/externals/), [esbuild](https://webpack.js.org/configuration/externals/), or [Next.js](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages). - -Read more about [building and bundling with Protect.js](#builds-and-bundling). - -## Getting started - -- 🆕 **Existing app?** Skip to [the next step](#configuration). -- 🌱 **Clean slate?** Check out the [getting started tutorial](./docs/getting-started.md). - -### Configuration - -If you haven't already, sign up for a [CipherStash account](https://cipherstash.com/signup). -Once you have an account, you will create a Workspace which is scoped to your application environment. - -Follow the onboarding steps to get your first set of credentials required to use Protect.js. -By the end of the onboarding, you will have the following environment variables: - -```bash -CS_WORKSPACE_CRN= # The workspace identifier -CS_CLIENT_ID= # The client identifier -CS_CLIENT_KEY= # The client key which is used as key material in combination with ZeroKMS -CS_CLIENT_ACCESS_KEY= # The API key used for authenticating with the CipherStash API -``` - -Save these environment variables to a `.env` file in your project. - -### Basic file structure - -The following is the basic file structure of the project. -In the `src/protect/` directory, we have the table definition in `schema.ts` and the protect client in `index.ts`. - -``` -📦 - ├ 📂 src - │ ├ 📂 protect - │ │ ├ 📜 index.ts - │ │ └ 📜 schema.ts - │ └ 📜 index.ts - ├ 📜 .env - ├ 📜 cipherstash.toml - ├ 📜 cipherstash.secret.toml - ├ 📜 package.json - └ 📜 tsconfig.json -``` - -### Define your schema - -Protect.js uses a schema to define the tables and columns that you want to encrypt and decrypt. - -Define your tables and columns by adding this to `src/protect/schema.ts`: - -```ts -import { csTable, csColumn } from "@cipherstash/protect"; - -export const users = csTable("users", { - email: csColumn("email"), -}); - -export const orders = csTable("orders", { - address: csColumn("address"), -}); -``` - -**Searchable encryption:** - -If you want to search encrypted data in your PostgreSQL database, you must declare the indexes in schema in `src/protect/schema.ts`: - -```ts -import { csTable, csColumn } from "@cipherstash/protect"; - -export const users = csTable("users", { - email: csColumn("email").freeTextSearch().equality().orderAndRange(), -}); - -export const orders = csTable("orders", { - address: csColumn("address"), -}); -``` - -Read more about [defining your schema](./docs/reference/schema.md). - -### Initialize the Protect client - -To import the `protect` function and initialize a client with your defined schema, add the following to `src/protect/index.ts`: - -```ts -import { protect, type ProtectClientConfig } from "@cipherstash/protect"; -import { users, orders } from "./schema"; - -const config: ProtectClientConfig = { - schemas: [users, orders], -} - -// Pass all your tables to the protect function to initialize the client -export const protectClient = await protect(config); -``` - -The `protect` function requires at least one `csTable` be provided in the `schemas` array. - -### Encrypt data - -Protect.js provides the `encrypt` function on `protectClient` to encrypt data. -`encrypt` takes a plaintext string, and an object with the table and column as parameters. - -To start encrypting data, add the following to `src/index.ts`: - -```typescript -import { users } from "./protect/schema"; -import { protectClient } from "./protect"; - -const encryptResult = await protectClient.encrypt("secret@squirrel.example", { - column: users.email, - table: users, -}); - -if (encryptResult.failure) { - // Handle the failure - console.log( - "error when encrypting:", - encryptResult.failure.type, - encryptResult.failure.message - ); -} - -console.log("EQL Payload containing ciphertexts:", encryptResult.data); -``` - -The `encrypt` function will return a `Result` object with either a `data` key, or a `failure` key. -The `encryptResult` will return one of the following: - -```typescript -// Success -{ - data: EncryptedPayload -} - -// Failure -{ - failure: { - type: 'EncryptionError', - message: 'A message about the error' - } -} -``` - -### Decrypt data - -Protect.js provides the `decrypt` function on `protectClient` to decrypt data. -`decrypt` takes an encrypted data object as a parameter. - -To start decrypting data, add the following to `src/index.ts`: - -```typescript -import { protectClient } from "./protect"; - -// encryptResult is the EQL payload from the previous step -const decryptResult = await protectClient.decrypt(encryptResult.data); - -if (decryptResult.failure) { - // Handle the failure - console.log( - "error when decrypting:", - decryptResult.failure.type, - decryptResult.failure.message - ); -} - -const plaintext = decryptResult.data; -console.log("plaintext:", plaintext); -``` - -The `decrypt` function returns a `Result` object with either a `data` key, or a `failure` key. -The `decryptResult` will return one of the following: - -```typescript -// Success -{ - data: 'secret@squirrel.example' -} - -// Failure -{ - failure: { - type: 'DecryptionError', - message: 'A message about the error' - } -} -``` - -### Working with models and objects - -Protect.js provides model-level encryption methods that make it easy to encrypt and decrypt entire objects. -These methods automatically handle the encryption of fields defined in your schema. - -If you are working with a large data set, the model operations are significantly faster than encrypting and decrypting individual objects as they are able to perform bulk operations. - -> [!TIP] -> CipherStash [ZeroKMS](https://cipherstash.com/products/zerokms) is optimized for bulk operations. -> -> All the model operations are able to take advantage of this performance for real-world use cases by only making a single call to ZeroKMS regardless of the number of objects you are encrypting or decrypting while still using a unique key for each record. - -#### Encrypting a model - -Use the `encryptModel` method to encrypt a model's fields that are defined in your schema: - -```typescript -import { protectClient } from "./protect"; -import { users } from "./protect/schema"; - -// Your model with plaintext values -const user = { - id: "1", - email: "user@example.com", - address: "123 Main St", - createdAt: new Date("2024-01-01"), -}; - -const encryptedResult = await protectClient.encryptModel(user, users); - -if (encryptedResult.failure) { - // Handle the failure - console.log( - "error when encrypting:", - encryptedResult.failure.type, - encryptedResult.failure.message - ); -} - -const encryptedUser = encryptedResult.data; -console.log("encrypted user:", encryptedUser); -``` - -The `encryptModel` function will only encrypt fields that are defined in your schema. -Other fields (like `id` and `createdAt` in the example above) will remain unchanged. - -#### Type safety with models - -Protect.js provides strong TypeScript support for model operations. -You can specify your model's type to ensure end-to-end type safety: - -```typescript -import { protectClient } from "./protect"; -import { users } from "./protect/schema"; - -// Define your model type -type User = { - id: string; - email: string | null; - address: string | null; - createdAt: Date; - updatedAt: Date; - metadata?: { - preferences?: { - notifications: boolean; - theme: string; - }; - }; -}; - -// The encryptModel method will ensure type safety -const encryptedResult = await protectClient.encryptModel(user, users); - -if (encryptedResult.failure) { - // Handle the failure -} - -const encryptedUser = encryptedResult.data; -// TypeScript knows that encryptedUser matches the User type structure -// but with encrypted fields for those defined in the schema - -// Decryption maintains type safety -const decryptedResult = await protectClient.decryptModel(encryptedUser); - -if (decryptedResult.failure) { - // Handle the failure -} - -const decryptedUser = decryptedResult.data; -// decryptedUser is fully typed as User - -// Bulk operations also support type safety -const bulkEncryptedResult = await protectClient.bulkEncryptModels( - userModels, - users -); - -const bulkDecryptedResult = await protectClient.bulkDecryptModels( - bulkEncryptedResult.data -); -``` - -The type system ensures that: - -- Input models match your defined type structure -- Only fields defined in your schema are encrypted -- Encrypted and decrypted results maintain the correct type structure -- Optional and nullable fields are properly handled -- Nested object structures are preserved -- Additional properties not defined in the schema remain unchanged - -This type safety helps catch potential issues at compile time and provides better IDE support with autocompletion and type hints. - -> [!TIP] -> When using TypeScript with an ORM, you can reuse your ORM's model types directly with Protect.js's model operations. - -Example with Drizzle infered types: - -```typescript -import { protectClient } from "./protect"; -import { jsonb, pgTable, serial, InferSelectModel } from "drizzle-orm/pg-core"; -import { csTable, csColumn } from "@cipherstash/protect"; - -const protectUsers = csTable("users", { - email: csColumn("email"), -}); - -const users = pgTable("users", { - id: serial("id").primaryKey(), - email: jsonb("email").notNull(), -}); - -type User = InferSelectModel; - -const user = { - id: "1", - email: "user@example.com", -}; - -// Drizzle User type works directly with model operations -const encryptedResult = await protectClient.encryptModel( - user, - protectUsers -); -``` - -#### Decrypting a model - -Use the `decryptModel` method to decrypt a model's encrypted fields: - -```typescript -import { protectClient } from "./protect"; - -const decryptedResult = await protectClient.decryptModel(encryptedUser); - -if (decryptedResult.failure) { - // Handle the failure - console.log( - "error when decrypting:", - decryptedResult.failure.type, - decryptedResult.failure.message - ); -} - -const decryptedUser = decryptedResult.data; -console.log("decrypted user:", decryptedUser); -``` - -#### Bulk model operations - -For better performance when working with multiple models, use the `bulkEncryptModels` and `bulkDecryptModels` methods: - -```typescript -import { protectClient } from "./protect"; -import { users } from "./protect/schema"; - -// Array of models with plaintext values -const userModels = [ - { - id: "1", - email: "user1@example.com", - address: "123 Main St", - }, - { - id: "2", - email: "user2@example.com", - address: "456 Oak Ave", - }, -]; - -// Encrypt multiple models at once -const encryptedResult = await protectClient.bulkEncryptModels( - userModels, - users -); - -if (encryptedResult.failure) { - // Handle the failure -} - -const encryptedUsers = encryptedResult.data; - -// Decrypt multiple models at once -const decryptedResult = await protectClient.bulkDecryptModels(encryptedUsers); - -if (decryptedResult.failure) { - // Handle the failure -} - -const decryptedUsers = decryptedResult.data; -``` - -The model encryption methods provide a higher-level interface that's particularly useful when working with ORMs or when you need to encrypt multiple fields in an object. -They automatically handle the mapping between your model's structure and the encrypted fields defined in your schema. - -### Bulk operations - -Protect.js provides direct access to ZeroKMS bulk operations through the `bulkEncrypt` and `bulkDecrypt` methods. These methods are ideal when you need maximum performance and want to handle the correlation between encrypted/decrypted values and your application data manually. - -> [!TIP] -> The bulk operations provide the most direct interface to ZeroKMS's blazing fast bulk encryption and decryption capabilities. Each value gets a unique key while maintaining optimal performance through a single call to ZeroKMS. - -#### Bulk encryption - -Use the `bulkEncrypt` method to encrypt multiple plaintext values at once: - -```typescript -import { protectClient } from "./protect"; -import { users } from "./protect/schema"; - -// Array of plaintext values with optional IDs for correlation -const plaintexts = [ - { id: "user1", plaintext: "alice@example.com" }, - { id: "user2", plaintext: "bob@example.com" }, - { id: "user3", plaintext: "charlie@example.com" }, -]; - -const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { - column: users.email, - table: users, -}); - -if (encryptedResult.failure) { - // Handle the failure - console.log( - "error when bulk encrypting:", - encryptedResult.failure.type, - encryptedResult.failure.message - ); -} - -const encryptedData = encryptedResult.data; -console.log("encrypted data:", encryptedData); -``` - -The `bulkEncrypt` method returns an array of objects with the following structure: - -```typescript -[ - { id: "user1", data: EncryptedPayload }, - { id: "user2", data: EncryptedPayload }, - { id: "user3", data: EncryptedPayload }, -] -``` - -You can also encrypt without IDs if you don't need correlation: - -```typescript -const plaintexts = [ - { plaintext: "alice@example.com" }, - { plaintext: "bob@example.com" }, - { plaintext: "charlie@example.com" }, -]; - -const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { - column: users.email, - table: users, -}); -``` - -#### Bulk decryption - -Use the `bulkDecrypt` method to decrypt multiple encrypted values at once: - -```typescript -import { protectClient } from "./protect"; - -// encryptedData is the result from bulkEncrypt -const decryptedResult = await protectClient.bulkDecrypt(encryptedData); - -if (decryptedResult.failure) { - // Handle the failure - console.log( - "error when bulk decrypting:", - decryptedResult.failure.type, - decryptedResult.failure.message - ); -} - -const decryptedData = decryptedResult.data; -console.log("decrypted data:", decryptedData); -``` - -The `bulkDecrypt` method returns an array of objects with the following structure: - -```typescript -[ - { id: "user1", data: "alice@example.com" }, - { id: "user2", data: "bob@example.com" }, - { id: "user3", data: "charlie@example.com" }, -] -``` - -#### Response structure - -The `bulkDecrypt` method returns a `Result` object that represents the overall operation status. When successful from an HTTP and execution perspective, the `data` field contains an array where each item can have one of two outcomes: - -- **Success**: The item has a `data` field containing the decrypted plaintext -- **Failure**: The item has an `error` field containing a specific error message explaining why that particular decryption failed - -```typescript -// Example response structure -{ - data: [ - { id: "user1", data: "alice@example.com" }, // Success - { id: "user2", error: "Invalid ciphertext format" }, // Failure - { id: "user3", data: "charlie@example.com" }, // Success - ] -} -``` - -> [!NOTE] -> The underlying ZeroKMS response uses HTTP status code 207 (Multi-Status) to indicate that the bulk operation completed, but individual items within the batch may have succeeded or failed. This allows you to handle partial failures gracefully while still processing the successful decryptions. - -You can handle mixed results by checking each item: - -```typescript -const decryptedResult = await protectClient.bulkDecrypt(encryptedData); - -if (decryptedResult.failure) { - // Handle overall operation failure - console.log("Bulk decryption failed:", decryptedResult.failure.message); - return; -} - -// Process individual results -decryptedResult.data.forEach((item) => { - if ('data' in item) { - // Success - item.data contains the decrypted plaintext - console.log(`Decrypted ${item.id}:`, item.data); - } else if ('error' in item) { - // Failure - item.error contains the specific error message - console.log(`Failed to decrypt ${item.id}:`, item.error); - } -}); -``` - -#### Handling null values - -Bulk operations properly handle null values in both encryption and decryption: - -```typescript -const plaintexts = [ - { id: "user1", plaintext: "alice@example.com" }, - { id: "user2", plaintext: null }, - { id: "user3", plaintext: "charlie@example.com" }, -]; - -const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { - column: users.email, - table: users, -}); - -// Null values are preserved in the encrypted result -// encryptedResult.data[1].data will be null - -const decryptedResult = await protectClient.bulkDecrypt(encryptedResult.data); - -// Null values are preserved in the decrypted result -// decryptedResult.data[1].data will be null -``` - -#### Using bulk operations with lock contexts - -Bulk operations support identity-aware encryption through lock contexts: - -```typescript -import { LockContext } from "@cipherstash/protect/identify"; - -const lc = new LockContext(); -const lockContext = await lc.identify(userJwt); - -if (lockContext.failure) { - // Handle the failure -} - -const plaintexts = [ - { id: "user1", plaintext: "alice@example.com" }, - { id: "user2", plaintext: "bob@example.com" }, -]; - -// Encrypt with lock context -const encryptedResult = await protectClient - .bulkEncrypt(plaintexts, { - column: users.email, - table: users, - }) - .withLockContext(lockContext.data); - -// Decrypt with lock context -const decryptedResult = await protectClient - .bulkDecrypt(encryptedResult.data) - .withLockContext(lockContext.data); -``` - -#### Performance considerations - -Bulk operations are optimized for performance and can handle thousands of values efficiently: - -```typescript -// Create a large array of values -const plaintexts = Array.from({ length: 1000 }, (_, i) => ({ - id: `user${i}`, - plaintext: `user${i}@example.com`, -})); - -// Single call to ZeroKMS for all 1000 values -const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { - column: users.email, - table: users, -}); - -// Single call to ZeroKMS for all 1000 values -const decryptedResult = await protectClient.bulkDecrypt(encryptedResult.data); -``` - -The bulk operations maintain the same security guarantees as individual operations - each value gets a unique key - while providing optimal performance through ZeroKMS's bulk processing capabilities. - -### Store encrypted data in a database - -Encrypted data can be stored in any database that supports JSONB, noting that searchable encryption is only supported in PostgreSQL at the moment. - -To store the encrypted data, specify the column type as `jsonb`. - -```sql -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - email jsonb NOT NULL, -); -``` - -#### Searchable encryption in PostgreSQL - -To enable searchable encryption in PostgreSQL, [install the EQL custom types and functions](https://github.com/cipherstash/encrypt-query-language?tab=readme-ov-file#installation). - -1. Download the latest EQL install script: - - ```sh - curl -sLo cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt.sql - ``` - - Using [Supabase](https://supabase.com/)? We ship an EQL release specifically for Supabase. - Download the latest EQL install script: - - ```sh - curl -sLo cipherstash-encrypt-supabase.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt-supabase.sql - ``` - -2. Run this command to install the custom types and functions: - - ```sh - psql -f cipherstash-encrypt.sql - ``` - - or with Supabase: - - ```sh - psql -f cipherstash-encrypt-supabase.sql - ``` - -EQL is now installed in your database and you can enable searchable encryption by adding the `eql_v2_encrypted` type to a column. - -```sql -CREATE TABLE users ( - id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - email eql_v2_encrypted -); -``` - -> [!WARNING] -> The `eql_v2_encrypted` type is a [composite type](https://www.postgresql.org/docs/current/rowtypes.html) and each ORM/client has a different way of handling inserts and selects. -> We've documented how to handle inserts and selects for the different ORMs/clients in the [docs](./docs/reference/working-with-composite-types.md). - -Read more about [how to search encrypted data](./docs/reference/searchable-encryption-postgres.md) in the docs. - -## Identity-aware encryption - -> [!IMPORTANT] -> Right now identity-aware encryption is only supported if you are using [Clerk](https://clerk.com/) as your identity provider. -> Read more about [lock contexts with Clerk and Next.js](./docs/how-to/lock-contexts-with-clerk.md). - -Protect.js can add an additional layer of protection to your data by requiring a valid JWT to perform a decryption. - -This ensures that only the user who encrypted data is able to decrypt it. - -Protect.js does this through a mechanism called a _lock context_. - -### Lock context - -Lock contexts ensure that only specific users can access sensitive data. - -> [!CAUTION] -> You must use the same lock context to encrypt and decrypt data. -> If you use different lock contexts, you will be unable to decrypt the data. - -To use a lock context, initialize a `LockContext` object with the identity claims. - -```typescript -import { LockContext } from "@cipherstash/protect/identify"; - -// protectClient from the previous steps -const lc = new LockContext(); -``` - -> [!NOTE] -> When initializing a `LockContext`, the default context is set to use the `sub` Identity Claim. - -### Identifying a user for a lock context - -A lock context needs to be locked to a user. -To identify the user, call the `identify` method on the lock context object, and pass a valid JWT from a user's session: - -```typescript -const identifyResult = await lc.identify(jwt); - -// The identify method returns the same Result pattern as the encrypt and decrypt methods. -if (identifyResult.failure) { - // Hanlde the failure -} - -const lockContext = identifyResult.data; -``` - -### Encrypting data with a lock context - -To encrypt data with a lock context, call the optional `withLockContext` method on the `encrypt` function and pass the lock context object as a parameter: - -```typescript -import { protectClient } from "./protect"; -import { users } from "./protect/schema"; - -const encryptResult = await protectClient - .encrypt("plaintext", { - table: users, - column: users.email, - }) - .withLockContext(lockContext); - -if (encryptResult.failure) { - // Handle the failure -} - -console.log("EQL Payload containing ciphertexts:", encryptResult.data); -``` - -### Decrypting data with a lock context - -To decrypt data with a lock context, call the optional `withLockContext` method on the `decrypt` function and pass the lock context object as a parameter: - -```typescript -import { protectClient } from "./protect"; - -const decryptResult = await protectClient - .decrypt(encryptResult.data) - .withLockContext(lockContext); - -if (decryptResult.failure) { - // Handle the failure -} - -const plaintext = decryptResult.data; -``` - -### Model encryption with lock context - -All model operations support lock contexts for identity-aware encryption: - -```typescript -import { protectClient } from "./protect"; -import { users } from "./protect/schema"; - -const myUsers = [ - { - id: "1", - email: "user@example.com", - address: "123 Main St", - createdAt: new Date("2024-01-01"), - }, - { - id: "2", - email: "user2@example.com", - address: "456 Oak Ave", - }, -]; - -// Encrypt a model with lock context -const encryptedResult = await protectClient - .encryptModel(myUsers[0], users) - .withLockContext(lockContext); - -if (encryptedResult.failure) { - // Handle the failure -} - -// Decrypt a model with lock context -const decryptedResult = await protectClient - .decryptModel(encryptedResult.data) - .withLockContext(lockContext); - -// Bulk operations also support lock contexts -const bulkEncryptedResult = await protectClient - .bulkEncryptModels(myUsers, users) - .withLockContext(lockContext); - -const bulkDecryptedResult = await protectClient - .bulkDecryptModels(bulkEncryptedResult.data) - .withLockContext(lockContext); -``` - -## Supported data types - -Protect.js currently supports encrypting and decrypting text. -Other data types like booleans, dates, ints, floats, and JSON are well-supported in other CipherStash products, and will be coming to Protect.js soon. - -Until support for other data types are available, you can express interest in this feature by adding a :+1: on this [GitHub Issue](https://github.com/cipherstash/protectjs/issues/48). - -## Searchable encryption - -Read more about [searching encrypted data](./docs/concepts/searchable-encryption.md) in the docs. - -## Multi-tenant encryption - -Protect.js supports multi-tenant encryption by using keysets. -Each keyset is cryptographically isolated from other keysets which esentially means that each tenant has their own unique keyspace. -If you are using a multi-tenant application, you can use keysets to encrypt data for each tenant creating a strong security boundary. - -In the [CipherStash Dashboard](https://dashboard.cipherstash.com/workspaces/_/encryption/keysets), you can create and manage keysets and then use the keyset identifier to encrypt data for each tenant when initializing the Protect.js client. - -```typescript -import { protect } from "@cipherstash/protect"; -import { users } from "./protect/schema"; - -const protectClient = await protect({ - schemas: [users], - keyset: { - // Must be a valid UUID which can be found in the CipherStash Dashboard - id: '123e4567-e89b-12d3-a456-426614174000' - }, -}) - -// or with a keyset name - -const protectClient = await protect({ - schemas: [users], - keyset: { - name: 'Company A' - }, -}) -``` - -> [!IMPORTANT] -> When creating a new keyset, make sure to grant your client access to the keyset or client initialization will fail. -> Read more about [managing keyset access](https://cipherstash.com/docs/platform/workspaces/key-sets). - -## Logging - -> [!TIP] -> `@cipherstash/protect` will NEVER log plaintext data. -> This is by design to prevent sensitive data from leaking into logs. - -`@cipherstash/protect` and `@cipherstash/nextjs` will log to the console with a log level of `info` by default. -To enable the logger, configure the following environment variable: - -```bash -PROTECT_LOG_LEVEL=debug # Enable debug logging -PROTECT_LOG_LEVEL=info # Enable info logging -PROTECT_LOG_LEVEL=error # Enable error logging -``` - -## CipherStash Client - -Protect.js is built on top of the CipherStash Client Rust SDK which is embedded with the `@cipherstash/protect-ffi` package. -The `@cipherstash/protect-ffi` source code is available on [GitHub](https://github.com/cipherstash/protectjs-ffi). - -Read more about configuring the CipherStash Client in the [configuration docs](./docs/reference/configuration.md). - -## Example applications - -Looking for examples of how to use Protect.js? -Check out the [example applications](./examples): - -- [Basic example](/examples/basic) demonstrates how to perform encryption operations -- [Drizzle example](/examples/drizzle) demonstrates how to use Protect.js with an ORM -- [Next.js and lock contexts example using Clerk](/examples/nextjs-clerk) demonstrates how to protect data with identity-aware encryption - -`@cipherstash/protect` can be used with most ORMs. -If you're interested in using `@cipherstash/protect` with a specific ORM, please [create an issue](https://github.com/cipherstash/protectjs/issues/new). - -## Builds and bundling - -`@cipherstash/protect` is a native Node.js module, and relies on native Node.js `require` to load the package. - -Here are a few resources to help based on your tool set: - -- [Required Next.js configuration](./docs/how-to/nextjs-external-packages.md). -- [SST and AWS serverless functions](./docs/how-to/sst-external-packages.md). - -> [!TIP] -> Deploying to Linux (e.g., AWS Lambda) with npm lockfile v3 and seeing runtime module load errors? See the troubleshooting guide: [`docs/how-to/npm-lockfile-v3`](./docs/how-to/npm-lockfile-v3-linux-deployments.md). - -## Contributing - -Please read the [contribution guide](CONTRIBUTE.md). - -## License - -Protect.js is [MIT licensed](./LICENSE.md). - ---- - -### Didn't find what you wanted? - -[Click here to let us know what was missing from our docs.](https://github.com/cipherstash/protectjs/issues/new?template=docs-feedback.yml&title=[Docs:]%20Feedback%20on%20README.md) +This package has been deprecated. Please use [@cipherstash/stash](https://www.npmjs.com/package/@cipherstash/stash) instead to leverage the entire data security stack. \ No newline at end of file diff --git a/packages/protect/__tests__/encrypt-query.test.ts b/packages/protect/__tests__/encrypt-query.test.ts index 0a76b354..580c2095 100644 --- a/packages/protect/__tests__/encrypt-query.test.ts +++ b/packages/protect/__tests__/encrypt-query.test.ts @@ -1,24 +1,26 @@ import 'dotenv/config' -import { describe, expect, it, beforeAll } from 'vitest' -import { protect, ProtectErrorTypes } from '../src' +import { beforeAll, describe, expect, it } from 'vitest' +import { ProtectErrorTypes, protect } from '../src' import type { ProtectClient } from '../src/ffi' import { - users, articles, - products, - metadata, + createFailingMockLockContext, createMockLockContext, createMockLockContextWithNullContext, - createFailingMockLockContext, - unwrapResult, expectFailure, + metadata, + products, + unwrapResult, + users, } from './fixtures' describe('encryptQuery', () => { let protectClient: ProtectClient beforeAll(async () => { - protectClient = await protect({ schemas: [users, articles, products, metadata] }) + protectClient = await protect({ + schemas: [users, articles, products, metadata], + }) }) describe('single value encryption with explicit queryType', () => { @@ -116,7 +118,7 @@ describe('encryptQuery', () => { }, 30000) it('rejects NaN values', async () => { - const result = await protectClient.encryptQuery(NaN, { + const result = await protectClient.encryptQuery(Number.NaN, { column: users.age, table: users, queryType: 'orderAndRange', @@ -126,21 +128,27 @@ describe('encryptQuery', () => { }, 30000) it('rejects Infinity values', async () => { - const result = await protectClient.encryptQuery(Infinity, { - column: users.age, - table: users, - queryType: 'orderAndRange', - }) + const result = await protectClient.encryptQuery( + Number.POSITIVE_INFINITY, + { + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ) expectFailure(result, 'Infinity') }, 30000) it('rejects negative Infinity values', async () => { - const result = await protectClient.encryptQuery(-Infinity, { - column: users.age, - table: users, - queryType: 'orderAndRange', - }) + const result = await protectClient.encryptQuery( + Number.NEGATIVE_INFINITY, + { + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ) expectFailure(result, 'Infinity') }, 30000) @@ -304,11 +312,14 @@ describe('encryptQuery', () => { }, 30000) it('encrypts strings with SQL special characters', async () => { - const result = await protectClient.encryptQuery("'; DROP TABLE users; --", { - column: users.email, - table: users, - queryType: 'equality', - }) + const result = await protectClient.encryptQuery( + "'; DROP TABLE users; --", + { + column: users.email, + table: users, + queryType: 'equality', + }, + ) const data = unwrapResult(result) expect(data).toMatchObject({ @@ -322,9 +333,24 @@ describe('encryptQuery', () => { describe('encryptQuery bulk (array overload)', () => { it('encrypts multiple terms in batch', async () => { const result = await protectClient.encryptQuery([ - { value: 'user@example.com', column: users.email, table: users, queryType: 'equality' }, - { value: 'search term', column: users.bio, table: users, queryType: 'freeTextSearch' }, - { value: 42, column: users.age, table: users, queryType: 'orderAndRange' }, + { + value: 'user@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: 'search term', + column: users.bio, + table: users, + queryType: 'freeTextSearch', + }, + { + value: 42, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, ]) const data = unwrapResult(result) @@ -345,8 +371,18 @@ describe('encryptQuery', () => { it('handles null values in batch', async () => { const result = await protectClient.encryptQuery([ - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality' }, - { value: null, column: users.bio, table: users, queryType: 'freeTextSearch' }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: null, + column: users.bio, + table: users, + queryType: 'freeTextSearch', + }, ]) const data = unwrapResult(result) @@ -371,8 +407,18 @@ describe('encryptQuery', () => { it('rejects NaN/Infinity values in batch', async () => { const result = await protectClient.encryptQuery([ - { value: NaN, column: users.age, table: users, queryType: 'orderAndRange' }, - { value: Infinity, column: users.age, table: users, queryType: 'orderAndRange' }, + { + value: Number.NaN, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + { + value: Number.POSITIVE_INFINITY, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, ]) expect(result.failure).toBeDefined() @@ -380,7 +426,12 @@ describe('encryptQuery', () => { it('rejects negative Infinity in batch', async () => { const result = await protectClient.encryptQuery([ - { value: -Infinity, column: users.age, table: users, queryType: 'orderAndRange' }, + { + value: Number.NEGATIVE_INFINITY, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, ]) expectFailure(result, 'Infinity') @@ -390,11 +441,36 @@ describe('encryptQuery', () => { describe('bulk index preservation', () => { it('preserves exact positions with multiple nulls interspersed', async () => { const result = await protectClient.encryptQuery([ - { value: null, column: users.email, table: users, queryType: 'equality' }, - { value: 'user@example.com', column: users.email, table: users, queryType: 'equality' }, - { value: null, column: users.bio, table: users, queryType: 'freeTextSearch' }, - { value: null, column: users.age, table: users, queryType: 'orderAndRange' }, - { value: 42, column: users.age, table: users, queryType: 'orderAndRange' }, + { + value: null, + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: 'user@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: null, + column: users.bio, + table: users, + queryType: 'freeTextSearch', + }, + { + value: null, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + { + value: 42, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, ]) const data = unwrapResult(result) @@ -411,7 +487,12 @@ describe('encryptQuery', () => { it('handles single-item array', async () => { const result = await protectClient.encryptQuery([ - { value: 'single@example.com', column: users.email, table: users, queryType: 'equality' }, + { + value: 'single@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, ]) const data = unwrapResult(result) @@ -423,9 +504,24 @@ describe('encryptQuery', () => { it('handles all-null array', async () => { const result = await protectClient.encryptQuery([ - { value: null, column: users.email, table: users, queryType: 'equality' }, - { value: null, column: users.bio, table: users, queryType: 'freeTextSearch' }, - { value: null, column: users.age, table: users, queryType: 'orderAndRange' }, + { + value: null, + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: null, + column: users.bio, + table: users, + queryType: 'freeTextSearch', + }, + { + value: null, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, ]) const data = unwrapResult(result) @@ -454,7 +550,12 @@ describe('encryptQuery', () => { it('passes audit metadata for bulk query', async () => { const result = await protectClient .encryptQuery([ - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality' }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, ]) .audit({ metadata: { userId: 'test-user' } }) @@ -466,7 +567,12 @@ describe('encryptQuery', () => { describe('returnType formatting', () => { it('returns Encrypted by default (no returnType)', async () => { const result = await protectClient.encryptQuery([ - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality' }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, ]) const data = unwrapResult(result) @@ -481,7 +587,13 @@ describe('encryptQuery', () => { it('returns composite-literal format when specified', async () => { const result = await protectClient.encryptQuery([ - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality', returnType: 'composite-literal' }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + returnType: 'composite-literal', + }, ]) const data = unwrapResult(result) @@ -494,7 +606,13 @@ describe('encryptQuery', () => { it('returns escaped-composite-literal format when specified', async () => { const result = await protectClient.encryptQuery([ - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality', returnType: 'escaped-composite-literal' }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + returnType: 'escaped-composite-literal', + }, ]) const data = unwrapResult(result) @@ -507,7 +625,13 @@ describe('encryptQuery', () => { it('returns eql format when explicitly specified', async () => { const result = await protectClient.encryptQuery([ - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality', returnType: 'eql' }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + returnType: 'eql', + }, ]) const data = unwrapResult(result) @@ -522,9 +646,26 @@ describe('encryptQuery', () => { it('handles mixed returnType values in same batch', async () => { const result = await protectClient.encryptQuery([ - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality' }, // default - { value: 'search term', column: users.bio, table: users, queryType: 'freeTextSearch', returnType: 'composite-literal' }, - { value: 42, column: users.age, table: users, queryType: 'orderAndRange', returnType: 'escaped-composite-literal' }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, // default + { + value: 'search term', + column: users.bio, + table: users, + queryType: 'freeTextSearch', + returnType: 'composite-literal', + }, + { + value: 42, + column: users.age, + table: users, + queryType: 'orderAndRange', + returnType: 'escaped-composite-literal', + }, ]) const data = unwrapResult(result) @@ -546,9 +687,27 @@ describe('encryptQuery', () => { it('handles returnType with null values', async () => { const result = await protectClient.encryptQuery([ - { value: null, column: users.email, table: users, queryType: 'equality', returnType: 'composite-literal' }, - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality', returnType: 'composite-literal' }, - { value: null, column: users.bio, table: users, queryType: 'freeTextSearch', returnType: 'escaped-composite-literal' }, + { + value: null, + column: users.email, + table: users, + queryType: 'equality', + returnType: 'composite-literal', + }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + returnType: 'composite-literal', + }, + { + value: null, + column: users.bio, + table: users, + queryType: 'freeTextSearch', + returnType: 'escaped-composite-literal', + }, ]) const data = unwrapResult(result) @@ -580,7 +739,12 @@ describe('encryptQuery', () => { const mockLockContext = createMockLockContext() const operation = protectClient.encryptQuery([ - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality' }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, ]) const withContext = operation.withLockContext(mockLockContext as any) @@ -614,8 +778,18 @@ describe('encryptQuery', () => { const mockLockContext = createMockLockContext() const operation = protectClient.encryptQuery([ - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality' }, - { value: 42, column: users.age, table: users, queryType: 'orderAndRange' }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: 42, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, ]) const withContext = operation.withLockContext(mockLockContext as any) @@ -632,7 +806,7 @@ describe('encryptQuery', () => { it('handles LockContext failure gracefully', async () => { const mockLockContext = createFailingMockLockContext( ProtectErrorTypes.CtsTokenError, - 'Mock LockContext failure' + 'Mock LockContext failure', ) const operation = protectClient.encryptQuery('test@example.com', { @@ -644,7 +818,11 @@ describe('encryptQuery', () => { const withContext = operation.withLockContext(mockLockContext as any) const result = await withContext.execute() - expectFailure(result, 'Mock LockContext failure', ProtectErrorTypes.CtsTokenError) + expectFailure( + result, + 'Mock LockContext failure', + ProtectErrorTypes.CtsTokenError, + ) }, 30000) it('handles null value with LockContext', async () => { @@ -670,7 +848,12 @@ describe('encryptQuery', () => { const mockLockContext = createMockLockContextWithNullContext() const operation = protectClient.encryptQuery([ - { value: 'test@example.com', column: users.email, table: users, queryType: 'equality' }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, ]) const withContext = operation.withLockContext(mockLockContext as any) diff --git a/packages/protect/__tests__/fixtures/index.ts b/packages/protect/__tests__/fixtures/index.ts index f681037c..f94c6d97 100644 --- a/packages/protect/__tests__/fixtures/index.ts +++ b/packages/protect/__tests__/fixtures/index.ts @@ -1,5 +1,5 @@ import { csColumn, csTable } from '@cipherstash/schema' -import { vi, expect } from 'vitest' +import { expect, vi } from 'vitest' // ============ Schema Fixtures ============ @@ -78,7 +78,10 @@ export function createMockLockContextWithNullContext() { /** * Creates a mock LockContext that returns a failure */ -export function createFailingMockLockContext(errorType: string, message: string) { +export function createFailingMockLockContext( + errorType: string, + message: string, +) { return { getLockContext: vi.fn().mockResolvedValue({ failure: { type: errorType, message }, @@ -92,7 +95,10 @@ export function createFailingMockLockContext(errorType: string, message: string) * Unwraps a Result type, throwing an error if it's a failure. * Use this to simplify test assertions when you expect success. */ -export function unwrapResult(result: { data?: T; failure?: { message: string } }): T { +export function unwrapResult(result: { + data?: T + failure?: { message: string } +}): T { if (result.failure) { throw new Error(result.failure.message) } @@ -105,7 +111,7 @@ export function unwrapResult(result: { data?: T; failure?: { message: string export function expectFailure( result: { failure?: { message: string; type?: string } }, messagePattern?: string | RegExp, - expectedType?: string + expectedType?: string, ) { expect(result.failure).toBeDefined() if (messagePattern) { diff --git a/packages/protect/__tests__/infer-index-type.test.ts b/packages/protect/__tests__/infer-index-type.test.ts index efb07c94..2ee776d1 100644 --- a/packages/protect/__tests__/infer-index-type.test.ts +++ b/packages/protect/__tests__/infer-index-type.test.ts @@ -1,6 +1,9 @@ -import { describe, expect, it } from 'vitest' import { csColumn, csTable } from '@cipherstash/schema' -import { inferIndexType, validateIndexType } from '../src/ffi/helpers/infer-index-type' +import { describe, expect, it } from 'vitest' +import { + inferIndexType, + validateIndexType, +} from '../src/ffi/helpers/infer-index-type' describe('infer-index-type helpers', () => { const users = csTable('users', { @@ -28,7 +31,9 @@ describe('infer-index-type helpers', () => { }) it('returns match when freeTextSearch and orderAndRange (priority: match > ore)', () => { - const schema = csTable('t', { col: csColumn('col').freeTextSearch().orderAndRange() }) + const schema = csTable('t', { + col: csColumn('col').freeTextSearch().orderAndRange(), + }) expect(inferIndexType(schema.col)).toBe('match') }) @@ -44,7 +49,9 @@ describe('infer-index-type helpers', () => { }) it('throws for unconfigured index type', () => { - expect(() => validateIndexType(users.email, 'match')).toThrow('not configured') + expect(() => validateIndexType(users.email, 'match')).toThrow( + 'not configured', + ) }) }) }) diff --git a/packages/protect/__tests__/jsonb-helpers.test.ts b/packages/protect/__tests__/jsonb-helpers.test.ts index d101ae17..9f12d131 100644 --- a/packages/protect/__tests__/jsonb-helpers.test.ts +++ b/packages/protect/__tests__/jsonb-helpers.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from 'vitest' -import { toJsonPath, buildNestedObject, parseJsonbPath } from '../src' +import { describe, expect, it } from 'vitest' +import { buildNestedObject, parseJsonbPath, toJsonPath } from '../src' describe('toJsonPath', () => { it('converts simple path to JSONPath format', () => { @@ -11,7 +11,9 @@ describe('toJsonPath', () => { }) it('converts deeply nested path', () => { - expect(toJsonPath('user.profile.settings.theme')).toBe('$.user.profile.settings.theme') + expect(toJsonPath('user.profile.settings.theme')).toBe( + '$.user.profile.settings.theme', + ) }) it('returns unchanged if already in JSONPath format', () => { @@ -67,7 +69,9 @@ describe('toJsonPath', () => { }) it('handles deeply nested path after array index', () => { - expect(toJsonPath('data[0].user.profile.settings')).toBe('$.data[0].user.profile.settings') + expect(toJsonPath('data[0].user.profile.settings')).toBe( + '$.data[0].user.profile.settings', + ) }) it('handles root array with nested array', () => { @@ -82,50 +86,50 @@ describe('buildNestedObject', () => { it('builds two-level nested object', () => { expect(buildNestedObject('user.role', 'admin')).toEqual({ - user: { role: 'admin' } + user: { role: 'admin' }, }) }) it('builds deeply nested object', () => { expect(buildNestedObject('a.b.c.d', 'value')).toEqual({ - a: { b: { c: { d: 'value' } } } + a: { b: { c: { d: 'value' } } }, }) }) it('handles numeric values', () => { expect(buildNestedObject('user.age', 30)).toEqual({ - user: { age: 30 } + user: { age: 30 }, }) }) it('handles boolean values', () => { expect(buildNestedObject('user.active', true)).toEqual({ - user: { active: true } + user: { active: true }, }) }) it('handles null values', () => { expect(buildNestedObject('user.data', null)).toEqual({ - user: { data: null } + user: { data: null }, }) }) it('handles object values', () => { const value = { nested: 'object' } expect(buildNestedObject('user.config', value)).toEqual({ - user: { config: { nested: 'object' } } + user: { config: { nested: 'object' } }, }) }) it('handles array values', () => { expect(buildNestedObject('user.tags', ['admin', 'user'])).toEqual({ - user: { tags: ['admin', 'user'] } + user: { tags: ['admin', 'user'] }, }) }) it('strips JSONPath prefix from path', () => { expect(buildNestedObject('$.user.role', 'admin')).toEqual({ - user: { role: 'admin' } + user: { role: 'admin' }, }) }) @@ -134,23 +138,33 @@ describe('buildNestedObject', () => { }) it('throws on root-only path', () => { - expect(() => buildNestedObject('$', 'value')).toThrow('Path must contain at least one segment') + expect(() => buildNestedObject('$', 'value')).toThrow( + 'Path must contain at least one segment', + ) }) it('throws on __proto__ segment', () => { - expect(() => buildNestedObject('__proto__.polluted', 'yes')).toThrow('Path contains forbidden segment: __proto__') + expect(() => buildNestedObject('__proto__.polluted', 'yes')).toThrow( + 'Path contains forbidden segment: __proto__', + ) }) it('throws on prototype segment', () => { - expect(() => buildNestedObject('user.prototype.hack', 'yes')).toThrow('Path contains forbidden segment: prototype') + expect(() => buildNestedObject('user.prototype.hack', 'yes')).toThrow( + 'Path contains forbidden segment: prototype', + ) }) it('throws on constructor segment', () => { - expect(() => buildNestedObject('constructor', 'yes')).toThrow('Path contains forbidden segment: constructor') + expect(() => buildNestedObject('constructor', 'yes')).toThrow( + 'Path contains forbidden segment: constructor', + ) }) it('throws on nested forbidden segment', () => { - expect(() => buildNestedObject('a.b.__proto__', 'yes')).toThrow('Path contains forbidden segment: __proto__') + expect(() => buildNestedObject('a.b.__proto__', 'yes')).toThrow( + 'Path contains forbidden segment: __proto__', + ) }) }) diff --git a/packages/protect/__tests__/supabase.test.ts b/packages/protect/__tests__/supabase.test.ts index 725824fa..ea16deb2 100644 --- a/packages/protect/__tests__/supabase.test.ts +++ b/packages/protect/__tests__/supabase.test.ts @@ -266,7 +266,13 @@ describe('supabase', () => { // Create encrypted query for equality search with composite-literal returnType const encryptedResult = await protectClient.encryptQuery([ - { value: testAge, column: table.age, table: table, queryType: 'equality', returnType: 'composite-literal' }, + { + value: testAge, + column: table.age, + table: table, + queryType: 'equality', + returnType: 'composite-literal', + }, ]) if (encryptedResult.failure) { diff --git a/packages/protect/src/ffi/helpers/infer-index-type.ts b/packages/protect/src/ffi/helpers/infer-index-type.ts index fcda480b..8e1200c0 100644 --- a/packages/protect/src/ffi/helpers/infer-index-type.ts +++ b/packages/protect/src/ffi/helpers/infer-index-type.ts @@ -1,6 +1,6 @@ +import type { ProtectColumn } from '@cipherstash/schema' import type { FfiIndexTypeName, QueryTypeName } from '../../types' import { queryTypeToFfi } from '../../types' -import type { ProtectColumn } from '@cipherstash/schema' /** * Infer the primary index type from a column's configured indexes. @@ -19,14 +19,17 @@ export function inferIndexType(column: ProtectColumn): FfiIndexTypeName { if (indexes.ore) return 'ore' throw new Error( - `Column "${column.getName()}" has no suitable index for scalar queries` + `Column "${column.getName()}" has no suitable index for scalar queries`, ) } /** * Validate that the specified index type is configured on the column */ -export function validateIndexType(column: ProtectColumn, indexType: FfiIndexTypeName): void { +export function validateIndexType( + column: ProtectColumn, + indexType: FfiIndexTypeName, +): void { const config = column.build() const indexes = config.indexes ?? {} @@ -38,7 +41,7 @@ export function validateIndexType(column: ProtectColumn, indexType: FfiIndexType if (!indexMap[indexType]) { throw new Error( - `Index type "${indexType}" is not configured on column "${column.getName()}"` + `Index type "${indexType}" is not configured on column "${column.getName()}"`, ) } } @@ -53,7 +56,7 @@ export function validateIndexType(column: ProtectColumn, indexType: FfiIndexType */ export function resolveIndexType( column: ProtectColumn, - queryType?: QueryTypeName + queryType?: QueryTypeName, ): FfiIndexTypeName { const indexType = queryType ? queryTypeToFfi[queryType] diff --git a/packages/protect/src/ffi/helpers/type-guards.ts b/packages/protect/src/ffi/helpers/type-guards.ts index 2108eb27..86fb2fec 100644 --- a/packages/protect/src/ffi/helpers/type-guards.ts +++ b/packages/protect/src/ffi/helpers/type-guards.ts @@ -5,7 +5,7 @@ import type { ScalarQueryTerm } from '../../types' * Used to discriminate between single value and bulk encryption in encryptQuery overloads. */ export function isScalarQueryTermArray( - value: unknown + value: unknown, ): value is readonly ScalarQueryTerm[] { return ( Array.isArray(value) && diff --git a/packages/protect/src/ffi/helpers/validation.ts b/packages/protect/src/ffi/helpers/validation.ts index c0b21b7b..d544bcf9 100644 --- a/packages/protect/src/ffi/helpers/validation.ts +++ b/packages/protect/src/ffi/helpers/validation.ts @@ -1,5 +1,5 @@ -import { type ProtectError, ProtectErrorTypes } from '../..' import type { Result } from '@byteslice/result' +import { type ProtectError, ProtectErrorTypes } from '../..' import type { FfiIndexTypeName } from '../../types' /** @@ -12,7 +12,7 @@ import type { FfiIndexTypeName } from '../../types' * @internal */ export function validateNumericValue( - value: unknown + value: unknown, ): Result | undefined { if (typeof value === 'number' && Number.isNaN(value)) { return { @@ -60,7 +60,7 @@ export function assertValidNumericValue(value: unknown): void { export function validateValueIndexCompatibility( value: unknown, indexType: FfiIndexTypeName, - columnName: string + columnName: string, ): Result | undefined { if (typeof value === 'number' && indexType === 'match') { return { @@ -84,11 +84,11 @@ export function validateValueIndexCompatibility( export function assertValueIndexCompatibility( value: unknown, indexType: FfiIndexTypeName, - columnName: string + columnName: string, ): void { if (typeof value === 'number' && indexType === 'match') { throw new Error( - `[protect]: Cannot use 'match' index with numeric value on column "${columnName}". The 'freeTextSearch' index only supports string values. Configure the column with 'orderAndRange()' or 'equality()' for numeric queries.` + `[protect]: Cannot use 'match' index with numeric value on column "${columnName}". The 'freeTextSearch' index only supports string values. Configure the column with 'orderAndRange()' or 'equality()' for numeric queries.`, ) } } diff --git a/packages/protect/src/ffi/index.ts b/packages/protect/src/ffi/index.ts index dc92a4c9..72a68762 100644 --- a/packages/protect/src/ffi/index.ts +++ b/packages/protect/src/ffi/index.ts @@ -10,7 +10,6 @@ import { type ProtectError, ProtectErrorTypes } from '..' import { loadWorkSpaceId } from '../../../utils/config' import { logger } from '../../../utils/logger' import { toFfiKeysetIdentifier } from '../helpers' -import { isScalarQueryTermArray } from './helpers/type-guards' import type { BulkDecryptPayload, BulkEncryptPayload, @@ -23,17 +22,18 @@ import type { ScalarQueryTerm, SearchTerm, } from '../types' +import { isScalarQueryTermArray } from './helpers/type-guards' +import { BatchEncryptQueryOperation } from './operations/batch-encrypt-query' import { BulkDecryptOperation } from './operations/bulk-decrypt' import { BulkDecryptModelsOperation } from './operations/bulk-decrypt-models' import { BulkEncryptOperation } from './operations/bulk-encrypt' import { BulkEncryptModelsOperation } from './operations/bulk-encrypt-models' -import { BatchEncryptQueryOperation } from './operations/batch-encrypt-query' import { DecryptOperation } from './operations/decrypt' import { DecryptModelOperation } from './operations/decrypt-model' +import { SearchTermsOperation } from './operations/deprecated/search-terms' import { EncryptOperation } from './operations/encrypt' import { EncryptModelOperation } from './operations/encrypt-model' import { EncryptQueryOperation } from './operations/encrypt-query' -import { SearchTermsOperation } from './operations/deprecated/search-terms' export const noClientError = () => new Error( @@ -239,9 +239,7 @@ export class ProtectClient { * Encrypt multiple values for use in queries (batch operation). * @param terms - Array of query terms to encrypt */ - encryptQuery( - terms: readonly ScalarQueryTerm[], - ): BatchEncryptQueryOperation + encryptQuery(terms: readonly ScalarQueryTerm[]): BatchEncryptQueryOperation encryptQuery( plaintextOrTerms: JsPlaintext | null | readonly ScalarQueryTerm[], @@ -256,8 +254,15 @@ export class ProtectClient { // Handle empty arrays: if opts provided, treat as single value; otherwise batch mode // This maintains backward compatibility for encryptQuery([]) while allowing // encryptQuery([], opts) to encrypt an empty array as a single value - if (Array.isArray(plaintextOrTerms) && plaintextOrTerms.length === 0 && !opts) { - return new BatchEncryptQueryOperation(this.client, [] as readonly ScalarQueryTerm[]) + if ( + Array.isArray(plaintextOrTerms) && + plaintextOrTerms.length === 0 && + !opts + ) { + return new BatchEncryptQueryOperation( + this.client, + [] as readonly ScalarQueryTerm[], + ) } return new EncryptQueryOperation( diff --git a/packages/protect/src/ffi/operations/batch-encrypt-query.ts b/packages/protect/src/ffi/operations/batch-encrypt-query.ts index ba95fb3f..efdb707c 100644 --- a/packages/protect/src/ffi/operations/batch-encrypt-query.ts +++ b/packages/protect/src/ffi/operations/batch-encrypt-query.ts @@ -1,27 +1,31 @@ import { type Result, withResult } from '@byteslice/result' import { type JsPlaintext, - encryptQueryBulk as ffiEncryptQueryBulk, type QueryPayload, + encryptQueryBulk as ffiEncryptQueryBulk, } from '@cipherstash/protect-ffi' +import type { Encrypted as CipherStashEncrypted } from '@cipherstash/protect-ffi' import { type ProtectError, ProtectErrorTypes } from '../..' import { logger } from '../../../../utils/logger' +import { + encryptedToCompositeLiteral, + encryptedToEscapedCompositeLiteral, +} from '../../helpers' import type { Context, LockContext } from '../../identify' -import type { Encrypted as CipherStashEncrypted } from '@cipherstash/protect-ffi' import type { Client, EncryptedQueryResult, ScalarQueryTerm } from '../../types' +import { resolveIndexType } from '../helpers/infer-index-type' +import { + assertValidNumericValue, + assertValueIndexCompatibility, +} from '../helpers/validation' import { noClientError } from '../index' import { ProtectOperation } from './base-operation' -import { resolveIndexType } from '../helpers/infer-index-type' -import { assertValidNumericValue, assertValueIndexCompatibility } from '../helpers/validation' -import { encryptedToCompositeLiteral, encryptedToEscapedCompositeLiteral } from '../../helpers' /** * Separates null values from non-null terms in the input array. * Returns a set of indices where values are null and an array of non-null terms with their original indices. */ -function filterNullTerms( - terms: readonly ScalarQueryTerm[], -): { +function filterNullTerms(terms: readonly ScalarQueryTerm[]): { nullIndices: Set nonNullTerms: { term: ScalarQueryTerm; originalIndex: number }[] } { @@ -53,11 +57,7 @@ function buildQueryPayload( const indexType = resolveIndexType(term.column, term.queryType) // Validate value/index compatibility - assertValueIndexCompatibility( - term.value, - indexType, - term.column.getName() - ) + assertValueIndexCompatibility(term.value, indexType, term.column.getName()) const payload: QueryPayload = { plaintext: term.value as JsPlaintext, @@ -104,7 +104,9 @@ function assembleResults( /** * @internal Use {@link ProtectClient.encryptQuery} with array input instead. */ -export class BatchEncryptQueryOperation extends ProtectOperation { +export class BatchEncryptQueryOperation extends ProtectOperation< + EncryptedQueryResult[] +> { constructor( private client: Client, private terms: readonly ScalarQueryTerm[], @@ -112,11 +114,20 @@ export class BatchEncryptQueryOperation extends ProtectOperation> { + public async execute(): Promise< + Result + > { logger.debug('Encrypting query terms', { count: this.terms.length }) if (this.terms.length === 0) { @@ -135,7 +146,9 @@ export class BatchEncryptQueryOperation extends ProtectOperation buildQueryPayload(term)) + const queries: QueryPayload[] = nonNullTerms.map(({ term }) => + buildQueryPayload(term), + ) const encrypted = await ffiEncryptQueryBulk(this.client, { queries, @@ -155,7 +168,9 @@ export class BatchEncryptQueryOperation extends ProtectOperation { +export class BatchEncryptQueryOperationWithLockContext extends ProtectOperation< + EncryptedQueryResult[] +> { constructor( private client: Client, private terms: readonly ScalarQueryTerm[], @@ -166,8 +181,12 @@ export class BatchEncryptQueryOperationWithLockContext extends ProtectOperation< this.auditMetadata = auditMetadata } - public async execute(): Promise> { - logger.debug('Encrypting query terms with lock context', { count: this.terms.length }) + public async execute(): Promise< + Result + > { + logger.debug('Encrypting query terms with lock context', { + count: this.terms.length, + }) if (this.terms.length === 0) { return { data: [] } @@ -193,7 +212,9 @@ export class BatchEncryptQueryOperationWithLockContext extends ProtectOperation< const { metadata } = this.getAuditData() - const queries: QueryPayload[] = nonNullTerms.map(({ term }) => buildQueryPayload(term, context)) + const queries: QueryPayload[] = nonNullTerms.map(({ term }) => + buildQueryPayload(term, context), + ) const encrypted = await ffiEncryptQueryBulk(this.client, { queries, diff --git a/packages/protect/src/ffi/operations/deprecated/search-terms.ts b/packages/protect/src/ffi/operations/deprecated/search-terms.ts index 2122df22..640284df 100644 --- a/packages/protect/src/ffi/operations/deprecated/search-terms.ts +++ b/packages/protect/src/ffi/operations/deprecated/search-terms.ts @@ -1,18 +1,20 @@ import { type Result, withResult } from '@byteslice/result' -import { encryptQueryBulk, type QueryPayload } from '@cipherstash/protect-ffi' +import { type QueryPayload, encryptQueryBulk } from '@cipherstash/protect-ffi' +import { noClientError } from '../..' import { type ProtectError, ProtectErrorTypes } from '../../..' import { logger } from '../../../../../utils/logger' +import type { LockContext } from '../../../identify' import type { Client, EncryptedSearchTerm, SearchTerm } from '../../../types' -import { noClientError } from '../..' -import { ProtectOperation } from '../base-operation' import { inferIndexType } from '../../helpers/infer-index-type' -import type { LockContext } from '../../../identify' +import { ProtectOperation } from '../base-operation' /** * @deprecated Use `BatchEncryptQueryOperation` instead. * This class is maintained for backward compatibility only. */ -export class SearchTermsOperation extends ProtectOperation { +export class SearchTermsOperation extends ProtectOperation< + EncryptedSearchTerm[] +> { constructor( private client: Client, private terms: SearchTerm[], @@ -20,12 +22,16 @@ export class SearchTermsOperation extends ProtectOperation> { - logger.debug('Creating search terms (deprecated API)', { count: this.terms.length }) + logger.debug('Creating search terms (deprecated API)', { + count: this.terms.length, + }) return await withResult( async () => { @@ -63,7 +69,9 @@ export class SearchTermsOperation extends ProtectOperation { +export class SearchTermsOperationWithLockContext extends ProtectOperation< + EncryptedSearchTerm[] +> { constructor( private operation: SearchTermsOperation, private lockContext: LockContext, @@ -79,7 +87,7 @@ export class SearchTermsOperationWithLockContext extends ProtectOperation { diff --git a/packages/protect/src/ffi/operations/encrypt-query.ts b/packages/protect/src/ffi/operations/encrypt-query.ts index 9b27a360..3b3d9ec0 100644 --- a/packages/protect/src/ffi/operations/encrypt-query.ts +++ b/packages/protect/src/ffi/operations/encrypt-query.ts @@ -6,11 +6,14 @@ import { import { type ProtectError, ProtectErrorTypes } from '../..' import { logger } from '../../../../utils/logger' import type { LockContext } from '../../identify' -import type { Client, Encrypted, EncryptQueryOptions } from '../../types' +import type { Client, EncryptQueryOptions, Encrypted } from '../../types' +import { resolveIndexType } from '../helpers/infer-index-type' +import { + assertValueIndexCompatibility, + validateNumericValue, +} from '../helpers/validation' import { noClientError } from '../index' import { ProtectOperation } from './base-operation' -import { resolveIndexType } from '../helpers/infer-index-type' -import { validateNumericValue, assertValueIndexCompatibility } from '../helpers/validation' /** * @internal Use {@link ProtectClient.encryptQuery} instead. @@ -24,8 +27,16 @@ export class EncryptQueryOperation extends ProtectOperation { super() } - public withLockContext(lockContext: LockContext): EncryptQueryOperationWithLockContext { - return new EncryptQueryOperationWithLockContext(this.client, this.plaintext, this.opts, lockContext, this.auditMetadata) + public withLockContext( + lockContext: LockContext, + ): EncryptQueryOperationWithLockContext { + return new EncryptQueryOperationWithLockContext( + this.client, + this.plaintext, + this.opts, + lockContext, + this.auditMetadata, + ) } public async execute(): Promise> { @@ -50,13 +61,16 @@ export class EncryptQueryOperation extends ProtectOperation { const { metadata } = this.getAuditData() - const indexType = resolveIndexType(this.opts.column, this.opts.queryType) + const indexType = resolveIndexType( + this.opts.column, + this.opts.queryType, + ) // Validate value/index compatibility assertValueIndexCompatibility( this.plaintext, indexType, - this.opts.column.getName() + this.opts.column.getName(), ) return await ffiEncryptQuery(this.client, { @@ -117,13 +131,16 @@ export class EncryptQueryOperationWithLockContext extends ProtectOperation { if (!path) { throw new Error('Path cannot be empty') diff --git a/packages/protect/src/index.ts b/packages/protect/src/index.ts index 4f1f3ef0..aba50627 100644 --- a/packages/protect/src/index.ts +++ b/packages/protect/src/index.ts @@ -100,11 +100,20 @@ export type { EncryptModelOperation } from './ffi/operations/encrypt-model' export type { EncryptOperation } from './ffi/operations/encrypt' // Operations -export { EncryptQueryOperation, EncryptQueryOperationWithLockContext } from './ffi/operations/encrypt-query' -export { BatchEncryptQueryOperation, BatchEncryptQueryOperationWithLockContext } from './ffi/operations/batch-encrypt-query' +export { + EncryptQueryOperation, + EncryptQueryOperationWithLockContext, +} from './ffi/operations/encrypt-query' +export { + BatchEncryptQueryOperation, + BatchEncryptQueryOperationWithLockContext, +} from './ffi/operations/batch-encrypt-query' // Helpers -export { inferIndexType, validateIndexType } from './ffi/helpers/infer-index-type' +export { + inferIndexType, + validateIndexType, +} from './ffi/helpers/infer-index-type' // Types export type { diff --git a/packages/protect/src/types.ts b/packages/protect/src/types.ts index d7d174f4..cae2516b 100644 --- a/packages/protect/src/types.ts +++ b/packages/protect/src/types.ts @@ -170,7 +170,7 @@ export const queryTypeToFfi: Record = { export type QueryTermBase = { column: ProtectColumn table: ProtectTable - queryType?: QueryTypeName // Optional - auto-infers if omitted + queryType?: QueryTypeName // Optional - auto-infers if omitted /** * The format for the returned encrypted value: * - `'eql'` (default) - Returns raw Encrypted object diff --git a/packages/schema/README.md b/packages/schema/README.md index b444bc85..0e1c5e0b 100644 --- a/packages/schema/README.md +++ b/packages/schema/README.md @@ -1,10 +1,10 @@ # @cipherstash/schema -A TypeScript schema builder for CipherStash Protect.js that enables you to define encryption schemas with searchable encryption capabilities. +A TypeScript schema builder for the Stash Stack that enables you to define encryption schemas with searchable encryption capabilities. ## Overview -`@cipherstash/schema` is a standalone package that provides the schema building functionality used by `@cipherstash/protect`. While not required for basic Protect.js usage, this package is available if you need to build encryption configuration schemas directly or want to understand the underlying schema structure. +`@cipherstash/schema` is a standalone package that provides the schema building functionality used by `@cipherstash/stack`. While not required for basic Stash Encryption usage, this package is available if you need to build encryption configuration schemas directly or want to understand the underlying schema structure. ## Installation @@ -19,13 +19,13 @@ pnpm add @cipherstash/schema ## Quick Start ```typescript -import { csTable, csColumn, buildEncryptConfig } from '@cipherstash/schema' +import { encryptedTable, encryptedColumn, buildEncryptConfig } from '@cipherstash/schema' // Define your schema -const users = csTable('users', { - email: csColumn('email').freeTextSearch().equality().orderAndRange(), - name: csColumn('name').freeTextSearch(), - age: csColumn('age').orderAndRange(), +const users = encryptedTable('users', { + email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + name: encryptedColumn('name').freeTextSearch(), + age: encryptedColumn('age').orderAndRange(), }) // Build the encryption configuration @@ -35,48 +35,48 @@ console.log(config) ## Core Functions -### `csTable(tableName, columns)` +### `encryptedTable(tableName, columns)` Creates a table definition with encrypted columns. ```typescript -import { csTable, csColumn } from '@cipherstash/schema' +import { encryptedTable, encryptedColumn } from '@cipherstash/schema' -const users = csTable('users', { - email: csColumn('email'), - name: csColumn('name'), +const users = encryptedTable('users', { + email: encryptedColumn('email'), + name: encryptedColumn('name'), }) ``` -### `csColumn(columnName)` +### `encryptedColumn(columnName)` Creates a column definition with configurable indexes and data types. ```typescript -import { csColumn } from '@cipherstash/schema' +import { encryptedColumn } from '@cipherstash/schema' -const emailColumn = csColumn('email') +const emailColumn = encryptedColumn('email') .freeTextSearch() // Enable text search .equality() // Enable exact matching .orderAndRange() // Enable sorting and range queries .dataType('string') // Set data type ``` -### `csValue(valueName)` +### `encryptedValue(valueName)` Creates a value definition for nested objects (up to 3 levels deep). ```typescript -import { csTable, csColumn, csValue } from '@cipherstash/schema' +import { encryptedTable, encryptedColumn, encryptedValue } from '@cipherstash/schema' -const users = csTable('users', { - email: csColumn('email').equality(), +const users = encryptedTable('users', { + email: encryptedColumn('email').equality(), profile: { - name: csValue('profile.name'), + name: encryptedValue('profile.name'), address: { - street: csValue('profile.address.street'), + street: encryptedValue('profile.address.street'), location: { - coordinates: csValue('profile.address.location.coordinates'), + coordinates: encryptedValue('profile.address.location.coordinates'), }, }, }, @@ -100,7 +100,7 @@ const config = buildEncryptConfig(users, orders, products) Enables exact matching queries. ```typescript -const emailColumn = csColumn('email').equality() +const emailColumn = encryptedColumn('email').equality() // SQL equivalent: WHERE email = 'example@example.com' ``` @@ -109,7 +109,7 @@ const emailColumn = csColumn('email').equality() Enables text search with configurable options. ```typescript -const descriptionColumn = csColumn('description').freeTextSearch({ +const descriptionColumn = encryptedColumn('description').freeTextSearch({ tokenizer: { kind: 'ngram', token_length: 3 }, token_filters: [{ kind: 'downcase' }], k: 6, @@ -124,7 +124,7 @@ const descriptionColumn = csColumn('description').freeTextSearch({ Enables sorting and range queries. ```typescript -const priceColumn = csColumn('price').orderAndRange() +const priceColumn = encryptedColumn('price').orderAndRange() // SQL equivalent: ORDER BY price ASC, WHERE price > 100 ``` @@ -133,7 +133,7 @@ const priceColumn = csColumn('price').orderAndRange() Set the data type for a column using `.dataType()`: ```typescript -const column = csColumn('field') +const column = encryptedColumn('field') .dataType('string') // text (default) .dataType('number') // Javascript number (i.e. integer or float) .dataType('jsonb') // JSON binary @@ -144,15 +144,15 @@ const column = csColumn('field') Support for nested object encryption (up to 3 levels deep): ```typescript -const users = csTable('users', { - email: csColumn('email').equality(), +const users = encryptedTable('users', { + email: encryptedColumn('email').equality(), profile: { - name: csValue('profile.name'), + name: encryptedValue('profile.name'), address: { - street: csValue('profile.address.street'), - city: csValue('profile.address.city'), + street: encryptedValue('profile.address.street'), + city: encryptedValue('profile.address.city'), location: { - coordinates: csValue('profile.address.location.coordinates'), + coordinates: encryptedValue('profile.address.location.coordinates'), }, }, }, @@ -166,7 +166,7 @@ const users = csTable('users', { ### Custom Token Filters ```typescript -const column = csColumn('field').equality([ +const column = encryptedColumn('field').equality([ { kind: 'downcase' } ]) ``` @@ -174,7 +174,7 @@ const column = csColumn('field').equality([ ### Custom Match Options ```typescript -const column = csColumn('field').freeTextSearch({ +const column = encryptedColumn('field').freeTextSearch({ tokenizer: { kind: 'standard' }, token_filters: [{ kind: 'downcase' }], k: 8, @@ -188,29 +188,29 @@ const column = csColumn('field').freeTextSearch({ The schema builder provides full TypeScript support: ```typescript -import { csTable, csColumn, type ProtectTableColumn } from '@cipherstash/schema' +import { encryptedTable, encryptedColumn, type EncryptedTableColumn } from '@cipherstash/schema' -const users = csTable('users', { - email: csColumn('email').equality(), - name: csColumn('name').freeTextSearch(), +const users = encryptedTable('users', { + email: encryptedColumn('email').equality(), + name: encryptedColumn('name').freeTextSearch(), } as const) // TypeScript will infer the correct types type UsersTable = typeof users ``` -## Integration with Protect.js +## Integration with Stash Encryption -While this package can be used standalone, it's typically used through `@cipherstash/protect`: +While this package can be used standalone, it's typically used through `@cipherstash/stack`: ```typescript -import { csTable, csColumn } from '@cipherstash/protect' +import { encryptedTable, encryptedColumn, Encryption } from '@cipherstash/stack' -const users = csTable('users', { - email: csColumn('email').equality().freeTextSearch(), +const users = encryptedTable('users', { + email: encryptedColumn('email').equality().freeTextSearch(), }) -const protectClient = await protect({ +const client = await Encryption({ schemas: [users], }) ``` @@ -245,14 +245,14 @@ The `buildEncryptConfig` function generates a configuration object like this: ## Use Cases -- **Standalone schema building**: When you need to generate encryption configurations outside of Protect.js +- **Standalone schema building**: When you need to generate encryption configurations outside of Stash Encryption - **Custom tooling**: Building tools that work with CipherStash encryption schemas -- **Schema validation**: Validating schema structures before using them with Protect.js +- **Schema validation**: Validating schema structures before using them with Stash Encryption - **Documentation generation**: Creating documentation from schema definitions ## API Reference -### `csTable(tableName: string, columns: ProtectTableColumn)` +### `encryptedTable(tableName: string, columns: EncryptionTableColumn)` Creates a table definition. @@ -260,16 +260,16 @@ Creates a table definition. - `tableName`: The name of the table in the database - `columns`: Object defining the columns and their configurations -**Returns:** `ProtectTable & T` +**Returns:** `EncryptionTable & T` -### `csColumn(columnName: string)` +### `encryptedColumn(columnName: string)` Creates a column definition. **Parameters:** - `columnName`: The name of the column in the database -**Returns:** `ProtectColumn` +**Returns:** `EncryptionColumn` **Methods:** - `.dataType(castAs: CastAs)`: Set the data type @@ -278,26 +278,26 @@ Creates a column definition. - `.orderAndRange()`: Enable order and range index - `.searchableJson()`: Enable searchable JSON index -### `csValue(valueName: string)` +### `encryptedValue(valueName: string)` Creates a value definition for nested objects. **Parameters:** - `valueName`: Dot-separated path to the value (e.g., 'profile.name') -**Returns:** `ProtectValue` +**Returns:** `EncryptionValue` **Methods:** - `.dataType(castAs: CastAs)`: Set the data type -### `buildEncryptConfig(...tables: ProtectTable[])` +### `buildEncryptConfig(...tables: EncryptionTable[])` Builds the encryption configuration. **Parameters:** - `...tables`: Variable number of table definitions -**Returns:** `EncryptConfig` +**Returns:** `EncryptionConfigSchema` ## License diff --git a/packages/schema/__tests__/searchable-json.test.ts b/packages/schema/__tests__/searchable-json.test.ts index 4e94d920..8f14ec7c 100644 --- a/packages/schema/__tests__/searchable-json.test.ts +++ b/packages/schema/__tests__/searchable-json.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from 'vitest' -import { buildEncryptConfig, csTable, csColumn } from '../src/index' +import { describe, expect, it } from 'vitest' +import { buildEncryptConfig, csColumn, csTable } from '../src/index' describe('searchableJson()', () => { it('sets cast_as to json and ste_vec marker on column build', () => { @@ -19,26 +19,28 @@ describe('searchableJson()', () => { describe('ProtectTable.build() with searchableJson', () => { it('transforms prefix to table/column format', () => { const users = csTable('users', { - metadata: csColumn('metadata').searchableJson() + metadata: csColumn('metadata').searchableJson(), }) const built = users.build() expect(built.columns.metadata.cast_as).toBe('json') - expect(built.columns.metadata.indexes.ste_vec?.prefix).toBe('users/metadata') + expect(built.columns.metadata.indexes.ste_vec?.prefix).toBe( + 'users/metadata', + ) }) }) describe('buildEncryptConfig with searchableJson', () => { it('emits ste_vec index with table/column prefix', () => { const users = csTable('users', { - metadata: csColumn('metadata').searchableJson() + metadata: csColumn('metadata').searchableJson(), }) const config = buildEncryptConfig(users) expect(config.tables.users.metadata.cast_as).toBe('json') expect(config.tables.users.metadata.indexes.ste_vec?.prefix).toBe( - 'users/metadata' + 'users/metadata', ) }) }) diff --git a/packages/schema/package.json b/packages/schema/package.json index 5ea65575..0e2f212f 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,17 +3,20 @@ "version": "2.0.2", "description": "CipherStash schema builder for TypeScript", "keywords": [ - "encrypted", - "protect", + "encryption", + "kms", + "typescript", + "searchable-encryption", + "zero-trust", "schema", "builder" ], "bugs": { - "url": "https://github.com/cipherstash/protectjs/issues" + "url": "https://github.com/cipherstash/stack/issues" }, "repository": { "type": "git", - "url": "git+https://github.com/cipherstash/protectjs.git" + "url": "git+https://github.com/cipherstash/stack.git" }, "license": "MIT", "author": "CipherStash ", diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index dcc11c8c..66404297 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -101,17 +101,17 @@ export type UniqueIndexOpts = z.infer export type OreIndexOpts = z.infer export type ColumnSchema = z.infer -export type ProtectTableColumn = { +export type EncryptionTableColumn = { [key: string]: - | ProtectColumn + | EncryptionColumn | { [key: string]: - | ProtectValue + | EncryptionValue | { [key: string]: - | ProtectValue + | EncryptionValue | { - [key: string]: ProtectValue + [key: string]: EncryptionValue } } } @@ -121,7 +121,7 @@ export type EncryptConfig = z.infer // ------------------------ // Interface definitions // ------------------------ -export class ProtectValue { +export class EncryptionValue { private valueName: string private castAsValue: CastAs @@ -150,7 +150,7 @@ export class ProtectValue { } } -export class ProtectColumn { +export class EncryptionColumn { private columnName: string private castAsValue: CastAs private indexesValue: { @@ -238,7 +238,7 @@ interface TableDefinition { columns: Record } -export class ProtectTable { +export class EncryptionTable { constructor( public readonly tableName: string, private readonly columnBuilders: T, @@ -252,19 +252,22 @@ export class ProtectTable { const processColumn = ( builder: - | ProtectColumn + | EncryptionColumn | Record< string, - | ProtectValue + | EncryptionValue | Record< string, - | ProtectValue - | Record> + | EncryptionValue + | Record< + string, + EncryptionValue | Record + > > >, colName: string, ) => { - if (builder instanceof ProtectColumn) { + if (builder instanceof EncryptionColumn) { const builtColumn = builder.build() // Hanlde building the ste_vec index for JSON columns so users don't have to pass the prefix. @@ -286,7 +289,7 @@ export class ProtectTable { } } else { for (const [key, value] of Object.entries(builder)) { - if (value instanceof ProtectValue) { + if (value instanceof EncryptionValue) { builtColumns[value.getName()] = value.build() } else { processColumn(value, key) @@ -306,43 +309,74 @@ export class ProtectTable { } } +// ------------------------ +// New public type aliases +// ------------------------ +export type EncryptedTable = EncryptionTable +export type EncryptedColumn = EncryptionColumn +export type EncryptedValue = EncryptionValue +export type EncryptedTableColumn = EncryptionTableColumn + +// ------------------------ +// Deprecated Protect* type aliases (backward compat) +// ------------------------ +/** @deprecated Use EncryptedTable */ +export type ProtectTable = EncryptionTable +/** @deprecated Use EncryptedColumn */ +export type ProtectColumn = EncryptionColumn +/** @deprecated Use EncryptedValue */ +export type ProtectValue = EncryptionValue +/** @deprecated Use EncryptedTableColumn */ +export type ProtectTableColumn = EncryptionTableColumn + // ------------------------ // User facing functions // ------------------------ -export function csTable( +export function encryptedTable( tableName: string, columns: T, -): ProtectTable & T { - const tableBuilder = new ProtectTable(tableName, columns) as ProtectTable & - T +): EncryptionTable & T { + const tableBuilder = new EncryptionTable( + tableName, + columns, + ) as EncryptionTable & T for (const [colName, colBuilder] of Object.entries(columns)) { - ;(tableBuilder as ProtectTableColumn)[colName] = colBuilder + ;(tableBuilder as EncryptionTableColumn)[colName] = colBuilder } return tableBuilder } -export function csColumn(columnName: string) { - return new ProtectColumn(columnName) +/** @deprecated Use encryptedTable */ +export const csTable = encryptedTable + +export function encryptedColumn(columnName: string) { + return new EncryptionColumn(columnName) } -export function csValue(valueName: string) { - return new ProtectValue(valueName) +/** @deprecated Use encryptedColumn */ +export const csColumn = encryptedColumn + +export function encryptedValue(valueName: string) { + return new EncryptionValue(valueName) } +/** @deprecated Use encryptedValue */ +export const csValue = encryptedValue + // ------------------------ // Internal functions // ------------------------ export function buildEncryptConfig( - ...protectTables: Array> + ...encryptionTables: Array> ): EncryptConfig { const config: EncryptConfig = { v: 2, tables: {}, } - for (const tb of protectTables) { + for (const tb of encryptionTables) { const tableDef = tb.build() config.tables[tableDef.tableName] = tableDef.columns } diff --git a/packages/stack/.npmignore b/packages/stack/.npmignore new file mode 100644 index 00000000..3490e24d --- /dev/null +++ b/packages/stack/.npmignore @@ -0,0 +1,5 @@ +.env +.turbo +node_modules +cipherstash.secret.toml +cipherstash.toml \ No newline at end of file diff --git a/packages/stack/README.md b/packages/stack/README.md new file mode 100644 index 00000000..2f876cba --- /dev/null +++ b/packages/stack/README.md @@ -0,0 +1,1094 @@ +

+ CipherStash Logo +
+ Stash Encryption +

+

+ End-to-end field level encryption for JavaScript/TypeScript apps with zero-knowledge key management. Search encrypted data without decrypting it. +
+

+

+
Please star this repo if you find it useful!
+
+ + + +Stash Encryption lets you encrypt every value with its own key--without sacrificing performance or usability. Encryption happens in your app; ciphertext is stored in your database. + +Per-value unique keys are powered by CipherStash [ZeroKMS](https://cipherstash.com/products/zerokms) bulk key operations, backed by a root key in [AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html). + +Encrypted data is structured as an [EQL](https://github.com/cipherstash/encrypt-query-language) JSON payload and can be stored in any database that supports JSONB. + +> [!IMPORTANT] +> Searching, sorting, and filtering on encrypted data is currently only supported when storing encrypted data in PostgreSQL. +> Read more about [searching encrypted data](./docs/concepts/searchable-encryption.md). + +Looking for DynamoDB support? Check out the [DynamoDB helper library](https://www.npmjs.com/package/@cipherstash/protect-dynamodb). + +## Quick start (60 seconds) + +Create an account and workspace in the [CipherStash dashboard](https://cipherstash.com/signup), then follow the onboarding guide to generate your client credentials and store them in your `.env` file. + +Install the package: + +```bash +npm install @cipherstash/stack +``` + +Start encrypting data: + +```ts +import { Encryption } from "@cipherstash/stack"; +import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; + +// 1) Define a schema +const users = encryptedTable("users", { email: encryptedColumn("email") }); + +// 2) Create a client (requires CS_* env vars) +const client = await Encryption({ schemas: [users] }); + +// 3) Encrypt -> store JSONB payload +const encrypted = await client.encrypt("alice@example.com", { + table: users, + column: users.email, +}); + +if (encrypted.failure) { + // You decide how to handle the failure and the user experience +} + +// 4) Decrypt later +const decrypted = await client.decrypt(encrypted.data); +``` + +> [!NOTE] +> **Migrating from `@cipherstash/protect`?** See the [migration guide](./MIGRATION.md) for a complete list of renames and how to update your code. +> All old names (`protect`, `csTable`, `csColumn`, `ProtectClient`, etc.) are still available as deprecated aliases. + +## Architecture (high level) + +![Stash Encryption Architecture Diagram](https://github.com/cipherstash/protectjs/blob/main/docs/images/protectjs-architecture.png) + +## Table of contents + +- [Quick start (60 seconds)](#quick-start-60-seconds) +- [Architecture (high level)](#architecture-high-level) +- [Features](#features) +- [Installation](#installation) +- [Getting started](#getting-started) +- [Identity-aware encryption](#identity-aware-encryption) +- [Supported data types](#supported-data-types) +- [Searchable encryption](#searchable-encryption) +- [Multi-tenant encryption](#multi-tenant-encryption) +- [Logging](#logging) +- [CipherStash Client](#cipherstash-client) +- [Example applications](#example-applications) +- [Builds and bundling](#builds-and-bundling) +- [Contributing](#contributing) +- [License](#license) + +For more specific documentation, refer to the [docs](https://github.com/cipherstash/protectjs/tree/main/docs). + +## Features + +Stash Encryption protects data in using industry-standard AES encryption. +Stash Encryption uses [ZeroKMS](https://cipherstash.com/products/zerokms) for bulk encryption and decryption operations. +This enables every encrypted value, in every column, in every row in your database to have a unique key -- without sacrificing performance. + +**Features:** + +- **Bulk encryption and decryption**: Stash Encryption uses [ZeroKMS](https://cipherstash.com/products/zerokms) for encrypting and decrypting thousands of records at once, while using a unique key for every value. +- **Single item encryption and decryption**: Just looking for a way to encrypt and decrypt single values? Stash Encryption has you covered. +- **Really fast:** ZeroKMS's performance makes using millions of unique keys feasible and performant for real-world applications built with Stash Encryption. +- **Identity-aware encryption**: Lock down access to sensitive data by requiring a valid JWT to perform a decryption. +- **Audit trail**: Every decryption event will be logged in ZeroKMS to help you prove compliance. +- **Searchable encryption**: Stash Encryption supports searching encrypted data in PostgreSQL. +- **TypeScript support**: Strongly typed with TypeScript interfaces and types. + +**Use cases:** + +- **Trusted data access**: make sure only your end-users can access their sensitive data stored in your product. +- **Meet compliance requirements faster:** meet and exceed the data encryption requirements of SOC2 and ISO27001. +- **Reduce the blast radius of data breaches:** limit the impact of exploited vulnerabilities to only the data your end-users can decrypt. + +## Installation + +Install the [`@cipherstash/stack` package](https://www.npmjs.com/package/@cipherstash/stack) with your package manager of choice: + +```bash +npm install @cipherstash/stack +# or +yarn add @cipherstash/stack +# or +pnpm add @cipherstash/stack +``` + +> [!TIP] +> [Bun](https://bun.sh/) is not currently supported due to a lack of [Node-API compatibility](https://github.com/oven-sh/bun/issues/158). Under the hood, Stash Encryption uses [CipherStash Client](#cipherstash-client) which is written in Rust and embedded using [Neon](https://github.com/neon-bindings/neon). + +### Opt-out of bundling + +> [!IMPORTANT] +> **You need to opt-out of bundling when using Stash Encryption.** + +Stash Encryption uses Node.js specific features and requires the use of the [native Node.js `require`](https://nodejs.org/api/modules.html#requireid). + +When using Stash Encryption, you need to opt-out of bundling for tools like [Webpack](https://webpack.js.org/configuration/externals/), [esbuild](https://webpack.js.org/configuration/externals/), or [Next.js](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages). + +Read more about [building and bundling with Stash Encryption](#builds-and-bundling). + +## Getting started + +- **Existing app?** Skip to [the next step](#configuration). +- **Clean slate?** Check out the [getting started tutorial](./docs/getting-started.md). + +### Configuration + +If you haven't already, sign up for a [CipherStash account](https://cipherstash.com/signup). +Once you have an account, you will create a Workspace which is scoped to your application environment. + +Follow the onboarding steps to get your first set of credentials required to use Stash Encryption. +By the end of the onboarding, you will have the following environment variables: + +```bash +CS_WORKSPACE_CRN= # The workspace identifier +CS_CLIENT_ID= # The client identifier +CS_CLIENT_KEY= # The client key which is used as key material in combination with ZeroKMS +CS_CLIENT_ACCESS_KEY= # The API key used for authenticating with the CipherStash API +``` + +Save these environment variables to a `.env` file in your project. + +### Basic file structure + +The following is the basic file structure of the project. +In the `src/protect/` directory, we have the table definition in `schema.ts` and the protect client in `index.ts`. + +``` + + src + protect + index.ts + schema.ts + index.ts + .env + cipherstash.toml + cipherstash.secret.toml + package.json + tsconfig.json +``` + +### Define your schema + +Stash Encryption uses a schema to define the tables and columns that you want to encrypt and decrypt. + +Define your tables and columns by adding this to `src/protect/schema.ts`: + +```ts +import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; + +export const users = encryptedTable("users", { + email: encryptedColumn("email"), +}); + +export const orders = encryptedTable("orders", { + address: encryptedColumn("address"), +}); +``` + +**Searchable encryption:** + +If you want to search encrypted data in your PostgreSQL database, you must declare the indexes in schema in `src/protect/schema.ts`: + +```ts +import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; + +export const users = encryptedTable("users", { + email: encryptedColumn("email").freeTextSearch().equality().orderAndRange(), +}); + +export const orders = encryptedTable("orders", { + address: encryptedColumn("address"), +}); +``` + +Read more about [defining your schema](./docs/reference/schema.md). + +### Initialize the encryption client + +To import the `Encryption` function and initialize a client with your defined schema, add the following to `src/protect/index.ts`: + +```ts +import { Encryption, type EncryptionClientConfig } from "@cipherstash/stack"; +import { users, orders } from "./schema"; + +const config: EncryptionClientConfig = { + schemas: [users, orders], +} + +// Pass all your tables to the Encryption function to initialize the client +export const protectClient = await Encryption(config); +``` + +The `Encryption` function requires at least one `encryptedTable` be provided in the `schemas` array. + +### Encrypt data + +Stash Encryption provides the `encrypt` function on `protectClient` to encrypt data. +`encrypt` takes a plaintext string, and an object with the table and column as parameters. + +To start encrypting data, add the following to `src/index.ts`: + +```typescript +import { users } from "./protect/schema"; +import { protectClient } from "./protect"; + +const encryptResult = await protectClient.encrypt("secret@squirrel.example", { + column: users.email, + table: users, +}); + +if (encryptResult.failure) { + // Handle the failure + console.log( + "error when encrypting:", + encryptResult.failure.type, + encryptResult.failure.message + ); +} + +console.log("EQL Payload containing ciphertexts:", encryptResult.data); +``` + +The `encrypt` function will return a `Result` object with either a `data` key, or a `failure` key. +The `encryptResult` will return one of the following: + +```typescript +// Success +{ + data: EncryptedPayload +} + +// Failure +{ + failure: { + type: 'EncryptionError', + message: 'A message about the error' + } +} +``` + +### Decrypt data + +Stash Encryption provides the `decrypt` function on `protectClient` to decrypt data. +`decrypt` takes an encrypted data object as a parameter. + +To start decrypting data, add the following to `src/index.ts`: + +```typescript +import { protectClient } from "./protect"; + +// encryptResult is the EQL payload from the previous step +const decryptResult = await protectClient.decrypt(encryptResult.data); + +if (decryptResult.failure) { + // Handle the failure + console.log( + "error when decrypting:", + decryptResult.failure.type, + decryptResult.failure.message + ); +} + +const plaintext = decryptResult.data; +console.log("plaintext:", plaintext); +``` + +The `decrypt` function returns a `Result` object with either a `data` key, or a `failure` key. +The `decryptResult` will return one of the following: + +```typescript +// Success +{ + data: 'secret@squirrel.example' +} + +// Failure +{ + failure: { + type: 'DecryptionError', + message: 'A message about the error' + } +} +``` + +### Working with models and objects + +Stash Encryption provides model-level encryption methods that make it easy to encrypt and decrypt entire objects. +These methods automatically handle the encryption of fields defined in your schema. + +If you are working with a large data set, the model operations are significantly faster than encrypting and decrypting individual objects as they are able to perform bulk operations. + +> [!TIP] +> CipherStash [ZeroKMS](https://cipherstash.com/products/zerokms) is optimized for bulk operations. +> +> All the model operations are able to take advantage of this performance for real-world use cases by only making a single call to ZeroKMS regardless of the number of objects you are encrypting or decrypting while still using a unique key for each record. + +#### Encrypting a model + +Use the `encryptModel` method to encrypt a model's fields that are defined in your schema: + +```typescript +import { protectClient } from "./protect"; +import { users } from "./protect/schema"; + +// Your model with plaintext values +const user = { + id: "1", + email: "user@example.com", + address: "123 Main St", + createdAt: new Date("2024-01-01"), +}; + +const encryptedResult = await protectClient.encryptModel(user, users); + +if (encryptedResult.failure) { + // Handle the failure + console.log( + "error when encrypting:", + encryptedResult.failure.type, + encryptedResult.failure.message + ); +} + +const encryptedUser = encryptedResult.data; +console.log("encrypted user:", encryptedUser); +``` + +The `encryptModel` function will only encrypt fields that are defined in your schema. +Other fields (like `id` and `createdAt` in the example above) will remain unchanged. + +#### Type safety with models + +Stash Encryption provides strong TypeScript support for model operations. +You can specify your model's type to ensure end-to-end type safety: + +```typescript +import { protectClient } from "./protect"; +import { users } from "./protect/schema"; + +// Define your model type +type User = { + id: string; + email: string | null; + address: string | null; + createdAt: Date; + updatedAt: Date; + metadata?: { + preferences?: { + notifications: boolean; + theme: string; + }; + }; +}; + +// The encryptModel method will ensure type safety +const encryptedResult = await protectClient.encryptModel(user, users); + +if (encryptedResult.failure) { + // Handle the failure +} + +const encryptedUser = encryptedResult.data; +// TypeScript knows that encryptedUser matches the User type structure +// but with encrypted fields for those defined in the schema + +// Decryption maintains type safety +const decryptedResult = await protectClient.decryptModel(encryptedUser); + +if (decryptedResult.failure) { + // Handle the failure +} + +const decryptedUser = decryptedResult.data; +// decryptedUser is fully typed as User + +// Bulk operations also support type safety +const bulkEncryptedResult = await protectClient.bulkEncryptModels( + userModels, + users +); + +const bulkDecryptedResult = await protectClient.bulkDecryptModels( + bulkEncryptedResult.data +); +``` + +The type system ensures that: + +- Input models match your defined type structure +- Only fields defined in your schema are encrypted +- Encrypted and decrypted results maintain the correct type structure +- Optional and nullable fields are properly handled +- Nested object structures are preserved +- Additional properties not defined in the schema remain unchanged + +This type safety helps catch potential issues at compile time and provides better IDE support with autocompletion and type hints. + +> [!TIP] +> When using TypeScript with an ORM, you can reuse your ORM's model types directly with Stash Encryption's model operations. + +Example with Drizzle infered types: + +```typescript +import { protectClient } from "./protect"; +import { jsonb, pgTable, serial, InferSelectModel } from "drizzle-orm/pg-core"; +import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; + +const protectUsers = encryptedTable("users", { + email: encryptedColumn("email"), +}); + +const users = pgTable("users", { + id: serial("id").primaryKey(), + email: jsonb("email").notNull(), +}); + +type User = InferSelectModel; + +const user = { + id: "1", + email: "user@example.com", +}; + +// Drizzle User type works directly with model operations +const encryptedResult = await protectClient.encryptModel( + user, + protectUsers +); +``` + +#### Decrypting a model + +Use the `decryptModel` method to decrypt a model's encrypted fields: + +```typescript +import { protectClient } from "./protect"; + +const decryptedResult = await protectClient.decryptModel(encryptedUser); + +if (decryptedResult.failure) { + // Handle the failure + console.log( + "error when decrypting:", + decryptedResult.failure.type, + decryptedResult.failure.message + ); +} + +const decryptedUser = decryptedResult.data; +console.log("decrypted user:", decryptedUser); +``` + +#### Bulk model operations + +For better performance when working with multiple models, use the `bulkEncryptModels` and `bulkDecryptModels` methods: + +```typescript +import { protectClient } from "./protect"; +import { users } from "./protect/schema"; + +// Array of models with plaintext values +const userModels = [ + { + id: "1", + email: "user1@example.com", + address: "123 Main St", + }, + { + id: "2", + email: "user2@example.com", + address: "456 Oak Ave", + }, +]; + +// Encrypt multiple models at once +const encryptedResult = await protectClient.bulkEncryptModels( + userModels, + users +); + +if (encryptedResult.failure) { + // Handle the failure +} + +const encryptedUsers = encryptedResult.data; + +// Decrypt multiple models at once +const decryptedResult = await protectClient.bulkDecryptModels(encryptedUsers); + +if (decryptedResult.failure) { + // Handle the failure +} + +const decryptedUsers = decryptedResult.data; +``` + +The model encryption methods provide a higher-level interface that's particularly useful when working with ORMs or when you need to encrypt multiple fields in an object. +They automatically handle the mapping between your model's structure and the encrypted fields defined in your schema. + +### Bulk operations + +Stash Encryption provides direct access to ZeroKMS bulk operations through the `bulkEncrypt` and `bulkDecrypt` methods. These methods are ideal when you need maximum performance and want to handle the correlation between encrypted/decrypted values and your application data manually. + +> [!TIP] +> The bulk operations provide the most direct interface to ZeroKMS's blazing fast bulk encryption and decryption capabilities. Each value gets a unique key while maintaining optimal performance through a single call to ZeroKMS. + +#### Bulk encryption + +Use the `bulkEncrypt` method to encrypt multiple plaintext values at once: + +```typescript +import { protectClient } from "./protect"; +import { users } from "./protect/schema"; + +// Array of plaintext values with optional IDs for correlation +const plaintexts = [ + { id: "user1", plaintext: "alice@example.com" }, + { id: "user2", plaintext: "bob@example.com" }, + { id: "user3", plaintext: "charlie@example.com" }, +]; + +const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, +}); + +if (encryptedResult.failure) { + // Handle the failure + console.log( + "error when bulk encrypting:", + encryptedResult.failure.type, + encryptedResult.failure.message + ); +} + +const encryptedData = encryptedResult.data; +console.log("encrypted data:", encryptedData); +``` + +The `bulkEncrypt` method returns an array of objects with the following structure: + +```typescript +[ + { id: "user1", data: EncryptedPayload }, + { id: "user2", data: EncryptedPayload }, + { id: "user3", data: EncryptedPayload }, +] +``` + +You can also encrypt without IDs if you don't need correlation: + +```typescript +const plaintexts = [ + { plaintext: "alice@example.com" }, + { plaintext: "bob@example.com" }, + { plaintext: "charlie@example.com" }, +]; + +const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, +}); +``` + +#### Bulk decryption + +Use the `bulkDecrypt` method to decrypt multiple encrypted values at once: + +```typescript +import { protectClient } from "./protect"; + +// encryptedData is the result from bulkEncrypt +const decryptedResult = await protectClient.bulkDecrypt(encryptedData); + +if (decryptedResult.failure) { + // Handle the failure + console.log( + "error when bulk decrypting:", + decryptedResult.failure.type, + decryptedResult.failure.message + ); +} + +const decryptedData = decryptedResult.data; +console.log("decrypted data:", decryptedData); +``` + +The `bulkDecrypt` method returns an array of objects with the following structure: + +```typescript +[ + { id: "user1", data: "alice@example.com" }, + { id: "user2", data: "bob@example.com" }, + { id: "user3", data: "charlie@example.com" }, +] +``` + +#### Response structure + +The `bulkDecrypt` method returns a `Result` object that represents the overall operation status. When successful from an HTTP and execution perspective, the `data` field contains an array where each item can have one of two outcomes: + +- **Success**: The item has a `data` field containing the decrypted plaintext +- **Failure**: The item has an `error` field containing a specific error message explaining why that particular decryption failed + +```typescript +// Example response structure +{ + data: [ + { id: "user1", data: "alice@example.com" }, // Success + { id: "user2", error: "Invalid ciphertext format" }, // Failure + { id: "user3", data: "charlie@example.com" }, // Success + ] +} +``` + +> [!NOTE] +> The underlying ZeroKMS response uses HTTP status code 207 (Multi-Status) to indicate that the bulk operation completed, but individual items within the batch may have succeeded or failed. This allows you to handle partial failures gracefully while still processing the successful decryptions. + +You can handle mixed results by checking each item: + +```typescript +const decryptedResult = await protectClient.bulkDecrypt(encryptedData); + +if (decryptedResult.failure) { + // Handle overall operation failure + console.log("Bulk decryption failed:", decryptedResult.failure.message); + return; +} + +// Process individual results +decryptedResult.data.forEach((item) => { + if ('data' in item) { + // Success - item.data contains the decrypted plaintext + console.log(`Decrypted ${item.id}:`, item.data); + } else if ('error' in item) { + // Failure - item.error contains the specific error message + console.log(`Failed to decrypt ${item.id}:`, item.error); + } +}); +``` + +#### Handling null values + +Bulk operations properly handle null values in both encryption and decryption: + +```typescript +const plaintexts = [ + { id: "user1", plaintext: "alice@example.com" }, + { id: "user2", plaintext: null }, + { id: "user3", plaintext: "charlie@example.com" }, +]; + +const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, +}); + +// Null values are preserved in the encrypted result +// encryptedResult.data[1].data will be null + +const decryptedResult = await protectClient.bulkDecrypt(encryptedResult.data); + +// Null values are preserved in the decrypted result +// decryptedResult.data[1].data will be null +``` + +#### Using bulk operations with lock contexts + +Bulk operations support identity-aware encryption through lock contexts: + +```typescript +import { LockContext } from "@cipherstash/stack/identity"; + +const lc = new LockContext(); +const lockContext = await lc.identify(userJwt); + +if (lockContext.failure) { + // Handle the failure +} + +const plaintexts = [ + { id: "user1", plaintext: "alice@example.com" }, + { id: "user2", plaintext: "bob@example.com" }, +]; + +// Encrypt with lock context +const encryptedResult = await protectClient + .bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + .withLockContext(lockContext.data); + +// Decrypt with lock context +const decryptedResult = await protectClient + .bulkDecrypt(encryptedResult.data) + .withLockContext(lockContext.data); +``` + +#### Performance considerations + +Bulk operations are optimized for performance and can handle thousands of values efficiently: + +```typescript +// Create a large array of values +const plaintexts = Array.from({ length: 1000 }, (_, i) => ({ + id: `user${i}`, + plaintext: `user${i}@example.com`, +})); + +// Single call to ZeroKMS for all 1000 values +const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, +}); + +// Single call to ZeroKMS for all 1000 values +const decryptedResult = await protectClient.bulkDecrypt(encryptedResult.data); +``` + +The bulk operations maintain the same security guarantees as individual operations - each value gets a unique key - while providing optimal performance through ZeroKMS's bulk processing capabilities. + +### Store encrypted data in a database + +Encrypted data can be stored in any database that supports JSONB, noting that searchable encryption is only supported in PostgreSQL at the moment. + +To store the encrypted data, specify the column type as `jsonb`. + +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email jsonb NOT NULL, +); +``` + +#### Searchable encryption in PostgreSQL + +To enable searchable encryption in PostgreSQL, [install the EQL custom types and functions](https://github.com/cipherstash/encrypt-query-language?tab=readme-ov-file#installation). + +1. Download the latest EQL install script: + + ```sh + curl -sLo cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt.sql + ``` + + Using [Supabase](https://supabase.com/)? We ship an EQL release specifically for Supabase. + Download the latest EQL install script: + + ```sh + curl -sLo cipherstash-encrypt-supabase.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt-supabase.sql + ``` + +2. Run this command to install the custom types and functions: + + ```sh + psql -f cipherstash-encrypt.sql + ``` + + or with Supabase: + + ```sh + psql -f cipherstash-encrypt-supabase.sql + ``` + +EQL is now installed in your database and you can enable searchable encryption by adding the `eql_v2_encrypted` type to a column. + +```sql +CREATE TABLE users ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + email eql_v2_encrypted +); +``` + +> [!WARNING] +> The `eql_v2_encrypted` type is a [composite type](https://www.postgresql.org/docs/current/rowtypes.html) and each ORM/client has a different way of handling inserts and selects. +> We've documented how to handle inserts and selects for the different ORMs/clients in the [docs](./docs/reference/working-with-composite-types.md). + +Read more about [how to search encrypted data](./docs/reference/searchable-encryption-postgres.md) in the docs. + +## Identity-aware encryption + +> [!IMPORTANT] +> Right now identity-aware encryption is only supported if you are using [Clerk](https://clerk.com/) as your identity provider. +> Read more about [lock contexts with Clerk and Next.js](./docs/how-to/lock-contexts-with-clerk.md). + +Stash Encryption can add an additional layer of protection to your data by requiring a valid JWT to perform a decryption. + +This ensures that only the user who encrypted data is able to decrypt it. + +Stash Encryption does this through a mechanism called a _lock context_. + +### Lock context + +Lock contexts ensure that only specific users can access sensitive data. + +> [!CAUTION] +> You must use the same lock context to encrypt and decrypt data. +> If you use different lock contexts, you will be unable to decrypt the data. + +To use a lock context, initialize a `LockContext` object with the identity claims. + +```typescript +import { LockContext } from "@cipherstash/stack/identity"; + +// protectClient from the previous steps +const lc = new LockContext(); +``` + +> [!NOTE] +> When initializing a `LockContext`, the default context is set to use the `sub` Identity Claim. + +### Identifying a user for a lock context + +A lock context needs to be locked to a user. +To identify the user, call the `identify` method on the lock context object, and pass a valid JWT from a user's session: + +```typescript +const identifyResult = await lc.identify(jwt); + +// The identify method returns the same Result pattern as the encrypt and decrypt methods. +if (identifyResult.failure) { + // Hanlde the failure +} + +const lockContext = identifyResult.data; +``` + +### Encrypting data with a lock context + +To encrypt data with a lock context, call the optional `withLockContext` method on the `encrypt` function and pass the lock context object as a parameter: + +```typescript +import { protectClient } from "./protect"; +import { users } from "./protect/schema"; + +const encryptResult = await protectClient + .encrypt("plaintext", { + table: users, + column: users.email, + }) + .withLockContext(lockContext); + +if (encryptResult.failure) { + // Handle the failure +} + +console.log("EQL Payload containing ciphertexts:", encryptResult.data); +``` + +### Decrypting data with a lock context + +To decrypt data with a lock context, call the optional `withLockContext` method on the `decrypt` function and pass the lock context object as a parameter: + +```typescript +import { protectClient } from "./protect"; + +const decryptResult = await protectClient + .decrypt(encryptResult.data) + .withLockContext(lockContext); + +if (decryptResult.failure) { + // Handle the failure +} + +const plaintext = decryptResult.data; +``` + +### Model encryption with lock context + +All model operations support lock contexts for identity-aware encryption: + +```typescript +import { protectClient } from "./protect"; +import { users } from "./protect/schema"; + +const myUsers = [ + { + id: "1", + email: "user@example.com", + address: "123 Main St", + createdAt: new Date("2024-01-01"), + }, + { + id: "2", + email: "user2@example.com", + address: "456 Oak Ave", + }, +]; + +// Encrypt a model with lock context +const encryptedResult = await protectClient + .encryptModel(myUsers[0], users) + .withLockContext(lockContext); + +if (encryptedResult.failure) { + // Handle the failure +} + +// Decrypt a model with lock context +const decryptedResult = await protectClient + .decryptModel(encryptedResult.data) + .withLockContext(lockContext); + +// Bulk operations also support lock contexts +const bulkEncryptedResult = await protectClient + .bulkEncryptModels(myUsers, users) + .withLockContext(lockContext); + +const bulkDecryptedResult = await protectClient + .bulkDecryptModels(bulkEncryptedResult.data) + .withLockContext(lockContext); +``` + +## Supported data types + +Stash Encryption currently supports encrypting and decrypting text. +Other data types like booleans, dates, ints, floats, and JSON are well-supported in other CipherStash products, and will be coming to Stash Encryption soon. + +Until support for other data types are available, you can express interest in this feature by adding a :+1: on this [GitHub Issue](https://github.com/cipherstash/protectjs/issues/48). + +## Searchable encryption + +Read more about [searching encrypted data](./docs/concepts/searchable-encryption.md) in the docs. + +## Multi-tenant encryption + +Stash Encryption supports multi-tenant encryption by using keysets. +Each keyset is cryptographically isolated from other keysets which esentially means that each tenant has their own unique keyspace. +If you are using a multi-tenant application, you can use keysets to encrypt data for each tenant creating a strong security boundary. + +In the [CipherStash Dashboard](https://dashboard.cipherstash.com/workspaces/_/encryption/keysets), you can create and manage keysets and then use the keyset identifier to encrypt data for each tenant when initializing the Stash Encryption client. + +```typescript +import { Encryption } from "@cipherstash/stack"; +import { users } from "./protect/schema"; + +const protectClient = await Encryption({ + schemas: [users], + keyset: { + // Must be a valid UUID which can be found in the CipherStash Dashboard + id: '123e4567-e89b-12d3-a456-426614174000' + }, +}) + +// or with a keyset name + +const protectClient = await Encryption({ + schemas: [users], + keyset: { + name: 'Company A' + }, +}) +``` + +> [!IMPORTANT] +> When creating a new keyset, make sure to grant your client access to the keyset or client initialization will fail. +> Read more about [managing keyset access](https://cipherstash.com/docs/platform/workspaces/key-sets). + +## Logging + +> [!TIP] +> `@cipherstash/stack` will NEVER log plaintext data. +> This is by design to prevent sensitive data from leaking into logs. + +`@cipherstash/stack` and `@cipherstash/nextjs` will log to the console with a log level of `info` by default. +To enable the logger, configure the following environment variable: + +```bash +PROTECT_LOG_LEVEL=debug # Enable debug logging +PROTECT_LOG_LEVEL=info # Enable info logging +PROTECT_LOG_LEVEL=error # Enable error logging +``` + +## CipherStash Client + +Stash Encryption is built on top of the CipherStash Client Rust SDK which is embedded with the `@cipherstash/protect-ffi` package. +The `@cipherstash/protect-ffi` source code is available on [GitHub](https://github.com/cipherstash/protectjs-ffi). + +Read more about configuring the CipherStash Client in the [configuration docs](./docs/reference/configuration.md). + +## Example applications + +Looking for examples of how to use Stash Encryption? +Check out the [example applications](./examples): + +- [Basic example](/examples/basic) demonstrates how to perform encryption operations +- [Drizzle example](/examples/drizzle) demonstrates how to use Stash Encryption with an ORM +- [Next.js and lock contexts example using Clerk](/examples/nextjs-clerk) demonstrates how to protect data with identity-aware encryption + +`@cipherstash/stack` can be used with most ORMs. +If you're interested in using `@cipherstash/stack` with a specific ORM, please [create an issue](https://github.com/cipherstash/protectjs/issues/new). + +## Builds and bundling + +`@cipherstash/stack` is a native Node.js module, and relies on native Node.js `require` to load the package. + +Here are a few resources to help based on your tool set: + +- [Required Next.js configuration](./docs/how-to/nextjs-external-packages.md). +- [SST and AWS serverless functions](./docs/how-to/sst-external-packages.md). + +> [!TIP] +> Deploying to Linux (e.g., AWS Lambda) with npm lockfile v3 and seeing runtime module load errors? See the troubleshooting guide: [`docs/how-to/npm-lockfile-v3`](./docs/how-to/npm-lockfile-v3-linux-deployments.md). + +## Contributing + +Please read the [contribution guide](CONTRIBUTE.md). + +## License + +Stash Encryption is [MIT licensed](./LICENSE.md). + +--- + +### Didn't find what you wanted? + +[Click here to let us know what was missing from our docs.](https://github.com/cipherstash/protectjs/issues/new?template=docs-feedback.yml&title=[Docs:]%20Feedback%20on%20README.md) diff --git a/packages/stack/__tests__/audit.test.ts b/packages/stack/__tests__/audit.test.ts new file mode 100644 index 00000000..4021e18c --- /dev/null +++ b/packages/stack/__tests__/audit.test.ts @@ -0,0 +1,472 @@ +import 'dotenv/config' +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { beforeAll, describe, expect, it } from 'vitest' +import { Encryption, LockContext } from '../src' + +const users = encryptedTable('users', { + auditable: encryptedColumn('auditable'), + email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + address: encryptedColumn('address').freeTextSearch(), +}) + +type User = { + id: string + email?: string | null + address?: string | null + auditable?: string | null + createdAt?: Date + updatedAt?: Date + number?: number +} + +let protectClient: Awaited> + +beforeAll(async () => { + protectClient = await Encryption({ + schemas: [users], + }) +}) + +describe('encryption and decryption with audit', () => { + it('should encrypt and decrypt a payload with audit metadata', async () => { + const email = 'very_secret_data' + + const ciphertext = await protectClient + .encrypt(email, { + column: users.auditable, + table: users, + }) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'encrypt', + }, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).toHaveProperty('c') + + const plaintext = await protectClient.decrypt(ciphertext.data).audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'decrypt', + }, + }) + + expect(plaintext).toEqual({ + data: email, + }) + }, 30000) + + it('should encrypt and decrypt a model with audit metadata', async () => { + // Create a model with decrypted values + const decryptedModel: User = { + id: '1', + email: 'test@example.com', + address: '123 Main St', + auditable: 'sensitive_data', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + } + + // Encrypt the model with audit + const encryptedModel = await protectClient + .encryptModel(decryptedModel, users) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'encrypt_model', + }, + }) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.address).toHaveProperty('c') + expect(encryptedModel.data.auditable).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.number).toBe(1) + + // Decrypt the model with audit + const decryptedResult = await protectClient + .decryptModel(encryptedModel.data) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'decrypt_model', + }, + }) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle null values in a model with audit metadata', async () => { + // Create a model with null values + const decryptedModel: User = { + id: '1', + email: null, + address: null, + auditable: null, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + } + + // Encrypt the model with audit + const encryptedModel = await protectClient + .encryptModel(decryptedModel, users) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'encrypt_model_nulls', + }, + }) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify null fields are preserved + expect(encryptedModel.data.email).toBeNull() + expect(encryptedModel.data.address).toBeNull() + expect(encryptedModel.data.auditable).toBeNull() + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.number).toBe(1) + + // Decrypt the model with audit + const decryptedResult = await protectClient + .decryptModel(encryptedModel.data) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'decrypt_model_nulls', + }, + }) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) +}) + +describe('bulk encryption with audit', () => { + it('should bulk encrypt and decrypt models with audit metadata', async () => { + // Create models with decrypted values + const decryptedModels: User[] = [ + { + id: '1', + email: 'test1@example.com', + address: '123 Main St', + auditable: 'sensitive_data_1', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + }, + { + id: '2', + email: 'test2@example.com', + address: '456 Oak St', + auditable: 'sensitive_data_2', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 2, + }, + ] + + // Encrypt the models with audit + const encryptedModels = await protectClient + .bulkEncryptModels(decryptedModels, users) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'bulk_encrypt_models', + }, + }) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields for each model + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].address).toHaveProperty('c') + expect(encryptedModels.data[0].auditable).toHaveProperty('c') + expect(encryptedModels.data[1].email).toHaveProperty('c') + expect(encryptedModels.data[1].address).toHaveProperty('c') + expect(encryptedModels.data[1].auditable).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].number).toBe(1) + expect(encryptedModels.data[1].id).toBe('2') + expect(encryptedModels.data[1].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].number).toBe(2) + + // Decrypt the models with audit + const decryptedResult = await protectClient + .bulkDecryptModels(encryptedModels.data) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'bulk_decrypt_models', + }, + }) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModels) + }, 30000) + + it('should handle mixed null and non-null values in bulk operations with audit', async () => { + const decryptedModels: User[] = [ + { + id: '1', + email: 'test1@example.com', + address: null, + auditable: 'sensitive_data_1', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + }, + { + id: '2', + email: null, + address: '123 Main St', + auditable: null, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 2, + }, + { + id: '3', + email: 'test3@example.com', + address: '456 Oak St', + auditable: 'sensitive_data_3', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 3, + }, + ] + + // Encrypt the models with audit + const encryptedModels = await protectClient + .bulkEncryptModels(decryptedModels, users) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'bulk_encrypt_mixed_nulls', + }, + }) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields for each model + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].address).toBeNull() + expect(encryptedModels.data[0].auditable).toHaveProperty('c') + expect(encryptedModels.data[1].email).toBeNull() + expect(encryptedModels.data[1].address).toHaveProperty('c') + expect(encryptedModels.data[1].auditable).toBeNull() + expect(encryptedModels.data[2].email).toHaveProperty('c') + expect(encryptedModels.data[2].address).toHaveProperty('c') + expect(encryptedModels.data[2].auditable).toHaveProperty('c') + + // Decrypt the models with audit + const decryptedResult = await protectClient + .bulkDecryptModels(decryptedModels) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'bulk_decrypt_mixed_nulls', + }, + }) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModels) + }, 30000) + + it('should return empty array if models is empty with audit', async () => { + // Encrypt empty array of models with audit + const encryptedModels = await protectClient + .bulkEncryptModels([], users) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'bulk_encrypt_empty', + }, + }) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + expect(encryptedModels.data).toEqual([]) + + // Decrypt empty array of models with audit + const decryptedResult = await protectClient + .bulkDecryptModels([]) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'bulk_decrypt_empty', + }, + }) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual([]) + }, 30000) +}) + +describe('audit with lock context', () => { + it('should encrypt and decrypt a model with both audit and lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + // Create a model with decrypted values + const decryptedModel: User = { + id: '1', + email: 'test@example.com', + auditable: 'sensitive_with_context', + } + + // Encrypt the model with both audit and lock context + const encryptedModel = await protectClient + .encryptModel(decryptedModel, users) + .withLockContext(lockContext.data) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'encrypt_with_context', + }, + }) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Decrypt the model with both audit and lock context + const decryptedResult = await protectClient + .decryptModel(encryptedModel.data) + .withLockContext(lockContext.data) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'decrypt_with_context', + }, + }) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should bulk encrypt and decrypt models with both audit and lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + // Create models with decrypted values + const decryptedModels: User[] = [ + { + id: '1', + email: 'test1@example.com', + auditable: 'bulk_sensitive_1', + }, + { + id: '2', + email: 'test2@example.com', + auditable: 'bulk_sensitive_2', + }, + ] + + // Encrypt the models with both audit and lock context + const encryptedModels = await protectClient + .bulkEncryptModels(decryptedModels, users) + .withLockContext(lockContext.data) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'bulk_encrypt_with_context', + }, + }) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Decrypt the models with both audit and lock context + const decryptedResult = await protectClient + .bulkDecryptModels(encryptedModels.data) + .withLockContext(lockContext.data) + .audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'bulk_decrypt_with_context', + }, + }) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModels) + }, 30000) +}) diff --git a/packages/stack/__tests__/backward-compat.test.ts b/packages/stack/__tests__/backward-compat.test.ts new file mode 100644 index 00000000..add6e554 --- /dev/null +++ b/packages/stack/__tests__/backward-compat.test.ts @@ -0,0 +1,65 @@ +import 'dotenv/config' +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { beforeAll, describe, expect, it } from 'vitest' +import { Encryption } from '../src' + +const users = encryptedTable('users', { + email: encryptedColumn('email'), +}) + +describe('k-field backward compatibility', () => { + let protectClient: Awaited> + + beforeAll(async () => { + protectClient = await Encryption({ schemas: [users] }) + }) + + it('should encrypt new data WITHOUT k field (forward compatibility)', async () => { + const testData = 'test@example.com' + + const result = await protectClient.encrypt(testData, { + column: users.email, + table: users, + }) + + if (result.failure) { + throw new Error(`Encryption failed: ${result.failure.message}`) + } + + // Forward compatibility: new encryptions should NOT have k field + expect(result.data).not.toHaveProperty('k') + expect(result.data).toHaveProperty('c') + expect(result.data).toHaveProperty('v') + expect(result.data).toHaveProperty('i') + }, 30000) + + it('should decrypt data with legacy k field (backward compatibility)', async () => { + // First encrypt some data + const testData = 'legacy@example.com' + + const encrypted = await protectClient.encrypt(testData, { + column: users.email, + table: users, + }) + + if (encrypted.failure) { + throw new Error(`Encryption failed: ${encrypted.failure.message}`) + } + + // Simulate legacy payload by adding k field to the encrypted data + // Use non-null assertion since we've already checked for failure above + const legacyPayload = { + ...encrypted.data!, + k: 'ct', // Legacy discriminant field - should be ignored during decryption + } + + // Decrypt should succeed even with legacy k field present + const result = await protectClient.decrypt(legacyPayload) + + if (result.failure) { + throw new Error(`Decryption failed: ${result.failure.message}`) + } + + expect(result.data).toBe(testData) + }, 30000) +}) diff --git a/packages/stack/__tests__/basic-protect.test.ts b/packages/stack/__tests__/basic-protect.test.ts new file mode 100644 index 00000000..e795f28c --- /dev/null +++ b/packages/stack/__tests__/basic-protect.test.ts @@ -0,0 +1,44 @@ +import 'dotenv/config' +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { beforeAll, describe, expect, it } from 'vitest' +import { Encryption } from '../src' + +const users = encryptedTable('users', { + email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + address: encryptedColumn('address').freeTextSearch(), + json: encryptedColumn('json').dataType('json'), +}) + +let protectClient: Awaited> + +beforeAll(async () => { + protectClient = await Encryption({ + schemas: [users], + }) +}) + +describe('encryption and decryption', () => { + it('should encrypt and decrypt a payload', async () => { + const email = 'hello@example.com' + + const ciphertext = await protectClient.encrypt(email, { + column: users.email, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).toHaveProperty('c') + + const a = ciphertext.data + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: email, + }) + }, 30000) +}) diff --git a/packages/stack/__tests__/bulk-protect.test.ts b/packages/stack/__tests__/bulk-protect.test.ts new file mode 100644 index 00000000..21b502bb --- /dev/null +++ b/packages/stack/__tests__/bulk-protect.test.ts @@ -0,0 +1,597 @@ +import 'dotenv/config' +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { beforeAll, describe, expect, it } from 'vitest' +import { type EncryptedPayload, Encryption, LockContext } from '../src' + +const users = encryptedTable('users', { + email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + address: encryptedColumn('address').freeTextSearch(), +}) + +type User = { + id: string + email?: string | null + createdAt?: Date + updatedAt?: Date + address?: string | null + number?: number +} + +let protectClient: Awaited> + +beforeAll(async () => { + protectClient = await Encryption({ + schemas: [users], + }) +}) + +describe('bulk encryption and decryption', () => { + describe('bulk encrypt', () => { + it('should bulk encrypt an array of plaintexts with IDs', async () => { + const plaintexts = [ + { id: 'user1', plaintext: 'alice@example.com' }, + { id: 'user2', plaintext: 'bob@example.com' }, + { id: 'user3', plaintext: 'charlie@example.com' }, + ] + + const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(3) + expect(encryptedData.data[0]).toHaveProperty('id', 'user1') + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).toHaveProperty('c') + expect(encryptedData.data[1]).toHaveProperty('id', 'user2') + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[2]).toHaveProperty('id', 'user3') + expect(encryptedData.data[2]).toHaveProperty('data') + + // Verify all encrypted values are different + expect(encryptedData.data[0].data?.c).not.toBe( + encryptedData.data[1].data?.c, + ) + expect(encryptedData.data[1].data?.c).not.toBe( + encryptedData.data[2].data?.c, + ) + expect(encryptedData.data[0].data?.c).not.toBe( + encryptedData.data[2].data?.c, + ) + }, 30000) + + it('should bulk encrypt an array of plaintexts without IDs', async () => { + const plaintexts = [ + { plaintext: 'alice@example.com' }, + { plaintext: 'bob@example.com' }, + { plaintext: 'charlie@example.com' }, + ] + + const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(3) + expect(encryptedData.data[0]).toHaveProperty('id', undefined) + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).toHaveProperty('c') + expect(encryptedData.data[1]).toHaveProperty('id', undefined) + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).toHaveProperty('c') + expect(encryptedData.data[2]).toHaveProperty('id', undefined) + expect(encryptedData.data[2]).toHaveProperty('data') + expect(encryptedData.data[2].data).toHaveProperty('c') + }, 30000) + + it('should handle null values in bulk encrypt', async () => { + const plaintexts = [ + { id: 'user1', plaintext: 'alice@example.com' }, + { id: 'user2', plaintext: null }, + { id: 'user3', plaintext: 'charlie@example.com' }, + ] + + const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(3) + expect(encryptedData.data[0]).toHaveProperty('id', 'user1') + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).toHaveProperty('c') + expect(encryptedData.data[1]).toHaveProperty('id', 'user2') + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).toBeNull() + expect(encryptedData.data[2]).toHaveProperty('id', 'user3') + expect(encryptedData.data[2]).toHaveProperty('data') + expect(encryptedData.data[2].data).toHaveProperty('c') + }, 30000) + + it('should handle all null values in bulk encrypt', async () => { + const plaintexts = [ + { id: 'user1', plaintext: null }, + { id: 'user2', plaintext: null }, + { id: 'user3', plaintext: null }, + ] + + const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(3) + expect(encryptedData.data[0]).toHaveProperty('id', 'user1') + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).toBeNull() + expect(encryptedData.data[1]).toHaveProperty('id', 'user2') + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).toBeNull() + expect(encryptedData.data[2]).toHaveProperty('id', 'user3') + expect(encryptedData.data[2]).toHaveProperty('data') + expect(encryptedData.data[2].data).toBeNull() + }, 30000) + + it('should handle empty array in bulk encrypt', async () => { + const plaintexts: Array<{ id?: string; plaintext: string | null }> = [] + + const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + expect(encryptedData.data).toHaveLength(0) + }, 30000) + }) + + describe('bulk decrypt', () => { + it('should bulk decrypt an array of encrypted payloads with IDs', async () => { + // First encrypt some data + const plaintexts = [ + { id: 'user1', plaintext: 'alice@example.com' }, + { id: 'user2', plaintext: 'bob@example.com' }, + { id: 'user3', plaintext: 'charlie@example.com' }, + ] + + const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Now decrypt the data + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify structure + expect(decryptedData.data).toHaveLength(3) + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('data', 'alice@example.com') + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', 'bob@example.com') + expect(decryptedData.data[2]).toHaveProperty('id', 'user3') + expect(decryptedData.data[2]).toHaveProperty( + 'data', + 'charlie@example.com', + ) + }, 30000) + + it('should bulk decrypt an array of encrypted payloads without IDs', async () => { + // First encrypt some data + const plaintexts = [ + { plaintext: 'alice@example.com' }, + { plaintext: 'bob@example.com' }, + { plaintext: 'charlie@example.com' }, + ] + + const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Now decrypt the data + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify structure + expect(decryptedData.data).toHaveLength(3) + expect(decryptedData.data[0]).toHaveProperty('id', undefined) + expect(decryptedData.data[0]).toHaveProperty('data', 'alice@example.com') + expect(decryptedData.data[1]).toHaveProperty('id', undefined) + expect(decryptedData.data[1]).toHaveProperty('data', 'bob@example.com') + expect(decryptedData.data[2]).toHaveProperty('id', undefined) + expect(decryptedData.data[2]).toHaveProperty( + 'data', + 'charlie@example.com', + ) + }, 30000) + + it('should handle null values in bulk decrypt', async () => { + // First encrypt some data with nulls + const plaintexts = [ + { id: 'user1', plaintext: 'alice@example.com' }, + { id: 'user2', plaintext: null }, + { id: 'user3', plaintext: 'charlie@example.com' }, + ] + + const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Now decrypt the data + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify structure + expect(decryptedData.data).toHaveLength(3) + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('data', 'alice@example.com') + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', null) + expect(decryptedData.data[2]).toHaveProperty('id', 'user3') + expect(decryptedData.data[2]).toHaveProperty( + 'data', + 'charlie@example.com', + ) + }, 30000) + + it('should handle all null values in bulk decrypt', async () => { + // First encrypt some data with all nulls + const plaintexts = [ + { id: 'user1', plaintext: null }, + { id: 'user2', plaintext: null }, + { id: 'user3', plaintext: null }, + ] + + const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Now decrypt the data + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify structure + expect(decryptedData.data).toHaveLength(3) + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('data', null) + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', null) + expect(decryptedData.data[2]).toHaveProperty('id', 'user3') + expect(decryptedData.data[2]).toHaveProperty('data', null) + }, 30000) + + it('should handle empty array in bulk decrypt', async () => { + const encryptedPayloads: Array<{ id?: string; data: EncryptedPayload }> = + [] + + const decryptedData = await protectClient.bulkDecrypt(encryptedPayloads) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + expect(decryptedData.data).toHaveLength(0) + }, 30000) + }) + + describe('bulk operations with lock context', () => { + it('should bulk encrypt and decrypt with lock context', async () => { + // This test requires a valid JWT token, so we'll skip it in CI + // TODO: Add proper JWT token handling for CI + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + const plaintexts = [ + { id: 'user1', plaintext: 'alice@example.com' }, + { id: 'user2', plaintext: 'bob@example.com' }, + { id: 'user3', plaintext: 'charlie@example.com' }, + ] + + // Encrypt with lock context + const encryptedData = await protectClient + .bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + .withLockContext(lockContext.data) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(3) + expect(encryptedData.data[0]).toHaveProperty('id', 'user1') + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).toHaveProperty('c') + expect(encryptedData.data[1]).toHaveProperty('id', 'user2') + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).toHaveProperty('c') + expect(encryptedData.data[2]).toHaveProperty('id', 'user3') + expect(encryptedData.data[2]).toHaveProperty('data') + expect(encryptedData.data[2].data).toHaveProperty('c') + + // Decrypt with lock context + const decryptedData = await protectClient + .bulkDecrypt(encryptedData.data) + .withLockContext(lockContext.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify decrypted data + expect(decryptedData.data).toHaveLength(3) + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('data', 'alice@example.com') + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', 'bob@example.com') + expect(decryptedData.data[2]).toHaveProperty('id', 'user3') + expect(decryptedData.data[2]).toHaveProperty( + 'data', + 'charlie@example.com', + ) + }, 30000) + + it('should handle null values with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + const plaintexts = [ + { id: 'user1', plaintext: 'alice@example.com' }, + { id: 'user2', plaintext: null }, + { id: 'user3', plaintext: 'charlie@example.com' }, + ] + + // Encrypt with lock context + const encryptedData = await protectClient + .bulkEncrypt(plaintexts, { + column: users.email, + table: users, + }) + .withLockContext(lockContext.data) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify null is preserved + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).toBeNull() + + // Decrypt with lock context + const decryptedData = await protectClient + .bulkDecrypt(encryptedData.data) + .withLockContext(lockContext.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify null is preserved + expect(decryptedData.data[1]).toHaveProperty('data') + expect(decryptedData.data[1].data).toBeNull() + }, 30000) + + it('should decrypt mixed lock context payloads with specific lock context', async () => { + const userJwt = process.env.USER_JWT + const user2Jwt = process.env.USER_2_JWT + + if (!userJwt || !user2Jwt) { + console.log( + 'Skipping mixed lock context test - missing USER_JWT or USER_2_JWT', + ) + return + } + + const lc = new LockContext() + const lc2 = new LockContext() + const lockContext1 = await lc.identify(userJwt) + const lockContext2 = await lc2.identify(user2Jwt) + + if (lockContext1.failure) { + throw new Error(`[protect]: ${lockContext1.failure.message}`) + } + + if (lockContext2.failure) { + throw new Error(`[protect]: ${lockContext2.failure.message}`) + } + + // Encrypt first value with USER_JWT lock context + const encryptedData1 = await protectClient + .bulkEncrypt([{ id: 'user1', plaintext: 'alice@example.com' }], { + column: users.email, + table: users, + }) + .withLockContext(lockContext1.data) + + if (encryptedData1.failure) { + throw new Error(`[protect]: ${encryptedData1.failure.message}`) + } + + // Encrypt second value with USER_2_JWT lock context + const encryptedData2 = await protectClient + .bulkEncrypt([{ id: 'user2', plaintext: 'bob@example.com' }], { + column: users.email, + table: users, + }) + .withLockContext(lockContext2.data) + + if (encryptedData2.failure) { + throw new Error(`[protect]: ${encryptedData2.failure.message}`) + } + + // Combine both encrypted payloads + const combinedEncryptedData = [ + ...encryptedData1.data, + ...encryptedData2.data, + ] + + // Decrypt with USER_2_JWT lock context + const decryptedData = await protectClient + .bulkDecrypt(combinedEncryptedData) + .withLockContext(lockContext2.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify both payloads are returned + expect(decryptedData.data).toHaveLength(2) + + // First payload should fail to decrypt since it was encrypted with different lock context + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('error') + expect(decryptedData.data[0]).not.toHaveProperty('data') + + // Second payload should be decrypted since it was encrypted with the same lock context + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', 'bob@example.com') + expect(decryptedData.data[1]).not.toHaveProperty('error') + }, 30000) + }) + + describe('bulk operations round-trip', () => { + it('should maintain data integrity through encrypt/decrypt cycle', async () => { + const originalData = [ + { id: 'user1', plaintext: 'alice@example.com' }, + { id: 'user2', plaintext: 'bob@example.com' }, + { id: 'user3', plaintext: null }, + { id: 'user4', plaintext: 'dave@example.com' }, + ] + + // Encrypt + const encryptedData = await protectClient.bulkEncrypt(originalData, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Decrypt + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify round-trip integrity + expect(decryptedData.data).toHaveLength(originalData.length) + + for (let i = 0; i < originalData.length; i++) { + expect(decryptedData.data[i].id).toBe(originalData[i].id) + expect(decryptedData.data[i].data).toBe(originalData[i].plaintext) + } + }, 30000) + + it('should handle large arrays efficiently', async () => { + const originalData = Array.from({ length: 100 }, (_, i) => ({ + id: `user${i}`, + plaintext: `user${i}@example.com`, + })) + + // Encrypt + const encryptedData = await protectClient.bulkEncrypt(originalData, { + column: users.email, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Decrypt + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify all data is preserved + expect(decryptedData.data).toHaveLength(100) + + for (let i = 0; i < 100; i++) { + expect(decryptedData.data[i].id).toBe(`user${i}`) + expect(decryptedData.data[i].data).toBe(`user${i}@example.com`) + } + }, 30000) + }) +}) diff --git a/packages/stack/__tests__/deprecated/search-terms.test.ts b/packages/stack/__tests__/deprecated/search-terms.test.ts new file mode 100644 index 00000000..ab25ef00 --- /dev/null +++ b/packages/stack/__tests__/deprecated/search-terms.test.ts @@ -0,0 +1,140 @@ +import 'dotenv/config' +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { describe, expect, it } from 'vitest' +import { Encryption, type SearchTerm } from '../../src' + +const users = encryptedTable('users', { + email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + address: encryptedColumn('address').freeTextSearch(), + age: encryptedColumn('age').dataType('number').equality(), + score: encryptedColumn('score').dataType('number').equality(), +}) + +describe('createSearchTerms (deprecated - backward compatibility)', () => { + it('should create search terms with default return type', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const searchTerms = [ + { + value: 'hello', + column: users.email, + table: users, + }, + { + value: 'world', + column: users.address, + table: users, + }, + ] as SearchTerm[] + + const searchTermsResult = await protectClient.createSearchTerms(searchTerms) + + if (searchTermsResult.failure) { + throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + } + + expect(searchTermsResult.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + v: 2, + }), + ]), + ) + }, 30000) + + it('should create search terms with composite-literal return type', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const searchTerms = [ + { + value: 'hello', + column: users.email, + table: users, + returnType: 'composite-literal', + }, + ] as SearchTerm[] + + const searchTermsResult = await protectClient.createSearchTerms(searchTerms) + + if (searchTermsResult.failure) { + throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + } + + const result = searchTermsResult.data[0] as string + expect(result).toMatch(/^\(.*\)$/) + expect(() => JSON.parse(result.slice(1, -1))).not.toThrow() + }, 30000) + + it('should create search terms with escaped-composite-literal return type', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const searchTerms = [ + { + value: 'hello', + column: users.email, + table: users, + returnType: 'escaped-composite-literal', + }, + ] as SearchTerm[] + + const searchTermsResult = await protectClient.createSearchTerms(searchTerms) + + if (searchTermsResult.failure) { + throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + } + + const result = searchTermsResult.data[0] as string + expect(result).toMatch(/^".*"$/) + const unescaped = JSON.parse(result) + expect(unescaped).toMatch(/^\(.*\)$/) + expect(() => JSON.parse(unescaped.slice(1, -1))).not.toThrow() + }, 30000) + + it('should create search terms with composite-literal return type for numbers', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const searchTerms = [ + { + value: 42, + column: users.age, + table: users, + returnType: 'composite-literal' as const, + }, + ] + + const searchTermsResult = await protectClient.createSearchTerms(searchTerms) + + if (searchTermsResult.failure) { + throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + } + + const result = searchTermsResult.data[0] as string + expect(result).toMatch(/^\(.*\)$/) + expect(() => JSON.parse(result.slice(1, -1))).not.toThrow() + }, 30000) + + it('should create search terms with escaped-composite-literal return type for numbers', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const searchTerms = [ + { + value: 99, + column: users.score, + table: users, + returnType: 'escaped-composite-literal' as const, + }, + ] + + const searchTermsResult = await protectClient.createSearchTerms(searchTerms) + + if (searchTermsResult.failure) { + throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + } + + const result = searchTermsResult.data[0] as string + expect(result).toMatch(/^".*"$/) + const unescaped = JSON.parse(result) + expect(unescaped).toMatch(/^\(.*\)$/) + expect(() => JSON.parse(unescaped.slice(1, -1))).not.toThrow() + }, 30000) +}) diff --git a/packages/stack/__tests__/encrypt-query.test.ts b/packages/stack/__tests__/encrypt-query.test.ts new file mode 100644 index 00000000..095c7a52 --- /dev/null +++ b/packages/stack/__tests__/encrypt-query.test.ts @@ -0,0 +1,872 @@ +import 'dotenv/config' +import { beforeAll, describe, expect, it } from 'vitest' +import { Encryption, EncryptionErrorTypes } from '../src' +import type { EncryptionClient } from '../src/ffi' +import { + articles, + createFailingMockLockContext, + createMockLockContext, + createMockLockContextWithNullContext, + expectFailure, + metadata, + products, + unwrapResult, + users, +} from './fixtures' + +describe('encryptQuery', () => { + let protectClient: EncryptionClient + + beforeAll(async () => { + protectClient = await Encryption({ + schemas: [users, articles, products, metadata], + }) + }) + + describe('single value encryption with explicit queryType', () => { + it('encrypts for equality query type', async () => { + const result = await protectClient.encryptQuery('test@example.com', { + column: users.email, + table: users, + queryType: 'equality', + }) + + const data = unwrapResult(result) + + expect(data).toMatchObject({ + i: { t: 'users', c: 'email' }, + v: 2, + }) + expect(data).toHaveProperty('hm') + }, 30000) + + it('encrypts for freeTextSearch query type', async () => { + const result = await protectClient.encryptQuery('hello world', { + column: users.bio, + table: users, + queryType: 'freeTextSearch', + }) + + const data = unwrapResult(result) + + expect(data).toMatchObject({ + i: { t: 'users', c: 'bio' }, + v: 2, + }) + expect(data).toHaveProperty('bf') + }, 30000) + + it('encrypts for orderAndRange query type', async () => { + const result = await protectClient.encryptQuery(25, { + column: users.age, + table: users, + queryType: 'orderAndRange', + }) + + const data = unwrapResult(result) + + expect(data).toMatchObject({ + i: { t: 'users', c: 'age' }, + v: 2, + }) + expect(data).toHaveProperty('ob') + }, 30000) + }) + + describe('auto-inference when queryType omitted', () => { + it('auto-infers equality for column with .equality()', async () => { + const result = await protectClient.encryptQuery('test@example.com', { + column: users.email, + table: users, + }) + + const data = unwrapResult(result) + expect(data).toHaveProperty('hm') + }, 30000) + + it('auto-infers freeTextSearch for match-only column', async () => { + const result = await protectClient.encryptQuery('search content', { + column: articles.content, + table: articles, + }) + + const data = unwrapResult(result) + expect(data).toHaveProperty('bf') + }, 30000) + + it('auto-infers orderAndRange for ore-only column', async () => { + const result = await protectClient.encryptQuery(99.99, { + column: products.price, + table: products, + }) + + const data = unwrapResult(result) + expect(data).toHaveProperty('ob') + }, 30000) + }) + + describe('edge cases', () => { + it('handles null values', async () => { + const result = await protectClient.encryptQuery(null, { + column: users.email, + table: users, + queryType: 'equality', + }) + + const data = unwrapResult(result) + expect(data).toBeNull() + }, 30000) + + it('rejects NaN values', async () => { + const result = await protectClient.encryptQuery(Number.NaN, { + column: users.age, + table: users, + queryType: 'orderAndRange', + }) + + expectFailure(result, 'NaN') + }, 30000) + + it('rejects Infinity values', async () => { + const result = await protectClient.encryptQuery( + Number.POSITIVE_INFINITY, + { + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ) + + expectFailure(result, 'Infinity') + }, 30000) + + it('rejects negative Infinity values', async () => { + const result = await protectClient.encryptQuery( + Number.NEGATIVE_INFINITY, + { + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ) + + expectFailure(result, 'Infinity') + }, 30000) + }) + + describe('validation errors', () => { + it('fails when queryType does not match column config', async () => { + const result = await protectClient.encryptQuery('test@example.com', { + column: users.email, + table: users, + queryType: 'freeTextSearch', // email only has equality + }) + + expectFailure(result, 'not configured') + }, 30000) + + it('fails when column has no indexes configured', async () => { + const result = await protectClient.encryptQuery('raw data', { + column: metadata.raw, + table: metadata, + }) + + expectFailure(result, 'no indexes configured') + }, 30000) + + it('provides descriptive error for queryType mismatch', async () => { + const result = await protectClient.encryptQuery(42, { + column: users.age, + table: users, + queryType: 'equality', // age only has orderAndRange + }) + + expectFailure(result, 'unique') + expectFailure( + result, + 'not configured', + EncryptionErrorTypes.EncryptionError, + ) + }, 30000) + }) + + describe('value/index type compatibility', () => { + it('fails when encrypting number with match index (explicit queryType)', async () => { + const result = await protectClient.encryptQuery(123, { + column: articles.content, // match-only column + table: articles, + queryType: 'freeTextSearch', + }) + + expectFailure(result, 'match') + expectFailure(result, 'numeric') + }, 30000) + + it('fails when encrypting number with auto-inferred match index', async () => { + const result = await protectClient.encryptQuery(123, { + column: articles.content, // match-only column, will infer 'match' + table: articles, + }) + + expectFailure(result, 'match') + }, 30000) + + it('fails in batch when number used with match index', async () => { + const result = await protectClient.encryptQuery([ + { value: 123, column: articles.content, table: articles }, + ]) + + expectFailure(result, 'match') + }, 30000) + + it('allows string with match index', async () => { + const result = await protectClient.encryptQuery('search text', { + column: articles.content, + table: articles, + }) + + const data = unwrapResult(result) + expect(data).toHaveProperty('bf') // bloom filter + }, 30000) + + it('allows number with ore index', async () => { + const result = await protectClient.encryptQuery(42, { + column: users.age, + table: users, + queryType: 'orderAndRange', + }) + + const data = unwrapResult(result) + expect(data).toHaveProperty('ob') // order bits + }, 30000) + }) + + describe('numeric edge cases', () => { + it('encrypts MAX_SAFE_INTEGER', async () => { + const result = await protectClient.encryptQuery(Number.MAX_SAFE_INTEGER, { + column: users.age, + table: users, + queryType: 'orderAndRange', + }) + + const data = unwrapResult(result) + expect(data).toMatchObject({ + i: { t: 'users', c: 'age' }, + v: 2, + }) + expect(data).toHaveProperty('ob') + }, 30000) + + it('encrypts MIN_SAFE_INTEGER', async () => { + const result = await protectClient.encryptQuery(Number.MIN_SAFE_INTEGER, { + column: users.age, + table: users, + queryType: 'orderAndRange', + }) + + const data = unwrapResult(result) + expect(data).toMatchObject({ + i: { t: 'users', c: 'age' }, + v: 2, + }) + expect(data).toHaveProperty('ob') + }, 30000) + + it('encrypts negative zero', async () => { + const result = await protectClient.encryptQuery(-0, { + column: users.age, + table: users, + queryType: 'orderAndRange', + }) + + const data = unwrapResult(result) + expect(data).toHaveProperty('ob') + }, 30000) + }) + + describe('string edge cases', () => { + it('encrypts empty string', async () => { + const result = await protectClient.encryptQuery('', { + column: users.email, + table: users, + queryType: 'equality', + }) + + const data = unwrapResult(result) + expect(data).toMatchObject({ + i: { t: 'users', c: 'email' }, + v: 2, + }) + expect(data).toHaveProperty('hm') + }, 30000) + + it('encrypts unicode/emoji strings', async () => { + const result = await protectClient.encryptQuery('Hello 世界 🌍🚀', { + column: users.bio, + table: users, + queryType: 'freeTextSearch', + }) + + const data = unwrapResult(result) + expect(data).toMatchObject({ + i: { t: 'users', c: 'bio' }, + v: 2, + }) + expect(data).toHaveProperty('bf') + }, 30000) + + it('encrypts strings with SQL special characters', async () => { + const result = await protectClient.encryptQuery( + "'; DROP TABLE users; --", + { + column: users.email, + table: users, + queryType: 'equality', + }, + ) + + const data = unwrapResult(result) + expect(data).toMatchObject({ + i: { t: 'users', c: 'email' }, + v: 2, + }) + expect(data).toHaveProperty('hm') + }, 30000) + }) + + describe('encryptQuery bulk (array overload)', () => { + it('encrypts multiple terms in batch', async () => { + const result = await protectClient.encryptQuery([ + { + value: 'user@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: 'search term', + column: users.bio, + table: users, + queryType: 'freeTextSearch', + }, + { + value: 42, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(3) + expect(data[0]).toMatchObject({ i: { t: 'users', c: 'email' } }) + expect(data[1]).toMatchObject({ i: { t: 'users', c: 'bio' } }) + expect(data[2]).toMatchObject({ i: { t: 'users', c: 'age' } }) + }, 30000) + + it('handles empty array', async () => { + // Empty arrays without opts are treated as empty batch for backward compatibility + const result = await protectClient.encryptQuery([]) + + const data = unwrapResult(result) + expect(data).toEqual([]) + }, 30000) + + it('handles null values in batch', async () => { + const result = await protectClient.encryptQuery([ + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: null, + column: users.bio, + table: users, + queryType: 'freeTextSearch', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(2) + expect(data[0]).not.toBeNull() + expect(data[1]).toBeNull() + }, 30000) + + it('auto-infers queryType when omitted', async () => { + const result = await protectClient.encryptQuery([ + { value: 'user@example.com', column: users.email, table: users }, + { value: 42, column: users.age, table: users }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(2) + expect(data[0]).toHaveProperty('hm') + expect(data[1]).toHaveProperty('ob') + }, 30000) + + it('rejects NaN/Infinity values in batch', async () => { + const result = await protectClient.encryptQuery([ + { + value: Number.NaN, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + { + value: Number.POSITIVE_INFINITY, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ]) + + expect(result.failure).toBeDefined() + }, 30000) + + it('rejects negative Infinity in batch', async () => { + const result = await protectClient.encryptQuery([ + { + value: Number.NEGATIVE_INFINITY, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ]) + + expectFailure(result, 'Infinity') + }, 30000) + }) + + describe('bulk index preservation', () => { + it('preserves exact positions with multiple nulls interspersed', async () => { + const result = await protectClient.encryptQuery([ + { + value: null, + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: 'user@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: null, + column: users.bio, + table: users, + queryType: 'freeTextSearch', + }, + { + value: null, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + { + value: 42, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(5) + expect(data[0]).toBeNull() + expect(data[1]).not.toBeNull() + expect(data[1]).toHaveProperty('hm') + expect(data[2]).toBeNull() + expect(data[3]).toBeNull() + expect(data[4]).not.toBeNull() + expect(data[4]).toHaveProperty('ob') + }, 30000) + + it('handles single-item array', async () => { + const result = await protectClient.encryptQuery([ + { + value: 'single@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(1) + expect(data[0]).toMatchObject({ i: { t: 'users', c: 'email' } }) + expect(data[0]).toHaveProperty('hm') + }, 30000) + + it('handles all-null array', async () => { + const result = await protectClient.encryptQuery([ + { + value: null, + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: null, + column: users.bio, + table: users, + queryType: 'freeTextSearch', + }, + { + value: null, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(3) + expect(data[0]).toBeNull() + expect(data[1]).toBeNull() + expect(data[2]).toBeNull() + }, 30000) + }) + + describe('audit support', () => { + it('passes audit metadata for single query', async () => { + const result = await protectClient + .encryptQuery('test@example.com', { + column: users.email, + table: users, + queryType: 'equality', + }) + .audit({ metadata: { userId: 'test-user' } }) + + const data = unwrapResult(result) + expect(data).toMatchObject({ i: { t: 'users', c: 'email' } }) + }, 30000) + + it('passes audit metadata for bulk query', async () => { + const result = await protectClient + .encryptQuery([ + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + ]) + .audit({ metadata: { userId: 'test-user' } }) + + const data = unwrapResult(result) + expect(data).toHaveLength(1) + }, 30000) + }) + + describe('returnType formatting', () => { + it('returns Encrypted by default (no returnType)', async () => { + const result = await protectClient.encryptQuery([ + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(1) + expect(data[0]).toMatchObject({ + i: { t: 'users', c: 'email' }, + v: 2, + }) + expect(typeof data[0]).toBe('object') + }, 30000) + + it('returns composite-literal format when specified', async () => { + const result = await protectClient.encryptQuery([ + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + returnType: 'composite-literal', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(1) + expect(typeof data[0]).toBe('string') + // Format: ("json") + expect(data[0]).toMatch(/^\(".*"\)$/) + }, 30000) + + it('returns escaped-composite-literal format when specified', async () => { + const result = await protectClient.encryptQuery([ + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + returnType: 'escaped-composite-literal', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(1) + expect(typeof data[0]).toBe('string') + // Format: "(\"json\")" - outer quotes with escaped inner quotes + expect(data[0]).toMatch(/^"\(.*\)"$/) + }, 30000) + + it('returns eql format when explicitly specified', async () => { + const result = await protectClient.encryptQuery([ + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + returnType: 'eql', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(1) + expect(data[0]).toMatchObject({ + i: { t: 'users', c: 'email' }, + v: 2, + }) + expect(typeof data[0]).toBe('object') + }, 30000) + + it('handles mixed returnType values in same batch', async () => { + const result = await protectClient.encryptQuery([ + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, // default + { + value: 'search term', + column: users.bio, + table: users, + queryType: 'freeTextSearch', + returnType: 'composite-literal', + }, + { + value: 42, + column: users.age, + table: users, + queryType: 'orderAndRange', + returnType: 'escaped-composite-literal', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(3) + + // First: default (Encrypted object) + expect(typeof data[0]).toBe('object') + expect(data[0]).toMatchObject({ i: { t: 'users', c: 'email' } }) + + // Second: composite-literal (string) + expect(typeof data[1]).toBe('string') + expect(data[1]).toMatch(/^\(".*"\)$/) + + // Third: escaped-composite-literal (string) + expect(typeof data[2]).toBe('string') + expect(data[2]).toMatch(/^"\(.*\)"$/) + }, 30000) + + it('handles returnType with null values', async () => { + const result = await protectClient.encryptQuery([ + { + value: null, + column: users.email, + table: users, + queryType: 'equality', + returnType: 'composite-literal', + }, + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + returnType: 'composite-literal', + }, + { + value: null, + column: users.bio, + table: users, + queryType: 'freeTextSearch', + returnType: 'escaped-composite-literal', + }, + ]) + + const data = unwrapResult(result) + + expect(data).toHaveLength(3) + expect(data[0]).toBeNull() + expect(typeof data[1]).toBe('string') + expect(data[1]).toMatch(/^\(".*"\)$/) + expect(data[2]).toBeNull() + }, 30000) + }) + + describe('LockContext support', () => { + it('single query with LockContext calls getLockContext', async () => { + const mockLockContext = createMockLockContext() + + const operation = protectClient.encryptQuery('test@example.com', { + column: users.email, + table: users, + queryType: 'equality', + }) + + const withContext = operation.withLockContext(mockLockContext as any) + expect(withContext).toHaveProperty('execute') + expect(typeof withContext.execute).toBe('function') + }, 30000) + + it('bulk query with LockContext calls getLockContext', async () => { + const mockLockContext = createMockLockContext() + + const operation = protectClient.encryptQuery([ + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + ]) + + const withContext = operation.withLockContext(mockLockContext as any) + expect(withContext).toHaveProperty('execute') + expect(typeof withContext.execute).toBe('function') + }, 30000) + + it('executes single query with LockContext mock', async () => { + const mockLockContext = createMockLockContext() + + const operation = protectClient.encryptQuery('test@example.com', { + column: users.email, + table: users, + queryType: 'equality', + }) + + const withContext = operation.withLockContext(mockLockContext as any) + const result = await withContext.execute() + + expect(mockLockContext.getLockContext).toHaveBeenCalledTimes(1) + + const data = unwrapResult(result) + expect(data).toMatchObject({ + i: { t: 'users', c: 'email' }, + v: 2, + }) + expect(data).toHaveProperty('hm') + }, 30000) + + it('executes bulk query with LockContext mock', async () => { + const mockLockContext = createMockLockContext() + + const operation = protectClient.encryptQuery([ + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + { + value: 42, + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ]) + + const withContext = operation.withLockContext(mockLockContext as any) + const result = await withContext.execute() + + expect(mockLockContext.getLockContext).toHaveBeenCalledTimes(1) + + const data = unwrapResult(result) + expect(data).toHaveLength(2) + expect(data[0]).toHaveProperty('hm') + expect(data[1]).toHaveProperty('ob') + }, 30000) + + it('handles LockContext failure gracefully', async () => { + const mockLockContext = createFailingMockLockContext( + EncryptionErrorTypes.CtsTokenError, + 'Mock LockContext failure', + ) + + const operation = protectClient.encryptQuery('test@example.com', { + column: users.email, + table: users, + queryType: 'equality', + }) + + const withContext = operation.withLockContext(mockLockContext as any) + const result = await withContext.execute() + + expectFailure( + result, + 'Mock LockContext failure', + EncryptionErrorTypes.CtsTokenError, + ) + }, 30000) + + it('handles null value with LockContext', async () => { + const mockLockContext = createMockLockContext() + + const operation = protectClient.encryptQuery(null, { + column: users.email, + table: users, + queryType: 'equality', + }) + + const withContext = operation.withLockContext(mockLockContext as any) + const result = await withContext.execute() + + // Null values should return null without calling LockContext + // since there's nothing to encrypt + const data = unwrapResult(result) + expect(data).toBeNull() + }, 30000) + + it('handles explicit null context from getLockContext gracefully', async () => { + // Simulate a runtime scenario where context is null (bypasses TypeScript) + const mockLockContext = createMockLockContextWithNullContext() + + const operation = protectClient.encryptQuery([ + { + value: 'test@example.com', + column: users.email, + table: users, + queryType: 'equality', + }, + ]) + + const withContext = operation.withLockContext(mockLockContext as any) + const result = await withContext.execute() + + // Should succeed - null context should not be passed to FFI + const data = unwrapResult(result) + expect(data).toHaveLength(1) + expect(data[0]).toHaveProperty('hm') + }, 30000) + }) +}) diff --git a/packages/stack/__tests__/fixtures/index.ts b/packages/stack/__tests__/fixtures/index.ts new file mode 100644 index 00000000..4d68ff1a --- /dev/null +++ b/packages/stack/__tests__/fixtures/index.ts @@ -0,0 +1,127 @@ +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { expect, vi } from 'vitest' + +// ============ Schema Fixtures ============ + +/** + * Users table with multiple index types for testing + */ +export const users = encryptedTable('users', { + email: encryptedColumn('email').equality(), + bio: encryptedColumn('bio').freeTextSearch(), + age: encryptedColumn('age').dataType('number').orderAndRange(), +}) + +/** + * Articles table with only freeTextSearch (for auto-inference test) + */ +export const articles = encryptedTable('articles', { + content: encryptedColumn('content').freeTextSearch(), +}) + +/** + * Products table with only orderAndRange (for auto-inference test) + */ +export const products = encryptedTable('products', { + price: encryptedColumn('price').dataType('number').orderAndRange(), +}) + +/** + * Metadata table with no indexes (for validation error test) + */ +export const metadata = encryptedTable('metadata', { + raw: encryptedColumn('raw'), +}) + +// ============ Mock Factories ============ + +/** + * Creates a mock LockContext with successful response + */ +export function createMockLockContext(overrides?: { + accessToken?: string + expiry?: number + identityClaim?: string[] +}) { + return { + getLockContext: vi.fn().mockResolvedValue({ + data: { + ctsToken: { + accessToken: overrides?.accessToken ?? 'mock-token', + expiry: overrides?.expiry ?? Date.now() + 3600000, + }, + context: { + identityClaim: overrides?.identityClaim ?? ['sub'], + }, + }, + }), + } +} + +/** + * Creates a mock LockContext with explicit null context (simulates runtime edge case) + */ +export function createMockLockContextWithNullContext() { + return { + getLockContext: vi.fn().mockResolvedValue({ + data: { + ctsToken: { + accessToken: 'mock-token', + expiry: Date.now() + 3600000, + }, + context: null, // Explicit null - simulating runtime edge case + }, + }), + } +} + +/** + * Creates a mock LockContext that returns a failure + */ +export function createFailingMockLockContext( + errorType: string, + message: string, +) { + return { + getLockContext: vi.fn().mockResolvedValue({ + failure: { type: errorType, message }, + }), + } +} + +// ============ Test Helpers ============ + +/** + * Unwraps a Result type, throwing an error if it's a failure. + * Use this to simplify test assertions when you expect success. + */ +export function unwrapResult(result: { + data?: T + failure?: { message: string } +}): T { + if (result.failure) { + throw new Error(result.failure.message) + } + return result.data as T +} + +/** + * Asserts that a result is a failure with optional message and type matching + */ +export function expectFailure( + result: { failure?: { message: string; type?: string } }, + messagePattern?: string | RegExp, + expectedType?: string, +) { + expect(result.failure).toBeDefined() + if (messagePattern) { + if (typeof messagePattern === 'string') { + expect(result.failure?.message).toContain(messagePattern) + } else { + expect(result.failure?.message).toMatch(messagePattern) + } + } + if (expectedType) { + expect(result.failure?.type).toBe(expectedType) + } +} diff --git a/packages/stack/__tests__/helpers.test.ts b/packages/stack/__tests__/helpers.test.ts new file mode 100644 index 00000000..abc2f6d8 --- /dev/null +++ b/packages/stack/__tests__/helpers.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, it } from 'vitest' +import { + bulkModelsToEncryptedPgComposites, + encryptedToCompositeLiteral, + encryptedToPgComposite, + isEncryptedPayload, + modelToEncryptedPgComposites, +} from '../src/helpers' + +describe('helpers', () => { + describe('encryptedToPgComposite', () => { + it('should convert encrypted payload to pg composite', () => { + const encrypted = { + v: 1, + c: 'ciphertext', + i: { + c: 'iv', + t: 't', + }, + k: 'k', + ob: ['a', 'b'], + bf: [1, 2, 3], + hm: 'hm', + } + + const pgComposite = encryptedToPgComposite(encrypted) + expect(pgComposite).toEqual({ + data: encrypted, + }) + }) + }) + + describe('encryptedToCompositeLiteral', () => { + it('should convert encrypted payload to pg composite literal string', () => { + const encrypted = { + v: 1, + c: 'ciphertext', + i: { + c: 'iv', + t: 't', + }, + } + + const literal = encryptedToCompositeLiteral(encrypted) + // Should produce PostgreSQL composite literal format: ("json_string") + expect(literal).toMatch(/^\(.*\)$/) + // The inner content should be a valid JSON string (double-stringified) + const innerContent = literal.slice(1, -1) // Remove outer parentheses + expect(() => JSON.parse(innerContent)).not.toThrow() + // Parsing the inner content should give us the original JSON + const parsedJson = JSON.parse(JSON.parse(innerContent)) + expect(parsedJson).toEqual(encrypted) + }) + }) + + describe('isEncryptedPayload', () => { + it('should return true for valid encrypted payload', () => { + const encrypted = { + v: 1, + c: 'ciphertext', + i: { c: 'iv', t: 't' }, + } + expect(isEncryptedPayload(encrypted)).toBe(true) + }) + + it('should return false for null', () => { + expect(isEncryptedPayload(null)).toBe(false) + }) + + it('should return false for non-encrypted object', () => { + expect(isEncryptedPayload({ foo: 'bar' })).toBe(false) + }) + }) + + describe('modelToEncryptedPgComposites', () => { + it('should transform model with encrypted fields', () => { + const model = { + name: 'John', + email: { + v: 1, + c: 'encrypted_email', + i: { c: 'iv', t: 't' }, + }, + age: 30, + } + + const result = modelToEncryptedPgComposites(model) + expect(result).toEqual({ + name: 'John', + email: { + data: { + v: 1, + c: 'encrypted_email', + i: { c: 'iv', t: 't' }, + }, + }, + age: 30, + }) + }) + }) + + describe('bulkModelsToEncryptedPgComposites', () => { + it('should transform multiple models with encrypted fields', () => { + const models = [ + { + name: 'John', + email: { + v: 1, + c: 'encrypted_email1', + i: { c: 'iv', t: 't' }, + }, + }, + { + name: 'Jane', + email: { + v: 1, + c: 'encrypted_email2', + i: { c: 'iv', t: 't' }, + }, + }, + ] + + const result = bulkModelsToEncryptedPgComposites(models) + expect(result).toEqual([ + { + name: 'John', + email: { + data: { + v: 1, + c: 'encrypted_email1', + i: { c: 'iv', t: 't' }, + }, + }, + }, + { + name: 'Jane', + email: { + data: { + v: 1, + c: 'encrypted_email2', + i: { c: 'iv', t: 't' }, + }, + }, + }, + ]) + }) + }) +}) diff --git a/packages/stack/__tests__/infer-index-type.test.ts b/packages/stack/__tests__/infer-index-type.test.ts new file mode 100644 index 00000000..81f86837 --- /dev/null +++ b/packages/stack/__tests__/infer-index-type.test.ts @@ -0,0 +1,57 @@ +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { describe, expect, it } from 'vitest' +import { + inferIndexType, + validateIndexType, +} from '../src/ffi/helpers/infer-index-type' + +describe('infer-index-type helpers', () => { + const users = encryptedTable('users', { + email: encryptedColumn('email').equality(), + bio: encryptedColumn('bio').freeTextSearch(), + age: encryptedColumn('age').orderAndRange(), + name: encryptedColumn('name').equality().freeTextSearch(), + }) + + describe('inferIndexType', () => { + it('returns unique for equality-only column', () => { + expect(inferIndexType(users.email)).toBe('unique') + }) + + it('returns match for freeTextSearch-only column', () => { + expect(inferIndexType(users.bio)).toBe('match') + }) + + it('returns ore for orderAndRange-only column', () => { + expect(inferIndexType(users.age)).toBe('ore') + }) + + it('returns unique when multiple indexes (priority: unique > match > ore)', () => { + expect(inferIndexType(users.name)).toBe('unique') + }) + + it('returns match when freeTextSearch and orderAndRange (priority: match > ore)', () => { + const schema = encryptedTable('t', { + col: encryptedColumn('col').freeTextSearch().orderAndRange(), + }) + expect(inferIndexType(schema.col)).toBe('match') + }) + + it('throws for column with no indexes', () => { + const noIndex = encryptedTable('t', { col: encryptedColumn('col') }) + expect(() => inferIndexType(noIndex.col)).toThrow('no indexes configured') + }) + }) + + describe('validateIndexType', () => { + it('does not throw for valid index type', () => { + expect(() => validateIndexType(users.email, 'unique')).not.toThrow() + }) + + it('throws for unconfigured index type', () => { + expect(() => validateIndexType(users.email, 'match')).toThrow( + 'not configured', + ) + }) + }) +}) diff --git a/packages/stack/__tests__/json-protect.test.ts b/packages/stack/__tests__/json-protect.test.ts new file mode 100644 index 00000000..830a2221 --- /dev/null +++ b/packages/stack/__tests__/json-protect.test.ts @@ -0,0 +1,1223 @@ +import 'dotenv/config' +import { + encryptedColumn, + encryptedTable, + encryptedValue, +} from '@cipherstash/schema' +import { beforeAll, describe, expect, it } from 'vitest' +import { Encryption, LockContext } from '../src' + +const users = encryptedTable('users', { + email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + address: encryptedColumn('address').freeTextSearch(), + json: encryptedColumn('json').dataType('json'), + metadata: { + profile: encryptedValue('metadata.profile').dataType('json'), + settings: { + preferences: encryptedValue('metadata.settings.preferences').dataType( + 'json', + ), + }, + }, +}) + +type User = { + id: string + email?: string | null + createdAt?: Date + updatedAt?: Date + address?: string | null + json?: Record | null + metadata?: { + profile?: Record | null + settings?: { + preferences?: Record | null + } + } +} + +let protectClient: Awaited> + +beforeAll(async () => { + protectClient = await Encryption({ + schemas: [users], + }) +}) + +describe('JSON encryption and decryption', () => { + it('should encrypt and decrypt a simple JSON payload', async () => { + const json = { + name: 'John Doe', + age: 30, + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) + + it('should encrypt and decrypt a complex JSON payload', async () => { + const json = { + user: { + id: 123, + name: 'Jane Smith', + email: 'jane@example.com', + preferences: { + theme: 'dark', + notifications: true, + language: 'en-US', + }, + tags: ['premium', 'verified'], + metadata: { + created: '2023-01-01T00:00:00Z', + lastLogin: '2023-12-01T10:30:00Z', + }, + }, + settings: { + privacy: { + public: false, + shareData: true, + }, + features: { + beta: true, + experimental: false, + }, + }, + array: [1, 2, 3, { nested: 'value' }], + nullValue: null, + booleanValue: true, + numberValue: 42.5, + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) + + it('should handle null JSON payload', async () => { + const ciphertext = await protectClient.encrypt(null, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify null is preserved + expect(ciphertext.data).toBeNull() + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: null, + }) + }, 30000) + + it('should handle empty JSON object', async () => { + const json = {} + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) + + it('should handle JSON with special characters', async () => { + const json = { + message: 'Hello "world" with \'quotes\' and \\backslashes\\', + unicode: '🚀 emoji and ñ special chars', + symbols: '!@#$%^&*()_+-=[]{}|;:,.<>?/~`', + multiline: 'Line 1\nLine 2\tTabbed', + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) +}) + +describe('JSON model encryption and decryption', () => { + it('should encrypt and decrypt a model with JSON field', async () => { + const decryptedModel = { + id: '1', + email: 'test@example.com', + address: '123 Main St', + json: { + name: 'John Doe', + age: 30, + preferences: { + theme: 'dark', + notifications: true, + }, + }, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).not.toHaveProperty('k') + expect(encryptedModel.data.address).not.toHaveProperty('k') + expect(encryptedModel.data.json).not.toHaveProperty('k') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle null JSON in model', async () => { + const decryptedModel = { + id: '2', + email: 'test2@example.com', + address: '456 Oak St', + json: null, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).not.toHaveProperty('k') + expect(encryptedModel.data.address).not.toHaveProperty('k') + expect(encryptedModel.data.json).toBeNull() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle undefined JSON in model', async () => { + const decryptedModel = { + id: '3', + email: 'test3@example.com', + address: '789 Pine St', + json: undefined, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).not.toHaveProperty('k') + expect(encryptedModel.data.address).not.toHaveProperty('k') + expect(encryptedModel.data.json).toBeUndefined() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) +}) + +describe('JSON bulk encryption and decryption', () => { + it('should bulk encrypt and decrypt JSON payloads', async () => { + const jsonPayloads = [ + { id: 'user1', plaintext: { name: 'Alice', age: 25 } }, + { id: 'user2', plaintext: { name: 'Bob', age: 30 } }, + { id: 'user3', plaintext: { name: 'Charlie', age: 35 } }, + ] + + const encryptedData = await protectClient.bulkEncrypt(jsonPayloads, { + column: users.json, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(3) + expect(encryptedData.data[0]).toHaveProperty('id', 'user1') + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).not.toHaveProperty('k') + expect(encryptedData.data[1]).toHaveProperty('id', 'user2') + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).not.toHaveProperty('k') + expect(encryptedData.data[2]).toHaveProperty('id', 'user3') + expect(encryptedData.data[2]).toHaveProperty('data') + expect(encryptedData.data[2].data).not.toHaveProperty('k') + + // Now decrypt the data + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify decrypted data + expect(decryptedData.data).toHaveLength(3) + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('data', { + name: 'Alice', + age: 25, + }) + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', { + name: 'Bob', + age: 30, + }) + expect(decryptedData.data[2]).toHaveProperty('id', 'user3') + expect(decryptedData.data[2]).toHaveProperty('data', { + name: 'Charlie', + age: 35, + }) + }, 30000) + + it('should handle mixed null and non-null JSON in bulk operations', async () => { + const jsonPayloads = [ + { id: 'user1', plaintext: { name: 'Alice', age: 25 } }, + { id: 'user2', plaintext: null }, + { id: 'user3', plaintext: { name: 'Charlie', age: 35 } }, + ] + + const encryptedData = await protectClient.bulkEncrypt(jsonPayloads, { + column: users.json, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(3) + expect(encryptedData.data[0]).toHaveProperty('id', 'user1') + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).not.toHaveProperty('k') + expect(encryptedData.data[1]).toHaveProperty('id', 'user2') + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).toBeNull() + expect(encryptedData.data[2]).toHaveProperty('id', 'user3') + expect(encryptedData.data[2]).toHaveProperty('data') + expect(encryptedData.data[2].data).not.toHaveProperty('k') + + // Now decrypt the data + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify decrypted data + expect(decryptedData.data).toHaveLength(3) + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('data', { + name: 'Alice', + age: 25, + }) + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', null) + expect(decryptedData.data[2]).toHaveProperty('id', 'user3') + expect(decryptedData.data[2]).toHaveProperty('data', { + name: 'Charlie', + age: 35, + }) + }, 30000) + + it('should bulk encrypt and decrypt models with JSON fields', async () => { + const decryptedModels = [ + { + id: '1', + email: 'test1@example.com', + address: '123 Main St', + json: { + name: 'Alice', + preferences: { theme: 'dark' }, + }, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + }, + { + id: '2', + email: 'test2@example.com', + address: '456 Oak St', + json: { + name: 'Bob', + preferences: { theme: 'light' }, + }, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + }, + ] + + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields for each model + expect(encryptedModels.data[0].email).not.toHaveProperty('k') + expect(encryptedModels.data[0].address).not.toHaveProperty('k') + expect(encryptedModels.data[0].json).not.toHaveProperty('k') + expect(encryptedModels.data[1].email).not.toHaveProperty('k') + expect(encryptedModels.data[1].address).not.toHaveProperty('k') + expect(encryptedModels.data[1].json).not.toHaveProperty('k') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].id).toBe('2') + expect(encryptedModels.data[1].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].updatedAt).toEqual(new Date('2021-01-01')) + + const decryptedResult = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModels) + }, 30000) +}) + +describe('JSON encryption with lock context', () => { + it('should encrypt and decrypt JSON with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + const json = { + name: 'John Doe', + age: 30, + preferences: { + theme: 'dark', + notifications: true, + }, + } + + const ciphertext = await protectClient + .encrypt(json, { + column: users.json, + table: users, + }) + .withLockContext(lockContext.data) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient + .decrypt(ciphertext.data) + .withLockContext(lockContext.data) + + if (plaintext.failure) { + throw new Error(`[protect]: ${plaintext.failure.message}`) + } + + expect(plaintext.data).toEqual(json) + }, 30000) + + it('should encrypt JSON model with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + const decryptedModel = { + id: '1', + email: 'test@example.com', + json: { + name: 'John Doe', + preferences: { theme: 'dark' }, + }, + } + + const encryptedModel = await protectClient + .encryptModel(decryptedModel, users) + .withLockContext(lockContext.data) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).not.toHaveProperty('k') + expect(encryptedModel.data.json).not.toHaveProperty('k') + + const decryptedResult = await protectClient + .decryptModel(encryptedModel.data) + .withLockContext(lockContext.data) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should bulk encrypt JSON with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + const jsonPayloads = [ + { id: 'user1', plaintext: { name: 'Alice', age: 25 } }, + { id: 'user2', plaintext: { name: 'Bob', age: 30 } }, + ] + + const encryptedData = await protectClient + .bulkEncrypt(jsonPayloads, { + column: users.json, + table: users, + }) + .withLockContext(lockContext.data) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(2) + expect(encryptedData.data[0]).toHaveProperty('id', 'user1') + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).not.toHaveProperty('k') + expect(encryptedData.data[1]).toHaveProperty('id', 'user2') + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).not.toHaveProperty('k') + + // Decrypt with lock context + const decryptedData = await protectClient + .bulkDecrypt(encryptedData.data) + .withLockContext(lockContext.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify decrypted data + expect(decryptedData.data).toHaveLength(2) + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('data', { + name: 'Alice', + age: 25, + }) + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', { + name: 'Bob', + age: 30, + }) + }, 30000) +}) + +describe('JSON nested object encryption', () => { + it('should encrypt and decrypt nested JSON objects', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '1', + email: 'test@example.com', + metadata: { + profile: { + name: 'John Doe', + age: 30, + preferences: { + theme: 'dark', + notifications: true, + }, + }, + settings: { + preferences: { + language: 'en-US', + timezone: 'UTC', + }, + }, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).not.toHaveProperty('k') + expect(encryptedModel.data.metadata?.profile).not.toHaveProperty('k') + expect(encryptedModel.data.metadata?.settings?.preferences).toHaveProperty( + 'c', + ) + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle null values in nested JSON objects', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '2', + email: 'test2@example.com', + metadata: { + profile: null, + settings: { + preferences: null, + }, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify null fields are preserved + expect(encryptedModel.data.email).not.toHaveProperty('k') + expect(encryptedModel.data.metadata?.profile).toBeNull() + expect(encryptedModel.data.metadata?.settings?.preferences).toBeNull() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle undefined values in nested JSON objects', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '3', + email: 'test3@example.com', + metadata: { + profile: undefined, + settings: { + preferences: undefined, + }, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify undefined fields are preserved + expect(encryptedModel.data.email).not.toHaveProperty('k') + expect(encryptedModel.data.metadata?.profile).toBeUndefined() + expect(encryptedModel.data.metadata?.settings?.preferences).toBeUndefined() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) +}) + +describe('JSON edge cases and error handling', () => { + it('should handle very large JSON objects', async () => { + const largeJson = { + data: Array.from({ length: 1000 }, (_, i) => ({ + id: i, + name: `User ${i}`, + email: `user${i}@example.com`, + metadata: { + preferences: { + theme: i % 2 === 0 ? 'dark' : 'light', + notifications: i % 3 === 0, + }, + }, + })), + metadata: { + total: 1000, + created: new Date().toISOString(), + }, + } + + const ciphertext = await protectClient.encrypt(largeJson, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: largeJson, + }) + }, 30000) + + it('should handle JSON with circular references (should fail gracefully)', async () => { + const circularObj: Record = { name: 'test' } + circularObj.self = circularObj + + try { + await protectClient.encrypt(circularObj, { + column: users.json, + table: users, + }) + // This should not reach here as JSON.stringify should fail + expect(true).toBe(false) + } catch (error) { + // Expected to fail due to circular reference + expect(error).toBeDefined() + } + }, 30000) + + it('should handle JSON with special data types', async () => { + const json = { + string: 'hello', + number: 42, + boolean: true, + null: null, + array: [1, 2, 3], + object: { nested: 'value' }, + date: new Date('2023-01-01T00:00:00Z'), + // Note: Functions and undefined are not JSON serializable + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + // Date objects get serialized to strings in JSON + const expectedJson = { + ...json, + date: '2023-01-01T00:00:00.000Z', + } + + expect(plaintext).toEqual({ + data: expectedJson, + }) + }, 30000) +}) + +describe('JSON performance tests', () => { + it('should handle large numbers of JSON objects efficiently', async () => { + const largeJsonArray = Array.from({ length: 100 }, (_, i) => ({ + id: i, + data: { + name: `User ${i}`, + preferences: { + theme: i % 2 === 0 ? 'dark' : 'light', + notifications: i % 3 === 0, + }, + metadata: { + created: new Date().toISOString(), + version: i, + }, + }, + })) + + const jsonPayloads = largeJsonArray.map((item, index) => ({ + id: `user${index}`, + plaintext: item, + })) + + const encryptedData = await protectClient.bulkEncrypt(jsonPayloads, { + column: users.json, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(100) + + // Decrypt the data + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify all data is preserved + expect(decryptedData.data).toHaveLength(100) + + for (let i = 0; i < 100; i++) { + expect(decryptedData.data[i].id).toBe(`user${i}`) + expect(decryptedData.data[i].data).toEqual(largeJsonArray[i]) + } + }, 5000) +}) + +describe('JSON advanced scenarios', () => { + it('should handle JSON with deeply nested arrays', async () => { + const json = { + users: [ + { + id: 1, + name: 'Alice', + roles: [ + { name: 'admin', permissions: ['read', 'write', 'delete'] }, + { name: 'user', permissions: ['read'] }, + ], + }, + { + id: 2, + name: 'Bob', + roles: [{ name: 'user', permissions: ['read'] }], + }, + ], + metadata: { + total: 2, + lastUpdated: new Date().toISOString(), + }, + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) + + it('should handle JSON with mixed data types in arrays', async () => { + const json = { + mixedArray: ['string', 42, true, null, { nested: 'object' }, [1, 2, 3]], + metadata: { + types: ['string', 'number', 'boolean', 'null', 'object', 'array'], + }, + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) + + it('should handle JSON with Unicode and international characters', async () => { + const json = { + international: { + chinese: '你好世界', + japanese: 'こんにちは世界', + korean: '안녕하세요 세계', + arabic: 'مرحبا بالعالم', + russian: 'Привет мир', + emoji: '🚀 🌍 💻 🎉', + }, + metadata: { + languages: ['Chinese', 'Japanese', 'Korean', 'Arabic', 'Russian'], + encoding: 'UTF-8', + }, + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) + + it('should handle JSON with scientific notation and large numbers', async () => { + const json = { + numbers: { + integer: 1234567890, + float: Math.PI, + scientific: 1.23e10, + negative: -9876543210, + zero: 0, + verySmall: 1.23e-10, + }, + metadata: { + precision: 'high', + format: 'scientific', + }, + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) + + it('should handle JSON with boolean edge cases', async () => { + const json = { + booleans: { + true: true, + false: false, + stringTrue: 'true', + stringFalse: 'false', + numberOne: 1, + numberZero: 0, + emptyString: '', + nullValue: null, + }, + metadata: { + type: 'boolean_edge_cases', + }, + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) +}) + +describe('JSON error handling and edge cases', () => { + it('should handle malformed JSON gracefully', async () => { + // This test ensures the library handles JSON serialization errors + const invalidJson = { + valid: 'data', + // This will cause JSON.stringify to fail + circular: null as unknown, + } + + // Create a circular reference + invalidJson.circular = invalidJson + + try { + await protectClient.encrypt(invalidJson, { + column: users.json, + table: users, + }) + expect(true).toBe(false) // Should not reach here + } catch (error) { + expect(error).toBeDefined() + expect(error).toBeInstanceOf(Error) + } + }, 30000) + + it('should handle empty arrays and objects', async () => { + const json = { + emptyArray: [], + emptyObject: {}, + nestedEmpty: { + array: [], + object: {}, + }, + mixedEmpty: { + data: 'present', + empty: [], + null: null, + }, + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) + + it('should handle JSON with very long strings', async () => { + const longString = 'A'.repeat(10000) // 10KB string + const json = { + longString, + metadata: { + length: longString.length, + type: 'long_string', + }, + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) + + it('should handle JSON with all primitive types', async () => { + const json = { + string: 'hello world', + number: 42, + float: 3.14, + boolean: true, + null: null, + array: [1, 2, 3], + object: { key: 'value' }, + nested: { + level1: { + level2: { + level3: 'deep value', + }, + }, + }, + } + + const ciphertext = await protectClient.encrypt(json, { + column: users.json, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).not.toHaveProperty('k') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: json, + }) + }, 30000) +}) diff --git a/packages/stack/__tests__/jsonb-helpers.test.ts b/packages/stack/__tests__/jsonb-helpers.test.ts new file mode 100644 index 00000000..9f12d131 --- /dev/null +++ b/packages/stack/__tests__/jsonb-helpers.test.ts @@ -0,0 +1,203 @@ +import { describe, expect, it } from 'vitest' +import { buildNestedObject, parseJsonbPath, toJsonPath } from '../src' + +describe('toJsonPath', () => { + it('converts simple path to JSONPath format', () => { + expect(toJsonPath('name')).toBe('$.name') + }) + + it('converts nested path to JSONPath format', () => { + expect(toJsonPath('user.email')).toBe('$.user.email') + }) + + it('converts deeply nested path', () => { + expect(toJsonPath('user.profile.settings.theme')).toBe( + '$.user.profile.settings.theme', + ) + }) + + it('returns unchanged if already in JSONPath format', () => { + expect(toJsonPath('$.user.email')).toBe('$.user.email') + }) + + it('normalizes bare $ prefix', () => { + expect(toJsonPath('$user.email')).toBe('$.user.email') + }) + + it('handles path starting with dot', () => { + expect(toJsonPath('.user.email')).toBe('$.user.email') + }) + + it('handles root path', () => { + expect(toJsonPath('$')).toBe('$') + }) + + it('handles empty string', () => { + expect(toJsonPath('')).toBe('$') + }) + + it('handles array index in path', () => { + expect(toJsonPath('user.roles[0]')).toBe('$.user.roles[0]') + }) + + it('handles array index with nested property', () => { + expect(toJsonPath('items[0].name')).toBe('$.items[0].name') + }) + + it('handles already-prefixed path with array index', () => { + expect(toJsonPath('$.data[2]')).toBe('$.data[2]') + }) + + it('handles nested array indices', () => { + expect(toJsonPath('matrix[0][1]')).toBe('$.matrix[0][1]') + }) + + it('handles array index at root level', () => { + expect(toJsonPath('[0].name')).toBe('$[0].name') + }) + + it('preserves already-prefixed root array index', () => { + expect(toJsonPath('$[0]')).toBe('$[0]') + }) + + it('preserves already-prefixed root array index with property', () => { + expect(toJsonPath('$[0].name')).toBe('$[0].name') + }) + + it('handles large array index', () => { + expect(toJsonPath('items[999].value')).toBe('$.items[999].value') + }) + + it('handles deeply nested path after array index', () => { + expect(toJsonPath('data[0].user.profile.settings')).toBe( + '$.data[0].user.profile.settings', + ) + }) + + it('handles root array with nested array', () => { + expect(toJsonPath('[0].items[1].name')).toBe('$[0].items[1].name') + }) +}) + +describe('buildNestedObject', () => { + it('builds single-level object', () => { + expect(buildNestedObject('name', 'alice')).toEqual({ name: 'alice' }) + }) + + it('builds two-level nested object', () => { + expect(buildNestedObject('user.role', 'admin')).toEqual({ + user: { role: 'admin' }, + }) + }) + + it('builds deeply nested object', () => { + expect(buildNestedObject('a.b.c.d', 'value')).toEqual({ + a: { b: { c: { d: 'value' } } }, + }) + }) + + it('handles numeric values', () => { + expect(buildNestedObject('user.age', 30)).toEqual({ + user: { age: 30 }, + }) + }) + + it('handles boolean values', () => { + expect(buildNestedObject('user.active', true)).toEqual({ + user: { active: true }, + }) + }) + + it('handles null values', () => { + expect(buildNestedObject('user.data', null)).toEqual({ + user: { data: null }, + }) + }) + + it('handles object values', () => { + const value = { nested: 'object' } + expect(buildNestedObject('user.config', value)).toEqual({ + user: { config: { nested: 'object' } }, + }) + }) + + it('handles array values', () => { + expect(buildNestedObject('user.tags', ['admin', 'user'])).toEqual({ + user: { tags: ['admin', 'user'] }, + }) + }) + + it('strips JSONPath prefix from path', () => { + expect(buildNestedObject('$.user.role', 'admin')).toEqual({ + user: { role: 'admin' }, + }) + }) + + it('throws on empty path', () => { + expect(() => buildNestedObject('', 'value')).toThrow('Path cannot be empty') + }) + + it('throws on root-only path', () => { + expect(() => buildNestedObject('$', 'value')).toThrow( + 'Path must contain at least one segment', + ) + }) + + it('throws on __proto__ segment', () => { + expect(() => buildNestedObject('__proto__.polluted', 'yes')).toThrow( + 'Path contains forbidden segment: __proto__', + ) + }) + + it('throws on prototype segment', () => { + expect(() => buildNestedObject('user.prototype.hack', 'yes')).toThrow( + 'Path contains forbidden segment: prototype', + ) + }) + + it('throws on constructor segment', () => { + expect(() => buildNestedObject('constructor', 'yes')).toThrow( + 'Path contains forbidden segment: constructor', + ) + }) + + it('throws on nested forbidden segment', () => { + expect(() => buildNestedObject('a.b.__proto__', 'yes')).toThrow( + 'Path contains forbidden segment: __proto__', + ) + }) +}) + +describe('parseJsonbPath', () => { + it('parses simple path', () => { + expect(parseJsonbPath('name')).toEqual(['name']) + }) + + it('parses nested path', () => { + expect(parseJsonbPath('user.email')).toEqual(['user', 'email']) + }) + + it('parses deeply nested path', () => { + expect(parseJsonbPath('a.b.c.d')).toEqual(['a', 'b', 'c', 'd']) + }) + + it('strips JSONPath prefix', () => { + expect(parseJsonbPath('$.user.email')).toEqual(['user', 'email']) + }) + + it('strips bare $ prefix', () => { + expect(parseJsonbPath('$user.email')).toEqual(['user', 'email']) + }) + + it('handles empty string', () => { + expect(parseJsonbPath('')).toEqual([]) + }) + + it('handles root only', () => { + expect(parseJsonbPath('$')).toEqual([]) + }) + + it('filters empty segments', () => { + expect(parseJsonbPath('user..email')).toEqual(['user', 'email']) + }) +}) diff --git a/packages/stack/__tests__/keysets.test.ts b/packages/stack/__tests__/keysets.test.ts new file mode 100644 index 00000000..5f093993 --- /dev/null +++ b/packages/stack/__tests__/keysets.test.ts @@ -0,0 +1,89 @@ +import 'dotenv/config' +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { describe, expect, it } from 'vitest' +import { Encryption } from '../src' + +const users = encryptedTable('users', { + email: encryptedColumn('email'), +}) + +describe('encryption and decryption with keyset id', () => { + it('should encrypt and decrypt a payload', async () => { + const protectClient = await Encryption({ + schemas: [users], + keyset: { + id: '4152449b-505a-4186-93b6-d3d87eba7a47', + }, + }) + + const email = 'hello@example.com' + + const ciphertext = await protectClient.encrypt(email, { + column: users.email, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).toHaveProperty('c') + + const a = ciphertext.data + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: email, + }) + }, 30000) +}) + +describe('encryption and decryption with keyset name', () => { + it('should encrypt and decrypt a payload', async () => { + const protectClient = await Encryption({ + schemas: [users], + keyset: { + name: 'Test', + }, + }) + + const email = 'hello@example.com' + + const ciphertext = await protectClient.encrypt(email, { + column: users.email, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).toHaveProperty('c') + + const a = ciphertext.data + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: email, + }) + }, 30000) +}) + +describe('encryption and decryption with invalid keyset id', () => { + it('should throw an error', async () => { + await expect( + Encryption({ + schemas: [users], + keyset: { + id: 'invalid-uuid', + }, + }), + ).rejects.toThrow( + '[encryption]: Invalid UUID provided for keyset id. Must be a valid UUID.', + ) + }) +}) diff --git a/packages/stack/__tests__/lock-context.test.ts b/packages/stack/__tests__/lock-context.test.ts new file mode 100644 index 00000000..53e392ad --- /dev/null +++ b/packages/stack/__tests__/lock-context.test.ts @@ -0,0 +1,208 @@ +import 'dotenv/config' +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { beforeAll, describe, expect, it } from 'vitest' +import { Encryption } from '../src' +import { LockContext } from '../src/identify' + +const users = encryptedTable('users', { + email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + address: encryptedColumn('address').freeTextSearch(), +}) + +type User = { + id: string + email?: string | null + createdAt?: Date + updatedAt?: Date + address?: string | null + number?: number +} + +let protectClient: Awaited> + +beforeAll(async () => { + protectClient = await Encryption({ + schemas: [users], + }) +}) + +describe('encryption and decryption with lock context', () => { + it('should encrypt and decrypt a payload with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + const email = 'hello@example.com' + + const ciphertext = await protectClient + .encrypt(email, { + column: users.email, + table: users, + }) + .withLockContext(lockContext.data) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + const plaintext = await protectClient + .decrypt(ciphertext.data) + .withLockContext(lockContext.data) + + if (plaintext.failure) { + throw new Error(`[protect]: ${plaintext.failure.message}`) + } + + expect(plaintext.data).toEqual(email) + }, 30000) + + it('should encrypt and decrypt a model with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + // Create a model with decrypted values + const decryptedModel = { + id: '1', + email: 'plaintext', + } + + // Encrypt the model with lock context + const encryptedModel = await protectClient + .encryptModel(decryptedModel, users) + .withLockContext(lockContext.data) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Decrypt the model with lock context + const decryptedResult = await protectClient + .decryptModel(encryptedModel.data) + .withLockContext(lockContext.data) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual({ + id: '1', + email: 'plaintext', + }) + }, 30000) + + it('should encrypt with context and be unable to decrypt without context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + // Create a model with decrypted values + const decryptedModel = { + id: '1', + email: 'plaintext', + } + + // Encrypt the model with lock context + const encryptedModel = await protectClient + .encryptModel(decryptedModel, users) + .withLockContext(lockContext.data) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + try { + await protectClient.decryptModel(encryptedModel.data) + } catch (error) { + const e = error as Error + expect(e.message.startsWith('Failed to retrieve key')).toEqual(true) + } + }, 30000) + + it('should bulk encrypt and decrypt models with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + // Create models with decrypted values + const decryptedModels = [ + { + id: '1', + email: 'test', + }, + { + id: '2', + email: 'test2', + }, + ] + + // Encrypt the models with lock context + const encryptedModels = await protectClient + .bulkEncryptModels(decryptedModels, users) + .withLockContext(lockContext.data) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Decrypt the models with lock context + const decryptedResult = await protectClient + .bulkDecryptModels(encryptedModels.data) + .withLockContext(lockContext.data) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual([ + { + id: '1', + email: 'test', + }, + { + id: '2', + email: 'test2', + }, + ]) + }, 30000) +}) diff --git a/packages/stack/__tests__/nested-models.test.ts b/packages/stack/__tests__/nested-models.test.ts new file mode 100644 index 00000000..98bb25b0 --- /dev/null +++ b/packages/stack/__tests__/nested-models.test.ts @@ -0,0 +1,962 @@ +import 'dotenv/config' +import { + encryptedColumn, + encryptedTable, + encryptedValue, +} from '@cipherstash/schema' +import { describe, expect, it, vi } from 'vitest' +import { Encryption, LockContext } from '../src' + +const users = encryptedTable('users', { + email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + address: encryptedColumn('address').freeTextSearch(), + name: encryptedColumn('name').freeTextSearch(), + example: { + field: encryptedValue('example.field'), + nested: { + deeper: encryptedValue('example.nested.deeper'), + }, + }, +}) + +type User = { + id: string + email?: string | null + createdAt?: Date + updatedAt?: Date + address?: string | null + notEncrypted?: string | null + example: { + field: string | undefined | null + nested?: { + deeper: string | undefined | null + plaintext?: string | undefined | null + notInSchema?: { + deeper: string | undefined | null + } + deeperNotInSchema?: string | undefined | null + extra?: { + plaintext: string | undefined | null + } + } + plaintext?: string | undefined | null + fieldNotInSchema?: string | undefined | null + notInSchema?: { + deeper: string | undefined | null + } + } +} + +describe('encrypt models with nested fields', () => { + it('should encrypt and decrypt a single value from a nested schema', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const encryptResponse = await protectClient.encrypt('hello world', { + column: users.example.field, + table: users, + }) + + if (encryptResponse.failure) { + throw new Error(`[protect]: ${encryptResponse.failure.message}`) + } + + // Verify encrypted field + expect(encryptResponse.data).toHaveProperty('c') + + const decryptResponse = await protectClient.decrypt(encryptResponse.data) + + if (decryptResponse.failure) { + throw new Error(`[protect]: ${decryptResponse.failure.message}`) + } + + expect(decryptResponse).toEqual({ + data: 'hello world', + }) + }) + + it('should encrypt and decrypt a model with nested fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '1', + email: 'test@example.com', + address: '123 Main St', + notEncrypted: 'not encrypted', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + example: { + field: 'test', + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.address).toHaveProperty('c') + expect(encryptedModel.data.example.field).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.notEncrypted).toBe('not encrypted') + expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle null values in nested fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '2', + email: null, + address: null, + example: { + field: null, + nested: { + deeper: null, + }, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify null fields are preserved + expect(encryptedModel.data.email).toBeNull() + expect(encryptedModel.data.address).toBeNull() + expect(encryptedModel.data.example.field).toBeNull() + expect(encryptedModel.data.example.nested?.deeper).toBeNull() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle undefined values in nested fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '3', + example: { + field: undefined, + nested: { + deeper: undefined, + }, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify undefined fields are preserved + expect(encryptedModel.data.email).toBeUndefined() + expect(encryptedModel.data.example.field).toBeUndefined() + expect(encryptedModel.data.example.nested?.deeper).toBeUndefined() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle mixed null and undefined values in nested fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '4', + email: 'test@example.com', + address: undefined, + notEncrypted: 'not encrypted', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + example: { + field: null, + nested: { + deeper: undefined, + }, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + + // Verify null/undefined fields are preserved + expect(encryptedModel.data.address).toBeUndefined() + expect(encryptedModel.data.example.field).toBeNull() + expect(encryptedModel.data.example.nested?.deeper).toBeUndefined() + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('4') + expect(encryptedModel.data.notEncrypted).toBe('not encrypted') + expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle deeply nested fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '3', + example: { + field: 'outer', + nested: { + deeper: 'inner value', + }, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.example.field).toHaveProperty('c') + expect(encryptedModel.data.example.nested?.deeper).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('3') + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle missing optional nested fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '5', + example: { + field: 'present', + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.example.field).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('5') + expect(encryptedModel.data.example.nested).toBeUndefined() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + describe('bulk operations with nested fields', () => { + it('should handle bulk encryption and decryption of models with nested fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModels: User[] = [ + { + id: '1', + email: 'test1@example.com', + example: { + field: 'test1', + nested: { + deeper: 'value1', + }, + }, + }, + { + id: '2', + email: 'test2@example.com', + example: { + field: 'test2', + nested: { + deeper: 'value2', + }, + }, + }, + ] + + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields for each model + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].example.field).toHaveProperty('c') + expect(encryptedModels.data[0].example.nested?.deeper).toHaveProperty('c') + expect(encryptedModels.data[1].email).toHaveProperty('c') + expect(encryptedModels.data[1].example.field).toHaveProperty('c') + expect(encryptedModels.data[1].example.nested?.deeper).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[1].id).toBe('2') + + const decryptedResults = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResults.failure) { + throw new Error(`[protect]: ${decryptedResults.failure.message}`) + } + + expect(decryptedResults.data).toEqual(decryptedModels) + }, 30000) + + it('should handle bulk operations with null and undefined values in nested fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModels: User[] = [ + { + id: '1', + email: null, + example: { + field: null, + nested: { + deeper: undefined, + }, + }, + }, + { + id: '2', + email: undefined, + example: { + field: undefined, + nested: { + deeper: null, + }, + }, + }, + ] + + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify null/undefined fields are preserved + expect(encryptedModels.data[0].email).toBeNull() + expect(encryptedModels.data[0].example.field).toBeNull() + expect(encryptedModels.data[0].example.nested?.deeper).toBeUndefined() + expect(encryptedModels.data[1].email).toBeUndefined() + expect(encryptedModels.data[1].example.field).toBeUndefined() + expect(encryptedModels.data[1].example.nested?.deeper).toBeNull() + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[1].id).toBe('2') + + const decryptedResults = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResults.failure) { + throw new Error(`[protect]: ${decryptedResults.failure.message}`) + } + + expect(decryptedResults.data).toEqual(decryptedModels) + }, 30000) + + it('should handle bulk operations with missing optional nested fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModels: User[] = [ + { + id: '1', + email: 'test1@example.com', + example: { + field: 'test1', + }, + }, + { + id: '2', + email: 'test2@example.com', + example: { + field: 'test2', + nested: { + deeper: 'value2', + }, + }, + }, + ] + + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields for each model + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].example.field).toHaveProperty('c') + expect(encryptedModels.data[1].email).toHaveProperty('c') + expect(encryptedModels.data[1].example.field).toHaveProperty('c') + expect(encryptedModels.data[1].example.nested?.deeper).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].example.nested).toBeUndefined() + expect(encryptedModels.data[1].id).toBe('2') + + const decryptedResults = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResults.failure) { + throw new Error(`[protect]: ${decryptedResults.failure.message}`) + } + + expect(decryptedResults.data).toEqual(decryptedModels) + }, 30000) + + it('should handle empty array in bulk operations', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModels: User[] = [] + + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + expect(encryptedModels.data).toEqual([]) + + const decryptedResults = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResults.failure) { + throw new Error(`[protect]: ${decryptedResults.failure.message}`) + } + + expect(decryptedResults.data).toEqual([]) + }, 30000) + }) +}) + +describe('nested fields with a plaintext field', () => { + it('should handle nested fields with a plaintext field', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '1', + email: 'test@example.com', + address: '123 Main St', + notEncrypted: 'not encrypted', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + example: { + field: 'test', + plaintext: 'plaintext', + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.address).toHaveProperty('c') + expect(encryptedModel.data.example.field).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.notEncrypted).toBe('not encrypted') + expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.example.plaintext).toBe('plaintext') + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }) + + it('should handle multiple plaintext fields at different nesting levels', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '1', + email: 'test@example.com', + address: '123 Main St', + notEncrypted: 'not encrypted', + example: { + field: 'encrypted field', + plaintext: 'top level plaintext', + nested: { + deeper: 'encrypted deeper', + plaintext: 'nested plaintext', + extra: { + plaintext: 'deeply nested plaintext', + }, + }, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.address).toHaveProperty('c') + expect(encryptedModel.data.example.field).toHaveProperty('c') + expect(encryptedModel.data.example.nested?.deeper).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.notEncrypted).toBe('not encrypted') + expect(encryptedModel.data.example.plaintext).toBe('top level plaintext') + expect(encryptedModel.data.example.nested?.plaintext).toBe( + 'nested plaintext', + ) + expect(encryptedModel.data.example.nested?.extra?.plaintext).toBe( + 'deeply nested plaintext', + ) + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }) + + it('should handle partial path matches in nested objects', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '1', + email: 'test@example.com', + example: { + field: 'encrypted field', + nested: { + deeper: 'encrypted deeper', + // This should not be encrypted as it's not in the schema + notInSchema: { + deeper: 'not encrypted', + }, + }, + // This should not be encrypted as it's not in the schema + notInSchema: { + deeper: 'not encrypted', + }, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.example.field).toHaveProperty('c') + expect(encryptedModel.data.example.nested?.deeper).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.example.nested?.notInSchema?.deeper).toBe( + 'not encrypted', + ) + expect(encryptedModel.data.example.notInSchema?.deeper).toBe( + 'not encrypted', + ) + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }) + + it('should handle mixed encrypted and plaintext fields with similar paths', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '1', + email: 'test@example.com', + example: { + field: 'encrypted field', + fieldNotInSchema: 'not encrypted', + nested: { + deeper: 'encrypted deeper', + deeperNotInSchema: 'not encrypted', + }, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.example.field).toHaveProperty('c') + expect(encryptedModel.data.example.nested?.deeper).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.example.fieldNotInSchema).toBe('not encrypted') + expect(encryptedModel.data.example.nested?.deeperNotInSchema).toBe( + 'not encrypted', + ) + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }) + + describe('bulk operations with plaintext fields', () => { + it('should handle bulk encryption and decryption with plaintext fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModels: User[] = [ + { + id: '1', + email: 'test1@example.com', + address: '123 Main St', + example: { + field: 'encrypted field 1', + plaintext: 'plaintext 1', + nested: { + deeper: 'encrypted deeper 1', + plaintext: 'nested plaintext 1', + }, + }, + }, + { + id: '2', + email: 'test2@example.com', + address: '456 Main St', + example: { + field: 'encrypted field 2', + plaintext: 'plaintext 2', + nested: { + deeper: 'encrypted deeper 2', + plaintext: 'nested plaintext 2', + }, + }, + }, + ] + + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].address).toHaveProperty('c') + expect(encryptedModels.data[0].example.field).toHaveProperty('c') + expect(encryptedModels.data[0].example.nested?.deeper).toHaveProperty('c') + expect(encryptedModels.data[1].email).toHaveProperty('c') + expect(encryptedModels.data[1].address).toHaveProperty('c') + expect(encryptedModels.data[1].example.field).toHaveProperty('c') + expect(encryptedModels.data[1].example.nested?.deeper).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].example.plaintext).toBe('plaintext 1') + expect(encryptedModels.data[0].example.nested?.plaintext).toBe( + 'nested plaintext 1', + ) + expect(encryptedModels.data[1].id).toBe('2') + expect(encryptedModels.data[1].example.plaintext).toBe('plaintext 2') + expect(encryptedModels.data[1].example.nested?.plaintext).toBe( + 'nested plaintext 2', + ) + + const decryptedResults = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResults.failure) { + throw new Error(`[protect]: ${decryptedResults.failure.message}`) + } + + expect(decryptedResults.data).toEqual(decryptedModels) + }) + + it('should handle bulk operations with mixed encrypted and non-encrypted fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModels: User[] = [ + { + id: '1', + email: 'test1@example.com', + example: { + field: 'encrypted field 1', + fieldNotInSchema: 'not encrypted 1', + nested: { + deeper: 'encrypted deeper 1', + deeperNotInSchema: 'not encrypted deeper 1', + }, + }, + }, + { + id: '2', + email: 'test2@example.com', + example: { + field: 'encrypted field 2', + fieldNotInSchema: 'not encrypted 2', + nested: { + deeper: 'encrypted deeper 2', + deeperNotInSchema: 'not encrypted deeper 2', + }, + }, + }, + ] + + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].example.field).toHaveProperty('c') + expect(encryptedModels.data[0].example.nested?.deeper).toHaveProperty('c') + expect(encryptedModels.data[1].email).toHaveProperty('c') + expect(encryptedModels.data[1].example.field).toHaveProperty('c') + expect(encryptedModels.data[1].example.nested?.deeper).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].example.fieldNotInSchema).toBe( + 'not encrypted 1', + ) + expect(encryptedModels.data[0].example.nested?.deeperNotInSchema).toBe( + 'not encrypted deeper 1', + ) + expect(encryptedModels.data[1].id).toBe('2') + expect(encryptedModels.data[1].example.fieldNotInSchema).toBe( + 'not encrypted 2', + ) + expect(encryptedModels.data[1].example.nested?.deeperNotInSchema).toBe( + 'not encrypted deeper 2', + ) + + const decryptedResults = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResults.failure) { + throw new Error(`[protect]: ${decryptedResults.failure.message}`) + } + + expect(decryptedResults.data).toEqual(decryptedModels) + }) + + it('should handle bulk operations with deeply nested plaintext fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModels: User[] = [ + { + id: '1', + email: 'test1@example.com', + example: { + field: 'encrypted field 1', + nested: { + deeper: 'encrypted deeper 1', + extra: { + plaintext: 'deeply nested plaintext 1', + }, + }, + }, + }, + { + id: '2', + email: 'test2@example.com', + example: { + field: 'encrypted field 2', + nested: { + deeper: 'encrypted deeper 2', + extra: { + plaintext: 'deeply nested plaintext 2', + }, + }, + }, + }, + ] + + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].example.field).toHaveProperty('c') + expect(encryptedModels.data[0].example.nested?.deeper).toHaveProperty('c') + expect(encryptedModels.data[1].email).toHaveProperty('c') + expect(encryptedModels.data[1].example.field).toHaveProperty('c') + expect(encryptedModels.data[1].example.nested?.deeper).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].example.nested?.extra?.plaintext).toBe( + 'deeply nested plaintext 1', + ) + expect(encryptedModels.data[1].id).toBe('2') + expect(encryptedModels.data[1].example.nested?.extra?.plaintext).toBe( + 'deeply nested plaintext 2', + ) + + const decryptedResults = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResults.failure) { + throw new Error(`[protect]: ${decryptedResults.failure.message}`) + } + + expect(decryptedResults.data).toEqual(decryptedModels) + }) + }) +}) diff --git a/packages/stack/__tests__/number-protect.test.ts b/packages/stack/__tests__/number-protect.test.ts new file mode 100644 index 00000000..abafbb0d --- /dev/null +++ b/packages/stack/__tests__/number-protect.test.ts @@ -0,0 +1,835 @@ +import 'dotenv/config' +import { + encryptedColumn, + encryptedTable, + encryptedValue, +} from '@cipherstash/schema' +import { beforeAll, describe, expect, it, test } from 'vitest' +import { Encryption, LockContext } from '../src' + +const users = encryptedTable('users', { + email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + address: encryptedColumn('address').freeTextSearch(), + age: encryptedColumn('age').dataType('number').equality().orderAndRange(), + score: encryptedColumn('score').dataType('number').equality().orderAndRange(), + metadata: { + count: encryptedValue('metadata.count').dataType('number'), + level: encryptedValue('metadata.level').dataType('number'), + }, +}) + +type User = { + id: string + email?: string + createdAt?: Date + updatedAt?: Date + address?: string + age?: number + score?: number + metadata?: { + count?: number + level?: number + } +} + +let protectClient: Awaited> + +beforeAll(async () => { + protectClient = await Encryption({ + schemas: [users], + }) +}) + +const cases = [ + 25, + 0, + -42, + 2147483647, + 77.9, + 0.0, + -117.123456, + 1e15, + -1e15, // Very large floats + 9007199254740991, // Max safe integer in JavaScript +] + +describe('Number encryption and decryption', () => { + test.each(cases)( + 'should encrypt and decrypt a number: %d', + async (age) => { + const ciphertext = await protectClient.encrypt(age, { + column: users.age, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).toHaveProperty('c') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: age, + }) + }, + 30000, + ) + + it('should handle null integer', async () => { + const ciphertext = await protectClient.encrypt(null, { + column: users.age, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify null is preserved + expect(ciphertext.data).toBeNull() + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: null, + }) + }, 30000) + + // Special case + it('should treat a negative zero valued float as 0.0', async () => { + const score = -0.0 + + const ciphertext = await protectClient.encrypt(score, { + column: users.score, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).toHaveProperty('c') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: 0.0, + }) + }, 30000) + + // Special case + it('should error for a NaN float', async () => { + const score = Number.NaN + + const result = await protectClient.encrypt(score, { + column: users.score, + table: users, + }) + + expect(result.failure).toBeDefined() + expect(result.failure?.message).toContain('Cannot encrypt NaN value') + }, 30000) + + // Special case + it('should error for Infinity', async () => { + const score = Number.POSITIVE_INFINITY + + const result = await protectClient.encrypt(score, { + column: users.score, + table: users, + }) + + expect(result.failure).toBeDefined() + expect(result.failure?.message).toContain('Cannot encrypt Infinity value') + }, 30000) + + // Special case + it('should error for -Infinity', async () => { + const score = Number.NEGATIVE_INFINITY + + const result = await protectClient.encrypt(score, { + column: users.score, + table: users, + }) + + expect(result.failure).toBeDefined() + expect(result.failure?.message).toContain('Cannot encrypt Infinity value') + }, 30000) +}) + +describe('Model encryption and decryption', () => { + it('should encrypt and decrypt a model with number fields', async () => { + const decryptedModel = { + id: '1', + email: 'test@example.com', + address: '123 Main St', + age: 30, + score: 95, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.address).toHaveProperty('c') + expect(encryptedModel.data.age).toHaveProperty('c') + expect(encryptedModel.data.score).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle null numbers in model', async () => { + const decryptedModel: User = { + id: '2', + email: 'test2@example.com', + address: '456 Oak St', + age: undefined, + score: undefined, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.address).toHaveProperty('c') + expect(encryptedModel.data.age).toBeUndefined() + expect(encryptedModel.data.score).toBeUndefined() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle undefined numbers in model', async () => { + const decryptedModel = { + id: '3', + email: 'test3@example.com', + address: '789 Pine St', + age: undefined, + score: undefined, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.address).toHaveProperty('c') + expect(encryptedModel.data.age).toBeUndefined() + expect(encryptedModel.data.score).toBeUndefined() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) +}) + +describe('Bulk encryption and decryption', () => { + it('should bulk encrypt and decrypt number payloads', async () => { + const intPayloads = [ + { id: 'user1', plaintext: 25 }, + { id: 'user2', plaintext: 30.7 }, + { id: 'user3', plaintext: -35.123 }, + ] + + const encryptedData = await protectClient.bulkEncrypt(intPayloads, { + column: users.age, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(3) + expect(encryptedData.data[0]).toHaveProperty('id', 'user1') + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).toHaveProperty('c') + expect(encryptedData.data[1]).toHaveProperty('id', 'user2') + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).toHaveProperty('c') + expect(encryptedData.data[2]).toHaveProperty('id', 'user3') + expect(encryptedData.data[2]).toHaveProperty('data') + expect(encryptedData.data[2].data).toHaveProperty('c') + + // Forward compatibility: new encryptions should NOT have k field + expect(encryptedData.data[0].data).not.toHaveProperty('k') + expect(encryptedData.data[1].data).not.toHaveProperty('k') + expect(encryptedData.data[2].data).not.toHaveProperty('k') + + // Verify all encrypted values are different + const getCiphertext = (data: { c?: unknown } | null | undefined) => data?.c + + expect(getCiphertext(encryptedData.data[0].data)).not.toBe( + getCiphertext(encryptedData.data[1].data), + ) + expect(getCiphertext(encryptedData.data[1].data)).not.toBe( + getCiphertext(encryptedData.data[2].data), + ) + expect(getCiphertext(encryptedData.data[0].data)).not.toBe( + getCiphertext(encryptedData.data[2].data), + ) + + // Now decrypt the data + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify decrypted data + expect(decryptedData.data).toHaveLength(3) + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('data', 25) + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', 30.7) + expect(decryptedData.data[2]).toHaveProperty('id', 'user3') + expect(decryptedData.data[2]).toHaveProperty('data', -35.123) + }, 30000) + + it('should handle mixed null and non-null numbers in bulk operations', async () => { + const intPayloads = [ + { id: 'user1', plaintext: 25 }, + { id: 'user2', plaintext: null }, + { id: 'user3', plaintext: 35 }, + ] + + const encryptedData = await protectClient.bulkEncrypt(intPayloads, { + column: users.age, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(3) + expect(encryptedData.data[0]).toHaveProperty('id', 'user1') + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).toHaveProperty('c') + expect(encryptedData.data[1]).toHaveProperty('id', 'user2') + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).toBeNull() + expect(encryptedData.data[2]).toHaveProperty('id', 'user3') + expect(encryptedData.data[2]).toHaveProperty('data') + expect(encryptedData.data[2].data).toHaveProperty('c') + + // Now decrypt the data + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify decrypted data + expect(decryptedData.data).toHaveLength(3) + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('data', 25) + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', null) + expect(decryptedData.data[2]).toHaveProperty('id', 'user3') + expect(decryptedData.data[2]).toHaveProperty('data', 35) + }, 30000) + + it('should bulk encrypt and decrypt models with number fields', async () => { + const decryptedModels = [ + { + id: '1', + email: 'test1@example.com', + address: '123 Main St', + age: 25, + score: 85, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + }, + { + id: '2', + email: 'test2@example.com', + address: '456 Oak St', + age: 30, + score: 90, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + }, + ] + + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields for each model + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].address).toHaveProperty('c') + expect(encryptedModels.data[0].age).toHaveProperty('c') + expect(encryptedModels.data[0].score).toHaveProperty('c') + expect(encryptedModels.data[1].email).toHaveProperty('c') + expect(encryptedModels.data[1].address).toHaveProperty('c') + expect(encryptedModels.data[1].age).toHaveProperty('c') + expect(encryptedModels.data[1].score).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].id).toBe('2') + expect(encryptedModels.data[1].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].updatedAt).toEqual(new Date('2021-01-01')) + + const decryptedResult = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModels) + }, 30000) +}) + +describe('Encryption with lock context', () => { + it('should encrypt and decrypt number with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + const age = 42 + + const ciphertext = await protectClient + .encrypt(age, { + column: users.age, + table: users, + }) + .withLockContext(lockContext.data) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).toHaveProperty('c') + + const plaintext = await protectClient + .decrypt(ciphertext.data) + .withLockContext(lockContext.data) + + if (plaintext.failure) { + throw new Error(`[protect]: ${plaintext.failure.message}`) + } + + expect(plaintext.data).toEqual(age) + }, 30000) + + it('should encrypt model with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + const decryptedModel = { + id: '1', + email: 'test@example.com', + age: 30, + score: 95, + } + + const encryptedModel = await protectClient + .encryptModel(decryptedModel, users) + .withLockContext(lockContext.data) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.age).toHaveProperty('c') + expect(encryptedModel.data.score).toHaveProperty('c') + + const decryptedResult = await protectClient + .decryptModel(encryptedModel.data) + .withLockContext(lockContext.data) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should bulk encrypt numbers with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + const intPayloads = [ + { id: 'user1', plaintext: 25 }, + { id: 'user2', plaintext: 30 }, + ] + + const encryptedData = await protectClient + .bulkEncrypt(intPayloads, { + column: users.age, + table: users, + }) + .withLockContext(lockContext.data) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(2) + expect(encryptedData.data[0]).toHaveProperty('id', 'user1') + expect(encryptedData.data[0]).toHaveProperty('data') + expect(encryptedData.data[0].data).toHaveProperty('c') + expect(encryptedData.data[1]).toHaveProperty('id', 'user2') + expect(encryptedData.data[1]).toHaveProperty('data') + expect(encryptedData.data[1].data).toHaveProperty('c') + + // Decrypt with lock context + const decryptedData = await protectClient + .bulkDecrypt(encryptedData.data) + .withLockContext(lockContext.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify decrypted data + expect(decryptedData.data).toHaveLength(2) + expect(decryptedData.data[0]).toHaveProperty('id', 'user1') + expect(decryptedData.data[0]).toHaveProperty('data', 25) + expect(decryptedData.data[1]).toHaveProperty('id', 'user2') + expect(decryptedData.data[1]).toHaveProperty('data', 30) + }, 30000) +}) + +describe('Nested object encryption', () => { + it('should encrypt and decrypt nested number objects', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '1', + email: 'test@example.com', + metadata: { + count: 100, + level: 5, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.metadata?.count).toHaveProperty('c') + expect(encryptedModel.data.metadata?.level).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle null values in nested objects with number fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel: User = { + id: '2', + email: 'test2@example.com', + metadata: { + count: undefined, + level: undefined, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify null fields are preserved + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.metadata?.count).toBeUndefined() + expect(encryptedModel.data.metadata?.level).toBeUndefined() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) + + it('should handle undefined values in nested objects with number fields', async () => { + const protectClient = await Encryption({ schemas: [users] }) + + const decryptedModel = { + id: '3', + email: 'test3@example.com', + metadata: { + count: undefined, + level: undefined, + }, + } + + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify undefined fields are preserved + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.metadata?.count).toBeUndefined() + expect(encryptedModel.data.metadata?.level).toBeUndefined() + + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModel) + }, 30000) +}) + +describe('encryptQuery for numbers', () => { + it('should create encrypted query for number fields', async () => { + const result = await protectClient.encryptQuery([ + { value: 25, column: users.age, table: users, queryType: 'equality' }, + { value: 100, column: users.score, table: users, queryType: 'equality' }, + ]) + + if (result.failure) { + throw new Error(`[protect]: ${result.failure.message}`) + } + + expect(result.data).toHaveLength(2) + expect(result.data[0]).toHaveProperty('v', 2) + expect(result.data[1]).toHaveProperty('v', 2) + }, 30000) +}) + +describe('Performance tests', () => { + it('should handle large numbers of numbers efficiently', async () => { + const largeNumArray = Array.from({ length: 100 }, (_, i) => ({ + id: i, + data: { + age: i + 18, // Ages 18-117 + score: (i % 100) + 1, // Scores 1-100 + }, + })) + + const numPayloads = largeNumArray.map((item, index) => ({ + id: `user${index}`, + plaintext: item.data.age, + })) + + const encryptedData = await protectClient.bulkEncrypt(numPayloads, { + column: users.age, + table: users, + }) + + if (encryptedData.failure) { + throw new Error(`[protect]: ${encryptedData.failure.message}`) + } + + // Verify structure + expect(encryptedData.data).toHaveLength(100) + + // Decrypt the data + const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + + if (decryptedData.failure) { + throw new Error(`[protect]: ${decryptedData.failure.message}`) + } + + // Verify all data is preserved + expect(decryptedData.data).toHaveLength(100) + + for (let i = 0; i < 100; i++) { + expect(decryptedData.data[i].id).toBe(`user${i}`) + expect(decryptedData.data[i].data).toEqual(largeNumArray[i].data.age) + } + }, 60000) +}) + +describe('Advanced scenarios', () => { + it('should handle boundary values', async () => { + const boundaryValues = [ + Number.MIN_SAFE_INTEGER, + -2147483648, // Min 32-bit signed integer + -1, + 0, + 1, + 2147483647, // Max 32-bit signed integer + Number.MAX_SAFE_INTEGER, + ] + + for (const value of boundaryValues) { + const ciphertext = await protectClient.encrypt(value, { + column: users.age, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify encrypted field + expect(ciphertext.data).toHaveProperty('c') + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: value, + }) + } + }, 30000) +}) + +const invalidPlaintexts = [ + '400', + 'aaa', + '100a', + '73.51', + {}, + [], + [123], + { num: 123 }, +] + +describe('Invalid or uncoercable values', () => { + test.each(invalidPlaintexts)( + 'should fail to encrypt', + async (input) => { + const result = await protectClient.encrypt(input, { + column: users.age, + table: users, + }) + + expect(result.failure).toBeDefined() + expect(result.failure?.message).toContain('Cannot convert') + }, + 30000, + ) +}) diff --git a/packages/stack/__tests__/protect-ops.test.ts b/packages/stack/__tests__/protect-ops.test.ts new file mode 100644 index 00000000..b31e4f11 --- /dev/null +++ b/packages/stack/__tests__/protect-ops.test.ts @@ -0,0 +1,843 @@ +import 'dotenv/config' +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { beforeAll, describe, expect, it } from 'vitest' +import { Encryption, LockContext } from '../src' + +const users = encryptedTable('users', { + email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + address: encryptedColumn('address').freeTextSearch(), +}) + +type User = { + id: string + email?: string | null + createdAt?: Date + updatedAt?: Date + address?: string | null + number?: number +} + +let protectClient: Awaited> + +beforeAll(async () => { + protectClient = await Encryption({ + schemas: [users], + }) +}) + +describe('encryption and decryption edge cases', () => { + it('should return null if plaintext is null', async () => { + const ciphertext = await protectClient.encrypt(null, { + column: users.email, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + // Verify null is preserved + expect(ciphertext.data).toBeNull() + + const plaintext = await protectClient.decrypt(ciphertext.data) + + expect(plaintext).toEqual({ + data: null, + }) + }, 30000) + + it('should encrypt and decrypt a model', async () => { + // Create a model with decrypted values + const decryptedModel = { + id: '1', + email: 'plaintext', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + address: '123 Main St', + number: 1, + } + + // Encrypt the model + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify encrypted fields + expect(encryptedModel.data.email).toHaveProperty('c') + expect(encryptedModel.data.address).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.number).toBe(1) + + // Decrypt the model + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual({ + id: '1', + email: 'plaintext', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + address: '123 Main St', + number: 1, + }) + }, 30000) + + it('should handle null values in a model', async () => { + // Create a model with null values + const decryptedModel = { + id: '1', + email: null, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + address: null, + } + + // Encrypt the model + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify null fields are preserved + expect(encryptedModel.data.email).toBeNull() + expect(encryptedModel.data.address).toBeNull() + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.number).toBe(1) + + // Decrypt the model + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual({ + id: '1', + email: null, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + address: null, + }) + }, 30000) + + it('should handle undefined values in a model', async () => { + // Create a model with undefined values + const decryptedModel = { + id: '1', + email: undefined, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + address: null, + } + + // Encrypt the model + const encryptedModel = await protectClient.encryptModel( + decryptedModel, + users, + ) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Verify undefined fields are preserved + expect(encryptedModel.data.email).toBeUndefined() + expect(encryptedModel.data.address).toBeNull() + + // Verify non-encrypted fields remain unchanged + expect(encryptedModel.data.id).toBe('1') + expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModel.data.number).toBe(1) + + // Decrypt the model + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual({ + id: '1', + email: undefined, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + address: null, + }) + }, 30000) +}) + +describe('bulk encryption', () => { + it('should bulk encrypt and decrypt models', async () => { + // Create models with decrypted values + const decryptedModels = [ + { + id: '1', + email: 'test', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + address: '123 Main St', + }, + { + id: '2', + email: 'test2', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 2, + address: null, + }, + ] + + // Encrypt the models + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields for each model + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].address).toHaveProperty('c') + expect(encryptedModels.data[1].email).toHaveProperty('c') + expect(encryptedModels.data[1].address).toBeNull() + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].number).toBe(1) + expect(encryptedModels.data[1].id).toBe('2') + expect(encryptedModels.data[1].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].number).toBe(2) + + // Decrypt the models + const decryptedResult = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual([ + { + id: '1', + email: 'test', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + address: '123 Main St', + }, + { + id: '2', + email: 'test2', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 2, + address: null, + }, + ]) + }, 30000) + + it('should return empty array if models is empty', async () => { + // Encrypt empty array of models + const encryptedModels = await protectClient.bulkEncryptModels( + [], + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + expect(encryptedModels.data).toEqual([]) + }, 30000) + + it('should return empty array if decrypting empty array of models', async () => { + // Decrypt empty array of models + const decryptedResult = await protectClient.bulkDecryptModels([]) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual([]) + }, 30000) +}) + +describe('bulk encryption edge cases', () => { + it('should handle mixed null and non-null values in bulk operations', async () => { + const decryptedModels = [ + { + id: '1', + email: 'test1', + address: null, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + }, + { + id: '2', + email: null, + address: '123 Main St', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 2, + }, + { + id: '3', + email: 'test3', + address: '456 Oak St', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 3, + }, + ] + + // Encrypt the models + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields for each model + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].address).toBeNull() + expect(encryptedModels.data[1].email).toBeNull() + expect(encryptedModels.data[1].address).toHaveProperty('c') + expect(encryptedModels.data[2].email).toHaveProperty('c') + expect(encryptedModels.data[2].address).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].number).toBe(1) + expect(encryptedModels.data[1].id).toBe('2') + expect(encryptedModels.data[1].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].number).toBe(2) + expect(encryptedModels.data[2].id).toBe('3') + expect(encryptedModels.data[2].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[2].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[2].number).toBe(3) + + // Decrypt the models + const decryptedResult = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModels) + }, 30000) + + it('should handle mixed undefined and non-undefined values in bulk operations', async () => { + const decryptedModels = [ + { + id: '1', + email: 'test1', + address: undefined, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + }, + { + id: '2', + email: null, + address: '123 Main St', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 2, + }, + { + id: '3', + email: 'test3', + address: '456 Oak St', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 3, + }, + ] + + // Encrypt the models + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields for each model + expect(encryptedModels.data[0].email).toHaveProperty('c') + expect(encryptedModels.data[0].address).toBeUndefined() + expect(encryptedModels.data[1].email).toBeNull() + expect(encryptedModels.data[1].address).toHaveProperty('c') + expect(encryptedModels.data[2].email).toHaveProperty('c') + expect(encryptedModels.data[2].address).toHaveProperty('c') + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].number).toBe(1) + expect(encryptedModels.data[1].id).toBe('2') + expect(encryptedModels.data[1].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].number).toBe(2) + expect(encryptedModels.data[2].id).toBe('3') + expect(encryptedModels.data[2].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[2].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[2].number).toBe(3) + + // Decrypt the models + const decryptedResult = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModels) + }, 30000) + + it('should handle empty models in bulk operations', async () => { + const decryptedModels = [ + { + id: '1', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 1, + }, // No encrypted fields + { + id: '2', + email: 'test2', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 2, + }, + { + id: '3', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: 3, + }, // No encrypted fields + ] + + // Encrypt the models + const encryptedModels = await protectClient.bulkEncryptModels( + decryptedModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Verify encrypted fields for each model + expect(encryptedModels.data[0].email).toBeUndefined() + expect(encryptedModels.data[0].address).toBeUndefined() + expect(encryptedModels.data[1].email).toHaveProperty('c') + expect(encryptedModels.data[1].address).toBeUndefined() + expect(encryptedModels.data[2].email).toBeUndefined() + expect(encryptedModels.data[2].address).toBeUndefined() + + // Verify non-encrypted fields remain unchanged + expect(encryptedModels.data[0].id).toBe('1') + expect(encryptedModels.data[0].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[0].number).toBe(1) + expect(encryptedModels.data[1].id).toBe('2') + expect(encryptedModels.data[1].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[1].number).toBe(2) + expect(encryptedModels.data[2].id).toBe('3') + expect(encryptedModels.data[2].createdAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[2].updatedAt).toEqual(new Date('2021-01-01')) + expect(encryptedModels.data[2].number).toBe(3) + + // Decrypt the models + const decryptedResult = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(decryptedModels) + }, 30000) +}) + +describe('error handling', () => { + it('should handle invalid encrypted payloads', async () => { + const validModel = { + id: '1', + email: 'test@example.com', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + address: '123 Main St', + number: 1, + } + + // First encrypt a valid model + const encryptedModel = await protectClient.encryptModel( + validModel, + users, + ) + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Create an invalid model by removing required fields + const invalidModel = { + id: '1', + // Missing required fields + } + + try { + await protectClient.decryptModel(invalidModel as User) + throw new Error('Expected decryption to fail') + } catch (error) { + expect(error).toBeDefined() + } + }, 30000) + + it('should handle missing required fields', async () => { + const model = { + id: '1', + email: null, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + address: null, + number: 1, + } + + try { + await protectClient.encryptModel(model, users) + throw new Error('Expected encryption to fail') + } catch (error) { + expect(error).toBeDefined() + } + }, 30000) +}) + +describe('type safety', () => { + it('should maintain type safety with complex nested objects', async () => { + const model = { + id: '1', + email: 'test@example.com', + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + address: '123 Main St', + number: 1, + metadata: { + preferences: { + notifications: true, + theme: 'dark', + }, + }, + } + + // Encrypt the model + const encryptedModel = await protectClient.encryptModel(model, users) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Decrypt the model + const decryptedResult = await protectClient.decryptModel( + encryptedModel.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(model) + }, 30000) +}) + +describe('performance', () => { + it('should handle large numbers of models efficiently', async () => { + const largeModels = Array(10) + .fill(null) + .map((_, i) => ({ + id: i.toString(), + email: `test${i}@example.com`, + address: `Address ${i}`, + createdAt: new Date('2021-01-01'), + updatedAt: new Date('2021-01-01'), + number: i, + })) + + // Encrypt the models + const encryptedModels = await protectClient.bulkEncryptModels( + largeModels, + users, + ) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Decrypt the models + const decryptedResult = await protectClient.bulkDecryptModels( + encryptedModels.data, + ) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual(largeModels) + }, 60000) +}) + +describe('encryption and decryption with lock context', () => { + it('should encrypt and decrypt a payload with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + const email = 'hello@example.com' + + const ciphertext = await protectClient + .encrypt(email, { + column: users.email, + table: users, + }) + .withLockContext(lockContext.data) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + const plaintext = await protectClient + .decrypt(ciphertext.data) + .withLockContext(lockContext.data) + + if (plaintext.failure) { + throw new Error(`[protect]: ${plaintext.failure.message}`) + } + + expect(plaintext.data).toEqual(email) + }, 30000) + + it('should encrypt and decrypt a model with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + // Create a model with decrypted values + const decryptedModel = { + id: '1', + email: 'plaintext', + } + + // Encrypt the model with lock context + const encryptedModel = await protectClient + .encryptModel(decryptedModel, users) + .withLockContext(lockContext.data) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + // Decrypt the model with lock context + const decryptedResult = await protectClient + .decryptModel(encryptedModel.data) + .withLockContext(lockContext.data) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual({ + id: '1', + email: 'plaintext', + }) + }, 30000) + + it('should encrypt with context and be unable to decrypt without context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + // Create a model with decrypted values + const decryptedModel = { + id: '1', + email: 'plaintext', + } + + // Encrypt the model with lock context + const encryptedModel = await protectClient + .encryptModel(decryptedModel, users) + .withLockContext(lockContext.data) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + try { + await protectClient.decryptModel(encryptedModel.data) + } catch (error) { + const e = error as Error + expect(e.message.startsWith('Failed to retrieve key')).toEqual(true) + } + }, 30000) + + it('should bulk encrypt and decrypt models with lock context', async () => { + const userJwt = process.env.USER_JWT + + if (!userJwt) { + console.log('Skipping lock context test - no USER_JWT provided') + return + } + + const lc = new LockContext() + const lockContext = await lc.identify(userJwt) + + if (lockContext.failure) { + throw new Error(`[protect]: ${lockContext.failure.message}`) + } + + // Create models with decrypted values + const decryptedModels = [ + { + id: '1', + email: 'test', + }, + { + id: '2', + email: 'test2', + }, + ] + + // Encrypt the models with lock context + const encryptedModels = await protectClient + .bulkEncryptModels(decryptedModels, users) + .withLockContext(lockContext.data) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + // Decrypt the models with lock context + const decryptedResult = await protectClient + .bulkDecryptModels(encryptedModels.data) + .withLockContext(lockContext.data) + + if (decryptedResult.failure) { + throw new Error(`[protect]: ${decryptedResult.failure.message}`) + } + + expect(decryptedResult.data).toEqual([ + { + id: '1', + email: 'test', + }, + { + id: '2', + email: 'test2', + }, + ]) + }, 30000) +}) + +describe('special characters', () => { + it('should encrypt and decrypt multiple special characters together', async () => { + const plaintext = + 'complex@string-with/slashes\\backslashes.and#symbols$%&+!@#$%^&*()_+-=[]{}|;:,.<>?/~`' + + const ciphertext = await protectClient.encrypt(plaintext, { + column: users.email, + table: users, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + const decrypted = await protectClient.decrypt(ciphertext.data) + + expect(decrypted).toEqual({ + data: plaintext, + }) + }, 30000) +}) diff --git a/packages/stack/__tests__/supabase.test.ts b/packages/stack/__tests__/supabase.test.ts new file mode 100644 index 00000000..03392efc --- /dev/null +++ b/packages/stack/__tests__/supabase.test.ts @@ -0,0 +1,307 @@ +import 'dotenv/config' +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { + type Encrypted, + Encryption, + bulkModelsToEncryptedPgComposites, + encryptedToPgComposite, + isEncryptedPayload, + modelToEncryptedPgComposites, +} from '../src' + +import { createClient } from '@supabase/supabase-js' + +if (!process.env.SUPABASE_URL) { + throw new Error('Missing env.SUPABASE_URL') +} +if (!process.env.SUPABASE_ANON_KEY) { + throw new Error('Missing env.SUPABASE_ANON_KEY') +} + +const supabase = createClient( + process.env.SUPABASE_URL, + process.env.SUPABASE_ANON_KEY, +) + +const table = encryptedTable('protect-ci', { + encrypted: encryptedColumn('encrypted').freeTextSearch().equality(), + age: encryptedColumn('age').dataType('number').equality(), + score: encryptedColumn('score').dataType('number').equality(), +}) + +// Hard code this as the CI database doesn't support order by on encrypted columns +const SKIP_ORDER_BY_TEST = true + +// Unique identifier for this test run to isolate data from concurrent test runs +// This is stored in a dedicated test_run_id column to avoid polluting test data +const TEST_RUN_ID = `test-run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + +// Track all inserted IDs for cleanup +const insertedIds: number[] = [] + +beforeAll(async () => { + // Clean up any data from this specific test run (safe for concurrent runs) + const { error } = await supabase + .from('protect-ci') + .delete() + .eq('test_run_id', TEST_RUN_ID) + + if (error) { + console.warn(`[protect]: Failed to clean up test data: ${error.message}`) + } +}) + +afterAll(async () => { + // Clean up all data from this test run + if (insertedIds.length > 0) { + const { error } = await supabase + .from('protect-ci') + .delete() + .in('id', insertedIds) + if (error) { + console.error(`[protect]: Failed to clean up test data: ${error.message}`) + } + } +}) + +describe('supabase', () => { + it('should insert and select encrypted data', async () => { + const protectClient = await Encryption({ schemas: [table] }) + + const e = 'hello world' + + const ciphertext = await protectClient.encrypt(e, { + column: table.encrypted, + table: table, + }) + + if (ciphertext.failure) { + throw new Error(`[protect]: ${ciphertext.failure.message}`) + } + + const { data: insertedData, error: insertError } = await supabase + .from('protect-ci') + .insert({ + encrypted: encryptedToPgComposite(ciphertext.data), + test_run_id: TEST_RUN_ID, + }) + .select('id') + + if (insertError) { + throw new Error(`[protect]: ${insertError.message}`) + } + + insertedIds.push(insertedData[0].id) + + const { data, error } = await supabase + .from('protect-ci') + .select('id, encrypted::jsonb') + .eq('id', insertedData[0].id) + + if (error) { + throw new Error(`[protect]: ${error.message}`) + } + + const dataToDecrypt = data[0].encrypted as Encrypted + const plaintext = await protectClient.decrypt(dataToDecrypt) + + expect(plaintext).toEqual({ + data: e, + }) + }, 30000) + + it('should insert and select encrypted model data', async () => { + const protectClient = await Encryption({ schemas: [table] }) + + const model = { + encrypted: 'hello world', + otherField: 'not encrypted', + } + + const encryptedModel = await protectClient.encryptModel(model, table) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + const { data: insertedData, error: insertError } = await supabase + .from('protect-ci') + .insert([ + { + ...modelToEncryptedPgComposites(encryptedModel.data), + test_run_id: TEST_RUN_ID, + }, + ]) + .select('id') + + if (insertError) { + throw new Error(`[protect]: ${insertError.message}`) + } + + insertedIds.push(insertedData[0].id) + + const { data, error } = await supabase + .from('protect-ci') + .select('id, encrypted::jsonb, otherField') + .eq('id', insertedData[0].id) + + if (error) { + throw new Error(`[protect]: ${error.message}`) + } + + if (!isEncryptedPayload(data[0].encrypted)) { + throw new Error('Expected encrypted payload') + } + + const decryptedModel = await protectClient.decryptModel(data[0]) + + if (decryptedModel.failure) { + throw new Error(`[protect]: ${decryptedModel.failure.message}`) + } + + expect({ + encrypted: decryptedModel.data.encrypted, + otherField: data[0].otherField, + }).toEqual(model) + }, 30000) + + it('should insert and select bulk encrypted model data', async () => { + const protectClient = await Encryption({ schemas: [table] }) + + const models = [ + { + encrypted: 'hello world 1', + otherField: 'not encrypted 1', + }, + { + encrypted: 'hello world 2', + otherField: 'not encrypted 2', + }, + ] + + const encryptedModels = await protectClient.bulkEncryptModels(models, table) + + if (encryptedModels.failure) { + throw new Error(`[protect]: ${encryptedModels.failure.message}`) + } + + const dataToInsert = bulkModelsToEncryptedPgComposites( + encryptedModels.data, + ).map((row) => ({ + ...row, + test_run_id: TEST_RUN_ID, + })) + + const { data: insertedData, error: insertError } = await supabase + .from('protect-ci') + .insert(dataToInsert) + .select('id') + + if (insertError) { + throw new Error(`[protect]: ${insertError.message}`) + } + + insertedIds.push(...insertedData.map((d: { id: number }) => d.id)) + + const { data, error } = await supabase + .from('protect-ci') + .select('id, encrypted::jsonb, otherField') + .in( + 'id', + insertedData.map((d: { id: number }) => d.id), + ) + + if (error) { + throw new Error(`[protect]: ${error.message}`) + } + + const decryptedModels = await protectClient.bulkDecryptModels(data) + + if (decryptedModels.failure) { + throw new Error(`[protect]: ${decryptedModels.failure.message}`) + } + + expect( + decryptedModels.data.map((d) => { + return { + encrypted: d.encrypted, + otherField: d.otherField, + } + }), + ).toEqual(models) + }, 30000) + + it('should insert and query encrypted number data with equality', async () => { + const protectClient = await Encryption({ schemas: [table] }) + + const testAge = 25 + const model = { + age: testAge, + otherField: 'not encrypted', + } + + const encryptedModel = await protectClient.encryptModel(model, table) + + if (encryptedModel.failure) { + throw new Error(`[protect]: ${encryptedModel.failure.message}`) + } + + const insertResult = await supabase + .from('protect-ci') + .insert([ + { + ...modelToEncryptedPgComposites(encryptedModel.data), + test_run_id: TEST_RUN_ID, + }, + ]) + .select('id') + + if (insertResult.error) { + throw new Error(`[protect]: ${insertResult.error.message}`) + } + + const insertedRecordId = insertResult.data[0].id + insertedIds.push(insertedRecordId) + + // Create encrypted query for equality search with composite-literal returnType + const encryptedResult = await protectClient.encryptQuery([ + { + value: testAge, + column: table.age, + table: table, + queryType: 'equality', + returnType: 'composite-literal', + }, + ]) + + if (encryptedResult.failure) { + throw new Error(`[protect]: ${encryptedResult.failure.message}`) + } + + const [searchTerm] = encryptedResult.data + + // Query filtering by both encrypted age AND our specific test run's ID + // This ensures we don't pick up stale data from other test runs + const { data, error } = await supabase + .from('protect-ci') + .select('id, age::jsonb, otherField') + .eq('age', searchTerm) + .eq('test_run_id', TEST_RUN_ID) + + if (error) { + throw new Error(`[protect]: ${error.message}`) + } + + // Verify we found our specific row with encrypted age match + expect(data).toHaveLength(1) + + const decryptedModel = await protectClient.decryptModel(data[0]) + + if (decryptedModel.failure) { + throw new Error(`[protect]: ${decryptedModel.failure.message}`) + } + + expect(decryptedModel.data.age).toBe(testAge) + }, 30000) +}) diff --git a/packages/stack/package.json b/packages/stack/package.json new file mode 100644 index 00000000..869c1051 --- /dev/null +++ b/packages/stack/package.json @@ -0,0 +1,80 @@ +{ + "name": "@cipherstash/stack", + "version": "0.0.0", + "description": "The CipherStash data security stack", + "keywords": [ + "encryption", + "secrets", + "kms", + "typescript", + "searchable-encryption", + "zero-trust" + ], + "bugs": { + "url": "https://github.com/cipherstash/stack/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/cipherstash/stack.git" + }, + "license": "MIT", + "author": "CipherStash ", + "type": "module", + "bin": { + "stash": "./dist/bin/stash.js" + }, + "main": "./dist/index.cjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./client": { + "types": "./dist/client.d.ts", + "import": "./dist/client.js", + "require": "./dist/client.cjs" + }, + "./identity": { + "types": "./dist/identity/index.d.ts", + "import": "./dist/identity/index.js", + "require": "./dist/identity/index.cjs" + }, + "./secrets": { + "types": "./dist/secrets/index.d.ts", + "import": "./dist/secrets/index.js", + "require": "./dist/secrets/index.cjs" + } + }, + "scripts": { + "build": "tsup", + "postbuild": "chmod +x ./dist/bin/stash.js", + "dev": "tsup --watch", + "test": "vitest run", + "release": "tsup" + }, + "devDependencies": { + "@supabase/supabase-js": "^2.47.10", + "execa": "^9.5.2", + "json-schema-to-typescript": "^15.0.2", + "tsup": "catalog:repo", + "tsx": "catalog:repo", + "typescript": "catalog:repo", + "vitest": "catalog:repo" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@byteslice/result": "^0.2.0", + "@cipherstash/protect-ffi": "0.20.1", + "@cipherstash/schema": "workspace:*", + "@stricli/core": "^1.2.5", + "dotenv": "16.4.7", + "zod": "^3.24.2" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.24.0" + } +} \ No newline at end of file diff --git a/packages/stack/src/bin/stash.ts b/packages/stack/src/bin/stash.ts new file mode 100644 index 00000000..0293f013 --- /dev/null +++ b/packages/stack/src/bin/stash.ts @@ -0,0 +1,499 @@ +import { config } from 'dotenv' +config() +import readline from 'node:readline' +import { + buildApplication, + buildCommand, + buildRouteMap, + run, +} from '@stricli/core' +import { Stash } from '../stash/index.js' + +// ANSI color codes for beautiful terminal output +const colors = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + green: '\x1b[32m', + red: '\x1b[31m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', + magenta: '\x1b[35m', +} + +const style = { + success: (text: string) => + `${colors.green}${colors.bold}✓${colors.reset} ${colors.green}${text}${colors.reset}`, + error: (text: string) => + `${colors.red}${colors.bold}✗${colors.reset} ${colors.red}${text}${colors.reset}`, + info: (text: string) => + `${colors.blue}${colors.bold}ℹ${colors.reset} ${colors.blue}${text}${colors.reset}`, + warning: (text: string) => + `${colors.yellow}${colors.bold}⚠${colors.reset} ${colors.yellow}${text}${colors.reset}`, + title: (text: string) => `${colors.bold}${colors.cyan}${text}${colors.reset}`, + label: (text: string) => `${colors.dim}${text}${colors.reset}`, + value: (text: string) => `${colors.bold}${text}${colors.reset}`, + bullet: () => `${colors.green}•${colors.reset}`, +} + +/** + * Get configuration from environment variables + */ +function getConfig(environment: string): Stash['config'] { + const workspaceCRN = process.env.CS_WORKSPACE_CRN + const clientId = process.env.CS_CLIENT_ID + const clientKey = process.env.CS_CLIENT_KEY + const apiKey = process.env.CS_CLIENT_ACCESS_KEY + const accessKey = process.env.CS_ACCESS_KEY + + const missing: string[] = [] + if (!workspaceCRN) missing.push('CS_WORKSPACE_CRN') + if (!clientId) missing.push('CS_CLIENT_ID') + if (!clientKey) missing.push('CS_CLIENT_KEY') + if (!apiKey) missing.push('CS_CLIENT_ACCESS_KEY') + + if (missing.length > 0) { + console.error( + style.error( + `Missing required environment variables: ${missing.join(', ')}`, + ), + ) + console.error( + `\n${style.info('Please set the following environment variables:')}`, + ) + for (const varName of missing) { + console.error(` ${style.bullet()} ${varName}`) + } + process.exit(1) + } + + if (!workspaceCRN || !clientId || !clientKey || !apiKey) { + // This should never happen due to the check above, but TypeScript needs it + throw new Error('Missing required configuration') + } + + return { + workspaceCRN, + clientId, + clientKey, + apiKey, + accessKey, + environment, + } +} + +/** + * Create a Stash instance with proper error handling + */ +function createStash(environment: string): Stash { + const config = getConfig(environment) + return new Stash(config) +} + +/** + * Prompt user for confirmation + */ +function askConfirmation(prompt: string): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }) + + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + rl.close() + const normalized = answer.trim().toLowerCase() + resolve(normalized === 'y' || normalized === 'yes') + }) + }) +} + +/** + * Set command - Store an encrypted secret + */ +const setCommand = buildCommand({ + func: async (flags: { name: string; value: string; environment: string }) => { + const { name, value, environment } = flags + const stash = createStash(environment) + + console.log( + `${style.info(`Encrypting and storing secret "${name}" in environment "${environment}"...`)}`, + ) + + const result = await stash.set(name, value) + if (result.failure) { + console.error( + style.error(`Failed to set secret: ${result.failure.message}`), + ) + process.exit(1) + } + + console.log( + style.success( + `Secret "${name}" stored successfully in environment "${environment}"`, + ), + ) + }, + parameters: { + flags: { + name: { + kind: 'parsed', + parse: String, + brief: 'Name of the secret to store', + }, + value: { + kind: 'parsed', + parse: String, + brief: 'Plaintext value to encrypt and store', + }, + environment: { + kind: 'parsed', + parse: String, + brief: 'Environment name (e.g., production, staging, development)', + }, + }, + aliases: { + n: 'name', + V: 'value', + e: 'environment', + }, + }, + docs: { + brief: 'Store an encrypted secret in CipherStash', + fullDescription: ` +Store a secret value that will be encrypted locally before being sent to the CipherStash API. +The secret is encrypted end-to-end, ensuring your plaintext never leaves your machine unencrypted. + +Examples: + stash secrets set --name DATABASE_URL --value "postgres://..." --environment production + stash secrets set -n DATABASE_URL -V "postgres://..." -e production + stash secrets set --name API_KEY --value "sk-123..." --environment staging + `.trim(), + }, +}) + +/** + * Get command - Retrieve and decrypt a secret + */ +const getCommand = buildCommand({ + func: async (flags: { name: string; environment: string }) => { + const { name, environment } = flags + const stash = createStash(environment) + + console.log( + `${style.info(`Retrieving secret "${name}" from environment "${environment}"...`)}`, + ) + + const result = await stash.get(name) + if (result.failure) { + console.error( + style.error(`Failed to get secret: ${result.failure.message}`), + ) + process.exit(1) + } + + console.log(`\n${style.title('Secret Value:')}`) + console.log(style.value(result.data)) + }, + parameters: { + flags: { + name: { + kind: 'parsed', + parse: String, + brief: 'Name of the secret to retrieve', + }, + environment: { + kind: 'parsed', + parse: String, + brief: 'Environment name (e.g., production, staging, development)', + }, + }, + aliases: { + n: 'name', + e: 'environment', + }, + }, + docs: { + brief: 'Retrieve and decrypt a secret from CipherStash', + fullDescription: ` +Retrieve a secret from CipherStash and decrypt it locally. The secret value is decrypted +on your machine, ensuring end-to-end security. + +Examples: + stash secrets get --name DATABASE_URL --environment production + stash secrets get -n DATABASE_URL -e production + stash secrets get --name API_KEY --environment staging + `.trim(), + }, +}) + +/** + * List command - List all secrets in an environment + */ +const listCommand = buildCommand({ + func: async (flags: { environment: string }) => { + const { environment } = flags + const stash = createStash(environment) + + console.log( + `${style.info(`Listing secrets in environment "${environment}"...`)}`, + ) + + const result = await stash.list() + if (result.failure) { + console.error( + style.error(`Failed to list secrets: ${result.failure.message}`), + ) + process.exit(1) + } + + if (result.data.length === 0) { + console.log( + `\n${style.warning(`No secrets found in environment "${environment}"`)}`, + ) + return + } + + console.log(`\n${style.title(`Secrets in environment "${environment}":`)}`) + console.log('') + + for (const secret of result.data) { + const name = style.value(secret.name) + const metadata: string[] = [] + if (secret.createdAt) { + metadata.push( + `${style.label('created:')} ${new Date(secret.createdAt).toLocaleString()}`, + ) + } + if (secret.updatedAt) { + metadata.push( + `${style.label('updated:')} ${new Date(secret.updatedAt).toLocaleString()}`, + ) + } + + const metaStr = + metadata.length > 0 + ? ` ${colors.dim}(${metadata.join(', ')})${colors.reset}` + : '' + console.log(` ${style.bullet()} ${name}${metaStr}`) + } + + console.log('') + console.log( + style.label( + `Total: ${result.data.length} secret${result.data.length === 1 ? '' : 's'}`, + ), + ) + }, + parameters: { + flags: { + environment: { + kind: 'parsed', + parse: String, + brief: 'Environment name (e.g., production, staging, development)', + }, + }, + aliases: { + e: 'environment', + }, + }, + docs: { + brief: 'List all secrets in an environment', + fullDescription: ` +List all secrets stored in the specified environment. Only secret names and metadata +are returned; values remain encrypted and are not displayed. + +Examples: + stash secrets list --environment production + stash secrets list -e production + stash secrets list --environment staging + `.trim(), + }, +}) + +/** + * Delete command - Delete a secret from the vault + */ +const deleteCommand = buildCommand({ + func: async (flags: { + name: string + environment: string + yes?: boolean + }) => { + const { name, environment, yes } = flags + const stash = createStash(environment) + + // Ask for confirmation unless --yes flag is set + if (!yes) { + const confirmation = await askConfirmation( + `${style.warning(`Are you sure you want to delete secret "${name}" from environment "${environment}"? This action cannot be undone. (yes/no): `)}`, + ) + + if (!confirmation) { + console.log(style.info('Deletion cancelled.')) + return + } + } + + console.log( + `${style.info(`Deleting secret "${name}" from environment "${environment}"...`)}`, + ) + + const result = await stash.delete(name) + if (result.failure) { + console.error( + style.error(`Failed to delete secret: ${result.failure.message}`), + ) + process.exit(1) + } + + console.log( + style.success( + `Secret "${name}" deleted successfully from environment "${environment}"`, + ), + ) + }, + parameters: { + flags: { + name: { + kind: 'parsed', + parse: String, + brief: 'Name of the secret to delete', + }, + environment: { + kind: 'parsed', + parse: String, + brief: 'Environment name (e.g., production, staging, development)', + }, + yes: { + kind: 'boolean', + optional: true, + brief: 'Skip confirmation prompt', + }, + }, + aliases: { + n: 'name', + e: 'environment', + y: 'yes', + }, + }, + docs: { + brief: 'Delete a secret from CipherStash', + fullDescription: ` +Permanently delete a secret from the specified environment. This action cannot be undone. +By default, you will be prompted for confirmation before deletion. Use --yes to skip the confirmation. + +Examples: + stash secrets delete --name DATABASE_URL --environment production + stash secrets delete -n DATABASE_URL -e production --yes + stash secrets delete --name API_KEY --environment staging -y + `.trim(), + }, +}) + +/** + * Secrets route map - Groups all secret management commands + */ +const secretsRouteMap = buildRouteMap({ + routes: { + set: setCommand, + get: getCommand, + list: listCommand, + delete: deleteCommand, + }, + docs: { + brief: 'Manage encrypted secrets in CipherStash', + fullDescription: ` +The secrets command group provides operations for managing encrypted secrets stored in CipherStash. +All secrets are encrypted locally before being sent to the API, ensuring end-to-end encryption. + +Available Commands: + set Store an encrypted secret + get Retrieve and decrypt a secret + list List all secrets in an environment + delete Delete a secret from the vault + +Environment Variables: + CS_WORKSPACE_CRN CipherStash workspace CRN (required) + CS_CLIENT_ID CipherStash client ID (required) + CS_CLIENT_KEY CipherStash client key (required) + CS_CLIENT_ACCESS_KEY CipherStash client access key (required) + +Examples: + stash secrets set --name DATABASE_URL --value "postgres://..." --environment production + stash secrets set -n DATABASE_URL -V "postgres://..." -e production + stash secrets get --name DATABASE_URL --environment production + stash secrets get -n DATABASE_URL -e production + stash secrets list --environment production + stash secrets list -e production + stash secrets delete --name DATABASE_URL --environment production + stash secrets delete -n DATABASE_URL -e production --yes + stash secrets delete -n DATABASE_URL -e production -y + `.trim(), + }, +}) + +/** + * Root command - Entry point for the CLI + */ +const rootRouteMap = buildRouteMap({ + routes: { + secrets: secretsRouteMap, + }, + docs: { + brief: 'CipherStash Protect - Encrypted secrets management', + fullDescription: ` +CipherStash Protect CLI + +Manage encrypted secrets with end-to-end encryption. Secrets are encrypted locally +before being sent to the CipherStash API, ensuring your plaintext never leaves +your machine unencrypted. + +Quick Start: + 1. Set required environment variables (CS_WORKSPACE_CRN, CS_CLIENT_ID, etc.) + 2. Use 'stash secrets set' to store your first secret + 3. Use 'stash secrets get' to retrieve secrets when needed + +Commands: + secrets Manage encrypted secrets + +Run 'stash --help' for more information about a command. + `.trim(), + }, +}) + +/** + * Build the CLI application + */ +const app = buildApplication(rootRouteMap, { + name: 'stash', + versionInfo: { currentVersion: '10.2.1' }, + scanner: { caseStyle: 'allow-kebab-for-camel' }, +}) + +/** + * Main entry point + */ +async function main(): Promise { + try { + await run(app, process.argv.slice(2), { + process, + async forCommand() { + return { + process, + } + }, + }) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error(style.error(`Unexpected error: ${message}`)) + process.exit(1) + } +} + +void main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error) + console.error(style.error(`Fatal error: ${message}`)) + process.exit(1) +}) diff --git a/packages/stack/src/client.ts b/packages/stack/src/client.ts new file mode 100644 index 00000000..79204b4d --- /dev/null +++ b/packages/stack/src/client.ts @@ -0,0 +1,31 @@ +/** + * Client-safe exports for @cipherstash/stack + * + * This entry point exports types and utilities that can be used in client-side code + * without requiring the @cipherstash/protect-ffi native module. + * + * Use this import path: `@cipherstash/stack/client` + */ + +// Schema types and utilities - client-safe (new names) +export { + encryptedTable, + encryptedColumn, + encryptedValue, + csTable, + csColumn, + csValue, +} from '@cipherstash/schema' +export type { + EncryptedColumn, + EncryptedTable, + EncryptedTableColumn, + EncryptedValue, + ProtectColumn, + ProtectTable, + ProtectTableColumn, + ProtectValue, +} from '@cipherstash/schema' +export type { EncryptionClient } from './ffi' +/** @deprecated Use EncryptionClient */ +export type { EncryptionClient as ProtectClient } from './ffi' diff --git a/packages/stack/src/ffi/helpers/infer-index-type.ts b/packages/stack/src/ffi/helpers/infer-index-type.ts new file mode 100644 index 00000000..de7fd8a1 --- /dev/null +++ b/packages/stack/src/ffi/helpers/infer-index-type.ts @@ -0,0 +1,70 @@ +import type { EncryptedColumn } from '@cipherstash/schema' +import type { FfiIndexTypeName, QueryTypeName } from '../../types' +import { queryTypeToFfi } from '../../types' + +/** + * Infer the primary index type from a column's configured indexes. + * Priority: unique > match > ore (for scalar queries) + */ +export function inferIndexType(column: EncryptedColumn): FfiIndexTypeName { + const config = column.build() + const indexes = config.indexes + + if (!indexes || Object.keys(indexes).length === 0) { + throw new Error(`Column "${column.getName()}" has no indexes configured`) + } + + if (indexes.unique) return 'unique' + if (indexes.match) return 'match' + if (indexes.ore) return 'ore' + + throw new Error( + `Column "${column.getName()}" has no suitable index for scalar queries`, + ) +} + +/** + * Validate that the specified index type is configured on the column + */ +export function validateIndexType( + column: EncryptedColumn, + indexType: FfiIndexTypeName, +): void { + const config = column.build() + const indexes = config.indexes ?? {} + + const indexMap: Record = { + unique: !!indexes.unique, + match: !!indexes.match, + ore: !!indexes.ore, + } + + if (!indexMap[indexType]) { + throw new Error( + `Index type "${indexType}" is not configured on column "${column.getName()}"`, + ) + } +} + +/** + * Resolve the index type for a query, either from explicit queryType or by inference. + * Validates the index type is configured on the column when queryType is explicit. + * + * @param column - The column to resolve the index type for + * @param queryType - Optional explicit query type (if provided, validates against column config) + * @returns The FFI index type name to use for the query + */ +export function resolveIndexType( + column: EncryptedColumn, + queryType?: QueryTypeName, +): FfiIndexTypeName { + const indexType = queryType + ? queryTypeToFfi[queryType] + : inferIndexType(column) + + if (queryType) { + validateIndexType(column, indexType) + } + + return indexType +} diff --git a/packages/stack/src/ffi/helpers/type-guards.ts b/packages/stack/src/ffi/helpers/type-guards.ts new file mode 100644 index 00000000..86fb2fec --- /dev/null +++ b/packages/stack/src/ffi/helpers/type-guards.ts @@ -0,0 +1,18 @@ +import type { ScalarQueryTerm } from '../../types' + +/** + * Type guard to check if a value is an array of ScalarQueryTerm objects. + * Used to discriminate between single value and bulk encryption in encryptQuery overloads. + */ +export function isScalarQueryTermArray( + value: unknown, +): value is readonly ScalarQueryTerm[] { + return ( + Array.isArray(value) && + value.length > 0 && + typeof value[0] === 'object' && + value[0] !== null && + 'column' in value[0] && + 'table' in value[0] + ) +} diff --git a/packages/stack/src/ffi/helpers/validation.ts b/packages/stack/src/ffi/helpers/validation.ts new file mode 100644 index 00000000..39ec839b --- /dev/null +++ b/packages/stack/src/ffi/helpers/validation.ts @@ -0,0 +1,94 @@ +import type { Result } from '@byteslice/result' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import type { FfiIndexTypeName } from '../../types' + +/** + * Validates that a value is not NaN or Infinity. + * Returns a failure Result if validation fails, undefined otherwise. + * Use this in async flows that return Result types. + * + * Uses `never` as the success type so the result can be assigned to any Result. + * + * @internal + */ +export function validateNumericValue( + value: unknown, +): Result | undefined { + if (typeof value === 'number' && Number.isNaN(value)) { + return { + failure: { + type: EncryptionErrorTypes.EncryptionError, + message: '[protect]: Cannot encrypt NaN value', + }, + } + } + if (typeof value === 'number' && !Number.isFinite(value)) { + return { + failure: { + type: EncryptionErrorTypes.EncryptionError, + message: '[protect]: Cannot encrypt Infinity value', + }, + } + } + return undefined +} + +/** + * Validates that a value is not NaN or Infinity. + * Throws an error if validation fails. + * Use this in sync flows where exceptions are caught. + * + * @internal + */ +export function assertValidNumericValue(value: unknown): void { + if (typeof value === 'number' && Number.isNaN(value)) { + throw new Error('[protect]: Cannot encrypt NaN value') + } + if (typeof value === 'number' && !Number.isFinite(value)) { + throw new Error('[protect]: Cannot encrypt Infinity value') + } +} + +/** + * Validates that the value type is compatible with the index type. + * Match index (freeTextSearch) only supports string values. + * Returns a failure Result if validation fails, undefined otherwise. + * Use this in async flows that return Result types. + * + * @internal + */ +export function validateValueIndexCompatibility( + value: unknown, + indexType: FfiIndexTypeName, + columnName: string, +): Result | undefined { + if (typeof value === 'number' && indexType === 'match') { + return { + failure: { + type: EncryptionErrorTypes.EncryptionError, + message: `[protect]: Cannot use 'match' index with numeric value on column "${columnName}". The 'freeTextSearch' index only supports string values. Configure the column with 'orderAndRange()' or 'equality()' for numeric queries.`, + }, + } + } + return undefined +} + +/** + * Validates that the value type is compatible with the index type. + * Match index (freeTextSearch) only supports string values. + * Throws an error if validation fails. + * Use this in sync flows where exceptions are caught. + * + * @internal + */ +export function assertValueIndexCompatibility( + value: unknown, + indexType: FfiIndexTypeName, + columnName: string, +): void { + if (typeof value === 'number' && indexType === 'match') { + throw new Error( + `[protect]: Cannot use 'match' index with numeric value on column "${columnName}". The 'freeTextSearch' index only supports string values. Configure the column with 'orderAndRange()' or 'equality()' for numeric queries.`, + ) + } +} diff --git a/packages/stack/src/ffi/index.ts b/packages/stack/src/ffi/index.ts new file mode 100644 index 00000000..2da0b8dd --- /dev/null +++ b/packages/stack/src/ffi/index.ts @@ -0,0 +1,438 @@ +import { type Result, withResult } from '@byteslice/result' +import { type JsPlaintext, newClient } from '@cipherstash/protect-ffi' +import { + type EncryptConfig, + type EncryptedTable, + type EncryptedTableColumn, + encryptConfigSchema, +} from '@cipherstash/schema' +import { type EncryptionError, EncryptionErrorTypes } from '..' +import { loadWorkSpaceId } from '../../../utils/config' +import { logger } from '../../../utils/logger' +import { toFfiKeysetIdentifier } from '../helpers' +import type { + BulkDecryptPayload, + BulkEncryptPayload, + Client, + Decrypted, + EncryptOptions, + EncryptQueryOptions, + Encrypted, + KeysetIdentifier, + ScalarQueryTerm, + SearchTerm, +} from '../types' +import { isScalarQueryTermArray } from './helpers/type-guards' +import { BatchEncryptQueryOperation } from './operations/batch-encrypt-query' +import { BulkDecryptOperation } from './operations/bulk-decrypt' +import { BulkDecryptModelsOperation } from './operations/bulk-decrypt-models' +import { BulkEncryptOperation } from './operations/bulk-encrypt' +import { BulkEncryptModelsOperation } from './operations/bulk-encrypt-models' +import { DecryptOperation } from './operations/decrypt' +import { DecryptModelOperation } from './operations/decrypt-model' +import { SearchTermsOperation } from './operations/deprecated/search-terms' +import { EncryptOperation } from './operations/encrypt' +import { EncryptModelOperation } from './operations/encrypt-model' +import { EncryptQueryOperation } from './operations/encrypt-query' + +export const noClientError = () => + new Error( + 'The EQL client has not been initialized. Please call init() before using the client.', + ) + +/** The EncryptionClient is the main entry point for interacting with the CipherStash encryption library. + * It provides methods for encrypting and decrypting individual values, as well as models (objects) and bulk operations. + * + * The client must be initialized using the {@link Encryption} function before it can be used. + */ +export class EncryptionClient { + private client: Client + private encryptConfig: EncryptConfig | undefined + private workspaceId: string | undefined + + constructor(workspaceCrn?: string) { + const workspaceId = loadWorkSpaceId(workspaceCrn) + this.workspaceId = workspaceId + } + + /** + * Initializes the EncryptionClient with the provided configuration. + * @internal + * @param config - The configuration object for initializing the client. + * @returns A promise that resolves to a {@link Result} containing the initialized EncryptionClient or a {@link EncryptionError}. + **/ + async init(config: { + encryptConfig: EncryptConfig + workspaceCrn?: string + accessKey?: string + clientId?: string + clientKey?: string + keyset?: KeysetIdentifier + }): Promise> { + return await withResult( + async () => { + const validated: EncryptConfig = encryptConfigSchema.parse( + config.encryptConfig, + ) + + logger.debug( + 'Initializing the Stash Encryption client with the following encrypt config:', + { + encryptConfig: validated, + }, + ) + + this.client = await newClient({ + encryptConfig: validated, + clientOpts: { + workspaceCrn: config.workspaceCrn, + accessKey: config.accessKey, + clientId: config.clientId, + clientKey: config.clientKey, + keyset: toFfiKeysetIdentifier(config.keyset), + }, + }) + + this.encryptConfig = validated + + logger.info('Successfully initialized the Stash Encryption client.') + return this + }, + (error: unknown) => ({ + type: EncryptionErrorTypes.ClientInitError, + message: (error as Error).message, + }), + ) + } + + /** + * Encrypt a value - returns a promise which resolves to an encrypted value. + * + * @param plaintext - The plaintext value to be encrypted. Can be null. + * @param opts - Options specifying the column and table for encryption. + * @returns An EncryptOperation that can be awaited or chained with additional methods. + * + * @example + * The following example demonstrates how to encrypt a value using the Encryption client. + * It includes defining an encryption schema with {@link encryptedTable} and {@link encryptedColumn}, + * initializing the client with {@link Encryption}, and performing the encryption. + * + * `encrypt` returns an {@link EncryptOperation} which can be awaited to get a {@link Result} + * which can either be the encrypted value or a {@link EncryptionError}. + * + * ```typescript + * // Define encryption schema + * import { encryptedTable, encryptedColumn } from "@cipherstash/stack" + * const userSchema = encryptedTable("users", { + * email: encryptedColumn("email"), + * }); + * + * // Initialize Encryption client + * const encryptionClient = await Encryption({ schemas: [userSchema] }) + * + * // Encrypt a value + * const encryptedResult = await encryptionClient.encrypt( + * "person@example.com", + * { column: userSchema.email, table: userSchema } + * ) + * + * // Handle encryption result + * if (encryptedResult.failure) { + * throw new Error(`Encryption failed: ${encryptedResult.failure.message}`); + * } + * + * console.log("Encrypted data:", encryptedResult.data); + * ``` + * + * @example + * When encrypting data, a {@link LockContext} can be provided to tie the encryption to a specific user or session. + * This ensures that the same lock context is required for decryption. + * + * The following example demonstrates how to create a lock context using a user's JWT token + * and use it during encryption. + * + * ```typescript + * // Define encryption schema and initialize client as above + * + * // Create a lock for the user's `sub` claim from their JWT + * const lc = new LockContext(); + * const lockContext = await lc.identify(userJwt); + * + * if (lockContext.failure) { + * // Handle the failure + * } + * + * // Encrypt a value with the lock context + * // Decryption will then require the same lock context + * const encryptedResult = await encryptionClient.encrypt( + * "person@example.com", + * { column: userSchema.email, table: userSchema } + * ) + * .withLockContext(lockContext) + * ``` + * + * @see {@link Result} + * @see {@link encryptedTable} + * @see {@link LockContext} + * @see {@link EncryptOperation} + */ + encrypt( + plaintext: JsPlaintext | null, + opts: EncryptOptions, + ): EncryptOperation { + return new EncryptOperation(this.client, plaintext, opts) + } + + /** + * Encrypt a query value - returns a promise which resolves to an encrypted query value. + * + * @param plaintext - The plaintext value to be encrypted for querying. Can be null. + * @param opts - Options specifying the column, table, and optional queryType for encryption. + * @returns An EncryptQueryOperation that can be awaited or chained with additional methods. + * + * @example + * The following example demonstrates how to encrypt a query value using the Encryption client. + * + * ```typescript + * // Define encryption schema + * import { encryptedTable, encryptedColumn } from "@cipherstash/stack" + * const userSchema = encryptedTable("users", { + * email: encryptedColumn("email").equality(), + * }); + * + * // Initialize Encryption client + * const encryptionClient = await Encryption({ schemas: [userSchema] }) + * + * // Encrypt a query value + * const encryptedResult = await encryptionClient.encryptQuery( + * "person@example.com", + * { column: userSchema.email, table: userSchema, queryType: 'equality' } + * ) + * + * // Handle encryption result + * if (encryptedResult.failure) { + * throw new Error(`Encryption failed: ${encryptedResult.failure.message}`); + * } + * + * console.log("Encrypted query:", encryptedResult.data); + * ``` + * + * @example + * The queryType can be auto-inferred from the column's configured indexes: + * + * ```typescript + * // When queryType is omitted, it will be inferred from the column's indexes + * const encryptedResult = await encryptionClient.encryptQuery( + * "person@example.com", + * { column: userSchema.email, table: userSchema } + * ) + * ``` + * + * @see {@link EncryptQueryOperation} + */ + encryptQuery( + plaintext: JsPlaintext | null, + opts: EncryptQueryOptions, + ): EncryptQueryOperation + + /** + * Encrypt multiple values for use in queries (batch operation). + * @param terms - Array of query terms to encrypt + */ + encryptQuery(terms: readonly ScalarQueryTerm[]): BatchEncryptQueryOperation + + encryptQuery( + plaintextOrTerms: JsPlaintext | null | readonly ScalarQueryTerm[], + opts?: EncryptQueryOptions, + ): EncryptQueryOperation | BatchEncryptQueryOperation { + // Discriminate between ScalarQueryTerm[] and JsPlaintext (which can also be an array) + // using a type guard function + if (isScalarQueryTermArray(plaintextOrTerms)) { + return new BatchEncryptQueryOperation(this.client, plaintextOrTerms) + } + + // Handle empty arrays: if opts provided, treat as single value; otherwise batch mode + // This maintains backward compatibility for encryptQuery([]) while allowing + // encryptQuery([], opts) to encrypt an empty array as a single value + if ( + Array.isArray(plaintextOrTerms) && + plaintextOrTerms.length === 0 && + !opts + ) { + return new BatchEncryptQueryOperation( + this.client, + [] as readonly ScalarQueryTerm[], + ) + } + + return new EncryptQueryOperation( + this.client, + plaintextOrTerms as JsPlaintext | null, + opts!, + ) + } + + /** + * Decryption - returns a promise which resolves to a decrypted value. + * + * @param encryptedData - The encrypted data to be decrypted. + * @returns A DecryptOperation that can be awaited or chained with additional methods. + * + * @example + * The following example demonstrates how to decrypt a value that was previously encrypted using {@link encrypt} client. + * It includes encrypting a value first, then decrypting it, and handling the result. + * + * ```typescript + * const encryptedData = await eqlClient.encrypt( + * "person@example.com", + * { column: "email", table: "users" } + * ) + * const decryptResult = await eqlClient.decrypt(encryptedData) + * if (decryptResult.failure) { + * throw new Error(`Decryption failed: ${decryptResult.failure.message}`); + * } + * console.log("Decrypted data:", decryptResult.data); + * ``` + * + * @example + * Provide a lock context when decrypting: + * ```typescript + * await eqlClient.decrypt(encryptedData) + * .withLockContext(lockContext) + * ``` + * + * @see {@link LockContext} + * @see {@link DecryptOperation} + */ + decrypt(encryptedData: Encrypted): DecryptOperation { + return new DecryptOperation(this.client, encryptedData) + } + + /** + * Encrypt a model based on its encryptConfig. + * + * @example + * ```typescript + * type User = { + * id: string; + * email: string; // encrypted + * } + * + * // Define the schema for the users table + * const usersSchema = encryptedTable('users', { + * email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), + * }) + * + * // Initialize the Encryption client + * const encryptionClient = await Encryption({ schemas: [usersSchema] }) + * + * // Encrypt a user model + * const encryptedModel = await encryptionClient.encryptModel( + * { id: 'user_123', email: 'person@example.com' }, + * usersSchema, + * ) + * ``` + */ + encryptModel>( + input: Decrypted, + table: EncryptedTable, + ): EncryptModelOperation { + return new EncryptModelOperation(this.client, input, table) + } + + /** + * Decrypt a model with encrypted values + * Usage: + * await eqlClient.decryptModel(encryptedModel) + * await eqlClient.decryptModel(encryptedModel).withLockContext(lockContext) + */ + decryptModel>( + input: T, + ): DecryptModelOperation { + return new DecryptModelOperation(this.client, input) + } + + /** + * Bulk encrypt models with decrypted values + * Usage: + * await eqlClient.bulkEncryptModels(decryptedModels, table) + * await eqlClient.bulkEncryptModels(decryptedModels, table).withLockContext(lockContext) + */ + bulkEncryptModels>( + input: Array>, + table: EncryptedTable, + ): BulkEncryptModelsOperation { + return new BulkEncryptModelsOperation(this.client, input, table) + } + + /** + * Bulk decrypt models with encrypted values + * Usage: + * await eqlClient.bulkDecryptModels(encryptedModels) + * await eqlClient.bulkDecryptModels(encryptedModels).withLockContext(lockContext) + */ + bulkDecryptModels>( + input: Array, + ): BulkDecryptModelsOperation { + return new BulkDecryptModelsOperation(this.client, input) + } + + /** + * Bulk encryption - returns a thenable object. + * Usage: + * await eqlClient.bulkEncrypt(plaintexts, { column, table }) + * await eqlClient.bulkEncrypt(plaintexts, { column, table }).withLockContext(lockContext) + */ + bulkEncrypt( + plaintexts: BulkEncryptPayload, + opts: EncryptOptions, + ): BulkEncryptOperation { + return new BulkEncryptOperation(this.client, plaintexts, opts) + } + + /** + * Bulk decryption - returns a thenable object. + * Usage: + * await eqlClient.bulkDecrypt(encryptedPayloads) + * await eqlClient.bulkDecrypt(encryptedPayloads).withLockContext(lockContext) + */ + bulkDecrypt(encryptedPayloads: BulkDecryptPayload): BulkDecryptOperation { + return new BulkDecryptOperation(this.client, encryptedPayloads) + } + + /** + * Create search terms to use in a query searching encrypted data + * + * @deprecated Use `encryptQuery(terms)` instead. + * + * Migration example: + * ```typescript + * // Before (deprecated) + * const result = await client.createSearchTerms([ + * { value: 'test', column: users.email, table: users } + * ]) + * + * // After + * const result = await client.encryptQuery([ + * { value: 'test', column: users.email, table: users, queryType: 'equality' } + * ]) + * ``` + * + * Usage: + * await eqlClient.createSearchTerms(searchTerms) + * await eqlClient.createSearchTerms(searchTerms).withLockContext(lockContext) + */ + createSearchTerms(terms: SearchTerm[]): SearchTermsOperation { + return new SearchTermsOperation(this.client, terms) + } + + /** e.g., debugging or environment info */ + clientInfo() { + return { + workspaceId: this.workspaceId, + } + } +} + +/** @deprecated Use EncryptionClient */ +export { EncryptionClient as ProtectClient } diff --git a/packages/stack/src/ffi/model-helpers.ts b/packages/stack/src/ffi/model-helpers.ts new file mode 100644 index 00000000..79df7014 --- /dev/null +++ b/packages/stack/src/ffi/model-helpers.ts @@ -0,0 +1,952 @@ +import { + type Encrypted as CipherStashEncrypted, + type DecryptBulkOptions, + type JsPlaintext, + decryptBulk, + encryptBulk, +} from '@cipherstash/protect-ffi' +import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/schema' +import { isEncryptedPayload } from '../helpers' +import type { GetLockContextResponse } from '../identify' +import type { Client, Decrypted, Encrypted } from '../types' +import type { AuditData } from './operations/base-operation' + +/** + * Helper function to extract encrypted fields from a model + */ +export function extractEncryptedFields>( + model: T, +): Record { + const result: Record = {} + + for (const [key, value] of Object.entries(model)) { + if (isEncryptedPayload(value)) { + result[key] = value + } + } + + return result +} + +/** + * Helper function to extract non-encrypted fields from a model + */ +export function extractOtherFields>( + model: T, +): Record { + const result: Record = {} + + for (const [key, value] of Object.entries(model)) { + if (!isEncryptedPayload(value)) { + result[key] = value + } + } + + return result +} + +/** + * Helper function to merge encrypted and non-encrypted fields into a model + */ +export function mergeFields( + otherFields: Record, + encryptedFields: Record, +): T { + return { ...otherFields, ...encryptedFields } as T +} + +/** + * Base interface for bulk operation payloads + */ +interface BulkOperationPayload { + id: string + [key: string]: unknown +} + +/** + * Interface for bulk operation key mapping + */ +interface BulkOperationKeyMap { + modelIndex: number + fieldKey: string +} + +/** + * Helper function to handle single model bulk operations with mapping + */ +async function handleSingleModelBulkOperation< + T extends BulkOperationPayload, + R, +>( + items: T[], + operation: (items: T[]) => Promise, + keyMap: Record, +): Promise> { + if (items.length === 0) { + return {} + } + + const results = await operation(items) + const mappedResults: Record = {} + + results.forEach((result, index) => { + const originalKey = keyMap[index.toString()] + mappedResults[originalKey] = result + }) + + return mappedResults +} + +/** + * Helper function to handle multiple model bulk operations with mapping + */ +async function handleMultiModelBulkOperation( + items: T[], + operation: (items: T[]) => Promise, + keyMap: Record, +): Promise> { + if (items.length === 0) { + return {} + } + + const results = await operation(items) + const mappedResults: Record = {} + + results.forEach((result, index) => { + const key = index.toString() + const { modelIndex, fieldKey } = keyMap[key] + mappedResults[`${modelIndex}-${fieldKey}`] = result + }) + + return mappedResults +} + +/** + * Helper function to prepare fields for decryption + */ +function prepareFieldsForDecryption>( + model: T, +): { + otherFields: Record + operationFields: Record + keyMap: Record + nullFields: Record +} { + const otherFields = { ...model } as Record + const operationFields: Record = {} + const nullFields: Record = {} + const keyMap: Record = {} + let index = 0 + + const processNestedFields = (obj: Record, prefix = '') => { + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (value === null || value === undefined) { + nullFields[fullKey] = value + continue + } + + if (typeof value === 'object' && !isEncryptedPayload(value)) { + // Recursively process nested objects + processNestedFields(value as Record, fullKey) + } else if (isEncryptedPayload(value)) { + // This is an encrypted field + const id = index.toString() + keyMap[id] = fullKey + operationFields[fullKey] = value + index++ + + // Remove from otherFields + const parts = fullKey.split('.') + let current = otherFields + for (let i = 0; i < parts.length - 1; i++) { + current = current[parts[i]] as Record + } + delete current[parts[parts.length - 1]] + } + } + } + + processNestedFields(model) + return { otherFields, operationFields, keyMap, nullFields } +} + +/** + * Helper function to prepare fields for encryption + */ +function prepareFieldsForEncryption>( + model: T, + table: EncryptedTable, +): { + otherFields: Record + operationFields: Record + keyMap: Record + nullFields: Record +} { + const otherFields = { ...model } as Record + const operationFields: Record = {} + const nullFields: Record = {} + const keyMap: Record = {} + let index = 0 + + const processNestedFields = ( + obj: Record, + prefix = '', + columnPaths: string[] = [], + ) => { + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (value === null || value === undefined) { + nullFields[fullKey] = value + continue + } + + if ( + typeof value === 'object' && + !isEncryptedPayload(value) && + !columnPaths.includes(fullKey) + ) { + // Only process nested objects if they're in the schema + if (columnPaths.some((path) => path.startsWith(fullKey))) { + processNestedFields( + value as Record, + fullKey, + columnPaths, + ) + } + } else if (columnPaths.includes(fullKey)) { + // Only process fields that are explicitly defined in the schema + const id = index.toString() + keyMap[id] = fullKey + operationFields[fullKey] = value + index++ + + // Remove from otherFields + const parts = fullKey.split('.') + let current = otherFields + for (let i = 0; i < parts.length - 1; i++) { + current = current[parts[i]] as Record + } + delete current[parts[parts.length - 1]] + } + } + } + + // Get all column paths from the table schema + const columnPaths = Object.keys(table.build().columns) + processNestedFields(model, '', columnPaths) + + return { otherFields, operationFields, keyMap, nullFields } +} + +/** + * Helper function to convert a model with encrypted fields to a decrypted model + */ +export async function decryptModelFields>( + model: T, + client: Client, + auditData?: AuditData, +): Promise> { + if (!client) { + throw new Error('Client not initialized') + } + + const { otherFields, operationFields, keyMap, nullFields } = + prepareFieldsForDecryption(model) + + const bulkDecryptPayload = Object.entries(operationFields).map( + ([key, value]) => ({ + id: key, + ciphertext: value as CipherStashEncrypted, + }), + ) + + const decryptedFields = await handleSingleModelBulkOperation( + bulkDecryptPayload, + (items) => + decryptBulk(client, { + ciphertexts: items, + unverifiedContext: auditData?.metadata, + }), + keyMap, + ) + + // Helper function to set a nested value + const setNestedValue = ( + obj: Record, + path: string[], + value: unknown, + ) => { + let current = obj + for (let i = 0; i < path.length - 1; i++) { + const part = path[i] + if (!(part in current)) { + current[part] = {} + } + current = current[part] as Record + } + current[path[path.length - 1]] = value + } + + // Reconstruct the object with proper nesting + const result: Record = { ...otherFields } + + // First, reconstruct the null/undefined fields + for (const [key, value] of Object.entries(nullFields)) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + // Then, reconstruct the decrypted fields + for (const [key, value] of Object.entries(decryptedFields)) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + return result as Decrypted +} + +/** + * Helper function to convert a decrypted model to a model with encrypted fields + */ +export async function encryptModelFields>( + model: Decrypted, + table: EncryptedTable, + client: Client, + auditData?: AuditData, +): Promise { + if (!client) { + throw new Error('Client not initialized') + } + + const { otherFields, operationFields, keyMap, nullFields } = + prepareFieldsForEncryption(model, table) + + const bulkEncryptPayload = Object.entries(operationFields).map( + ([key, value]) => ({ + id: key, + plaintext: value as string, + table: table.tableName, + column: key, + }), + ) + + const encryptedData = await handleSingleModelBulkOperation( + bulkEncryptPayload, + (items) => + encryptBulk(client, { + plaintexts: items, + unverifiedContext: auditData?.metadata, + }), + keyMap, + ) + + // Helper function to set a nested value + const setNestedValue = ( + obj: Record, + path: string[], + value: unknown, + ) => { + let current = obj + for (let i = 0; i < path.length - 1; i++) { + const part = path[i] + if (!(part in current)) { + current[part] = {} + } + current = current[part] as Record + } + current[path[path.length - 1]] = value + } + + // Reconstruct the object with proper nesting + const result: Record = { ...otherFields } + + // First, reconstruct the null/undefined fields + for (const [key, value] of Object.entries(nullFields)) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + // Then, reconstruct the encrypted fields + for (const [key, value] of Object.entries(encryptedData)) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + return result as T +} + +/** + * Helper function to convert a model with encrypted fields to a decrypted model with lock context + */ +export async function decryptModelFieldsWithLockContext< + T extends Record, +>( + model: T, + client: Client, + lockContext: GetLockContextResponse, + auditData?: AuditData, +): Promise> { + if (!client) { + throw new Error('Client not initialized') + } + + if (!lockContext) { + throw new Error('Lock context is not initialized') + } + + const { otherFields, operationFields, keyMap, nullFields } = + prepareFieldsForDecryption(model) + + const bulkDecryptPayload = Object.entries(operationFields).map( + ([key, value]) => ({ + id: key, + ciphertext: value as CipherStashEncrypted, + lockContext: lockContext.context, + }), + ) + + const decryptedFields = await handleSingleModelBulkOperation( + bulkDecryptPayload, + (items) => + decryptBulk(client, { + ciphertexts: items, + serviceToken: lockContext.ctsToken, + unverifiedContext: auditData?.metadata, + }), + keyMap, + ) + + // Helper function to set a nested value + const setNestedValue = ( + obj: Record, + path: string[], + value: unknown, + ) => { + let current = obj + for (let i = 0; i < path.length - 1; i++) { + const part = path[i] + if (!(part in current)) { + current[part] = {} + } + current = current[part] as Record + } + current[path[path.length - 1]] = value + } + + // Reconstruct the object with proper nesting + const result: Record = { ...otherFields } + + // First, reconstruct the null/undefined fields + for (const [key, value] of Object.entries(nullFields)) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + // Then, reconstruct the decrypted fields + for (const [key, value] of Object.entries(decryptedFields)) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + return result as Decrypted +} + +/** + * Helper function to convert a decrypted model to a model with encrypted fields with lock context + */ +export async function encryptModelFieldsWithLockContext< + T extends Record, +>( + model: Decrypted, + table: EncryptedTable, + client: Client, + lockContext: GetLockContextResponse, + auditData?: AuditData, +): Promise { + if (!client) { + throw new Error('Client not initialized') + } + + if (!lockContext) { + throw new Error('Lock context is not initialized') + } + + const { otherFields, operationFields, keyMap, nullFields } = + prepareFieldsForEncryption(model, table) + + const bulkEncryptPayload = Object.entries(operationFields).map( + ([key, value]) => ({ + id: key, + plaintext: value as string, + table: table.tableName, + column: key, + lockContext: lockContext.context, + }), + ) + + const encryptedData = await handleSingleModelBulkOperation( + bulkEncryptPayload, + (items) => + encryptBulk(client, { + plaintexts: items, + serviceToken: lockContext.ctsToken, + unverifiedContext: auditData?.metadata, + }), + keyMap, + ) + + // Helper function to set a nested value + const setNestedValue = ( + obj: Record, + path: string[], + value: unknown, + ) => { + let current = obj + for (let i = 0; i < path.length - 1; i++) { + const part = path[i] + if (!(part in current)) { + current[part] = {} + } + current = current[part] as Record + } + current[path[path.length - 1]] = value + } + + // Reconstruct the object with proper nesting + const result: Record = { ...otherFields } + + // First, reconstruct the null/undefined fields + for (const [key, value] of Object.entries(nullFields)) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + // Then, reconstruct the encrypted fields + for (const [key, value] of Object.entries(encryptedData)) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + return result as T +} + +/** + * Helper function to prepare multiple models for bulk operation + */ +function prepareBulkModelsForOperation>( + models: T[], + table?: EncryptedTable, +): { + otherFields: Record[] + operationFields: Record[] + keyMap: Record + nullFields: Record[] +} { + const otherFields: Record[] = [] + const operationFields: Record[] = [] + const nullFields: Record[] = [] + const keyMap: Record = {} + let index = 0 + + for (let modelIndex = 0; modelIndex < models.length; modelIndex++) { + const model = models[modelIndex] + const modelOtherFields = { ...model } as Record + const modelOperationFields: Record = {} + const modelNullFields: Record = {} + + const processNestedFields = ( + obj: Record, + prefix = '', + columnPaths: string[] = [], + ) => { + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (value === null || value === undefined) { + modelNullFields[fullKey] = value + continue + } + + if ( + typeof value === 'object' && + !isEncryptedPayload(value) && + !columnPaths.includes(fullKey) + ) { + // Only process nested objects if they're in the schema + if (columnPaths.some((path) => path.startsWith(fullKey))) { + processNestedFields( + value as Record, + fullKey, + columnPaths, + ) + } + } else if (columnPaths.includes(fullKey)) { + // Only process fields that are explicitly defined in the schema + const id = index.toString() + keyMap[id] = { modelIndex, fieldKey: fullKey } + modelOperationFields[fullKey] = value + index++ + + // Remove from otherFields + const parts = fullKey.split('.') + let current = modelOtherFields + for (let i = 0; i < parts.length - 1; i++) { + current = current[parts[i]] as Record + } + delete current[parts[parts.length - 1]] + } + } + } + + if (table) { + // Get all column paths from the table schema + const columnPaths = Object.keys(table.build().columns) + processNestedFields(model, '', columnPaths) + } else { + // For decryption, process all encrypted fields + const processEncryptedFields = ( + obj: Record, + prefix = '', + columnPaths: string[] = [], + ) => { + for (const [key, value] of Object.entries(obj)) { + const fullKey = prefix ? `${prefix}.${key}` : key + + if (value === null || value === undefined) { + modelNullFields[fullKey] = value + continue + } + + if ( + typeof value === 'object' && + !isEncryptedPayload(value) && + !columnPaths.includes(fullKey) + ) { + // Recursively process nested objects + processEncryptedFields( + value as Record, + fullKey, + columnPaths, + ) + } else if (isEncryptedPayload(value)) { + // This is an encrypted field + const id = index.toString() + keyMap[id] = { modelIndex, fieldKey: fullKey } + modelOperationFields[fullKey] = value + index++ + + // Remove from otherFields + const parts = fullKey.split('.') + let current = modelOtherFields + for (let i = 0; i < parts.length - 1; i++) { + current = current[parts[i]] as Record + } + delete current[parts[parts.length - 1]] + } + } + } + processEncryptedFields(model) + } + + otherFields.push(modelOtherFields) + operationFields.push(modelOperationFields) + nullFields.push(modelNullFields) + } + + return { otherFields, operationFields, keyMap, nullFields } +} + +/** + * Helper function to convert multiple decrypted models to models with encrypted fields + */ +export async function bulkEncryptModels>( + models: Decrypted[], + table: EncryptedTable, + client: Client, + auditData?: AuditData, +): Promise { + if (!client) { + throw new Error('Client not initialized') + } + + if (!models || models.length === 0) { + return [] + } + + const { otherFields, operationFields, keyMap, nullFields } = + prepareBulkModelsForOperation(models, table) + + const bulkEncryptPayload = operationFields.flatMap((fields, modelIndex) => + Object.entries(fields).map(([key, value]) => ({ + id: `${modelIndex}-${key}`, + plaintext: value as string, + table: table.tableName, + column: key, + })), + ) + + const encryptedData = await handleMultiModelBulkOperation( + bulkEncryptPayload, + (items) => + encryptBulk(client, { + plaintexts: items, + unverifiedContext: auditData?.metadata, + }), + keyMap, + ) + + // Helper function to set a nested value + const setNestedValue = ( + obj: Record, + path: string[], + value: unknown, + ) => { + let current = obj + for (let i = 0; i < path.length - 1; i++) { + const part = path[i] + if (!(part in current)) { + current[part] = {} + } + current = current[part] as Record + } + current[path[path.length - 1]] = value + } + + return models.map((_, modelIndex) => { + const result: Record = { ...otherFields[modelIndex] } + + // First, reconstruct the null/undefined fields + for (const [key, value] of Object.entries(nullFields[modelIndex])) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + // Then, reconstruct the encrypted fields + const modelData = Object.fromEntries( + Object.entries(encryptedData) + .filter(([key]) => { + const [idx] = key.split('-') + return Number.parseInt(idx) === modelIndex + }) + .map(([key, value]) => { + const [_, fieldKey] = key.split('-') + return [fieldKey, value] + }), + ) + + for (const [key, value] of Object.entries(modelData)) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + return result as T + }) +} + +/** + * Helper function to convert multiple models with encrypted fields to decrypted models + */ +export async function bulkDecryptModels>( + models: T[], + client: Client, + auditData?: AuditData, +): Promise[]> { + if (!client) { + throw new Error('Client not initialized') + } + + if (!models || models.length === 0) { + return [] + } + + const { otherFields, operationFields, keyMap, nullFields } = + prepareBulkModelsForOperation(models) + + const bulkDecryptPayload = operationFields.flatMap((fields, modelIndex) => + Object.entries(fields).map(([key, value]) => ({ + id: `${modelIndex}-${key}`, + ciphertext: value as CipherStashEncrypted, + })), + ) + + const decryptedFields = await handleMultiModelBulkOperation( + bulkDecryptPayload, + (items) => + decryptBulk(client, { + ciphertexts: items, + unverifiedContext: auditData?.metadata, + }), + keyMap, + ) + + // Helper function to set a nested value + const setNestedValue = ( + obj: Record, + path: string[], + value: unknown, + ) => { + let current = obj + for (let i = 0; i < path.length - 1; i++) { + const part = path[i] + if (!(part in current)) { + current[part] = {} + } + current = current[part] as Record + } + current[path[path.length - 1]] = value + } + + return models.map((_, modelIndex) => { + const result: Record = { ...otherFields[modelIndex] } + + // First, reconstruct the null/undefined fields + for (const [key, value] of Object.entries(nullFields[modelIndex])) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + // Then, reconstruct the decrypted fields + const modelData = Object.fromEntries( + Object.entries(decryptedFields) + .filter(([key]) => { + const [idx] = key.split('-') + return Number.parseInt(idx) === modelIndex + }) + .map(([key, value]) => { + const [_, fieldKey] = key.split('-') + return [fieldKey, value] + }), + ) + + for (const [key, value] of Object.entries(modelData)) { + const parts = key.split('.') + setNestedValue(result, parts, value) + } + + return result as Decrypted + }) +} + +/** + * Helper function to convert multiple models with encrypted fields to decrypted models with lock context + */ +export async function bulkDecryptModelsWithLockContext< + T extends Record, +>( + models: T[], + client: Client, + lockContext: GetLockContextResponse, + auditData?: AuditData, +): Promise[]> { + if (!client) { + throw new Error('Client not initialized') + } + + if (!lockContext) { + throw new Error('Lock context is not initialized') + } + + const { otherFields, operationFields, keyMap, nullFields } = + prepareBulkModelsForOperation(models) + + const bulkDecryptPayload = operationFields.flatMap((fields, modelIndex) => + Object.entries(fields).map(([key, value]) => ({ + id: `${modelIndex}-${key}`, + ciphertext: value as CipherStashEncrypted, + lockContext: lockContext.context, + })), + ) + + const decryptedFields = await handleMultiModelBulkOperation( + bulkDecryptPayload, + (items) => + decryptBulk(client, { + ciphertexts: items, + serviceToken: lockContext.ctsToken, + unverifiedContext: auditData?.metadata, + }), + keyMap, + ) + + // Reconstruct models + return models.map((_, modelIndex) => ({ + ...otherFields[modelIndex], + ...nullFields[modelIndex], + ...Object.fromEntries( + Object.entries(decryptedFields) + .filter(([key]) => { + const [idx] = key.split('-') + return Number.parseInt(idx) === modelIndex + }) + .map(([key, value]) => { + const [_, fieldKey] = key.split('-') + return [fieldKey, value] + }), + ), + })) as Decrypted[] +} + +/** + * Helper function to convert multiple decrypted models to models with encrypted fields with lock context + */ +export async function bulkEncryptModelsWithLockContext< + T extends Record, +>( + models: Decrypted[], + table: EncryptedTable, + client: Client, + lockContext: GetLockContextResponse, + auditData?: AuditData, +): Promise { + if (!client) { + throw new Error('Client not initialized') + } + + if (!lockContext) { + throw new Error('Lock context is not initialized') + } + + const { otherFields, operationFields, keyMap, nullFields } = + prepareBulkModelsForOperation(models, table) + + const bulkEncryptPayload = operationFields.flatMap((fields, modelIndex) => + Object.entries(fields).map(([key, value]) => ({ + id: `${modelIndex}-${key}`, + plaintext: value as string, + table: table.tableName, + column: key, + lockContext: lockContext.context, + })), + ) + + const encryptedData = await handleMultiModelBulkOperation( + bulkEncryptPayload, + (items) => + encryptBulk(client, { + plaintexts: items, + serviceToken: lockContext.ctsToken, + unverifiedContext: auditData?.metadata, + }), + keyMap, + ) + + // Reconstruct models + return models.map((_, modelIndex) => ({ + ...otherFields[modelIndex], + ...nullFields[modelIndex], + ...Object.fromEntries( + Object.entries(encryptedData) + .filter(([key]) => { + const [idx] = key.split('-') + return Number.parseInt(idx) === modelIndex + }) + .map(([key, value]) => { + const [_, fieldKey] = key.split('-') + return [fieldKey, value] + }), + ), + })) as T[] +} diff --git a/packages/stack/src/ffi/operations/base-operation.ts b/packages/stack/src/ffi/operations/base-operation.ts new file mode 100644 index 00000000..abba3b2b --- /dev/null +++ b/packages/stack/src/ffi/operations/base-operation.ts @@ -0,0 +1,55 @@ +import type { Result } from '@byteslice/result' +import type { EncryptionError } from '../..' + +export type AuditConfig = { + metadata?: Record +} + +export type AuditData = { + metadata?: Record +} + +export abstract class EncryptionOperation { + protected auditMetadata?: Record + + /** + * Attach audit metadata to this operation. Can be chained. + * @param config Configuration for ZeroKMS audit logging + * @param config.metadata Arbitrary JSON object for appending metadata to the audit log + */ + audit(config: AuditConfig): this { + this.auditMetadata = config.metadata + return this + } + + /** + * Get the audit data for this operation. + */ + public getAuditData(): AuditData { + return { + metadata: this.auditMetadata, + } + } + + /** + * Execute the operation and return a Result + */ + abstract execute(): Promise> + + /** + * Make the operation thenable + */ + public then, TResult2 = never>( + onfulfilled?: + | (( + value: Result, + ) => TResult1 | PromiseLike) + | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, + ): Promise { + return this.execute().then(onfulfilled, onrejected) + } +} + +/** @deprecated Use EncryptionOperation */ +export { EncryptionOperation as ProtectOperation } diff --git a/packages/stack/src/ffi/operations/batch-encrypt-query.ts b/packages/stack/src/ffi/operations/batch-encrypt-query.ts new file mode 100644 index 00000000..73cc45e5 --- /dev/null +++ b/packages/stack/src/ffi/operations/batch-encrypt-query.ts @@ -0,0 +1,233 @@ +import { type Result, withResult } from '@byteslice/result' +import { + type JsPlaintext, + type QueryPayload, + encryptQueryBulk as ffiEncryptQueryBulk, +} from '@cipherstash/protect-ffi' +import type { Encrypted as CipherStashEncrypted } from '@cipherstash/protect-ffi' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import { + encryptedToCompositeLiteral, + encryptedToEscapedCompositeLiteral, +} from '../../helpers' +import type { Context, LockContext } from '../../identify' +import type { Client, EncryptedQueryResult, ScalarQueryTerm } from '../../types' +import { resolveIndexType } from '../helpers/infer-index-type' +import { + assertValidNumericValue, + assertValueIndexCompatibility, +} from '../helpers/validation' +import { noClientError } from '../index' +import { EncryptionOperation } from './base-operation' + +/** + * Separates null values from non-null terms in the input array. + * Returns a set of indices where values are null and an array of non-null terms with their original indices. + */ +function filterNullTerms(terms: readonly ScalarQueryTerm[]): { + nullIndices: Set + nonNullTerms: { term: ScalarQueryTerm; originalIndex: number }[] +} { + const nullIndices = new Set() + const nonNullTerms: { term: ScalarQueryTerm; originalIndex: number }[] = [] + + terms.forEach((term, index) => { + if (term.value === null) { + nullIndices.add(index) + } else { + nonNullTerms.push({ term, originalIndex: index }) + } + }) + + return { nullIndices, nonNullTerms } +} + +/** + * Validates and transforms a single term into a QueryPayload. + * Throws an error if the value is NaN or Infinity. + * Optionally includes lockContext if provided. + */ +function buildQueryPayload( + term: ScalarQueryTerm, + lockContext?: Context, +): QueryPayload { + assertValidNumericValue(term.value) + + const indexType = resolveIndexType(term.column, term.queryType) + + // Validate value/index compatibility + assertValueIndexCompatibility(term.value, indexType, term.column.getName()) + + const payload: QueryPayload = { + plaintext: term.value as JsPlaintext, + column: term.column.getName(), + table: term.table.tableName, + indexType, + } + + if (lockContext != null) { + payload.lockContext = lockContext + } + + return payload +} + +/** + * Reconstructs the results array with nulls in their original positions. + * Non-null encrypted values are placed at their original indices. + * Applies formatting based on term.returnType. + */ +function assembleResults( + totalLength: number, + encryptedValues: CipherStashEncrypted[], + nonNullTerms: { term: ScalarQueryTerm; originalIndex: number }[], +): EncryptedQueryResult[] { + const results: EncryptedQueryResult[] = new Array(totalLength).fill(null) + + // Fill in encrypted values at their original positions, applying formatting + nonNullTerms.forEach(({ term, originalIndex }, i) => { + const encrypted = encryptedValues[i] + + if (term.returnType === 'composite-literal') { + results[originalIndex] = encryptedToCompositeLiteral(encrypted) + } else if (term.returnType === 'escaped-composite-literal') { + results[originalIndex] = encryptedToEscapedCompositeLiteral(encrypted) + } else { + results[originalIndex] = encrypted + } + }) + + return results +} + +/** + * @internal Use {@link ProtectClient.encryptQuery} with array input instead. + */ +export class BatchEncryptQueryOperation extends EncryptionOperation< + EncryptedQueryResult[] +> { + constructor( + private client: Client, + private terms: readonly ScalarQueryTerm[], + ) { + super() + } + + public withLockContext( + lockContext: LockContext, + ): BatchEncryptQueryOperationWithLockContext { + return new BatchEncryptQueryOperationWithLockContext( + this.client, + this.terms, + lockContext, + this.auditMetadata, + ) + } + + public async execute(): Promise< + Result + > { + logger.debug('Encrypting query terms', { count: this.terms.length }) + + if (this.terms.length === 0) { + return { data: [] } + } + + const { nullIndices, nonNullTerms } = filterNullTerms(this.terms) + + if (nonNullTerms.length === 0) { + return { data: this.terms.map(() => null) } + } + + return await withResult( + async () => { + if (!this.client) throw noClientError() + + const { metadata } = this.getAuditData() + + const queries: QueryPayload[] = nonNullTerms.map(({ term }) => + buildQueryPayload(term), + ) + + const encrypted = await ffiEncryptQueryBulk(this.client, { + queries, + unverifiedContext: metadata, + }) + + return assembleResults(this.terms.length, encrypted, nonNullTerms) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } +} + +/** + * @internal Use {@link ProtectClient.encryptQuery} with array input and `.withLockContext()` instead. + */ +export class BatchEncryptQueryOperationWithLockContext extends EncryptionOperation< + EncryptedQueryResult[] +> { + constructor( + private client: Client, + private terms: readonly ScalarQueryTerm[], + private lockContext: LockContext, + auditMetadata?: Record, + ) { + super() + this.auditMetadata = auditMetadata + } + + public async execute(): Promise< + Result + > { + logger.debug('Encrypting query terms with lock context', { + count: this.terms.length, + }) + + if (this.terms.length === 0) { + return { data: [] } + } + + // Check for all-null terms BEFORE fetching lockContext to avoid unnecessary network call + const { nullIndices, nonNullTerms } = filterNullTerms(this.terms) + + if (nonNullTerms.length === 0) { + return { data: this.terms.map(() => null) } + } + + const lockContextResult = await this.lockContext.getLockContext() + if (lockContextResult.failure) { + return { failure: lockContextResult.failure } + } + + const { ctsToken, context } = lockContextResult.data + + return await withResult( + async () => { + if (!this.client) throw noClientError() + + const { metadata } = this.getAuditData() + + const queries: QueryPayload[] = nonNullTerms.map(({ term }) => + buildQueryPayload(term, context), + ) + + const encrypted = await ffiEncryptQueryBulk(this.client, { + queries, + serviceToken: ctsToken, + unverifiedContext: metadata, + }) + + return assembleResults(this.terms.length, encrypted, nonNullTerms) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/bulk-decrypt-models.ts b/packages/stack/src/ffi/operations/bulk-decrypt-models.ts new file mode 100644 index 00000000..afb23fd8 --- /dev/null +++ b/packages/stack/src/ffi/operations/bulk-decrypt-models.ts @@ -0,0 +1,109 @@ +import { type Result, withResult } from '@byteslice/result' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import type { LockContext } from '../../identify' +import type { Client, Decrypted } from '../../types' +import { noClientError } from '../index' +import { + bulkDecryptModels, + bulkDecryptModelsWithLockContext, +} from '../model-helpers' +import { EncryptionOperation } from './base-operation' + +export class BulkDecryptModelsOperation< + T extends Record, +> extends EncryptionOperation[]> { + private client: Client + private models: T[] + + constructor(client: Client, models: T[]) { + super() + this.client = client + this.models = models + } + + public withLockContext( + lockContext: LockContext, + ): BulkDecryptModelsOperationWithLockContext { + return new BulkDecryptModelsOperationWithLockContext(this, lockContext) + } + + public async execute(): Promise[], EncryptionError>> { + logger.debug('Bulk decrypting models WITHOUT a lock context') + + return await withResult( + async () => { + if (!this.client) { + throw noClientError() + } + + const auditData = this.getAuditData() + + return await bulkDecryptModels(this.models, this.client, auditData) + }, + (error) => ({ + type: EncryptionErrorTypes.DecryptionError, + message: error.message, + }), + ) + } + + public getOperation(): { + client: Client + models: T[] + } { + return { + client: this.client, + models: this.models, + } + } +} + +export class BulkDecryptModelsOperationWithLockContext< + T extends Record, +> extends EncryptionOperation[]> { + private operation: BulkDecryptModelsOperation + private lockContext: LockContext + + constructor( + operation: BulkDecryptModelsOperation, + lockContext: LockContext, + ) { + super() + this.operation = operation + this.lockContext = lockContext + } + + public async execute(): Promise[], EncryptionError>> { + return await withResult( + async () => { + const { client, models } = this.operation.getOperation() + + logger.debug('Bulk decrypting models WITH a lock context') + + if (!client) { + throw noClientError() + } + + const context = await this.lockContext.getLockContext() + + if (context.failure) { + throw new Error(`[protect]: ${context.failure.message}`) + } + + const auditData = this.getAuditData() + + return await bulkDecryptModelsWithLockContext( + models, + client, + context.data, + auditData, + ) + }, + (error) => ({ + type: EncryptionErrorTypes.DecryptionError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/bulk-decrypt.ts b/packages/stack/src/ffi/operations/bulk-decrypt.ts new file mode 100644 index 00000000..39959883 --- /dev/null +++ b/packages/stack/src/ffi/operations/bulk-decrypt.ts @@ -0,0 +1,175 @@ +import { type Result, withResult } from '@byteslice/result' +import { + type Encrypted as CipherStashEncrypted, + type DecryptResult, + decryptBulkFallible, +} from '@cipherstash/protect-ffi' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import type { Context, LockContext } from '../../identify' +import type { BulkDecryptPayload, BulkDecryptedData, Client } from '../../types' +import { noClientError } from '../index' +import { EncryptionOperation } from './base-operation' + +// Helper functions for better composability +const createDecryptPayloads = ( + encryptedPayloads: BulkDecryptPayload, + lockContext?: Context, +) => { + return encryptedPayloads + .map((item, index) => ({ ...item, originalIndex: index })) + .filter(({ data }) => data !== null) + .map(({ id, data, originalIndex }) => ({ + id, + ciphertext: data as CipherStashEncrypted, + originalIndex, + ...(lockContext && { lockContext }), + })) +} + +const createNullResult = ( + encryptedPayloads: BulkDecryptPayload, +): BulkDecryptedData => { + return encryptedPayloads.map(({ id }) => ({ + id, + data: null, + })) +} + +const mapDecryptedDataToResult = ( + encryptedPayloads: BulkDecryptPayload, + decryptedData: DecryptResult[], +): BulkDecryptedData => { + const result: BulkDecryptedData = new Array(encryptedPayloads.length) + let decryptedIndex = 0 + + for (let i = 0; i < encryptedPayloads.length; i++) { + if (encryptedPayloads[i].data === null) { + result[i] = { id: encryptedPayloads[i].id, data: null } + } else { + const decryptResult = decryptedData[decryptedIndex] + if ('error' in decryptResult) { + result[i] = { + id: encryptedPayloads[i].id, + error: decryptResult.error, + } + } else { + result[i] = { + id: encryptedPayloads[i].id, + data: decryptResult.data, + } + } + decryptedIndex++ + } + } + + return result +} + +export class BulkDecryptOperation extends EncryptionOperation { + private client: Client + private encryptedPayloads: BulkDecryptPayload + + constructor(client: Client, encryptedPayloads: BulkDecryptPayload) { + super() + this.client = client + this.encryptedPayloads = encryptedPayloads + } + + public withLockContext( + lockContext: LockContext, + ): BulkDecryptOperationWithLockContext { + return new BulkDecryptOperationWithLockContext(this, lockContext) + } + + public async execute(): Promise> { + logger.debug('Bulk decrypting data WITHOUT a lock context') + return await withResult( + async () => { + if (!this.client) throw noClientError() + if (!this.encryptedPayloads || this.encryptedPayloads.length === 0) + return [] + + const nonNullPayloads = createDecryptPayloads(this.encryptedPayloads) + + if (nonNullPayloads.length === 0) { + return createNullResult(this.encryptedPayloads) + } + + const { metadata } = this.getAuditData() + + const decryptedData = await decryptBulkFallible(this.client, { + ciphertexts: nonNullPayloads, + unverifiedContext: metadata, + }) + + return mapDecryptedDataToResult(this.encryptedPayloads, decryptedData) + }, + (error: unknown) => ({ + type: EncryptionErrorTypes.DecryptionError, + message: (error as Error).message, + }), + ) + } + + public getOperation(): { + client: Client + encryptedPayloads: BulkDecryptPayload + } { + return { + client: this.client, + encryptedPayloads: this.encryptedPayloads, + } + } +} + +export class BulkDecryptOperationWithLockContext extends EncryptionOperation { + private operation: BulkDecryptOperation + private lockContext: LockContext + + constructor(operation: BulkDecryptOperation, lockContext: LockContext) { + super() + this.operation = operation + this.lockContext = lockContext + } + + public async execute(): Promise> { + return await withResult( + async () => { + const { client, encryptedPayloads } = this.operation.getOperation() + logger.debug('Bulk decrypting data WITH a lock context') + + if (!client) throw noClientError() + if (!encryptedPayloads || encryptedPayloads.length === 0) return [] + + const context = await this.lockContext.getLockContext() + if (context.failure) { + throw new Error(`[protect]: ${context.failure.message}`) + } + + const nonNullPayloads = createDecryptPayloads( + encryptedPayloads, + context.data.context, + ) + + if (nonNullPayloads.length === 0) { + return createNullResult(encryptedPayloads) + } + + const { metadata } = this.getAuditData() + + const decryptedData = await decryptBulkFallible(client, { + ciphertexts: nonNullPayloads, + serviceToken: context.data.ctsToken, + unverifiedContext: metadata, + }) + + return mapDecryptedDataToResult(encryptedPayloads, decryptedData) + }, + (error: unknown) => ({ + type: EncryptionErrorTypes.DecryptionError, + message: (error as Error).message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/bulk-encrypt-models.ts b/packages/stack/src/ffi/operations/bulk-encrypt-models.ts new file mode 100644 index 00000000..afbe626a --- /dev/null +++ b/packages/stack/src/ffi/operations/bulk-encrypt-models.ts @@ -0,0 +1,128 @@ +import { type Result, withResult } from '@byteslice/result' +import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/schema' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import type { LockContext } from '../../identify' +import type { Client, Decrypted } from '../../types' +import { noClientError } from '../index' +import { + bulkEncryptModels, + bulkEncryptModelsWithLockContext, +} from '../model-helpers' +import { EncryptionOperation } from './base-operation' + +export class BulkEncryptModelsOperation< + T extends Record, +> extends EncryptionOperation { + private client: Client + private models: Decrypted[] + private table: EncryptedTable + + constructor( + client: Client, + models: Decrypted[], + table: EncryptedTable, + ) { + super() + this.client = client + this.models = models + this.table = table + } + + public withLockContext( + lockContext: LockContext, + ): BulkEncryptModelsOperationWithLockContext { + return new BulkEncryptModelsOperationWithLockContext(this, lockContext) + } + + public async execute(): Promise> { + logger.debug('Bulk encrypting models WITHOUT a lock context', { + table: this.table.tableName, + }) + + return await withResult( + async () => { + if (!this.client) { + throw noClientError() + } + + const auditData = this.getAuditData() + + return await bulkEncryptModels( + this.models, + this.table, + this.client, + auditData, + ) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } + + public getOperation(): { + client: Client + models: Decrypted[] + table: EncryptedTable + } { + return { + client: this.client, + models: this.models, + table: this.table, + } + } +} + +export class BulkEncryptModelsOperationWithLockContext< + T extends Record, +> extends EncryptionOperation { + private operation: BulkEncryptModelsOperation + private lockContext: LockContext + + constructor( + operation: BulkEncryptModelsOperation, + lockContext: LockContext, + ) { + super() + this.operation = operation + this.lockContext = lockContext + } + + public async execute(): Promise> { + return await withResult( + async () => { + const { client, models, table } = this.operation.getOperation() + + logger.debug('Bulk encrypting models WITH a lock context', { + table: table.tableName, + }) + + if (!client) { + throw noClientError() + } + + const context = await this.lockContext.getLockContext() + + if (context.failure) { + throw new Error(`[protect]: ${context.failure.message}`) + } + + const auditData = this.getAuditData() + + return await bulkEncryptModelsWithLockContext( + models, + table, + client, + context.data, + auditData, + ) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/bulk-encrypt.ts b/packages/stack/src/ffi/operations/bulk-encrypt.ts new file mode 100644 index 00000000..42bd8bd9 --- /dev/null +++ b/packages/stack/src/ffi/operations/bulk-encrypt.ts @@ -0,0 +1,210 @@ +import { type Result, withResult } from '@byteslice/result' +import { type JsPlaintext, encryptBulk } from '@cipherstash/protect-ffi' +import type { + EncryptedColumn, + EncryptedTable, + EncryptedTableColumn, + EncryptedValue, +} from '@cipherstash/schema' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import type { Context, LockContext } from '../../identify' +import type { + BulkEncryptPayload, + BulkEncryptedData, + Client, + EncryptOptions, + Encrypted, +} from '../../types' +import { noClientError } from '../index' +import { EncryptionOperation } from './base-operation' + +// Helper functions for better composability +const createEncryptPayloads = ( + plaintexts: BulkEncryptPayload, + column: EncryptedColumn | EncryptedValue, + table: EncryptedTable, + lockContext?: Context, +) => { + return plaintexts + .map((item, index) => ({ ...item, originalIndex: index })) + .filter(({ plaintext }) => plaintext !== null) + .map(({ id, plaintext, originalIndex }) => ({ + id, + plaintext: plaintext as JsPlaintext, + column: column.getName(), + table: table.tableName, + originalIndex, + ...(lockContext && { lockContext }), + })) +} + +const createNullResult = ( + plaintexts: BulkEncryptPayload, +): BulkEncryptedData => { + return plaintexts.map(({ id }) => ({ id, data: null })) +} + +const mapEncryptedDataToResult = ( + plaintexts: BulkEncryptPayload, + encryptedData: Encrypted[], +): BulkEncryptedData => { + const result: BulkEncryptedData = new Array(plaintexts.length) + let encryptedIndex = 0 + + for (let i = 0; i < plaintexts.length; i++) { + if (plaintexts[i].plaintext === null) { + result[i] = { id: plaintexts[i].id, data: null } + } else { + result[i] = { + id: plaintexts[i].id, + data: encryptedData[encryptedIndex], + } + encryptedIndex++ + } + } + + return result +} + +export class BulkEncryptOperation extends EncryptionOperation { + private client: Client + private plaintexts: BulkEncryptPayload + private column: EncryptedColumn | EncryptedValue + private table: EncryptedTable + + constructor( + client: Client, + plaintexts: BulkEncryptPayload, + opts: EncryptOptions, + ) { + super() + this.client = client + this.plaintexts = plaintexts + this.column = opts.column + this.table = opts.table + } + + public withLockContext( + lockContext: LockContext, + ): BulkEncryptOperationWithLockContext { + return new BulkEncryptOperationWithLockContext(this, lockContext) + } + + public async execute(): Promise> { + logger.debug('Bulk encrypting data WITHOUT a lock context', { + column: this.column.getName(), + table: this.table.tableName, + }) + + return await withResult( + async () => { + if (!this.client) { + throw noClientError() + } + if (!this.plaintexts || this.plaintexts.length === 0) { + return [] + } + + const nonNullPayloads = createEncryptPayloads( + this.plaintexts, + this.column, + this.table, + ) + + if (nonNullPayloads.length === 0) { + return createNullResult(this.plaintexts) + } + + const { metadata } = this.getAuditData() + + const encryptedData = await encryptBulk(this.client, { + plaintexts: nonNullPayloads, + unverifiedContext: metadata, + }) + + return mapEncryptedDataToResult(this.plaintexts, encryptedData) + }, + (error: unknown) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: (error as Error).message, + }), + ) + } + + public getOperation(): { + client: Client + plaintexts: BulkEncryptPayload + column: EncryptedColumn | EncryptedValue + table: EncryptedTable + } { + return { + client: this.client, + plaintexts: this.plaintexts, + column: this.column, + table: this.table, + } + } +} + +export class BulkEncryptOperationWithLockContext extends EncryptionOperation { + private operation: BulkEncryptOperation + private lockContext: LockContext + + constructor(operation: BulkEncryptOperation, lockContext: LockContext) { + super() + this.operation = operation + this.lockContext = lockContext + } + + public async execute(): Promise> { + return await withResult( + async () => { + const { client, plaintexts, column, table } = + this.operation.getOperation() + + logger.debug('Bulk encrypting data WITH a lock context', { + column: column.getName(), + table: table.tableName, + }) + + if (!client) { + throw noClientError() + } + if (!plaintexts || plaintexts.length === 0) { + return [] + } + + const context = await this.lockContext.getLockContext() + if (context.failure) { + throw new Error(`[protect]: ${context.failure.message}`) + } + + const nonNullPayloads = createEncryptPayloads( + plaintexts, + column, + table, + context.data.context, + ) + + if (nonNullPayloads.length === 0) { + return createNullResult(plaintexts) + } + + const { metadata } = this.getAuditData() + + const encryptedData = await encryptBulk(client, { + plaintexts: nonNullPayloads, + serviceToken: context.data.ctsToken, + unverifiedContext: metadata, + }) + + return mapEncryptedDataToResult(plaintexts, encryptedData) + }, + (error: unknown) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: (error as Error).message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/decrypt-model.ts b/packages/stack/src/ffi/operations/decrypt-model.ts new file mode 100644 index 00000000..aacc31eb --- /dev/null +++ b/packages/stack/src/ffi/operations/decrypt-model.ts @@ -0,0 +1,106 @@ +import { type Result, withResult } from '@byteslice/result' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import type { LockContext } from '../../identify' +import type { Client, Decrypted } from '../../types' +import { noClientError } from '../index' +import { + decryptModelFields, + decryptModelFieldsWithLockContext, +} from '../model-helpers' +import { EncryptionOperation } from './base-operation' + +export class DecryptModelOperation< + T extends Record, +> extends EncryptionOperation> { + private client: Client + private model: T + + constructor(client: Client, model: T) { + super() + this.client = client + this.model = model + } + + public withLockContext( + lockContext: LockContext, + ): DecryptModelOperationWithLockContext { + return new DecryptModelOperationWithLockContext(this, lockContext) + } + + public async execute(): Promise, EncryptionError>> { + logger.debug('Decrypting model WITHOUT a lock context') + + return await withResult( + async () => { + if (!this.client) { + throw noClientError() + } + + const auditData = this.getAuditData() + + return await decryptModelFields(this.model, this.client, auditData) + }, + (error) => ({ + type: EncryptionErrorTypes.DecryptionError, + message: error.message, + }), + ) + } + + public getOperation(): { + client: Client + model: T + } { + return { + client: this.client, + model: this.model, + } + } +} + +export class DecryptModelOperationWithLockContext< + T extends Record, +> extends EncryptionOperation> { + private operation: DecryptModelOperation + private lockContext: LockContext + + constructor(operation: DecryptModelOperation, lockContext: LockContext) { + super() + this.operation = operation + this.lockContext = lockContext + } + + public async execute(): Promise, EncryptionError>> { + return await withResult( + async () => { + const { client, model } = this.operation.getOperation() + + logger.debug('Decrypting model WITH a lock context') + + if (!client) { + throw noClientError() + } + + const context = await this.lockContext.getLockContext() + + if (context.failure) { + throw new Error(`[protect]: ${context.failure.message}`) + } + + const auditData = this.getAuditData() + + return await decryptModelFieldsWithLockContext( + model, + client, + context.data, + auditData, + ) + }, + (error) => ({ + type: EncryptionErrorTypes.DecryptionError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/decrypt.ts b/packages/stack/src/ffi/operations/decrypt.ts new file mode 100644 index 00000000..3790aab7 --- /dev/null +++ b/packages/stack/src/ffi/operations/decrypt.ts @@ -0,0 +1,127 @@ +import { type Result, withResult } from '@byteslice/result' +import { + type JsPlaintext, + decrypt as ffiDecrypt, +} from '@cipherstash/protect-ffi' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import type { LockContext } from '../../identify' +import type { Client, Encrypted } from '../../types' +import { noClientError } from '../index' +import { EncryptionOperation } from './base-operation' + +/** + * Decrypts an encrypted payload using the provided client. + * This is the type returned by the {@link ProtectClient.decrypt | decrypt} method of the {@link ProtectClient}. + */ +export class DecryptOperation extends EncryptionOperation { + private client: Client + private encryptedData: Encrypted + + constructor(client: Client, encryptedData: Encrypted) { + super() + this.client = client + this.encryptedData = encryptedData + } + + public withLockContext( + lockContext: LockContext, + ): DecryptOperationWithLockContext { + return new DecryptOperationWithLockContext(this, lockContext) + } + + public async execute(): Promise> { + return await withResult( + async () => { + if (!this.client) { + throw noClientError() + } + + if (this.encryptedData === null) { + return null + } + + const { metadata } = this.getAuditData() + + logger.debug('Decrypting data WITHOUT a lock context', { + metadata, + }) + + return await ffiDecrypt(this.client, { + ciphertext: this.encryptedData, + unverifiedContext: metadata, + }) + }, + (error) => ({ + type: EncryptionErrorTypes.DecryptionError, + message: error.message, + }), + ) + } + + public getOperation(): { + client: Client + encryptedData: Encrypted + auditData?: Record + } { + return { + client: this.client, + encryptedData: this.encryptedData, + auditData: this.getAuditData(), + } + } +} + +export class DecryptOperationWithLockContext extends EncryptionOperation { + private operation: DecryptOperation + private lockContext: LockContext + + constructor(operation: DecryptOperation, lockContext: LockContext) { + super() + this.operation = operation + this.lockContext = lockContext + const auditData = operation.getAuditData() + if (auditData) { + this.audit(auditData) + } + } + + public async execute(): Promise> { + return await withResult( + async () => { + const { client, encryptedData } = this.operation.getOperation() + + if (!client) { + throw noClientError() + } + + if (encryptedData === null) { + return null + } + + const { metadata } = this.getAuditData() + + logger.debug('Decrypting data WITH a lock context', { + metadata, + }) + + const context = await this.lockContext.getLockContext() + + if (context.failure) { + throw new Error(`[protect]: ${context.failure.message}`) + } + + return await ffiDecrypt(client, { + ciphertext: encryptedData, + unverifiedContext: metadata, + lockContext: context.data.context, + serviceToken: context.data.ctsToken, + }) + }, + (error) => ({ + type: EncryptionErrorTypes.DecryptionError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/deprecated/search-terms.ts b/packages/stack/src/ffi/operations/deprecated/search-terms.ts new file mode 100644 index 00000000..f28113a1 --- /dev/null +++ b/packages/stack/src/ffi/operations/deprecated/search-terms.ts @@ -0,0 +1,132 @@ +import { type Result, withResult } from '@byteslice/result' +import { type QueryPayload, encryptQueryBulk } from '@cipherstash/protect-ffi' +import { noClientError } from '../..' +import { type EncryptionError, EncryptionErrorTypes } from '../../..' +import { logger } from '../../../../../utils/logger' +import type { LockContext } from '../../../identify' +import type { Client, EncryptedSearchTerm, SearchTerm } from '../../../types' +import { inferIndexType } from '../../helpers/infer-index-type' +import { EncryptionOperation } from '../base-operation' + +/** + * @deprecated Use `BatchEncryptQueryOperation` instead. + * This class is maintained for backward compatibility only. + */ +export class SearchTermsOperation extends EncryptionOperation< + EncryptedSearchTerm[] +> { + constructor( + private client: Client, + private terms: SearchTerm[], + ) { + super() + } + + public withLockContext( + lockContext: LockContext, + ): SearchTermsOperationWithLockContext { + return new SearchTermsOperationWithLockContext(this, lockContext) + } + + public async execute(): Promise< + Result + > { + logger.debug('Creating search terms (deprecated API)', { + count: this.terms.length, + }) + + return await withResult( + async () => { + if (!this.client) throw noClientError() + + const { metadata } = this.getAuditData() + + const queries: QueryPayload[] = this.terms.map((term) => ({ + plaintext: term.value, + column: term.column.getName(), + table: term.table.tableName, + indexType: inferIndexType(term.column), + })) + + const encryptedTerms = await encryptQueryBulk(this.client, { + queries, + unverifiedContext: metadata, + }) + + return this.terms.map((term, index) => { + if (term.returnType === 'composite-literal') { + return `(${JSON.stringify(JSON.stringify(encryptedTerms[index]))})` + } + if (term.returnType === 'escaped-composite-literal') { + return `${JSON.stringify(`(${JSON.stringify(JSON.stringify(encryptedTerms[index]))})`)}` + } + return encryptedTerms[index] + }) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } +} + +export class SearchTermsOperationWithLockContext extends EncryptionOperation< + EncryptedSearchTerm[] +> { + constructor( + private operation: SearchTermsOperation, + private lockContext: LockContext, + ) { + super() + this.auditMetadata = (operation as any).auditMetadata + } + + public async execute(): Promise< + Result + > { + const lockContextResult = await this.lockContext.getLockContext() + if (lockContextResult.failure) { + return { failure: lockContextResult.failure } + } + + const { ctsToken, context } = lockContextResult.data + const op = this.operation as any + + return await withResult( + async () => { + if (!op.client) throw noClientError() + + const { metadata } = this.getAuditData() + + const queries: QueryPayload[] = op.terms.map((term: SearchTerm) => ({ + plaintext: term.value, + column: term.column.getName(), + table: term.table.tableName, + indexType: inferIndexType(term.column), + lockContext: context, + })) + + const encryptedTerms = await encryptQueryBulk(op.client, { + queries, + serviceToken: ctsToken, + unverifiedContext: metadata, + }) + + return op.terms.map((term: SearchTerm, index: number) => { + if (term.returnType === 'composite-literal') { + return `(${JSON.stringify(JSON.stringify(encryptedTerms[index]))})` + } + if (term.returnType === 'escaped-composite-literal') { + return `${JSON.stringify(`(${JSON.stringify(JSON.stringify(encryptedTerms[index]))})`)}` + } + return encryptedTerms[index] + }) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/encrypt-model.ts b/packages/stack/src/ffi/operations/encrypt-model.ts new file mode 100644 index 00000000..6313f6df --- /dev/null +++ b/packages/stack/src/ffi/operations/encrypt-model.ts @@ -0,0 +1,125 @@ +import { type Result, withResult } from '@byteslice/result' +import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/schema' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import type { LockContext } from '../../identify' +import type { Client, Decrypted } from '../../types' +import { noClientError } from '../index' +import { + encryptModelFields, + encryptModelFieldsWithLockContext, +} from '../model-helpers' +import { EncryptionOperation } from './base-operation' + +export class EncryptModelOperation< + T extends Record, +> extends EncryptionOperation { + private client: Client + private model: Decrypted + private table: EncryptedTable + + constructor( + client: Client, + model: Decrypted, + table: EncryptedTable, + ) { + super() + this.client = client + this.model = model + this.table = table + } + + public withLockContext( + lockContext: LockContext, + ): EncryptModelOperationWithLockContext { + return new EncryptModelOperationWithLockContext(this, lockContext) + } + + public async execute(): Promise> { + logger.debug('Encrypting model WITHOUT a lock context', { + table: this.table.tableName, + }) + + return await withResult( + async () => { + if (!this.client) { + throw noClientError() + } + + const auditData = this.getAuditData() + + return await encryptModelFields( + this.model, + this.table, + this.client, + auditData, + ) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } + + public getOperation(): { + client: Client + model: Decrypted + table: EncryptedTable + } { + return { + client: this.client, + model: this.model, + table: this.table, + } + } +} + +export class EncryptModelOperationWithLockContext< + T extends Record, +> extends EncryptionOperation { + private operation: EncryptModelOperation + private lockContext: LockContext + + constructor(operation: EncryptModelOperation, lockContext: LockContext) { + super() + this.operation = operation + this.lockContext = lockContext + } + + public async execute(): Promise> { + return await withResult( + async () => { + const { client, model, table } = this.operation.getOperation() + + logger.debug('Encrypting model WITH a lock context', { + table: table.tableName, + }) + + if (!client) { + throw noClientError() + } + + const context = await this.lockContext.getLockContext() + + if (context.failure) { + throw new Error(`[protect]: ${context.failure.message}`) + } + + const auditData = this.getAuditData() + + return await encryptModelFieldsWithLockContext( + model, + table, + client, + context.data, + auditData, + ) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/encrypt-query.ts b/packages/stack/src/ffi/operations/encrypt-query.ts new file mode 100644 index 00000000..73522bb3 --- /dev/null +++ b/packages/stack/src/ffi/operations/encrypt-query.ts @@ -0,0 +1,162 @@ +import { type Result, withResult } from '@byteslice/result' +import { + type JsPlaintext, + encryptQuery as ffiEncryptQuery, +} from '@cipherstash/protect-ffi' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import type { LockContext } from '../../identify' +import type { Client, EncryptQueryOptions, Encrypted } from '../../types' +import { resolveIndexType } from '../helpers/infer-index-type' +import { + assertValueIndexCompatibility, + validateNumericValue, +} from '../helpers/validation' +import { noClientError } from '../index' +import { EncryptionOperation } from './base-operation' + +/** + * @internal Use {@link ProtectClient.encryptQuery} instead. + */ +export class EncryptQueryOperation extends EncryptionOperation { + constructor( + private client: Client, + private plaintext: JsPlaintext | null, + private opts: EncryptQueryOptions, + ) { + super() + } + + public withLockContext( + lockContext: LockContext, + ): EncryptQueryOperationWithLockContext { + return new EncryptQueryOperationWithLockContext( + this.client, + this.plaintext, + this.opts, + lockContext, + this.auditMetadata, + ) + } + + public async execute(): Promise> { + logger.debug('Encrypting query', { + column: this.opts.column.getName(), + table: this.opts.table.tableName, + queryType: this.opts.queryType, + }) + + if (this.plaintext === null) { + return { data: null } + } + + const validationError = validateNumericValue(this.plaintext) + if (validationError?.failure) { + return { failure: validationError.failure } + } + + return await withResult( + async () => { + if (!this.client) throw noClientError() + + const { metadata } = this.getAuditData() + + const indexType = resolveIndexType( + this.opts.column, + this.opts.queryType, + ) + + // Validate value/index compatibility + assertValueIndexCompatibility( + this.plaintext, + indexType, + this.opts.column.getName(), + ) + + return await ffiEncryptQuery(this.client, { + plaintext: this.plaintext as JsPlaintext, + column: this.opts.column.getName(), + table: this.opts.table.tableName, + indexType, + unverifiedContext: metadata, + }) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } + + public getOperation() { + return { client: this.client, plaintext: this.plaintext, ...this.opts } + } +} + +/** + * @internal Use {@link ProtectClient.encryptQuery} with `.withLockContext()` instead. + */ +export class EncryptQueryOperationWithLockContext extends EncryptionOperation { + constructor( + private client: Client, + private plaintext: JsPlaintext | null, + private opts: EncryptQueryOptions, + private lockContext: LockContext, + auditMetadata?: Record, + ) { + super() + this.auditMetadata = auditMetadata + } + + public async execute(): Promise> { + if (this.plaintext === null) { + return { data: null } + } + + const validationError = validateNumericValue(this.plaintext) + if (validationError?.failure) { + return { failure: validationError.failure } + } + + const lockContextResult = await this.lockContext.getLockContext() + if (lockContextResult.failure) { + return { failure: lockContextResult.failure } + } + + const { ctsToken, context } = lockContextResult.data + + return await withResult( + async () => { + if (!this.client) throw noClientError() + + const { metadata } = this.getAuditData() + + const indexType = resolveIndexType( + this.opts.column, + this.opts.queryType, + ) + + // Validate value/index compatibility + assertValueIndexCompatibility( + this.plaintext, + indexType, + this.opts.column.getName(), + ) + + return await ffiEncryptQuery(this.client, { + plaintext: this.plaintext as JsPlaintext, + column: this.opts.column.getName(), + table: this.opts.table.tableName, + indexType, + lockContext: context, + serviceToken: ctsToken, + unverifiedContext: metadata, + }) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/encrypt.ts b/packages/stack/src/ffi/operations/encrypt.ts new file mode 100644 index 00000000..d9604f62 --- /dev/null +++ b/packages/stack/src/ffi/operations/encrypt.ts @@ -0,0 +1,155 @@ +import { type Result, withResult } from '@byteslice/result' +import { + type JsPlaintext, + encrypt as ffiEncrypt, +} from '@cipherstash/protect-ffi' +import type { + EncryptedColumn, + EncryptedTable, + EncryptedTableColumn, + EncryptedValue, +} from '@cipherstash/schema' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import type { LockContext } from '../../identify' +import type { Client, EncryptOptions, Encrypted } from '../../types' +import { noClientError } from '../index' +import { EncryptionOperation } from './base-operation' + +export class EncryptOperation extends EncryptionOperation { + private client: Client + private plaintext: JsPlaintext | null + private column: EncryptedColumn | EncryptedValue + private table: EncryptedTable + + constructor( + client: Client, + plaintext: JsPlaintext | null, + opts: EncryptOptions, + ) { + super() + this.client = client + this.plaintext = plaintext + this.column = opts.column + this.table = opts.table + } + + public withLockContext( + lockContext: LockContext, + ): EncryptOperationWithLockContext { + return new EncryptOperationWithLockContext(this, lockContext) + } + + public async execute(): Promise> { + logger.debug('Encrypting data WITHOUT a lock context', { + column: this.column.getName(), + table: this.table.tableName, + }) + + return await withResult( + async () => { + if (!this.client) { + throw noClientError() + } + + if (this.plaintext === null) { + return null + } + + if ( + typeof this.plaintext === 'number' && + Number.isNaN(this.plaintext) + ) { + throw new Error('[protect]: Cannot encrypt NaN value') + } + + if ( + typeof this.plaintext === 'number' && + !Number.isFinite(this.plaintext) + ) { + throw new Error('[protect]: Cannot encrypt Infinity value') + } + + const { metadata } = this.getAuditData() + + return await ffiEncrypt(this.client, { + plaintext: this.plaintext, + column: this.column.getName(), + table: this.table.tableName, + unverifiedContext: metadata, + }) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } + + public getOperation(): { + client: Client + plaintext: JsPlaintext | null + column: EncryptedColumn | EncryptedValue + table: EncryptedTable + } { + return { + client: this.client, + plaintext: this.plaintext, + column: this.column, + table: this.table, + } + } +} + +export class EncryptOperationWithLockContext extends EncryptionOperation { + private operation: EncryptOperation + private lockContext: LockContext + + constructor(operation: EncryptOperation, lockContext: LockContext) { + super() + this.operation = operation + this.lockContext = lockContext + } + + public async execute(): Promise> { + return await withResult( + async () => { + const { client, plaintext, column, table } = + this.operation.getOperation() + + logger.debug('Encrypting data WITH a lock context', { + column: column, + table: table, + }) + + if (!client) { + throw noClientError() + } + + if (plaintext === null) { + return null + } + + const { metadata } = this.getAuditData() + const context = await this.lockContext.getLockContext() + + if (context.failure) { + throw new Error(`[protect]: ${context.failure.message}`) + } + + return await ffiEncrypt(client, { + plaintext, + column: column.getName(), + table: table.tableName, + lockContext: context.data.context, + serviceToken: context.data.ctsToken, + unverifiedContext: metadata, + }) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/ffi/operations/search-terms.ts b/packages/stack/src/ffi/operations/search-terms.ts new file mode 100644 index 00000000..11cefc2f --- /dev/null +++ b/packages/stack/src/ffi/operations/search-terms.ts @@ -0,0 +1,63 @@ +import { type Result, withResult } from '@byteslice/result' +import { encryptBulk } from '@cipherstash/protect-ffi' +import { type EncryptionError, EncryptionErrorTypes } from '../..' +import { logger } from '../../../../utils/logger' +import type { Client, EncryptedSearchTerm, SearchTerm } from '../../types' +import { noClientError } from '../index' +import { EncryptionOperation } from './base-operation' + +export class SearchTermsOperation extends EncryptionOperation< + EncryptedSearchTerm[] +> { + private client: Client + private terms: SearchTerm[] + + constructor(client: Client, terms: SearchTerm[]) { + super() + this.client = client + this.terms = terms + } + + public async execute(): Promise< + Result + > { + logger.debug('Creating search terms', { + terms: this.terms, + }) + + return await withResult( + async () => { + if (!this.client) { + throw noClientError() + } + + const { metadata } = this.getAuditData() + + const encryptedSearchTerms = await encryptBulk(this.client, { + plaintexts: this.terms.map((term) => ({ + plaintext: term.value, + column: term.column.getName(), + table: term.table.tableName, + })), + unverifiedContext: metadata, + }) + + return this.terms.map((term, index) => { + if (term.returnType === 'composite-literal') { + return `(${JSON.stringify(JSON.stringify(encryptedSearchTerms[index]))})` + } + + if (term.returnType === 'escaped-composite-literal') { + return `${JSON.stringify(`(${JSON.stringify(JSON.stringify(encryptedSearchTerms[index]))})`)}` + } + + return encryptedSearchTerms[index] + }) + }, + (error) => ({ + type: EncryptionErrorTypes.EncryptionError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/helpers/index.ts b/packages/stack/src/helpers/index.ts new file mode 100644 index 00000000..fa6dc719 --- /dev/null +++ b/packages/stack/src/helpers/index.ts @@ -0,0 +1,139 @@ +import type { + Encrypted as CipherStashEncrypted, + KeysetIdentifier as KeysetIdentifierFfi, +} from '@cipherstash/protect-ffi' +import type { Encrypted, KeysetIdentifier } from '../types' + +export type EncryptedPgComposite = { + data: Encrypted +} + +/** + * Helper function to transform an encrypted payload into a PostgreSQL composite type. + * Use this when inserting data via Supabase or similar clients. + */ +export function encryptedToPgComposite(obj: Encrypted): EncryptedPgComposite { + return { + data: obj, + } +} + +/** + * Helper function to transform an encrypted payload into a PostgreSQL composite literal string. + * Use this when querying with `.eq()` or similar equality operations in Supabase. + * + * @deprecated Use `encryptQuery()` with `returnType: 'composite-literal'` instead. + * @example + * ```typescript + * // Before (deprecated): + * const [encrypted] = await protectClient.encryptQuery([ + * { value: searchValue, column, table, queryType: 'equality' } + * ]) + * const literal = encryptedToCompositeLiteral(encrypted) + * await supabase.from('table').select().eq('column', literal) + * + * // After (recommended): + * const [searchTerm] = await protectClient.encryptQuery([ + * { value: searchValue, column, table, queryType: 'equality', returnType: 'composite-literal' } + * ]) + * await supabase.from('table').select().eq('column', searchTerm) + * ``` + */ +export function encryptedToCompositeLiteral(obj: CipherStashEncrypted): string { + if (obj === null) { + throw new Error('encryptedToCompositeLiteral: obj cannot be null') + } + return `(${JSON.stringify(JSON.stringify(obj))})` +} + +/** + * Helper function to transform an encrypted payload into an escaped PostgreSQL composite literal string. + * Use this when you need the composite literal format to be escaped as a string value. + * + * @deprecated Use `encryptQuery()` with `returnType: 'escaped-composite-literal'` instead. + * See also: `encryptedToCompositeLiteral` for parallel deprecation guidance. + * @example + * ```typescript + * // Before (deprecated): + * const [encrypted] = await protectClient.encryptQuery([ + * { value: searchValue, column, table, queryType: 'equality' } + * ]) + * const escapedLiteral = encryptedToEscapedCompositeLiteral(encrypted) + * + * // After (recommended): + * const [searchTerm] = await protectClient.encryptQuery([ + * { value: searchValue, column, table, queryType: 'equality', returnType: 'escaped-composite-literal' } + * ]) + * ``` + */ +export function encryptedToEscapedCompositeLiteral( + obj: CipherStashEncrypted, +): string { + if (obj === null) { + throw new Error('encryptedToEscapedCompositeLiteral: obj cannot be null') + } + return JSON.stringify(encryptedToCompositeLiteral(obj)) +} + +/** + * Helper function to transform a model's encrypted fields into PostgreSQL composite types + */ +export function modelToEncryptedPgComposites>( + model: T, +): T { + const result: Record = {} + + for (const [key, value] of Object.entries(model)) { + if (isEncryptedPayload(value)) { + result[key] = encryptedToPgComposite(value) + } else { + result[key] = value + } + } + + return result as T +} + +/** + * Helper function to transform multiple models' encrypted fields into PostgreSQL composite types + */ +export function bulkModelsToEncryptedPgComposites< + T extends Record, +>(models: T[]): T[] { + return models.map((model) => modelToEncryptedPgComposites(model)) +} + +export function toFfiKeysetIdentifier( + keyset: KeysetIdentifier | undefined, +): KeysetIdentifierFfi | undefined { + if (!keyset) return undefined + + if ('name' in keyset) { + return { Name: keyset.name } + } + + return { Uuid: keyset.id } +} + +/** + * Helper function to check if a value is an encrypted payload + */ +export function isEncryptedPayload(value: unknown): value is Encrypted { + if (value === null) return false + + // TODO: this can definitely be improved + if (typeof value === 'object') { + const obj = value as Encrypted + return ( + obj !== null && 'v' in obj && ('c' in obj || 'sv' in obj) && 'i' in obj + ) + } + + return false +} + +export { + toJsonPath, + buildNestedObject, + parseJsonbPath, +} from './jsonb' diff --git a/packages/stack/src/helpers/jsonb.ts b/packages/stack/src/helpers/jsonb.ts new file mode 100644 index 00000000..018ea99c --- /dev/null +++ b/packages/stack/src/helpers/jsonb.ts @@ -0,0 +1,99 @@ +/** + * JSONB path utilities for converting between path formats. + * + * These utilities support dot-notation and basic JSONPath-style array indices (e.g., "[0]"). + * Only limited validation is performed (forbidden prototype keys); callers should still + * ensure segments are valid property names. + */ + +/** + * Convert a dot-notation path to JSONPath selector format. + * + * @example + * toJsonPath("user.email") // "$.user.email" + * toJsonPath("$.user.email") // "$.user.email" (unchanged) + * toJsonPath(".user.email") // "$.user.email" + * toJsonPath("name") // "$.name" + */ +export function toJsonPath(path: string): string { + if (!path || path === '$') return '$' + if (path.startsWith('$[')) return path + if (path.startsWith('$.')) return path + if (path.startsWith('$')) return `$.${path.slice(1)}` + if (path.startsWith('.')) return `$${path}` + if (path.startsWith('[')) return `$${path}` + return `$.${path}` +} + +/** + * Parse a JSONB path string into segments. + * Handles both dot notation and JSONPath format. + * + * Returns an empty array for empty, null, or undefined input (defensive for JS consumers). + * + * @example + * parseJsonbPath("user.email") // ["user", "email"] + * parseJsonbPath("$.user.email") // ["user", "email"] + * parseJsonbPath("name") // ["name"] + * parseJsonbPath("$.name") // ["name"] + */ +export function parseJsonbPath(path: string): string[] { + if (!path || typeof path !== 'string') return [] + + // Remove leading $. or $ prefix + const normalized = path.replace(/^\$\.?/, '') + + if (!normalized) return [] + + return normalized.split('.').filter(Boolean) +} + +/** + * Build a nested object from a dot-notation path and value. + * + * @example + * buildNestedObject("user.role", "admin") + * // Returns: { user: { role: "admin" } } + * + * buildNestedObject("name", "alice") + * // Returns: { name: "alice" } + * + * buildNestedObject("a.b.c", 123) + * // Returns: { a: { b: { c: 123 } } } + */ +const FORBIDDEN_KEYS = ['__proto__', 'prototype', 'constructor'] + +function validateSegment(segment: string): void { + if (FORBIDDEN_KEYS.includes(segment)) { + throw new Error(`Path contains forbidden segment: ${segment}`) + } +} + +export function buildNestedObject( + path: string, + value: unknown, +): Record { + if (!path) { + throw new Error('Path cannot be empty') + } + + const segments = parseJsonbPath(path) + if (segments.length === 0) { + throw new Error('Path must contain at least one segment') + } + + const result: Record = Object.create(null) + let current = result + + for (let i = 0; i < segments.length - 1; i++) { + const key = segments[i] + validateSegment(key) + current[key] = Object.create(null) + current = current[key] as Record + } + + const leafKey = segments[segments.length - 1] + validateSegment(leafKey) + current[leafKey] = value + return result +} diff --git a/packages/stack/src/identify/index.ts b/packages/stack/src/identify/index.ts new file mode 100644 index 00000000..4f42001f --- /dev/null +++ b/packages/stack/src/identify/index.ts @@ -0,0 +1,130 @@ +import { type Result, withResult } from '@byteslice/result' +import { type EncryptionError, EncryptionErrorTypes } from '..' +import { loadWorkSpaceId } from '../../../utils/config' +import { logger } from '../../../utils/logger' + +export type CtsRegions = 'ap-southeast-2' + +export type IdentifyOptions = { + fetchFromCts?: boolean +} + +export type CtsToken = { + accessToken: string + expiry: number +} + +export type Context = { + identityClaim: string[] +} + +export type LockContextOptions = { + context?: Context + ctsToken?: CtsToken +} + +export type GetLockContextResponse = { + ctsToken: CtsToken + context: Context +} + +export class LockContext { + private ctsToken: CtsToken | undefined + private workspaceId: string + private context: Context + + constructor({ + context = { identityClaim: ['sub'] }, + ctsToken, + }: LockContextOptions = {}) { + const workspaceId = loadWorkSpaceId() + + if (!workspaceId) { + throw new Error( + 'You have not defined a workspace ID in your config file, or the CS_WORKSPACE_ID environment variable.', + ) + } + + if (ctsToken) { + this.ctsToken = ctsToken + } + + this.workspaceId = workspaceId + this.context = context + logger.debug('Successfully initialized the EQL lock context.') + } + + async identify( + jwtToken: string, + ): Promise> { + const workspaceId = this.workspaceId + + const ctsEndpoint = + process.env.CS_CTS_ENDPOINT || + 'https://ap-southeast-2.aws.auth.viturhosted.net' + + const ctsFetchResult = await withResult( + () => + fetch(`${ctsEndpoint}/api/authorize`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + workspaceId, + oidcToken: jwtToken, + }), + }), + (error) => ({ + type: EncryptionErrorTypes.CtsTokenError, + message: error.message, + }), + ) + + if (ctsFetchResult.failure) { + return ctsFetchResult + } + + const identifiedLockContext = await withResult( + async () => { + const ctsToken = (await ctsFetchResult.data.json()) as CtsToken + + if (!ctsToken.accessToken) { + throw new Error( + 'The response from the CipherStash API did not contain an access token. Please contact support.', + ) + } + + this.ctsToken = ctsToken + return this + }, + (error) => ({ + type: EncryptionErrorTypes.CtsTokenError, + message: error.message, + }), + ) + + return identifiedLockContext + } + + getLockContext(): Promise> { + return withResult( + () => { + if (!this.ctsToken?.accessToken && !this.ctsToken?.expiry) { + throw new Error( + 'The CTS token is not set. Please call identify() with a users JWT token, or pass an existing CTS token to the LockContext constructor before calling getLockContext().', + ) + } + + return { + context: this.context, + ctsToken: this.ctsToken, + } + }, + (error) => ({ + type: EncryptionErrorTypes.CtsTokenError, + message: error.message, + }), + ) + } +} diff --git a/packages/stack/src/index.ts b/packages/stack/src/index.ts new file mode 100644 index 00000000..de7b6f8a --- /dev/null +++ b/packages/stack/src/index.ts @@ -0,0 +1,177 @@ +import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/schema' +import { buildEncryptConfig } from '@cipherstash/schema' +import { EncryptionClient } from './ffi' +import type { KeysetIdentifier } from './types' + +export const EncryptionErrorTypes = { + ClientInitError: 'ClientInitError', + EncryptionError: 'EncryptionError', + DecryptionError: 'DecryptionError', + LockContextError: 'LockContextError', + CtsTokenError: 'CtsTokenError', +} + +/** @deprecated Use EncryptionErrorTypes */ +export const ProtectErrorTypes = EncryptionErrorTypes + +export interface EncryptionError { + type: (typeof EncryptionErrorTypes)[keyof typeof EncryptionErrorTypes] + message: string +} + +/** @deprecated Use EncryptionError */ +export type ProtectError = EncryptionError + +type AtLeastOneCsTable = [T, ...T[]] + +export type EncryptionClientConfig = { + schemas: AtLeastOneCsTable> + workspaceCrn?: string + accessKey?: string + clientId?: string + clientKey?: string + keyset?: KeysetIdentifier +} + +/** @deprecated Use EncryptionClientConfig */ +export type ProtectClientConfig = EncryptionClientConfig + +function isValidUuid(uuid: string): boolean { + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + return uuidRegex.test(uuid) +} + +/* Initialize an Encryption client with the provided configuration. + + @param config - The configuration object for initializing the Encryption client. + + @see {@link EncryptionClientConfig} for details on the configuration options. + + @returns A Promise that resolves to an instance of EncryptionClient. + + @throws Will throw an error if no schemas are provided or if the keyset ID is not a valid UUID. +*/ +export const Encryption = async ( + config: EncryptionClientConfig, +): Promise => { + const { schemas } = config + + if (!schemas.length) { + throw new Error( + '[encryption]: At least one encryptedTable must be provided to initialize the encryption client', + ) + } + + if ( + config.keyset && + 'id' in config.keyset && + !isValidUuid(config.keyset.id) + ) { + throw new Error( + '[encryption]: Invalid UUID provided for keyset id. Must be a valid UUID.', + ) + } + + const clientConfig = { + workspaceCrn: config.workspaceCrn, + accessKey: config.accessKey, + clientId: config.clientId, + clientKey: config.clientKey, + keyset: config.keyset, + } + + const client = new EncryptionClient(clientConfig.workspaceCrn) + const encryptConfig = buildEncryptConfig(...schemas) + + const result = await client.init({ + encryptConfig, + ...clientConfig, + }) + + if (result.failure) { + throw new Error(`[encryption]: ${result.failure.message}`) + } + + return result.data +} + +/** @deprecated Use Encryption */ +export const protect = Encryption + +export type { Result } from '@byteslice/result' +export type { EncryptionClient } from './ffi' +/** @deprecated Use EncryptionClient */ +export type { EncryptionClient as ProtectClient } from './ffi' +export type { EncryptionOperation } from './ffi/operations/base-operation' +/** @deprecated Use EncryptionOperation */ +export type { EncryptionOperation as ProtectOperation } from './ffi/operations/base-operation' +export type { BulkEncryptOperation } from './ffi/operations/bulk-encrypt' +export type { BulkDecryptOperation } from './ffi/operations/bulk-decrypt' +export type { BulkEncryptModelsOperation } from './ffi/operations/bulk-encrypt-models' +export type { BulkDecryptModelsOperation } from './ffi/operations/bulk-decrypt-models' +export type { DecryptOperation } from './ffi/operations/decrypt' +export type { DecryptModelOperation } from './ffi/operations/decrypt-model' +export type { EncryptModelOperation } from './ffi/operations/encrypt-model' +export type { EncryptOperation } from './ffi/operations/encrypt' + +// Operations +export { + EncryptQueryOperation, + EncryptQueryOperationWithLockContext, +} from './ffi/operations/encrypt-query' +export { + BatchEncryptQueryOperation, + BatchEncryptQueryOperationWithLockContext, +} from './ffi/operations/batch-encrypt-query' + +// Helpers +export { + inferIndexType, + validateIndexType, +} from './ffi/helpers/infer-index-type' + +// Types +export type { + QueryTypeName, + FfiIndexTypeName, + EncryptQueryOptions, + ScalarQueryTerm, +} from './types' + +export { queryTypes, queryTypeToFfi } from './types' + +// Schema re-exports (new names) +export { + encryptedTable, + encryptedColumn, + encryptedValue, + csTable, + csColumn, + csValue, +} from '@cipherstash/schema' +export type { + EncryptedTable, + EncryptedColumn, + EncryptedValue, + EncryptedTableColumn, + ProtectTable, + ProtectColumn, + ProtectTableColumn, + ProtectValue, +} from '@cipherstash/schema' + +// LockContext class export (value export for instantiation) +export { LockContext } from './identify' + +// LockContext related type exports +export type { + CtsRegions, + IdentifyOptions, + CtsToken, + Context, + LockContextOptions, + GetLockContextResponse, +} from './identify' +export * from './helpers' +export * from './types' diff --git a/packages/stack/src/stash/index.ts b/packages/stack/src/stash/index.ts new file mode 100644 index 00000000..059e5e2e --- /dev/null +++ b/packages/stack/src/stash/index.ts @@ -0,0 +1,474 @@ +import type { Result } from '@byteslice/result' +import { encryptedColumn, encryptedTable } from '@cipherstash/schema' +import { + Encryption, + type EncryptionClient, + encryptedToPgComposite, +} from '../index' +import type { Encrypted } from '../types' + +export type SecretName = string +export type SecretValue = string + +/** + * Configuration options for initializing the Stash client + */ +export interface StashConfig { + workspaceCRN: string + clientId: string + clientKey: string + environment: string + apiKey: string + accessKey?: string +} + +/** + * Secret metadata returned from the API + */ +export interface SecretMetadata { + id?: string + name: string + environment: string + createdAt?: string + updatedAt?: string +} + +/** + * API response for listing secrets + */ +export interface ListSecretsResponse { + environment: string + secrets: SecretMetadata[] +} + +/** + * API response for getting a secret + */ +export interface GetSecretResponse { + name: string + environment: string + encryptedValue: { + data: Encrypted + } + createdAt?: string + updatedAt?: string +} + +export interface DecryptedSecretResponse { + name: string + environment: string + value: string + createdAt?: string + updatedAt?: string +} + +/** + * The Stash client provides a high-level API for managing encrypted secrets + * stored in CipherStash. Secrets are encrypted locally before being sent to + * the API, ensuring end-to-end encryption. + */ +export class Stash { + private encryptionClient: EncryptionClient | null = null + private config: StashConfig + private readonly apiBaseUrl = + process.env.STASH_API_URL || 'https://getstash.sh/api/secrets' + private readonly secretsSchema = encryptedTable('secrets', { + value: encryptedColumn('value'), + }) + + /** + * Extracts the workspace ID from a CRN string. + * CRN format: crn:region.aws:ID + * + * @param crn The CRN string to extract from + * @returns The workspace ID portion of the CRN + */ + private extractWorkspaceIdFromCrn(crn: string): string { + const match = crn.match(/crn:[^:]+:([^:]+)$/) + if (!match) { + throw new Error('Invalid CRN format') + } + return match[1] + } + + constructor(config: StashConfig) { + this.config = config + } + + /** + * Initialize the Stash client and underlying Encryption client + */ + private async ensureInitialized(): Promise { + if (this.encryptionClient) { + return + } + + this.encryptionClient = await Encryption({ + schemas: [this.secretsSchema], + workspaceCrn: this.config.workspaceCRN, + clientId: this.config.clientId, + clientKey: this.config.clientKey, + accessKey: this.config.apiKey, + keyset: { + name: this.config.environment, + }, + }) + } + + /** + * Get the authorization header for API requests + */ + private getAuthHeader(): string { + return `Bearer ${this.config.apiKey}` + } + + /** + * Make an API request with error handling + */ + private async apiRequest( + method: string, + path: string, + body?: unknown, + ): Promise> { + try { + const url = `${this.apiBaseUrl}${path}` + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: this.getAuthHeader(), + } + + const response = await fetch(url, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }) + + if (!response.ok) { + const errorText = await response.text() + let errorMessage = `API request failed with status ${response.status}` + try { + const errorJson = JSON.parse(errorText) + errorMessage = errorJson.message || errorMessage + } catch { + errorMessage = errorText || errorMessage + } + + return { + failure: { + type: 'ApiError', + message: errorMessage, + }, + } + } + + const data = await response.json() + return { data } + } catch (error) { + return { + failure: { + type: 'NetworkError', + message: + error instanceof Error + ? error.message + : 'Unknown network error occurred', + }, + } + } + } + + /** + * Store an encrypted secret in the vault. + * The value is encrypted locally before being sent to the API. + * + * @param name - The name of the secret + * @param value - The plaintext value to encrypt and store + * @returns A Result indicating success or failure + * + * @example + * ```typescript + * const stash = new Stash({ ... }) + * const result = await stash.set('DATABASE_URL', 'postgres://user:pass@localhost:5432/mydb') + * if (result.failure) { + * console.error('Failed to set secret:', result.failure.message) + * } + * ``` + */ + async set( + name: SecretName, + value: SecretValue, + ): Promise> { + await this.ensureInitialized() + + if (!this.encryptionClient) { + return { + failure: { + type: 'ClientError', + message: 'Failed to initialize Encryption client', + }, + } + } + + // Encrypt the value locally + const encryptResult = await this.encryptionClient.encrypt(value, { + column: this.secretsSchema.value, + table: this.secretsSchema, + }) + + if (encryptResult.failure) { + return { + failure: { + type: 'EncryptionError', + message: encryptResult.failure.message, + }, + } + } + + // Extract workspaceId from CRN + const workspaceId = this.extractWorkspaceIdFromCrn(this.config.workspaceCRN) + + // Send encrypted value to API + return await this.apiRequest('POST', '/set', { + workspaceId, + environment: this.config.environment, + name, + encryptedValue: encryptedToPgComposite(encryptResult.data), + }) + } + + /** + * Retrieve and decrypt a secret from the vault. + * The secret is decrypted locally after retrieval. + * + * @param name - The name of the secret to retrieve + * @returns A Result containing the decrypted value or an error + * + * @example + * ```typescript + * const stash = new Stash({ ... }) + * const result = await stash.get('DATABASE_URL') + * if (result.failure) { + * console.error('Failed to get secret:', result.failure.message) + * } else { + * console.log('Secret value:', result.data) + * } + * ``` + */ + async get( + name: SecretName, + ): Promise> { + await this.ensureInitialized() + + if (!this.encryptionClient) { + return { + failure: { + type: 'ClientError', + message: 'Failed to initialize Encryption client', + }, + } + } + + // Extract workspaceId from CRN + const workspaceId = this.extractWorkspaceIdFromCrn(this.config.workspaceCRN) + + // Fetch encrypted value from API + const apiResult = await this.apiRequest('POST', '/get', { + workspaceId, + environment: this.config.environment, + name, + }) + + if (apiResult.failure) { + return apiResult + } + + // Decrypt the value locally + const decryptResult = await this.encryptionClient.decrypt( + apiResult.data.encryptedValue.data, + ) + + if (decryptResult.failure) { + return { + failure: { + type: 'DecryptionError', + message: decryptResult.failure.message, + }, + } + } + + if (typeof decryptResult.data !== 'string') { + return { + failure: { + type: 'DecryptionError', + message: 'Decrypted value is not a string', + }, + } + } + + return { data: decryptResult.data } + } + + /** + * Retrieve and decrypt many secrets from the vault. + * The secrets are decrypted locally after retrieval. + * This method only triggers a single network request to the ZeroKMS. + * + * @param names - The names of the secrets to retrieve + * @returns A Result containing an object mapping secret names to their decrypted values + * + * @example + * ```typescript + * const stash = new Stash({ ... }) + * const result = await stash.getMany(['DATABASE_URL', 'API_KEY']) + * if (result.failure) { + * console.error('Failed to get secrets:', result.failure.message) + * } else { + * const dbUrl = result.data.DATABASE_URL // Access by name + * const apiKey = result.data.API_KEY + * } + * ``` + */ + async getMany( + names: SecretName[], + ): Promise< + Result, { type: string; message: string }> + > { + await this.ensureInitialized() + + if (!this.encryptionClient) { + return { + failure: { + type: 'ClientError', + message: 'Failed to initialize Encryption client', + }, + } + } + + // Extract workspaceId from CRN + const workspaceId = this.extractWorkspaceIdFromCrn(this.config.workspaceCRN) + + // Fetch encrypted value from API + const apiResult = await this.apiRequest( + 'POST', + '/get-many', + { + workspaceId, + environment: this.config.environment, + names, + }, + ) + + if (apiResult.failure) { + return apiResult + } + + const dataToDecrypt = apiResult.data.map((item) => ({ + name: item.name, + value: item.encryptedValue.data, + })) + + const decryptResult = + await this.encryptionClient.bulkDecryptModels(dataToDecrypt) + + if (decryptResult.failure) { + return { + failure: { + type: 'DecryptionError', + message: decryptResult.failure.message, + }, + } + } + + console.log('Decrypt result:', JSON.stringify(decryptResult.data, null, 2)) + + // Transform array of decrypted secrets into an object keyed by secret name + const decryptedSecrets = + decryptResult.data as unknown as DecryptedSecretResponse[] + const secretsMap: Record = {} + + for (const secret of decryptedSecrets) { + if (secret.name && secret.value) { + secretsMap[secret.name] = secret.value + } + } + + return { data: secretsMap } + } + + /** + * List all secrets in the environment. + * Only names and metadata are returned; values remain encrypted. + * + * @returns A Result containing the list of secrets or an error + * + * @example + * ```typescript + * const stash = new Stash({ ... }) + * const result = await stash.list() + * if (result.failure) { + * console.error('Failed to list secrets:', result.failure.message) + * } else { + * console.log('Secrets:', result.data) + * } + * ``` + */ + async list(): Promise< + Result + > { + // Extract workspaceId from CRN + const workspaceId = this.extractWorkspaceIdFromCrn(this.config.workspaceCRN) + + const apiResult = await this.apiRequest( + 'POST', + '/list', + { + workspaceId, + environment: this.config.environment, + }, + ) + + if (apiResult.failure) { + return apiResult + } + + return { data: apiResult.data.secrets } + } + + /** + * Delete a secret from the vault. + * + * @param name - The name of the secret to delete + * @returns A Result indicating success or failure + * + * @example + * ```typescript + * const stash = new Stash({ ... }) + * const result = await stash.delete('DATABASE_URL') + * if (result.failure) { + * console.error('Failed to delete secret:', result.failure.message) + * } + * ``` + */ + async delete( + name: SecretName, + ): Promise> { + // Extract workspaceId from CRN + const workspaceId = this.extractWorkspaceIdFromCrn(this.config.workspaceCRN) + + return await this.apiRequest('POST', '/delete', { + workspaceId, + environment: this.config.environment, + name, + }) + } +} + +/** + * Initialize a Secrets client for managing encrypted secrets. + * + * @param config - The configuration options for the Stash client + * @returns A Promise that resolves to an initialized Stash instance + */ +export async function Secrets(config: StashConfig): Promise { + const client = new Stash(config) + return client +} diff --git a/packages/stack/src/types.ts b/packages/stack/src/types.ts new file mode 100644 index 00000000..69f27ca4 --- /dev/null +++ b/packages/stack/src/types.ts @@ -0,0 +1,193 @@ +import type { + Encrypted as CipherStashEncrypted, + JsPlaintext, + newClient, +} from '@cipherstash/protect-ffi' +import type { + EncryptedColumn, + EncryptedTable, + EncryptedTableColumn, + EncryptedValue, +} from '@cipherstash/schema' + +/** + * Type to represent the client object + */ +export type Client = Awaited> | undefined + +/** + * Type to represent an encrypted payload + */ +export type Encrypted = CipherStashEncrypted | null + +/** + * Represents an encrypted payload in the database + * @deprecated Use `Encrypted` instead + */ +export type EncryptedPayload = Encrypted | null + +/** + * Represents an encrypted data object in the database + * @deprecated Use `Encrypted` instead + */ +export type EncryptedData = Encrypted | null + +/** + * Represents a value that will be encrypted and used in a search + */ +export type SearchTerm = { + value: JsPlaintext + column: EncryptedColumn + table: EncryptedTable + returnType?: 'eql' | 'composite-literal' | 'escaped-composite-literal' +} + +export type KeysetIdentifier = + | { + name: string + } + | { + id: string + } + +/** + * The return type of the search term based on the return type specified in the `SearchTerm` type + * If the return type is `eql`, the return type is `Encrypted` + * If the return type is `composite-literal`, the return type is `string` where the value is a composite literal + * If the return type is `escaped-composite-literal`, the return type is `string` where the value is an escaped composite literal + */ +export type EncryptedSearchTerm = Encrypted | string + +/** + * Result type for encryptQuery batch operations. + * Can be Encrypted (default), string (for composite-literal formats), or null. + */ +export type EncryptedQueryResult = Encrypted | string | null + +/** + * Represents a payload to be encrypted using the `encrypt` function + */ +export type EncryptPayload = JsPlaintext | null + +/** + * Represents the options for encrypting a payload using the `encrypt` function + */ +export type EncryptOptions = { + column: EncryptedColumn | EncryptedValue + table: EncryptedTable +} + +/** + * Type to identify encrypted fields in a model + */ +export type EncryptedFields = { + [K in keyof T as T[K] extends Encrypted ? K : never]: T[K] +} + +/** + * Type to identify non-encrypted fields in a model + */ +export type OtherFields = { + [K in keyof T as T[K] extends Encrypted ? never : K]: T[K] +} + +/** + * Type to represent decrypted fields in a model + */ +export type DecryptedFields = { + [K in keyof T as T[K] extends Encrypted ? K : never]: string +} + +/** + * Represents a model with plaintext (decrypted) values instead of the EQL/JSONB types + */ +export type Decrypted = OtherFields & DecryptedFields + +/** + * Types for bulk encryption and decryption operations. + */ +export type BulkEncryptPayload = Array<{ + id?: string + plaintext: JsPlaintext | null +}> + +export type BulkEncryptedData = Array<{ id?: string; data: Encrypted }> +export type BulkDecryptPayload = Array<{ id?: string; data: Encrypted }> +export type BulkDecryptedData = Array> + +type DecryptionSuccess = { + error?: never + data: T + id?: string +} + +type DecryptionError = { + error: T + id?: string + data?: never +} + +export type DecryptionResult = DecryptionSuccess | DecryptionError + +/** + * User-facing query type names for encrypting query values. + * + * - `'equality'`: For exact match queries. {@link https://cipherstash.com/docs/platform/searchable-encryption/supported-queries/exact | Exact Queries} + * - `'freeTextSearch'`: For text search queries. {@link https://cipherstash.com/docs/platform/searchable-encryption/supported-queries/match | Match Queries} + * - `'orderAndRange'`: For comparison and range queries. {@link https://cipherstash.com/docs/platform/searchable-encryption/supported-queries/range | Range Queries} + */ +export type QueryTypeName = 'orderAndRange' | 'freeTextSearch' | 'equality' + +/** + * Internal FFI index type names. + * @internal + */ +export type FfiIndexTypeName = 'ore' | 'match' | 'unique' + +/** + * Query type constants for use with encryptQuery(). + */ +export const queryTypes = { + orderAndRange: 'orderAndRange', + freeTextSearch: 'freeTextSearch', + equality: 'equality', +} as const satisfies Record + +/** + * Maps user-friendly query type names to FFI index type names. + * @internal + */ +export const queryTypeToFfi: Record = { + orderAndRange: 'ore', + freeTextSearch: 'match', + equality: 'unique', +} + +/** + * Base type for query term options shared between single and bulk operations. + * @internal + */ +export type QueryTermBase = { + column: EncryptedColumn + table: EncryptedTable + queryType?: QueryTypeName // Optional - auto-infers if omitted + /** + * The format for the returned encrypted value: + * - `'eql'` (default) - Returns raw Encrypted object + * - `'composite-literal'` - Returns PostgreSQL composite literal format `("json")` + * - `'escaped-composite-literal'` - Returns escaped format `"(\"json\")"` + */ + returnType?: 'eql' | 'composite-literal' | 'escaped-composite-literal' +} + +/** + * Options for encrypting a single query term. + */ +export type EncryptQueryOptions = QueryTermBase + +/** + * Individual query term for bulk operations. + */ +export type ScalarQueryTerm = QueryTermBase & { + value: JsPlaintext | null +} diff --git a/packages/stack/tsconfig.json b/packages/stack/tsconfig.json new file mode 100644 index 00000000..63982418 --- /dev/null +++ b/packages/stack/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ES2022", "DOM"], + "target": "ES2022", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/packages/stack/tsup.config.ts b/packages/stack/tsup.config.ts new file mode 100644 index 00000000..a8817fb3 --- /dev/null +++ b/packages/stack/tsup.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'tsup' + +export default defineConfig([ + { + entry: { + index: 'src/index.ts', + client: 'src/client.ts', + 'identify/index': 'src/identify/index.ts', + 'secrets/index': 'src/stash/index.ts', + }, + format: ['cjs', 'esm'], + sourcemap: true, + dts: true, + target: 'es2022', + tsconfig: './tsconfig.json', + }, + { + entry: ['src/bin/stash.ts'], + outDir: 'dist/bin', + format: ['esm'], + target: 'es2022', + banner: { + js: '#!/usr/bin/env node', + }, + dts: false, + sourcemap: true, + external: ['dotenv'], + noExternal: [], + }, +]) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7998546d..a1de93e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,9 +59,9 @@ importers: examples/basic: dependencies: - '@cipherstash/protect': + '@cipherstash/stack': specifier: workspace:* - version: link:../../packages/protect + version: link:../../packages/stack dotenv: specifier: ^16.4.7 version: 16.6.1 @@ -78,9 +78,9 @@ importers: '@cipherstash/drizzle': specifier: workspace:* version: link:../../packages/drizzle - '@cipherstash/protect': + '@cipherstash/stack': specifier: workspace:* - version: link:../../packages/protect + version: link:../../packages/stack drizzle-orm: specifier: ^0.44.7 version: 0.44.7(@types/pg@8.16.0)(gel@2.2.0)(mysql2@3.16.0)(pg@8.16.3)(postgres@3.4.7) @@ -127,12 +127,12 @@ importers: '@aws-sdk/util-dynamodb': specifier: ^3.817.0 version: 3.955.0(@aws-sdk/client-dynamodb@3.955.0) - '@cipherstash/protect': - specifier: workspace:* - version: link:../../packages/protect '@cipherstash/protect-dynamodb': specifier: workspace:* - version: link:../../packages/protect-dynamodb + version: link:../../packages/dynamodb + '@cipherstash/stack': + specifier: workspace:* + version: link:../../packages/stack pg: specifier: ^8.16.3 version: 8.16.3 @@ -149,9 +149,9 @@ importers: examples/hono-supabase: dependencies: - '@cipherstash/protect': + '@cipherstash/stack': specifier: workspace:* - version: link:../../packages/protect + version: link:../../packages/stack '@hono/node-server': specifier: ^1.13.7 version: 1.19.7(hono@4.11.1) @@ -177,9 +177,9 @@ importers: examples/nest: dependencies: - '@cipherstash/protect': + '@cipherstash/stack': specifier: workspace:* - version: link:../../packages/protect + version: link:../../packages/stack '@nestjs/common': specifier: ^11.0.1 version: 11.1.9(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -265,9 +265,9 @@ importers: examples/next-drizzle-mysql: dependencies: - '@cipherstash/protect': + '@cipherstash/stack': specifier: workspace:* - version: link:../../packages/protect + version: link:../../packages/stack '@hookform/resolvers': specifier: ^5.0.1 version: 5.2.2(react-hook-form@7.68.0(react@19.2.3)) @@ -323,9 +323,9 @@ importers: '@cipherstash/nextjs': specifier: workspace:* version: link:../../packages/nextjs - '@cipherstash/protect': + '@cipherstash/stack': specifier: workspace:* - version: link:../../packages/protect + version: link:../../packages/stack '@clerk/nextjs': specifier: catalog:security version: 6.31.2(next@15.5.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -408,9 +408,9 @@ importers: examples/typeorm: dependencies: - '@cipherstash/protect': + '@cipherstash/stack': specifier: workspace:* - version: link:../../packages/protect + version: link:../../packages/stack dotenv: specifier: ^16.4.7 version: 16.6.1 @@ -455,12 +455,12 @@ importers: specifier: ^3.4.7 version: 3.4.7 devDependencies: - '@cipherstash/protect': - specifier: workspace:* - version: link:../protect '@cipherstash/schema': specifier: workspace:* version: link:../schema + '@cipherstash/stack': + specifier: workspace:* + version: link:../stack dotenv: specifier: ^16.4.7 version: 16.6.1 @@ -477,6 +477,31 @@ importers: specifier: catalog:repo version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + packages/dynamodb: + dependencies: + '@byteslice/result': + specifier: ^0.2.0 + version: 0.2.2 + devDependencies: + '@cipherstash/stack': + specifier: workspace:* + version: link:../stack + dotenv: + specifier: ^16.4.7 + version: 16.4.7 + tsup: + specifier: catalog:repo + version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) + tsx: + specifier: catalog:repo + version: 4.19.3 + typescript: + specifier: catalog:repo + version: 5.6.3 + vitest: + specifier: catalog:repo + version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + packages/nextjs: dependencies: jose: @@ -553,24 +578,15 @@ importers: specifier: 4.24.0 version: 4.24.0 - packages/protect-dynamodb: + packages/schema: dependencies: - '@byteslice/result': - specifier: ^0.2.0 - version: 0.2.2 + zod: + specifier: ^3.24.2 + version: 3.25.76 devDependencies: - '@cipherstash/protect': - specifier: workspace:* - version: link:../protect - dotenv: - specifier: ^16.4.7 - version: 16.6.1 tsup: specifier: catalog:repo version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) - tsx: - specifier: catalog:repo - version: 4.19.3 typescript: specifier: catalog:repo version: 5.6.3 @@ -578,21 +594,52 @@ importers: specifier: catalog:repo version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) - packages/schema: + packages/stack: dependencies: + '@byteslice/result': + specifier: ^0.2.0 + version: 0.2.2 + '@cipherstash/protect-ffi': + specifier: 0.20.1 + version: 0.20.1 + '@cipherstash/schema': + specifier: workspace:* + version: link:../schema + '@stricli/core': + specifier: ^1.2.5 + version: 1.2.5 + dotenv: + specifier: 16.4.7 + version: 16.4.7 zod: specifier: ^3.24.2 version: 3.25.76 devDependencies: + '@supabase/supabase-js': + specifier: ^2.47.10 + version: 2.89.0 + execa: + specifier: ^9.5.2 + version: 9.6.1 + json-schema-to-typescript: + specifier: ^15.0.2 + version: 15.0.4 tsup: specifier: catalog:repo version: 8.4.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.19.3)(typescript@5.6.3) + tsx: + specifier: catalog:repo + version: 4.19.3 typescript: specifier: catalog:repo version: 5.6.3 vitest: specifier: catalog:repo version: 3.1.3(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.19.3) + optionalDependencies: + '@rollup/rollup-linux-x64-gnu': + specifier: 4.24.0 + version: 4.24.0 packages: From f8a12292bed82fd453a95b237346a5fd28bfd3a4 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Wed, 11 Feb 2026 17:54:14 -0700 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20complete=20Protect=20=E2=86=92=20Enc?= =?UTF-8?q?ryption=20branding=20rename=20in=20active=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace `ReturnType` with `EncryptionClient` type in 8 stack test files - Fix `ProtectClient` JSDoc reference in decrypt.ts → `EncryptionClient` - Rename `[protect]` error prefix to `[encryption]` in 10 stack runtime files - Update Nest example README: `ProtectClient` → `EncryptionClient` - Update Drizzle docs prose: "protect operators" → "encryption operators" - Update schema tests: `csTable`/`csColumn`/`csValue` → `encryptedTable`/`encryptedColumn`/`encryptedValue` Co-Authored-By: Claude Opus 4.6 --- docs/reference/drizzle/drizzle-protect.md | 20 ++++---- docs/reference/drizzle/drizzle.md | 8 ++-- examples/nest/README.md | 2 +- packages/schema/__tests__/schema.test.ts | 46 +++++++++++-------- .../schema/__tests__/searchable-json.test.ts | 18 +++++--- packages/stack/__tests__/audit.test.ts | 4 +- .../stack/__tests__/backward-compat.test.ts | 4 +- .../stack/__tests__/basic-protect.test.ts | 4 +- packages/stack/__tests__/bulk-protect.test.ts | 12 +++-- packages/stack/__tests__/json-protect.test.ts | 4 +- packages/stack/__tests__/lock-context.test.ts | 4 +- .../stack/__tests__/number-protect.test.ts | 4 +- packages/stack/__tests__/protect-ops.test.ts | 4 +- packages/stack/src/ffi/helpers/validation.ts | 12 ++--- .../src/ffi/operations/bulk-decrypt-models.ts | 2 +- .../stack/src/ffi/operations/bulk-decrypt.ts | 2 +- .../src/ffi/operations/bulk-encrypt-models.ts | 2 +- .../stack/src/ffi/operations/bulk-encrypt.ts | 2 +- .../stack/src/ffi/operations/decrypt-model.ts | 2 +- packages/stack/src/ffi/operations/decrypt.ts | 4 +- .../stack/src/ffi/operations/encrypt-model.ts | 2 +- packages/stack/src/ffi/operations/encrypt.ts | 6 +-- 22 files changed, 92 insertions(+), 76 deletions(-) diff --git a/docs/reference/drizzle/drizzle-protect.md b/docs/reference/drizzle/drizzle-protect.md index 3d9ce7f7..0f740dfb 100644 --- a/docs/reference/drizzle/drizzle-protect.md +++ b/docs/reference/drizzle/drizzle-protect.md @@ -10,7 +10,7 @@ This page demonstrates how to perform queries on encrypted data using **Drizzle - Use standard Drizzle operators (`eq()`, `gte()`, `lte()`) with pre-encrypted values - Manually decrypt results using `protectClient.bulkDecryptModels()` -This verbose pattern demonstrates the low-level encryption workflow. For cleaner syntax, see the [protect operators pattern](/reference/drizzle/drizzle). +This verbose pattern demonstrates the low-level encryption workflow. For cleaner syntax, see the [encryption operators pattern](/reference/drizzle/drizzle). ## Overview @@ -21,7 +21,7 @@ This example uses a `transactions` table with the following encrypted fields: - **`description`**: Encrypted with full-text search support - **`createdAt`**: Encrypted timestamp with range query support -**Key differences from protect operators pattern:** +**Key differences from encryption operators pattern:** 1. **Manual encryption** of query parameters using `protectClient.encrypt()` 2. **Standard Drizzle operators** (`eq()`, `gte()`, `lte()`) with pre-encrypted values @@ -39,7 +39,7 @@ This gives you explicit visibility into the encryption/decryption workflow at th - You're implementing batch operations with encryption - You want to inspect encrypted values for debugging -**Recommended:** Most applications should use the [protect operators pattern](/reference/drizzle/drizzle) for cleaner syntax. +**Recommended:** Most applications should use the [encryption operators pattern](/reference/drizzle/drizzle) for cleaner syntax. ## Setup @@ -226,7 +226,7 @@ return decrypted.data Search for transactions with "gym" in the description. With the manual encryption pattern, you must encrypt the search pattern and cast it to the encrypted type. -**Note:** Unlike the protect operators pattern which provides `protect.like()` wrapper, the manual encryption pattern requires using Drizzle's `sql` template with manual type casting. This gives you full control over the encryption and query construction at the cost of more verbose syntax. +**Note:** Unlike the encryption operators pattern which provides `protect.like()` wrapper, the manual encryption pattern requires using Drizzle's `sql` template with manual type casting. This gives you full control over the encryption and query construction at the cost of more verbose syntax. ```ts:run // Encrypt the search pattern @@ -451,7 +451,7 @@ return decrypted.data ## Understanding the manual approach -When using the manual encryption pattern instead of protect operators, you have explicit control over each step: +When using the manual encryption pattern instead of encryption operators, you have explicit control over each step: ### Encryption flow @@ -461,9 +461,9 @@ When using the manual encryption pattern instead of protect operators, you have 4. **Decrypt results** using `protectClient.bulkDecryptModels(results)` 5. **Return plaintext** - Results are now readable -### Key differences from protect operators pattern +### Key differences from encryption operators pattern -| Aspect | Protect Operators | Manual Encryption | +| Aspect | Encryption Operators | Manual Encryption | |--------|------------------|-------------------| | Encryption | `protect.eq(col, val)` | `encrypt()` + `eq()` | | Code | Auto-encrypting operators | Explicit encryption calls | @@ -483,7 +483,7 @@ When using the manual encryption pattern instead of protect operators, you have - You're implementing batch operations - You want to inspect encrypted values for debugging -✅ **Use protect operators when:** +✅ **Use encryption operators when:** - You want the simplest developer experience - You prefer cleaner query syntax - You're building standard CRUD operations @@ -545,7 +545,7 @@ transactions.amount transactions.description transactions.createdAt -// Protect schema (for encryption operations) +// Encryption schema (for encryption operations) protectTransactions.account_number // Note: snake_case protectTransactions.amount protectTransactions.description @@ -556,7 +556,7 @@ protectTransactions.created_at // Note: snake_case ## Next steps -- **Compare patterns**: Try the same query with both protect operators and manual encryption +- **Compare patterns**: Try the same query with both encryption operators and manual encryption - **Explore the code**: Check out the source code in the repository - **Try different queries**: Modify the examples above and run them - **Read the docs**: Visit [CipherStash Stash Encryption documentation](https://docs.cipherstash.com/) diff --git a/docs/reference/drizzle/drizzle.md b/docs/reference/drizzle/drizzle.md index e32f38bb..afd4492c 100644 --- a/docs/reference/drizzle/drizzle.md +++ b/docs/reference/drizzle/drizzle.md @@ -1,7 +1,7 @@ # Drizzle + Stash Encryption Query Examples -## Protect Operators Pattern (Recommended) +## Encryption Operators Pattern (Recommended) -This page demonstrates how to perform queries on encrypted data using **Drizzle ORM** with **CipherStash Stash Encryption** using the **protect operators pattern**. +This page demonstrates how to perform queries on encrypted data using **Drizzle ORM** with **CipherStash Stash Encryption** using the **encryption operators pattern**. **Pattern:** Auto-encrypting operators from `createProtectOperators()` provide clean syntax with automatic encryption. @@ -30,7 +30,7 @@ The TypeScript schema uses camelCase property names that map to snake_case datab ## When to Use This Pattern -✅ **Use the protect operators pattern when:** +✅ **Use the encryption operators pattern when:** - You want clean, readable query syntax - You're building standard CRUD applications - You prefer minimal boilerplate code @@ -39,7 +39,7 @@ The TypeScript schema uses camelCase property names that map to snake_case datab This is the **recommended pattern** for most use cases. -**Alternative:** See [manual encryption pattern](/reference/drizzle/drizzle-protect) for explicit control over encryption workflow. +**Alternative:** See [manual encryption pattern](/reference/drizzle/drizzle-protect) for explicit control over the encryption workflow. ## Setup diff --git a/examples/nest/README.md b/examples/nest/README.md index 2f54cc35..f627b661 100644 --- a/examples/nest/README.md +++ b/examples/nest/README.md @@ -25,7 +25,7 @@ CS_CLIENT_ACCESS_KEY= ### How encryption works here - `src/protect/schema.ts` defines tables with `.equality()`, `.orderAndRange()`, `.freeTextSearch()` for searchable encryption on Postgres. -- `ProtectModule` initializes a `ProtectClient` with those schemas and injects a `ProtectService`. +- `ProtectModule` initializes an `EncryptionClient` with those schemas and injects a `ProtectService`. - `AppService` uses `encryptModel`/`decryptModel` and bulk variants to demonstrate single and bulk flows. ### Minimal API demo diff --git a/packages/schema/__tests__/schema.test.ts b/packages/schema/__tests__/schema.test.ts index d1d99a51..6ade91f7 100644 --- a/packages/schema/__tests__/schema.test.ts +++ b/packages/schema/__tests__/schema.test.ts @@ -1,15 +1,23 @@ import { describe, expect, it } from 'vitest' -import { buildEncryptConfig, csColumn, csTable, csValue } from '../src' +import { + buildEncryptConfig, + encryptedColumn, + encryptedTable, + encryptedValue, +} from '../src' describe('Schema with nested columns', () => { it('should handle nested column structures in encrypt config', () => { - const users = csTable('users', { - email: csColumn('email').freeTextSearch().equality().orderAndRange(), - address: csColumn('address').freeTextSearch(), + const users = encryptedTable('users', { + email: encryptedColumn('email') + .freeTextSearch() + .equality() + .orderAndRange(), + address: encryptedColumn('address').freeTextSearch(), example: { - field: csValue('example.field'), + field: encryptedValue('example.field'), nested: { - deep: csValue('example.nested.deep'), + deep: encryptedValue('example.nested.deep'), }, }, } as const) @@ -65,17 +73,17 @@ describe('Schema with nested columns', () => { }) it('should handle multiple tables with nested columns', () => { - const users = csTable('users', { - email: csColumn('email').equality(), + const users = encryptedTable('users', { + email: encryptedColumn('email').equality(), profile: { - name: csValue('profile.name'), + name: encryptedValue('profile.name'), }, } as const) - const posts = csTable('posts', { - title: csColumn('title').freeTextSearch(), + const posts = encryptedTable('posts', { + title: encryptedColumn('title').freeTextSearch(), metadata: { - tags: csValue('metadata.tags'), + tags: encryptedValue('metadata.tags'), }, } as const) @@ -94,14 +102,14 @@ describe('Schema with nested columns', () => { }) it('should handle complex nested structures with multiple index types', () => { - const complex = csTable('complex', { - id: csColumn('id').equality(), + const complex = encryptedTable('complex', { + id: encryptedColumn('id').equality(), content: { - text: csValue('content.text'), + text: encryptedValue('content.text'), metadata: { - tags: csValue('content.metadata.tags'), + tags: encryptedValue('content.metadata.tags'), stats: { - views: csValue('content.metadata.stats.views'), + views: encryptedValue('content.metadata.stats.views'), }, }, }, @@ -132,8 +140,8 @@ describe('Schema with nested columns', () => { // NOTE: Leaving this test commented out until stevec indexing for JSON is supported. /*it('should handle ste_vec index for JSON columns', () => { - const users = csTable('users', { - json: csColumn('json').dataType('jsonb').searchableJson(), + const users = encryptedTable('users', { + json: encryptedColumn('json').dataType('jsonb').searchableJson(), } as const) const config = buildEncryptConfig(users) diff --git a/packages/schema/__tests__/searchable-json.test.ts b/packages/schema/__tests__/searchable-json.test.ts index 8f14ec7c..841c2193 100644 --- a/packages/schema/__tests__/searchable-json.test.ts +++ b/packages/schema/__tests__/searchable-json.test.ts @@ -1,9 +1,13 @@ import { describe, expect, it } from 'vitest' -import { buildEncryptConfig, csColumn, csTable } from '../src/index' +import { + buildEncryptConfig, + encryptedColumn, + encryptedTable, +} from '../src/index' describe('searchableJson()', () => { it('sets cast_as to json and ste_vec marker on column build', () => { - const column = csColumn('metadata').searchableJson() + const column = encryptedColumn('metadata').searchableJson() const config = column.build() expect(config.cast_as).toBe('json') @@ -11,15 +15,15 @@ describe('searchableJson()', () => { }) it('is chainable', () => { - const column = csColumn('metadata') + const column = encryptedColumn('metadata') expect(column.searchableJson()).toBe(column) }) }) describe('ProtectTable.build() with searchableJson', () => { it('transforms prefix to table/column format', () => { - const users = csTable('users', { - metadata: csColumn('metadata').searchableJson(), + const users = encryptedTable('users', { + metadata: encryptedColumn('metadata').searchableJson(), }) const built = users.build() @@ -32,8 +36,8 @@ describe('ProtectTable.build() with searchableJson', () => { describe('buildEncryptConfig with searchableJson', () => { it('emits ste_vec index with table/column prefix', () => { - const users = csTable('users', { - metadata: csColumn('metadata').searchableJson(), + const users = encryptedTable('users', { + metadata: encryptedColumn('metadata').searchableJson(), }) const config = buildEncryptConfig(users) diff --git a/packages/stack/__tests__/audit.test.ts b/packages/stack/__tests__/audit.test.ts index 4021e18c..d01363f0 100644 --- a/packages/stack/__tests__/audit.test.ts +++ b/packages/stack/__tests__/audit.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config' import { encryptedColumn, encryptedTable } from '@cipherstash/schema' import { beforeAll, describe, expect, it } from 'vitest' -import { Encryption, LockContext } from '../src' +import { Encryption, type EncryptionClient, LockContext } from '../src' const users = encryptedTable('users', { auditable: encryptedColumn('auditable'), @@ -19,7 +19,7 @@ type User = { number?: number } -let protectClient: Awaited> +let protectClient: EncryptionClient beforeAll(async () => { protectClient = await Encryption({ diff --git a/packages/stack/__tests__/backward-compat.test.ts b/packages/stack/__tests__/backward-compat.test.ts index add6e554..1b17a70a 100644 --- a/packages/stack/__tests__/backward-compat.test.ts +++ b/packages/stack/__tests__/backward-compat.test.ts @@ -1,14 +1,14 @@ import 'dotenv/config' import { encryptedColumn, encryptedTable } from '@cipherstash/schema' import { beforeAll, describe, expect, it } from 'vitest' -import { Encryption } from '../src' +import { Encryption, type EncryptionClient } from '../src' const users = encryptedTable('users', { email: encryptedColumn('email'), }) describe('k-field backward compatibility', () => { - let protectClient: Awaited> + let protectClient: EncryptionClient beforeAll(async () => { protectClient = await Encryption({ schemas: [users] }) diff --git a/packages/stack/__tests__/basic-protect.test.ts b/packages/stack/__tests__/basic-protect.test.ts index e795f28c..6021f18e 100644 --- a/packages/stack/__tests__/basic-protect.test.ts +++ b/packages/stack/__tests__/basic-protect.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config' import { encryptedColumn, encryptedTable } from '@cipherstash/schema' import { beforeAll, describe, expect, it } from 'vitest' -import { Encryption } from '../src' +import { Encryption, type EncryptionClient } from '../src' const users = encryptedTable('users', { email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), @@ -9,7 +9,7 @@ const users = encryptedTable('users', { json: encryptedColumn('json').dataType('json'), }) -let protectClient: Awaited> +let protectClient: EncryptionClient beforeAll(async () => { protectClient = await Encryption({ diff --git a/packages/stack/__tests__/bulk-protect.test.ts b/packages/stack/__tests__/bulk-protect.test.ts index 21b502bb..97fac935 100644 --- a/packages/stack/__tests__/bulk-protect.test.ts +++ b/packages/stack/__tests__/bulk-protect.test.ts @@ -1,7 +1,12 @@ import 'dotenv/config' import { encryptedColumn, encryptedTable } from '@cipherstash/schema' import { beforeAll, describe, expect, it } from 'vitest' -import { type EncryptedPayload, Encryption, LockContext } from '../src' +import { + type Encrypted, + Encryption, + type EncryptionClient, + LockContext, +} from '../src' const users = encryptedTable('users', { email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), @@ -17,7 +22,7 @@ type User = { number?: number } -let protectClient: Awaited> +let protectClient: EncryptionClient beforeAll(async () => { protectClient = await Encryption({ @@ -315,8 +320,7 @@ describe('bulk encryption and decryption', () => { }, 30000) it('should handle empty array in bulk decrypt', async () => { - const encryptedPayloads: Array<{ id?: string; data: EncryptedPayload }> = - [] + const encryptedPayloads: Array<{ id?: string; data: Encrypted }> = [] const decryptedData = await protectClient.bulkDecrypt(encryptedPayloads) diff --git a/packages/stack/__tests__/json-protect.test.ts b/packages/stack/__tests__/json-protect.test.ts index 830a2221..4aac798e 100644 --- a/packages/stack/__tests__/json-protect.test.ts +++ b/packages/stack/__tests__/json-protect.test.ts @@ -5,7 +5,7 @@ import { encryptedValue, } from '@cipherstash/schema' import { beforeAll, describe, expect, it } from 'vitest' -import { Encryption, LockContext } from '../src' +import { Encryption, type EncryptionClient, LockContext } from '../src' const users = encryptedTable('users', { email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), @@ -36,7 +36,7 @@ type User = { } } -let protectClient: Awaited> +let protectClient: EncryptionClient beforeAll(async () => { protectClient = await Encryption({ diff --git a/packages/stack/__tests__/lock-context.test.ts b/packages/stack/__tests__/lock-context.test.ts index 53e392ad..489c0a29 100644 --- a/packages/stack/__tests__/lock-context.test.ts +++ b/packages/stack/__tests__/lock-context.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config' import { encryptedColumn, encryptedTable } from '@cipherstash/schema' import { beforeAll, describe, expect, it } from 'vitest' -import { Encryption } from '../src' +import { Encryption, type EncryptionClient } from '../src' import { LockContext } from '../src/identify' const users = encryptedTable('users', { @@ -18,7 +18,7 @@ type User = { number?: number } -let protectClient: Awaited> +let protectClient: EncryptionClient beforeAll(async () => { protectClient = await Encryption({ diff --git a/packages/stack/__tests__/number-protect.test.ts b/packages/stack/__tests__/number-protect.test.ts index abafbb0d..b3ad72ff 100644 --- a/packages/stack/__tests__/number-protect.test.ts +++ b/packages/stack/__tests__/number-protect.test.ts @@ -5,7 +5,7 @@ import { encryptedValue, } from '@cipherstash/schema' import { beforeAll, describe, expect, it, test } from 'vitest' -import { Encryption, LockContext } from '../src' +import { Encryption, type EncryptionClient, LockContext } from '../src' const users = encryptedTable('users', { email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), @@ -32,7 +32,7 @@ type User = { } } -let protectClient: Awaited> +let protectClient: EncryptionClient beforeAll(async () => { protectClient = await Encryption({ diff --git a/packages/stack/__tests__/protect-ops.test.ts b/packages/stack/__tests__/protect-ops.test.ts index b31e4f11..74b9dac8 100644 --- a/packages/stack/__tests__/protect-ops.test.ts +++ b/packages/stack/__tests__/protect-ops.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config' import { encryptedColumn, encryptedTable } from '@cipherstash/schema' import { beforeAll, describe, expect, it } from 'vitest' -import { Encryption, LockContext } from '../src' +import { Encryption, type EncryptionClient, LockContext } from '../src' const users = encryptedTable('users', { email: encryptedColumn('email').freeTextSearch().equality().orderAndRange(), @@ -17,7 +17,7 @@ type User = { number?: number } -let protectClient: Awaited> +let protectClient: EncryptionClient beforeAll(async () => { protectClient = await Encryption({ diff --git a/packages/stack/src/ffi/helpers/validation.ts b/packages/stack/src/ffi/helpers/validation.ts index 39ec839b..e2e60b94 100644 --- a/packages/stack/src/ffi/helpers/validation.ts +++ b/packages/stack/src/ffi/helpers/validation.ts @@ -18,7 +18,7 @@ export function validateNumericValue( return { failure: { type: EncryptionErrorTypes.EncryptionError, - message: '[protect]: Cannot encrypt NaN value', + message: '[encryption]: Cannot encrypt NaN value', }, } } @@ -26,7 +26,7 @@ export function validateNumericValue( return { failure: { type: EncryptionErrorTypes.EncryptionError, - message: '[protect]: Cannot encrypt Infinity value', + message: '[encryption]: Cannot encrypt Infinity value', }, } } @@ -42,10 +42,10 @@ export function validateNumericValue( */ export function assertValidNumericValue(value: unknown): void { if (typeof value === 'number' && Number.isNaN(value)) { - throw new Error('[protect]: Cannot encrypt NaN value') + throw new Error('[encryption]: Cannot encrypt NaN value') } if (typeof value === 'number' && !Number.isFinite(value)) { - throw new Error('[protect]: Cannot encrypt Infinity value') + throw new Error('[encryption]: Cannot encrypt Infinity value') } } @@ -66,7 +66,7 @@ export function validateValueIndexCompatibility( return { failure: { type: EncryptionErrorTypes.EncryptionError, - message: `[protect]: Cannot use 'match' index with numeric value on column "${columnName}". The 'freeTextSearch' index only supports string values. Configure the column with 'orderAndRange()' or 'equality()' for numeric queries.`, + message: `[encryption]: Cannot use 'match' index with numeric value on column "${columnName}". The 'freeTextSearch' index only supports string values. Configure the column with 'orderAndRange()' or 'equality()' for numeric queries.`, }, } } @@ -88,7 +88,7 @@ export function assertValueIndexCompatibility( ): void { if (typeof value === 'number' && indexType === 'match') { throw new Error( - `[protect]: Cannot use 'match' index with numeric value on column "${columnName}". The 'freeTextSearch' index only supports string values. Configure the column with 'orderAndRange()' or 'equality()' for numeric queries.`, + `[encryption]: Cannot use 'match' index with numeric value on column "${columnName}". The 'freeTextSearch' index only supports string values. Configure the column with 'orderAndRange()' or 'equality()' for numeric queries.`, ) } } diff --git a/packages/stack/src/ffi/operations/bulk-decrypt-models.ts b/packages/stack/src/ffi/operations/bulk-decrypt-models.ts index c3d40415..12f50abe 100644 --- a/packages/stack/src/ffi/operations/bulk-decrypt-models.ts +++ b/packages/stack/src/ffi/operations/bulk-decrypt-models.ts @@ -90,7 +90,7 @@ export class BulkDecryptModelsOperationWithLockContext< const context = await this.lockContext.getLockContext() if (context.failure) { - throw new Error(`[protect]: ${context.failure.message}`) + throw new Error(`[encryption]: ${context.failure.message}`) } const auditData = this.getAuditData() diff --git a/packages/stack/src/ffi/operations/bulk-decrypt.ts b/packages/stack/src/ffi/operations/bulk-decrypt.ts index ea1afc1b..c368e624 100644 --- a/packages/stack/src/ffi/operations/bulk-decrypt.ts +++ b/packages/stack/src/ffi/operations/bulk-decrypt.ts @@ -146,7 +146,7 @@ export class BulkDecryptOperationWithLockContext extends EncryptionOperation { private client: Client @@ -110,7 +110,7 @@ export class DecryptOperationWithLockContext extends EncryptionOperation { typeof this.plaintext === 'number' && Number.isNaN(this.plaintext) ) { - throw new Error('[protect]: Cannot encrypt NaN value') + throw new Error('[encryption]: Cannot encrypt NaN value') } if ( typeof this.plaintext === 'number' && !Number.isFinite(this.plaintext) ) { - throw new Error('[protect]: Cannot encrypt Infinity value') + throw new Error('[encryption]: Cannot encrypt Infinity value') } const { metadata } = this.getAuditData() @@ -136,7 +136,7 @@ export class EncryptOperationWithLockContext extends EncryptionOperation Date: Wed, 11 Feb 2026 17:59:43 -0700 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20address=20remaining=20Protect=20?= =?UTF-8?q?=E2=86=92=20Encryption=20branding=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes from collated rename verification report: - E1-A1: Fix TypeORM example type error (EncryptionClientConfig → EncryptionClient) - E9-A2: Update CLI branding from "CipherStash Protect" to "CipherStash Stash" - E10-A2: Add @cipherstash/stack to SECURITY.md supported packages - C3: Rename internal AtLeastOneCsTable → AtLeastOneEncryptedTable - E3-A1: Update JSDoc eqlClient variable references → encryptionClient - E12-A2: Fix Drizzle JSDoc "ProtectTable" → "EncryptedTable" - E2-A1: Replace [protect]: error prefix with [encryption]: in 232 test assertions - E15-A2: Update schema test describe block ProtectTable → encryptedTable - E6-A2: Add deprecation notice to protect package README - E11-A2: Add @cipherstash/stack to changeset Co-Authored-By: Claude Opus 4.6 --- .changeset/ste-vec-query-support.md | 1 + SECURITY.md | 6 ++ .../typeorm/src/helpers/protect-entity.ts | 4 +- packages/drizzle/src/pg/operators.ts | 2 +- packages/protect/README.md | 2 + .../schema/__tests__/searchable-json.test.ts | 2 +- packages/stack/__tests__/audit.test.ts | 34 +++---- .../stack/__tests__/basic-protect.test.ts | 2 +- packages/stack/__tests__/bulk-protect.test.ts | 58 ++++++------ .../__tests__/deprecated/search-terms.test.ts | 10 +-- packages/stack/__tests__/json-protect.test.ts | 88 +++++++++---------- packages/stack/__tests__/keysets.test.ts | 4 +- packages/stack/__tests__/lock-context.test.ts | 22 ++--- .../stack/__tests__/nested-models.test.ts | 72 +++++++-------- .../stack/__tests__/number-protect.test.ts | 68 +++++++------- packages/stack/__tests__/protect-ops.test.ts | 68 +++++++------- packages/stack/__tests__/supabase.test.ts | 38 ++++---- packages/stack/src/bin/stash.ts | 4 +- packages/stack/src/ffi/index.ts | 30 +++---- packages/stack/src/index.ts | 4 +- 20 files changed, 265 insertions(+), 254 deletions(-) diff --git a/.changeset/ste-vec-query-support.md b/.changeset/ste-vec-query-support.md index f8a1762c..6ffa5eb7 100644 --- a/.changeset/ste-vec-query-support.md +++ b/.changeset/ste-vec-query-support.md @@ -1,6 +1,7 @@ --- "@cipherstash/protect": minor "@cipherstash/schema": minor +"@cipherstash/stack": minor --- Add encrypted JSONB query support with `searchableJson()` (recommended). diff --git a/SECURITY.md b/SECURITY.md index 6516f076..be964f78 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -38,6 +38,12 @@ The below tables list each package along with the currently supported (receiving | 5.1.x | :white_check_mark: | | < 5.1 | :x: | +### `@cipherstash/stack` + +| Version | Supported | +| ------- | ------------------ | +| 0.x.x | :white_check_mark: | + ### `@cipherstash/nextjs` | Version | Supported | diff --git a/examples/typeorm/src/helpers/protect-entity.ts b/examples/typeorm/src/helpers/protect-entity.ts index eca0c805..4519f241 100644 --- a/examples/typeorm/src/helpers/protect-entity.ts +++ b/examples/typeorm/src/helpers/protect-entity.ts @@ -1,5 +1,5 @@ import { - type EncryptionClientConfig, + type EncryptionClient, encryptedToPgComposite, } from '@cipherstash/stack' import type { EntityTarget } from 'typeorm' @@ -9,7 +9,7 @@ import { AppDataSource } from '../data-source' * Helper functions for working with encrypted entities in TypeORM */ export class ProtectEntityHelper { - constructor(private protectClient: EncryptionClientConfig) {} + constructor(private protectClient: EncryptionClient) {} /** * Bulk encrypt and save entities to the database diff --git a/packages/drizzle/src/pg/operators.ts b/packages/drizzle/src/pg/operators.ts index 0b54ab36..a71724c2 100644 --- a/packages/drizzle/src/pg/operators.ts +++ b/packages/drizzle/src/pg/operators.ts @@ -162,7 +162,7 @@ function getProtectTableFromColumn( } /** - * Helper to get the EncryptedColumn for a Drizzle column from the ProtectTable + * Helper to get the EncryptedColumn for a Drizzle column from the EncryptedTable */ function getEncryptedColumn( drizzleColumn: SQLWrapper, diff --git a/packages/protect/README.md b/packages/protect/README.md index 558d106f..7038be3f 100644 --- a/packages/protect/README.md +++ b/packages/protect/README.md @@ -50,6 +50,8 @@ +> **Deprecated:** This package has been superseded by [`@cipherstash/stack`](https://www.npmjs.com/package/@cipherstash/stack). Please migrate to `@cipherstash/stack` for all new projects. This package will continue to receive critical fixes but no new features. + Protect.js lets you encrypt every value with its own key—without sacrificing performance or usability. Encryption happens in your app; ciphertext is stored in your database. Per‑value unique keys are powered by CipherStash [ZeroKMS](https://cipherstash.com/products/zerokms) bulk key operations, backed by a root key in [AWS KMS](https://docs.aws.amazon.com/kms/latest/developerguide/overview.html). diff --git a/packages/schema/__tests__/searchable-json.test.ts b/packages/schema/__tests__/searchable-json.test.ts index 841c2193..903afd06 100644 --- a/packages/schema/__tests__/searchable-json.test.ts +++ b/packages/schema/__tests__/searchable-json.test.ts @@ -20,7 +20,7 @@ describe('searchableJson()', () => { }) }) -describe('ProtectTable.build() with searchableJson', () => { +describe('encryptedTable.build() with searchableJson', () => { it('transforms prefix to table/column format', () => { const users = encryptedTable('users', { metadata: encryptedColumn('metadata').searchableJson(), diff --git a/packages/stack/__tests__/audit.test.ts b/packages/stack/__tests__/audit.test.ts index d01363f0..818bf0bf 100644 --- a/packages/stack/__tests__/audit.test.ts +++ b/packages/stack/__tests__/audit.test.ts @@ -44,7 +44,7 @@ describe('encryption and decryption with audit', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -85,7 +85,7 @@ describe('encryption and decryption with audit', () => { }) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -110,7 +110,7 @@ describe('encryption and decryption with audit', () => { }) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -139,7 +139,7 @@ describe('encryption and decryption with audit', () => { }) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify null fields are preserved @@ -164,7 +164,7 @@ describe('encryption and decryption with audit', () => { }) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -206,7 +206,7 @@ describe('bulk encryption with audit', () => { }) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields for each model @@ -238,7 +238,7 @@ describe('bulk encryption with audit', () => { }) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModels) @@ -286,7 +286,7 @@ describe('bulk encryption with audit', () => { }) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields for each model @@ -311,7 +311,7 @@ describe('bulk encryption with audit', () => { }) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModels) @@ -329,7 +329,7 @@ describe('bulk encryption with audit', () => { }) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } expect(encryptedModels.data).toEqual([]) @@ -345,7 +345,7 @@ describe('bulk encryption with audit', () => { }) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual([]) @@ -365,7 +365,7 @@ describe('audit with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } // Create a model with decrypted values @@ -387,7 +387,7 @@ describe('audit with lock context', () => { }) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Decrypt the model with both audit and lock context @@ -402,7 +402,7 @@ describe('audit with lock context', () => { }) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -420,7 +420,7 @@ describe('audit with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } // Create models with decrypted values @@ -449,7 +449,7 @@ describe('audit with lock context', () => { }) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Decrypt the models with both audit and lock context @@ -464,7 +464,7 @@ describe('audit with lock context', () => { }) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModels) diff --git a/packages/stack/__tests__/basic-protect.test.ts b/packages/stack/__tests__/basic-protect.test.ts index 6021f18e..6abab4e6 100644 --- a/packages/stack/__tests__/basic-protect.test.ts +++ b/packages/stack/__tests__/basic-protect.test.ts @@ -27,7 +27,7 @@ describe('encryption and decryption', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field diff --git a/packages/stack/__tests__/bulk-protect.test.ts b/packages/stack/__tests__/bulk-protect.test.ts index 97fac935..ad28c429 100644 --- a/packages/stack/__tests__/bulk-protect.test.ts +++ b/packages/stack/__tests__/bulk-protect.test.ts @@ -45,7 +45,7 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -83,7 +83,7 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -112,7 +112,7 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -141,7 +141,7 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -166,7 +166,7 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } expect(encryptedData.data).toHaveLength(0) @@ -188,14 +188,14 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Now decrypt the data const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify structure @@ -225,14 +225,14 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Now decrypt the data const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify structure @@ -262,14 +262,14 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Now decrypt the data const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify structure @@ -299,14 +299,14 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Now decrypt the data const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify structure @@ -325,7 +325,7 @@ describe('bulk encryption and decryption', () => { const decryptedData = await protectClient.bulkDecrypt(encryptedPayloads) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } expect(decryptedData.data).toHaveLength(0) @@ -347,7 +347,7 @@ describe('bulk encryption and decryption', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } const plaintexts = [ @@ -365,7 +365,7 @@ describe('bulk encryption and decryption', () => { .withLockContext(lockContext.data) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -386,7 +386,7 @@ describe('bulk encryption and decryption', () => { .withLockContext(lockContext.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify decrypted data @@ -414,7 +414,7 @@ describe('bulk encryption and decryption', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } const plaintexts = [ @@ -432,7 +432,7 @@ describe('bulk encryption and decryption', () => { .withLockContext(lockContext.data) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify null is preserved @@ -445,7 +445,7 @@ describe('bulk encryption and decryption', () => { .withLockContext(lockContext.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify null is preserved @@ -470,11 +470,11 @@ describe('bulk encryption and decryption', () => { const lockContext2 = await lc2.identify(user2Jwt) if (lockContext1.failure) { - throw new Error(`[protect]: ${lockContext1.failure.message}`) + throw new Error(`[encryption]: ${lockContext1.failure.message}`) } if (lockContext2.failure) { - throw new Error(`[protect]: ${lockContext2.failure.message}`) + throw new Error(`[encryption]: ${lockContext2.failure.message}`) } // Encrypt first value with USER_JWT lock context @@ -486,7 +486,7 @@ describe('bulk encryption and decryption', () => { .withLockContext(lockContext1.data) if (encryptedData1.failure) { - throw new Error(`[protect]: ${encryptedData1.failure.message}`) + throw new Error(`[encryption]: ${encryptedData1.failure.message}`) } // Encrypt second value with USER_2_JWT lock context @@ -498,7 +498,7 @@ describe('bulk encryption and decryption', () => { .withLockContext(lockContext2.data) if (encryptedData2.failure) { - throw new Error(`[protect]: ${encryptedData2.failure.message}`) + throw new Error(`[encryption]: ${encryptedData2.failure.message}`) } // Combine both encrypted payloads @@ -513,7 +513,7 @@ describe('bulk encryption and decryption', () => { .withLockContext(lockContext2.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify both payloads are returned @@ -547,14 +547,14 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Decrypt const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify round-trip integrity @@ -579,14 +579,14 @@ describe('bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Decrypt const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify all data is preserved diff --git a/packages/stack/__tests__/deprecated/search-terms.test.ts b/packages/stack/__tests__/deprecated/search-terms.test.ts index ab25ef00..40a501fb 100644 --- a/packages/stack/__tests__/deprecated/search-terms.test.ts +++ b/packages/stack/__tests__/deprecated/search-terms.test.ts @@ -30,7 +30,7 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { const searchTermsResult = await protectClient.createSearchTerms(searchTerms) if (searchTermsResult.failure) { - throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) } expect(searchTermsResult.data).toEqual( @@ -57,7 +57,7 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { const searchTermsResult = await protectClient.createSearchTerms(searchTerms) if (searchTermsResult.failure) { - throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) } const result = searchTermsResult.data[0] as string @@ -80,7 +80,7 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { const searchTermsResult = await protectClient.createSearchTerms(searchTerms) if (searchTermsResult.failure) { - throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) } const result = searchTermsResult.data[0] as string @@ -105,7 +105,7 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { const searchTermsResult = await protectClient.createSearchTerms(searchTerms) if (searchTermsResult.failure) { - throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) } const result = searchTermsResult.data[0] as string @@ -128,7 +128,7 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { const searchTermsResult = await protectClient.createSearchTerms(searchTerms) if (searchTermsResult.failure) { - throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) } const result = searchTermsResult.data[0] as string diff --git a/packages/stack/__tests__/json-protect.test.ts b/packages/stack/__tests__/json-protect.test.ts index 4aac798e..33e74f8f 100644 --- a/packages/stack/__tests__/json-protect.test.ts +++ b/packages/stack/__tests__/json-protect.test.ts @@ -57,7 +57,7 @@ describe('JSON encryption and decryption', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -109,7 +109,7 @@ describe('JSON encryption and decryption', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -129,7 +129,7 @@ describe('JSON encryption and decryption', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify null is preserved @@ -151,7 +151,7 @@ describe('JSON encryption and decryption', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -178,7 +178,7 @@ describe('JSON encryption and decryption', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -216,7 +216,7 @@ describe('JSON model encryption and decryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -234,7 +234,7 @@ describe('JSON model encryption and decryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -256,7 +256,7 @@ describe('JSON model encryption and decryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -269,7 +269,7 @@ describe('JSON model encryption and decryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -291,7 +291,7 @@ describe('JSON model encryption and decryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -304,7 +304,7 @@ describe('JSON model encryption and decryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -325,7 +325,7 @@ describe('JSON bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -344,7 +344,7 @@ describe('JSON bulk encryption and decryption', () => { const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify decrypted data @@ -379,7 +379,7 @@ describe('JSON bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -398,7 +398,7 @@ describe('JSON bulk encryption and decryption', () => { const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify decrypted data @@ -449,7 +449,7 @@ describe('JSON bulk encryption and decryption', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields for each model @@ -473,7 +473,7 @@ describe('JSON bulk encryption and decryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModels) @@ -493,7 +493,7 @@ describe('JSON encryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } const json = { @@ -513,7 +513,7 @@ describe('JSON encryption with lock context', () => { .withLockContext(lockContext.data) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -524,7 +524,7 @@ describe('JSON encryption with lock context', () => { .withLockContext(lockContext.data) if (plaintext.failure) { - throw new Error(`[protect]: ${plaintext.failure.message}`) + throw new Error(`[encryption]: ${plaintext.failure.message}`) } expect(plaintext.data).toEqual(json) @@ -542,7 +542,7 @@ describe('JSON encryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } const decryptedModel = { @@ -559,7 +559,7 @@ describe('JSON encryption with lock context', () => { .withLockContext(lockContext.data) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -571,7 +571,7 @@ describe('JSON encryption with lock context', () => { .withLockContext(lockContext.data) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -589,7 +589,7 @@ describe('JSON encryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } const jsonPayloads = [ @@ -605,7 +605,7 @@ describe('JSON encryption with lock context', () => { .withLockContext(lockContext.data) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -623,7 +623,7 @@ describe('JSON encryption with lock context', () => { .withLockContext(lockContext.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify decrypted data @@ -672,7 +672,7 @@ describe('JSON nested object encryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -690,7 +690,7 @@ describe('JSON nested object encryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -716,7 +716,7 @@ describe('JSON nested object encryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify null fields are preserved @@ -729,7 +729,7 @@ describe('JSON nested object encryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -755,7 +755,7 @@ describe('JSON nested object encryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify undefined fields are preserved @@ -768,7 +768,7 @@ describe('JSON nested object encryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -801,7 +801,7 @@ describe('JSON edge cases and error handling', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -849,7 +849,7 @@ describe('JSON edge cases and error handling', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -897,7 +897,7 @@ describe('JSON performance tests', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -907,7 +907,7 @@ describe('JSON performance tests', () => { const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify all data is preserved @@ -950,7 +950,7 @@ describe('JSON advanced scenarios', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -977,7 +977,7 @@ describe('JSON advanced scenarios', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -1012,7 +1012,7 @@ describe('JSON advanced scenarios', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -1047,7 +1047,7 @@ describe('JSON advanced scenarios', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -1083,7 +1083,7 @@ describe('JSON advanced scenarios', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -1142,7 +1142,7 @@ describe('JSON error handling and edge cases', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -1171,7 +1171,7 @@ describe('JSON error handling and edge cases', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -1208,7 +1208,7 @@ describe('JSON error handling and edge cases', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field diff --git a/packages/stack/__tests__/keysets.test.ts b/packages/stack/__tests__/keysets.test.ts index 5f093993..7f0437af 100644 --- a/packages/stack/__tests__/keysets.test.ts +++ b/packages/stack/__tests__/keysets.test.ts @@ -24,7 +24,7 @@ describe('encryption and decryption with keyset id', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -57,7 +57,7 @@ describe('encryption and decryption with keyset name', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field diff --git a/packages/stack/__tests__/lock-context.test.ts b/packages/stack/__tests__/lock-context.test.ts index 489c0a29..0dbe72ae 100644 --- a/packages/stack/__tests__/lock-context.test.ts +++ b/packages/stack/__tests__/lock-context.test.ts @@ -39,7 +39,7 @@ describe('encryption and decryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } const email = 'hello@example.com' @@ -52,7 +52,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } const plaintext = await protectClient @@ -60,7 +60,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (plaintext.failure) { - throw new Error(`[protect]: ${plaintext.failure.message}`) + throw new Error(`[encryption]: ${plaintext.failure.message}`) } expect(plaintext.data).toEqual(email) @@ -78,7 +78,7 @@ describe('encryption and decryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } // Create a model with decrypted values @@ -93,7 +93,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Decrypt the model with lock context @@ -102,7 +102,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual({ @@ -123,7 +123,7 @@ describe('encryption and decryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } // Create a model with decrypted values @@ -138,7 +138,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } try { @@ -161,7 +161,7 @@ describe('encryption and decryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } // Create models with decrypted values @@ -182,7 +182,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Decrypt the models with lock context @@ -191,7 +191,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual([ diff --git a/packages/stack/__tests__/nested-models.test.ts b/packages/stack/__tests__/nested-models.test.ts index 98bb25b0..8450f59c 100644 --- a/packages/stack/__tests__/nested-models.test.ts +++ b/packages/stack/__tests__/nested-models.test.ts @@ -57,7 +57,7 @@ describe('encrypt models with nested fields', () => { }) if (encryptResponse.failure) { - throw new Error(`[protect]: ${encryptResponse.failure.message}`) + throw new Error(`[encryption]: ${encryptResponse.failure.message}`) } // Verify encrypted field @@ -66,7 +66,7 @@ describe('encrypt models with nested fields', () => { const decryptResponse = await protectClient.decrypt(encryptResponse.data) if (decryptResponse.failure) { - throw new Error(`[protect]: ${decryptResponse.failure.message}`) + throw new Error(`[encryption]: ${decryptResponse.failure.message}`) } expect(decryptResponse).toEqual({ @@ -95,7 +95,7 @@ describe('encrypt models with nested fields', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -114,7 +114,7 @@ describe('encrypt models with nested fields', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -141,7 +141,7 @@ describe('encrypt models with nested fields', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify null fields are preserved @@ -155,7 +155,7 @@ describe('encrypt models with nested fields', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -180,7 +180,7 @@ describe('encrypt models with nested fields', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify undefined fields are preserved @@ -193,7 +193,7 @@ describe('encrypt models with nested fields', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -223,7 +223,7 @@ describe('encrypt models with nested fields', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -245,7 +245,7 @@ describe('encrypt models with nested fields', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -270,7 +270,7 @@ describe('encrypt models with nested fields', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -285,7 +285,7 @@ describe('encrypt models with nested fields', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -307,7 +307,7 @@ describe('encrypt models with nested fields', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -322,7 +322,7 @@ describe('encrypt models with nested fields', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -361,7 +361,7 @@ describe('encrypt models with nested fields', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields for each model @@ -381,7 +381,7 @@ describe('encrypt models with nested fields', () => { ) if (decryptedResults.failure) { - throw new Error(`[protect]: ${decryptedResults.failure.message}`) + throw new Error(`[encryption]: ${decryptedResults.failure.message}`) } expect(decryptedResults.data).toEqual(decryptedModels) @@ -419,7 +419,7 @@ describe('encrypt models with nested fields', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify null/undefined fields are preserved @@ -439,7 +439,7 @@ describe('encrypt models with nested fields', () => { ) if (decryptedResults.failure) { - throw new Error(`[protect]: ${decryptedResults.failure.message}`) + throw new Error(`[encryption]: ${decryptedResults.failure.message}`) } expect(decryptedResults.data).toEqual(decryptedModels) @@ -474,7 +474,7 @@ describe('encrypt models with nested fields', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields for each model @@ -494,7 +494,7 @@ describe('encrypt models with nested fields', () => { ) if (decryptedResults.failure) { - throw new Error(`[protect]: ${decryptedResults.failure.message}`) + throw new Error(`[encryption]: ${decryptedResults.failure.message}`) } expect(decryptedResults.data).toEqual(decryptedModels) @@ -511,7 +511,7 @@ describe('encrypt models with nested fields', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } expect(encryptedModels.data).toEqual([]) @@ -521,7 +521,7 @@ describe('encrypt models with nested fields', () => { ) if (decryptedResults.failure) { - throw new Error(`[protect]: ${decryptedResults.failure.message}`) + throw new Error(`[encryption]: ${decryptedResults.failure.message}`) } expect(decryptedResults.data).toEqual([]) @@ -552,7 +552,7 @@ describe('nested fields with a plaintext field', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -572,7 +572,7 @@ describe('nested fields with a plaintext field', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -605,7 +605,7 @@ describe('nested fields with a plaintext field', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -630,7 +630,7 @@ describe('nested fields with a plaintext field', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -664,7 +664,7 @@ describe('nested fields with a plaintext field', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -686,7 +686,7 @@ describe('nested fields with a plaintext field', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -714,7 +714,7 @@ describe('nested fields with a plaintext field', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -734,7 +734,7 @@ describe('nested fields with a plaintext field', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -779,7 +779,7 @@ describe('nested fields with a plaintext field', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields @@ -809,7 +809,7 @@ describe('nested fields with a plaintext field', () => { ) if (decryptedResults.failure) { - throw new Error(`[protect]: ${decryptedResults.failure.message}`) + throw new Error(`[encryption]: ${decryptedResults.failure.message}`) } expect(decryptedResults.data).toEqual(decryptedModels) @@ -851,7 +851,7 @@ describe('nested fields with a plaintext field', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields @@ -883,7 +883,7 @@ describe('nested fields with a plaintext field', () => { ) if (decryptedResults.failure) { - throw new Error(`[protect]: ${decryptedResults.failure.message}`) + throw new Error(`[encryption]: ${decryptedResults.failure.message}`) } expect(decryptedResults.data).toEqual(decryptedModels) @@ -927,7 +927,7 @@ describe('nested fields with a plaintext field', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields @@ -953,7 +953,7 @@ describe('nested fields with a plaintext field', () => { ) if (decryptedResults.failure) { - throw new Error(`[protect]: ${decryptedResults.failure.message}`) + throw new Error(`[encryption]: ${decryptedResults.failure.message}`) } expect(decryptedResults.data).toEqual(decryptedModels) diff --git a/packages/stack/__tests__/number-protect.test.ts b/packages/stack/__tests__/number-protect.test.ts index b3ad72ff..f635cda7 100644 --- a/packages/stack/__tests__/number-protect.test.ts +++ b/packages/stack/__tests__/number-protect.test.ts @@ -63,7 +63,7 @@ describe('Number encryption and decryption', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -85,7 +85,7 @@ describe('Number encryption and decryption', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify null is preserved @@ -108,7 +108,7 @@ describe('Number encryption and decryption', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -179,7 +179,7 @@ describe('Model encryption and decryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -198,7 +198,7 @@ describe('Model encryption and decryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -221,7 +221,7 @@ describe('Model encryption and decryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -235,7 +235,7 @@ describe('Model encryption and decryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -258,7 +258,7 @@ describe('Model encryption and decryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -272,7 +272,7 @@ describe('Model encryption and decryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -293,7 +293,7 @@ describe('Bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -330,7 +330,7 @@ describe('Bulk encryption and decryption', () => { const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify decrypted data @@ -356,7 +356,7 @@ describe('Bulk encryption and decryption', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -375,7 +375,7 @@ describe('Bulk encryption and decryption', () => { const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify decrypted data @@ -416,7 +416,7 @@ describe('Bulk encryption and decryption', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields for each model @@ -442,7 +442,7 @@ describe('Bulk encryption and decryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModels) @@ -462,7 +462,7 @@ describe('Encryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } const age = 42 @@ -475,7 +475,7 @@ describe('Encryption with lock context', () => { .withLockContext(lockContext.data) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field @@ -486,7 +486,7 @@ describe('Encryption with lock context', () => { .withLockContext(lockContext.data) if (plaintext.failure) { - throw new Error(`[protect]: ${plaintext.failure.message}`) + throw new Error(`[encryption]: ${plaintext.failure.message}`) } expect(plaintext.data).toEqual(age) @@ -504,7 +504,7 @@ describe('Encryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } const decryptedModel = { @@ -519,7 +519,7 @@ describe('Encryption with lock context', () => { .withLockContext(lockContext.data) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -532,7 +532,7 @@ describe('Encryption with lock context', () => { .withLockContext(lockContext.data) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -550,7 +550,7 @@ describe('Encryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } const intPayloads = [ @@ -566,7 +566,7 @@ describe('Encryption with lock context', () => { .withLockContext(lockContext.data) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -584,7 +584,7 @@ describe('Encryption with lock context', () => { .withLockContext(lockContext.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify decrypted data @@ -615,7 +615,7 @@ describe('Nested object encryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -631,7 +631,7 @@ describe('Nested object encryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -655,7 +655,7 @@ describe('Nested object encryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify null fields are preserved @@ -668,7 +668,7 @@ describe('Nested object encryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -692,7 +692,7 @@ describe('Nested object encryption', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify undefined fields are preserved @@ -705,7 +705,7 @@ describe('Nested object encryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModel) @@ -720,7 +720,7 @@ describe('encryptQuery for numbers', () => { ]) if (result.failure) { - throw new Error(`[protect]: ${result.failure.message}`) + throw new Error(`[encryption]: ${result.failure.message}`) } expect(result.data).toHaveLength(2) @@ -750,7 +750,7 @@ describe('Performance tests', () => { }) if (encryptedData.failure) { - throw new Error(`[protect]: ${encryptedData.failure.message}`) + throw new Error(`[encryption]: ${encryptedData.failure.message}`) } // Verify structure @@ -760,7 +760,7 @@ describe('Performance tests', () => { const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { - throw new Error(`[protect]: ${decryptedData.failure.message}`) + throw new Error(`[encryption]: ${decryptedData.failure.message}`) } // Verify all data is preserved @@ -792,7 +792,7 @@ describe('Advanced scenarios', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify encrypted field diff --git a/packages/stack/__tests__/protect-ops.test.ts b/packages/stack/__tests__/protect-ops.test.ts index 74b9dac8..9b7fea5e 100644 --- a/packages/stack/__tests__/protect-ops.test.ts +++ b/packages/stack/__tests__/protect-ops.test.ts @@ -33,7 +33,7 @@ describe('encryption and decryption edge cases', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } // Verify null is preserved @@ -64,7 +64,7 @@ describe('encryption and decryption edge cases', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify encrypted fields @@ -83,7 +83,7 @@ describe('encryption and decryption edge cases', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual({ @@ -114,7 +114,7 @@ describe('encryption and decryption edge cases', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify null fields are preserved @@ -133,7 +133,7 @@ describe('encryption and decryption edge cases', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual({ @@ -164,7 +164,7 @@ describe('encryption and decryption edge cases', () => { ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Verify undefined fields are preserved @@ -183,7 +183,7 @@ describe('encryption and decryption edge cases', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual({ @@ -226,7 +226,7 @@ describe('bulk encryption', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields for each model @@ -251,7 +251,7 @@ describe('bulk encryption', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual([ @@ -282,7 +282,7 @@ describe('bulk encryption', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } expect(encryptedModels.data).toEqual([]) @@ -293,7 +293,7 @@ describe('bulk encryption', () => { const decryptedResult = await protectClient.bulkDecryptModels([]) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual([]) @@ -336,7 +336,7 @@ describe('bulk encryption edge cases', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields for each model @@ -367,7 +367,7 @@ describe('bulk encryption edge cases', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModels) @@ -408,7 +408,7 @@ describe('bulk encryption edge cases', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields for each model @@ -439,7 +439,7 @@ describe('bulk encryption edge cases', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModels) @@ -475,7 +475,7 @@ describe('bulk encryption edge cases', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Verify encrypted fields for each model @@ -506,7 +506,7 @@ describe('bulk encryption edge cases', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(decryptedModels) @@ -530,7 +530,7 @@ describe('error handling', () => { users, ) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Create an invalid model by removing required fields @@ -587,7 +587,7 @@ describe('type safety', () => { const encryptedModel = await protectClient.encryptModel(model, users) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Decrypt the model @@ -596,7 +596,7 @@ describe('type safety', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(model) @@ -623,7 +623,7 @@ describe('performance', () => { ) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Decrypt the models @@ -632,7 +632,7 @@ describe('performance', () => { ) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual(largeModels) @@ -652,7 +652,7 @@ describe('encryption and decryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } const email = 'hello@example.com' @@ -665,7 +665,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } const plaintext = await protectClient @@ -673,7 +673,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (plaintext.failure) { - throw new Error(`[protect]: ${plaintext.failure.message}`) + throw new Error(`[encryption]: ${plaintext.failure.message}`) } expect(plaintext.data).toEqual(email) @@ -691,7 +691,7 @@ describe('encryption and decryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } // Create a model with decrypted values @@ -706,7 +706,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Decrypt the model with lock context @@ -715,7 +715,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual({ @@ -736,7 +736,7 @@ describe('encryption and decryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } // Create a model with decrypted values @@ -751,7 +751,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } try { @@ -774,7 +774,7 @@ describe('encryption and decryption with lock context', () => { const lockContext = await lc.identify(userJwt) if (lockContext.failure) { - throw new Error(`[protect]: ${lockContext.failure.message}`) + throw new Error(`[encryption]: ${lockContext.failure.message}`) } // Create models with decrypted values @@ -795,7 +795,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } // Decrypt the models with lock context @@ -804,7 +804,7 @@ describe('encryption and decryption with lock context', () => { .withLockContext(lockContext.data) if (decryptedResult.failure) { - throw new Error(`[protect]: ${decryptedResult.failure.message}`) + throw new Error(`[encryption]: ${decryptedResult.failure.message}`) } expect(decryptedResult.data).toEqual([ @@ -831,7 +831,7 @@ describe('special characters', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } const decrypted = await protectClient.decrypt(ciphertext.data) diff --git a/packages/stack/__tests__/supabase.test.ts b/packages/stack/__tests__/supabase.test.ts index 03392efc..77fb7bf1 100644 --- a/packages/stack/__tests__/supabase.test.ts +++ b/packages/stack/__tests__/supabase.test.ts @@ -48,7 +48,7 @@ beforeAll(async () => { .eq('test_run_id', TEST_RUN_ID) if (error) { - console.warn(`[protect]: Failed to clean up test data: ${error.message}`) + console.warn(`[encryption]: Failed to clean up test data: ${error.message}`) } }) @@ -60,7 +60,9 @@ afterAll(async () => { .delete() .in('id', insertedIds) if (error) { - console.error(`[protect]: Failed to clean up test data: ${error.message}`) + console.error( + `[encryption]: Failed to clean up test data: ${error.message}`, + ) } } }) @@ -77,7 +79,7 @@ describe('supabase', () => { }) if (ciphertext.failure) { - throw new Error(`[protect]: ${ciphertext.failure.message}`) + throw new Error(`[encryption]: ${ciphertext.failure.message}`) } const { data: insertedData, error: insertError } = await supabase @@ -89,7 +91,7 @@ describe('supabase', () => { .select('id') if (insertError) { - throw new Error(`[protect]: ${insertError.message}`) + throw new Error(`[encryption]: ${insertError.message}`) } insertedIds.push(insertedData[0].id) @@ -100,7 +102,7 @@ describe('supabase', () => { .eq('id', insertedData[0].id) if (error) { - throw new Error(`[protect]: ${error.message}`) + throw new Error(`[encryption]: ${error.message}`) } const dataToDecrypt = data[0].encrypted as Encrypted @@ -122,7 +124,7 @@ describe('supabase', () => { const encryptedModel = await protectClient.encryptModel(model, table) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } const { data: insertedData, error: insertError } = await supabase @@ -136,7 +138,7 @@ describe('supabase', () => { .select('id') if (insertError) { - throw new Error(`[protect]: ${insertError.message}`) + throw new Error(`[encryption]: ${insertError.message}`) } insertedIds.push(insertedData[0].id) @@ -147,7 +149,7 @@ describe('supabase', () => { .eq('id', insertedData[0].id) if (error) { - throw new Error(`[protect]: ${error.message}`) + throw new Error(`[encryption]: ${error.message}`) } if (!isEncryptedPayload(data[0].encrypted)) { @@ -157,7 +159,7 @@ describe('supabase', () => { const decryptedModel = await protectClient.decryptModel(data[0]) if (decryptedModel.failure) { - throw new Error(`[protect]: ${decryptedModel.failure.message}`) + throw new Error(`[encryption]: ${decryptedModel.failure.message}`) } expect({ @@ -183,7 +185,7 @@ describe('supabase', () => { const encryptedModels = await protectClient.bulkEncryptModels(models, table) if (encryptedModels.failure) { - throw new Error(`[protect]: ${encryptedModels.failure.message}`) + throw new Error(`[encryption]: ${encryptedModels.failure.message}`) } const dataToInsert = bulkModelsToEncryptedPgComposites( @@ -199,7 +201,7 @@ describe('supabase', () => { .select('id') if (insertError) { - throw new Error(`[protect]: ${insertError.message}`) + throw new Error(`[encryption]: ${insertError.message}`) } insertedIds.push(...insertedData.map((d: { id: number }) => d.id)) @@ -213,13 +215,13 @@ describe('supabase', () => { ) if (error) { - throw new Error(`[protect]: ${error.message}`) + throw new Error(`[encryption]: ${error.message}`) } const decryptedModels = await protectClient.bulkDecryptModels(data) if (decryptedModels.failure) { - throw new Error(`[protect]: ${decryptedModels.failure.message}`) + throw new Error(`[encryption]: ${decryptedModels.failure.message}`) } expect( @@ -244,7 +246,7 @@ describe('supabase', () => { const encryptedModel = await protectClient.encryptModel(model, table) if (encryptedModel.failure) { - throw new Error(`[protect]: ${encryptedModel.failure.message}`) + throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } const insertResult = await supabase @@ -258,7 +260,7 @@ describe('supabase', () => { .select('id') if (insertResult.error) { - throw new Error(`[protect]: ${insertResult.error.message}`) + throw new Error(`[encryption]: ${insertResult.error.message}`) } const insertedRecordId = insertResult.data[0].id @@ -276,7 +278,7 @@ describe('supabase', () => { ]) if (encryptedResult.failure) { - throw new Error(`[protect]: ${encryptedResult.failure.message}`) + throw new Error(`[encryption]: ${encryptedResult.failure.message}`) } const [searchTerm] = encryptedResult.data @@ -290,7 +292,7 @@ describe('supabase', () => { .eq('test_run_id', TEST_RUN_ID) if (error) { - throw new Error(`[protect]: ${error.message}`) + throw new Error(`[encryption]: ${error.message}`) } // Verify we found our specific row with encrypted age match @@ -299,7 +301,7 @@ describe('supabase', () => { const decryptedModel = await protectClient.decryptModel(data[0]) if (decryptedModel.failure) { - throw new Error(`[protect]: ${decryptedModel.failure.message}`) + throw new Error(`[encryption]: ${decryptedModel.failure.message}`) } expect(decryptedModel.data.age).toBe(testAge) diff --git a/packages/stack/src/bin/stash.ts b/packages/stack/src/bin/stash.ts index 0293f013..65a23d80 100644 --- a/packages/stack/src/bin/stash.ts +++ b/packages/stack/src/bin/stash.ts @@ -442,9 +442,9 @@ const rootRouteMap = buildRouteMap({ secrets: secretsRouteMap, }, docs: { - brief: 'CipherStash Protect - Encrypted secrets management', + brief: 'CipherStash Stash - Encrypted secrets management', fullDescription: ` -CipherStash Protect CLI +CipherStash Stash CLI Manage encrypted secrets with end-to-end encryption. Secrets are encrypted locally before being sent to the CipherStash API, ensuring your plaintext never leaves diff --git a/packages/stack/src/ffi/index.ts b/packages/stack/src/ffi/index.ts index 2da0b8dd..fdea7bac 100644 --- a/packages/stack/src/ffi/index.ts +++ b/packages/stack/src/ffi/index.ts @@ -283,11 +283,11 @@ export class EncryptionClient { * It includes encrypting a value first, then decrypting it, and handling the result. * * ```typescript - * const encryptedData = await eqlClient.encrypt( + * const encryptedData = await encryptionClient.encrypt( * "person@example.com", * { column: "email", table: "users" } * ) - * const decryptResult = await eqlClient.decrypt(encryptedData) + * const decryptResult = await encryptionClient.decrypt(encryptedData) * if (decryptResult.failure) { * throw new Error(`Decryption failed: ${decryptResult.failure.message}`); * } @@ -297,7 +297,7 @@ export class EncryptionClient { * @example * Provide a lock context when decrypting: * ```typescript - * await eqlClient.decrypt(encryptedData) + * await encryptionClient.decrypt(encryptedData) * .withLockContext(lockContext) * ``` * @@ -343,8 +343,8 @@ export class EncryptionClient { /** * Decrypt a model with encrypted values * Usage: - * await eqlClient.decryptModel(encryptedModel) - * await eqlClient.decryptModel(encryptedModel).withLockContext(lockContext) + * await encryptionClient.decryptModel(encryptedModel) + * await encryptionClient.decryptModel(encryptedModel).withLockContext(lockContext) */ decryptModel>( input: T, @@ -355,8 +355,8 @@ export class EncryptionClient { /** * Bulk encrypt models with decrypted values * Usage: - * await eqlClient.bulkEncryptModels(decryptedModels, table) - * await eqlClient.bulkEncryptModels(decryptedModels, table).withLockContext(lockContext) + * await encryptionClient.bulkEncryptModels(decryptedModels, table) + * await encryptionClient.bulkEncryptModels(decryptedModels, table).withLockContext(lockContext) */ bulkEncryptModels>( input: Array>, @@ -368,8 +368,8 @@ export class EncryptionClient { /** * Bulk decrypt models with encrypted values * Usage: - * await eqlClient.bulkDecryptModels(encryptedModels) - * await eqlClient.bulkDecryptModels(encryptedModels).withLockContext(lockContext) + * await encryptionClient.bulkDecryptModels(encryptedModels) + * await encryptionClient.bulkDecryptModels(encryptedModels).withLockContext(lockContext) */ bulkDecryptModels>( input: Array, @@ -380,8 +380,8 @@ export class EncryptionClient { /** * Bulk encryption - returns a thenable object. * Usage: - * await eqlClient.bulkEncrypt(plaintexts, { column, table }) - * await eqlClient.bulkEncrypt(plaintexts, { column, table }).withLockContext(lockContext) + * await encryptionClient.bulkEncrypt(plaintexts, { column, table }) + * await encryptionClient.bulkEncrypt(plaintexts, { column, table }).withLockContext(lockContext) */ bulkEncrypt( plaintexts: BulkEncryptPayload, @@ -393,8 +393,8 @@ export class EncryptionClient { /** * Bulk decryption - returns a thenable object. * Usage: - * await eqlClient.bulkDecrypt(encryptedPayloads) - * await eqlClient.bulkDecrypt(encryptedPayloads).withLockContext(lockContext) + * await encryptionClient.bulkDecrypt(encryptedPayloads) + * await encryptionClient.bulkDecrypt(encryptedPayloads).withLockContext(lockContext) */ bulkDecrypt(encryptedPayloads: BulkDecryptPayload): BulkDecryptOperation { return new BulkDecryptOperation(this.client, encryptedPayloads) @@ -419,8 +419,8 @@ export class EncryptionClient { * ``` * * Usage: - * await eqlClient.createSearchTerms(searchTerms) - * await eqlClient.createSearchTerms(searchTerms).withLockContext(lockContext) + * await encryptionClient.createSearchTerms(searchTerms) + * await encryptionClient.createSearchTerms(searchTerms).withLockContext(lockContext) */ createSearchTerms(terms: SearchTerm[]): SearchTermsOperation { return new SearchTermsOperation(this.client, terms) diff --git a/packages/stack/src/index.ts b/packages/stack/src/index.ts index 537cb46c..9ad871ef 100644 --- a/packages/stack/src/index.ts +++ b/packages/stack/src/index.ts @@ -29,10 +29,10 @@ export interface EncryptionError { /** @deprecated Use EncryptionError */ export type ProtectError = EncryptionError -type AtLeastOneCsTable = [T, ...T[]] +type AtLeastOneEncryptedTable = [T, ...T[]] export type EncryptionClientConfig = { - schemas: AtLeastOneCsTable> + schemas: AtLeastOneEncryptedTable> workspaceCrn?: string accessKey?: string clientId?: string From f3d781f54e2d5ab48972c005ffe25b9a2c5ec3f2 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Wed, 11 Feb 2026 18:06:51 -0700 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20rebrand=20Secrets=20interface=20(Sta?= =?UTF-8?q?sh=20=E2=86=92=20SecretsClient/SecretsConfig)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename class Stash → SecretsClient, interface StashConfig → SecretsConfig - Add deprecated aliases: Stash, StashConfig - Update CLI (stash.ts) to use new names and import from secrets/ - Fix tsup.config.ts entry path: src/stash/ → src/secrets/ - Remove stale src/stash/ directory (was duplicate of src/secrets/) - Update all JSDoc examples to use Secrets() factory pattern Co-Authored-By: Claude Opus 4.6 --- packages/stack/src/bin/stash.ts | 26 +++++----- .../stack/src/{stash => secrets}/index.ts | 47 ++++++++++--------- packages/stack/tsup.config.ts | 2 +- 3 files changed, 40 insertions(+), 35 deletions(-) rename packages/stack/src/{stash => secrets}/index.ts (89%) diff --git a/packages/stack/src/bin/stash.ts b/packages/stack/src/bin/stash.ts index 65a23d80..c855aba7 100644 --- a/packages/stack/src/bin/stash.ts +++ b/packages/stack/src/bin/stash.ts @@ -7,7 +7,7 @@ import { buildRouteMap, run, } from '@stricli/core' -import { Stash } from '../stash/index.js' +import { SecretsClient, type SecretsConfig } from '../secrets/index.js' // ANSI color codes for beautiful terminal output const colors = { @@ -40,7 +40,7 @@ const style = { /** * Get configuration from environment variables */ -function getConfig(environment: string): Stash['config'] { +function getConfig(environment: string): SecretsConfig { const workspaceCRN = process.env.CS_WORKSPACE_CRN const clientId = process.env.CS_CLIENT_ID const clientKey = process.env.CS_CLIENT_KEY @@ -84,11 +84,11 @@ function getConfig(environment: string): Stash['config'] { } /** - * Create a Stash instance with proper error handling + * Create a SecretsClient instance with proper error handling */ -function createStash(environment: string): Stash { +function createSecretsClient(environment: string): SecretsClient { const config = getConfig(environment) - return new Stash(config) + return new SecretsClient(config) } /** @@ -115,13 +115,13 @@ function askConfirmation(prompt: string): Promise { const setCommand = buildCommand({ func: async (flags: { name: string; value: string; environment: string }) => { const { name, value, environment } = flags - const stash = createStash(environment) + const secrets = createSecretsClient(environment) console.log( `${style.info(`Encrypting and storing secret "${name}" in environment "${environment}"...`)}`, ) - const result = await stash.set(name, value) + const result = await secrets.set(name, value) if (result.failure) { console.error( style.error(`Failed to set secret: ${result.failure.message}`), @@ -179,13 +179,13 @@ Examples: const getCommand = buildCommand({ func: async (flags: { name: string; environment: string }) => { const { name, environment } = flags - const stash = createStash(environment) + const secrets = createSecretsClient(environment) console.log( `${style.info(`Retrieving secret "${name}" from environment "${environment}"...`)}`, ) - const result = await stash.get(name) + const result = await secrets.get(name) if (result.failure) { console.error( style.error(`Failed to get secret: ${result.failure.message}`), @@ -234,13 +234,13 @@ Examples: const listCommand = buildCommand({ func: async (flags: { environment: string }) => { const { environment } = flags - const stash = createStash(environment) + const secrets = createSecretsClient(environment) console.log( `${style.info(`Listing secrets in environment "${environment}"...`)}`, ) - const result = await stash.list() + const result = await secrets.list() if (result.failure) { console.error( style.error(`Failed to list secrets: ${result.failure.message}`), @@ -322,7 +322,7 @@ const deleteCommand = buildCommand({ yes?: boolean }) => { const { name, environment, yes } = flags - const stash = createStash(environment) + const secrets = createSecretsClient(environment) // Ask for confirmation unless --yes flag is set if (!yes) { @@ -340,7 +340,7 @@ const deleteCommand = buildCommand({ `${style.info(`Deleting secret "${name}" from environment "${environment}"...`)}`, ) - const result = await stash.delete(name) + const result = await secrets.delete(name) if (result.failure) { console.error( style.error(`Failed to delete secret: ${result.failure.message}`), diff --git a/packages/stack/src/stash/index.ts b/packages/stack/src/secrets/index.ts similarity index 89% rename from packages/stack/src/stash/index.ts rename to packages/stack/src/secrets/index.ts index 059e5e2e..614849c4 100644 --- a/packages/stack/src/stash/index.ts +++ b/packages/stack/src/secrets/index.ts @@ -11,9 +11,9 @@ export type SecretName = string export type SecretValue = string /** - * Configuration options for initializing the Stash client + * Configuration options for initializing the Secrets client */ -export interface StashConfig { +export interface SecretsConfig { workspaceCRN: string clientId: string clientKey: string @@ -63,13 +63,13 @@ export interface DecryptedSecretResponse { } /** - * The Stash client provides a high-level API for managing encrypted secrets + * The SecretsClient provides a high-level API for managing encrypted secrets * stored in CipherStash. Secrets are encrypted locally before being sent to * the API, ensuring end-to-end encryption. */ -export class Stash { +export class SecretsClient { private encryptionClient: EncryptionClient | null = null - private config: StashConfig + private config: SecretsConfig private readonly apiBaseUrl = process.env.STASH_API_URL || 'https://getstash.sh/api/secrets' private readonly secretsSchema = encryptedTable('secrets', { @@ -91,12 +91,12 @@ export class Stash { return match[1] } - constructor(config: StashConfig) { + constructor(config: SecretsConfig) { this.config = config } /** - * Initialize the Stash client and underlying Encryption client + * Initialize the Secrets client and underlying Encryption client */ private async ensureInitialized(): Promise { if (this.encryptionClient) { @@ -186,8 +186,8 @@ export class Stash { * * @example * ```typescript - * const stash = new Stash({ ... }) - * const result = await stash.set('DATABASE_URL', 'postgres://user:pass@localhost:5432/mydb') + * const secrets = await Secrets({ ... }) + * const result = await secrets.set('DATABASE_URL', 'postgres://user:pass@localhost:5432/mydb') * if (result.failure) { * console.error('Failed to set secret:', result.failure.message) * } @@ -244,8 +244,8 @@ export class Stash { * * @example * ```typescript - * const stash = new Stash({ ... }) - * const result = await stash.get('DATABASE_URL') + * const secrets = await Secrets({ ... }) + * const result = await secrets.get('DATABASE_URL') * if (result.failure) { * console.error('Failed to get secret:', result.failure.message) * } else { @@ -317,8 +317,8 @@ export class Stash { * * @example * ```typescript - * const stash = new Stash({ ... }) - * const result = await stash.getMany(['DATABASE_URL', 'API_KEY']) + * const secrets = await Secrets({ ... }) + * const result = await secrets.getMany(['DATABASE_URL', 'API_KEY']) * if (result.failure) { * console.error('Failed to get secrets:', result.failure.message) * } else { @@ -402,8 +402,8 @@ export class Stash { * * @example * ```typescript - * const stash = new Stash({ ... }) - * const result = await stash.list() + * const secrets = await Secrets({ ... }) + * const result = await secrets.list() * if (result.failure) { * console.error('Failed to list secrets:', result.failure.message) * } else { @@ -441,8 +441,8 @@ export class Stash { * * @example * ```typescript - * const stash = new Stash({ ... }) - * const result = await stash.delete('DATABASE_URL') + * const secrets = await Secrets({ ... }) + * const result = await secrets.delete('DATABASE_URL') * if (result.failure) { * console.error('Failed to delete secret:', result.failure.message) * } @@ -465,10 +465,15 @@ export class Stash { /** * Initialize a Secrets client for managing encrypted secrets. * - * @param config - The configuration options for the Stash client - * @returns A Promise that resolves to an initialized Stash instance + * @param config - The configuration options for the Secrets client + * @returns A Promise that resolves to an initialized SecretsClient instance */ -export async function Secrets(config: StashConfig): Promise { - const client = new Stash(config) +export async function Secrets(config: SecretsConfig): Promise { + const client = new SecretsClient(config) return client } + +/** @deprecated Use SecretsConfig */ +export type StashConfig = SecretsConfig +/** @deprecated Use SecretsClient */ +export { SecretsClient as Stash } diff --git a/packages/stack/tsup.config.ts b/packages/stack/tsup.config.ts index a8817fb3..f9e4d979 100644 --- a/packages/stack/tsup.config.ts +++ b/packages/stack/tsup.config.ts @@ -6,7 +6,7 @@ export default defineConfig([ index: 'src/index.ts', client: 'src/client.ts', 'identify/index': 'src/identify/index.ts', - 'secrets/index': 'src/stash/index.ts', + 'secrets/index': 'src/secrets/index.ts', }, format: ['cjs', 'esm'], sourcemap: true, From 4c7581e2a8284a1debbac3ce0879450c341946f9 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Wed, 11 Feb 2026 18:21:56 -0700 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20comprehensive=20protectClient=20?= =?UTF-8?q?=E2=86=92=20encryptionClient=20rename=20(round=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bulk rename of protectClient variable to encryptionClient across the entire repo, plus related branding fixes. Changes by area: - utils/logger: [protect] → [encryption] log prefix - dynamodb: protectClient → encryptionClient, ProtectDynamoDB* types → EncryptedDynamoDB* (with deprecated aliases), protectDynamoDB() → encryptedDynamoDB(), PROTECT_DYNAMODB_ERROR → DYNAMODB_ENCRYPTION_ERROR - drizzle: protectClient/protectTable/protectColumn internal vars renamed, test protectOps → encryptionOps - stack tests: protectClient → encryptionClient across all 16 test files - stack source: EQL client → Encryption client, Stash Encryption → Encryption - all 7 examples: protectClient → encryptionClient, [protect]: → [encryption]:, initializeProtectClient → initializeEncryptionClient, ProtectEntityHelper → EncryptionEntityHelper - all docs/READMEs: protectClient → encryptionClient (~180 replacements) Co-Authored-By: Claude Opus 4.6 --- .../aws-kms-vs-cipherstash-comparison.md | 24 +- docs/concepts/searchable-encryption.md | 2 +- docs/getting-started.md | 12 +- docs/how-to/lock-contexts-with-clerk.md | 2 +- docs/prompts/init-protect.md | 10 +- docs/reference/configuration.md | 2 +- docs/reference/drizzle/DRIFT-TESTING.md | 8 +- docs/reference/drizzle/drizzle-protect.md | 84 ++--- docs/reference/model-operations.md | 30 +- docs/reference/schema.md | 2 +- .../searchable-encryption-postgres.md | 40 +- docs/reference/supabase-sdk.md | 26 +- examples/basic/index.ts | 14 +- examples/basic/protect.ts | 2 +- .../drizzle/src/controllers/transactions.ts | 16 +- examples/drizzle/src/protect/config.ts | 4 +- examples/dynamo/src/bulk-operations.ts | 4 +- examples/dynamo/src/common/protect.ts | 2 +- examples/dynamo/src/encrypted-key-in-gsi.ts | 6 +- .../dynamo/src/encrypted-partition-key.ts | 6 +- examples/dynamo/src/encrypted-sort-key.ts | 6 +- examples/dynamo/src/export-to-pg.ts | 6 +- examples/dynamo/src/simple.ts | 4 +- examples/hono-supabase/src/index.ts | 6 +- .../next-drizzle-mysql/src/app/actions.ts | 4 +- examples/next-drizzle-mysql/src/app/page.tsx | 4 +- .../next-drizzle-mysql/src/protect/index.ts | 2 +- examples/nextjs-clerk/src/app/page.tsx | 4 +- .../nextjs-clerk/src/core/protect/index.ts | 4 +- examples/nextjs-clerk/src/lib/actions.ts | 4 +- examples/typeorm/README.md | 12 +- .../typeorm/src/helpers/protect-entity.ts | 20 +- examples/typeorm/src/index.ts | 16 +- examples/typeorm/src/protect.ts | 10 +- packages/drizzle/README.md | 18 +- packages/drizzle/__tests__/docs.test.ts | 18 +- packages/drizzle/__tests__/drizzle.test.ts | 67 ++-- packages/drizzle/src/pg/operators.ts | 341 +++++++++--------- packages/drizzle/src/pg/schema-extraction.ts | 4 +- packages/dynamodb/README.md | 4 +- packages/dynamodb/__tests__/audit.test.ts | 15 +- packages/dynamodb/__tests__/dynamodb.test.ts | 15 +- .../dynamodb/__tests__/error-codes.test.ts | 30 +- packages/dynamodb/src/helpers.ts | 26 +- packages/dynamodb/src/index.ts | 28 +- .../src/operations/bulk-decrypt-models.ts | 12 +- .../src/operations/bulk-encrypt-models.ts | 12 +- .../dynamodb/src/operations/decrypt-model.ts | 14 +- .../dynamodb/src/operations/encrypt-model.ts | 12 +- .../dynamodb/src/operations/search-terms.ts | 18 +- packages/dynamodb/src/types.ts | 25 +- packages/stack/README.md | 90 ++--- packages/stack/__tests__/audit.test.ts | 36 +- .../stack/__tests__/backward-compat.test.ts | 10 +- .../stack/__tests__/basic-protect.test.ts | 8 +- packages/stack/__tests__/bulk-protect.test.ts | 67 ++-- .../__tests__/deprecated/search-terms.test.ts | 25 +- .../encrypt-query-searchable-json.test.ts | 132 +++---- .../__tests__/encrypt-query-stevec.test.ts | 77 ++-- .../stack/__tests__/encrypt-query.test.ts | 122 ++++--- packages/stack/__tests__/error-codes.test.ts | 41 ++- packages/stack/__tests__/json-protect.test.ts | 126 +++---- packages/stack/__tests__/keysets.test.ts | 12 +- packages/stack/__tests__/lock-context.test.ts | 20 +- .../stack/__tests__/nested-models.test.ts | 108 +++--- .../stack/__tests__/number-protect.test.ts | 88 ++--- packages/stack/__tests__/protect-ops.test.ts | 77 ++-- packages/stack/__tests__/supabase.test.ts | 29 +- packages/stack/src/ffi/helpers/validation.ts | 2 +- packages/stack/src/ffi/index.ts | 6 +- packages/stack/src/helpers/index.ts | 8 +- packages/utils/logger/index.ts | 6 +- 72 files changed, 1111 insertions(+), 1036 deletions(-) diff --git a/docs/concepts/aws-kms-vs-cipherstash-comparison.md b/docs/concepts/aws-kms-vs-cipherstash-comparison.md index e1a26a95..e64bdb68 100644 --- a/docs/concepts/aws-kms-vs-cipherstash-comparison.md +++ b/docs/concepts/aws-kms-vs-cipherstash-comparison.md @@ -66,12 +66,12 @@ const users = encryptedTable('users', { }); // One-time setup: Initialize client -const protectClient = await Encryption({ +const encryptionClient = await Encryption({ schemas: [users], }); // Encrypt: One line, no manual encoding, no key management -const encryptResult = await protectClient.encrypt( +const encryptResult = await encryptionClient.encrypt( 'secret@squirrel.example', { column: users.email, table: users } ); @@ -129,7 +129,7 @@ async function decryptWithKMS(base64Ciphertext: string): Promise { ```typescript // Decrypt: One call, returns typed value -const decryptResult = await protectClient.decrypt(ciphertext); +const decryptResult = await encryptionClient.decrypt(ciphertext); if (decryptResult.failure) { throw new Error(decryptResult.failure.message); @@ -159,13 +159,13 @@ const users = encryptedTable('users', { }); // Encrypt as usual -const encryptResult = await protectClient.encrypt( +const encryptResult = await encryptionClient.encrypt( 'secret@squirrel.example', { column: users.email, table: users } ); // Create search terms and query directly in PostgreSQL -const searchTerms = await protectClient.createSearchTerms({ +const searchTerms = await encryptionClient.createSearchTerms({ terms: ['secret'], column: users.email, table: users, @@ -206,13 +206,13 @@ const lc = new LockContext(); const lockContext = await lc.identify(userJwt); // Encrypt with lock context (chainable API) -const encryptResult = await protectClient.encrypt( +const encryptResult = await encryptionClient.encrypt( 'secret@squirrel.example', { column: users.email, table: users } ).withLockContext(lockContext); // Decrypt requires the same lock context (enforced by Stash Encryption) -const decryptResult = await protectClient.decrypt(ciphertext) +const decryptResult = await encryptionClient.decrypt(ciphertext) .withLockContext(lockContext); ``` @@ -244,7 +244,7 @@ const bulkPlaintexts = [ { id: '3', plaintext: 'Charlie' }, ]; -const bulkResult = await protectClient.bulkEncrypt(bulkPlaintexts, { +const bulkResult = await encryptionClient.bulkEncrypt(bulkPlaintexts, { column: users.name, table: users, }); @@ -276,7 +276,7 @@ try { **Stash Encryption:** Type-safe Result pattern: ```typescript -const result = await protectClient.encrypt(plaintext, options); +const result = await encryptionClient.encrypt(plaintext, options); if (result.failure) { // Type-safe error handling with autocomplete @@ -375,12 +375,12 @@ const users = encryptedTable('users', { }); // One-time initialization -const protectClient = await Encryption({ +const encryptionClient = await Encryption({ schemas: [users], }); // Encrypt -const encryptResult = await protectClient.encrypt( +const encryptResult = await encryptionClient.encrypt( 'secret@squirrel.example', { column: users.email, table: users } ); @@ -392,7 +392,7 @@ if (encryptResult.failure) { const ciphertext = encryptResult.data; // Decrypt -const decryptResult = await protectClient.decrypt(ciphertext); +const decryptResult = await encryptionClient.decrypt(ciphertext); if (decryptResult.failure) { throw new Error(decryptResult.failure.message); diff --git a/docs/concepts/searchable-encryption.md b/docs/concepts/searchable-encryption.md index 9ae0edd8..019ea0e0 100644 --- a/docs/concepts/searchable-encryption.md +++ b/docs/concepts/searchable-encryption.md @@ -70,7 +70,7 @@ CipherStash uses [EQL](https://github.com/cipherstash/encrypt-query-language) to // 1) Encrypt the search term const searchTerm = 'alice.johnson@example.com' -const encryptedParam = await protectClient.createSearchTerms([{ +const encryptedParam = await encryptionClient.createSearchTerms([{ value: searchTerm, table: protectedUsers, // Reference to the Protect table schema column: protectedUsers.email, // Your Protect column definition diff --git a/docs/getting-started.md b/docs/getting-started.md index bc1a8b9a..c0973362 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -142,21 +142,21 @@ const config: EncryptionClientConfig = { schemas: [users, orders], } -export const protectClient = await Encryption(config); +export const encryptionClient = await Encryption(config); ``` ## Step 5: Encrypt data -Stash Encryption provides the `encrypt` function on `protectClient` to encrypt data. +Stash Encryption provides the `encrypt` function on `encryptionClient` to encrypt data. `encrypt` takes a plaintext string, and an object with the table and column as parameters. Start encrypting data by adding this to `src/index.ts`: ```typescript import { users } from "./protect/schema"; -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; -const encryptResult = await protectClient.encrypt("secret@squirrel.example", { +const encryptResult = await encryptionClient.encrypt("secret@squirrel.example", { column: users.email, table: users, }); @@ -209,9 +209,9 @@ Use the `decrypt` function to decrypt data. `decrypt` takes an encrypted data object as a parameter. ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; -const decryptResult = await protectClient.decrypt(ciphertext); +const decryptResult = await encryptionClient.decrypt(ciphertext); if (decryptResult.failure) { // Handle the failure diff --git a/docs/how-to/lock-contexts-with-clerk.md b/docs/how-to/lock-contexts-with-clerk.md index ad5f088f..4484bf8d 100644 --- a/docs/how-to/lock-contexts-with-clerk.md +++ b/docs/how-to/lock-contexts-with-clerk.md @@ -97,7 +97,7 @@ If you want to override the default context, you can pass a custom context to th ```typescript import { LockContext } from '@cipherstash/stack/identity' -// protectClient from the previous steps +// encryptionClient from the previous steps const lc = new LockContext({ context: { identityClaim: ['sub'], // this is the default context diff --git a/docs/prompts/init-protect.md b/docs/prompts/init-protect.md index 3e46ab50..3590dbb0 100644 --- a/docs/prompts/init-protect.md +++ b/docs/prompts/init-protect.md @@ -38,27 +38,27 @@ export const protectedExample = encryptedTable('example_table', { import { Encryption } from '@cipherstash/stack' import { * as protectSchemas } from './schemas' -export const protectClient = Encryption({ +export const encryptionClient = Encryption({ schemas: [...protectSchemas] }) ``` `protect/example.(ts/js)` ```js -import { protectClient } from './index' +import { encryptionClient } from './index' import { * as protectSchemas } from './schemas' const sensitiveData = "Let's encrypt some data." /** - * There is no need to wrap any protectClient method in a try/catch as it will always return a Result pattern. + * There is no need to wrap any encryptionClient method in a try/catch as it will always return a Result pattern. * --- * The Result will either contain a `failure` OR a `data` key. You should ALWAYS check for the `failure` key first. * If the `failure` key is present, you should handle the error accordingly. * If the `data` key is present, the operation was successful. */ // -const encryptResult = protectClient.encrypt(sensitiveData, { +const encryptResult = encryptionClient.encrypt(sensitiveData, { table: protectSchemas.protectedExample column: protectSchemas.protectedExample.sensitiveData }) @@ -85,7 +85,7 @@ const encryptedData = encryptResult.data **/ console.log('encryptedData:', encryptedData) -const decryptResult = protectClient.decrypt(encryptedData) +const decryptResult = encryptionClient.decrypt(encryptedData) if (decryptResult.failure) { // Again, you as the developer can determine exactly how you want to handle the failure scenario. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 87122f5d..9b959f9e 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -114,7 +114,7 @@ const config: EncryptionClientConfig = { clientKey: "your-client-key", } -const protectClient = await Encryption(config); +const encryptionClient = await Encryption(config); ``` ## Deploying to production diff --git a/docs/reference/drizzle/DRIFT-TESTING.md b/docs/reference/drizzle/DRIFT-TESTING.md index d1375d4f..96f0d4f5 100644 --- a/docs/reference/drizzle/DRIFT-TESTING.md +++ b/docs/reference/drizzle/DRIFT-TESTING.md @@ -64,7 +64,7 @@ const context: ExecutionContext = { db, // Drizzle database instance transactions, // Table schema protect, // Protect operators (eq, gte, like, etc.) - protectClient, // Raw protect client for manual encryption + encryptionClient, // Raw protect client for manual encryption protectTransactions, // Protect schema for encryption eq, gte, lte, ilike, // Drizzle operators and, or, desc, asc, // Drizzle combinators @@ -308,8 +308,8 @@ When writing `ts:run` blocks, these variables are available: ### Protect Client (Manual Encryption) | Variable | Description | |----------|-------------| -| `protectClient.encrypt(value, opts)` | Encrypt a single value | -| `protectClient.bulkDecryptModels(results)` | Decrypt query results | +| `encryptionClient.encrypt(value, opts)` | Encrypt a single value | +| `encryptionClient.bulkDecryptModels(results)` | Decrypt query results | ### Drizzle Operators | Variable | Description | @@ -385,7 +385,7 @@ Verify you're using the correct column names from the `transactions` schema: For manual encryption pattern (`drizzle-protect.md`), ensure you call `bulkDecryptModels()`: ```typescript const results = await db.select().from(transactions) -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data // Not results! ``` diff --git a/docs/reference/drizzle/drizzle-protect.md b/docs/reference/drizzle/drizzle-protect.md index 0f740dfb..b010b357 100644 --- a/docs/reference/drizzle/drizzle-protect.md +++ b/docs/reference/drizzle/drizzle-protect.md @@ -6,9 +6,9 @@ This page demonstrates how to perform queries on encrypted data using **Drizzle **Pattern:** Manually encrypt query values before passing them to standard Drizzle operators. **How it works:** -- Explicitly call `protectClient.encrypt()` for each query value +- Explicitly call `encryptionClient.encrypt()` for each query value - Use standard Drizzle operators (`eq()`, `gte()`, `lte()`) with pre-encrypted values -- Manually decrypt results using `protectClient.bulkDecryptModels()` +- Manually decrypt results using `encryptionClient.bulkDecryptModels()` This verbose pattern demonstrates the low-level encryption workflow. For cleaner syntax, see the [encryption operators pattern](/reference/drizzle/drizzle). @@ -23,9 +23,9 @@ This example uses a `transactions` table with the following encrypted fields: **Key differences from encryption operators pattern:** -1. **Manual encryption** of query parameters using `protectClient.encrypt()` +1. **Manual encryption** of query parameters using `encryptionClient.encrypt()` 2. **Standard Drizzle operators** (`eq()`, `gte()`, `lte()`) with pre-encrypted values -3. **Manual decryption** of results using `protectClient.bulkDecryptModels()` +3. **Manual decryption** of results using `encryptionClient.bulkDecryptModels()` This gives you explicit visibility into the encryption/decryption workflow at the cost of more verbose code. @@ -55,7 +55,7 @@ The simplest query - retrieve all transactions from the database. Note that resu ```ts:run const results = await db.select().from(transactions) -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -69,7 +69,7 @@ const results = await db.select({ amount: transactions.amount, createdAt: transactions.createdAt }).from(transactions) -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -79,7 +79,7 @@ Retrieve only the first 5 transactions with manual decryption. ```ts:run const results = await db.select().from(transactions).limit(5) -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -92,7 +92,7 @@ const results = await db.select() .from(transactions) .limit(5) .offset(5) -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -106,7 +106,7 @@ Find transactions with a specific amount. First encrypt the search value, then u ```ts:run // Encrypt the search value -const encryptedAmount = await protectClient.encrypt(800.00, { +const encryptedAmount = await encryptionClient.encrypt(800.00, { table: protectTransactions, column: protectTransactions.amount }) @@ -117,7 +117,7 @@ const results = await db.select() .where(eq(transactions.amount, encryptedAmount.data)) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -127,7 +127,7 @@ Find transactions with a specific description using manual encryption. ```ts:run // Encrypt the search value -const encryptedDesc = await protectClient.encrypt('Salary deposit', { +const encryptedDesc = await encryptionClient.encrypt('Salary deposit', { table: protectTransactions, column: protectTransactions.description }) @@ -138,7 +138,7 @@ const results = await db.select() .where(eq(transactions.description, encryptedDesc.data)) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -148,11 +148,11 @@ Find transactions matching multiple encrypted fields using manual encryption. ```ts:run // Encrypt both search values -const encryptedAccount = await protectClient.encrypt('1234567890', { +const encryptedAccount = await encryptionClient.encrypt('1234567890', { table: protectTransactions, column: protectTransactions.account_number }) -const encryptedAmount = await protectClient.encrypt(800.00, { +const encryptedAmount = await encryptionClient.encrypt(800.00, { table: protectTransactions, column: protectTransactions.amount }) @@ -168,7 +168,7 @@ const results = await db.select() ) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -182,7 +182,7 @@ Find transactions with amounts less than or equal to $150 using manual encryptio ```ts:run // Encrypt the comparison value -const encryptedAmount = await protectClient.encrypt(150.00, { +const encryptedAmount = await encryptionClient.encrypt(150.00, { table: protectTransactions, column: protectTransactions.amount }) @@ -193,7 +193,7 @@ const results = await db.select() .where(lte(transactions.amount, encryptedAmount.data)) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -203,7 +203,7 @@ Find transactions with amounts greater than or equal to $1250. ```ts:run // Encrypt the comparison value -const encryptedAmount = await protectClient.encrypt(1250.00, { +const encryptedAmount = await encryptionClient.encrypt(1250.00, { table: protectTransactions, column: protectTransactions.amount }) @@ -214,7 +214,7 @@ const results = await db.select() .where(gte(transactions.amount, encryptedAmount.data)) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -230,7 +230,7 @@ Search for transactions with "gym" in the description. With the manual encryptio ```ts:run // Encrypt the search pattern -const encryptedPattern = await protectClient.encrypt('%gym%', { +const encryptedPattern = await encryptionClient.encrypt('%gym%', { table: protectTransactions, column: protectTransactions.description }) @@ -241,7 +241,7 @@ const results = await db.select() .where(sql`${transactions.description} ilike ${JSON.stringify(encryptedPattern.data)}::jsonb::eql_v2_encrypted`) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -251,13 +251,13 @@ Combine text search with manual encryption for other fields. All search values m ```ts:run // Encrypt the amount comparison value -const encryptedAmount = await protectClient.encrypt(150.00, { +const encryptedAmount = await encryptionClient.encrypt(150.00, { table: protectTransactions, column: protectTransactions.amount }) // Encrypt the search pattern for text search -const encryptedPattern = await protectClient.encrypt('%payment%', { +const encryptedPattern = await encryptionClient.encrypt('%payment%', { table: protectTransactions, column: protectTransactions.description }) @@ -272,7 +272,7 @@ const results = await db.select() ) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -286,11 +286,11 @@ Find transactions with amounts between $150 and $1250. ```ts:run // Encrypt both range boundaries -const encryptedMin = await protectClient.encrypt(150.00, { +const encryptedMin = await encryptionClient.encrypt(150.00, { table: protectTransactions, column: protectTransactions.amount }) -const encryptedMax = await protectClient.encrypt(1250.00, { +const encryptedMax = await encryptionClient.encrypt(1250.00, { table: protectTransactions, column: protectTransactions.amount }) @@ -306,7 +306,7 @@ const results = await db.select() ) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -320,11 +320,11 @@ const now = new Date() const twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000) // Encrypt both date boundaries -const encryptedStart = await protectClient.encrypt(twoWeeksAgo.getTime(), { +const encryptedStart = await encryptionClient.encrypt(twoWeeksAgo.getTime(), { table: protectTransactions, column: protectTransactions.created_at }) -const encryptedEnd = await protectClient.encrypt(now.getTime(), { +const encryptedEnd = await encryptionClient.encrypt(now.getTime(), { table: protectTransactions, column: protectTransactions.created_at }) @@ -340,7 +340,7 @@ const results = await db.select() ) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -361,7 +361,7 @@ const results = await db.select() .limit(10) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -376,7 +376,7 @@ const results = await db.select() .limit(10) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -391,7 +391,7 @@ const results = await db.select() .limit(10) // Manually decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -408,7 +408,7 @@ const results = await db.select({ count: sql`count(*)` }) .from(transactions) // Manually decrypt result -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -418,7 +418,7 @@ Count transactions with amounts greater than or equal to $1250. ```ts:run // Encrypt the search value -const encryptedAmount = await protectClient.encrypt(1250.00, { +const encryptedAmount = await encryptionClient.encrypt(1250.00, { table: protectTransactions, column: protectTransactions.amount }) @@ -428,7 +428,7 @@ const results = await db.select({ count: sql`count(*)` }) .where(gte(transactions.amount, encryptedAmount.data)) // Manually decrypt result -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -443,7 +443,7 @@ const results = await db.select() .limit(1) // Manually decrypt result -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) return decrypted.data ``` @@ -455,10 +455,10 @@ When using the manual encryption pattern instead of encryption operators, you ha ### Encryption flow -1. **Encrypt search values** using `protectClient.encrypt(plaintext, { table, column })` +1. **Encrypt search values** using `encryptionClient.encrypt(plaintext, { table, column })` 2. **Pass encrypted data** to regular Drizzle operators (`eq()`, `gte()`, `ilike()`, etc.) 3. **Execute query** - PostgreSQL searches encrypted data -4. **Decrypt results** using `protectClient.bulkDecryptModels(results)` +4. **Decrypt results** using `encryptionClient.bulkDecryptModels(results)` 5. **Return plaintext** - Results are now readable ### Key differences from encryption operators pattern @@ -492,7 +492,7 @@ When using the manual encryption pattern instead of encryption operators, you ha ### What's happening behind the scenes -1. **`protectClient.encrypt()`**: Encrypts plaintext and generates search indexes +1. **`encryptionClient.encrypt()`**: Encrypts plaintext and generates search indexes 2. **Regular Drizzle operators**: Work with encrypted data as if it were normal data 3. **PostgreSQL**: Uses special indexes to search encrypted fields efficiently 4. **`bulkDecryptModels()`**: Batch decrypts all encrypted fields in results @@ -514,7 +514,7 @@ When using the manual encryption pattern instead of encryption operators, you ha ```ts // Single value encryption -const encrypted = await protectClient.encrypt(plaintext, { +const encrypted = await encryptionClient.encrypt(plaintext, { table: protectTransactions, column: protectTransactions.column_name }) @@ -529,7 +529,7 @@ const results = await db.select() ```ts // Bulk decrypt query results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) // Use decrypted.data return decrypted.data diff --git a/docs/reference/model-operations.md b/docs/reference/model-operations.md index 865c576b..c603370f 100644 --- a/docs/reference/model-operations.md +++ b/docs/reference/model-operations.md @@ -24,7 +24,7 @@ These operations automatically handle the encryption of fields defined in your s The `encryptModel` method encrypts fields in your model that are defined in your schema while leaving other fields unchanged. ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; import { users } from "./protect/schema"; const user = { @@ -35,7 +35,7 @@ const user = { metadata: { role: "admin" }, // Will remain unchanged }; -const encryptedResult = await protectClient.encryptModel(user, users); +const encryptedResult = await encryptionClient.encryptModel(user, users); if (encryptedResult.failure) { console.error("Encryption failed:", encryptedResult.failure.message); @@ -57,7 +57,7 @@ const encryptedUser = encryptedResult.data; The `decryptModel` method automatically detects and decrypts any encrypted fields in your model. ```typescript -const decryptedResult = await protectClient.decryptModel(encryptedUser); +const decryptedResult = await encryptionClient.decryptModel(encryptedUser); if (decryptedResult.failure) { console.error("Decryption failed:", decryptedResult.failure.message); @@ -88,7 +88,7 @@ const users = [ }, ]; -const encryptedResult = await protectClient.bulkEncryptModels(users, users); +const encryptedResult = await encryptionClient.bulkEncryptModels(users, users); if (encryptedResult.failure) { console.error("Bulk encryption failed:", encryptedResult.failure.message); @@ -101,7 +101,7 @@ const encryptedUsers = encryptedResult.data; ### Bulk decryption ```typescript -const decryptedResult = await protectClient.bulkDecryptModels(encryptedUsers); +const decryptedResult = await encryptionClient.bulkDecryptModels(encryptedUsers); if (decryptedResult.failure) { console.error("Bulk decryption failed:", decryptedResult.failure.message); @@ -133,16 +133,16 @@ type User = { }; // Use the type parameter for type safety -const encryptedResult = await protectClient.encryptModel(user, users); -const decryptedResult = await protectClient.decryptModel(encryptedUser); +const encryptedResult = await encryptionClient.encryptModel(user, users); +const decryptedResult = await encryptionClient.decryptModel(encryptedUser); // Bulk operations -const bulkEncryptedResult = await protectClient.bulkEncryptModels( +const bulkEncryptedResult = await encryptionClient.bulkEncryptModels( userModels, users ); const bulkDecryptedResult = - await protectClient.bulkDecryptModels(encryptedUsers); + await encryptionClient.bulkDecryptModels(encryptedUsers); ``` The type system ensures: @@ -163,7 +163,7 @@ const users = encryptedTable("users", { }); // Types are inferred from the schema -const result = await protectClient.encryptModel(user, users); +const result = await encryptionClient.encryptModel(user, users); // Result type includes encrypted fields for email and address ``` @@ -173,20 +173,20 @@ All model operations support lock contexts for identity-aware encryption: ```typescript // Single model operations -const encryptedResult = await protectClient +const encryptedResult = await encryptionClient .encryptModel(user, users) .withLockContext(lockContext); -const decryptedResult = await protectClient +const decryptedResult = await encryptionClient .decryptModel(encryptedUser) .withLockContext(lockContext); // Bulk operations -const bulkEncryptedResult = await protectClient +const bulkEncryptedResult = await encryptionClient .bulkEncryptModels(userModels, users) .withLockContext(lockContext); -const bulkDecryptedResult = await protectClient +const bulkDecryptedResult = await encryptionClient .bulkDecryptModels(encryptedUsers) .withLockContext(lockContext); ``` @@ -196,7 +196,7 @@ const bulkDecryptedResult = await protectClient All model operations return a `Result` type that includes either a `data` or `failure` property: ```typescript -const result = await protectClient.encryptModel(user, users); +const result = await encryptionClient.encryptModel(user, users); if (result.failure) { // Handle specific error types diff --git a/docs/reference/schema.md b/docs/reference/schema.md index dcebf38d..f0fc0962 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -144,7 +144,7 @@ const config: EncryptionClientConfig = { schemas: [protectedUsers], // At least one encryptedTable is required } -const protectClient = await Encryption(config); +const encryptionClient = await Encryption(config); ``` --- diff --git a/docs/reference/searchable-encryption-postgres.md b/docs/reference/searchable-encryption-postgres.md index 99555588..de39d7b3 100644 --- a/docs/reference/searchable-encryption-postgres.md +++ b/docs/reference/searchable-encryption-postgres.md @@ -90,7 +90,7 @@ The function takes an array of objects, each with the following properties: Example: ```typescript -const term = await protectClient.createSearchTerms([{ +const term = await encryptionClient.createSearchTerms([{ value: 'user@example.com', column: schema.email, table: schema, @@ -142,19 +142,19 @@ Pass a string to `encryptQuery` to perform a JSONPath selector query. The string ```typescript // Simple path query -const pathTerm = await protectClient.encryptQuery('$.user.email', { +const pathTerm = await encryptionClient.encryptQuery('$.user.email', { column: documents.metadata, table: documents, }) // Nested path query -const nestedTerm = await protectClient.encryptQuery('$.user.profile.role', { +const nestedTerm = await encryptionClient.encryptQuery('$.user.profile.role', { column: documents.metadata, table: documents, }) // Array index path query -const arrayTerm = await protectClient.encryptQuery('$.items[0].name', { +const arrayTerm = await encryptionClient.encryptQuery('$.items[0].name', { column: documents.metadata, table: documents, }) @@ -177,13 +177,13 @@ Pass an object or array to `encryptQuery` to perform a containment query. ```typescript // Key-value containment -const roleTerm = await protectClient.encryptQuery({ role: 'admin' }, { +const roleTerm = await encryptionClient.encryptQuery({ role: 'admin' }, { column: documents.metadata, table: documents, }) // Nested object containment -const nestedTerm = await protectClient.encryptQuery( +const nestedTerm = await encryptionClient.encryptQuery( { user: { profile: { role: 'admin' } } }, { column: documents.metadata, @@ -192,7 +192,7 @@ const nestedTerm = await protectClient.encryptQuery( ) // Array containment -const tagsTerm = await protectClient.encryptQuery(['admin', 'user'], { +const tagsTerm = await encryptionClient.encryptQuery(['admin', 'user'], { column: documents.metadata, table: documents, }) @@ -204,10 +204,10 @@ const tagsTerm = await protectClient.encryptQuery(['admin', 'user'], { > > ```typescript > // Wrong for searchableJson - will fail (works for orderAndRange) -> await protectClient.encryptQuery(42, { column: documents.metadata, table: documents }) +> await encryptionClient.encryptQuery(42, { column: documents.metadata, table: documents }) > > // Correct - wrap in an object -> await protectClient.encryptQuery({ value: 42 }, { column: documents.metadata, table: documents }) +> await encryptionClient.encryptQuery({ value: 42 }, { column: documents.metadata, table: documents }) > ``` @@ -227,7 +227,7 @@ const tagsTerm = await protectClient.encryptQuery(['admin', 'user'], { Use `encryptQuery` with an array to encrypt multiple JSONB query terms in a single call. Each item can have a different plaintext type: ```typescript -const terms = await protectClient.encryptQuery([ +const terms = await encryptionClient.encryptQuery([ { value: '$.user.email', // string → JSONPath selector column: documents.metadata, @@ -257,7 +257,7 @@ console.log(terms.data) // array of encrypted query terms To use encrypted JSONB query terms in PostgreSQL queries, specify `returnType: 'composite-literal'` to get the terms formatted for direct use in SQL: ```typescript -const term = await protectClient.encryptQuery([{ +const term = await encryptionClient.encryptQuery([{ value: '$.user.email', column: documents.metadata, table: documents, @@ -287,14 +287,14 @@ For advanced use cases, you can specify the query type explicitly instead of rel ```typescript // Explicit steVecSelector -const selectorTerm = await protectClient.encryptQuery('$.user.email', { +const selectorTerm = await encryptionClient.encryptQuery('$.user.email', { column: documents.metadata, table: documents, queryType: 'steVecSelector', }) // Explicit steVecTerm -const containTerm = await protectClient.encryptQuery({ role: 'admin' }, { +const containTerm = await encryptionClient.encryptQuery({ role: 'admin' }, { column: documents.metadata, table: documents, queryType: 'steVecTerm', @@ -307,7 +307,7 @@ const containTerm = await protectClient.encryptQuery({ role: 'admin' }, { > > ```typescript > // To find documents where a field contains the string "admin" -> const term = await protectClient.encryptQuery(['admin'], { +> const term = await encryptionClient.encryptQuery(['admin'], { > column: documents.metadata, > table: documents, > queryType: 'steVecTerm', // Explicit for clarity @@ -320,7 +320,7 @@ Use `.equality()` when you need to find exact matches: ```typescript // Find user with specific email -const term = await protectClient.createSearchTerms([{ +const term = await encryptionClient.createSearchTerms([{ value: 'user@example.com', column: schema.email, table: schema, @@ -344,7 +344,7 @@ Use `.freeTextSearch()` for text-based searches: ```typescript // Search for users with emails containing "example" -const term = await protectClient.createSearchTerms([{ +const term = await encryptionClient.createSearchTerms([{ value: 'example', column: schema.email, table: schema, @@ -395,12 +395,12 @@ const client = new Client({ // your connection details }) -const protectClient = await Encryption({ +const encryptionClient = await Encryption({ schemas: [schema] }) // Insert encrypted data -const encryptedData = await protectClient.encryptModel({ +const encryptedData = await encryptionClient.encryptModel({ email: 'user@example.com' }, schema) @@ -414,7 +414,7 @@ await client.query( ) // Search encrypted data -const searchTerm = await protectClient.createSearchTerms([{ +const searchTerm = await encryptionClient.createSearchTerms([{ value: 'example.com', column: schema.email, table: schema, @@ -431,7 +431,7 @@ const result = await client.query( ) // Decrypt results -const decryptedData = await protectClient.bulkDecryptModels(result.rows) +const decryptedData = await encryptionClient.bulkDecryptModels(result.rows) ``` ### Using Supabase SDK diff --git a/docs/reference/supabase-sdk.md b/docs/reference/supabase-sdk.md index da32fc8d..c2d74a9a 100644 --- a/docs/reference/supabase-sdk.md +++ b/docs/reference/supabase-sdk.md @@ -42,9 +42,9 @@ const config: EncryptionClientConfig = { schemas: [users], } -const protectClient = await Encryption(config) +const encryptionClient = await Encryption(config) -const encryptedResult = await protectClient.encryptModel( +const encryptedResult = await encryptionClient.encryptModel( { name: 'John Doe', email: 'john.doe@example.com' @@ -74,7 +74,7 @@ const { data, error } = await supabase Without the `::jsonb` cast, the encrypted payload would be wrapped in an object with a `data` key, which would require additional handling before decryption. The cast ensures you get the raw encrypted payload that can be directly used with Stash Encryption for decryption: ```typescript -const decryptedResult = await protectClient.decryptModel(data[0]) +const decryptedResult = await encryptionClient.decryptModel(data[0]) if (decryptedResult.failure) { // Handle the failure @@ -105,7 +105,7 @@ const config: EncryptionClientConfig = { schemas: [users], } -const protectClient = await Encryption(config) +const encryptionClient = await Encryption(config) const model = { name: 'John Doe', @@ -113,7 +113,7 @@ const model = { otherField: 'not encrypted' } -const encryptedModel = await protectClient.encryptModel(model, users) +const encryptedModel = await encryptionClient.encryptModel(model, users) const { data, error } = await supabase .from('users') @@ -136,7 +136,7 @@ const models = [ } ] -const encryptedModels = await protectClient.bulkEncryptModels(models, users) +const encryptedModels = await encryptionClient.bulkEncryptModels(models, users) const { data, error } = await supabase .from('users') @@ -149,7 +149,7 @@ const { data: selectedData, error: selectError } = await supabase .select('id, name::jsonb, email::jsonb, otherField') // Decrypt all models at once -const decryptedModels = await protectClient.bulkDecryptModels(selectedData) +const decryptedModels = await encryptionClient.bulkDecryptModels(selectedData) ``` ## Exposing EQL schema @@ -174,7 +174,7 @@ ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA eql_v2 GRANT ALL ON SEQUENC When searching encrypted data, you need to convert the encrypted payload into a format that PostgreSQL and the Supabase SDK can understand. The encrypted payload needs to be converted to a raw composite type format by double stringifying the JSON: ```typescript -const searchTerms = await protectClient.createSearchTerms([ +const searchTerms = await encryptionClient.createSearchTerms([ { value: 'billy@example.com', column: users.email, @@ -189,7 +189,7 @@ const searchTerm = searchTerms.data[0] For certain queries, when including the encrypted search term with an operator that uses the string logic syntax, you need to use the 'escaped-composite-literal' return type: ```typescript -const searchTerms = await protectClient.createSearchTerms([ +const searchTerms = await encryptionClient.createSearchTerms([ { value: 'billy@example.com', column: users.email, @@ -208,7 +208,7 @@ Here are examples of different ways to search encrypted data using the Supabase ### Equality Search ```typescript -const searchTerms = await protectClient.createSearchTerms([ +const searchTerms = await encryptionClient.createSearchTerms([ { value: 'billy@example.com', column: users.email, @@ -226,7 +226,7 @@ const { data, error } = await supabase ### Pattern Matching Search ```typescript -const searchTerms = await protectClient.createSearchTerms([ +const searchTerms = await encryptionClient.createSearchTerms([ { value: 'example.com', column: users.email, @@ -247,7 +247,7 @@ When you need to search for multiple encrypted values, you can use the IN operat ```typescript // Encrypt multiple search terms -const searchTerms = await protectClient.createSearchTerms([ +const searchTerms = await encryptionClient.createSearchTerms([ { value: 'value1', column: users.name, @@ -275,7 +275,7 @@ You can combine multiple encrypted search conditions using the `.or()` syntax. T ```typescript // Encrypt search terms for different columns -const searchTerms = await protectClient.createSearchTerms([ +const searchTerms = await encryptionClient.createSearchTerms([ { value: 'user@example.com', column: users.email, diff --git a/examples/basic/index.ts b/examples/basic/index.ts index 9e9e8fce..7a380f9c 100644 --- a/examples/basic/index.ts +++ b/examples/basic/index.ts @@ -1,6 +1,6 @@ import 'dotenv/config' import readline from 'node:readline' -import { protectClient, users } from './protect' +import { encryptionClient, users } from './protect' const rl = readline.createInterface({ input: process.stdin, @@ -18,13 +18,13 @@ const askQuestion = (): Promise => { async function main() { const input = await askQuestion() - const encryptResult = await protectClient.encrypt(input, { + const encryptResult = await encryptionClient.encrypt(input, { column: users.name, table: users, }) if (encryptResult.failure) { - throw new Error(`[protect]: ${encryptResult.failure.message}`) + throw new Error(`[encryption]: ${encryptResult.failure.message}`) } const ciphertext = encryptResult.data @@ -32,10 +32,10 @@ async function main() { console.log('Encrypting your name...') console.log('The ciphertext is:', ciphertext) - const decryptResult = await protectClient.decrypt(ciphertext) + const decryptResult = await encryptionClient.decrypt(ciphertext) if (decryptResult.failure) { - throw new Error(`[protect]: ${decryptResult.failure.message}`) + throw new Error(`[encryption]: ${decryptResult.failure.message}`) } const plaintext = decryptResult.data @@ -58,13 +58,13 @@ async function main() { bulkPlaintexts.map((p) => p.plaintext), ) - const bulkEncryptResult = await protectClient.bulkEncrypt(bulkPlaintexts, { + const bulkEncryptResult = await encryptionClient.bulkEncrypt(bulkPlaintexts, { column: users.name, table: users, }) if (bulkEncryptResult.failure) { - throw new Error(`[protect]: ${bulkEncryptResult.failure.message}`) + throw new Error(`[encryption]: ${bulkEncryptResult.failure.message}`) } console.log('Bulk encrypted data:', bulkEncryptResult.data) diff --git a/examples/basic/protect.ts b/examples/basic/protect.ts index 0ecb6ec2..50a9c3c2 100644 --- a/examples/basic/protect.ts +++ b/examples/basic/protect.ts @@ -14,4 +14,4 @@ const config: EncryptionClientConfig = { schemas: [users], } -export const protectClient = await Encryption(config) +export const encryptionClient = await Encryption(config) diff --git a/examples/drizzle/src/controllers/transactions.ts b/examples/drizzle/src/controllers/transactions.ts index 308a1daf..d83d179e 100644 --- a/examples/drizzle/src/controllers/transactions.ts +++ b/examples/drizzle/src/controllers/transactions.ts @@ -3,7 +3,7 @@ import type { Request, Response } from 'express' import { db } from '../db' import { transactions } from '../db/schema' import { - protectClient, + encryptionClient, protectOps, transactionsSchema, } from '../protect/config' @@ -74,7 +74,7 @@ export async function getTransactions(req: Request, res: Response) { const results = await query.execute() // Decrypt results - const decryptedResult = await protectClient.bulkDecryptModels(results) + const decryptedResult = await encryptionClient.bulkDecryptModels(results) if (decryptedResult.failure) { return res.status(500).json({ error: 'Decryption failed', @@ -119,7 +119,7 @@ export async function createTransaction(req: Request, res: Response) { } // Encrypt the transaction model - const encryptedResult = await protectClient.encryptModel< + const encryptedResult = await encryptionClient.encryptModel< typeof transactionData >(transactionData, transactionsSchema) @@ -137,7 +137,7 @@ export async function createTransaction(req: Request, res: Response) { .returning() // Decrypt the inserted record for response - const decryptedResult = await protectClient.decryptModel(inserted) + const decryptedResult = await encryptionClient.decryptModel(inserted) if (decryptedResult.failure) { return res.status(500).json({ error: 'Decryption failed', @@ -175,7 +175,7 @@ export async function getTransaction(req: Request, res: Response) { } // Decrypt the transaction - const decryptedResult = await protectClient.decryptModel(transaction) + const decryptedResult = await encryptionClient.decryptModel(transaction) if (decryptedResult.failure) { return res.status(500).json({ error: 'Decryption failed', @@ -234,7 +234,7 @@ export async function updateTransaction(req: Request, res: Response) { body.description !== undefined ) { // Decrypt existing transaction to get current values - const decryptedExisting = await protectClient.decryptModel(existing) + const decryptedExisting = await encryptionClient.decryptModel(existing) if (decryptedExisting.failure) { return res.status(500).json({ error: 'Decryption failed', @@ -254,7 +254,7 @@ export async function updateTransaction(req: Request, res: Response) { } // Encrypt the merged data - const encryptedResult = await protectClient.encryptModel( + const encryptedResult = await encryptionClient.encryptModel( mergedData, transactionsSchema, ) @@ -282,7 +282,7 @@ export async function updateTransaction(req: Request, res: Response) { .returning() // Decrypt the updated record for response - const decryptedResult = await protectClient.decryptModel(updated) + const decryptedResult = await encryptionClient.decryptModel(updated) if (decryptedResult.failure) { return res.status(500).json({ error: 'Decryption failed', diff --git a/examples/drizzle/src/protect/config.ts b/examples/drizzle/src/protect/config.ts index 1e3d8a12..fa5f512a 100644 --- a/examples/drizzle/src/protect/config.ts +++ b/examples/drizzle/src/protect/config.ts @@ -10,9 +10,9 @@ import { transactions } from '../db/schema' export const transactionsSchema = extractProtectSchema(transactions) // Initialize Stash Encryption client -export const protectClient = await Encryption({ +export const encryptionClient = await Encryption({ schemas: [transactionsSchema], }) // Create Protect operators for encrypted field queries -export const protectOps = createProtectOperators(protectClient) +export const protectOps = createProtectOperators(encryptionClient) diff --git a/examples/dynamo/src/bulk-operations.ts b/examples/dynamo/src/bulk-operations.ts index 2973b679..e685e689 100644 --- a/examples/dynamo/src/bulk-operations.ts +++ b/examples/dynamo/src/bulk-operations.ts @@ -2,7 +2,7 @@ import { BatchGetCommand, BatchWriteCommand } from '@aws-sdk/lib-dynamodb' import { protectDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' import { log } from './common/log' -import { protectClient, users } from './common/protect' +import { encryptionClient, users } from './common/protect' const tableName = 'UsersBulkOperations' @@ -29,7 +29,7 @@ const main = async () => { }) const protectDynamo = protectDynamoDB({ - protectClient, + encryptionClient, }) const items = [ diff --git a/examples/dynamo/src/common/protect.ts b/examples/dynamo/src/common/protect.ts index 0ed66006..fe68870d 100644 --- a/examples/dynamo/src/common/protect.ts +++ b/examples/dynamo/src/common/protect.ts @@ -4,6 +4,6 @@ export const users = encryptedTable('users', { email: encryptedColumn('email').equality(), }) -export const protectClient = await Encryption({ +export const encryptionClient = await Encryption({ schemas: [users], }) diff --git a/examples/dynamo/src/encrypted-key-in-gsi.ts b/examples/dynamo/src/encrypted-key-in-gsi.ts index 9d50d949..c0ee5529 100644 --- a/examples/dynamo/src/encrypted-key-in-gsi.ts +++ b/examples/dynamo/src/encrypted-key-in-gsi.ts @@ -2,7 +2,7 @@ import { PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb' import { protectDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' import { log } from './common/log' -import { protectClient, users } from './common/protect' +import { encryptionClient, users } from './common/protect' const tableName = 'UsersEncryptedKeyInGSI' const indexName = 'EmailIndex' @@ -45,7 +45,7 @@ const main = async () => { }) const protectDynamo = protectDynamoDB({ - protectClient, + encryptionClient, }) const user = { @@ -67,7 +67,7 @@ const main = async () => { await dynamoClient.send(putCommand) // Use encryptQuery to create the search term for GSI query - const encryptedResult = await protectClient.encryptQuery([ + const encryptedResult = await encryptionClient.encryptQuery([ { value: 'abc@example.com', column: users.email, diff --git a/examples/dynamo/src/encrypted-partition-key.ts b/examples/dynamo/src/encrypted-partition-key.ts index 183f119f..7bee1e91 100644 --- a/examples/dynamo/src/encrypted-partition-key.ts +++ b/examples/dynamo/src/encrypted-partition-key.ts @@ -2,7 +2,7 @@ import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' import { protectDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient } from './common/dynamo' import { log } from './common/log' -import { protectClient, users } from './common/protect' +import { encryptionClient, users } from './common/protect' const tableName = 'UsersEncryptedPartitionKey' @@ -28,7 +28,7 @@ const main = async () => { }) const protectDynamo = protectDynamoDB({ - protectClient, + encryptionClient, }) const user = { @@ -50,7 +50,7 @@ const main = async () => { await docClient.send(putCommand) // Use encryptQuery to create the search term for partition key lookup - const encryptedResult = await protectClient.encryptQuery([ + const encryptedResult = await encryptionClient.encryptQuery([ { value: 'abc@example.com', column: users.email, diff --git a/examples/dynamo/src/encrypted-sort-key.ts b/examples/dynamo/src/encrypted-sort-key.ts index b868ea45..3e3deb6c 100644 --- a/examples/dynamo/src/encrypted-sort-key.ts +++ b/examples/dynamo/src/encrypted-sort-key.ts @@ -2,7 +2,7 @@ import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' import { protectDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' import { log } from './common/log' -import { protectClient, users } from './common/protect' +import { encryptionClient, users } from './common/protect' const tableName = 'UsersEncryptedSortKey' @@ -37,7 +37,7 @@ const main = async () => { }) const protectDynamo = protectDynamoDB({ - protectClient, + encryptionClient, }) const user = { @@ -59,7 +59,7 @@ const main = async () => { await docClient.send(putCommand) // Use encryptQuery to create the search term for sort key range query - const encryptedResult = await protectClient.encryptQuery([ + const encryptedResult = await encryptionClient.encryptQuery([ { value: 'abc@example.com', column: users.email, diff --git a/examples/dynamo/src/export-to-pg.ts b/examples/dynamo/src/export-to-pg.ts index 7ecf5c80..d3bc191a 100644 --- a/examples/dynamo/src/export-to-pg.ts +++ b/examples/dynamo/src/export-to-pg.ts @@ -4,7 +4,7 @@ import pg from 'pg' // Insert data in dynamo, scan it back out, insert/copy into PG, query from PG. import { createTable, docClient, dynamoClient } from './common/dynamo' import { log } from './common/log' -import { protectClient, users } from './common/protect' +import { encryptionClient, users } from './common/protect' const PgClient = pg.Client const tableName = 'UsersExportToPG' @@ -32,7 +32,7 @@ const main = async () => { }) const protectDynamo = protectDynamoDB({ - protectClient, + encryptionClient, }) const user = { @@ -128,7 +128,7 @@ const main = async () => { log('inserted rows', insertResult.rows) - const decryptRowsResult = await protectClient.bulkDecryptModels( + const decryptRowsResult = await encryptionClient.bulkDecryptModels( insertResult.rows, ) diff --git a/examples/dynamo/src/simple.ts b/examples/dynamo/src/simple.ts index 999ffa35..9feb0bae 100644 --- a/examples/dynamo/src/simple.ts +++ b/examples/dynamo/src/simple.ts @@ -2,7 +2,7 @@ import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' import { protectDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' import { log } from './common/log' -import { protectClient, users } from './common/protect' +import { encryptionClient, users } from './common/protect' const tableName = 'UsersSimple' @@ -29,7 +29,7 @@ const main = async () => { }) const protectDynamo = protectDynamoDB({ - protectClient, + encryptionClient, }) const user = { diff --git a/examples/hono-supabase/src/index.ts b/examples/hono-supabase/src/index.ts index a108c3c4..cceee1b5 100644 --- a/examples/hono-supabase/src/index.ts +++ b/examples/hono-supabase/src/index.ts @@ -19,7 +19,7 @@ const config: EncryptionClientConfig = { schemas: [users], } -export const protectClient = await Encryption(config) +export const encryptionClient = await Encryption(config) // Create a single supabase client for interacting with the database const supabaseUrl = process.env.SUPABASE_URL @@ -46,7 +46,7 @@ app.get('/users', async (c) => { users.map(async (user) => { // The encrypted data is stored in the EQL format: { c: 'ciphertext' } // and the decrypt function expects the data to be in this format. - const decryptResult = await protectClient.decrypt(user.email) + const decryptResult = await encryptionClient.decrypt(user.email) if (decryptResult.failure) { console.error( @@ -82,7 +82,7 @@ app.post('/users', async (c) => { // The encrypt function expects the plaintext to be of type string // and the second argument to be an object with the table and column // names of the table where you are storing the data. - const encryptedResult = await protectClient.encrypt(email, { + const encryptedResult = await encryptionClient.encrypt(email, { column: users.email, table: users, }) diff --git a/examples/next-drizzle-mysql/src/app/actions.ts b/examples/next-drizzle-mysql/src/app/actions.ts index 7ef7e663..1d6decc1 100644 --- a/examples/next-drizzle-mysql/src/app/actions.ts +++ b/examples/next-drizzle-mysql/src/app/actions.ts @@ -3,13 +3,13 @@ import type { FormData } from '@/components/form' import { db } from '@/db' import { users } from '@/db/schema' -import { protectClient } from '@/protect' +import { encryptionClient } from '@/protect' import { users as protectedUsers } from '@/protect/schema' export async function createUser(data: FormData) { console.log(data) - const result = await protectClient.encryptModel(data, protectedUsers) + const result = await encryptionClient.encryptModel(data, protectedUsers) if (result.failure) { console.error(result.failure.message) diff --git a/examples/next-drizzle-mysql/src/app/page.tsx b/examples/next-drizzle-mysql/src/app/page.tsx index 6194c61b..a0d146dd 100644 --- a/examples/next-drizzle-mysql/src/app/page.tsx +++ b/examples/next-drizzle-mysql/src/app/page.tsx @@ -1,7 +1,7 @@ import { ClientForm } from '@/components/form' import { db } from '@/db' import { users } from '@/db/schema' -import { protectClient } from '@/protect' +import { encryptionClient } from '@/protect' import { users as protectedUsers } from '@/protect/schema' type User = { @@ -13,7 +13,7 @@ type User = { export default async function Home() { const u = await db.select().from(users).limit(10) - const decryptedUsers = await protectClient.bulkDecryptModels(u) + const decryptedUsers = await encryptionClient.bulkDecryptModels(u) if (decryptedUsers.failure) { throw new Error(decryptedUsers.failure.message) diff --git a/examples/next-drizzle-mysql/src/protect/index.ts b/examples/next-drizzle-mysql/src/protect/index.ts index 09e6df3b..a7daee21 100644 --- a/examples/next-drizzle-mysql/src/protect/index.ts +++ b/examples/next-drizzle-mysql/src/protect/index.ts @@ -5,4 +5,4 @@ const config: EncryptionClientConfig = { schemas: [users], } -export const protectClient = await Encryption(config) +export const encryptionClient = await Encryption(config) diff --git a/examples/nextjs-clerk/src/app/page.tsx b/examples/nextjs-clerk/src/app/page.tsx index e883f685..4a0fadd1 100644 --- a/examples/nextjs-clerk/src/app/page.tsx +++ b/examples/nextjs-clerk/src/app/page.tsx @@ -1,6 +1,6 @@ import { db } from '@/core/db' import { users } from '@/core/db/schema' -import { getLockContext, protectClient } from '@/core/protect' +import { encryptionClient, getLockContext } from '@/core/protect' import { getCtsToken } from '@cipherstash/nextjs' import type { EncryptedData } from '@cipherstash/stack' import { auth, currentUser } from '@clerk/nextjs/server' @@ -25,7 +25,7 @@ async function getUsers(): Promise { const lockContext = getLockContext(cts_token) const promises = results.map(async (row) => { - const decryptResult = await protectClient + const decryptResult = await encryptionClient .decrypt(row.email as EncryptedData) .withLockContext(lockContext) diff --git a/examples/nextjs-clerk/src/core/protect/index.ts b/examples/nextjs-clerk/src/core/protect/index.ts index 7e96e293..419ab582 100644 --- a/examples/nextjs-clerk/src/core/protect/index.ts +++ b/examples/nextjs-clerk/src/core/protect/index.ts @@ -16,12 +16,12 @@ const config: EncryptionClientConfig = { schemas: [users], } -export const protectClient = await Encryption(config) +export const encryptionClient = await Encryption(config) export const getLockContext = (cts_token?: CtsToken) => { if (!cts_token) { throw new Error( - '[protect] A CTS token is required in order to get a lock context.', + '[encryption] A CTS token is required in order to get a lock context.', ) } diff --git a/examples/nextjs-clerk/src/lib/actions.ts b/examples/nextjs-clerk/src/lib/actions.ts index 9772a10f..c7b9c8ed 100644 --- a/examples/nextjs-clerk/src/lib/actions.ts +++ b/examples/nextjs-clerk/src/lib/actions.ts @@ -2,7 +2,7 @@ import { db } from '@/core/db' import { users } from '@/core/db/schema' -import { protectClient, users as protectUsers } from '@/core/protect' +import { encryptionClient, users as protectUsers } from '@/core/protect' import { getLockContext } from '@/core/protect' import { getCtsToken } from '@cipherstash/nextjs' import { auth } from '@clerk/nextjs/server' @@ -30,7 +30,7 @@ export async function addUser(formData: FormData) { } const lockContext = getLockContext(ctsToken.ctsToken) - const encryptedResult = await protectClient + const encryptedResult = await encryptionClient .encrypt(email, { column: protectUsers.email, table: protectUsers, diff --git a/examples/typeorm/README.md b/examples/typeorm/README.md index dd905a3c..38c9a065 100644 --- a/examples/typeorm/README.md +++ b/examples/typeorm/README.md @@ -80,7 +80,7 @@ export const protectedUser = encryptedTable('user', { phone: encryptedColumn('phone').equality(), }) -export const protectClient = await Encryption({ +export const encryptionClient = await Encryption({ schemas: [protectedUser], }) ``` @@ -91,7 +91,7 @@ export const protectClient = await Encryption({ // src/helpers/protect-entity.ts import { ProtectEntityHelper } from './helpers/protect-entity' -const helper = new ProtectEntityHelper(protectClient) +const helper = new ProtectEntityHelper(encryptionClient) // 🚀 Bulk create with encryption (recommended for production) const users = await helper.bulkEncryptAndSave( @@ -155,7 +155,7 @@ email: EncryptedData | null ```typescript // Encrypt individual fields -const emailResult = await protectClient.encrypt('user@example.com', { +const emailResult = await encryptionClient.encrypt('user@example.com', { table: protectedUser, column: protectedUser.email, }) @@ -203,7 +203,7 @@ const newUser = { ssn: '111-22-3333' } -const encryptedModelResult = await protectClient.encryptModel(newUser, protectedUser) +const encryptedModelResult = await encryptionClient.encryptModel(newUser, protectedUser) if (encryptedModelResult.failure) { throw new Error(`Model encryption failed: ${encryptedModelResult.failure.message}`) @@ -286,7 +286,7 @@ export const encryptedDataTransformer = { ### 2. Error Handling ```typescript -const result = await protectClient.encrypt(data, { table, column }) +const result = await encryptionClient.encrypt(data, { table, column }) if (result.failure) { // Always handle failures gracefully @@ -298,7 +298,7 @@ if (result.failure) { ### 3. Environment Configuration ```typescript // Use environment variables for all sensitive data -export const protectClient = await Encryption({ +export const encryptionClient = await Encryption({ schemas: [protectedUser], }) ``` diff --git a/examples/typeorm/src/helpers/protect-entity.ts b/examples/typeorm/src/helpers/protect-entity.ts index 4519f241..161d8418 100644 --- a/examples/typeorm/src/helpers/protect-entity.ts +++ b/examples/typeorm/src/helpers/protect-entity.ts @@ -8,8 +8,8 @@ import { AppDataSource } from '../data-source' /** * Helper functions for working with encrypted entities in TypeORM */ -export class ProtectEntityHelper { - constructor(private protectClient: EncryptionClient) {} +export class EncryptionEntityHelper { + constructor(private encryptionClient: EncryptionClient) {} /** * Bulk encrypt and save entities to the database @@ -60,10 +60,13 @@ export class ProtectEntityHelper { plaintext: entity[fieldName] || null, })) - const encryptedResult = await this.protectClient.bulkEncrypt(plaintexts, { - table, - column, - }) + const encryptedResult = await this.encryptionClient.bulkEncrypt( + plaintexts, + { + table, + column, + }, + ) if (encryptedResult.failure) { throw new Error( @@ -148,7 +151,8 @@ export class ProtectEntityHelper { } // Bulk decrypt - const decryptedResult = await this.protectClient.bulkDecrypt(encryptedData) + const decryptedResult = + await this.encryptionClient.bulkDecrypt(encryptedData) if (decryptedResult.failure) { throw new Error( @@ -203,7 +207,7 @@ export class ProtectEntityHelper { fieldConfig: { table: any; column: any }, ): Promise { // Use encryptQuery instead of deprecated createSearchTerms - const encryptedResult = await this.protectClient.encryptQuery([ + const encryptedResult = await this.encryptionClient.encryptQuery([ { value: searchValue, column: fieldConfig.column, diff --git a/examples/typeorm/src/index.ts b/examples/typeorm/src/index.ts index 34a31d51..bb5162ef 100644 --- a/examples/typeorm/src/index.ts +++ b/examples/typeorm/src/index.ts @@ -2,8 +2,8 @@ import 'reflect-metadata' import 'dotenv/config' import { AppDataSource } from './data-source' import { User } from './entity/User' -import { ProtectEntityHelper } from './helpers/protect-entity' -import { initializeProtectClient, protectedUser } from './protect' +import { EncryptionEntityHelper } from './helpers/protect-entity' +import { initializeEncryptionClient, protectedUser } from './protect' async function main() { try { @@ -12,11 +12,11 @@ async function main() { console.log('✅ Database connection established') // Initialize the Encryption client - const protectClient = await initializeProtectClient() + const encryptionClient = await initializeEncryptionClient() console.log('✅ Encryption client initialized') // Initialize the helper for streamlined operations - const helper = new ProtectEntityHelper(protectClient) + const helper = new EncryptionEntityHelper(encryptionClient) console.log('\n🔐 Stash Encryption TypeORM Integration Demo') console.log('=====================================') @@ -31,15 +31,15 @@ async function main() { // Encrypt individual fields const [emailResult, ssnResult, phoneResult] = await Promise.all([ - protectClient.encrypt(emailToInsert, { + encryptionClient.encrypt(emailToInsert, { table: protectedUser, column: protectedUser.email, }), - protectClient.encrypt(ssnToInsert, { + encryptionClient.encrypt(ssnToInsert, { table: protectedUser, column: protectedUser.ssn, }), - protectClient.encrypt(phoneToInsert, { + encryptionClient.encrypt(phoneToInsert, { table: protectedUser, column: protectedUser.phone, }), @@ -195,7 +195,7 @@ async function main() { } // Encrypt the entire model - const encryptedModelResult = await protectClient.encryptModel( + const encryptedModelResult = await encryptionClient.encryptModel( newUser, protectedUser, ) diff --git a/examples/typeorm/src/protect.ts b/examples/typeorm/src/protect.ts index 9f37e637..d82c87ca 100644 --- a/examples/typeorm/src/protect.ts +++ b/examples/typeorm/src/protect.ts @@ -14,13 +14,13 @@ export const protectedUser = encryptedTable('user', { * Initialize the Encryption client with the defined schema * This will be used throughout the application for encryption/decryption operations */ -let protectClient: Awaited> +let encryptionClient: Awaited> -export async function initializeProtectClient() { - if (!protectClient) { - protectClient = await Encryption({ +export async function initializeEncryptionClient() { + if (!encryptionClient) { + encryptionClient = await Encryption({ schemas: [protectedUser], }) } - return protectClient + return encryptionClient } diff --git a/packages/drizzle/README.md b/packages/drizzle/README.md index d6b09514..ae40776e 100644 --- a/packages/drizzle/README.md +++ b/packages/drizzle/README.md @@ -124,7 +124,7 @@ import { usersTable } from '../db/schema' export const users = extractProtectSchema(usersTable) // Initialize Stash Encryption client -export const protectClient = await Encryption({ +export const encryptionClient = await Encryption({ schemas: [users] }) ``` @@ -134,10 +134,10 @@ export const protectClient = await Encryption({ ```typescript // protect/operators.ts import { createProtectOperators } from '@cipherstash/drizzle/pg' -import { protectClient } from './config' +import { encryptionClient } from './config' // Create operators that automatically handle encryption in queries -export const protectOps = createProtectOperators(protectClient) +export const protectOps = createProtectOperators(encryptionClient) ``` ## Usage Examples @@ -151,7 +151,7 @@ const newUsers = [ ] // Encrypt all models at once -const encryptedUsers = await protectClient.bulkEncryptModels(newUsers, users) +const encryptedUsers = await encryptionClient.bulkEncryptModels(newUsers, users) if (encryptedUsers.failure) { throw new Error(`Encryption failed: ${encryptedUsers.failure.message}`) } @@ -173,7 +173,7 @@ const results = await db .from(usersTable) // Decrypt all results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) if (decrypted.failure) { throw new Error(`Decryption failed: ${decrypted.failure.message}`) } @@ -213,7 +213,7 @@ const results = await db ) // Decrypt results -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) ``` ### Sorting encrypted columns @@ -224,7 +224,7 @@ const results = await db .from(usersTable) .orderBy(protectOps.asc(usersTable.age)) -const decrypted = await protectClient.bulkDecryptModels(results) +const decrypted = await encryptionClient.bulkDecryptModels(results) ``` > [!IMPORTANT] @@ -317,11 +317,11 @@ Extracts a Stash Encryption schema from a Drizzle table definition. **Returns:** Stash Encryption schema object -### `createProtectOperators(protectClient)` +### `createProtectOperators(encryptionClient)` Creates Drizzle-compatible operators that automatically handle encryption. **Parameters:** -- `protectClient` - Initialized Stash Encryption client +- `encryptionClient` - Initialized Stash Encryption client **Returns:** Object with all operator functions diff --git a/packages/drizzle/__tests__/docs.test.ts b/packages/drizzle/__tests__/docs.test.ts index 1e379048..af05a8fc 100644 --- a/packages/drizzle/__tests__/docs.test.ts +++ b/packages/drizzle/__tests__/docs.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config' import { existsSync, readFileSync } from 'node:fs' import { join } from 'node:path' -import { Encryption } from '@cipherstash/stack' +import { Encryption, type EncryptionClient } from '@cipherstash/stack' import * as drizzleOrm from 'drizzle-orm' import { integer, pgTable } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/postgres-js' @@ -67,15 +67,15 @@ const protectTransactions = extractProtectSchema(transactions) describe('Documentation Drift Tests', () => { let db: ReturnType let client: ReturnType - let protectClient: Awaited> - let protectOps: ReturnType + let encryptionClient: EncryptionClient + let encryptionOps: ReturnType let seedDataIds: number[] = [] beforeAll(async () => { client = postgres(process.env.DATABASE_URL as string) db = drizzle({ client }) - protectClient = await Encryption({ schemas: [protectTransactions] }) - protectOps = createProtectOperators(protectClient) + encryptionClient = await Encryption({ schemas: [protectTransactions] }) + encryptionOps = createProtectOperators(encryptionClient) // Create test table with EQL encrypted columns (drop if exists for clean state) await client`DROP TABLE IF EXISTS "drizzle-docs-test"` @@ -90,7 +90,7 @@ describe('Documentation Drift Tests', () => { ` // Seed test data - const encrypted = await protectClient.bulkEncryptModels( + const encrypted = await encryptionClient.bulkEncryptModels( docSeedData, protectTransactions, ) @@ -135,8 +135,8 @@ describe('Documentation Drift Tests', () => { const context: ExecutionContext = { db, transactions, - protect: protectOps, - protectClient, + protect: encryptionOps, + encryptionClient, protectTransactions, ...drizzleOrm, } @@ -172,7 +172,7 @@ describe('Documentation Drift Tests', () => { const context: ExecutionContext = { db, transactions, - protectClient, + encryptionClient, protectTransactions, ...drizzleOrm, // Note: 'protect' intentionally omitted diff --git a/packages/drizzle/__tests__/drizzle.test.ts b/packages/drizzle/__tests__/drizzle.test.ts index d9ebc0af..c4be5bee 100644 --- a/packages/drizzle/__tests__/drizzle.test.ts +++ b/packages/drizzle/__tests__/drizzle.test.ts @@ -1,5 +1,5 @@ import 'dotenv/config' -import { Encryption } from '@cipherstash/stack' +import { Encryption, type EncryptionClient } from '@cipherstash/stack' import { and, eq, inArray, sql } from 'drizzle-orm' import { integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core' import { drizzle } from 'drizzle-orm/postgres-js' @@ -56,7 +56,7 @@ const drizzleUsersTable = pgTable('protect-ci', { testRunId: text('test_run_id'), }) -// Extract Stash Encryption schema from Drizzle table +// Extract Encryption schema from Drizzle table const users = extractProtectSchema(drizzleUsersTable) // Hard code this as the CI database doesn't support order by on encrypted columns @@ -78,15 +78,15 @@ interface DecryptedUser { } } -let protectClient: Awaited> -let protectOps: ReturnType +let encryptionClient: EncryptionClient +let encryptionOps: ReturnType let db: ReturnType const testData: TestUser[] = [] beforeAll(async () => { - // Initialize Stash Encryption client using schema extracted from Drizzle table - protectClient = await Encryption({ schemas: [users] }) - protectOps = createProtectOperators(protectClient) + // Initialize Encryption client using schema extracted from Drizzle table + encryptionClient = await Encryption({ schemas: [users] }) + encryptionOps = createProtectOperators(encryptionClient) const client = postgres(process.env.DATABASE_URL as string) db = drizzle({ client }) @@ -146,7 +146,10 @@ beforeAll(async () => { ] // Encrypt and insert test data using Drizzle - const encryptedUser = await protectClient.bulkEncryptModels(testUsers, users) + const encryptedUser = await encryptionClient.bulkEncryptModels( + testUsers, + users, + ) if (encryptedUser.failure) { throw new Error(`Encryption failed: ${encryptedUser.failure.message}`) @@ -180,7 +183,7 @@ afterAll(async () => { .where(eq(drizzleUsersTable.testRunId, TEST_RUN_ID)) }, 30000) -describe('Drizzle ORM Integration with Stash Encryption', () => { +describe('Drizzle ORM Integration with Encryption', () => { it('should perform equality search using Protect operators', async () => { const searchEmail = 'jane.smith@example.com' @@ -197,14 +200,14 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { .where( and( eq(drizzleUsersTable.testRunId, TEST_RUN_ID), - await protectOps.eq(drizzleUsersTable.email, searchEmail), + await encryptionOps.eq(drizzleUsersTable.email, searchEmail), ), ) expect(results).toHaveLength(1) // Decrypt and verify - const decrypted = await protectClient.decryptModel(results[0]) + const decrypted = await encryptionClient.decryptModel(results[0]) if (decrypted.failure) { throw new Error(`Decryption failed: ${decrypted.failure.message}`) } @@ -229,7 +232,7 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { .where( and( eq(drizzleUsersTable.testRunId, TEST_RUN_ID), - await protectOps.ilike(drizzleUsersTable.email, searchText), + await encryptionOps.ilike(drizzleUsersTable.email, searchText), ), ) @@ -237,7 +240,7 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { expect(results.length).toBeGreaterThan(0) // Decrypt and verify - const decryptedResults = await protectClient.bulkDecryptModels(results) + const decryptedResults = await encryptionClient.bulkDecryptModels(results) if (decryptedResults.failure) { throw new Error( `Bulk decryption failed: ${decryptedResults.failure.message}`, @@ -273,7 +276,7 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { .where( and( eq(drizzleUsersTable.testRunId, TEST_RUN_ID), - await protectOps.gte(drizzleUsersTable.age, minAge), + await encryptionOps.gte(drizzleUsersTable.age, minAge), ), ) @@ -281,7 +284,7 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { expect(results.length).toBeGreaterThan(0) // Decrypt and verify - const decryptedResults = await protectClient.bulkDecryptModels(results) + const decryptedResults = await encryptionClient.bulkDecryptModels(results) if (decryptedResults.failure) { throw new Error( `Bulk decryption failed: ${decryptedResults.failure.message}`, @@ -316,14 +319,14 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { }) .from(drizzleUsersTable) .where(eq(drizzleUsersTable.testRunId, TEST_RUN_ID)) - .orderBy(protectOps.asc(drizzleUsersTable.age)) + .orderBy(encryptionOps.asc(drizzleUsersTable.age)) const results = await a expect(results.length).toBeGreaterThan(0) // Decrypt and verify sorting - const decryptedResults = await protectClient.bulkDecryptModels(results) + const decryptedResults = await encryptionClient.bulkDecryptModels(results) if (decryptedResults.failure) { throw new Error( `Bulk decryption failed: ${decryptedResults.failure.message}`, @@ -360,16 +363,16 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { }) .from(drizzleUsersTable) .where( - await protectOps.and( + await encryptionOps.and( eq(drizzleUsersTable.testRunId, TEST_RUN_ID), - protectOps.gte(drizzleUsersTable.age, minAge), - protectOps.lte(drizzleUsersTable.age, maxAge), - protectOps.ilike(drizzleUsersTable.email, searchText), + encryptionOps.gte(drizzleUsersTable.age, minAge), + encryptionOps.lte(drizzleUsersTable.age, maxAge), + encryptionOps.ilike(drizzleUsersTable.email, searchText), ), ) // Decrypt and verify - const decryptedResults = await protectClient.bulkDecryptModels(results) + const decryptedResults = await encryptionClient.bulkDecryptModels(results) if (decryptedResults.failure) { throw new Error( `Bulk decryption failed: ${decryptedResults.failure.message}`, @@ -412,11 +415,11 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { }) .from(drizzleUsersTable) .where( - await protectOps.and( + await encryptionOps.and( eq(drizzleUsersTable.testRunId, TEST_RUN_ID), - protectOps.or( - protectOps.eq(drizzleUsersTable.email, targetEmails[0]), - protectOps.eq(drizzleUsersTable.email, targetEmails[1]), + encryptionOps.or( + encryptionOps.eq(drizzleUsersTable.email, targetEmails[0]), + encryptionOps.eq(drizzleUsersTable.email, targetEmails[1]), eq(drizzleUsersTable.id, fallbackId), ), ), @@ -424,7 +427,7 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { expect(results.length).toBe(targetEmails.length + 1) // +1 for fallbackId row - const decryptedResults = await protectClient.bulkDecryptModels(results) + const decryptedResults = await encryptionClient.bulkDecryptModels(results) if (decryptedResults.failure) { throw new Error( `Bulk decryption failed: ${decryptedResults.failure.message}`, @@ -459,7 +462,7 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { } // Decrypt and verify nested fields - const decrypted = await protectClient.decryptModel(results[0]) + const decrypted = await encryptionClient.decryptModel(results[0]) if (decrypted.failure) { throw new Error(`Decryption failed: ${decrypted.failure.message}`) } @@ -490,7 +493,7 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { .where( and( eq(drizzleUsersTable.testRunId, TEST_RUN_ID), - await protectOps.inArray(drizzleUsersTable.email, searchEmails), + await encryptionOps.inArray(drizzleUsersTable.email, searchEmails), ), ) @@ -498,7 +501,7 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { expect(results.length).toBe(2) // Decrypt and verify - const decryptedResults = await protectClient.bulkDecryptModels(results) + const decryptedResults = await encryptionClient.bulkDecryptModels(results) if (decryptedResults.failure) { throw new Error( `Bulk decryption failed: ${decryptedResults.failure.message}`, @@ -530,7 +533,7 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { .where( and( eq(drizzleUsersTable.testRunId, TEST_RUN_ID), - await protectOps.between(drizzleUsersTable.age, minAge, maxAge), + await encryptionOps.between(drizzleUsersTable.age, minAge, maxAge), ), ) @@ -538,7 +541,7 @@ describe('Drizzle ORM Integration with Stash Encryption', () => { expect(results.length).toBeGreaterThan(0) // Decrypt and verify - const decryptedResults = await protectClient.bulkDecryptModels(results) + const decryptedResults = await encryptionClient.bulkDecryptModels(results) if (decryptedResults.failure) { throw new Error( `Bulk decryption failed: ${decryptedResults.failure.message}`, diff --git a/packages/drizzle/src/pg/operators.ts b/packages/drizzle/src/pg/operators.ts index a71724c2..a9a9d466 100644 --- a/packages/drizzle/src/pg/operators.ts +++ b/packages/drizzle/src/pg/operators.ts @@ -127,11 +127,11 @@ function getDrizzleTableFromColumn(drizzleColumn: SQLWrapper): unknown { } /** - * Helper to extract protect table from a drizzle column by deriving it from the column's parent table + * Helper to extract encryption table from a drizzle column by deriving it from the column's parent table */ -function getProtectTableFromColumn( +function getEncryptionTableFromColumn( drizzleColumn: SQLWrapper, - protectTableCache: Map>, + encryptionTableCache: Map>, ): EncryptedTable | undefined { const drizzleTable = getDrizzleTableFromColumn(drizzleColumn) if (!drizzleTable) { @@ -144,17 +144,17 @@ function getProtectTableFromColumn( } // Check cache first - let protectTable = protectTableCache.get(tableName) - if (protectTable) { - return protectTable + let encryptionTable = encryptionTableCache.get(tableName) + if (encryptionTable) { + return encryptionTable } - // Extract protect schema from drizzle table and cache it + // Extract encrypted schema from drizzle table and cache it try { // biome-ignore lint/suspicious/noExplicitAny: PgTable type doesn't expose all needed properties - protectTable = extractProtectSchema(drizzleTable as PgTable) - protectTableCache.set(tableName, protectTable) - return protectTable + encryptionTable = extractProtectSchema(drizzleTable as PgTable) + encryptionTableCache.set(tableName, encryptionTable) + return encryptionTable } catch { // Table doesn't have encrypted columns or extraction failed return undefined @@ -166,7 +166,7 @@ function getProtectTableFromColumn( */ function getEncryptedColumn( drizzleColumn: SQLWrapper, - protectTable: EncryptedTable, + encryptionTable: EncryptedTable, ): EncryptedColumn | undefined { const column = drizzleColumn as unknown as Record const columnName = column.name as string | undefined @@ -174,64 +174,70 @@ function getEncryptedColumn( return undefined } - const protectTableAny = protectTable as unknown as Record - return protectTableAny[columnName] as EncryptedColumn | undefined + const encryptionTableAny = encryptionTable as unknown as Record< + string, + unknown + > + return encryptionTableAny[columnName] as EncryptedColumn | undefined } /** * Column metadata extracted from a Drizzle column */ interface ColumnInfo { - readonly protectColumn: EncryptedColumn | undefined + readonly encryptionColumn: EncryptedColumn | undefined readonly config: (EncryptedColumnConfig & { name: string }) | undefined - readonly protectTable: EncryptedTable | undefined + readonly encryptionTable: EncryptedTable | undefined readonly columnName: string readonly tableName: string | undefined } /** * Helper to get the EncryptedColumn and column config for a Drizzle column - * If protectTable is not provided, it will be derived from the column + * If encryptionTable is not provided, it will be derived from the column */ function getColumnInfo( drizzleColumn: SQLWrapper, - protectTable: EncryptedTable | undefined, - protectTableCache: Map>, + encryptionTable: EncryptedTable | undefined, + encryptionTableCache: Map>, ): ColumnInfo { const column = drizzleColumn as unknown as Record const columnName = (column.name as string | undefined) || 'unknown' - // If protectTable not provided, try to derive it from the column - let resolvedProtectTable = protectTable - if (!resolvedProtectTable) { - resolvedProtectTable = getProtectTableFromColumn( + // If encryptionTable not provided, try to derive it from the column + let resolvedEncryptionTable = encryptionTable + if (!resolvedEncryptionTable) { + resolvedEncryptionTable = getEncryptionTableFromColumn( drizzleColumn, - protectTableCache, + encryptionTableCache, ) } const drizzleTable = getDrizzleTableFromColumn(drizzleColumn) const tableName = getDrizzleTableName(drizzleTable) - if (!resolvedProtectTable) { + if (!resolvedEncryptionTable) { // Column is not from an encrypted table const config = getEncryptedColumnConfig(columnName, drizzleColumn) return { - protectColumn: undefined, + encryptionColumn: undefined, config, - protectTable: undefined, + encryptionTable: undefined, columnName, tableName, } } - const protectColumn = getEncryptedColumn(drizzleColumn, resolvedProtectTable) + const encryptionColumn = getEncryptedColumn( + drizzleColumn, + resolvedEncryptionTable, + ) const config = getEncryptedColumnConfig(columnName, drizzleColumn) return { - protectColumn, + encryptionColumn, config, - protectTable: resolvedProtectTable, + encryptionTable: resolvedEncryptionTable, columnName, tableName, } @@ -268,14 +274,14 @@ interface ValueToEncrypt { * Returns an array of encrypted search terms or original values if not encrypted */ async function encryptValues( - protectClient: EncryptionClient, + encryptionClient: EncryptionClient, values: Array<{ value: unknown column: SQLWrapper queryType?: QueryTypeName }>, - protectTable: EncryptedTable | undefined, - protectTableCache: Map>, + encryptionTable: EncryptedTable | undefined, + encryptionTableCache: Map>, ): Promise { if (values.length === 0) { return [] @@ -287,12 +293,16 @@ async function encryptValues( for (let i = 0; i < values.length; i++) { const { value, column, queryType } = values[i] - const columnInfo = getColumnInfo(column, protectTable, protectTableCache) + const columnInfo = getColumnInfo( + column, + encryptionTable, + encryptionTableCache, + ) if ( - !columnInfo.protectColumn || + !columnInfo.encryptionColumn || !columnInfo.config || - !columnInfo.protectTable + !columnInfo.encryptionTable ) { // Column is not encrypted, return value as-is results[i] = value @@ -332,8 +342,8 @@ async function encryptValues( // Safe access with validation - we know these exist from earlier checks if ( !columnInfo.config || - !columnInfo.protectColumn || - !columnInfo.protectTable + !columnInfo.encryptionColumn || + !columnInfo.encryptionTable ) { continue } @@ -342,8 +352,8 @@ async function encryptValues( let group = columnGroups.get(columnName) if (!group) { group = { - column: columnInfo.protectColumn, - table: columnInfo.protectTable, + column: columnInfo.encryptionColumn, + table: columnInfo.encryptionTable, values: [], resultIndices: [], } @@ -373,7 +383,7 @@ async function encryptValues( queryType: v.queryType, })) - const encryptedTerms = await protectClient.encryptQuery(terms) + const encryptedTerms = await encryptionClient.encryptQuery(terms) if (encryptedTerms.failure) { throw new ProtectOperatorError( @@ -410,18 +420,18 @@ async function encryptValues( * Returns the encrypted search term or the original value if not encrypted */ async function encryptValue( - protectClient: EncryptionClient, + encryptionClient: EncryptionClient, value: unknown, drizzleColumn: SQLWrapper, - protectTable: EncryptedTable | undefined, - protectTableCache: Map>, + encryptionTable: EncryptedTable | undefined, + encryptionTableCache: Map>, queryType?: QueryTypeName, ): Promise { const results = await encryptValues( - protectClient, + encryptionClient, [{ value, column: drizzleColumn, queryType }], - protectTable, - protectTableCache, + encryptionTable, + encryptionTableCache, ) return results[0] } @@ -476,9 +486,9 @@ function createLazyOperator( ) => SQL, needsEncryption: boolean, columnInfo: ColumnInfo, - protectClient: EncryptionClient, - defaultProtectTable: EncryptedTable | undefined, - protectTableCache: Map>, + encryptionClient: EncryptionClient, + defaultEncryptionTable: EncryptedTable | undefined, + encryptionTableCache: Map>, min?: unknown, max?: unknown, queryType?: QueryTypeName, @@ -512,9 +522,9 @@ function createLazyOperator( if (!encryptionPromise) { encryptionPromise = executeLazyOperatorDirect( lazyOp, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } const sql = await encryptionPromise @@ -610,9 +620,9 @@ async function executeLazyOperator( */ async function executeLazyOperatorDirect( lazyOp: LazyOperator, - protectClient: EncryptionClient, - defaultProtectTable: EncryptedTable | undefined, - protectTableCache: Map>, + encryptionClient: EncryptionClient, + defaultEncryptionTable: EncryptedTable | undefined, + encryptionTableCache: Map>, ): Promise { if (!lazyOp.needsEncryption) { return lazyOp.execute(lazyOp.right) @@ -621,24 +631,24 @@ async function executeLazyOperatorDirect( if (lazyOp.min !== undefined && lazyOp.max !== undefined) { // Between operator - encrypt min and max const [encryptedMin, encryptedMax] = await encryptValues( - protectClient, + encryptionClient, [ { value: lazyOp.min, column: lazyOp.left, queryType: lazyOp.queryType }, { value: lazyOp.max, column: lazyOp.left, queryType: lazyOp.queryType }, ], - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return lazyOp.execute(undefined, encryptedMin, encryptedMax) } // Single value operator const encrypted = await encryptValue( - protectClient, + encryptionClient, lazyOp.right, lazyOp.left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, lazyOp.queryType, ) @@ -657,9 +667,9 @@ function createComparisonOperator( left: SQLWrapper, right: unknown, columnInfo: ColumnInfo, - protectClient: EncryptionClient, - defaultProtectTable: EncryptedTable | undefined, - protectTableCache: Map>, + encryptionClient: EncryptionClient, + defaultEncryptionTable: EncryptedTable | undefined, + encryptionTableCache: Map>, ): Promise | SQL { const { config } = columnInfo @@ -703,9 +713,9 @@ function createComparisonOperator( executeFn, true, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, undefined, // min undefined, // max 'orderAndRange', @@ -737,9 +747,9 @@ function createComparisonOperator( executeFn, true, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, undefined, // min undefined, // max 'equality', @@ -759,9 +769,9 @@ function createRangeOperator( min: unknown, max: unknown, columnInfo: ColumnInfo, - protectClient: EncryptionClient, - defaultProtectTable: EncryptedTable | undefined, - protectTableCache: Map>, + encryptionClient: EncryptionClient, + defaultEncryptionTable: EncryptedTable | undefined, + encryptionTableCache: Map>, ): Promise | SQL { const { config } = columnInfo @@ -801,9 +811,9 @@ function createRangeOperator( executeFn, true, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, min, max, 'orderAndRange', @@ -818,9 +828,9 @@ function createTextSearchOperator( left: SQLWrapper, right: unknown, columnInfo: ColumnInfo, - protectClient: EncryptionClient, - defaultProtectTable: EncryptedTable | undefined, - protectTableCache: Map>, + encryptionClient: EncryptionClient, + defaultEncryptionTable: EncryptedTable | undefined, + encryptionTableCache: Map>, ): Promise | SQL { const { config } = columnInfo @@ -860,9 +870,9 @@ function createTextSearchOperator( executeFn, true, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, undefined, // min undefined, // max 'freeTextSearch', @@ -874,7 +884,7 @@ function createTextSearchOperator( // ============================================================================ /** - * Creates a set of Stash Encryption-aware operators that automatically encrypt values + * Creates a set of Encryption-aware operators that automatically encrypt values * for encrypted columns before using them with Drizzle operators. * * For equality and text search operators (eq, ne, like, ilike, inArray, etc.): @@ -885,13 +895,13 @@ function createTextSearchOperator( * Values are encrypted and then use eql_v2.* functions (eql_v2.gt(), eql_v2.gte(), etc.) * which are required for ORE (Order-Revealing Encryption) comparisons. * - * @param protectClient - The Stash Encryption client instance + * @param encryptionClient - The Encryption client instance * @returns An object with all Drizzle operators wrapped for encrypted columns * * @example * ```ts * // Initialize operators - * const protectOps = createProtectOperators(protectClient) + * const protectOps = createProtectOperators(encryptionClient) * * // Equality search - automatically encrypts and uses PostgreSQL operators * const results = await db @@ -906,7 +916,7 @@ function createTextSearchOperator( * .where(await protectOps.gte(usersTable.age, 25)) * ``` */ -export function createProtectOperators(protectClient: EncryptionClient): { +export function createProtectOperators(encryptionClient: EncryptionClient): { // Comparison operators /** * Equality operator - encrypts value for encrypted columns. @@ -1073,13 +1083,14 @@ export function createProtectOperators(protectClient: EncryptionClient): { arrayContained: typeof arrayContained arrayOverlaps: typeof arrayOverlaps } { - // Create a cache for protect tables keyed by table name - const protectTableCache = new Map< + // Create a cache for encryption tables keyed by table name + const encryptionTableCache = new Map< string, EncryptedTable >() - const defaultProtectTable: EncryptedTable | undefined = - undefined + const defaultEncryptionTable: + | EncryptedTable + | undefined = undefined /** * Equality operator - encrypts value and uses regular Drizzle operator @@ -1087,17 +1098,17 @@ export function createProtectOperators(protectClient: EncryptionClient): { const protectEq = (left: SQLWrapper, right: unknown): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createComparisonOperator( 'eq', left, right, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1107,17 +1118,17 @@ export function createProtectOperators(protectClient: EncryptionClient): { const protectNe = (left: SQLWrapper, right: unknown): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createComparisonOperator( 'ne', left, right, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1127,17 +1138,17 @@ export function createProtectOperators(protectClient: EncryptionClient): { const protectGt = (left: SQLWrapper, right: unknown): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createComparisonOperator( 'gt', left, right, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1147,17 +1158,17 @@ export function createProtectOperators(protectClient: EncryptionClient): { const protectGte = (left: SQLWrapper, right: unknown): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createComparisonOperator( 'gte', left, right, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1167,17 +1178,17 @@ export function createProtectOperators(protectClient: EncryptionClient): { const protectLt = (left: SQLWrapper, right: unknown): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createComparisonOperator( 'lt', left, right, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1187,17 +1198,17 @@ export function createProtectOperators(protectClient: EncryptionClient): { const protectLte = (left: SQLWrapper, right: unknown): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createComparisonOperator( 'lte', left, right, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1211,8 +1222,8 @@ export function createProtectOperators(protectClient: EncryptionClient): { ): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createRangeOperator( 'between', @@ -1220,9 +1231,9 @@ export function createProtectOperators(protectClient: EncryptionClient): { min, max, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1236,8 +1247,8 @@ export function createProtectOperators(protectClient: EncryptionClient): { ): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createRangeOperator( 'notBetween', @@ -1245,9 +1256,9 @@ export function createProtectOperators(protectClient: EncryptionClient): { min, max, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1260,17 +1271,17 @@ export function createProtectOperators(protectClient: EncryptionClient): { ): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createTextSearchOperator( 'like', left, right, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1283,17 +1294,17 @@ export function createProtectOperators(protectClient: EncryptionClient): { ): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createTextSearchOperator( 'ilike', left, right, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1306,17 +1317,17 @@ export function createProtectOperators(protectClient: EncryptionClient): { ): Promise | SQL => { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) return createTextSearchOperator( 'notIlike', left, right, columnInfo, - protectClient, - defaultProtectTable, - protectTableCache, + encryptionClient, + defaultEncryptionTable, + encryptionTableCache, ) } @@ -1334,8 +1345,8 @@ export function createProtectOperators(protectClient: EncryptionClient): { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) if (!columnInfo.config?.equality || !Array.isArray(right)) { @@ -1344,14 +1355,14 @@ export function createProtectOperators(protectClient: EncryptionClient): { // Encrypt all values in the array in a single batch const encryptedValues = await encryptValues( - protectClient, + encryptionClient, right.map((value) => ({ value, column: left, queryType: 'equality' as const, })), - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) // Use regular eq for each encrypted value - PostgreSQL operators handle it @@ -1384,8 +1395,8 @@ export function createProtectOperators(protectClient: EncryptionClient): { const columnInfo = getColumnInfo( left, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) if (!columnInfo.config?.equality || !Array.isArray(right)) { @@ -1394,14 +1405,14 @@ export function createProtectOperators(protectClient: EncryptionClient): { // Encrypt all values in the array in a single batch const encryptedValues = await encryptValues( - protectClient, + encryptionClient, right.map((value) => ({ value, column: left, queryType: 'equality' as const, })), - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) // Use regular ne for each encrypted value - PostgreSQL operators handle it @@ -1423,8 +1434,8 @@ export function createProtectOperators(protectClient: EncryptionClient): { const protectAsc = (column: SQLWrapper): SQL => { const columnInfo = getColumnInfo( column, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) if (columnInfo.config?.orderAndRange) { @@ -1440,8 +1451,8 @@ export function createProtectOperators(protectClient: EncryptionClient): { const protectDesc = (column: SQLWrapper): SQL => { const columnInfo = getColumnInfo( column, - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) if (columnInfo.config?.orderAndRange) { @@ -1537,14 +1548,14 @@ export function createProtectOperators(protectClient: EncryptionClient): { // Batch encrypt all values const encryptedResults = await encryptValues( - protectClient, + encryptionClient, valuesToEncrypt.map((v) => ({ value: v.value, column: v.column, queryType: v.queryType, })), - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) // Group encrypted values by lazy operator index @@ -1696,14 +1707,14 @@ export function createProtectOperators(protectClient: EncryptionClient): { } const encryptedResults = await encryptValues( - protectClient, + encryptionClient, valuesToEncrypt.map((v) => ({ value: v.value, column: v.column, queryType: v.queryType, })), - defaultProtectTable, - protectTableCache, + defaultEncryptionTable, + encryptionTableCache, ) const encryptedByLazyOp = new Map< diff --git a/packages/drizzle/src/pg/schema-extraction.ts b/packages/drizzle/src/pg/schema-extraction.ts index 00dcd511..5b817532 100644 --- a/packages/drizzle/src/pg/schema-extraction.ts +++ b/packages/drizzle/src/pg/schema-extraction.ts @@ -21,8 +21,8 @@ import { getEncryptedColumnConfig } from './index.js' * age: encryptedType('age', { dataType: 'number', orderAndRange: true }), * }) * - * const protectSchema = extractProtectSchema(drizzleUsersTable) - * const encryptionClient = await Encryption({ schemas: [protectSchema.build()] }) + * const encryptionSchema = extractProtectSchema(drizzleUsersTable) + * const encryptionClient = await Encryption({ schemas: [encryptionSchema.build()] }) * ``` */ // We use any for the PgTable generic because we need to access Drizzle's internal properties diff --git a/packages/dynamodb/README.md b/packages/dynamodb/README.md index cf154749..9318abc3 100644 --- a/packages/dynamodb/README.md +++ b/packages/dynamodb/README.md @@ -32,13 +32,13 @@ const users = encryptedTable('users', { }) // Initialize the Encryption client -const protectClient = await Encryption({ +const encryptionClient = await Encryption({ schemas: [users], }) // Create the DynamoDB helper instance const protectDynamo = protectDynamoDB({ - protectClient, + encryptionClient, }) // Encrypt and store a user diff --git a/packages/dynamodb/__tests__/audit.test.ts b/packages/dynamodb/__tests__/audit.test.ts index 9b7fb1f1..4ef88e8b 100644 --- a/packages/dynamodb/__tests__/audit.test.ts +++ b/packages/dynamodb/__tests__/audit.test.ts @@ -5,8 +5,9 @@ import { encryptedTable, encryptedValue, } from '@cipherstash/stack' +import type { EncryptionClient } from '@cipherstash/stack' import { beforeAll, describe, expect, it } from 'vitest' -import { protectDynamoDB } from '../src' +import { encryptedDynamoDB } from '../src' const schema = encryptedTable('dynamo_cipherstash_test', { email: encryptedColumn('email').equality(), @@ -21,17 +22,17 @@ const schema = encryptedTable('dynamo_cipherstash_test', { }, }) -describe('protect dynamodb helpers', () => { - let protectClient: Awaited> - let protectDynamo: ReturnType +describe('dynamodb helpers', () => { + let encryptionClient: EncryptionClient + let protectDynamo: ReturnType beforeAll(async () => { - protectClient = await Encryption({ + encryptionClient = await Encryption({ schemas: [schema], }) - protectDynamo = protectDynamoDB({ - protectClient, + protectDynamo = encryptedDynamoDB({ + encryptionClient, }) }) diff --git a/packages/dynamodb/__tests__/dynamodb.test.ts b/packages/dynamodb/__tests__/dynamodb.test.ts index 886b5b38..c01ab673 100644 --- a/packages/dynamodb/__tests__/dynamodb.test.ts +++ b/packages/dynamodb/__tests__/dynamodb.test.ts @@ -5,8 +5,9 @@ import { encryptedTable, encryptedValue, } from '@cipherstash/stack' +import type { EncryptionClient } from '@cipherstash/stack' import { beforeAll, describe, expect, it } from 'vitest' -import { protectDynamoDB } from '../src' +import { encryptedDynamoDB } from '../src' const schema = encryptedTable('dynamo_cipherstash_test', { email: encryptedColumn('email').equality(), @@ -27,17 +28,17 @@ const schema = encryptedTable('dynamo_cipherstash_test', { }, }) -describe('protect dynamodb helpers', () => { - let protectClient: Awaited> - let protectDynamo: ReturnType +describe('dynamodb helpers', () => { + let encryptionClient: EncryptionClient + let protectDynamo: ReturnType beforeAll(async () => { - protectClient = await Encryption({ + encryptionClient = await Encryption({ schemas: [schema], }) - protectDynamo = protectDynamoDB({ - protectClient, + protectDynamo = encryptedDynamoDB({ + encryptionClient, }) }) diff --git a/packages/dynamodb/__tests__/error-codes.test.ts b/packages/dynamodb/__tests__/error-codes.test.ts index 3b03462e..7c16e435 100644 --- a/packages/dynamodb/__tests__/error-codes.test.ts +++ b/packages/dynamodb/__tests__/error-codes.test.ts @@ -7,14 +7,14 @@ import { } from '@cipherstash/stack' import type { EncryptionClient } from '@cipherstash/stack' import { beforeAll, describe, expect, it } from 'vitest' -import { protectDynamoDB } from '../src' -import type { ProtectDynamoDBError } from '../src/types' +import { encryptedDynamoDB } from '../src' +import type { EncryptedDynamoDBError } from '../src/types' const FFI_TEST_TIMEOUT = 30_000 -describe('ProtectDynamoDB Error Code Preservation', () => { - let protectClient: EncryptionClient - let protectDynamo: ReturnType +describe('EncryptedDynamoDB Error Code Preservation', () => { + let encryptionClient: EncryptionClient + let protectDynamo: ReturnType const testSchema = encryptedTable('test_table', { email: encryptedColumn('email').equality(), @@ -25,8 +25,8 @@ describe('ProtectDynamoDB Error Code Preservation', () => { }) beforeAll(async () => { - protectClient = await Encryption({ schemas: [testSchema] }) - protectDynamo = protectDynamoDB({ protectClient }) + encryptionClient = await Encryption({ schemas: [testSchema] }) + protectDynamo = encryptedDynamoDB({ encryptionClient }) }) describe('handleError FFI error code extraction', () => { @@ -49,7 +49,7 @@ describe('ProtectDynamoDB Error Code Preservation', () => { const result = await protectDynamo.encryptModel(model, badSchema) expect(result.failure).toBeDefined() - expect((result.failure as ProtectDynamoDBError).code).toBe( + expect((result.failure as EncryptedDynamoDBError).code).toBe( 'UNKNOWN_COLUMN', ) }, @@ -59,7 +59,7 @@ describe('ProtectDynamoDB Error Code Preservation', () => { describe('decryptModel error codes', () => { it( - 'uses PROTECT_DYNAMODB_ERROR for IO/parsing errors without FFI codes', + 'uses DYNAMODB_ENCRYPTION_ERROR for IO/parsing errors without FFI codes', async () => { // Malformed ciphertext causes IO/parsing errors that don't have FFI error codes const malformedItem = { @@ -73,8 +73,8 @@ describe('ProtectDynamoDB Error Code Preservation', () => { expect(result.failure).toBeDefined() // FFI returns undefined code for IO/parsing errors, so we fall back to generic code - expect((result.failure as ProtectDynamoDBError).code).toBe( - 'PROTECT_DYNAMODB_ERROR', + expect((result.failure as EncryptedDynamoDBError).code).toBe( + 'DYNAMODB_ENCRYPTION_ERROR', ) }, FFI_TEST_TIMEOUT, @@ -90,7 +90,7 @@ describe('ProtectDynamoDB Error Code Preservation', () => { const result = await protectDynamo.bulkEncryptModels(models, badSchema) expect(result.failure).toBeDefined() - expect((result.failure as ProtectDynamoDBError).code).toBe( + expect((result.failure as EncryptedDynamoDBError).code).toBe( 'UNKNOWN_COLUMN', ) }, @@ -100,7 +100,7 @@ describe('ProtectDynamoDB Error Code Preservation', () => { describe('bulkDecryptModels error codes', () => { it( - 'uses PROTECT_DYNAMODB_ERROR for IO/parsing errors without FFI codes', + 'uses DYNAMODB_ENCRYPTION_ERROR for IO/parsing errors without FFI codes', async () => { // Malformed ciphertext causes IO/parsing errors that don't have FFI error codes const malformedItems = [ @@ -115,8 +115,8 @@ describe('ProtectDynamoDB Error Code Preservation', () => { expect(result.failure).toBeDefined() // FFI returns undefined code for IO/parsing errors, so we fall back to generic code - expect((result.failure as ProtectDynamoDBError).code).toBe( - 'PROTECT_DYNAMODB_ERROR', + expect((result.failure as EncryptedDynamoDBError).code).toBe( + 'DYNAMODB_ENCRYPTION_ERROR', ) }, FFI_TEST_TIMEOUT, diff --git a/packages/dynamodb/src/helpers.ts b/packages/dynamodb/src/helpers.ts index ffd49320..122798d6 100644 --- a/packages/dynamodb/src/helpers.ts +++ b/packages/dynamodb/src/helpers.ts @@ -5,21 +5,21 @@ import type { ProtectErrorCode, } from '@cipherstash/stack' import { FfiProtectError } from '@cipherstash/stack' -import type { ProtectDynamoDBError } from './types' +import type { EncryptedDynamoDBError } from './types' export const ciphertextAttrSuffix = '__source' export const searchTermAttrSuffix = '__hmac' -export class ProtectDynamoDBErrorImpl +export class EncryptedDynamoDBErrorImpl extends Error - implements ProtectDynamoDBError + implements EncryptedDynamoDBError { constructor( message: string, - public code: ProtectErrorCode | 'PROTECT_DYNAMODB_ERROR', + public code: ProtectErrorCode | 'DYNAMODB_ENCRYPTION_ERROR', public details?: Record, ) { super(message) - this.name = 'ProtectDynamoDBError' + this.name = 'EncryptedDynamoDBError' } } @@ -30,11 +30,11 @@ export function handleError( logger?: { error: (message: string, error: Error) => void } - errorHandler?: (error: ProtectDynamoDBError) => void + errorHandler?: (error: EncryptedDynamoDBError) => void }, -): ProtectDynamoDBError { +): EncryptedDynamoDBError { // Preserve FFI error code if available, otherwise use generic DynamoDB error code - // Check for FfiProtectError instance or plain ProtectError objects with code property + // Check for FfiProtectError instance or plain error objects with code property const errorObj = error as Record const errorCode = error instanceof FfiProtectError @@ -44,7 +44,7 @@ export function handleError( 'code' in errorObj && typeof errorObj.code === 'string' ? (errorObj.code as ProtectErrorCode) - : 'PROTECT_DYNAMODB_ERROR' + : 'DYNAMODB_ENCRYPTION_ERROR' const errorMessage = error instanceof Error @@ -53,19 +53,19 @@ export function handleError( ? errorObj.message : String(error) - const protectError = new ProtectDynamoDBErrorImpl(errorMessage, errorCode, { + const dynamoError = new EncryptedDynamoDBErrorImpl(errorMessage, errorCode, { context, }) if (options?.errorHandler) { - options.errorHandler(protectError) + options.errorHandler(dynamoError) } if (options?.logger) { - options.logger.error(`Error in ${context}`, protectError) + options.logger.error(`Error in ${context}`, dynamoError) } - return protectError + return dynamoError } export function deepClone(obj: T): T { diff --git a/packages/dynamodb/src/index.ts b/packages/dynamodb/src/index.ts index b5c75cca..209c22ba 100644 --- a/packages/dynamodb/src/index.ts +++ b/packages/dynamodb/src/index.ts @@ -9,12 +9,15 @@ import { BulkEncryptModelsOperation } from './operations/bulk-encrypt-models' import { DecryptModelOperation } from './operations/decrypt-model' import { EncryptModelOperation } from './operations/encrypt-model' import { SearchTermsOperation } from './operations/search-terms' -import type { ProtectDynamoDBConfig, ProtectDynamoDBInstance } from './types' +import type { + EncryptedDynamoDBConfig, + EncryptedDynamoDBInstance, +} from './types' -export function protectDynamoDB( - config: ProtectDynamoDBConfig, -): ProtectDynamoDBInstance { - const { protectClient, options } = config +export function encryptedDynamoDB( + config: EncryptedDynamoDBConfig, +): EncryptedDynamoDBInstance { + const { encryptionClient, options } = config return { encryptModel>( @@ -22,7 +25,7 @@ export function protectDynamoDB( protectTable: EncryptedTable, ) { return new EncryptModelOperation( - protectClient, + encryptionClient, item, protectTable, options, @@ -34,7 +37,7 @@ export function protectDynamoDB( protectTable: EncryptedTable, ) { return new BulkEncryptModelsOperation( - protectClient, + encryptionClient, items, protectTable, options, @@ -46,7 +49,7 @@ export function protectDynamoDB( protectTable: EncryptedTable, ) { return new DecryptModelOperation( - protectClient, + encryptionClient, item, protectTable, options, @@ -58,7 +61,7 @@ export function protectDynamoDB( protectTable: EncryptedTable, ) { return new BulkDecryptModelsOperation( - protectClient, + encryptionClient, items, protectTable, options, @@ -66,12 +69,15 @@ export function protectDynamoDB( }, /** - * @deprecated Use `protectClient.encryptQuery(terms)` instead and extract the `hm` field for DynamoDB key lookups. + * @deprecated Use `encryptionClient.encryptQuery(terms)` instead and extract the `hm` field for DynamoDB key lookups. */ createSearchTerms(terms: SearchTerm[]) { - return new SearchTermsOperation(protectClient, terms, options) + return new SearchTermsOperation(encryptionClient, terms, options) }, } } +/** @deprecated Use `encryptedDynamoDB` instead. */ +export { encryptedDynamoDB as protectDynamoDB } + export * from './types' diff --git a/packages/dynamodb/src/operations/bulk-decrypt-models.ts b/packages/dynamodb/src/operations/bulk-decrypt-models.ts index 9afe5b21..e416076a 100644 --- a/packages/dynamodb/src/operations/bulk-decrypt-models.ts +++ b/packages/dynamodb/src/operations/bulk-decrypt-models.ts @@ -7,7 +7,7 @@ import type { EncryptionClient, } from '@cipherstash/stack' import { handleError, toItemWithEqlPayloads } from '../helpers' -import type { ProtectDynamoDBError } from '../types' +import type { EncryptedDynamoDBError } from '../types' import { DynamoDBOperation, type DynamoDBOperationOptions, @@ -16,24 +16,24 @@ import { export class BulkDecryptModelsOperation< T extends Record, > extends DynamoDBOperation[]> { - private protectClient: EncryptionClient + private encryptionClient: EncryptionClient private items: Record[] private protectTable: EncryptedTable constructor( - protectClient: EncryptionClient, + encryptionClient: EncryptionClient, items: Record[], protectTable: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) - this.protectClient = protectClient + this.encryptionClient = encryptionClient this.items = items this.protectTable = protectTable } public async execute(): Promise< - Result[], ProtectDynamoDBError> + Result[], EncryptedDynamoDBError> > { return await withResult( async () => { @@ -41,7 +41,7 @@ export class BulkDecryptModelsOperation< toItemWithEqlPayloads(item, this.protectTable), ) - const decryptResult = await this.protectClient + const decryptResult = await this.encryptionClient .bulkDecryptModels(itemsWithEqlPayloads as T[]) .audit(this.getAuditData()) diff --git a/packages/dynamodb/src/operations/bulk-encrypt-models.ts b/packages/dynamodb/src/operations/bulk-encrypt-models.ts index 7a233e4e..f2e77f4a 100644 --- a/packages/dynamodb/src/operations/bulk-encrypt-models.ts +++ b/packages/dynamodb/src/operations/bulk-encrypt-models.ts @@ -5,7 +5,7 @@ import type { EncryptionClient, } from '@cipherstash/stack' import { deepClone, handleError, toEncryptedDynamoItem } from '../helpers' -import type { ProtectDynamoDBError } from '../types' +import type { EncryptedDynamoDBError } from '../types' import { DynamoDBOperation, type DynamoDBOperationOptions, @@ -14,26 +14,26 @@ import { export class BulkEncryptModelsOperation< T extends Record, > extends DynamoDBOperation { - private protectClient: EncryptionClient + private encryptionClient: EncryptionClient private items: T[] private protectTable: EncryptedTable constructor( - protectClient: EncryptionClient, + encryptionClient: EncryptionClient, items: T[], protectTable: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) - this.protectClient = protectClient + this.encryptionClient = encryptionClient this.items = items this.protectTable = protectTable } - public async execute(): Promise> { + public async execute(): Promise> { return await withResult( async () => { - const encryptResult = await this.protectClient + const encryptResult = await this.encryptionClient .bulkEncryptModels( this.items.map((item) => deepClone(item)), this.protectTable, diff --git a/packages/dynamodb/src/operations/decrypt-model.ts b/packages/dynamodb/src/operations/decrypt-model.ts index 0268a7fc..2f0f0065 100644 --- a/packages/dynamodb/src/operations/decrypt-model.ts +++ b/packages/dynamodb/src/operations/decrypt-model.ts @@ -7,7 +7,7 @@ import type { EncryptionClient, } from '@cipherstash/stack' import { handleError, toItemWithEqlPayloads } from '../helpers' -import type { ProtectDynamoDBError } from '../types' +import type { EncryptedDynamoDBError } from '../types' import { DynamoDBOperation, type DynamoDBOperationOptions, @@ -16,23 +16,25 @@ import { export class DecryptModelOperation< T extends Record, > extends DynamoDBOperation> { - private protectClient: EncryptionClient + private encryptionClient: EncryptionClient private item: Record private protectTable: EncryptedTable constructor( - protectClient: EncryptionClient, + encryptionClient: EncryptionClient, item: Record, protectTable: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) - this.protectClient = protectClient + this.encryptionClient = encryptionClient this.item = item this.protectTable = protectTable } - public async execute(): Promise, ProtectDynamoDBError>> { + public async execute(): Promise< + Result, EncryptedDynamoDBError> + > { return await withResult( async () => { const withEqlPayloads = toItemWithEqlPayloads( @@ -40,7 +42,7 @@ export class DecryptModelOperation< this.protectTable, ) - const decryptResult = await this.protectClient + const decryptResult = await this.encryptionClient .decryptModel(withEqlPayloads as T) .audit(this.getAuditData()) diff --git a/packages/dynamodb/src/operations/encrypt-model.ts b/packages/dynamodb/src/operations/encrypt-model.ts index 80b27cef..f302e535 100644 --- a/packages/dynamodb/src/operations/encrypt-model.ts +++ b/packages/dynamodb/src/operations/encrypt-model.ts @@ -5,7 +5,7 @@ import type { EncryptionClient, } from '@cipherstash/stack' import { deepClone, handleError, toEncryptedDynamoItem } from '../helpers' -import type { ProtectDynamoDBError } from '../types' +import type { EncryptedDynamoDBError } from '../types' import { DynamoDBOperation, type DynamoDBOperationOptions, @@ -14,26 +14,26 @@ import { export class EncryptModelOperation< T extends Record, > extends DynamoDBOperation { - private protectClient: EncryptionClient + private encryptionClient: EncryptionClient private item: T private protectTable: EncryptedTable constructor( - protectClient: EncryptionClient, + encryptionClient: EncryptionClient, item: T, protectTable: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) - this.protectClient = protectClient + this.encryptionClient = encryptionClient this.item = item this.protectTable = protectTable } - public async execute(): Promise> { + public async execute(): Promise> { return await withResult( async () => { - const encryptResult = await this.protectClient + const encryptResult = await this.encryptionClient .encryptModel(deepClone(this.item), this.protectTable) .audit(this.getAuditData()) diff --git a/packages/dynamodb/src/operations/search-terms.ts b/packages/dynamodb/src/operations/search-terms.ts index afa6ac50..4f28bba2 100644 --- a/packages/dynamodb/src/operations/search-terms.ts +++ b/packages/dynamodb/src/operations/search-terms.ts @@ -1,14 +1,14 @@ import { type Result, withResult } from '@byteslice/result' import type { EncryptionClient, SearchTerm } from '@cipherstash/stack' import { handleError } from '../helpers' -import type { ProtectDynamoDBError } from '../types' +import type { EncryptedDynamoDBError } from '../types' import { DynamoDBOperation, type DynamoDBOperationOptions, } from './base-operation' /** - * @deprecated Use `protectClient.encryptQuery(terms)` instead and extract the `hm` field for DynamoDB key lookups. + * @deprecated Use `encryptionClient.encryptQuery(terms)` instead and extract the `hm` field for DynamoDB key lookups. * * @example * ```typescript @@ -17,33 +17,33 @@ import { * const hmac = result.data[0] * * // After (new API) - * const [encrypted] = await protectClient.encryptQuery([{ value, column, table, queryType: 'equality' }]) + * const [encrypted] = await encryptionClient.encryptQuery([{ value, column, table, queryType: 'equality' }]) * const hmac = encrypted.hm * ``` */ export class SearchTermsOperation extends DynamoDBOperation { - private protectClient: EncryptionClient + private encryptionClient: EncryptionClient private terms: SearchTerm[] constructor( - protectClient: EncryptionClient, + encryptionClient: EncryptionClient, terms: SearchTerm[], options?: DynamoDBOperationOptions, ) { super(options) - this.protectClient = protectClient + this.encryptionClient = encryptionClient this.terms = terms } - public async execute(): Promise> { + public async execute(): Promise> { return await withResult( async () => { - const searchTermsResult = await this.protectClient + const searchTermsResult = await this.encryptionClient .createSearchTerms(this.terms) .audit(this.getAuditData()) if (searchTermsResult.failure) { - throw new Error(`[protect]: ${searchTermsResult.failure.message}`) + throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) } return searchTermsResult.data.map((term) => { diff --git a/packages/dynamodb/src/types.ts b/packages/dynamodb/src/types.ts index 9e881362..5e8a4e97 100644 --- a/packages/dynamodb/src/types.ts +++ b/packages/dynamodb/src/types.ts @@ -12,22 +12,22 @@ import type { DecryptModelOperation } from './operations/decrypt-model' import type { EncryptModelOperation } from './operations/encrypt-model' import type { SearchTermsOperation } from './operations/search-terms' -export interface ProtectDynamoDBConfig { - protectClient: EncryptionClient +export interface EncryptedDynamoDBConfig { + encryptionClient: EncryptionClient options?: { logger?: { error: (message: string, error: Error) => void } - errorHandler?: (error: ProtectDynamoDBError) => void + errorHandler?: (error: EncryptedDynamoDBError) => void } } -export interface ProtectDynamoDBError extends Error { - code: ProtectErrorCode | 'PROTECT_DYNAMODB_ERROR' +export interface EncryptedDynamoDBError extends Error { + code: ProtectErrorCode | 'DYNAMODB_ENCRYPTION_ERROR' details?: Record } -export interface ProtectDynamoDBInstance { +export interface EncryptedDynamoDBInstance { encryptModel>( item: T, protectTable: EncryptedTable, @@ -49,7 +49,7 @@ export interface ProtectDynamoDBInstance { ): BulkDecryptModelsOperation /** - * @deprecated Use `protectClient.encryptQuery(terms)` instead and extract the `hm` field for DynamoDB key lookups. + * @deprecated Use `encryptionClient.encryptQuery(terms)` instead and extract the `hm` field for DynamoDB key lookups. * * @example * ```typescript @@ -58,9 +58,18 @@ export interface ProtectDynamoDBInstance { * const hmac = result.data[0] * * // After (new API) - * const [encrypted] = await protectClient.encryptQuery([{ value, column, table, queryType: 'equality' }]) + * const [encrypted] = await encryptionClient.encryptQuery([{ value, column, table, queryType: 'equality' }]) * const hmac = encrypted.hm * ``` */ createSearchTerms(terms: SearchTerm[]): SearchTermsOperation } + +/** @deprecated Use `EncryptedDynamoDBConfig` instead. */ +export type ProtectDynamoDBConfig = EncryptedDynamoDBConfig +/** @deprecated Use `EncryptedDynamoDBError` instead. */ +export type ProtectDynamoDBError = EncryptedDynamoDBError +/** @deprecated Use `EncryptedDynamoDBInstance` instead. */ +export type ProtectDynamoDBInstance = EncryptedDynamoDBInstance +/** @deprecated Use `'DYNAMODB_ENCRYPTION_ERROR'` instead. */ +export const PROTECT_DYNAMODB_ERROR = 'DYNAMODB_ENCRYPTION_ERROR' as const diff --git a/packages/stack/README.md b/packages/stack/README.md index 2f876cba..a7bf8b62 100644 --- a/packages/stack/README.md +++ b/packages/stack/README.md @@ -264,23 +264,23 @@ const config: EncryptionClientConfig = { } // Pass all your tables to the Encryption function to initialize the client -export const protectClient = await Encryption(config); +export const encryptionClient = await Encryption(config); ``` The `Encryption` function requires at least one `encryptedTable` be provided in the `schemas` array. ### Encrypt data -Stash Encryption provides the `encrypt` function on `protectClient` to encrypt data. +Stash Encryption provides the `encrypt` function on `encryptionClient` to encrypt data. `encrypt` takes a plaintext string, and an object with the table and column as parameters. To start encrypting data, add the following to `src/index.ts`: ```typescript import { users } from "./protect/schema"; -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; -const encryptResult = await protectClient.encrypt("secret@squirrel.example", { +const encryptResult = await encryptionClient.encrypt("secret@squirrel.example", { column: users.email, table: users, }); @@ -317,16 +317,16 @@ The `encryptResult` will return one of the following: ### Decrypt data -Stash Encryption provides the `decrypt` function on `protectClient` to decrypt data. +Stash Encryption provides the `decrypt` function on `encryptionClient` to decrypt data. `decrypt` takes an encrypted data object as a parameter. To start decrypting data, add the following to `src/index.ts`: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; // encryptResult is the EQL payload from the previous step -const decryptResult = await protectClient.decrypt(encryptResult.data); +const decryptResult = await encryptionClient.decrypt(encryptResult.data); if (decryptResult.failure) { // Handle the failure @@ -376,7 +376,7 @@ If you are working with a large data set, the model operations are significantly Use the `encryptModel` method to encrypt a model's fields that are defined in your schema: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; import { users } from "./protect/schema"; // Your model with plaintext values @@ -387,7 +387,7 @@ const user = { createdAt: new Date("2024-01-01"), }; -const encryptedResult = await protectClient.encryptModel(user, users); +const encryptedResult = await encryptionClient.encryptModel(user, users); if (encryptedResult.failure) { // Handle the failure @@ -411,7 +411,7 @@ Stash Encryption provides strong TypeScript support for model operations. You can specify your model's type to ensure end-to-end type safety: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; import { users } from "./protect/schema"; // Define your model type @@ -430,7 +430,7 @@ type User = { }; // The encryptModel method will ensure type safety -const encryptedResult = await protectClient.encryptModel(user, users); +const encryptedResult = await encryptionClient.encryptModel(user, users); if (encryptedResult.failure) { // Handle the failure @@ -441,7 +441,7 @@ const encryptedUser = encryptedResult.data; // but with encrypted fields for those defined in the schema // Decryption maintains type safety -const decryptedResult = await protectClient.decryptModel(encryptedUser); +const decryptedResult = await encryptionClient.decryptModel(encryptedUser); if (decryptedResult.failure) { // Handle the failure @@ -451,12 +451,12 @@ const decryptedUser = decryptedResult.data; // decryptedUser is fully typed as User // Bulk operations also support type safety -const bulkEncryptedResult = await protectClient.bulkEncryptModels( +const bulkEncryptedResult = await encryptionClient.bulkEncryptModels( userModels, users ); -const bulkDecryptedResult = await protectClient.bulkDecryptModels( +const bulkDecryptedResult = await encryptionClient.bulkDecryptModels( bulkEncryptedResult.data ); ``` @@ -478,7 +478,7 @@ This type safety helps catch potential issues at compile time and provides bette Example with Drizzle infered types: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; import { jsonb, pgTable, serial, InferSelectModel } from "drizzle-orm/pg-core"; import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; @@ -499,7 +499,7 @@ const user = { }; // Drizzle User type works directly with model operations -const encryptedResult = await protectClient.encryptModel( +const encryptedResult = await encryptionClient.encryptModel( user, protectUsers ); @@ -510,9 +510,9 @@ const encryptedResult = await protectClient.encryptModel( Use the `decryptModel` method to decrypt a model's encrypted fields: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; -const decryptedResult = await protectClient.decryptModel(encryptedUser); +const decryptedResult = await encryptionClient.decryptModel(encryptedUser); if (decryptedResult.failure) { // Handle the failure @@ -532,7 +532,7 @@ console.log("decrypted user:", decryptedUser); For better performance when working with multiple models, use the `bulkEncryptModels` and `bulkDecryptModels` methods: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; import { users } from "./protect/schema"; // Array of models with plaintext values @@ -550,7 +550,7 @@ const userModels = [ ]; // Encrypt multiple models at once -const encryptedResult = await protectClient.bulkEncryptModels( +const encryptedResult = await encryptionClient.bulkEncryptModels( userModels, users ); @@ -562,7 +562,7 @@ if (encryptedResult.failure) { const encryptedUsers = encryptedResult.data; // Decrypt multiple models at once -const decryptedResult = await protectClient.bulkDecryptModels(encryptedUsers); +const decryptedResult = await encryptionClient.bulkDecryptModels(encryptedUsers); if (decryptedResult.failure) { // Handle the failure @@ -586,7 +586,7 @@ Stash Encryption provides direct access to ZeroKMS bulk operations through the ` Use the `bulkEncrypt` method to encrypt multiple plaintext values at once: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; import { users } from "./protect/schema"; // Array of plaintext values with optional IDs for correlation @@ -596,7 +596,7 @@ const plaintexts = [ { id: "user3", plaintext: "charlie@example.com" }, ]; -const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { +const encryptedResult = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }); @@ -633,7 +633,7 @@ const plaintexts = [ { plaintext: "charlie@example.com" }, ]; -const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { +const encryptedResult = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }); @@ -644,10 +644,10 @@ const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { Use the `bulkDecrypt` method to decrypt multiple encrypted values at once: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; // encryptedData is the result from bulkEncrypt -const decryptedResult = await protectClient.bulkDecrypt(encryptedData); +const decryptedResult = await encryptionClient.bulkDecrypt(encryptedData); if (decryptedResult.failure) { // Handle the failure @@ -696,7 +696,7 @@ The `bulkDecrypt` method returns a `Result` object that represents the overall o You can handle mixed results by checking each item: ```typescript -const decryptedResult = await protectClient.bulkDecrypt(encryptedData); +const decryptedResult = await encryptionClient.bulkDecrypt(encryptedData); if (decryptedResult.failure) { // Handle overall operation failure @@ -727,7 +727,7 @@ const plaintexts = [ { id: "user3", plaintext: "charlie@example.com" }, ]; -const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { +const encryptedResult = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }); @@ -735,7 +735,7 @@ const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { // Null values are preserved in the encrypted result // encryptedResult.data[1].data will be null -const decryptedResult = await protectClient.bulkDecrypt(encryptedResult.data); +const decryptedResult = await encryptionClient.bulkDecrypt(encryptedResult.data); // Null values are preserved in the decrypted result // decryptedResult.data[1].data will be null @@ -761,7 +761,7 @@ const plaintexts = [ ]; // Encrypt with lock context -const encryptedResult = await protectClient +const encryptedResult = await encryptionClient .bulkEncrypt(plaintexts, { column: users.email, table: users, @@ -769,7 +769,7 @@ const encryptedResult = await protectClient .withLockContext(lockContext.data); // Decrypt with lock context -const decryptedResult = await protectClient +const decryptedResult = await encryptionClient .bulkDecrypt(encryptedResult.data) .withLockContext(lockContext.data); ``` @@ -786,13 +786,13 @@ const plaintexts = Array.from({ length: 1000 }, (_, i) => ({ })); // Single call to ZeroKMS for all 1000 values -const encryptedResult = await protectClient.bulkEncrypt(plaintexts, { +const encryptedResult = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }); // Single call to ZeroKMS for all 1000 values -const decryptedResult = await protectClient.bulkDecrypt(encryptedResult.data); +const decryptedResult = await encryptionClient.bulkDecrypt(encryptedResult.data); ``` The bulk operations maintain the same security guarantees as individual operations - each value gets a unique key - while providing optimal performance through ZeroKMS's bulk processing capabilities. @@ -879,7 +879,7 @@ To use a lock context, initialize a `LockContext` object with the identity claim ```typescript import { LockContext } from "@cipherstash/stack/identity"; -// protectClient from the previous steps +// encryptionClient from the previous steps const lc = new LockContext(); ``` @@ -907,10 +907,10 @@ const lockContext = identifyResult.data; To encrypt data with a lock context, call the optional `withLockContext` method on the `encrypt` function and pass the lock context object as a parameter: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; import { users } from "./protect/schema"; -const encryptResult = await protectClient +const encryptResult = await encryptionClient .encrypt("plaintext", { table: users, column: users.email, @@ -929,9 +929,9 @@ console.log("EQL Payload containing ciphertexts:", encryptResult.data); To decrypt data with a lock context, call the optional `withLockContext` method on the `decrypt` function and pass the lock context object as a parameter: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; -const decryptResult = await protectClient +const decryptResult = await encryptionClient .decrypt(encryptResult.data) .withLockContext(lockContext); @@ -947,7 +947,7 @@ const plaintext = decryptResult.data; All model operations support lock contexts for identity-aware encryption: ```typescript -import { protectClient } from "./protect"; +import { encryptionClient } from "./protect"; import { users } from "./protect/schema"; const myUsers = [ @@ -965,7 +965,7 @@ const myUsers = [ ]; // Encrypt a model with lock context -const encryptedResult = await protectClient +const encryptedResult = await encryptionClient .encryptModel(myUsers[0], users) .withLockContext(lockContext); @@ -974,16 +974,16 @@ if (encryptedResult.failure) { } // Decrypt a model with lock context -const decryptedResult = await protectClient +const decryptedResult = await encryptionClient .decryptModel(encryptedResult.data) .withLockContext(lockContext); // Bulk operations also support lock contexts -const bulkEncryptedResult = await protectClient +const bulkEncryptedResult = await encryptionClient .bulkEncryptModels(myUsers, users) .withLockContext(lockContext); -const bulkDecryptedResult = await protectClient +const bulkDecryptedResult = await encryptionClient .bulkDecryptModels(bulkEncryptedResult.data) .withLockContext(lockContext); ``` @@ -1011,7 +1011,7 @@ In the [CipherStash Dashboard](https://dashboard.cipherstash.com/workspaces/_/en import { Encryption } from "@cipherstash/stack"; import { users } from "./protect/schema"; -const protectClient = await Encryption({ +const encryptionClient = await Encryption({ schemas: [users], keyset: { // Must be a valid UUID which can be found in the CipherStash Dashboard @@ -1021,7 +1021,7 @@ const protectClient = await Encryption({ // or with a keyset name -const protectClient = await Encryption({ +const encryptionClient = await Encryption({ schemas: [users], keyset: { name: 'Company A' diff --git a/packages/stack/__tests__/audit.test.ts b/packages/stack/__tests__/audit.test.ts index 818bf0bf..1b4c8027 100644 --- a/packages/stack/__tests__/audit.test.ts +++ b/packages/stack/__tests__/audit.test.ts @@ -19,10 +19,10 @@ type User = { number?: number } -let protectClient: EncryptionClient +let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ + encryptionClient = await Encryption({ schemas: [users], }) }) @@ -31,7 +31,7 @@ describe('encryption and decryption with audit', () => { it('should encrypt and decrypt a payload with audit metadata', async () => { const email = 'very_secret_data' - const ciphertext = await protectClient + const ciphertext = await encryptionClient .encrypt(email, { column: users.auditable, table: users, @@ -50,7 +50,7 @@ describe('encryption and decryption with audit', () => { // Verify encrypted field expect(ciphertext.data).toHaveProperty('c') - const plaintext = await protectClient.decrypt(ciphertext.data).audit({ + const plaintext = await encryptionClient.decrypt(ciphertext.data).audit({ metadata: { sub: 'cj@cjb.io', type: 'decrypt', @@ -75,7 +75,7 @@ describe('encryption and decryption with audit', () => { } // Encrypt the model with audit - const encryptedModel = await protectClient + const encryptedModel = await encryptionClient .encryptModel(decryptedModel, users) .audit({ metadata: { @@ -100,7 +100,7 @@ describe('encryption and decryption with audit', () => { expect(encryptedModel.data.number).toBe(1) // Decrypt the model with audit - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .decryptModel(encryptedModel.data) .audit({ metadata: { @@ -129,7 +129,7 @@ describe('encryption and decryption with audit', () => { } // Encrypt the model with audit - const encryptedModel = await protectClient + const encryptedModel = await encryptionClient .encryptModel(decryptedModel, users) .audit({ metadata: { @@ -154,7 +154,7 @@ describe('encryption and decryption with audit', () => { expect(encryptedModel.data.number).toBe(1) // Decrypt the model with audit - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .decryptModel(encryptedModel.data) .audit({ metadata: { @@ -196,7 +196,7 @@ describe('bulk encryption with audit', () => { ] // Encrypt the models with audit - const encryptedModels = await protectClient + const encryptedModels = await encryptionClient .bulkEncryptModels(decryptedModels, users) .audit({ metadata: { @@ -228,7 +228,7 @@ describe('bulk encryption with audit', () => { expect(encryptedModels.data[1].number).toBe(2) // Decrypt the models with audit - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .bulkDecryptModels(encryptedModels.data) .audit({ metadata: { @@ -276,7 +276,7 @@ describe('bulk encryption with audit', () => { ] // Encrypt the models with audit - const encryptedModels = await protectClient + const encryptedModels = await encryptionClient .bulkEncryptModels(decryptedModels, users) .audit({ metadata: { @@ -301,7 +301,7 @@ describe('bulk encryption with audit', () => { expect(encryptedModels.data[2].auditable).toHaveProperty('c') // Decrypt the models with audit - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .bulkDecryptModels(decryptedModels) .audit({ metadata: { @@ -319,7 +319,7 @@ describe('bulk encryption with audit', () => { it('should return empty array if models is empty with audit', async () => { // Encrypt empty array of models with audit - const encryptedModels = await protectClient + const encryptedModels = await encryptionClient .bulkEncryptModels([], users) .audit({ metadata: { @@ -335,7 +335,7 @@ describe('bulk encryption with audit', () => { expect(encryptedModels.data).toEqual([]) // Decrypt empty array of models with audit - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .bulkDecryptModels([]) .audit({ metadata: { @@ -376,7 +376,7 @@ describe('audit with lock context', () => { } // Encrypt the model with both audit and lock context - const encryptedModel = await protectClient + const encryptedModel = await encryptionClient .encryptModel(decryptedModel, users) .withLockContext(lockContext.data) .audit({ @@ -391,7 +391,7 @@ describe('audit with lock context', () => { } // Decrypt the model with both audit and lock context - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .decryptModel(encryptedModel.data) .withLockContext(lockContext.data) .audit({ @@ -438,7 +438,7 @@ describe('audit with lock context', () => { ] // Encrypt the models with both audit and lock context - const encryptedModels = await protectClient + const encryptedModels = await encryptionClient .bulkEncryptModels(decryptedModels, users) .withLockContext(lockContext.data) .audit({ @@ -453,7 +453,7 @@ describe('audit with lock context', () => { } // Decrypt the models with both audit and lock context - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .bulkDecryptModels(encryptedModels.data) .withLockContext(lockContext.data) .audit({ diff --git a/packages/stack/__tests__/backward-compat.test.ts b/packages/stack/__tests__/backward-compat.test.ts index 1b17a70a..5731a09a 100644 --- a/packages/stack/__tests__/backward-compat.test.ts +++ b/packages/stack/__tests__/backward-compat.test.ts @@ -8,16 +8,16 @@ const users = encryptedTable('users', { }) describe('k-field backward compatibility', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [users] }) + encryptionClient = await Encryption({ schemas: [users] }) }) it('should encrypt new data WITHOUT k field (forward compatibility)', async () => { const testData = 'test@example.com' - const result = await protectClient.encrypt(testData, { + const result = await encryptionClient.encrypt(testData, { column: users.email, table: users, }) @@ -37,7 +37,7 @@ describe('k-field backward compatibility', () => { // First encrypt some data const testData = 'legacy@example.com' - const encrypted = await protectClient.encrypt(testData, { + const encrypted = await encryptionClient.encrypt(testData, { column: users.email, table: users, }) @@ -54,7 +54,7 @@ describe('k-field backward compatibility', () => { } // Decrypt should succeed even with legacy k field present - const result = await protectClient.decrypt(legacyPayload) + const result = await encryptionClient.decrypt(legacyPayload) if (result.failure) { throw new Error(`Decryption failed: ${result.failure.message}`) diff --git a/packages/stack/__tests__/basic-protect.test.ts b/packages/stack/__tests__/basic-protect.test.ts index 6abab4e6..0549bbd2 100644 --- a/packages/stack/__tests__/basic-protect.test.ts +++ b/packages/stack/__tests__/basic-protect.test.ts @@ -9,10 +9,10 @@ const users = encryptedTable('users', { json: encryptedColumn('json').dataType('json'), }) -let protectClient: EncryptionClient +let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ + encryptionClient = await Encryption({ schemas: [users], }) }) @@ -21,7 +21,7 @@ describe('encryption and decryption', () => { it('should encrypt and decrypt a payload', async () => { const email = 'hello@example.com' - const ciphertext = await protectClient.encrypt(email, { + const ciphertext = await encryptionClient.encrypt(email, { column: users.email, table: users, }) @@ -35,7 +35,7 @@ describe('encryption and decryption', () => { const a = ciphertext.data - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: email, diff --git a/packages/stack/__tests__/bulk-protect.test.ts b/packages/stack/__tests__/bulk-protect.test.ts index ad28c429..4680c24e 100644 --- a/packages/stack/__tests__/bulk-protect.test.ts +++ b/packages/stack/__tests__/bulk-protect.test.ts @@ -22,10 +22,10 @@ type User = { number?: number } -let protectClient: EncryptionClient +let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ + encryptionClient = await Encryption({ schemas: [users], }) }) @@ -39,7 +39,7 @@ describe('bulk encryption and decryption', () => { { id: 'user3', plaintext: 'charlie@example.com' }, ] - const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + const encryptedData = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) @@ -77,7 +77,7 @@ describe('bulk encryption and decryption', () => { { plaintext: 'charlie@example.com' }, ] - const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + const encryptedData = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) @@ -106,7 +106,7 @@ describe('bulk encryption and decryption', () => { { id: 'user3', plaintext: 'charlie@example.com' }, ] - const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + const encryptedData = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) @@ -135,7 +135,7 @@ describe('bulk encryption and decryption', () => { { id: 'user3', plaintext: null }, ] - const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + const encryptedData = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) @@ -160,7 +160,7 @@ describe('bulk encryption and decryption', () => { it('should handle empty array in bulk encrypt', async () => { const plaintexts: Array<{ id?: string; plaintext: string | null }> = [] - const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + const encryptedData = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) @@ -182,7 +182,7 @@ describe('bulk encryption and decryption', () => { { id: 'user3', plaintext: 'charlie@example.com' }, ] - const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + const encryptedData = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) @@ -192,7 +192,9 @@ describe('bulk encryption and decryption', () => { } // Now decrypt the data - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt( + encryptedData.data, + ) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -219,7 +221,7 @@ describe('bulk encryption and decryption', () => { { plaintext: 'charlie@example.com' }, ] - const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + const encryptedData = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) @@ -229,7 +231,9 @@ describe('bulk encryption and decryption', () => { } // Now decrypt the data - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt( + encryptedData.data, + ) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -256,7 +260,7 @@ describe('bulk encryption and decryption', () => { { id: 'user3', plaintext: 'charlie@example.com' }, ] - const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + const encryptedData = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) @@ -266,7 +270,9 @@ describe('bulk encryption and decryption', () => { } // Now decrypt the data - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt( + encryptedData.data, + ) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -293,7 +299,7 @@ describe('bulk encryption and decryption', () => { { id: 'user3', plaintext: null }, ] - const encryptedData = await protectClient.bulkEncrypt(plaintexts, { + const encryptedData = await encryptionClient.bulkEncrypt(plaintexts, { column: users.email, table: users, }) @@ -303,7 +309,9 @@ describe('bulk encryption and decryption', () => { } // Now decrypt the data - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt( + encryptedData.data, + ) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -322,7 +330,8 @@ describe('bulk encryption and decryption', () => { it('should handle empty array in bulk decrypt', async () => { const encryptedPayloads: Array<{ id?: string; data: Encrypted }> = [] - const decryptedData = await protectClient.bulkDecrypt(encryptedPayloads) + const decryptedData = + await encryptionClient.bulkDecrypt(encryptedPayloads) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -357,7 +366,7 @@ describe('bulk encryption and decryption', () => { ] // Encrypt with lock context - const encryptedData = await protectClient + const encryptedData = await encryptionClient .bulkEncrypt(plaintexts, { column: users.email, table: users, @@ -381,7 +390,7 @@ describe('bulk encryption and decryption', () => { expect(encryptedData.data[2].data).toHaveProperty('c') // Decrypt with lock context - const decryptedData = await protectClient + const decryptedData = await encryptionClient .bulkDecrypt(encryptedData.data) .withLockContext(lockContext.data) @@ -424,7 +433,7 @@ describe('bulk encryption and decryption', () => { ] // Encrypt with lock context - const encryptedData = await protectClient + const encryptedData = await encryptionClient .bulkEncrypt(plaintexts, { column: users.email, table: users, @@ -440,7 +449,7 @@ describe('bulk encryption and decryption', () => { expect(encryptedData.data[1].data).toBeNull() // Decrypt with lock context - const decryptedData = await protectClient + const decryptedData = await encryptionClient .bulkDecrypt(encryptedData.data) .withLockContext(lockContext.data) @@ -478,7 +487,7 @@ describe('bulk encryption and decryption', () => { } // Encrypt first value with USER_JWT lock context - const encryptedData1 = await protectClient + const encryptedData1 = await encryptionClient .bulkEncrypt([{ id: 'user1', plaintext: 'alice@example.com' }], { column: users.email, table: users, @@ -490,7 +499,7 @@ describe('bulk encryption and decryption', () => { } // Encrypt second value with USER_2_JWT lock context - const encryptedData2 = await protectClient + const encryptedData2 = await encryptionClient .bulkEncrypt([{ id: 'user2', plaintext: 'bob@example.com' }], { column: users.email, table: users, @@ -508,7 +517,7 @@ describe('bulk encryption and decryption', () => { ] // Decrypt with USER_2_JWT lock context - const decryptedData = await protectClient + const decryptedData = await encryptionClient .bulkDecrypt(combinedEncryptedData) .withLockContext(lockContext2.data) @@ -541,7 +550,7 @@ describe('bulk encryption and decryption', () => { ] // Encrypt - const encryptedData = await protectClient.bulkEncrypt(originalData, { + const encryptedData = await encryptionClient.bulkEncrypt(originalData, { column: users.email, table: users, }) @@ -551,7 +560,9 @@ describe('bulk encryption and decryption', () => { } // Decrypt - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt( + encryptedData.data, + ) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -573,7 +584,7 @@ describe('bulk encryption and decryption', () => { })) // Encrypt - const encryptedData = await protectClient.bulkEncrypt(originalData, { + const encryptedData = await encryptionClient.bulkEncrypt(originalData, { column: users.email, table: users, }) @@ -583,7 +594,9 @@ describe('bulk encryption and decryption', () => { } // Decrypt - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt( + encryptedData.data, + ) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) diff --git a/packages/stack/__tests__/deprecated/search-terms.test.ts b/packages/stack/__tests__/deprecated/search-terms.test.ts index 40a501fb..e1c0292c 100644 --- a/packages/stack/__tests__/deprecated/search-terms.test.ts +++ b/packages/stack/__tests__/deprecated/search-terms.test.ts @@ -12,7 +12,7 @@ const users = encryptedTable('users', { describe('createSearchTerms (deprecated - backward compatibility)', () => { it('should create search terms with default return type', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const searchTerms = [ { @@ -27,7 +27,8 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { }, ] as SearchTerm[] - const searchTermsResult = await protectClient.createSearchTerms(searchTerms) + const searchTermsResult = + await encryptionClient.createSearchTerms(searchTerms) if (searchTermsResult.failure) { throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) @@ -43,7 +44,7 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { }, 30000) it('should create search terms with composite-literal return type', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const searchTerms = [ { @@ -54,7 +55,8 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { }, ] as SearchTerm[] - const searchTermsResult = await protectClient.createSearchTerms(searchTerms) + const searchTermsResult = + await encryptionClient.createSearchTerms(searchTerms) if (searchTermsResult.failure) { throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) @@ -66,7 +68,7 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { }, 30000) it('should create search terms with escaped-composite-literal return type', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const searchTerms = [ { @@ -77,7 +79,8 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { }, ] as SearchTerm[] - const searchTermsResult = await protectClient.createSearchTerms(searchTerms) + const searchTermsResult = + await encryptionClient.createSearchTerms(searchTerms) if (searchTermsResult.failure) { throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) @@ -91,7 +94,7 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { }, 30000) it('should create search terms with composite-literal return type for numbers', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const searchTerms = [ { @@ -102,7 +105,8 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { }, ] - const searchTermsResult = await protectClient.createSearchTerms(searchTerms) + const searchTermsResult = + await encryptionClient.createSearchTerms(searchTerms) if (searchTermsResult.failure) { throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) @@ -114,7 +118,7 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { }, 30000) it('should create search terms with escaped-composite-literal return type for numbers', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const searchTerms = [ { @@ -125,7 +129,8 @@ describe('createSearchTerms (deprecated - backward compatibility)', () => { }, ] - const searchTermsResult = await protectClient.createSearchTerms(searchTerms) + const searchTermsResult = + await encryptionClient.createSearchTerms(searchTerms) if (searchTermsResult.failure) { throw new Error(`[encryption]: ${searchTermsResult.failure.message}`) diff --git a/packages/stack/__tests__/encrypt-query-searchable-json.test.ts b/packages/stack/__tests__/encrypt-query-searchable-json.test.ts index b1125fa2..12a8c554 100644 --- a/packages/stack/__tests__/encrypt-query-searchable-json.test.ts +++ b/packages/stack/__tests__/encrypt-query-searchable-json.test.ts @@ -35,16 +35,16 @@ function expectTerm(data: any) { } describe('encryptQuery with searchableJson queryType', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema, metadata] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema, metadata] }) }) // Core functionality: auto-inference from plaintext type it('auto-infers ste_vec_selector for string plaintext (JSONPath)', async () => { - const result = await protectClient.encryptQuery('$.user.email', { + const result = await encryptionClient.encryptQuery('$.user.email', { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -60,7 +60,7 @@ describe('encryptQuery with searchableJson queryType', () => { }, 30000) it('auto-infers ste_vec_term for object plaintext (containment)', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { role: 'admin' }, { column: jsonbSchema.metadata, @@ -79,7 +79,7 @@ describe('encryptQuery with searchableJson queryType', () => { }, 30000) it('auto-infers ste_vec_term for nested object', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { user: { profile: { role: 'admin' } } }, { column: jsonbSchema.metadata, @@ -98,7 +98,7 @@ describe('encryptQuery with searchableJson queryType', () => { }, 30000) it('auto-infers ste_vec_term for array plaintext', async () => { - const result = await protectClient.encryptQuery(['admin', 'user'], { + const result = await encryptionClient.encryptQuery(['admin', 'user'], { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -114,7 +114,7 @@ describe('encryptQuery with searchableJson queryType', () => { }, 30000) it('returns null for null plaintext', async () => { - const result = await protectClient.encryptQuery(null, { + const result = await encryptionClient.encryptQuery(null, { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -127,7 +127,7 @@ describe('encryptQuery with searchableJson queryType', () => { // Edge cases: number/boolean require wrapping (same as steVecTerm) it('fails for bare number plaintext (requires wrapping)', async () => { - const result = await protectClient.encryptQuery(42, { + const result = await encryptionClient.encryptQuery(42, { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -137,7 +137,7 @@ describe('encryptQuery with searchableJson queryType', () => { }, 30000) it('fails for bare boolean plaintext (requires wrapping)', async () => { - const result = await protectClient.encryptQuery(true, { + const result = await encryptionClient.encryptQuery(true, { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -148,14 +148,14 @@ describe('encryptQuery with searchableJson queryType', () => { }) describe('encryptQuery with searchableJson column and omitted queryType', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema, metadata] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema, metadata] }) }) it('auto-infers ste_vec_selector for string plaintext (JSONPath)', async () => { - const result = await protectClient.encryptQuery('$.user.email', { + const result = await encryptionClient.encryptQuery('$.user.email', { column: jsonbSchema.metadata, table: jsonbSchema, }) @@ -170,7 +170,7 @@ describe('encryptQuery with searchableJson column and omitted queryType', () => }, 30000) it('auto-infers ste_vec_term for object plaintext (containment)', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { role: 'admin' }, { column: jsonbSchema.metadata, @@ -188,7 +188,7 @@ describe('encryptQuery with searchableJson column and omitted queryType', () => }, 30000) it('returns null for null plaintext', async () => { - const result = await protectClient.encryptQuery(null, { + const result = await encryptionClient.encryptQuery(null, { column: jsonbSchema.metadata, table: jsonbSchema, }) @@ -198,7 +198,7 @@ describe('encryptQuery with searchableJson column and omitted queryType', () => }, 30000) it('fails for bare number plaintext (requires wrapping)', async () => { - const result = await protectClient.encryptQuery(42, { + const result = await encryptionClient.encryptQuery(42, { column: jsonbSchema.metadata, table: jsonbSchema, }) @@ -207,7 +207,7 @@ describe('encryptQuery with searchableJson column and omitted queryType', () => }, 30000) it('fails for bare boolean plaintext (requires wrapping)', async () => { - const result = await protectClient.encryptQuery(true, { + const result = await encryptionClient.encryptQuery(true, { column: jsonbSchema.metadata, table: jsonbSchema, }) @@ -217,14 +217,14 @@ describe('encryptQuery with searchableJson column and omitted queryType', () => }) describe('searchableJson validation', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema, metadata] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema, metadata] }) }) it('throws when used on column without ste_vec index', async () => { - const result = await protectClient.encryptQuery('$.path', { + const result = await encryptionClient.encryptQuery('$.path', { column: metadata.raw, // raw column has no ste_vec index table: metadata, queryType: 'searchableJson', @@ -235,14 +235,14 @@ describe('searchableJson validation', () => { }) describe('searchableJson batch operations', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema] }) }) it('handles mixed plaintext types in single batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: '$.user.email', // string → ste_vec_selector column: jsonbSchema.metadata, @@ -274,7 +274,7 @@ describe('searchableJson batch operations', () => { }, 30000) it('handles null values in batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: null, column: jsonbSchema.metadata, @@ -304,7 +304,7 @@ describe('searchableJson batch operations', () => { }, 30000) it('can be mixed with explicit steVecSelector/steVecTerm in batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: '$.path1', column: jsonbSchema.metadata, @@ -333,7 +333,7 @@ describe('searchableJson batch operations', () => { }, 30000) it('can omit queryType for searchableJson in batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: '$.path1', column: jsonbSchema.metadata, @@ -360,14 +360,14 @@ describe('searchableJson batch operations', () => { }) describe('searchableJson with returnType formatting', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema] }) }) it('supports composite-literal returnType', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: '$.user.email', column: jsonbSchema.metadata, @@ -385,7 +385,7 @@ describe('searchableJson with returnType formatting', () => { }, 30000) it('supports escaped-composite-literal returnType', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: { role: 'admin' }, column: jsonbSchema.metadata, @@ -404,14 +404,14 @@ describe('searchableJson with returnType formatting', () => { }) describe('searchableJson with LockContext', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema] }) }) it('exposes withLockContext method', async () => { - const operation = protectClient.encryptQuery('$.user.email', { + const operation = encryptionClient.encryptQuery('$.user.email', { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -424,7 +424,7 @@ describe('searchableJson with LockContext', () => { it('executes string plaintext with LockContext mock', async () => { const mockLockContext = createMockLockContext() - const operation = protectClient.encryptQuery('$.user.email', { + const operation = encryptionClient.encryptQuery('$.user.email', { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -446,7 +446,7 @@ describe('searchableJson with LockContext', () => { it('executes object plaintext with LockContext mock', async () => { const mockLockContext = createMockLockContext() - const operation = protectClient.encryptQuery( + const operation = encryptionClient.encryptQuery( { role: 'admin' }, { column: jsonbSchema.metadata, @@ -478,7 +478,7 @@ describe('searchableJson with LockContext', () => { it('executes batch with LockContext mock', async () => { const mockLockContext = createMockLockContext() - const operation = protectClient.encryptQuery([ + const operation = encryptionClient.encryptQuery([ { value: '$.user.email', column: jsonbSchema.metadata, @@ -512,7 +512,7 @@ describe('searchableJson with LockContext', () => { 'Mock LockContext failure', ) - const operation = protectClient.encryptQuery('$.user.email', { + const operation = encryptionClient.encryptQuery('$.user.email', { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -531,7 +531,7 @@ describe('searchableJson with LockContext', () => { it('handles null value with LockContext', async () => { const mockLockContext = createMockLockContext() - const operation = protectClient.encryptQuery(null, { + const operation = encryptionClient.encryptQuery(null, { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -550,7 +550,7 @@ describe('searchableJson with LockContext', () => { it('handles explicit null context from getLockContext gracefully', async () => { const mockLockContext = createMockLockContextWithNullContext() - const operation = protectClient.encryptQuery([ + const operation = encryptionClient.encryptQuery([ { value: '$.user.email', column: jsonbSchema.metadata, @@ -571,20 +571,20 @@ describe('searchableJson with LockContext', () => { }) describe('searchableJson equivalence', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema] }) }) it('produces identical metadata to omitting queryType for string', async () => { - const explicitResult = await protectClient.encryptQuery('$.user.email', { + const explicitResult = await encryptionClient.encryptQuery('$.user.email', { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', }) - const implicitResult = await protectClient.encryptQuery('$.user.email', { + const implicitResult = await encryptionClient.encryptQuery('$.user.email', { column: jsonbSchema.metadata, table: jsonbSchema, }) @@ -600,7 +600,7 @@ describe('searchableJson equivalence', () => { }, 30000) it('produces identical metadata to omitting queryType for object', async () => { - const explicitResult = await protectClient.encryptQuery( + const explicitResult = await encryptionClient.encryptQuery( { role: 'admin' }, { column: jsonbSchema.metadata, @@ -609,7 +609,7 @@ describe('searchableJson equivalence', () => { }, ) - const implicitResult = await protectClient.encryptQuery( + const implicitResult = await encryptionClient.encryptQuery( { role: 'admin' }, { column: jsonbSchema.metadata, @@ -627,7 +627,7 @@ describe('searchableJson equivalence', () => { }, 30000) it('produces identical metadata to explicit steVecSelector for string', async () => { - const searchableJsonResult = await protectClient.encryptQuery( + const searchableJsonResult = await encryptionClient.encryptQuery( '$.user.email', { column: jsonbSchema.metadata, @@ -636,7 +636,7 @@ describe('searchableJson equivalence', () => { }, ) - const steVecSelectorResult = await protectClient.encryptQuery( + const steVecSelectorResult = await encryptionClient.encryptQuery( '$.user.email', { column: jsonbSchema.metadata, @@ -655,7 +655,7 @@ describe('searchableJson equivalence', () => { }, 30000) it('produces identical metadata to explicit steVecTerm for object', async () => { - const searchableJsonResult = await protectClient.encryptQuery( + const searchableJsonResult = await encryptionClient.encryptQuery( { role: 'admin' }, { column: jsonbSchema.metadata, @@ -664,7 +664,7 @@ describe('searchableJson equivalence', () => { }, ) - const steVecTermResult = await protectClient.encryptQuery( + const steVecTermResult = await encryptionClient.encryptQuery( { role: 'admin' }, { column: jsonbSchema.metadata, @@ -684,16 +684,16 @@ describe('searchableJson equivalence', () => { }) describe('searchableJson edge cases', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema] }) }) // Valid edge cases that should succeed it('succeeds for empty object', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( {}, { column: jsonbSchema.metadata, @@ -711,7 +711,7 @@ describe('searchableJson edge cases', () => { }, 30000) it('succeeds for empty array', async () => { - const result = await protectClient.encryptQuery([], { + const result = await encryptionClient.encryptQuery([], { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -726,7 +726,7 @@ describe('searchableJson edge cases', () => { }, 30000) it('succeeds for object with wrapped number', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { value: 42 }, { column: jsonbSchema.metadata, @@ -744,7 +744,7 @@ describe('searchableJson edge cases', () => { }, 30000) it('succeeds for object with wrapped boolean', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { active: true }, { column: jsonbSchema.metadata, @@ -762,7 +762,7 @@ describe('searchableJson edge cases', () => { }, 30000) it('succeeds for object with null value', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { field: null }, { column: jsonbSchema.metadata, @@ -780,7 +780,7 @@ describe('searchableJson edge cases', () => { }, 30000) it('succeeds for deeply nested object (3+ levels)', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { level1: { level2: { @@ -810,7 +810,7 @@ describe('searchableJson edge cases', () => { // String edge cases for JSONPath selectors it('succeeds for JSONPath with array index notation', async () => { - const result = await protectClient.encryptQuery('$.items[0].name', { + const result = await encryptionClient.encryptQuery('$.items[0].name', { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -825,7 +825,7 @@ describe('searchableJson edge cases', () => { }, 30000) it('succeeds for JSONPath with wildcard', async () => { - const result = await protectClient.encryptQuery('$.items[*].name', { + const result = await encryptionClient.encryptQuery('$.items[*].name', { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', @@ -841,20 +841,20 @@ describe('searchableJson edge cases', () => { }) describe('searchableJson batch edge cases', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema] }) }) it('handles single-item batch identically to scalar', async () => { - const scalarResult = await protectClient.encryptQuery('$.user.email', { + const scalarResult = await encryptionClient.encryptQuery('$.user.email', { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'searchableJson', }) - const batchResult = await protectClient.encryptQuery([ + const batchResult = await encryptionClient.encryptQuery([ { value: '$.user.email', column: jsonbSchema.metadata, @@ -874,7 +874,7 @@ describe('searchableJson batch edge cases', () => { }, 30000) it('handles all-null batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: null, column: jsonbSchema.metadata, @@ -903,7 +903,7 @@ describe('searchableJson batch edge cases', () => { }, 30000) it('handles empty batch', async () => { - const result = await protectClient.encryptQuery([]) + const result = await encryptionClient.encryptQuery([]) const data = unwrapResult(result) expect(data).toHaveLength(0) @@ -917,7 +917,7 @@ describe('searchableJson batch edge cases', () => { queryType: 'searchableJson' as const, })) - const result = await protectClient.encryptQuery(items) + const result = await encryptionClient.encryptQuery(items) const data = unwrapResult(result) expect(data).toHaveLength(12) @@ -935,7 +935,7 @@ describe('searchableJson batch edge cases', () => { }, 30000) it('handles multiple interspersed nulls at various positions', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: null, column: jsonbSchema.metadata, diff --git a/packages/stack/__tests__/encrypt-query-stevec.test.ts b/packages/stack/__tests__/encrypt-query-stevec.test.ts index fc16eba6..bb0381c5 100644 --- a/packages/stack/__tests__/encrypt-query-stevec.test.ts +++ b/packages/stack/__tests__/encrypt-query-stevec.test.ts @@ -6,14 +6,14 @@ type EncryptionClient = Awaited> import { expectFailure, jsonbSchema, metadata, unwrapResult } from './fixtures' describe('encryptQuery with steVecSelector', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema, metadata] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema, metadata] }) }) it('encrypts a JSONPath selector', async () => { - const result = await protectClient.encryptQuery('$.user.email', { + const result = await encryptionClient.encryptQuery('$.user.email', { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'steVecSelector', @@ -27,11 +27,14 @@ describe('encryptQuery with steVecSelector', () => { }, 30000) it('encrypts nested path selector', async () => { - const result = await protectClient.encryptQuery('$.user.profile.settings', { - column: jsonbSchema.metadata, - table: jsonbSchema, - queryType: 'steVecSelector', - }) + const result = await encryptionClient.encryptQuery( + '$.user.profile.settings', + { + column: jsonbSchema.metadata, + table: jsonbSchema, + queryType: 'steVecSelector', + }, + ) const data = unwrapResult(result) expect(data).toBeDefined() @@ -41,7 +44,7 @@ describe('encryptQuery with steVecSelector', () => { }, 30000) it('fails for non-string plaintext with steVecSelector (object)', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { role: 'admin' }, { column: jsonbSchema.metadata, @@ -55,14 +58,14 @@ describe('encryptQuery with steVecSelector', () => { }) describe('encryptQuery with steVecTerm', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema, metadata] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema, metadata] }) }) it('encrypts an object for containment query', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { role: 'admin' }, { column: jsonbSchema.metadata, @@ -79,7 +82,7 @@ describe('encryptQuery with steVecTerm', () => { }, 30000) it('encrypts nested object for containment', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { user: { profile: { role: 'admin' } } }, { column: jsonbSchema.metadata, @@ -96,7 +99,7 @@ describe('encryptQuery with steVecTerm', () => { }, 30000) it('encrypts array for containment query', async () => { - const result = await protectClient.encryptQuery([1, 2, 3], { + const result = await encryptionClient.encryptQuery([1, 2, 3], { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'steVecTerm', @@ -112,7 +115,7 @@ describe('encryptQuery with steVecTerm', () => { it('rejects string plaintext with steVecTerm', async () => { // steVecTerm requires object or array, not string // For path queries like '$.field', use steVecSelector instead - const result = await protectClient.encryptQuery('search text', { + const result = await encryptionClient.encryptQuery('search text', { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'steVecTerm', @@ -123,14 +126,14 @@ describe('encryptQuery with steVecTerm', () => { }) describe('encryptQuery STE Vec validation', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema, metadata] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema, metadata] }) }) it('throws when steVecSelector used on non-ste_vec column', async () => { - const result = await protectClient.encryptQuery('$.user.email', { + const result = await encryptionClient.encryptQuery('$.user.email', { column: metadata.raw, // raw column has no ste_vec index table: metadata, queryType: 'steVecSelector', @@ -140,7 +143,7 @@ describe('encryptQuery STE Vec validation', () => { }, 30000) it('throws when steVecTerm used on non-ste_vec column', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { field: 'value' }, { column: metadata.raw, // raw column has no ste_vec index @@ -154,14 +157,14 @@ describe('encryptQuery STE Vec validation', () => { }) describe('encryptQuery batch with STE Vec', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema, metadata] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema, metadata] }) }) it('handles mixed query types in batch (steVecSelector + steVecTerm)', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: '$.user.email', column: jsonbSchema.metadata, @@ -184,7 +187,7 @@ describe('encryptQuery batch with STE Vec', () => { }, 30000) it('handles multiple steVecSelector queries in batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: '$.user.email', column: jsonbSchema.metadata, @@ -207,7 +210,7 @@ describe('encryptQuery batch with STE Vec', () => { }, 30000) it('handles null values with steVecSelector in batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: null, column: jsonbSchema.metadata, @@ -231,7 +234,7 @@ describe('encryptQuery batch with STE Vec', () => { }, 30000) it('handles null values with steVecTerm in batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: null, column: jsonbSchema.metadata, @@ -256,14 +259,14 @@ describe('encryptQuery batch with STE Vec', () => { }) describe('encryptQuery with queryType inference', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema] }) }) it('infers steVecSelector for string plaintext without queryType', async () => { - const result = await protectClient.encryptQuery('$.user.email', { + const result = await encryptionClient.encryptQuery('$.user.email', { column: jsonbSchema.metadata, table: jsonbSchema, // No queryType - should infer steVecSelector from string @@ -277,7 +280,7 @@ describe('encryptQuery with queryType inference', () => { }, 30000) it('infers steVecTerm for object plaintext without queryType', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( { role: 'admin' }, { column: jsonbSchema.metadata, @@ -294,7 +297,7 @@ describe('encryptQuery with queryType inference', () => { }, 30000) it('infers steVecTerm for array plaintext without queryType', async () => { - const result = await protectClient.encryptQuery(['admin', 'user'], { + const result = await encryptionClient.encryptQuery(['admin', 'user'], { column: jsonbSchema.metadata, table: jsonbSchema, // No queryType - should infer steVecTerm from array @@ -309,7 +312,7 @@ describe('encryptQuery with queryType inference', () => { it('infers steVecTerm for number plaintext but FFI requires wrapping', async () => { // Numbers infer steVecTerm but FFI requires wrapping in object/array - const result = await protectClient.encryptQuery(42, { + const result = await encryptionClient.encryptQuery(42, { column: jsonbSchema.metadata, table: jsonbSchema, // No queryType - infers steVecTerm, FFI rejects with helpful message @@ -320,7 +323,7 @@ describe('encryptQuery with queryType inference', () => { it('infers steVecTerm for boolean plaintext but FFI requires wrapping', async () => { // Booleans infer steVecTerm but FFI requires wrapping in object/array - const result = await protectClient.encryptQuery(true, { + const result = await encryptionClient.encryptQuery(true, { column: jsonbSchema.metadata, table: jsonbSchema, // No queryType - infers steVecTerm, FFI rejects with helpful message @@ -330,7 +333,7 @@ describe('encryptQuery with queryType inference', () => { }, 30000) it('returns null for null plaintext (no inference needed)', async () => { - const result = await protectClient.encryptQuery(null, { + const result = await encryptionClient.encryptQuery(null, { column: jsonbSchema.metadata, table: jsonbSchema, // No queryType and null plaintext - should return null @@ -346,7 +349,7 @@ describe('encryptQuery with queryType inference', () => { // Note: steVecTerm with string fails FFI validation, so we test the opposite direction // Using a number (which would infer steVecTerm) with explicit steVecSelector would also fail // So we verify with array + steVecTerm (already tested) and trust unit test coverage for precedence - const result = await protectClient.encryptQuery([42], { + const result = await encryptionClient.encryptQuery([42], { column: jsonbSchema.metadata, table: jsonbSchema, queryType: 'steVecTerm', // Explicit - matches inference but proves explicit path works @@ -361,14 +364,14 @@ describe('encryptQuery with queryType inference', () => { }) describe('encryptQuery batch with queryType inference', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ schemas: [jsonbSchema] }) + encryptionClient = await Encryption({ schemas: [jsonbSchema] }) }) it('infers queryOp for each term independently in batch', async () => { - const results = await protectClient.encryptQuery([ + const results = await encryptionClient.encryptQuery([ { value: '$.user.email', // string → steVecSelector column: jsonbSchema.metadata, diff --git a/packages/stack/__tests__/encrypt-query.test.ts b/packages/stack/__tests__/encrypt-query.test.ts index 095c7a52..23907087 100644 --- a/packages/stack/__tests__/encrypt-query.test.ts +++ b/packages/stack/__tests__/encrypt-query.test.ts @@ -15,17 +15,17 @@ import { } from './fixtures' describe('encryptQuery', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ + encryptionClient = await Encryption({ schemas: [users, articles, products, metadata], }) }) describe('single value encryption with explicit queryType', () => { it('encrypts for equality query type', async () => { - const result = await protectClient.encryptQuery('test@example.com', { + const result = await encryptionClient.encryptQuery('test@example.com', { column: users.email, table: users, queryType: 'equality', @@ -41,7 +41,7 @@ describe('encryptQuery', () => { }, 30000) it('encrypts for freeTextSearch query type', async () => { - const result = await protectClient.encryptQuery('hello world', { + const result = await encryptionClient.encryptQuery('hello world', { column: users.bio, table: users, queryType: 'freeTextSearch', @@ -57,7 +57,7 @@ describe('encryptQuery', () => { }, 30000) it('encrypts for orderAndRange query type', async () => { - const result = await protectClient.encryptQuery(25, { + const result = await encryptionClient.encryptQuery(25, { column: users.age, table: users, queryType: 'orderAndRange', @@ -75,7 +75,7 @@ describe('encryptQuery', () => { describe('auto-inference when queryType omitted', () => { it('auto-infers equality for column with .equality()', async () => { - const result = await protectClient.encryptQuery('test@example.com', { + const result = await encryptionClient.encryptQuery('test@example.com', { column: users.email, table: users, }) @@ -85,7 +85,7 @@ describe('encryptQuery', () => { }, 30000) it('auto-infers freeTextSearch for match-only column', async () => { - const result = await protectClient.encryptQuery('search content', { + const result = await encryptionClient.encryptQuery('search content', { column: articles.content, table: articles, }) @@ -95,7 +95,7 @@ describe('encryptQuery', () => { }, 30000) it('auto-infers orderAndRange for ore-only column', async () => { - const result = await protectClient.encryptQuery(99.99, { + const result = await encryptionClient.encryptQuery(99.99, { column: products.price, table: products, }) @@ -107,7 +107,7 @@ describe('encryptQuery', () => { describe('edge cases', () => { it('handles null values', async () => { - const result = await protectClient.encryptQuery(null, { + const result = await encryptionClient.encryptQuery(null, { column: users.email, table: users, queryType: 'equality', @@ -118,7 +118,7 @@ describe('encryptQuery', () => { }, 30000) it('rejects NaN values', async () => { - const result = await protectClient.encryptQuery(Number.NaN, { + const result = await encryptionClient.encryptQuery(Number.NaN, { column: users.age, table: users, queryType: 'orderAndRange', @@ -128,7 +128,7 @@ describe('encryptQuery', () => { }, 30000) it('rejects Infinity values', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( Number.POSITIVE_INFINITY, { column: users.age, @@ -141,7 +141,7 @@ describe('encryptQuery', () => { }, 30000) it('rejects negative Infinity values', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( Number.NEGATIVE_INFINITY, { column: users.age, @@ -156,7 +156,7 @@ describe('encryptQuery', () => { describe('validation errors', () => { it('fails when queryType does not match column config', async () => { - const result = await protectClient.encryptQuery('test@example.com', { + const result = await encryptionClient.encryptQuery('test@example.com', { column: users.email, table: users, queryType: 'freeTextSearch', // email only has equality @@ -166,7 +166,7 @@ describe('encryptQuery', () => { }, 30000) it('fails when column has no indexes configured', async () => { - const result = await protectClient.encryptQuery('raw data', { + const result = await encryptionClient.encryptQuery('raw data', { column: metadata.raw, table: metadata, }) @@ -175,7 +175,7 @@ describe('encryptQuery', () => { }, 30000) it('provides descriptive error for queryType mismatch', async () => { - const result = await protectClient.encryptQuery(42, { + const result = await encryptionClient.encryptQuery(42, { column: users.age, table: users, queryType: 'equality', // age only has orderAndRange @@ -192,7 +192,7 @@ describe('encryptQuery', () => { describe('value/index type compatibility', () => { it('fails when encrypting number with match index (explicit queryType)', async () => { - const result = await protectClient.encryptQuery(123, { + const result = await encryptionClient.encryptQuery(123, { column: articles.content, // match-only column table: articles, queryType: 'freeTextSearch', @@ -203,7 +203,7 @@ describe('encryptQuery', () => { }, 30000) it('fails when encrypting number with auto-inferred match index', async () => { - const result = await protectClient.encryptQuery(123, { + const result = await encryptionClient.encryptQuery(123, { column: articles.content, // match-only column, will infer 'match' table: articles, }) @@ -212,7 +212,7 @@ describe('encryptQuery', () => { }, 30000) it('fails in batch when number used with match index', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 123, column: articles.content, table: articles }, ]) @@ -220,7 +220,7 @@ describe('encryptQuery', () => { }, 30000) it('allows string with match index', async () => { - const result = await protectClient.encryptQuery('search text', { + const result = await encryptionClient.encryptQuery('search text', { column: articles.content, table: articles, }) @@ -230,7 +230,7 @@ describe('encryptQuery', () => { }, 30000) it('allows number with ore index', async () => { - const result = await protectClient.encryptQuery(42, { + const result = await encryptionClient.encryptQuery(42, { column: users.age, table: users, queryType: 'orderAndRange', @@ -243,11 +243,14 @@ describe('encryptQuery', () => { describe('numeric edge cases', () => { it('encrypts MAX_SAFE_INTEGER', async () => { - const result = await protectClient.encryptQuery(Number.MAX_SAFE_INTEGER, { - column: users.age, - table: users, - queryType: 'orderAndRange', - }) + const result = await encryptionClient.encryptQuery( + Number.MAX_SAFE_INTEGER, + { + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ) const data = unwrapResult(result) expect(data).toMatchObject({ @@ -258,11 +261,14 @@ describe('encryptQuery', () => { }, 30000) it('encrypts MIN_SAFE_INTEGER', async () => { - const result = await protectClient.encryptQuery(Number.MIN_SAFE_INTEGER, { - column: users.age, - table: users, - queryType: 'orderAndRange', - }) + const result = await encryptionClient.encryptQuery( + Number.MIN_SAFE_INTEGER, + { + column: users.age, + table: users, + queryType: 'orderAndRange', + }, + ) const data = unwrapResult(result) expect(data).toMatchObject({ @@ -273,7 +279,7 @@ describe('encryptQuery', () => { }, 30000) it('encrypts negative zero', async () => { - const result = await protectClient.encryptQuery(-0, { + const result = await encryptionClient.encryptQuery(-0, { column: users.age, table: users, queryType: 'orderAndRange', @@ -286,7 +292,7 @@ describe('encryptQuery', () => { describe('string edge cases', () => { it('encrypts empty string', async () => { - const result = await protectClient.encryptQuery('', { + const result = await encryptionClient.encryptQuery('', { column: users.email, table: users, queryType: 'equality', @@ -301,7 +307,7 @@ describe('encryptQuery', () => { }, 30000) it('encrypts unicode/emoji strings', async () => { - const result = await protectClient.encryptQuery('Hello 世界 🌍🚀', { + const result = await encryptionClient.encryptQuery('Hello 世界 🌍🚀', { column: users.bio, table: users, queryType: 'freeTextSearch', @@ -316,7 +322,7 @@ describe('encryptQuery', () => { }, 30000) it('encrypts strings with SQL special characters', async () => { - const result = await protectClient.encryptQuery( + const result = await encryptionClient.encryptQuery( "'; DROP TABLE users; --", { column: users.email, @@ -336,7 +342,7 @@ describe('encryptQuery', () => { describe('encryptQuery bulk (array overload)', () => { it('encrypts multiple terms in batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 'user@example.com', column: users.email, @@ -367,14 +373,14 @@ describe('encryptQuery', () => { it('handles empty array', async () => { // Empty arrays without opts are treated as empty batch for backward compatibility - const result = await protectClient.encryptQuery([]) + const result = await encryptionClient.encryptQuery([]) const data = unwrapResult(result) expect(data).toEqual([]) }, 30000) it('handles null values in batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 'test@example.com', column: users.email, @@ -397,7 +403,7 @@ describe('encryptQuery', () => { }, 30000) it('auto-infers queryType when omitted', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 'user@example.com', column: users.email, table: users }, { value: 42, column: users.age, table: users }, ]) @@ -410,7 +416,7 @@ describe('encryptQuery', () => { }, 30000) it('rejects NaN/Infinity values in batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: Number.NaN, column: users.age, @@ -429,7 +435,7 @@ describe('encryptQuery', () => { }, 30000) it('rejects negative Infinity in batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: Number.NEGATIVE_INFINITY, column: users.age, @@ -444,7 +450,7 @@ describe('encryptQuery', () => { describe('bulk index preservation', () => { it('preserves exact positions with multiple nulls interspersed', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: null, column: users.email, @@ -490,7 +496,7 @@ describe('encryptQuery', () => { }, 30000) it('handles single-item array', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 'single@example.com', column: users.email, @@ -507,7 +513,7 @@ describe('encryptQuery', () => { }, 30000) it('handles all-null array', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: null, column: users.email, @@ -539,7 +545,7 @@ describe('encryptQuery', () => { describe('audit support', () => { it('passes audit metadata for single query', async () => { - const result = await protectClient + const result = await encryptionClient .encryptQuery('test@example.com', { column: users.email, table: users, @@ -552,7 +558,7 @@ describe('encryptQuery', () => { }, 30000) it('passes audit metadata for bulk query', async () => { - const result = await protectClient + const result = await encryptionClient .encryptQuery([ { value: 'test@example.com', @@ -570,7 +576,7 @@ describe('encryptQuery', () => { describe('returnType formatting', () => { it('returns Encrypted by default (no returnType)', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 'test@example.com', column: users.email, @@ -590,7 +596,7 @@ describe('encryptQuery', () => { }, 30000) it('returns composite-literal format when specified', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 'test@example.com', column: users.email, @@ -609,7 +615,7 @@ describe('encryptQuery', () => { }, 30000) it('returns escaped-composite-literal format when specified', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 'test@example.com', column: users.email, @@ -628,7 +634,7 @@ describe('encryptQuery', () => { }, 30000) it('returns eql format when explicitly specified', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 'test@example.com', column: users.email, @@ -649,7 +655,7 @@ describe('encryptQuery', () => { }, 30000) it('handles mixed returnType values in same batch', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 'test@example.com', column: users.email, @@ -690,7 +696,7 @@ describe('encryptQuery', () => { }, 30000) it('handles returnType with null values', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: null, column: users.email, @@ -728,7 +734,7 @@ describe('encryptQuery', () => { it('single query with LockContext calls getLockContext', async () => { const mockLockContext = createMockLockContext() - const operation = protectClient.encryptQuery('test@example.com', { + const operation = encryptionClient.encryptQuery('test@example.com', { column: users.email, table: users, queryType: 'equality', @@ -742,7 +748,7 @@ describe('encryptQuery', () => { it('bulk query with LockContext calls getLockContext', async () => { const mockLockContext = createMockLockContext() - const operation = protectClient.encryptQuery([ + const operation = encryptionClient.encryptQuery([ { value: 'test@example.com', column: users.email, @@ -759,7 +765,7 @@ describe('encryptQuery', () => { it('executes single query with LockContext mock', async () => { const mockLockContext = createMockLockContext() - const operation = protectClient.encryptQuery('test@example.com', { + const operation = encryptionClient.encryptQuery('test@example.com', { column: users.email, table: users, queryType: 'equality', @@ -781,7 +787,7 @@ describe('encryptQuery', () => { it('executes bulk query with LockContext mock', async () => { const mockLockContext = createMockLockContext() - const operation = protectClient.encryptQuery([ + const operation = encryptionClient.encryptQuery([ { value: 'test@example.com', column: users.email, @@ -813,7 +819,7 @@ describe('encryptQuery', () => { 'Mock LockContext failure', ) - const operation = protectClient.encryptQuery('test@example.com', { + const operation = encryptionClient.encryptQuery('test@example.com', { column: users.email, table: users, queryType: 'equality', @@ -832,7 +838,7 @@ describe('encryptQuery', () => { it('handles null value with LockContext', async () => { const mockLockContext = createMockLockContext() - const operation = protectClient.encryptQuery(null, { + const operation = encryptionClient.encryptQuery(null, { column: users.email, table: users, queryType: 'equality', @@ -851,7 +857,7 @@ describe('encryptQuery', () => { // Simulate a runtime scenario where context is null (bypasses TypeScript) const mockLockContext = createMockLockContextWithNullContext() - const operation = protectClient.encryptQuery([ + const operation = encryptionClient.encryptQuery([ { value: 'test@example.com', column: users.email, diff --git a/packages/stack/__tests__/error-codes.test.ts b/packages/stack/__tests__/error-codes.test.ts index 8e9695b8..e6943670 100644 --- a/packages/stack/__tests__/error-codes.test.ts +++ b/packages/stack/__tests__/error-codes.test.ts @@ -13,7 +13,7 @@ const FFI_TEST_TIMEOUT = 30_000 * enabling programmatic error handling. */ describe('FFI Error Code Preservation', () => { - let protectClient: EncryptionClient + let encryptionClient: EncryptionClient // Schema with a valid column for testing const testSchema = encryptedTable('test_table', { @@ -34,7 +34,9 @@ describe('FFI Error Code Preservation', () => { }) beforeAll(async () => { - protectClient = await Encryption({ schemas: [testSchema, noIndexSchema] }) + encryptionClient = await Encryption({ + schemas: [testSchema, noIndexSchema], + }) }) describe('FfiProtectError class', () => { @@ -55,7 +57,7 @@ describe('FFI Error Code Preservation', () => { // Create a fake column that doesn't exist in the schema const fakeColumn = encryptedColumn('nonexistent_column').equality() - const result = await protectClient.encryptQuery('test', { + const result = await encryptionClient.encryptQuery('test', { column: fakeColumn, table: testSchema, queryType: 'equality', @@ -72,7 +74,7 @@ describe('FFI Error Code Preservation', () => { 'returns undefined code for columns without indexes (non-FFI validation)', async () => { // This error is caught during pre-FFI validation, not by FFI itself - const result = await protectClient.encryptQuery('test', { + const result = await encryptionClient.encryptQuery('test', { column: noIndexSchema.raw, table: noIndexSchema, }) @@ -90,7 +92,7 @@ describe('FFI Error Code Preservation', () => { 'returns undefined code for non-FFI validation errors', async () => { // NaN validation happens before FFI call - const result = await protectClient.encryptQuery(Number.NaN, { + const result = await encryptionClient.encryptQuery(Number.NaN, { column: testSchema.age, table: testSchema, queryType: 'orderAndRange', @@ -111,7 +113,7 @@ describe('FFI Error Code Preservation', () => { async () => { const fakeColumn = encryptedColumn('nonexistent_column').equality() - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 'test', column: fakeColumn, @@ -130,7 +132,7 @@ describe('FFI Error Code Preservation', () => { it( 'returns undefined code for non-FFI batch errors', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: Number.NaN, column: testSchema.age, @@ -152,7 +154,7 @@ describe('FFI Error Code Preservation', () => { async () => { const fakeColumn = encryptedColumn('nonexistent_column') - const result = await protectClient.encrypt('test', { + const result = await encryptionClient.encrypt('test', { column: fakeColumn, table: testSchema, }) @@ -167,7 +169,7 @@ describe('FFI Error Code Preservation', () => { it( 'returns undefined code for non-FFI encrypt errors', async () => { - const result = await protectClient.encrypt(Number.NaN, { + const result = await encryptionClient.encrypt(Number.NaN, { column: testSchema.age, table: testSchema, }) @@ -192,7 +194,7 @@ describe('FFI Error Code Preservation', () => { c: 'invalid_ciphertext_data', } - const result = await protectClient.decrypt(invalidCiphertext) + const result = await encryptionClient.decrypt(invalidCiphertext) expect(result.failure).toBeDefined() expect(result.failure?.type).toBe(EncryptionErrorTypes.DecryptionError) @@ -209,7 +211,7 @@ describe('FFI Error Code Preservation', () => { async () => { const fakeColumn = encryptedColumn('nonexistent_column') - const result = await protectClient.bulkEncrypt( + const result = await encryptionClient.bulkEncrypt( [{ plaintext: 'test1' }, { plaintext: 'test2' }], { column: fakeColumn, @@ -227,7 +229,7 @@ describe('FFI Error Code Preservation', () => { it( 'returns undefined code for non-FFI validation errors', async () => { - const result = await protectClient.bulkEncrypt( + const result = await encryptionClient.bulkEncrypt( [{ plaintext: Number.NaN }], { column: testSchema.age, @@ -251,7 +253,7 @@ describe('FFI Error Code Preservation', () => { { data: { i: { t: 'test_table', c: 'email' }, v: 2, c: 'invalid2' } }, ] - const result = await protectClient.bulkDecrypt(invalidCiphertexts) + const result = await encryptionClient.bulkDecrypt(invalidCiphertexts) expect(result.failure).toBeDefined() expect(result.failure?.type).toBe(EncryptionErrorTypes.DecryptionError) @@ -268,7 +270,10 @@ describe('FFI Error Code Preservation', () => { async () => { const model = { nonexistent: 'test value' } - const result = await protectClient.encryptModel(model, badModelSchema) + const result = await encryptionClient.encryptModel( + model, + badModelSchema, + ) expect(result.failure).toBeDefined() expect(result.failure?.type).toBe(EncryptionErrorTypes.EncryptionError) @@ -290,7 +295,7 @@ describe('FFI Error Code Preservation', () => { }, } - const result = await protectClient.decryptModel(malformedModel) + const result = await encryptionClient.decryptModel(malformedModel) expect(result.failure).toBeDefined() expect(result.failure?.type).toBe(EncryptionErrorTypes.DecryptionError) @@ -306,7 +311,7 @@ describe('FFI Error Code Preservation', () => { async () => { const models = [{ nonexistent: 'value1' }, { nonexistent: 'value2' }] - const result = await protectClient.bulkEncryptModels( + const result = await encryptionClient.bulkEncryptModels( models, badModelSchema, ) @@ -332,7 +337,7 @@ describe('FFI Error Code Preservation', () => { }, ] - const result = await protectClient.bulkDecryptModels(malformedModels) + const result = await encryptionClient.bulkDecryptModels(malformedModels) expect(result.failure).toBeDefined() expect(result.failure?.type).toBe(EncryptionErrorTypes.DecryptionError) @@ -348,7 +353,7 @@ describe('FFI Error Code Preservation', () => { async () => { const fakeColumn = encryptedColumn('nonexistent_column').equality() - const result = await protectClient.createSearchTerms([ + const result = await encryptionClient.createSearchTerms([ { value: 'test', column: fakeColumn, table: testSchema }, ]) diff --git a/packages/stack/__tests__/json-protect.test.ts b/packages/stack/__tests__/json-protect.test.ts index 33e74f8f..8584cef3 100644 --- a/packages/stack/__tests__/json-protect.test.ts +++ b/packages/stack/__tests__/json-protect.test.ts @@ -36,10 +36,10 @@ type User = { } } -let protectClient: EncryptionClient +let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ + encryptionClient = await Encryption({ schemas: [users], }) }) @@ -51,7 +51,7 @@ describe('JSON encryption and decryption', () => { age: 30, } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -63,7 +63,7 @@ describe('JSON encryption and decryption', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -103,7 +103,7 @@ describe('JSON encryption and decryption', () => { numberValue: 42.5, } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -115,7 +115,7 @@ describe('JSON encryption and decryption', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -123,7 +123,7 @@ describe('JSON encryption and decryption', () => { }, 30000) it('should handle null JSON payload', async () => { - const ciphertext = await protectClient.encrypt(null, { + const ciphertext = await encryptionClient.encrypt(null, { column: users.json, table: users, }) @@ -135,7 +135,7 @@ describe('JSON encryption and decryption', () => { // Verify null is preserved expect(ciphertext.data).toBeNull() - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: null, @@ -145,7 +145,7 @@ describe('JSON encryption and decryption', () => { it('should handle empty JSON object', async () => { const json = {} - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -157,7 +157,7 @@ describe('JSON encryption and decryption', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -172,7 +172,7 @@ describe('JSON encryption and decryption', () => { multiline: 'Line 1\nLine 2\tTabbed', } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -184,7 +184,7 @@ describe('JSON encryption and decryption', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -210,7 +210,7 @@ describe('JSON model encryption and decryption', () => { updatedAt: new Date('2021-01-01'), } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -229,7 +229,7 @@ describe('JSON model encryption and decryption', () => { expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -250,7 +250,7 @@ describe('JSON model encryption and decryption', () => { updatedAt: new Date('2021-01-01'), } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -264,7 +264,7 @@ describe('JSON model encryption and decryption', () => { expect(encryptedModel.data.address).not.toHaveProperty('k') expect(encryptedModel.data.json).toBeNull() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -285,7 +285,7 @@ describe('JSON model encryption and decryption', () => { updatedAt: new Date('2021-01-01'), } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -299,7 +299,7 @@ describe('JSON model encryption and decryption', () => { expect(encryptedModel.data.address).not.toHaveProperty('k') expect(encryptedModel.data.json).toBeUndefined() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -319,7 +319,7 @@ describe('JSON bulk encryption and decryption', () => { { id: 'user3', plaintext: { name: 'Charlie', age: 35 } }, ] - const encryptedData = await protectClient.bulkEncrypt(jsonPayloads, { + const encryptedData = await encryptionClient.bulkEncrypt(jsonPayloads, { column: users.json, table: users, }) @@ -341,7 +341,7 @@ describe('JSON bulk encryption and decryption', () => { expect(encryptedData.data[2].data).not.toHaveProperty('k') // Now decrypt the data - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -373,7 +373,7 @@ describe('JSON bulk encryption and decryption', () => { { id: 'user3', plaintext: { name: 'Charlie', age: 35 } }, ] - const encryptedData = await protectClient.bulkEncrypt(jsonPayloads, { + const encryptedData = await encryptionClient.bulkEncrypt(jsonPayloads, { column: users.json, table: users, }) @@ -395,7 +395,7 @@ describe('JSON bulk encryption and decryption', () => { expect(encryptedData.data[2].data).not.toHaveProperty('k') // Now decrypt the data - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -443,7 +443,7 @@ describe('JSON bulk encryption and decryption', () => { }, ] - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -468,7 +468,7 @@ describe('JSON bulk encryption and decryption', () => { expect(encryptedModels.data[1].createdAt).toEqual(new Date('2021-01-01')) expect(encryptedModels.data[1].updatedAt).toEqual(new Date('2021-01-01')) - const decryptedResult = await protectClient.bulkDecryptModels( + const decryptedResult = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -505,7 +505,7 @@ describe('JSON encryption with lock context', () => { }, } - const ciphertext = await protectClient + const ciphertext = await encryptionClient .encrypt(json, { column: users.json, table: users, @@ -519,7 +519,7 @@ describe('JSON encryption with lock context', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient + const plaintext = await encryptionClient .decrypt(ciphertext.data) .withLockContext(lockContext.data) @@ -554,7 +554,7 @@ describe('JSON encryption with lock context', () => { }, } - const encryptedModel = await protectClient + const encryptedModel = await encryptionClient .encryptModel(decryptedModel, users) .withLockContext(lockContext.data) @@ -566,7 +566,7 @@ describe('JSON encryption with lock context', () => { expect(encryptedModel.data.email).not.toHaveProperty('k') expect(encryptedModel.data.json).not.toHaveProperty('k') - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .decryptModel(encryptedModel.data) .withLockContext(lockContext.data) @@ -597,7 +597,7 @@ describe('JSON encryption with lock context', () => { { id: 'user2', plaintext: { name: 'Bob', age: 30 } }, ] - const encryptedData = await protectClient + const encryptedData = await encryptionClient .bulkEncrypt(jsonPayloads, { column: users.json, table: users, @@ -618,7 +618,7 @@ describe('JSON encryption with lock context', () => { expect(encryptedData.data[1].data).not.toHaveProperty('k') // Decrypt with lock context - const decryptedData = await protectClient + const decryptedData = await encryptionClient .bulkDecrypt(encryptedData.data) .withLockContext(lockContext.data) @@ -643,7 +643,7 @@ describe('JSON encryption with lock context', () => { describe('JSON nested object encryption', () => { it('should encrypt and decrypt nested JSON objects', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '1', @@ -666,7 +666,7 @@ describe('JSON nested object encryption', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -685,7 +685,7 @@ describe('JSON nested object encryption', () => { // Verify non-encrypted fields remain unchanged expect(encryptedModel.data.id).toBe('1') - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -697,7 +697,7 @@ describe('JSON nested object encryption', () => { }, 30000) it('should handle null values in nested JSON objects', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '2', @@ -710,7 +710,7 @@ describe('JSON nested object encryption', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -724,7 +724,7 @@ describe('JSON nested object encryption', () => { expect(encryptedModel.data.metadata?.profile).toBeNull() expect(encryptedModel.data.metadata?.settings?.preferences).toBeNull() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -736,7 +736,7 @@ describe('JSON nested object encryption', () => { }, 30000) it('should handle undefined values in nested JSON objects', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '3', @@ -749,7 +749,7 @@ describe('JSON nested object encryption', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -763,7 +763,7 @@ describe('JSON nested object encryption', () => { expect(encryptedModel.data.metadata?.profile).toBeUndefined() expect(encryptedModel.data.metadata?.settings?.preferences).toBeUndefined() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -795,7 +795,7 @@ describe('JSON edge cases and error handling', () => { }, } - const ciphertext = await protectClient.encrypt(largeJson, { + const ciphertext = await encryptionClient.encrypt(largeJson, { column: users.json, table: users, }) @@ -807,7 +807,7 @@ describe('JSON edge cases and error handling', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: largeJson, @@ -819,7 +819,7 @@ describe('JSON edge cases and error handling', () => { circularObj.self = circularObj try { - await protectClient.encrypt(circularObj, { + await encryptionClient.encrypt(circularObj, { column: users.json, table: users, }) @@ -843,7 +843,7 @@ describe('JSON edge cases and error handling', () => { // Note: Functions and undefined are not JSON serializable } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -855,7 +855,7 @@ describe('JSON edge cases and error handling', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) // Date objects get serialized to strings in JSON const expectedJson = { @@ -891,7 +891,7 @@ describe('JSON performance tests', () => { plaintext: item, })) - const encryptedData = await protectClient.bulkEncrypt(jsonPayloads, { + const encryptedData = await encryptionClient.bulkEncrypt(jsonPayloads, { column: users.json, table: users, }) @@ -904,7 +904,7 @@ describe('JSON performance tests', () => { expect(encryptedData.data).toHaveLength(100) // Decrypt the data - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -944,7 +944,7 @@ describe('JSON advanced scenarios', () => { }, } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -956,7 +956,7 @@ describe('JSON advanced scenarios', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -971,7 +971,7 @@ describe('JSON advanced scenarios', () => { }, } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -983,7 +983,7 @@ describe('JSON advanced scenarios', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -1006,7 +1006,7 @@ describe('JSON advanced scenarios', () => { }, } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -1018,7 +1018,7 @@ describe('JSON advanced scenarios', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -1041,7 +1041,7 @@ describe('JSON advanced scenarios', () => { }, } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -1053,7 +1053,7 @@ describe('JSON advanced scenarios', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -1077,7 +1077,7 @@ describe('JSON advanced scenarios', () => { }, } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -1089,7 +1089,7 @@ describe('JSON advanced scenarios', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -1110,7 +1110,7 @@ describe('JSON error handling and edge cases', () => { invalidJson.circular = invalidJson try { - await protectClient.encrypt(invalidJson, { + await encryptionClient.encrypt(invalidJson, { column: users.json, table: users, }) @@ -1136,7 +1136,7 @@ describe('JSON error handling and edge cases', () => { }, } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -1148,7 +1148,7 @@ describe('JSON error handling and edge cases', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -1165,7 +1165,7 @@ describe('JSON error handling and edge cases', () => { }, } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -1177,7 +1177,7 @@ describe('JSON error handling and edge cases', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, @@ -1202,7 +1202,7 @@ describe('JSON error handling and edge cases', () => { }, } - const ciphertext = await protectClient.encrypt(json, { + const ciphertext = await encryptionClient.encrypt(json, { column: users.json, table: users, }) @@ -1214,7 +1214,7 @@ describe('JSON error handling and edge cases', () => { // Verify encrypted field expect(ciphertext.data).not.toHaveProperty('k') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: json, diff --git a/packages/stack/__tests__/keysets.test.ts b/packages/stack/__tests__/keysets.test.ts index 7f0437af..739a1c70 100644 --- a/packages/stack/__tests__/keysets.test.ts +++ b/packages/stack/__tests__/keysets.test.ts @@ -9,7 +9,7 @@ const users = encryptedTable('users', { describe('encryption and decryption with keyset id', () => { it('should encrypt and decrypt a payload', async () => { - const protectClient = await Encryption({ + const encryptionClient = await Encryption({ schemas: [users], keyset: { id: '4152449b-505a-4186-93b6-d3d87eba7a47', @@ -18,7 +18,7 @@ describe('encryption and decryption with keyset id', () => { const email = 'hello@example.com' - const ciphertext = await protectClient.encrypt(email, { + const ciphertext = await encryptionClient.encrypt(email, { column: users.email, table: users, }) @@ -32,7 +32,7 @@ describe('encryption and decryption with keyset id', () => { const a = ciphertext.data - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: email, @@ -42,7 +42,7 @@ describe('encryption and decryption with keyset id', () => { describe('encryption and decryption with keyset name', () => { it('should encrypt and decrypt a payload', async () => { - const protectClient = await Encryption({ + const encryptionClient = await Encryption({ schemas: [users], keyset: { name: 'Test', @@ -51,7 +51,7 @@ describe('encryption and decryption with keyset name', () => { const email = 'hello@example.com' - const ciphertext = await protectClient.encrypt(email, { + const ciphertext = await encryptionClient.encrypt(email, { column: users.email, table: users, }) @@ -65,7 +65,7 @@ describe('encryption and decryption with keyset name', () => { const a = ciphertext.data - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: email, diff --git a/packages/stack/__tests__/lock-context.test.ts b/packages/stack/__tests__/lock-context.test.ts index 0dbe72ae..9e2aaf31 100644 --- a/packages/stack/__tests__/lock-context.test.ts +++ b/packages/stack/__tests__/lock-context.test.ts @@ -18,10 +18,10 @@ type User = { number?: number } -let protectClient: EncryptionClient +let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ + encryptionClient = await Encryption({ schemas: [users], }) }) @@ -44,7 +44,7 @@ describe('encryption and decryption with lock context', () => { const email = 'hello@example.com' - const ciphertext = await protectClient + const ciphertext = await encryptionClient .encrypt(email, { column: users.email, table: users, @@ -55,7 +55,7 @@ describe('encryption and decryption with lock context', () => { throw new Error(`[encryption]: ${ciphertext.failure.message}`) } - const plaintext = await protectClient + const plaintext = await encryptionClient .decrypt(ciphertext.data) .withLockContext(lockContext.data) @@ -88,7 +88,7 @@ describe('encryption and decryption with lock context', () => { } // Encrypt the model with lock context - const encryptedModel = await protectClient + const encryptedModel = await encryptionClient .encryptModel(decryptedModel, users) .withLockContext(lockContext.data) @@ -97,7 +97,7 @@ describe('encryption and decryption with lock context', () => { } // Decrypt the model with lock context - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .decryptModel(encryptedModel.data) .withLockContext(lockContext.data) @@ -133,7 +133,7 @@ describe('encryption and decryption with lock context', () => { } // Encrypt the model with lock context - const encryptedModel = await protectClient + const encryptedModel = await encryptionClient .encryptModel(decryptedModel, users) .withLockContext(lockContext.data) @@ -142,7 +142,7 @@ describe('encryption and decryption with lock context', () => { } try { - await protectClient.decryptModel(encryptedModel.data) + await encryptionClient.decryptModel(encryptedModel.data) } catch (error) { const e = error as Error expect(e.message.startsWith('Failed to retrieve key')).toEqual(true) @@ -177,7 +177,7 @@ describe('encryption and decryption with lock context', () => { ] // Encrypt the models with lock context - const encryptedModels = await protectClient + const encryptedModels = await encryptionClient .bulkEncryptModels(decryptedModels, users) .withLockContext(lockContext.data) @@ -186,7 +186,7 @@ describe('encryption and decryption with lock context', () => { } // Decrypt the models with lock context - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .bulkDecryptModels(encryptedModels.data) .withLockContext(lockContext.data) diff --git a/packages/stack/__tests__/nested-models.test.ts b/packages/stack/__tests__/nested-models.test.ts index 8450f59c..4d7ba370 100644 --- a/packages/stack/__tests__/nested-models.test.ts +++ b/packages/stack/__tests__/nested-models.test.ts @@ -49,9 +49,9 @@ type User = { describe('encrypt models with nested fields', () => { it('should encrypt and decrypt a single value from a nested schema', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) - const encryptResponse = await protectClient.encrypt('hello world', { + const encryptResponse = await encryptionClient.encrypt('hello world', { column: users.example.field, table: users, }) @@ -63,7 +63,7 @@ describe('encrypt models with nested fields', () => { // Verify encrypted field expect(encryptResponse.data).toHaveProperty('c') - const decryptResponse = await protectClient.decrypt(encryptResponse.data) + const decryptResponse = await encryptionClient.decrypt(encryptResponse.data) if (decryptResponse.failure) { throw new Error(`[encryption]: ${decryptResponse.failure.message}`) @@ -75,7 +75,7 @@ describe('encrypt models with nested fields', () => { }) it('should encrypt and decrypt a model with nested fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '1', @@ -89,7 +89,7 @@ describe('encrypt models with nested fields', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -109,7 +109,7 @@ describe('encrypt models with nested fields', () => { expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -121,7 +121,7 @@ describe('encrypt models with nested fields', () => { }, 30000) it('should handle null values in nested fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '2', @@ -135,7 +135,7 @@ describe('encrypt models with nested fields', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -150,7 +150,7 @@ describe('encrypt models with nested fields', () => { expect(encryptedModel.data.example.field).toBeNull() expect(encryptedModel.data.example.nested?.deeper).toBeNull() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -162,7 +162,7 @@ describe('encrypt models with nested fields', () => { }, 30000) it('should handle undefined values in nested fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '3', @@ -174,7 +174,7 @@ describe('encrypt models with nested fields', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -188,7 +188,7 @@ describe('encrypt models with nested fields', () => { expect(encryptedModel.data.example.field).toBeUndefined() expect(encryptedModel.data.example.nested?.deeper).toBeUndefined() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -200,7 +200,7 @@ describe('encrypt models with nested fields', () => { }, 30000) it('should handle mixed null and undefined values in nested fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '4', @@ -217,7 +217,7 @@ describe('encrypt models with nested fields', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -240,7 +240,7 @@ describe('encrypt models with nested fields', () => { expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -252,7 +252,7 @@ describe('encrypt models with nested fields', () => { }, 30000) it('should handle deeply nested fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '3', @@ -264,7 +264,7 @@ describe('encrypt models with nested fields', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -280,7 +280,7 @@ describe('encrypt models with nested fields', () => { // Verify non-encrypted fields remain unchanged expect(encryptedModel.data.id).toBe('3') - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -292,7 +292,7 @@ describe('encrypt models with nested fields', () => { }, 30000) it('should handle missing optional nested fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '5', @@ -301,7 +301,7 @@ describe('encrypt models with nested fields', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -317,7 +317,7 @@ describe('encrypt models with nested fields', () => { expect(encryptedModel.data.id).toBe('5') expect(encryptedModel.data.example.nested).toBeUndefined() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -330,7 +330,7 @@ describe('encrypt models with nested fields', () => { describe('bulk operations with nested fields', () => { it('should handle bulk encryption and decryption of models with nested fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModels: User[] = [ { @@ -355,7 +355,7 @@ describe('encrypt models with nested fields', () => { }, ] - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -376,7 +376,7 @@ describe('encrypt models with nested fields', () => { expect(encryptedModels.data[0].id).toBe('1') expect(encryptedModels.data[1].id).toBe('2') - const decryptedResults = await protectClient.bulkDecryptModels( + const decryptedResults = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -388,7 +388,7 @@ describe('encrypt models with nested fields', () => { }, 30000) it('should handle bulk operations with null and undefined values in nested fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModels: User[] = [ { @@ -413,7 +413,7 @@ describe('encrypt models with nested fields', () => { }, ] - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -434,7 +434,7 @@ describe('encrypt models with nested fields', () => { expect(encryptedModels.data[0].id).toBe('1') expect(encryptedModels.data[1].id).toBe('2') - const decryptedResults = await protectClient.bulkDecryptModels( + const decryptedResults = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -446,7 +446,7 @@ describe('encrypt models with nested fields', () => { }, 30000) it('should handle bulk operations with missing optional nested fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModels: User[] = [ { @@ -468,7 +468,7 @@ describe('encrypt models with nested fields', () => { }, ] - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -489,7 +489,7 @@ describe('encrypt models with nested fields', () => { expect(encryptedModels.data[0].example.nested).toBeUndefined() expect(encryptedModels.data[1].id).toBe('2') - const decryptedResults = await protectClient.bulkDecryptModels( + const decryptedResults = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -501,11 +501,11 @@ describe('encrypt models with nested fields', () => { }, 30000) it('should handle empty array in bulk operations', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModels: User[] = [] - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -516,7 +516,7 @@ describe('encrypt models with nested fields', () => { expect(encryptedModels.data).toEqual([]) - const decryptedResults = await protectClient.bulkDecryptModels( + const decryptedResults = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -531,7 +531,7 @@ describe('encrypt models with nested fields', () => { describe('nested fields with a plaintext field', () => { it('should handle nested fields with a plaintext field', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '1', @@ -546,7 +546,7 @@ describe('nested fields with a plaintext field', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -567,7 +567,7 @@ describe('nested fields with a plaintext field', () => { expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) expect(encryptedModel.data.example.plaintext).toBe('plaintext') - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -579,7 +579,7 @@ describe('nested fields with a plaintext field', () => { }) it('should handle multiple plaintext fields at different nesting levels', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '1', @@ -599,7 +599,7 @@ describe('nested fields with a plaintext field', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -625,7 +625,7 @@ describe('nested fields with a plaintext field', () => { 'deeply nested plaintext', ) - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -637,7 +637,7 @@ describe('nested fields with a plaintext field', () => { }) it('should handle partial path matches in nested objects', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '1', @@ -658,7 +658,7 @@ describe('nested fields with a plaintext field', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -681,7 +681,7 @@ describe('nested fields with a plaintext field', () => { 'not encrypted', ) - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -693,7 +693,7 @@ describe('nested fields with a plaintext field', () => { }) it('should handle mixed encrypted and plaintext fields with similar paths', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '1', @@ -708,7 +708,7 @@ describe('nested fields with a plaintext field', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -729,7 +729,7 @@ describe('nested fields with a plaintext field', () => { 'not encrypted', ) - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -742,7 +742,7 @@ describe('nested fields with a plaintext field', () => { describe('bulk operations with plaintext fields', () => { it('should handle bulk encryption and decryption with plaintext fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModels: User[] = [ { @@ -773,7 +773,7 @@ describe('nested fields with a plaintext field', () => { }, ] - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -804,7 +804,7 @@ describe('nested fields with a plaintext field', () => { 'nested plaintext 2', ) - const decryptedResults = await protectClient.bulkDecryptModels( + const decryptedResults = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -816,7 +816,7 @@ describe('nested fields with a plaintext field', () => { }) it('should handle bulk operations with mixed encrypted and non-encrypted fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModels: User[] = [ { @@ -845,7 +845,7 @@ describe('nested fields with a plaintext field', () => { }, ] - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -878,7 +878,7 @@ describe('nested fields with a plaintext field', () => { 'not encrypted deeper 2', ) - const decryptedResults = await protectClient.bulkDecryptModels( + const decryptedResults = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -890,7 +890,7 @@ describe('nested fields with a plaintext field', () => { }) it('should handle bulk operations with deeply nested plaintext fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModels: User[] = [ { @@ -921,7 +921,7 @@ describe('nested fields with a plaintext field', () => { }, ] - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -948,7 +948,7 @@ describe('nested fields with a plaintext field', () => { 'deeply nested plaintext 2', ) - const decryptedResults = await protectClient.bulkDecryptModels( + const decryptedResults = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) diff --git a/packages/stack/__tests__/number-protect.test.ts b/packages/stack/__tests__/number-protect.test.ts index f635cda7..4abe1b75 100644 --- a/packages/stack/__tests__/number-protect.test.ts +++ b/packages/stack/__tests__/number-protect.test.ts @@ -32,10 +32,10 @@ type User = { } } -let protectClient: EncryptionClient +let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ + encryptionClient = await Encryption({ schemas: [users], }) }) @@ -57,7 +57,7 @@ describe('Number encryption and decryption', () => { test.each(cases)( 'should encrypt and decrypt a number: %d', async (age) => { - const ciphertext = await protectClient.encrypt(age, { + const ciphertext = await encryptionClient.encrypt(age, { column: users.age, table: users, }) @@ -69,7 +69,7 @@ describe('Number encryption and decryption', () => { // Verify encrypted field expect(ciphertext.data).toHaveProperty('c') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: age, @@ -79,7 +79,7 @@ describe('Number encryption and decryption', () => { ) it('should handle null integer', async () => { - const ciphertext = await protectClient.encrypt(null, { + const ciphertext = await encryptionClient.encrypt(null, { column: users.age, table: users, }) @@ -91,7 +91,7 @@ describe('Number encryption and decryption', () => { // Verify null is preserved expect(ciphertext.data).toBeNull() - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: null, @@ -102,7 +102,7 @@ describe('Number encryption and decryption', () => { it('should treat a negative zero valued float as 0.0', async () => { const score = -0.0 - const ciphertext = await protectClient.encrypt(score, { + const ciphertext = await encryptionClient.encrypt(score, { column: users.score, table: users, }) @@ -114,7 +114,7 @@ describe('Number encryption and decryption', () => { // Verify encrypted field expect(ciphertext.data).toHaveProperty('c') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: 0.0, @@ -125,7 +125,7 @@ describe('Number encryption and decryption', () => { it('should error for a NaN float', async () => { const score = Number.NaN - const result = await protectClient.encrypt(score, { + const result = await encryptionClient.encrypt(score, { column: users.score, table: users, }) @@ -138,7 +138,7 @@ describe('Number encryption and decryption', () => { it('should error for Infinity', async () => { const score = Number.POSITIVE_INFINITY - const result = await protectClient.encrypt(score, { + const result = await encryptionClient.encrypt(score, { column: users.score, table: users, }) @@ -151,7 +151,7 @@ describe('Number encryption and decryption', () => { it('should error for -Infinity', async () => { const score = Number.NEGATIVE_INFINITY - const result = await protectClient.encrypt(score, { + const result = await encryptionClient.encrypt(score, { column: users.score, table: users, }) @@ -173,7 +173,7 @@ describe('Model encryption and decryption', () => { updatedAt: new Date('2021-01-01'), } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -193,7 +193,7 @@ describe('Model encryption and decryption', () => { expect(encryptedModel.data.createdAt).toEqual(new Date('2021-01-01')) expect(encryptedModel.data.updatedAt).toEqual(new Date('2021-01-01')) - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -215,7 +215,7 @@ describe('Model encryption and decryption', () => { updatedAt: new Date('2021-01-01'), } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -230,7 +230,7 @@ describe('Model encryption and decryption', () => { expect(encryptedModel.data.age).toBeUndefined() expect(encryptedModel.data.score).toBeUndefined() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -252,7 +252,7 @@ describe('Model encryption and decryption', () => { updatedAt: new Date('2021-01-01'), } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -267,7 +267,7 @@ describe('Model encryption and decryption', () => { expect(encryptedModel.data.age).toBeUndefined() expect(encryptedModel.data.score).toBeUndefined() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -287,7 +287,7 @@ describe('Bulk encryption and decryption', () => { { id: 'user3', plaintext: -35.123 }, ] - const encryptedData = await protectClient.bulkEncrypt(intPayloads, { + const encryptedData = await encryptionClient.bulkEncrypt(intPayloads, { column: users.age, table: users, }) @@ -327,7 +327,7 @@ describe('Bulk encryption and decryption', () => { ) // Now decrypt the data - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -350,7 +350,7 @@ describe('Bulk encryption and decryption', () => { { id: 'user3', plaintext: 35 }, ] - const encryptedData = await protectClient.bulkEncrypt(intPayloads, { + const encryptedData = await encryptionClient.bulkEncrypt(intPayloads, { column: users.age, table: users, }) @@ -372,7 +372,7 @@ describe('Bulk encryption and decryption', () => { expect(encryptedData.data[2].data).toHaveProperty('c') // Now decrypt the data - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -410,7 +410,7 @@ describe('Bulk encryption and decryption', () => { }, ] - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -437,7 +437,7 @@ describe('Bulk encryption and decryption', () => { expect(encryptedModels.data[1].createdAt).toEqual(new Date('2021-01-01')) expect(encryptedModels.data[1].updatedAt).toEqual(new Date('2021-01-01')) - const decryptedResult = await protectClient.bulkDecryptModels( + const decryptedResult = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -467,7 +467,7 @@ describe('Encryption with lock context', () => { const age = 42 - const ciphertext = await protectClient + const ciphertext = await encryptionClient .encrypt(age, { column: users.age, table: users, @@ -481,7 +481,7 @@ describe('Encryption with lock context', () => { // Verify encrypted field expect(ciphertext.data).toHaveProperty('c') - const plaintext = await protectClient + const plaintext = await encryptionClient .decrypt(ciphertext.data) .withLockContext(lockContext.data) @@ -514,7 +514,7 @@ describe('Encryption with lock context', () => { score: 95, } - const encryptedModel = await protectClient + const encryptedModel = await encryptionClient .encryptModel(decryptedModel, users) .withLockContext(lockContext.data) @@ -527,7 +527,7 @@ describe('Encryption with lock context', () => { expect(encryptedModel.data.age).toHaveProperty('c') expect(encryptedModel.data.score).toHaveProperty('c') - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .decryptModel(encryptedModel.data) .withLockContext(lockContext.data) @@ -558,7 +558,7 @@ describe('Encryption with lock context', () => { { id: 'user2', plaintext: 30 }, ] - const encryptedData = await protectClient + const encryptedData = await encryptionClient .bulkEncrypt(intPayloads, { column: users.age, table: users, @@ -579,7 +579,7 @@ describe('Encryption with lock context', () => { expect(encryptedData.data[1].data).toHaveProperty('c') // Decrypt with lock context - const decryptedData = await protectClient + const decryptedData = await encryptionClient .bulkDecrypt(encryptedData.data) .withLockContext(lockContext.data) @@ -598,7 +598,7 @@ describe('Encryption with lock context', () => { describe('Nested object encryption', () => { it('should encrypt and decrypt nested number objects', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '1', @@ -609,7 +609,7 @@ describe('Nested object encryption', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -626,7 +626,7 @@ describe('Nested object encryption', () => { // Verify non-encrypted fields remain unchanged expect(encryptedModel.data.id).toBe('1') - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -638,7 +638,7 @@ describe('Nested object encryption', () => { }, 30000) it('should handle null values in nested objects with number fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel: User = { id: '2', @@ -649,7 +649,7 @@ describe('Nested object encryption', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -663,7 +663,7 @@ describe('Nested object encryption', () => { expect(encryptedModel.data.metadata?.count).toBeUndefined() expect(encryptedModel.data.metadata?.level).toBeUndefined() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -675,7 +675,7 @@ describe('Nested object encryption', () => { }, 30000) it('should handle undefined values in nested objects with number fields', async () => { - const protectClient = await Encryption({ schemas: [users] }) + const encryptionClient = await Encryption({ schemas: [users] }) const decryptedModel = { id: '3', @@ -686,7 +686,7 @@ describe('Nested object encryption', () => { }, } - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -700,7 +700,7 @@ describe('Nested object encryption', () => { expect(encryptedModel.data.metadata?.count).toBeUndefined() expect(encryptedModel.data.metadata?.level).toBeUndefined() - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -714,7 +714,7 @@ describe('Nested object encryption', () => { describe('encryptQuery for numbers', () => { it('should create encrypted query for number fields', async () => { - const result = await protectClient.encryptQuery([ + const result = await encryptionClient.encryptQuery([ { value: 25, column: users.age, table: users, queryType: 'equality' }, { value: 100, column: users.score, table: users, queryType: 'equality' }, ]) @@ -744,7 +744,7 @@ describe('Performance tests', () => { plaintext: item.data.age, })) - const encryptedData = await protectClient.bulkEncrypt(numPayloads, { + const encryptedData = await encryptionClient.bulkEncrypt(numPayloads, { column: users.age, table: users, }) @@ -757,7 +757,7 @@ describe('Performance tests', () => { expect(encryptedData.data).toHaveLength(100) // Decrypt the data - const decryptedData = await protectClient.bulkDecrypt(encryptedData.data) + const decryptedData = await encryptionClient.bulkDecrypt(encryptedData.data) if (decryptedData.failure) { throw new Error(`[encryption]: ${decryptedData.failure.message}`) @@ -786,7 +786,7 @@ describe('Advanced scenarios', () => { ] for (const value of boundaryValues) { - const ciphertext = await protectClient.encrypt(value, { + const ciphertext = await encryptionClient.encrypt(value, { column: users.age, table: users, }) @@ -798,7 +798,7 @@ describe('Advanced scenarios', () => { // Verify encrypted field expect(ciphertext.data).toHaveProperty('c') - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: value, @@ -822,7 +822,7 @@ describe('Invalid or uncoercable values', () => { test.each(invalidPlaintexts)( 'should fail to encrypt', async (input) => { - const result = await protectClient.encrypt(input, { + const result = await encryptionClient.encrypt(input, { column: users.age, table: users, }) diff --git a/packages/stack/__tests__/protect-ops.test.ts b/packages/stack/__tests__/protect-ops.test.ts index 9b7fea5e..a8978afc 100644 --- a/packages/stack/__tests__/protect-ops.test.ts +++ b/packages/stack/__tests__/protect-ops.test.ts @@ -17,17 +17,17 @@ type User = { number?: number } -let protectClient: EncryptionClient +let encryptionClient: EncryptionClient beforeAll(async () => { - protectClient = await Encryption({ + encryptionClient = await Encryption({ schemas: [users], }) }) describe('encryption and decryption edge cases', () => { it('should return null if plaintext is null', async () => { - const ciphertext = await protectClient.encrypt(null, { + const ciphertext = await encryptionClient.encrypt(null, { column: users.email, table: users, }) @@ -39,7 +39,7 @@ describe('encryption and decryption edge cases', () => { // Verify null is preserved expect(ciphertext.data).toBeNull() - const plaintext = await protectClient.decrypt(ciphertext.data) + const plaintext = await encryptionClient.decrypt(ciphertext.data) expect(plaintext).toEqual({ data: null, @@ -58,7 +58,7 @@ describe('encryption and decryption edge cases', () => { } // Encrypt the model - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -78,7 +78,7 @@ describe('encryption and decryption edge cases', () => { expect(encryptedModel.data.number).toBe(1) // Decrypt the model - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -108,7 +108,7 @@ describe('encryption and decryption edge cases', () => { } // Encrypt the model - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -128,7 +128,7 @@ describe('encryption and decryption edge cases', () => { expect(encryptedModel.data.number).toBe(1) // Decrypt the model - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -158,7 +158,7 @@ describe('encryption and decryption edge cases', () => { } // Encrypt the model - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( decryptedModel, users, ) @@ -178,7 +178,7 @@ describe('encryption and decryption edge cases', () => { expect(encryptedModel.data.number).toBe(1) // Decrypt the model - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -220,7 +220,7 @@ describe('bulk encryption', () => { ] // Encrypt the models - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -246,7 +246,7 @@ describe('bulk encryption', () => { expect(encryptedModels.data[1].number).toBe(2) // Decrypt the models - const decryptedResult = await protectClient.bulkDecryptModels( + const decryptedResult = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -276,7 +276,7 @@ describe('bulk encryption', () => { it('should return empty array if models is empty', async () => { // Encrypt empty array of models - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( [], users, ) @@ -290,7 +290,7 @@ describe('bulk encryption', () => { it('should return empty array if decrypting empty array of models', async () => { // Decrypt empty array of models - const decryptedResult = await protectClient.bulkDecryptModels([]) + const decryptedResult = await encryptionClient.bulkDecryptModels([]) if (decryptedResult.failure) { throw new Error(`[encryption]: ${decryptedResult.failure.message}`) @@ -330,7 +330,7 @@ describe('bulk encryption edge cases', () => { ] // Encrypt the models - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -362,7 +362,7 @@ describe('bulk encryption edge cases', () => { expect(encryptedModels.data[2].number).toBe(3) // Decrypt the models - const decryptedResult = await protectClient.bulkDecryptModels( + const decryptedResult = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -402,7 +402,7 @@ describe('bulk encryption edge cases', () => { ] // Encrypt the models - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -434,7 +434,7 @@ describe('bulk encryption edge cases', () => { expect(encryptedModels.data[2].number).toBe(3) // Decrypt the models - const decryptedResult = await protectClient.bulkDecryptModels( + const decryptedResult = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -469,7 +469,7 @@ describe('bulk encryption edge cases', () => { ] // Encrypt the models - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( decryptedModels, users, ) @@ -501,7 +501,7 @@ describe('bulk encryption edge cases', () => { expect(encryptedModels.data[2].number).toBe(3) // Decrypt the models - const decryptedResult = await protectClient.bulkDecryptModels( + const decryptedResult = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -525,7 +525,7 @@ describe('error handling', () => { } // First encrypt a valid model - const encryptedModel = await protectClient.encryptModel( + const encryptedModel = await encryptionClient.encryptModel( validModel, users, ) @@ -540,7 +540,7 @@ describe('error handling', () => { } try { - await protectClient.decryptModel(invalidModel as User) + await encryptionClient.decryptModel(invalidModel as User) throw new Error('Expected decryption to fail') } catch (error) { expect(error).toBeDefined() @@ -558,7 +558,7 @@ describe('error handling', () => { } try { - await protectClient.encryptModel(model, users) + await encryptionClient.encryptModel(model, users) throw new Error('Expected encryption to fail') } catch (error) { expect(error).toBeDefined() @@ -584,14 +584,17 @@ describe('type safety', () => { } // Encrypt the model - const encryptedModel = await protectClient.encryptModel(model, users) + const encryptedModel = await encryptionClient.encryptModel( + model, + users, + ) if (encryptedModel.failure) { throw new Error(`[encryption]: ${encryptedModel.failure.message}`) } // Decrypt the model - const decryptedResult = await protectClient.decryptModel( + const decryptedResult = await encryptionClient.decryptModel( encryptedModel.data, ) @@ -617,7 +620,7 @@ describe('performance', () => { })) // Encrypt the models - const encryptedModels = await protectClient.bulkEncryptModels( + const encryptedModels = await encryptionClient.bulkEncryptModels( largeModels, users, ) @@ -627,7 +630,7 @@ describe('performance', () => { } // Decrypt the models - const decryptedResult = await protectClient.bulkDecryptModels( + const decryptedResult = await encryptionClient.bulkDecryptModels( encryptedModels.data, ) @@ -657,7 +660,7 @@ describe('encryption and decryption with lock context', () => { const email = 'hello@example.com' - const ciphertext = await protectClient + const ciphertext = await encryptionClient .encrypt(email, { column: users.email, table: users, @@ -668,7 +671,7 @@ describe('encryption and decryption with lock context', () => { throw new Error(`[encryption]: ${ciphertext.failure.message}`) } - const plaintext = await protectClient + const plaintext = await encryptionClient .decrypt(ciphertext.data) .withLockContext(lockContext.data) @@ -701,7 +704,7 @@ describe('encryption and decryption with lock context', () => { } // Encrypt the model with lock context - const encryptedModel = await protectClient + const encryptedModel = await encryptionClient .encryptModel(decryptedModel, users) .withLockContext(lockContext.data) @@ -710,7 +713,7 @@ describe('encryption and decryption with lock context', () => { } // Decrypt the model with lock context - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .decryptModel(encryptedModel.data) .withLockContext(lockContext.data) @@ -746,7 +749,7 @@ describe('encryption and decryption with lock context', () => { } // Encrypt the model with lock context - const encryptedModel = await protectClient + const encryptedModel = await encryptionClient .encryptModel(decryptedModel, users) .withLockContext(lockContext.data) @@ -755,7 +758,7 @@ describe('encryption and decryption with lock context', () => { } try { - await protectClient.decryptModel(encryptedModel.data) + await encryptionClient.decryptModel(encryptedModel.data) } catch (error) { const e = error as Error expect(e.message.startsWith('Failed to retrieve key')).toEqual(true) @@ -790,7 +793,7 @@ describe('encryption and decryption with lock context', () => { ] // Encrypt the models with lock context - const encryptedModels = await protectClient + const encryptedModels = await encryptionClient .bulkEncryptModels(decryptedModels, users) .withLockContext(lockContext.data) @@ -799,7 +802,7 @@ describe('encryption and decryption with lock context', () => { } // Decrypt the models with lock context - const decryptedResult = await protectClient + const decryptedResult = await encryptionClient .bulkDecryptModels(encryptedModels.data) .withLockContext(lockContext.data) @@ -825,7 +828,7 @@ describe('special characters', () => { const plaintext = 'complex@string-with/slashes\\backslashes.and#symbols$%&+!@#$%^&*()_+-=[]{}|;:,.<>?/~`' - const ciphertext = await protectClient.encrypt(plaintext, { + const ciphertext = await encryptionClient.encrypt(plaintext, { column: users.email, table: users, }) @@ -834,7 +837,7 @@ describe('special characters', () => { throw new Error(`[encryption]: ${ciphertext.failure.message}`) } - const decrypted = await protectClient.decrypt(ciphertext.data) + const decrypted = await encryptionClient.decrypt(ciphertext.data) expect(decrypted).toEqual({ data: plaintext, diff --git a/packages/stack/__tests__/supabase.test.ts b/packages/stack/__tests__/supabase.test.ts index 77fb7bf1..1834efd4 100644 --- a/packages/stack/__tests__/supabase.test.ts +++ b/packages/stack/__tests__/supabase.test.ts @@ -69,11 +69,11 @@ afterAll(async () => { describe('supabase', () => { it('should insert and select encrypted data', async () => { - const protectClient = await Encryption({ schemas: [table] }) + const encryptionClient = await Encryption({ schemas: [table] }) const e = 'hello world' - const ciphertext = await protectClient.encrypt(e, { + const ciphertext = await encryptionClient.encrypt(e, { column: table.encrypted, table: table, }) @@ -106,7 +106,7 @@ describe('supabase', () => { } const dataToDecrypt = data[0].encrypted as Encrypted - const plaintext = await protectClient.decrypt(dataToDecrypt) + const plaintext = await encryptionClient.decrypt(dataToDecrypt) expect(plaintext).toEqual({ data: e, @@ -114,14 +114,14 @@ describe('supabase', () => { }, 30000) it('should insert and select encrypted model data', async () => { - const protectClient = await Encryption({ schemas: [table] }) + const encryptionClient = await Encryption({ schemas: [table] }) const model = { encrypted: 'hello world', otherField: 'not encrypted', } - const encryptedModel = await protectClient.encryptModel(model, table) + const encryptedModel = await encryptionClient.encryptModel(model, table) if (encryptedModel.failure) { throw new Error(`[encryption]: ${encryptedModel.failure.message}`) @@ -156,7 +156,7 @@ describe('supabase', () => { throw new Error('Expected encrypted payload') } - const decryptedModel = await protectClient.decryptModel(data[0]) + const decryptedModel = await encryptionClient.decryptModel(data[0]) if (decryptedModel.failure) { throw new Error(`[encryption]: ${decryptedModel.failure.message}`) @@ -169,7 +169,7 @@ describe('supabase', () => { }, 30000) it('should insert and select bulk encrypted model data', async () => { - const protectClient = await Encryption({ schemas: [table] }) + const encryptionClient = await Encryption({ schemas: [table] }) const models = [ { @@ -182,7 +182,10 @@ describe('supabase', () => { }, ] - const encryptedModels = await protectClient.bulkEncryptModels(models, table) + const encryptedModels = await encryptionClient.bulkEncryptModels( + models, + table, + ) if (encryptedModels.failure) { throw new Error(`[encryption]: ${encryptedModels.failure.message}`) @@ -218,7 +221,7 @@ describe('supabase', () => { throw new Error(`[encryption]: ${error.message}`) } - const decryptedModels = await protectClient.bulkDecryptModels(data) + const decryptedModels = await encryptionClient.bulkDecryptModels(data) if (decryptedModels.failure) { throw new Error(`[encryption]: ${decryptedModels.failure.message}`) @@ -235,7 +238,7 @@ describe('supabase', () => { }, 30000) it('should insert and query encrypted number data with equality', async () => { - const protectClient = await Encryption({ schemas: [table] }) + const encryptionClient = await Encryption({ schemas: [table] }) const testAge = 25 const model = { @@ -243,7 +246,7 @@ describe('supabase', () => { otherField: 'not encrypted', } - const encryptedModel = await protectClient.encryptModel(model, table) + const encryptedModel = await encryptionClient.encryptModel(model, table) if (encryptedModel.failure) { throw new Error(`[encryption]: ${encryptedModel.failure.message}`) @@ -267,7 +270,7 @@ describe('supabase', () => { insertedIds.push(insertedRecordId) // Create encrypted query for equality search with composite-literal returnType - const encryptedResult = await protectClient.encryptQuery([ + const encryptedResult = await encryptionClient.encryptQuery([ { value: testAge, column: table.age, @@ -298,7 +301,7 @@ describe('supabase', () => { // Verify we found our specific row with encrypted age match expect(data).toHaveLength(1) - const decryptedModel = await protectClient.decryptModel(data[0]) + const decryptedModel = await encryptionClient.decryptModel(data[0]) if (decryptedModel.failure) { throw new Error(`[encryption]: ${decryptedModel.failure.message}`) diff --git a/packages/stack/src/ffi/helpers/validation.ts b/packages/stack/src/ffi/helpers/validation.ts index e2e60b94..3962398b 100644 --- a/packages/stack/src/ffi/helpers/validation.ts +++ b/packages/stack/src/ffi/helpers/validation.ts @@ -7,7 +7,7 @@ import type { FfiIndexTypeName } from '../../types' * Returns a failure Result if validation fails, undefined otherwise. * Use this in async flows that return Result types. * - * Uses `never` as the success type so the result can be assigned to any Result. + * Uses `never` as the success type so the result can be assigned to any Result. * * @internal */ diff --git a/packages/stack/src/ffi/index.ts b/packages/stack/src/ffi/index.ts index fdea7bac..b9aca0df 100644 --- a/packages/stack/src/ffi/index.ts +++ b/packages/stack/src/ffi/index.ts @@ -37,7 +37,7 @@ import { EncryptQueryOperation } from './operations/encrypt-query' export const noClientError = () => new Error( - 'The EQL client has not been initialized. Please call init() before using the client.', + 'The Encryption client has not been initialized. Please call init() before using the client.', ) /** The EncryptionClient is the main entry point for interacting with the CipherStash encryption library. @@ -76,7 +76,7 @@ export class EncryptionClient { ) logger.debug( - 'Initializing the Stash Encryption client with the following encrypt config:', + 'Initializing the Encryption client with the following encrypt config:', { encryptConfig: validated, }, @@ -95,7 +95,7 @@ export class EncryptionClient { this.encryptConfig = validated - logger.info('Successfully initialized the Stash Encryption client.') + logger.info('Successfully initialized the Encryption client.') return this }, (error: unknown) => ({ diff --git a/packages/stack/src/helpers/index.ts b/packages/stack/src/helpers/index.ts index fa6dc719..a2dd9d0c 100644 --- a/packages/stack/src/helpers/index.ts +++ b/packages/stack/src/helpers/index.ts @@ -26,14 +26,14 @@ export function encryptedToPgComposite(obj: Encrypted): EncryptedPgComposite { * @example * ```typescript * // Before (deprecated): - * const [encrypted] = await protectClient.encryptQuery([ + * const [encrypted] = await encryptionClient.encryptQuery([ * { value: searchValue, column, table, queryType: 'equality' } * ]) * const literal = encryptedToCompositeLiteral(encrypted) * await supabase.from('table').select().eq('column', literal) * * // After (recommended): - * const [searchTerm] = await protectClient.encryptQuery([ + * const [searchTerm] = await encryptionClient.encryptQuery([ * { value: searchValue, column, table, queryType: 'equality', returnType: 'composite-literal' } * ]) * await supabase.from('table').select().eq('column', searchTerm) @@ -55,13 +55,13 @@ export function encryptedToCompositeLiteral(obj: CipherStashEncrypted): string { * @example * ```typescript * // Before (deprecated): - * const [encrypted] = await protectClient.encryptQuery([ + * const [encrypted] = await encryptionClient.encryptQuery([ * { value: searchValue, column, table, queryType: 'equality' } * ]) * const escapedLiteral = encryptedToEscapedCompositeLiteral(encrypted) * * // After (recommended): - * const [searchTerm] = await protectClient.encryptQuery([ + * const [searchTerm] = await encryptionClient.encryptQuery([ * { value: searchValue, column, table, queryType: 'equality', returnType: 'escaped-composite-literal' } * ]) * ``` diff --git a/packages/utils/logger/index.ts b/packages/utils/logger/index.ts index 93af0474..0bb555fc 100644 --- a/packages/utils/logger/index.ts +++ b/packages/utils/logger/index.ts @@ -16,19 +16,19 @@ const currentLevel = getLevelValue(envLogLevel) function debug(...args: unknown[]): void { if (currentLevel <= getLevelValue('debug')) { - console.debug('[protect] DEBUG', ...args) + console.debug('[encryption] DEBUG', ...args) } } function info(...args: unknown[]): void { if (currentLevel <= getLevelValue('info')) { - console.info('[protect] INFO', ...args) + console.info('[encryption] INFO', ...args) } } function error(...args: unknown[]): void { if (currentLevel <= getLevelValue('error')) { - console.error('[protect] ERROR', ...args) + console.error('[encryption] ERROR', ...args) } } From 7c5a3aa0c640a4e82cc1e76574c1455e6dc42021 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Wed, 11 Feb 2026 18:33:44 -0700 Subject: [PATCH 6/9] fix: deep branding rename across dynamodb, drizzle, examples, and docs (round 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renamed internal variables, error classes, and public API functions: - dynamodb: protectTable→table params, ProtectDynamoDBError→EncryptedDynamoDBError - drizzle: ProtectOperatorError→EncryptionOperatorError, createProtectOperators→createEncryptionOperators - nest example: full ProtectModule→EncryptionModule rebrand across all files - docs/examples: remaining protect* variable and function references All deprecated aliases preserved for backward compatibility. Co-Authored-By: Claude Opus 4.6 --- docs/README.md | 2 +- docs/concepts/searchable-encryption.md | 6 +- docs/reference/drizzle/DRIFT-TESTING.md | 28 +-- docs/reference/drizzle/drizzle-protect.md | 74 ++++---- docs/reference/drizzle/drizzle.md | 36 ++-- docs/reference/schema.md | 10 +- .../drizzle/src/controllers/transactions.ts | 10 +- examples/drizzle/src/protect/config.ts | 6 +- examples/dynamo/src/bulk-operations.ts | 8 +- examples/dynamo/src/encrypted-key-in-gsi.ts | 8 +- .../dynamo/src/encrypted-partition-key.ts | 11 +- examples/dynamo/src/encrypted-sort-key.ts | 11 +- examples/dynamo/src/export-to-pg.ts | 6 +- examples/dynamo/src/simple.ts | 11 +- examples/nest/README.md | 2 +- examples/nest/src/app.controller.spec.ts | 12 +- examples/nest/src/app.module.ts | 4 +- examples/nest/src/app.service.ts | 16 +- .../protect/decorators/decrypt.decorator.ts | 20 +-- .../protect/decorators/encrypt.decorator.ts | 22 +-- examples/nest/src/protect/index.ts | 8 +- .../interceptors/decrypt.interceptor.ts | 22 +-- .../interceptors/encrypt.interceptor.ts | 22 +-- .../interfaces/protect-config.interface.ts | 2 +- .../nest/src/protect/protect.constants.ts | 4 +- examples/nest/src/protect/protect.module.ts | 54 +++--- .../nest/src/protect/protect.service.spec.ts | 14 +- examples/nest/src/protect/protect.service.ts | 6 +- .../protect/utils/get-protect-service.util.ts | 10 +- examples/nest/test/app.e2e-spec.ts | 8 +- .../next-drizzle-mysql/src/app/actions.ts | 4 +- examples/next-drizzle-mysql/src/app/page.tsx | 2 +- examples/nextjs-clerk/src/lib/actions.ts | 6 +- examples/typeorm/README.md | 6 +- packages/drizzle/README.md | 38 ++-- packages/drizzle/__tests__/docs.test.ts | 18 +- packages/drizzle/__tests__/drizzle.test.ts | 28 +-- packages/drizzle/src/pg/index.ts | 20 ++- packages/drizzle/src/pg/operators.ts | 168 +++++++++++------- packages/drizzle/src/pg/schema-extraction.ts | 9 +- packages/dynamodb/README.md | 30 ++-- packages/dynamodb/__tests__/audit.test.ts | 32 ++-- packages/dynamodb/__tests__/dynamodb.test.ts | 28 ++- .../dynamodb/__tests__/error-codes.test.ts | 15 +- packages/dynamodb/src/index.ts | 16 +- .../dynamodb/src/operations/base-operation.ts | 10 +- .../src/operations/bulk-decrypt-models.ts | 8 +- .../src/operations/bulk-encrypt-models.ts | 10 +- .../dynamodb/src/operations/decrypt-model.ts | 11 +- .../dynamodb/src/operations/encrypt-model.ts | 10 +- .../dynamodb/src/operations/search-terms.ts | 2 +- packages/dynamodb/src/types.ts | 10 +- packages/stack/README.md | 6 +- 53 files changed, 484 insertions(+), 456 deletions(-) diff --git a/docs/README.md b/docs/README.md index bc5243ee..19c82a48 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,7 +21,7 @@ The documentation for Stash Encryption is organized into the following sections: ### Drizzle ORM Integration -- [Protect Operators Pattern](reference/drizzle/drizzle.md) - Recommended approach with auto-encrypting operators +- [Encryption Operators Pattern](reference/drizzle/drizzle.md) - Recommended approach with auto-encrypting operators - [Manual Encryption Pattern](reference/drizzle/drizzle-protect.md) - Explicit control over encryption workflow ## How-to guides diff --git a/docs/concepts/searchable-encryption.md b/docs/concepts/searchable-encryption.md index 019ea0e0..f29eab2f 100644 --- a/docs/concepts/searchable-encryption.md +++ b/docs/concepts/searchable-encryption.md @@ -72,8 +72,8 @@ const searchTerm = 'alice.johnson@example.com' const encryptedParam = await encryptionClient.createSearchTerms([{ value: searchTerm, - table: protectedUsers, // Reference to the Protect table schema - column: protectedUsers.email, // Your Protect column definition + table: encryptedUsers, // Reference to the encrypted table schema + column: encryptedUsers.email, // Your encrypted column definition }]) if (encryptedParam.failure) { @@ -90,7 +90,7 @@ const equalitySQL = ` // 3) Execute the query, passing in the Postgres column name // and the encrypted search term as the second parameter // (client is an arbitrary Postgres client) -const result = await client.query(equalitySQL, [ protectedUser.email.getName(), encryptedParam.data ]) +const result = await client.query(equalitySQL, [ encryptedUsers.email.getName(), encryptedParam.data ]) ``` Using the above approach, Stash Encryption is generating the EQL payloads and which means you never have to drop down to writing complex SQL queries. diff --git a/docs/reference/drizzle/DRIFT-TESTING.md b/docs/reference/drizzle/DRIFT-TESTING.md index 96f0d4f5..145f939c 100644 --- a/docs/reference/drizzle/DRIFT-TESTING.md +++ b/docs/reference/drizzle/DRIFT-TESTING.md @@ -27,7 +27,7 @@ packages/drizzle/ │ └── code-executor.test.ts # Executor unit tests │ docs/reference/drizzle/ -├── drizzle.md # Protect operators pattern (recommended) +├── drizzle.md # Encryption operators pattern (recommended) ├── drizzle-protect.md # Manual encryption pattern (verbose) └── DRIFT-TESTING.md # This document ``` @@ -63,9 +63,9 @@ The executor (`code-executor.ts`) runs extracted code blocks in a controlled con const context: ExecutionContext = { db, // Drizzle database instance transactions, // Table schema - protect, // Protect operators (eq, gte, like, etc.) - encryptionClient, // Raw protect client for manual encryption - protectTransactions, // Protect schema for encryption + encryption, // Encryption operators (eq, gte, like, etc.) + encryptionClient, // Encryption client for manual encryption + encryptionTransactions, // Encryption schema for encryption eq, gte, lte, ilike, // Drizzle operators and, or, desc, asc, // Drizzle combinators sql, inArray, // Drizzle utilities @@ -230,7 +230,7 @@ Explanation of what this example demonstrates. // Your executable code here const results = await db.select() .from(transactions) - .where(await protect.eq(transactions.amount, 800.00)) + .where(await encryption.eq(transactions.amount, 800.00)) return results ​``` ``` @@ -280,7 +280,7 @@ Failed block at line 156: --- const results = await db.select() .from(transactions) - .where(await protect.eq(transactions.nonexistent, 'value')) + .where(await encryption.eq(transactions.nonexistent, 'value')) return results --- Error: Column "nonexistent" does not exist @@ -295,17 +295,17 @@ When writing `ts:run` blocks, these variables are available: |----------|------|-------------| | `db` | Drizzle instance | Database connection | | `transactions` | Table schema | The test table definition | -| `protectTransactions` | Protect schema | Schema for encryption operations | +| `encryptionTransactions` | Encryption schema | Schema for encryption operations | -### Protect Operators (Auto-Encrypting) +### Encryption Operators (Auto-Encrypting) | Variable | Description | |----------|-------------| -| `protect.eq(column, value)` | Equality match on encrypted field | -| `protect.gte(column, value)` | Greater than or equal | -| `protect.lte(column, value)` | Less than or equal | -| `protect.like(column, pattern)` | Text search with wildcards | +| `encryption.eq(column, value)` | Equality match on encrypted field | +| `encryption.gte(column, value)` | Greater than or equal | +| `encryption.lte(column, value)` | Less than or equal | +| `encryption.like(column, pattern)` | Text search with wildcards | -### Protect Client (Manual Encryption) +### Encryption Client (Manual Encryption) | Variable | Description | |----------|-------------| | `encryptionClient.encrypt(value, opts)` | Encrypt a single value | @@ -339,7 +339,7 @@ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) ✅ **Match seed data values** when demonstrating specific queries: ```typescript // Seed has account '1234567890' with amount 800.00 -await protect.eq(transactions.account, '1234567890') +await encryption.eq(transactions.account, '1234567890') ``` ✅ **Keep examples focused** - one concept per block diff --git a/docs/reference/drizzle/drizzle-protect.md b/docs/reference/drizzle/drizzle-protect.md index b010b357..135e2129 100644 --- a/docs/reference/drizzle/drizzle-protect.md +++ b/docs/reference/drizzle/drizzle-protect.md @@ -107,8 +107,8 @@ Find transactions with a specific amount. First encrypt the search value, then u ```ts:run // Encrypt the search value const encryptedAmount = await encryptionClient.encrypt(800.00, { - table: protectTransactions, - column: protectTransactions.amount + table: encryptionTransactions, + column: encryptionTransactions.amount }) // Query with regular Drizzle eq() @@ -128,8 +128,8 @@ Find transactions with a specific description using manual encryption. ```ts:run // Encrypt the search value const encryptedDesc = await encryptionClient.encrypt('Salary deposit', { - table: protectTransactions, - column: protectTransactions.description + table: encryptionTransactions, + column: encryptionTransactions.description }) // Query with regular Drizzle eq() @@ -149,12 +149,12 @@ Find transactions matching multiple encrypted fields using manual encryption. ```ts:run // Encrypt both search values const encryptedAccount = await encryptionClient.encrypt('1234567890', { - table: protectTransactions, - column: protectTransactions.account_number + table: encryptionTransactions, + column: encryptionTransactions.account_number }) const encryptedAmount = await encryptionClient.encrypt(800.00, { - table: protectTransactions, - column: protectTransactions.amount + table: encryptionTransactions, + column: encryptionTransactions.amount }) // Query with regular Drizzle operators @@ -183,8 +183,8 @@ Find transactions with amounts less than or equal to $150 using manual encryptio ```ts:run // Encrypt the comparison value const encryptedAmount = await encryptionClient.encrypt(150.00, { - table: protectTransactions, - column: protectTransactions.amount + table: encryptionTransactions, + column: encryptionTransactions.amount }) // Query with regular Drizzle lte() @@ -204,8 +204,8 @@ Find transactions with amounts greater than or equal to $1250. ```ts:run // Encrypt the comparison value const encryptedAmount = await encryptionClient.encrypt(1250.00, { - table: protectTransactions, - column: protectTransactions.amount + table: encryptionTransactions, + column: encryptionTransactions.amount }) // Query with regular Drizzle gte() @@ -226,13 +226,13 @@ return decrypted.data Search for transactions with "gym" in the description. With the manual encryption pattern, you must encrypt the search pattern and cast it to the encrypted type. -**Note:** Unlike the encryption operators pattern which provides `protect.like()` wrapper, the manual encryption pattern requires using Drizzle's `sql` template with manual type casting. This gives you full control over the encryption and query construction at the cost of more verbose syntax. +**Note:** Unlike the encryption operators pattern which provides `encryption.like()` wrapper, the manual encryption pattern requires using Drizzle's `sql` template with manual type casting. This gives you full control over the encryption and query construction at the cost of more verbose syntax. ```ts:run // Encrypt the search pattern const encryptedPattern = await encryptionClient.encrypt('%gym%', { - table: protectTransactions, - column: protectTransactions.description + table: encryptionTransactions, + column: encryptionTransactions.description }) // Cast encrypted pattern to jsonb then to eql_v2_encrypted type @@ -252,14 +252,14 @@ Combine text search with manual encryption for other fields. All search values m ```ts:run // Encrypt the amount comparison value const encryptedAmount = await encryptionClient.encrypt(150.00, { - table: protectTransactions, - column: protectTransactions.amount + table: encryptionTransactions, + column: encryptionTransactions.amount }) // Encrypt the search pattern for text search const encryptedPattern = await encryptionClient.encrypt('%payment%', { - table: protectTransactions, - column: protectTransactions.description + table: encryptionTransactions, + column: encryptionTransactions.description }) const results = await db.select() @@ -287,12 +287,12 @@ Find transactions with amounts between $150 and $1250. ```ts:run // Encrypt both range boundaries const encryptedMin = await encryptionClient.encrypt(150.00, { - table: protectTransactions, - column: protectTransactions.amount + table: encryptionTransactions, + column: encryptionTransactions.amount }) const encryptedMax = await encryptionClient.encrypt(1250.00, { - table: protectTransactions, - column: protectTransactions.amount + table: encryptionTransactions, + column: encryptionTransactions.amount }) // Query with regular Drizzle operators @@ -321,12 +321,12 @@ const twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000) // Encrypt both date boundaries const encryptedStart = await encryptionClient.encrypt(twoWeeksAgo.getTime(), { - table: protectTransactions, - column: protectTransactions.created_at + table: encryptionTransactions, + column: encryptionTransactions.created_at }) const encryptedEnd = await encryptionClient.encrypt(now.getTime(), { - table: protectTransactions, - column: protectTransactions.created_at + table: encryptionTransactions, + column: encryptionTransactions.created_at }) // Query with regular Drizzle operators @@ -419,8 +419,8 @@ Count transactions with amounts greater than or equal to $1250. ```ts:run // Encrypt the search value const encryptedAmount = await encryptionClient.encrypt(1250.00, { - table: protectTransactions, - column: protectTransactions.amount + table: encryptionTransactions, + column: encryptionTransactions.amount }) const results = await db.select({ count: sql`count(*)` }) @@ -465,10 +465,10 @@ When using the manual encryption pattern instead of encryption operators, you ha | Aspect | Encryption Operators | Manual Encryption | |--------|------------------|-------------------| -| Encryption | `protect.eq(col, val)` | `encrypt()` + `eq()` | +| Encryption | `encryption.eq(col, val)` | `encrypt()` + `eq()` | | Code | Auto-encrypting operators | Explicit encryption calls | | Decryption | Handled by executor | Manual with `bulkDecryptModels()` | -| LIKE queries | `protect.like()` | `encrypt()` + `ilike()` | +| LIKE queries | `encryption.like()` | `encrypt()` + `ilike()` | | Control | High-level | Low-level | | Use case | Clean syntax | Full control | @@ -515,8 +515,8 @@ When using the manual encryption pattern instead of encryption operators, you ha ```ts // Single value encryption const encrypted = await encryptionClient.encrypt(plaintext, { - table: protectTransactions, - column: protectTransactions.column_name + table: encryptionTransactions, + column: encryptionTransactions.column_name }) // Use encrypted.data in queries @@ -546,10 +546,10 @@ transactions.description transactions.createdAt // Encryption schema (for encryption operations) -protectTransactions.account_number // Note: snake_case -protectTransactions.amount -protectTransactions.description -protectTransactions.created_at // Note: snake_case +encryptionTransactions.account_number // Note: snake_case +encryptionTransactions.amount +encryptionTransactions.description +encryptionTransactions.created_at // Note: snake_case ``` --- diff --git a/docs/reference/drizzle/drizzle.md b/docs/reference/drizzle/drizzle.md index afd4492c..b9b1a398 100644 --- a/docs/reference/drizzle/drizzle.md +++ b/docs/reference/drizzle/drizzle.md @@ -3,10 +3,10 @@ This page demonstrates how to perform queries on encrypted data using **Drizzle ORM** with **CipherStash Stash Encryption** using the **encryption operators pattern**. -**Pattern:** Auto-encrypting operators from `createProtectOperators()` provide clean syntax with automatic encryption. +**Pattern:** Auto-encrypting operators from `createEncryptionOperators()` provide clean syntax with automatic encryption. **How it works:** -- Use `await protect.eq()`, `await protect.gte()`, `await protect.like()` for queries +- Use `await encryption.eq()`, `await encryption.gte()`, `await encryption.like()` for queries - Operators automatically detect encrypted columns and encrypt query values - Results are automatically decrypted by the executor @@ -103,7 +103,7 @@ Find transactions with a specific amount. ```ts:run const results = await db.select() .from(transactions) - .where(await protect.eq(transactions.amount, 800.00)) + .where(await encryption.eq(transactions.amount, 800.00)) return results ``` @@ -114,7 +114,7 @@ Find transactions with a specific description. ```ts:run const results = await db.select() .from(transactions) - .where(await protect.eq(transactions.description, 'Salary deposit')) + .where(await encryption.eq(transactions.description, 'Salary deposit')) return results ``` @@ -127,8 +127,8 @@ const results = await db.select() .from(transactions) .where( and( - await protect.eq(transactions.account, '1234567890'), - await protect.eq(transactions.amount, 800.00) + await encryption.eq(transactions.account, '1234567890'), + await encryption.eq(transactions.amount, 800.00) ) ) return results @@ -145,7 +145,7 @@ Find transactions with amounts less than or equal to $150. ```ts:run const results = await db.select() .from(transactions) - .where(await protect.lte(transactions.amount, 150.00)) + .where(await encryption.lte(transactions.amount, 150.00)) return results ``` @@ -156,7 +156,7 @@ Find transactions with amounts greater than or equal to $1250. ```ts:run const results = await db.select() .from(transactions) - .where(await protect.gte(transactions.amount, 1250.00)) + .where(await encryption.gte(transactions.amount, 1250.00)) return results ``` @@ -171,7 +171,7 @@ Search for transactions with "gym" in the description using full-text search. ```ts:run const results = await db.select() .from(transactions) - .where(await protect.like(transactions.description, '%gym%')) + .where(await encryption.like(transactions.description, '%gym%')) return results ``` @@ -184,8 +184,8 @@ const results = await db.select() .from(transactions) .where( and( - await protect.gte(transactions.amount, 150.00), - await protect.like(transactions.description, '%payment%') + await encryption.gte(transactions.amount, 150.00), + await encryption.like(transactions.description, '%payment%') ) ) return results @@ -204,8 +204,8 @@ const results = await db.select() .from(transactions) .where( and( - await protect.gte(transactions.amount, 150.00), - await protect.lte(transactions.amount, 1250.00) + await encryption.gte(transactions.amount, 150.00), + await encryption.lte(transactions.amount, 1250.00) ) ) return results @@ -223,8 +223,8 @@ const results = await db.select() .from(transactions) .where( and( - await protect.gte(transactions.createdAt, twoWeeksAgo.getTime()), - await protect.lte(transactions.createdAt, now.getTime()) + await encryption.gte(transactions.createdAt, twoWeeksAgo.getTime()), + await encryption.lte(transactions.createdAt, now.getTime()) ) ) return results @@ -293,7 +293,7 @@ Count transactions with amounts greater than or equal to $1250. ```ts:run const result = await db.select({ count: sql`count(*)` }) .from(transactions) - .where(await protect.gte(transactions.amount, 1250.00)) + .where(await encryption.gte(transactions.amount, 1250.00)) return result ``` @@ -318,7 +318,7 @@ All results are automatically **decrypted** by Stash Encryption before being ret ### What's happening behind the scenes 1. **Query Construction**: You write normal Drizzle queries -2. **Encryption**: `protect` operators encrypt your search values +2. **Encryption**: `encryption` operators encrypt your search values 3. **Database Search**: PostgreSQL searches encrypted data using special indexes 4. **Decryption**: Results are decrypted before being returned 5. **Display**: You see plain text results @@ -327,7 +327,7 @@ All results are automatically **decrypted** by Stash Encryption before being ret - ✅ **Data encrypted at rest** - Database breaches don't expose sensitive data - ✅ **Searchable encryption** - Equality, range, and text search work on encrypted fields -- ✅ **Familiar API** - Use standard Drizzle syntax with `protect` +- ✅ **Familiar API** - Use standard Drizzle syntax with `encryption` - ✅ **Automatic decryption** - No manual decryption needed - ✅ **Type safety** - Full TypeScript support diff --git a/docs/reference/schema.md b/docs/reference/schema.md index f0fc0962..df4184e3 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -59,7 +59,7 @@ Start by importing the `encryptedTable` and `encryptedColumn` functions from `@c ```ts import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; -export const protectedUsers = encryptedTable("users", { +export const encryptedUsers = encryptedTable("users", { email: encryptedColumn("email"), }); ``` @@ -71,7 +71,7 @@ If you are looking to enable searchable encryption in a PostgreSQL database, you ```ts import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; -export const protectedUsers = encryptedTable("users", { +export const encryptedUsers = encryptedTable("users", { email: encryptedColumn("email").freeTextSearch().equality().orderAndRange(), }); ``` @@ -90,7 +90,7 @@ You can define nested objects by using the `encryptedValue` function to define a ```ts import { encryptedTable, encryptedColumn, encryptedValue } from "@cipherstash/stack"; -export const protectedUsers = encryptedTable("users", { +export const encryptedUsers = encryptedTable("users", { email: encryptedColumn("email").freeTextSearch().equality().orderAndRange(), profile: { name: encryptedValue("profile.name"), @@ -138,10 +138,10 @@ Simply import your schemas and pass them to the `Encryption` function. ```ts import { Encryption, type EncryptionClientConfig } from "@cipherstash/stack"; -import { protectedUsers } from "./schemas/users"; +import { encryptedUsers } from "./schemas/users"; const config: EncryptionClientConfig = { - schemas: [protectedUsers], // At least one encryptedTable is required + schemas: [encryptedUsers], // At least one encryptedTable is required } const encryptionClient = await Encryption(config); diff --git a/examples/drizzle/src/controllers/transactions.ts b/examples/drizzle/src/controllers/transactions.ts index d83d179e..4a704bc7 100644 --- a/examples/drizzle/src/controllers/transactions.ts +++ b/examples/drizzle/src/controllers/transactions.ts @@ -4,7 +4,7 @@ import { db } from '../db' import { transactions } from '../db/schema' import { encryptionClient, - protectOps, + encryptionOps, transactionsSchema, } from '../protect/config' @@ -36,7 +36,7 @@ export async function getTransactions(req: Request, res: Response) { // Account number search (encrypted field) if (accountNumber && typeof accountNumber === 'string') { - const accountCondition = await protectOps.like( + const accountCondition = await encryptionOps.like( transactions.accountNumber, accountNumber, ) @@ -48,13 +48,13 @@ export async function getTransactions(req: Request, res: Response) { if (minAmount !== undefined) { const minAmountNum = Number(minAmount) if (!Number.isNaN(minAmountNum)) { - conditions.push(protectOps.gte(transactions.amount, minAmountNum)) + conditions.push(encryptionOps.gte(transactions.amount, minAmountNum)) } } if (maxAmount !== undefined) { const maxAmountNum = Number(maxAmount) if (!Number.isNaN(maxAmountNum)) { - conditions.push(protectOps.lte(transactions.amount, maxAmountNum)) + conditions.push(encryptionOps.lte(transactions.amount, maxAmountNum)) } } } @@ -66,7 +66,7 @@ export async function getTransactions(req: Request, res: Response) { // Apply conditions if (conditions.length > 0) { - const condition = await protectOps.and(...conditions) + const condition = await encryptionOps.and(...conditions) query = query.where(condition) as typeof query } diff --git a/examples/drizzle/src/protect/config.ts b/examples/drizzle/src/protect/config.ts index fa5f512a..b7d5f19c 100644 --- a/examples/drizzle/src/protect/config.ts +++ b/examples/drizzle/src/protect/config.ts @@ -1,6 +1,6 @@ import 'dotenv/config' import { - createProtectOperators, + createEncryptionOperators, extractProtectSchema, } from '@cipherstash/drizzle/pg' import { Encryption } from '@cipherstash/stack' @@ -14,5 +14,5 @@ export const encryptionClient = await Encryption({ schemas: [transactionsSchema], }) -// Create Protect operators for encrypted field queries -export const protectOps = createProtectOperators(encryptionClient) +// Create encryption operators for encrypted field queries +export const encryptionOps = createEncryptionOperators(encryptionClient) diff --git a/examples/dynamo/src/bulk-operations.ts b/examples/dynamo/src/bulk-operations.ts index e685e689..eea6ebcd 100644 --- a/examples/dynamo/src/bulk-operations.ts +++ b/examples/dynamo/src/bulk-operations.ts @@ -1,5 +1,5 @@ import { BatchGetCommand, BatchWriteCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' +import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' import { log } from './common/log' import { encryptionClient, users } from './common/protect' @@ -28,7 +28,7 @@ const main = async () => { ], }) - const protectDynamo = protectDynamoDB({ + const dynamodb = encryptedDynamoDB({ encryptionClient, }) @@ -45,7 +45,7 @@ const main = async () => { }, ] - const encryptResult = await protectDynamo.bulkEncryptModels(items, users) + const encryptResult = await dynamodb.bulkEncryptModels(items, users) if (encryptResult.failure) { throw new Error(`Failed to encrypt items: ${encryptResult.failure.message}`) @@ -79,7 +79,7 @@ const main = async () => { const getResult = await docClient.send(batchGetCommand) - const decryptedItems = await protectDynamo.bulkDecryptModels( + const decryptedItems = await dynamodb.bulkDecryptModels( getResult.Responses?.[tableName], users, ) diff --git a/examples/dynamo/src/encrypted-key-in-gsi.ts b/examples/dynamo/src/encrypted-key-in-gsi.ts index c0ee5529..8cc9be7d 100644 --- a/examples/dynamo/src/encrypted-key-in-gsi.ts +++ b/examples/dynamo/src/encrypted-key-in-gsi.ts @@ -1,5 +1,5 @@ import { PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' +import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' import { log } from './common/log' import { encryptionClient, users } from './common/protect' @@ -44,7 +44,7 @@ const main = async () => { ], }) - const protectDynamo = protectDynamoDB({ + const dynamodb = encryptedDynamoDB({ encryptionClient, }) @@ -55,7 +55,7 @@ const main = async () => { email: 'abc@example.com', } - const encryptResult = await protectDynamo.encryptModel(user, users) + const encryptResult = await dynamodb.encryptModel(user, users) log('encrypted item', encryptResult) @@ -105,7 +105,7 @@ const main = async () => { throw new Error('Item not found') } - const decryptedItem = await protectDynamo.decryptModel( + const decryptedItem = await dynamodb.decryptModel( queryResult.Items[0], users, ) diff --git a/examples/dynamo/src/encrypted-partition-key.ts b/examples/dynamo/src/encrypted-partition-key.ts index 7bee1e91..e95f0703 100644 --- a/examples/dynamo/src/encrypted-partition-key.ts +++ b/examples/dynamo/src/encrypted-partition-key.ts @@ -1,5 +1,5 @@ import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' +import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient } from './common/dynamo' import { log } from './common/log' import { encryptionClient, users } from './common/protect' @@ -27,7 +27,7 @@ const main = async () => { ], }) - const protectDynamo = protectDynamoDB({ + const dynamodb = encryptedDynamoDB({ encryptionClient, }) @@ -38,7 +38,7 @@ const main = async () => { somePlaintextAttr: 'abc', } - const encryptResult = await protectDynamo.encryptModel(user, users) + const encryptResult = await dynamodb.encryptModel(user, users) log('encrypted item', encryptResult) @@ -79,10 +79,7 @@ const main = async () => { const getResult = await docClient.send(getCommand) - const decryptedItem = await protectDynamo.decryptModel( - getResult.Item, - users, - ) + const decryptedItem = await dynamodb.decryptModel(getResult.Item, users) log('decrypted item', decryptedItem) } diff --git a/examples/dynamo/src/encrypted-sort-key.ts b/examples/dynamo/src/encrypted-sort-key.ts index 3e3deb6c..9c8839db 100644 --- a/examples/dynamo/src/encrypted-sort-key.ts +++ b/examples/dynamo/src/encrypted-sort-key.ts @@ -1,5 +1,5 @@ import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' +import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' import { log } from './common/log' import { encryptionClient, users } from './common/protect' @@ -36,7 +36,7 @@ const main = async () => { ], }) - const protectDynamo = protectDynamoDB({ + const dynamodb = encryptedDynamoDB({ encryptionClient, }) @@ -47,7 +47,7 @@ const main = async () => { email: 'abc@example.com', } - const encryptResult = await protectDynamo.encryptModel(user, users) + const encryptResult = await dynamodb.encryptModel(user, users) log('encrypted item', encryptResult) @@ -92,10 +92,7 @@ const main = async () => { throw new Error('Item not found') } - const decryptedItem = await protectDynamo.decryptModel( - getResult.Item, - users, - ) + const decryptedItem = await dynamodb.decryptModel(getResult.Item, users) log('decrypted item', decryptedItem) } diff --git a/examples/dynamo/src/export-to-pg.ts b/examples/dynamo/src/export-to-pg.ts index d3bc191a..4678ab47 100644 --- a/examples/dynamo/src/export-to-pg.ts +++ b/examples/dynamo/src/export-to-pg.ts @@ -1,5 +1,5 @@ import { PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' +import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import pg from 'pg' // Insert data in dynamo, scan it back out, insert/copy into PG, query from PG. import { createTable, docClient, dynamoClient } from './common/dynamo' @@ -31,7 +31,7 @@ const main = async () => { ], }) - const protectDynamo = protectDynamoDB({ + const dynamodb = encryptedDynamoDB({ encryptionClient, }) @@ -42,7 +42,7 @@ const main = async () => { email: 'abc@example.com', } - const encryptResult = await protectDynamo.encryptModel(user, users) + const encryptResult = await dynamodb.encryptModel(user, users) const putCommand = new PutCommand({ TableName: tableName, diff --git a/examples/dynamo/src/simple.ts b/examples/dynamo/src/simple.ts index 9feb0bae..5dd16e54 100644 --- a/examples/dynamo/src/simple.ts +++ b/examples/dynamo/src/simple.ts @@ -1,5 +1,5 @@ import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' +import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' import { log } from './common/log' import { encryptionClient, users } from './common/protect' @@ -28,7 +28,7 @@ const main = async () => { ], }) - const protectDynamo = protectDynamoDB({ + const dynamodb = encryptedDynamoDB({ encryptionClient, }) @@ -39,7 +39,7 @@ const main = async () => { email: 'abc@example.com', } - const encryptResult = await protectDynamo.encryptModel(user, users) + const encryptResult = await dynamodb.encryptModel(user, users) log('encrypted item', encryptResult) @@ -57,10 +57,7 @@ const main = async () => { const getResult = await docClient.send(getCommand) - const decryptedItem = await protectDynamo.decryptModel( - getResult.Item, - users, - ) + const decryptedItem = await dynamodb.decryptModel(getResult.Item, users) log('decrypted item', decryptedItem) } diff --git a/examples/nest/README.md b/examples/nest/README.md index f627b661..d1a63be6 100644 --- a/examples/nest/README.md +++ b/examples/nest/README.md @@ -25,7 +25,7 @@ CS_CLIENT_ACCESS_KEY= ### How encryption works here - `src/protect/schema.ts` defines tables with `.equality()`, `.orderAndRange()`, `.freeTextSearch()` for searchable encryption on Postgres. -- `ProtectModule` initializes an `EncryptionClient` with those schemas and injects a `ProtectService`. +- `EncryptionModule` initializes an `EncryptionClient` with those schemas and injects an `EncryptionService`. - `AppService` uses `encryptModel`/`decryptModel` and bulk variants to demonstrate single and bulk flows. ### Minimal API demo diff --git a/examples/nest/src/app.controller.spec.ts b/examples/nest/src/app.controller.spec.ts index b2deea6d..20ad4218 100644 --- a/examples/nest/src/app.controller.spec.ts +++ b/examples/nest/src/app.controller.spec.ts @@ -2,12 +2,12 @@ import type { Decrypted, EncryptedPayload } from '@cipherstash/stack' import { Test, type TestingModule } from '@nestjs/testing' import { AppController } from './app.controller' import { AppService, type CreateUserDto, type User } from './app.service' -import { ProtectService } from './protect' +import { EncryptionService } from './protect' describe('AppController', () => { let appController: AppController let appService: AppService - let protectService: ProtectService + let encryptionService: EncryptionService const mockEncryptedPayload: EncryptedPayload = { c: 'mock-encrypted-data', @@ -27,7 +27,7 @@ describe('AppController', () => { } beforeEach(async () => { - const mockProtectService = { + const mockEncryptionService = { encryptModel: jest.fn(), decryptModel: jest.fn(), bulkEncryptModels: jest.fn(), @@ -39,15 +39,15 @@ describe('AppController', () => { providers: [ AppService, { - provide: ProtectService, - useValue: mockProtectService, + provide: EncryptionService, + useValue: mockEncryptionService, }, ], }).compile() appController = app.get(AppController) appService = app.get(AppService) - protectService = app.get(ProtectService) + encryptionService = app.get(EncryptionService) }) describe('getHello', () => { diff --git a/examples/nest/src/app.module.ts b/examples/nest/src/app.module.ts index 58b0d1b7..a8809ce8 100644 --- a/examples/nest/src/app.module.ts +++ b/examples/nest/src/app.module.ts @@ -2,14 +2,14 @@ import { Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' import { AppController } from './app.controller' import { AppService } from './app.service' -import { ProtectModule, schemas } from './protect' +import { EncryptionModule, schemas } from './protect' @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, }), - ProtectModule.forRoot({ + EncryptionModule.forRoot({ schemas, }), ], diff --git a/examples/nest/src/app.service.ts b/examples/nest/src/app.service.ts index 7e4ddc4b..12d0f2dd 100644 --- a/examples/nest/src/app.service.ts +++ b/examples/nest/src/app.service.ts @@ -1,6 +1,6 @@ import type { Decrypted, EncryptedPayload } from '@cipherstash/stack' import { Injectable } from '@nestjs/common' -import type { ProtectService } from './protect' +import type { EncryptionService } from './protect' import { users } from './protect' export type User = { @@ -20,7 +20,7 @@ export type CreateUserDto = { @Injectable() export class AppService { - constructor(private readonly protectService: ProtectService) {} + constructor(private readonly encryptionService: EncryptionService) {} async getHello(): Promise<{ encryptedUser: User @@ -38,7 +38,7 @@ export class AppService { name: 'John Doe', } - const encryptedResult = await this.protectService.encryptModel( + const encryptedResult = await this.encryptionService.encryptModel( { id: '1', email_encrypted: userData.email, @@ -53,7 +53,7 @@ export class AppService { throw new Error(`Encryption failed: ${encryptedResult.failure.message}`) } - const decryptedResult = await this.protectService.decryptModel( + const decryptedResult = await this.encryptionService.decryptModel( encryptedResult.data, ) @@ -76,7 +76,7 @@ export class AppService { ] const bulkEncryptedResult = - await this.protectService.bulkEncryptModels( + await this.encryptionService.bulkEncryptModels( bulkUsers.map((user, index) => ({ id: (index + 2).toString(), email_encrypted: user.email, @@ -93,7 +93,7 @@ export class AppService { } const bulkDecryptedResult = - await this.protectService.bulkDecryptModels( + await this.encryptionService.bulkDecryptModels( bulkEncryptedResult.data, ) @@ -114,7 +114,7 @@ export class AppService { } async createUser(userData: CreateUserDto): Promise { - const encryptedResult = await this.protectService.encryptModel( + const encryptedResult = await this.encryptionService.encryptModel( { id: Date.now().toString(), email_encrypted: userData.email, @@ -136,7 +136,7 @@ export class AppService { async getUser(id: string, encryptedUser: User): Promise> { const decryptedResult = - await this.protectService.decryptModel(encryptedUser) + await this.encryptionService.decryptModel(encryptedUser) if (decryptedResult.failure) { throw new Error( diff --git a/examples/nest/src/protect/decorators/decrypt.decorator.ts b/examples/nest/src/protect/decorators/decrypt.decorator.ts index 668731cd..fed81c31 100644 --- a/examples/nest/src/protect/decorators/decrypt.decorator.ts +++ b/examples/nest/src/protect/decorators/decrypt.decorator.ts @@ -1,16 +1,16 @@ import { type ExecutionContext, createParamDecorator } from '@nestjs/common' -import { getProtectService } from '../utils/get-protect-service.util' +import { getEncryptionService } from '../utils/get-protect-service.util' import type { + EncryptedColumn, EncryptedTable, EncryptedTableColumn, EncryptedValue, - ProtectColumn, } from '@cipherstash/stack' export interface DecryptOptions { table: EncryptedTable - column: ProtectColumn | EncryptedValue + column: EncryptedColumn | EncryptedValue lockContext?: unknown // JWT or LockContext } @@ -35,11 +35,11 @@ export interface DecryptOptions { export const Decrypt = createParamDecorator( async (field: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest() - const protectService = getProtectService(ctx) + const encryptionService = getEncryptionService(ctx) - if (!protectService) { + if (!encryptionService) { throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', + 'EncryptionService not found. Make sure EncryptionModule is imported.', ) } @@ -51,7 +51,7 @@ export const Decrypt = createParamDecorator( // Check if value is already an encrypted payload if (typeof value === 'object' && value.c) { - const result = await protectService.decrypt(value) + const result = await encryptionService.decrypt(value) if (result.failure) { throw new Error(`Decryption failed: ${result.failure.message}`) } @@ -69,11 +69,11 @@ export const Decrypt = createParamDecorator( export const DecryptModel = createParamDecorator( async (tableName: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest() - const protectService = getProtectService(ctx) + const encryptionService = getEncryptionService(ctx) - if (!protectService) { + if (!encryptionService) { throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', + 'EncryptionService not found. Make sure EncryptionModule is imported.', ) } diff --git a/examples/nest/src/protect/decorators/encrypt.decorator.ts b/examples/nest/src/protect/decorators/encrypt.decorator.ts index cd66bc15..ea08e0f6 100644 --- a/examples/nest/src/protect/decorators/encrypt.decorator.ts +++ b/examples/nest/src/protect/decorators/encrypt.decorator.ts @@ -1,18 +1,18 @@ import { type ExecutionContext, createParamDecorator } from '@nestjs/common' -import type { ProtectService } from '../protect.service' +import type { EncryptionService } from '../protect.service' import { users } from '../schema' -import { getProtectService } from '../utils/get-protect-service.util' +import { getEncryptionService } from '../utils/get-protect-service.util' import type { + EncryptedColumn, EncryptedTable, EncryptedTableColumn, EncryptedValue, - ProtectColumn, } from '@cipherstash/stack' export interface EncryptOptions { table: EncryptedTable - column: ProtectColumn | EncryptedValue + column: EncryptedColumn | EncryptedValue lockContext?: unknown // JWT or LockContext } @@ -37,11 +37,11 @@ export interface EncryptOptions { export const Encrypt = createParamDecorator( async (field: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest() - const protectService = getProtectService(ctx) + const encryptionService = getEncryptionService(ctx) - if (!protectService) { + if (!encryptionService) { throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', + 'EncryptionService not found. Make sure EncryptionModule is imported.', ) } @@ -52,7 +52,7 @@ export const Encrypt = createParamDecorator( // Note: This is a simplified example. In practice, you'd need to pass actual table/column objects // from your schema definitions rather than creating them inline - const result = await protectService.encrypt(value, { + const result = await encryptionService.encrypt(value, { table: users, column: users.email_encrypted, }) @@ -71,11 +71,11 @@ export const Encrypt = createParamDecorator( export const EncryptModel = createParamDecorator( async (tableName: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest() - const protectService = getProtectService(ctx) + const encryptionService = getEncryptionService(ctx) - if (!protectService) { + if (!encryptionService) { throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', + 'EncryptionService not found. Make sure EncryptionModule is imported.', ) } diff --git a/examples/nest/src/protect/index.ts b/examples/nest/src/protect/index.ts index d9b04bf0..1f9a6603 100644 --- a/examples/nest/src/protect/index.ts +++ b/examples/nest/src/protect/index.ts @@ -1,6 +1,6 @@ // Main module exports -export { ProtectModule } from './protect.module' -export { ProtectService } from './protect.service' +export { EncryptionModule } from './protect.module' +export { EncryptionService } from './protect.service' // Schema exports export * from './schema' @@ -14,5 +14,5 @@ export { EncryptInterceptor } from './interceptors/encrypt.interceptor' export { DecryptInterceptor } from './interceptors/decrypt.interceptor' // Type exports -export type { ProtectConfig } from './interfaces/protect-config.interface' -export { PROTECT_CONFIG, PROTECT_CLIENT } from './protect.constants' +export type { EncryptionConfig } from './interfaces/protect-config.interface' +export { ENCRYPTION_CONFIG, ENCRYPTION_CLIENT } from './protect.constants' diff --git a/examples/nest/src/protect/interceptors/decrypt.interceptor.ts b/examples/nest/src/protect/interceptors/decrypt.interceptor.ts index 039cf10f..a05df697 100644 --- a/examples/nest/src/protect/interceptors/decrypt.interceptor.ts +++ b/examples/nest/src/protect/interceptors/decrypt.interceptor.ts @@ -6,20 +6,20 @@ import { } from '@nestjs/common' import type { Observable } from 'rxjs' import { map } from 'rxjs/operators' -import type { ProtectService } from '../protect.service' -import { getProtectService } from '../utils/get-protect-service.util' +import type { EncryptionService } from '../protect.service' +import { getEncryptionService } from '../utils/get-protect-service.util' import type { + EncryptedColumn, EncryptedTable, EncryptedTableColumn, EncryptedValue, - ProtectColumn, } from '@cipherstash/stack' export interface DecryptInterceptorOptions { fields?: string[] table: EncryptedTable - column: ProtectColumn | EncryptedValue + column: EncryptedColumn | EncryptedValue lockContext?: unknown } @@ -47,11 +47,11 @@ export class DecryptInterceptor implements NestInterceptor { context: ExecutionContext, next: CallHandler, ): Promise> { - const protectService = getProtectService(context) + const encryptionService = getEncryptionService(context) - if (!protectService) { + if (!encryptionService) { throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', + 'EncryptionService not found. Make sure EncryptionModule is imported.', ) } @@ -61,18 +61,18 @@ export class DecryptInterceptor implements NestInterceptor { if (Array.isArray(data)) { return Promise.all( - data.map((item) => this.decryptItem(item, protectService)), + data.map((item) => this.decryptItem(item, encryptionService)), ) } - return this.decryptItem(data, protectService) + return this.decryptItem(data, encryptionService) }), ) } private async decryptItem( item: unknown, - protectService: ProtectService, + encryptionService: EncryptionService, ): Promise { if (!item || typeof item !== 'object') { return item @@ -85,7 +85,7 @@ export class DecryptInterceptor implements NestInterceptor { if (result[field] !== undefined && result[field] !== null) { // Check if the field contains an encrypted payload if (typeof result[field] === 'object' && result[field].c) { - const decryptResult = await protectService.decrypt(result[field]) + const decryptResult = await encryptionService.decrypt(result[field]) if (decryptResult.failure) { throw new Error( diff --git a/examples/nest/src/protect/interceptors/encrypt.interceptor.ts b/examples/nest/src/protect/interceptors/encrypt.interceptor.ts index a04839bc..19c579d0 100644 --- a/examples/nest/src/protect/interceptors/encrypt.interceptor.ts +++ b/examples/nest/src/protect/interceptors/encrypt.interceptor.ts @@ -6,20 +6,20 @@ import { } from '@nestjs/common' import type { Observable } from 'rxjs' import { map } from 'rxjs/operators' -import type { ProtectService } from '../protect.service' -import { getProtectService } from '../utils/get-protect-service.util' +import type { EncryptionService } from '../protect.service' +import { getEncryptionService } from '../utils/get-protect-service.util' import type { + EncryptedColumn, EncryptedTable, EncryptedTableColumn, EncryptedValue, - ProtectColumn, } from '@cipherstash/stack' export interface EncryptInterceptorOptions { fields?: string[] table: EncryptedTable - column: ProtectColumn | EncryptedValue + column: EncryptedColumn | EncryptedValue lockContext?: unknown } @@ -47,11 +47,11 @@ export class EncryptInterceptor implements NestInterceptor { context: ExecutionContext, next: CallHandler, ): Promise> { - const protectService = getProtectService(context) + const encryptionService = getEncryptionService(context) - if (!protectService) { + if (!encryptionService) { throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', + 'EncryptionService not found. Make sure EncryptionModule is imported.', ) } @@ -61,18 +61,18 @@ export class EncryptInterceptor implements NestInterceptor { if (Array.isArray(data)) { return Promise.all( - data.map((item) => this.encryptItem(item, protectService)), + data.map((item) => this.encryptItem(item, encryptionService)), ) } - return this.encryptItem(data, protectService) + return this.encryptItem(data, encryptionService) }), ) } private async encryptItem( item: unknown, - protectService: ProtectService, + encryptionService: EncryptionService, ): Promise { if (!item || typeof item !== 'object') { return item @@ -83,7 +83,7 @@ export class EncryptInterceptor implements NestInterceptor { if (this.options.fields) { for (const field of this.options.fields) { if (result[field] !== undefined && result[field] !== null) { - const encryptResult = await protectService.encrypt(result[field], { + const encryptResult = await encryptionService.encrypt(result[field], { table: this.options.table, column: this.options.column, }) diff --git a/examples/nest/src/protect/interfaces/protect-config.interface.ts b/examples/nest/src/protect/interfaces/protect-config.interface.ts index 0fc2494c..f582724e 100644 --- a/examples/nest/src/protect/interfaces/protect-config.interface.ts +++ b/examples/nest/src/protect/interfaces/protect-config.interface.ts @@ -1,6 +1,6 @@ import type { EncryptedTable, EncryptedTableColumn } from '@cipherstash/stack' -export interface ProtectConfig { +export interface EncryptionConfig { workspaceCrn: string clientId: string clientKey: string diff --git a/examples/nest/src/protect/protect.constants.ts b/examples/nest/src/protect/protect.constants.ts index f1c70b35..afb51c6a 100644 --- a/examples/nest/src/protect/protect.constants.ts +++ b/examples/nest/src/protect/protect.constants.ts @@ -1,2 +1,2 @@ -export const PROTECT_CONFIG = 'PROTECT_CONFIG' -export const PROTECT_CLIENT = 'PROTECT_CLIENT' +export const ENCRYPTION_CONFIG = 'ENCRYPTION_CONFIG' +export const ENCRYPTION_CLIENT = 'ENCRYPTION_CLIENT' diff --git a/examples/nest/src/protect/protect.module.ts b/examples/nest/src/protect/protect.module.ts index e65a5e48..c04971f7 100644 --- a/examples/nest/src/protect/protect.module.ts +++ b/examples/nest/src/protect/protect.module.ts @@ -7,23 +7,23 @@ import { } from '@cipherstash/stack' import { type DynamicModule, Global, Module } from '@nestjs/common' import { ConfigModule, ConfigService } from '@nestjs/config' -import type { ProtectConfig } from './interfaces/protect-config.interface' -import { PROTECT_CLIENT, PROTECT_CONFIG } from './protect.constants' -import { ProtectService } from './protect.service' +import type { EncryptionConfig } from './interfaces/protect-config.interface' +import { ENCRYPTION_CLIENT, ENCRYPTION_CONFIG } from './protect.constants' +import { EncryptionService } from './protect.service' import { users } from './schema' @Global() @Module({}) // biome-ignore lint/complexity/noStaticOnlyClass: NestJS module -export class ProtectModule { - static forRoot(config?: Partial): DynamicModule { +export class EncryptionModule { + static forRoot(config?: Partial): DynamicModule { return { - module: ProtectModule, + module: EncryptionModule, imports: [ConfigModule], providers: [ { - provide: PROTECT_CONFIG, - useFactory: (configService: ConfigService): ProtectConfig => { + provide: ENCRYPTION_CONFIG, + useFactory: (configService: ConfigService): EncryptionConfig => { const workspaceCrn = configService.get('CS_WORKSPACE_CRN') const clientId = configService.get('CS_CLIENT_ID') const clientKey = configService.get('CS_CLIENT_KEY') @@ -31,7 +31,7 @@ export class ProtectModule { 'CS_CLIENT_ACCESS_KEY', ) - const defaultConfig: ProtectConfig = { + const defaultConfig: EncryptionConfig = { workspaceCrn: workspaceCrn ?? '', clientId: clientId ?? '', clientKey: clientKey ?? '', @@ -62,11 +62,11 @@ export class ProtectModule { inject: [ConfigService], }, { - provide: PROTECT_CLIENT, + provide: ENCRYPTION_CLIENT, useFactory: async ( - config: ProtectConfig, + config: EncryptionConfig, ): Promise => { - const protectConfig: EncryptionClientConfig = { + const encryptionConfig: EncryptionClientConfig = { schemas: (config.schemas && config.schemas.length > 0 ? config.schemas : [users]) as [ @@ -75,35 +75,37 @@ export class ProtectModule { ], } - return await Encryption(protectConfig) + return await Encryption(encryptionConfig) }, - inject: [PROTECT_CONFIG], + inject: [ENCRYPTION_CONFIG], }, - ProtectService, + EncryptionService, ], - exports: [ProtectService, PROTECT_CLIENT], + exports: [EncryptionService, ENCRYPTION_CLIENT], } } static forRootAsync(options: { - useFactory: (...args: unknown[]) => Promise | ProtectConfig + useFactory: ( + ...args: unknown[] + ) => Promise | EncryptionConfig inject?: unknown[] }): DynamicModule { return { - module: ProtectModule, + module: EncryptionModule, imports: [ConfigModule], providers: [ { - provide: PROTECT_CONFIG, + provide: ENCRYPTION_CONFIG, useFactory: options.useFactory, inject: options.inject || [], }, { - provide: PROTECT_CLIENT, + provide: ENCRYPTION_CLIENT, useFactory: async ( - config: ProtectConfig, + config: EncryptionConfig, ): Promise => { - const protectConfig: EncryptionClientConfig = { + const encryptionConfig: EncryptionClientConfig = { schemas: (config.schemas && config.schemas.length > 0 ? config.schemas : [users]) as [ @@ -112,13 +114,13 @@ export class ProtectModule { ], } - return await Encryption(protectConfig) + return await Encryption(encryptionConfig) }, - inject: [PROTECT_CONFIG], + inject: [ENCRYPTION_CONFIG], }, - ProtectService, + EncryptionService, ], - exports: [ProtectService, PROTECT_CLIENT], + exports: [EncryptionService, ENCRYPTION_CLIENT], } } } diff --git a/examples/nest/src/protect/protect.service.spec.ts b/examples/nest/src/protect/protect.service.spec.ts index be8bcd84..fe664dcd 100644 --- a/examples/nest/src/protect/protect.service.spec.ts +++ b/examples/nest/src/protect/protect.service.spec.ts @@ -1,11 +1,11 @@ import type { EncryptedPayload, EncryptionClient } from '@cipherstash/stack' import { Test, type TestingModule } from '@nestjs/testing' -import { PROTECT_CLIENT } from './protect.constants' -import { ProtectService } from './protect.service' +import { ENCRYPTION_CLIENT } from './protect.constants' +import { EncryptionService } from './protect.service' import { users } from './schema' -describe('ProtectService', () => { - let service: ProtectService +describe('EncryptionService', () => { + let service: EncryptionService let mockClient: jest.Mocked const mockEncryptedPayload: EncryptedPayload = { @@ -27,15 +27,15 @@ describe('ProtectService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - ProtectService, + EncryptionService, { - provide: PROTECT_CLIENT, + provide: ENCRYPTION_CLIENT, useValue: mockClient, }, ], }).compile() - service = module.get(ProtectService) + service = module.get(EncryptionService) }) describe('encrypt', () => { diff --git a/examples/nest/src/protect/protect.service.ts b/examples/nest/src/protect/protect.service.ts index 54bbe921..904038da 100644 --- a/examples/nest/src/protect/protect.service.ts +++ b/examples/nest/src/protect/protect.service.ts @@ -8,12 +8,12 @@ import type { LockContext, } from '@cipherstash/stack' import { Inject, Injectable } from '@nestjs/common' -import { PROTECT_CLIENT } from './protect.constants' +import { ENCRYPTION_CLIENT } from './protect.constants' @Injectable() -export class ProtectService { +export class EncryptionService { constructor( - @Inject(PROTECT_CLIENT) + @Inject(ENCRYPTION_CLIENT) private readonly client: EncryptionClient, ) {} diff --git a/examples/nest/src/protect/utils/get-protect-service.util.ts b/examples/nest/src/protect/utils/get-protect-service.util.ts index 22bd5a03..317fde17 100644 --- a/examples/nest/src/protect/utils/get-protect-service.util.ts +++ b/examples/nest/src/protect/utils/get-protect-service.util.ts @@ -1,20 +1,20 @@ import type { ExecutionContext } from '@nestjs/common' import type { ModuleRef } from '@nestjs/core' -import { ProtectService } from '../protect.service' +import { EncryptionService } from '../protect.service' -export function getProtectService( +export function getEncryptionService( ctx: ExecutionContext, -): ProtectService | null { +): EncryptionService | null { try { const app = ctx.switchToHttp().getRequest().app if (app?.get) { - return app.get(ProtectService) + return app.get(EncryptionService) } // Fallback: try to get from module ref if available const moduleRef = ctx.switchToHttp().getRequest().moduleRef as ModuleRef if (moduleRef) { - return moduleRef.get(ProtectService, { strict: false }) + return moduleRef.get(EncryptionService, { strict: false }) } return null diff --git a/examples/nest/test/app.e2e-spec.ts b/examples/nest/test/app.e2e-spec.ts index 5a7a65c6..29dc0b1d 100644 --- a/examples/nest/test/app.e2e-spec.ts +++ b/examples/nest/test/app.e2e-spec.ts @@ -4,11 +4,11 @@ import { Test, type TestingModule } from '@nestjs/testing' import request from 'supertest' import { AppController } from '../src/app.controller' import { AppService } from '../src/app.service' -import { ProtectService } from '../src/protect' +import { EncryptionService } from '../src/protect' describe('AppController (e2e)', () => { let app: INestApplication - let protectService: ProtectService + let encryptionService: EncryptionService const mockEncryptedPayload: EncryptedPayload = { c: 'mock-encrypted-data', @@ -21,7 +21,7 @@ describe('AppController (e2e)', () => { providers: [ AppService, { - provide: ProtectService, + provide: EncryptionService, useValue: { encryptModel: jest.fn().mockImplementation((model) => Promise.resolve({ @@ -73,7 +73,7 @@ describe('AppController (e2e)', () => { }).compile() app = moduleFixture.createNestApplication() - protectService = moduleFixture.get(ProtectService) + encryptionService = moduleFixture.get(EncryptionService) await app.init() }) diff --git a/examples/next-drizzle-mysql/src/app/actions.ts b/examples/next-drizzle-mysql/src/app/actions.ts index 1d6decc1..3e231016 100644 --- a/examples/next-drizzle-mysql/src/app/actions.ts +++ b/examples/next-drizzle-mysql/src/app/actions.ts @@ -4,12 +4,12 @@ import type { FormData } from '@/components/form' import { db } from '@/db' import { users } from '@/db/schema' import { encryptionClient } from '@/protect' -import { users as protectedUsers } from '@/protect/schema' +import { users as encryptedUsers } from '@/protect/schema' export async function createUser(data: FormData) { console.log(data) - const result = await encryptionClient.encryptModel(data, protectedUsers) + const result = await encryptionClient.encryptModel(data, encryptedUsers) if (result.failure) { console.error(result.failure.message) diff --git a/examples/next-drizzle-mysql/src/app/page.tsx b/examples/next-drizzle-mysql/src/app/page.tsx index a0d146dd..3bc549e8 100644 --- a/examples/next-drizzle-mysql/src/app/page.tsx +++ b/examples/next-drizzle-mysql/src/app/page.tsx @@ -2,7 +2,7 @@ import { ClientForm } from '@/components/form' import { db } from '@/db' import { users } from '@/db/schema' import { encryptionClient } from '@/protect' -import { users as protectedUsers } from '@/protect/schema' +import { users as encryptedUsers } from '@/protect/schema' type User = { id: number diff --git a/examples/nextjs-clerk/src/lib/actions.ts b/examples/nextjs-clerk/src/lib/actions.ts index c7b9c8ed..e8232d25 100644 --- a/examples/nextjs-clerk/src/lib/actions.ts +++ b/examples/nextjs-clerk/src/lib/actions.ts @@ -2,7 +2,7 @@ import { db } from '@/core/db' import { users } from '@/core/db/schema' -import { encryptionClient, users as protectUsers } from '@/core/protect' +import { users as encryptedUsers, encryptionClient } from '@/core/protect' import { getLockContext } from '@/core/protect' import { getCtsToken } from '@cipherstash/nextjs' import { auth } from '@clerk/nextjs/server' @@ -32,8 +32,8 @@ export async function addUser(formData: FormData) { const lockContext = getLockContext(ctsToken.ctsToken) const encryptedResult = await encryptionClient .encrypt(email, { - column: protectUsers.email, - table: protectUsers, + column: encryptedUsers.email, + table: encryptedUsers, }) .withLockContext(lockContext) diff --git a/examples/typeorm/README.md b/examples/typeorm/README.md index 38c9a065..5b737a09 100644 --- a/examples/typeorm/README.md +++ b/examples/typeorm/README.md @@ -88,10 +88,10 @@ export const encryptionClient = await Encryption({ ### 5. Use Streamlined Helpers ```typescript -// src/helpers/protect-entity.ts -import { ProtectEntityHelper } from './helpers/protect-entity' +// src/helpers/encryption-entity.ts +import { EncryptionEntityHelper } from './helpers/encryption-entity' -const helper = new ProtectEntityHelper(encryptionClient) +const helper = new EncryptionEntityHelper(encryptionClient) // 🚀 Bulk create with encryption (recommended for production) const users = await helper.bulkEncryptAndSave( diff --git a/packages/drizzle/README.md b/packages/drizzle/README.md index ae40776e..d94edc8e 100644 --- a/packages/drizzle/README.md +++ b/packages/drizzle/README.md @@ -117,11 +117,11 @@ export const usersTable = pgTable('users', { ```typescript // protect/config.ts import { Encryption } from '@cipherstash/stack' -import { extractProtectSchema } from '@cipherstash/drizzle/pg' +import { extractEncryptionSchema } from '@cipherstash/drizzle/pg' import { usersTable } from '../db/schema' // Extract Stash Encryption schema from Drizzle table -export const users = extractProtectSchema(usersTable) +export const users = extractEncryptionSchema(usersTable) // Initialize Stash Encryption client export const encryptionClient = await Encryption({ @@ -129,15 +129,15 @@ export const encryptionClient = await Encryption({ }) ``` -### 3. Create Protect operators +### 3. Create encryption operators ```typescript // protect/operators.ts -import { createProtectOperators } from '@cipherstash/drizzle/pg' +import { createEncryptionOperators } from '@cipherstash/drizzle/pg' import { encryptionClient } from './config' // Create operators that automatically handle encryption in queries -export const protectOps = createProtectOperators(encryptionClient) +export const encryptionOps = createEncryptionOperators(encryptionClient) ``` ## Usage Examples @@ -193,22 +193,22 @@ decrypted.data.forEach(user => { const results = await db .select() .from(usersTable) - .where(await protectOps.eq(usersTable.email, 'jane@example.com')) + .where(await encryptionOps.eq(usersTable.email, 'jane@example.com')) // Text search (LIKE/ILIKE) const results = await db .select() .from(usersTable) - .where(await protectOps.ilike(usersTable.email, 'smith')) + .where(await encryptionOps.ilike(usersTable.email, 'smith')) // Range queries const results = await db .select() .from(usersTable) .where( - await protectOps.and( - protectOps.gte(usersTable.age, 25), - protectOps.lte(usersTable.age, 35), + await encryptionOps.and( + encryptionOps.gte(usersTable.age, 25), + encryptionOps.lte(usersTable.age, 35), ), ) @@ -222,7 +222,7 @@ const decrypted = await encryptionClient.bulkDecryptModels(results) const results = await db .select() .from(usersTable) - .orderBy(protectOps.asc(usersTable.age)) + .orderBy(encryptionOps.asc(usersTable.age)) const decrypted = await encryptionClient.bulkDecryptModels(results) ``` @@ -235,15 +235,15 @@ const decrypted = await encryptionClient.bulkDecryptModels(results) ```typescript import { eq } from 'drizzle-orm' -// Mix Protect operators (encrypted) with regular Drizzle operators (non-encrypted) +// Mix encryption operators (encrypted) with regular Drizzle operators (non-encrypted) const results = await db .select() .from(usersTable) .where( - await protectOps.and( - // Protect operators for encrypted columns (batched for efficiency) - protectOps.gte(usersTable.age, 25), - protectOps.ilike(usersTable.email, 'developer'), + await encryptionOps.and( + // Encryption operators for encrypted columns (batched for efficiency) + encryptionOps.gte(usersTable.age, 25), + encryptionOps.ilike(usersTable.email, 'developer'), // Regular Drizzle operators for non-encrypted columns eq(usersTable.id, 1), ), @@ -251,7 +251,7 @@ const results = await db ``` > [!TIP] -> **Performance Tip**: Using `protectOps.and()` batches all encryption operations into a single `createSearchTerms` call, which is more efficient than awaiting each operator individually. +> **Performance Tip**: Using `encryptionOps.and()` batches all encryption operations into a single `createSearchTerms` call, which is more efficient than awaiting each operator individually. ## Available Operators @@ -308,7 +308,7 @@ Creates an encrypted column type for Drizzle schemas. - `equality?: boolean` - Enable equality queries - `orderAndRange?: boolean` - Enable range queries and sorting -### `extractProtectSchema(table)` +### `extractEncryptionSchema(table)` Extracts a Stash Encryption schema from a Drizzle table definition. @@ -317,7 +317,7 @@ Extracts a Stash Encryption schema from a Drizzle table definition. **Returns:** Stash Encryption schema object -### `createProtectOperators(encryptionClient)` +### `createEncryptionOperators(encryptionClient)` Creates Drizzle-compatible operators that automatically handle encryption. diff --git a/packages/drizzle/__tests__/docs.test.ts b/packages/drizzle/__tests__/docs.test.ts index af05a8fc..9c16aa7e 100644 --- a/packages/drizzle/__tests__/docs.test.ts +++ b/packages/drizzle/__tests__/docs.test.ts @@ -8,9 +8,9 @@ import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { - createProtectOperators, + createEncryptionOperators, encryptedType, - extractProtectSchema, + extractEncryptionSchema, } from '../src/pg' import { docSeedData } from './fixtures/doc-seed-data' import { type ExecutionContext, executeCodeBlock } from './utils/code-executor' @@ -62,20 +62,20 @@ const transactions = pgTable('drizzle-docs-test', { }), }) -const protectTransactions = extractProtectSchema(transactions) +const encryptionTransactions = extractEncryptionSchema(transactions) describe('Documentation Drift Tests', () => { let db: ReturnType let client: ReturnType let encryptionClient: EncryptionClient - let encryptionOps: ReturnType + let encryptionOps: ReturnType let seedDataIds: number[] = [] beforeAll(async () => { client = postgres(process.env.DATABASE_URL as string) db = drizzle({ client }) - encryptionClient = await Encryption({ schemas: [protectTransactions] }) - encryptionOps = createProtectOperators(encryptionClient) + encryptionClient = await Encryption({ schemas: [encryptionTransactions] }) + encryptionOps = createEncryptionOperators(encryptionClient) // Create test table with EQL encrypted columns (drop if exists for clean state) await client`DROP TABLE IF EXISTS "drizzle-docs-test"` @@ -92,7 +92,7 @@ describe('Documentation Drift Tests', () => { // Seed test data const encrypted = await encryptionClient.bulkEncryptModels( docSeedData, - protectTransactions, + encryptionTransactions, ) if (encrypted.failure) { throw new Error(`Encryption failed: ${encrypted.failure.message}`) @@ -137,7 +137,7 @@ describe('Documentation Drift Tests', () => { transactions, protect: encryptionOps, encryptionClient, - protectTransactions, + encryptionTransactions, ...drizzleOrm, } @@ -173,7 +173,7 @@ describe('Documentation Drift Tests', () => { db, transactions, encryptionClient, - protectTransactions, + encryptionTransactions, ...drizzleOrm, // Note: 'protect' intentionally omitted } diff --git a/packages/drizzle/__tests__/drizzle.test.ts b/packages/drizzle/__tests__/drizzle.test.ts index c4be5bee..6860d8ec 100644 --- a/packages/drizzle/__tests__/drizzle.test.ts +++ b/packages/drizzle/__tests__/drizzle.test.ts @@ -6,9 +6,9 @@ import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { - createProtectOperators, + createEncryptionOperators, encryptedType, - extractProtectSchema, + extractEncryptionSchema, } from '../src/pg' if (!process.env.DATABASE_URL) { @@ -57,7 +57,7 @@ const drizzleUsersTable = pgTable('protect-ci', { }) // Extract Encryption schema from Drizzle table -const users = extractProtectSchema(drizzleUsersTable) +const users = extractEncryptionSchema(drizzleUsersTable) // Hard code this as the CI database doesn't support order by on encrypted columns const SKIP_ORDER_BY_TEST = true @@ -79,14 +79,14 @@ interface DecryptedUser { } let encryptionClient: EncryptionClient -let encryptionOps: ReturnType +let encryptionOps: ReturnType let db: ReturnType const testData: TestUser[] = [] beforeAll(async () => { // Initialize Encryption client using schema extracted from Drizzle table encryptionClient = await Encryption({ schemas: [users] }) - encryptionOps = createProtectOperators(encryptionClient) + encryptionOps = createEncryptionOperators(encryptionClient) const client = postgres(process.env.DATABASE_URL as string) db = drizzle({ client }) @@ -184,10 +184,10 @@ afterAll(async () => { }, 30000) describe('Drizzle ORM Integration with Encryption', () => { - it('should perform equality search using Protect operators', async () => { + it('should perform equality search using encryption operators', async () => { const searchEmail = 'jane.smith@example.com' - // Query using Protect operators - encryption is handled automatically + // Query using encryption operators - encryption is handled automatically const results = await db .select({ id: drizzleUsersTable.id, @@ -216,10 +216,10 @@ describe('Drizzle ORM Integration with Encryption', () => { expect(decryptedUser.email).toBe(searchEmail) }, 30000) - it('should perform text search using Protect operators', async () => { + it('should perform text search using encryption operators', async () => { const searchText = 'smith' - // Query using Protect operators - encryption is handled automatically + // Query using encryption operators - encryption is handled automatically const results = await db .select({ id: drizzleUsersTable.id, @@ -260,10 +260,10 @@ describe('Drizzle ORM Integration with Encryption', () => { expect(foundMatch).toBe(true) }, 30000) - it('should perform number range queries using Protect operators', async () => { + it('should perform number range queries using encryption operators', async () => { const minAge = 28 - // Query using Protect operators - encryption is handled automatically + // Query using encryption operators - encryption is handled automatically const results = await db .select({ id: drizzleUsersTable.id, @@ -351,7 +351,7 @@ describe('Drizzle ORM Integration with Encryption', () => { const maxAge = 35 const searchText = 'developer' - // Complex query using Protect operators with batched and() - encryption is handled automatically + // Complex query using encryption operators with batched and() - encryption is handled automatically // All operator calls are batched into a single createSearchTerms call const results = await db .select({ @@ -480,7 +480,7 @@ describe('Drizzle ORM Integration with Encryption', () => { it('should handle inArray operator with encrypted columns', async () => { const searchEmails = ['jane.smith@example.com', 'bob.wilson@example.com'] - // Query using Protect operators with inArray + // Query using encryption operators with inArray const results = await db .select({ id: drizzleUsersTable.id, @@ -520,7 +520,7 @@ describe('Drizzle ORM Integration with Encryption', () => { const minAge = 25 const maxAge = 30 - // Query using Protect operators with between + // Query using encryption operators with between const results = await db .select({ id: drizzleUsersTable.id, diff --git a/packages/drizzle/src/pg/index.ts b/packages/drizzle/src/pg/index.ts index f8f8bce2..85213aa6 100644 --- a/packages/drizzle/src/pg/index.ts +++ b/packages/drizzle/src/pg/index.ts @@ -139,7 +139,7 @@ export const encryptedType = ( // Also store on property for immediate access (before pgTable processes it) // We need to use any here because Drizzle columns don't have a type for custom properties // biome-ignore lint/suspicious/noExplicitAny: Drizzle columns don't expose custom property types - ;(column as any)._protectConfig = fullConfig + ;(column as any)._encryptionConfig = fullConfig return column } @@ -170,8 +170,8 @@ export function getEncryptedColumnConfig( if (isEncrypted) { // Try to get config from property (if still there) - if (columnAny._protectConfig) { - return columnAny._protectConfig + if (columnAny._encryptionConfig) { + return columnAny._encryptionConfig } // Look up config by column name (the name passed to encryptedType) @@ -184,7 +184,17 @@ export function getEncryptedColumnConfig( } // Re-export schema extraction utility -export { extractProtectSchema } from './schema-extraction.js' +export { + extractEncryptionSchema, + extractProtectSchema, +} from './schema-extraction.js' // Re-export operators -export { createProtectOperators } from './operators.js' +export { + createEncryptionOperators, + createProtectOperators, + EncryptionOperatorError, + ProtectOperatorError, + EncryptionConfigError, + ProtectConfigError, +} from './operators.js' diff --git a/packages/drizzle/src/pg/operators.ts b/packages/drizzle/src/pg/operators.ts index a9a9d466..1521a96a 100644 --- a/packages/drizzle/src/pg/operators.ts +++ b/packages/drizzle/src/pg/operators.ts @@ -38,7 +38,7 @@ import { bindIfParam, sql } from 'drizzle-orm' import type { PgTable } from 'drizzle-orm/pg-core' import type { EncryptedColumnConfig } from './index.js' import { getEncryptedColumnConfig } from './index.js' -import { extractProtectSchema } from './schema-extraction.js' +import { extractEncryptionSchema } from './schema-extraction.js' // ============================================================================ // Type Definitions and Type Guards @@ -78,7 +78,7 @@ function isPgTable(value: unknown): value is EncryptedDrizzleTable { /** * Custom error types for better debugging */ -export class ProtectOperatorError extends Error { +export class EncryptionOperatorError extends Error { constructor( message: string, public readonly context?: { @@ -88,14 +88,14 @@ export class ProtectOperatorError extends Error { }, ) { super(message) - this.name = 'ProtectOperatorError' + this.name = 'EncryptionOperatorError' } } -export class ProtectConfigError extends ProtectOperatorError { - constructor(message: string, context?: ProtectOperatorError['context']) { +export class EncryptionConfigError extends EncryptionOperatorError { + constructor(message: string, context?: EncryptionOperatorError['context']) { super(message, context) - this.name = 'ProtectConfigError' + this.name = 'EncryptionConfigError' } } @@ -152,7 +152,7 @@ function getEncryptionTableFromColumn( // Extract encrypted schema from drizzle table and cache it try { // biome-ignore lint/suspicious/noExplicitAny: PgTable type doesn't expose all needed properties - encryptionTable = extractProtectSchema(drizzleTable as PgTable) + encryptionTable = extractEncryptionSchema(drizzleTable as PgTable) encryptionTableCache.set(tableName, encryptionTable) return encryptionTable } catch { @@ -386,7 +386,7 @@ async function encryptValues( const encryptedTerms = await encryptionClient.encryptQuery(terms) if (encryptedTerms.failure) { - throw new ProtectOperatorError( + throw new EncryptionOperatorError( `Failed to encrypt query terms for column "${columnName}": ${encryptedTerms.failure.message}`, { columnName }, ) @@ -400,12 +400,12 @@ async function encryptValues( } } } catch (error) { - if (error instanceof ProtectOperatorError) { + if (error instanceof EncryptionOperatorError) { throw error } const errorMessage = error instanceof Error ? error.message : String(error) - throw new ProtectOperatorError( + throw new EncryptionOperatorError( `Unexpected error encrypting values for column "${columnName}": ${errorMessage}`, { columnName }, ) @@ -560,7 +560,7 @@ async function executeLazyOperator( encryptedMin = encryptedValues[0]?.encrypted encryptedMax = encryptedValues[1]?.encrypted } else { - throw new ProtectOperatorError( + throw new EncryptionOperatorError( 'Between operator requires both min and max encrypted values', { columnName: lazyOp.columnInfo.columnName, @@ -571,7 +571,7 @@ async function executeLazyOperator( } if (encryptedMin === undefined || encryptedMax === undefined) { - throw new ProtectOperatorError( + throw new EncryptionOperatorError( 'Between operator requires both min and max values to be encrypted', { columnName: lazyOp.columnInfo.columnName, @@ -590,7 +590,7 @@ async function executeLazyOperator( if (encryptedValues && encryptedValues.length > 0) { encrypted = encryptedValues[0]?.encrypted } else { - throw new ProtectOperatorError( + throw new EncryptionOperatorError( 'Operator requires encrypted value but none provided', { columnName: lazyOp.columnInfo.columnName, @@ -601,7 +601,7 @@ async function executeLazyOperator( } if (encrypted === undefined) { - throw new ProtectOperatorError( + throw new EncryptionOperatorError( 'Encryption failed or value was not encrypted', { columnName: lazyOp.columnInfo.columnName, @@ -694,7 +694,7 @@ function createComparisonOperator( // This will be replaced with encrypted value in executeLazyOperator const executeFn = (encrypted: unknown) => { if (encrypted === undefined) { - throw new ProtectOperatorError( + throw new EncryptionOperatorError( `Encryption failed for ${operator} operator`, { columnName: columnInfo.columnName, @@ -728,7 +728,7 @@ function createComparisonOperator( if (requiresEquality && config?.equality) { const executeFn = (encrypted: unknown) => { if (encrypted === undefined) { - throw new ProtectOperatorError( + throw new EncryptionOperatorError( `Encryption failed for ${operator} operator`, { columnName: columnInfo.columnName, @@ -787,7 +787,7 @@ function createRangeOperator( encryptedMax?: unknown, ) => { if (encryptedMin === undefined || encryptedMax === undefined) { - throw new ProtectOperatorError( + throw new EncryptionOperatorError( `${operator} operator requires both min and max values`, { columnName: columnInfo.columnName, @@ -849,7 +849,7 @@ function createTextSearchOperator( const executeFn = (encrypted: unknown) => { if (encrypted === undefined) { - throw new ProtectOperatorError( + throw new EncryptionOperatorError( `Encryption failed for ${operator} operator`, { columnName: columnInfo.columnName, @@ -880,7 +880,8 @@ function createTextSearchOperator( } // ============================================================================ -// Public API: createProtectOperators +// Public API: createEncryptionOperators +// Deprecated: Use createEncryptionOperators instead of createProtectOperators // ============================================================================ /** @@ -901,22 +902,22 @@ function createTextSearchOperator( * @example * ```ts * // Initialize operators - * const protectOps = createProtectOperators(encryptionClient) + * const encryptionOps = createEncryptionOperators(encryptionClient) * * // Equality search - automatically encrypts and uses PostgreSQL operators * const results = await db * .select() * .from(usersTable) - * .where(await protectOps.eq(usersTable.email, 'user@example.com')) + * .where(await encryptionOps.eq(usersTable.email, 'user@example.com')) * * // Range query - automatically encrypts and uses eql_v2.gte() * const olderUsers = await db * .select() * .from(usersTable) - * .where(await protectOps.gte(usersTable.age, 25)) + * .where(await encryptionOps.gte(usersTable.age, 25)) * ``` */ -export function createProtectOperators(encryptionClient: EncryptionClient): { +export function createEncryptionOperators(encryptionClient: EncryptionClient): { // Comparison operators /** * Equality operator - encrypts value for encrypted columns. @@ -925,7 +926,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { * @example * Select users with a specific email address. * ```ts - * const condition = await protectOps.eq(usersTable.email, 'user@example.com') + * const condition = await encryptionOps.eq(usersTable.email, 'user@example.com') * const results = await db.select().from(usersTable).where(condition) * ``` */ @@ -938,7 +939,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { * @example * Select users whose email address is not a specific value. * ```ts - * const condition = await protectOps.ne(usersTable.email, 'user@example.com') + * const condition = await encryptionOps.ne(usersTable.email, 'user@example.com') * const results = await db.select().from(usersTable).where(condition) * ``` */ @@ -951,7 +952,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { * @example * Select users older than a specific age. * ```ts - * const condition = await protectOps.gt(usersTable.age, 30) + * const condition = await encryptionOps.gt(usersTable.age, 30) * const results = await db.select().from(usersTable).where(condition) * ``` */ @@ -964,7 +965,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { * @example * Select users older than or equal to a specific age. * ```ts - * const condition = await protectOps.gte(usersTable.age, 30) + * const condition = await encryptionOps.gte(usersTable.age, 30) * const results = await db.select().from(usersTable).where(condition) * ``` */ @@ -977,7 +978,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { * @example * Select users younger than a specific age. * ```ts - * const condition = await protectOps.lt(usersTable.age, 30) + * const condition = await encryptionOps.lt(usersTable.age, 30) * const results = await db.select().from(usersTable).where(condition) * ``` */ @@ -990,7 +991,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { * @example * Select users younger than or equal to a specific age. * ```ts - * const condition = await protectOps.lte(usersTable.age, 30) + * const condition = await encryptionOps.lte(usersTable.age, 30) * const results = await db.select().from(usersTable).where(condition) * ``` */ @@ -1003,7 +1004,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { * @example * Select users within a specific age range. * ```ts - * const condition = await protectOps.between(usersTable.age, 20, 30) + * const condition = await encryptionOps.between(usersTable.age, 20, 30) * const results = await db.select().from(usersTable).where(condition) * ``` */ @@ -1016,7 +1017,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { * @example * Select users outside a specific age range. * ```ts - * const condition = await protectOps.notBetween(usersTable.age, 20, 30) + * const condition = await encryptionOps.notBetween(usersTable.age, 20, 30) * const results = await db.select().from(usersTable).where(condition) * ``` */ @@ -1037,7 +1038,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { * @example * Select users with email addresses matching a pattern. * ```ts - * const condition = await protectOps.like(usersTable.email, '%@example.com') + * const condition = await encryptionOps.like(usersTable.email, '%@example.com') * const results = await db.select().from(usersTable).where(condition) * ``` */ @@ -1054,7 +1055,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { * @example * Select users with email addresses matching a pattern (case-insensitive). * ```ts - * const condition = await protectOps.ilike(usersTable.email, '%@example.com') + * const condition = await encryptionOps.ilike(usersTable.email, '%@example.com') * const results = await db.select().from(usersTable).where(condition) * ``` */ @@ -1095,7 +1096,10 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Equality operator - encrypts value and uses regular Drizzle operator */ - const protectEq = (left: SQLWrapper, right: unknown): Promise | SQL => { + const encryptionEq = ( + left: SQLWrapper, + right: unknown, + ): Promise | SQL => { const columnInfo = getColumnInfo( left, defaultEncryptionTable, @@ -1115,7 +1119,10 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Not equal operator - encrypts value and uses regular Drizzle operator */ - const protectNe = (left: SQLWrapper, right: unknown): Promise | SQL => { + const encryptionNe = ( + left: SQLWrapper, + right: unknown, + ): Promise | SQL => { const columnInfo = getColumnInfo( left, defaultEncryptionTable, @@ -1135,7 +1142,10 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Greater than operator - uses eql_v2.gt() for encrypted columns with ORE index */ - const protectGt = (left: SQLWrapper, right: unknown): Promise | SQL => { + const encryptionGt = ( + left: SQLWrapper, + right: unknown, + ): Promise | SQL => { const columnInfo = getColumnInfo( left, defaultEncryptionTable, @@ -1155,7 +1165,10 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Greater than or equal operator - uses eql_v2.gte() for encrypted columns with ORE index */ - const protectGte = (left: SQLWrapper, right: unknown): Promise | SQL => { + const encryptionGte = ( + left: SQLWrapper, + right: unknown, + ): Promise | SQL => { const columnInfo = getColumnInfo( left, defaultEncryptionTable, @@ -1175,7 +1188,10 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Less than operator - uses eql_v2.lt() for encrypted columns with ORE index */ - const protectLt = (left: SQLWrapper, right: unknown): Promise | SQL => { + const encryptionLt = ( + left: SQLWrapper, + right: unknown, + ): Promise | SQL => { const columnInfo = getColumnInfo( left, defaultEncryptionTable, @@ -1195,7 +1211,10 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Less than or equal operator - uses eql_v2.lte() for encrypted columns with ORE index */ - const protectLte = (left: SQLWrapper, right: unknown): Promise | SQL => { + const encryptionLte = ( + left: SQLWrapper, + right: unknown, + ): Promise | SQL => { const columnInfo = getColumnInfo( left, defaultEncryptionTable, @@ -1215,7 +1234,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Between operator - uses eql_v2.gte() and eql_v2.lte() for encrypted columns with ORE index */ - const protectBetween = ( + const encryptionBetween = ( left: SQLWrapper, min: unknown, max: unknown, @@ -1240,7 +1259,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Not between operator - uses eql_v2.gte() and eql_v2.lte() for encrypted columns with ORE index */ - const protectNotBetween = ( + const encryptionNotBetween = ( left: SQLWrapper, min: unknown, max: unknown, @@ -1265,7 +1284,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Like operator - encrypts value and uses eql_v2.like() for encrypted columns with match index */ - const protectLike = ( + const encryptionLike = ( left: SQLWrapper, right: unknown, ): Promise | SQL => { @@ -1288,7 +1307,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Case-insensitive like operator - encrypts value and uses eql_v2.ilike() for encrypted columns with match index */ - const protectIlike = ( + const encryptionIlike = ( left: SQLWrapper, right: unknown, ): Promise | SQL => { @@ -1311,7 +1330,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Not like operator (case insensitive) - encrypts value and uses eql_v2.ilike() for encrypted columns with match index */ - const protectNotIlike = ( + const encryptionNotIlike = ( left: SQLWrapper, right: unknown, ): Promise | SQL => { @@ -1334,7 +1353,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * In array operator - encrypts all values in the array */ - const protectInArray = async ( + const encryptionInArray = async ( left: SQLWrapper, right: unknown[] | SQLWrapper, ): Promise => { @@ -1381,7 +1400,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Not in array operator */ - const protectNotInArray = async ( + const encryptionNotInArray = async ( left: SQLWrapper, right: unknown[] | SQLWrapper, ): Promise => { @@ -1431,7 +1450,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Ascending order helper - uses eql_v2.order_by() for encrypted columns with ORE index */ - const protectAsc = (column: SQLWrapper): SQL => { + const encryptionAsc = (column: SQLWrapper): SQL => { const columnInfo = getColumnInfo( column, defaultEncryptionTable, @@ -1448,7 +1467,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Descending order helper - uses eql_v2.order_by() for encrypted columns with ORE index */ - const protectDesc = (column: SQLWrapper): SQL => { + const encryptionDesc = (column: SQLWrapper): SQL => { const columnInfo = getColumnInfo( column, defaultEncryptionTable, @@ -1465,7 +1484,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Batched AND operator - collects lazy operators, batches encryption, and combines conditions */ - const protectAnd = async ( + const encryptionAnd = async ( ...conditions: (SQL | SQLWrapper | Promise | undefined)[] ): Promise => { // Single pass: separate lazy operators from regular conditions @@ -1629,7 +1648,7 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { /** * Batched OR operator - collects lazy operators, batches encryption, and combines conditions */ - const protectOr = async ( + const encryptionOr = async ( ...conditions: (SQL | SQLWrapper | Promise | undefined)[] ): Promise => { const lazyOperators: LazyOperator[] = [] @@ -1783,35 +1802,35 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { return { // Comparison operators - eq: protectEq, - ne: protectNe, - gt: protectGt, - gte: protectGte, - lt: protectLt, - lte: protectLte, + eq: encryptionEq, + ne: encryptionNe, + gt: encryptionGt, + gte: encryptionGte, + lt: encryptionLt, + lte: encryptionLte, // Range operators - between: protectBetween, - notBetween: protectNotBetween, + between: encryptionBetween, + notBetween: encryptionNotBetween, // Text search operators - like: protectLike, - ilike: protectIlike, - notIlike: protectNotIlike, + like: encryptionLike, + ilike: encryptionIlike, + notIlike: encryptionNotIlike, // Array operators - inArray: protectInArray, - notInArray: protectNotInArray, + inArray: encryptionInArray, + notInArray: encryptionNotInArray, // Sorting operators - asc: protectAsc, - desc: protectDesc, + asc: encryptionAsc, + desc: encryptionDesc, // AND operator - batches encryption operations - and: protectAnd, + and: encryptionAnd, // OR operator - batches encryption operations - or: protectOr, + or: encryptionOr, // Operators that don't need encryption (pass through to Drizzle) exists, @@ -1825,3 +1844,18 @@ export function createProtectOperators(encryptionClient: EncryptionClient): { arrayOverlaps, } } + +/** + * @deprecated Use `EncryptionOperatorError` instead. + */ +export { EncryptionOperatorError as ProtectOperatorError } + +/** + * @deprecated Use `EncryptionConfigError` instead. + */ +export { EncryptionConfigError as ProtectConfigError } + +/** + * @deprecated Use `createEncryptionOperators` instead. + */ +export { createEncryptionOperators as createProtectOperators } diff --git a/packages/drizzle/src/pg/schema-extraction.ts b/packages/drizzle/src/pg/schema-extraction.ts index 5b817532..205a5511 100644 --- a/packages/drizzle/src/pg/schema-extraction.ts +++ b/packages/drizzle/src/pg/schema-extraction.ts @@ -21,13 +21,13 @@ import { getEncryptedColumnConfig } from './index.js' * age: encryptedType('age', { dataType: 'number', orderAndRange: true }), * }) * - * const encryptionSchema = extractProtectSchema(drizzleUsersTable) + * const encryptionSchema = extractEncryptionSchema(drizzleUsersTable) * const encryptionClient = await Encryption({ schemas: [encryptionSchema.build()] }) * ``` */ // We use any for the PgTable generic because we need to access Drizzle's internal properties // biome-ignore lint/suspicious/noExplicitAny: Drizzle table types don't expose Symbol properties -export function extractProtectSchema>( +export function extractEncryptionSchema>( table: T, ): ReturnType>> { // Drizzle tables store the name in a Symbol property @@ -103,3 +103,8 @@ export function extractProtectSchema>( return encryptedTable(tableName, columns) } + +/** + * @deprecated Use `extractEncryptionSchema` instead. + */ +export { extractEncryptionSchema as extractProtectSchema } diff --git a/packages/dynamodb/README.md b/packages/dynamodb/README.md index 9318abc3..0b0a5b6e 100644 --- a/packages/dynamodb/README.md +++ b/packages/dynamodb/README.md @@ -22,7 +22,7 @@ pnpm add @cipherstash/protect-dynamodb ## Quick Start ```typescript -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' +import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { Encryption, encryptedColumn, encryptedTable } from '@cipherstash/stack' import { PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb' @@ -37,7 +37,7 @@ const encryptionClient = await Encryption({ }) // Create the DynamoDB helper instance -const protectDynamo = protectDynamoDB({ +const dynamodb = encryptedDynamoDB({ encryptionClient, }) @@ -46,7 +46,7 @@ const user = { email: 'user@example.com', } -const encryptResult = await protectDynamo.encryptModel(user, users) +const encryptResult = await dynamodb.encryptModel(user, users) if (encryptResult.failure) { throw new Error(`Failed to encrypt user: ${encryptResult.failure.message}`) } @@ -58,7 +58,7 @@ await docClient.send(new PutCommand({ })) // Create search terms for querying -const searchTermsResult = await protectDynamo.createSearchTerms([ +const searchTermsResult = await dynamodb.createSearchTerms([ { value: 'user@example.com', column: users.email, @@ -82,7 +82,7 @@ if (!result.Item) { } // Decrypt the result -const decryptResult = await protectDynamo.decryptModel( +const decryptResult = await dynamodb.decryptModel( result.Item, users, ) @@ -108,7 +108,7 @@ The package provides methods to encrypt and decrypt data for DynamoDB: All methods return a `Result` type that must be checked for failures: ```typescript -const result = await protectDynamo.encryptModel(user, users) +const result = await dynamodb.encryptModel(user, users) if (result.failure) { // Handle error console.error(result.failure.message) @@ -125,7 +125,7 @@ Create search terms for querying encrypted data: - `createSearchTerms`: Creates search terms for one or more columns ```typescript -const searchTermsResult = await protectDynamo.createSearchTerms([ +const searchTermsResult = await dynamodb.createSearchTerms([ { value: 'user@example.com', column: users.email, @@ -158,7 +158,7 @@ const users = encryptedTable('users', { }) // Encrypt and store -const encryptResult = await protectDynamo.encryptModel({ +const encryptResult = await dynamodb.encryptModel({ pk: 'user#1', email: 'user@example.com', }, users) @@ -168,7 +168,7 @@ if (encryptResult.failure) { } // Query using search terms -const searchTermsResult = await protectDynamo.createSearchTerms([ +const searchTermsResult = await dynamodb.createSearchTerms([ { value: 'user@example.com', column: users.email, @@ -202,7 +202,7 @@ const table = { } // Create search terms for querying -const searchTermsResult = await protectDynamo.createSearchTerms([ +const searchTermsResult = await dynamodb.createSearchTerms([ { value: 'user@example.com', column: users.email, @@ -246,7 +246,7 @@ const table = { } // Create search terms for querying -const searchTermsResult = await protectDynamo.createSearchTerms([ +const searchTermsResult = await dynamodb.createSearchTerms([ { value: 'user@example.com', column: users.email, @@ -301,7 +301,7 @@ const table = { } // Create search terms for querying -const searchTermsResult = await protectDynamo.createSearchTerms([ +const searchTermsResult = await dynamodb.createSearchTerms([ { value: 'user@example.com', column: users.email, @@ -321,7 +321,7 @@ const [emailHmac] = searchTermsResult.data All methods return a `Result` type from `@byteslice/result` that must be checked for failures: ```typescript -const result = await protectDynamo.encryptModel(user, users) +const result = await dynamodb.encryptModel(user, users) if (result.failure) { // Handle error @@ -343,14 +343,14 @@ type User = { } // Type-safe encryption -const encryptResult = await protectDynamo.encryptModel(user, users) +const encryptResult = await dynamodb.encryptModel(user, users) if (encryptResult.failure) { throw new Error(`Failed to encrypt user: ${encryptResult.failure.message}`) } const encryptedUser = encryptResult.data // Type-safe decryption -const decryptResult = await protectDynamo.decryptModel(item, users) +const decryptResult = await dynamodb.decryptModel(item, users) if (decryptResult.failure) { throw new Error(`Failed to decrypt user: ${decryptResult.failure.message}`) } diff --git a/packages/dynamodb/__tests__/audit.test.ts b/packages/dynamodb/__tests__/audit.test.ts index 4ef88e8b..1856f824 100644 --- a/packages/dynamodb/__tests__/audit.test.ts +++ b/packages/dynamodb/__tests__/audit.test.ts @@ -24,14 +24,14 @@ const schema = encryptedTable('dynamo_cipherstash_test', { describe('dynamodb helpers', () => { let encryptionClient: EncryptionClient - let protectDynamo: ReturnType + let dynamodb: ReturnType beforeAll(async () => { encryptionClient = await Encryption({ schemas: [schema], }) - protectDynamo = encryptedDynamoDB({ + dynamodb = encryptedDynamoDB({ encryptionClient, }) }) @@ -58,7 +58,7 @@ describe('dynamodb helpers', () => { }, } - const result = await protectDynamo.encryptModel(testData, schema).audit({ + const result = await dynamodb.encryptModel(testData, schema).audit({ metadata: { sub: 'cj@cjb.io', type: 'dynamo_encrypt_model' }, }) if (result.failure) { @@ -108,7 +108,7 @@ describe('dynamodb helpers', () => { }, } - const result = await protectDynamo.encryptModel(testData, schema).audit({ + const result = await dynamodb.encryptModel(testData, schema).audit({ metadata: { sub: 'cj@cjb.io', type: 'dynamo_encrypt_model' }, }) if (result.failure) { @@ -142,7 +142,7 @@ describe('dynamodb helpers', () => { metadata: { role: 'admin!@#$%^&*()' }, } - const result = await protectDynamo.encryptModel(testData, schema).audit({ + const result = await dynamodb.encryptModel(testData, schema).audit({ metadata: { sub: 'cj@cjb.io', type: 'dynamo_encrypt_model' }, }) if (result.failure) { @@ -184,14 +184,12 @@ describe('dynamodb helpers', () => { }, ] - const result = await protectDynamo - .bulkEncryptModels(testData, schema) - .audit({ - metadata: { - sub: 'cj@cjb.io', - type: 'dynamo_bulk_encrypt_models', - }, - }) + const result = await dynamodb.bulkEncryptModels(testData, schema).audit({ + metadata: { + sub: 'cj@cjb.io', + type: 'dynamo_bulk_encrypt_models', + }, + }) if (result.failure) { throw new Error(`Bulk encryption failed: ${result.failure.message}`) } @@ -238,7 +236,7 @@ describe('dynamodb helpers', () => { } // First encrypt - const encryptResult = await protectDynamo + const encryptResult = await dynamodb .encryptModel(originalData, schema) .audit({ metadata: { sub: 'cj@cjb.io', type: 'dynamo_encrypt_model' }, @@ -249,7 +247,7 @@ describe('dynamodb helpers', () => { } // Then decrypt - const decryptResult = await protectDynamo + const decryptResult = await dynamodb .decryptModel(encryptResult.data, schema) .audit({ metadata: { sub: 'cj@cjb.io', type: 'dynamo_decrypt_model' }, @@ -291,7 +289,7 @@ describe('dynamodb helpers', () => { ] // First encrypt - const encryptResult = await protectDynamo + const encryptResult = await dynamodb .bulkEncryptModels(originalData, schema) .audit({ metadata: { sub: 'cj@cjb.io', type: 'dynamo_bulk_encrypt_models' }, @@ -303,7 +301,7 @@ describe('dynamodb helpers', () => { } // Then decrypt - const decryptResult = await protectDynamo + const decryptResult = await dynamodb .bulkDecryptModels(encryptResult.data, schema) .audit({ metadata: { sub: 'cj@cjb.io', type: 'dynamo_bulk_decrypt_models' }, diff --git a/packages/dynamodb/__tests__/dynamodb.test.ts b/packages/dynamodb/__tests__/dynamodb.test.ts index c01ab673..e55e9f00 100644 --- a/packages/dynamodb/__tests__/dynamodb.test.ts +++ b/packages/dynamodb/__tests__/dynamodb.test.ts @@ -30,14 +30,14 @@ const schema = encryptedTable('dynamo_cipherstash_test', { describe('dynamodb helpers', () => { let encryptionClient: EncryptionClient - let protectDynamo: ReturnType + let dynamodb: ReturnType beforeAll(async () => { encryptionClient = await Encryption({ schemas: [schema], }) - protectDynamo = encryptedDynamoDB({ + dynamodb = encryptedDynamoDB({ encryptionClient, }) }) @@ -83,7 +83,7 @@ describe('dynamodb helpers', () => { }, } - const result = await protectDynamo.encryptModel(testData, schema) + const result = await dynamodb.encryptModel(testData, schema) if (result.failure) { throw new Error(`Encryption failed: ${result.failure.message}`) } @@ -112,10 +112,7 @@ describe('dynamodb helpers', () => { expect(encryptedData.example.deep.notProtected).toBe('deep not protected') expect(encryptedData.metadata).toEqual({ role: 'admin' }) - const decryptResult = await protectDynamo.decryptModel( - encryptedData, - schema, - ) + const decryptResult = await dynamodb.decryptModel(encryptedData, schema) if (decryptResult.failure) { throw new Error(`Decryption failed: ${decryptResult.failure.message}`) } @@ -141,7 +138,7 @@ describe('dynamodb helpers', () => { }, } - const result = await protectDynamo.encryptModel(testData, schema) + const result = await dynamodb.encryptModel(testData, schema) if (result.failure) { throw new Error(`Encryption failed: ${result.failure.message}`) } @@ -173,7 +170,7 @@ describe('dynamodb helpers', () => { metadata: { role: 'admin!@#$%^&*()' }, } - const result = await protectDynamo.encryptModel(testData, schema) + const result = await dynamodb.encryptModel(testData, schema) if (result.failure) { throw new Error(`Encryption failed: ${result.failure.message}`) } @@ -213,7 +210,7 @@ describe('dynamodb helpers', () => { }, ] - const result = await protectDynamo.bulkEncryptModels(testData, schema) + const result = await dynamodb.bulkEncryptModels(testData, schema) if (result.failure) { throw new Error(`Bulk encryption failed: ${result.failure.message}`) } @@ -260,14 +257,14 @@ describe('dynamodb helpers', () => { } // First encrypt - const encryptResult = await protectDynamo.encryptModel(originalData, schema) + const encryptResult = await dynamodb.encryptModel(originalData, schema) if (encryptResult.failure) { throw new Error(`Encryption failed: ${encryptResult.failure.message}`) } // Then decrypt - const decryptResult = await protectDynamo.decryptModel( + const decryptResult = await dynamodb.decryptModel( encryptResult.data, schema, ) @@ -308,10 +305,7 @@ describe('dynamodb helpers', () => { ] // First encrypt - const encryptResult = await protectDynamo.bulkEncryptModels( - originalData, - schema, - ) + const encryptResult = await dynamodb.bulkEncryptModels(originalData, schema) if (encryptResult.failure) { throw new Error( `Bulk encryption failed: ${encryptResult.failure.message}`, @@ -319,7 +313,7 @@ describe('dynamodb helpers', () => { } // Then decrypt - const decryptResult = await protectDynamo.bulkDecryptModels( + const decryptResult = await dynamodb.bulkDecryptModels( encryptResult.data, schema, ) diff --git a/packages/dynamodb/__tests__/error-codes.test.ts b/packages/dynamodb/__tests__/error-codes.test.ts index 7c16e435..f474fde7 100644 --- a/packages/dynamodb/__tests__/error-codes.test.ts +++ b/packages/dynamodb/__tests__/error-codes.test.ts @@ -14,7 +14,7 @@ const FFI_TEST_TIMEOUT = 30_000 describe('EncryptedDynamoDB Error Code Preservation', () => { let encryptionClient: EncryptionClient - let protectDynamo: ReturnType + let dynamodb: ReturnType const testSchema = encryptedTable('test_table', { email: encryptedColumn('email').equality(), @@ -26,7 +26,7 @@ describe('EncryptedDynamoDB Error Code Preservation', () => { beforeAll(async () => { encryptionClient = await Encryption({ schemas: [testSchema] }) - protectDynamo = encryptedDynamoDB({ encryptionClient }) + dynamodb = encryptedDynamoDB({ encryptionClient }) }) describe('handleError FFI error code extraction', () => { @@ -46,7 +46,7 @@ describe('EncryptedDynamoDB Error Code Preservation', () => { async () => { const model = { nonexistent: 'test value' } - const result = await protectDynamo.encryptModel(model, badSchema) + const result = await dynamodb.encryptModel(model, badSchema) expect(result.failure).toBeDefined() expect((result.failure as EncryptedDynamoDBError).code).toBe( @@ -66,10 +66,7 @@ describe('EncryptedDynamoDB Error Code Preservation', () => { email__source: 'invalid_ciphertext_data', } - const result = await protectDynamo.decryptModel( - malformedItem, - testSchema, - ) + const result = await dynamodb.decryptModel(malformedItem, testSchema) expect(result.failure).toBeDefined() // FFI returns undefined code for IO/parsing errors, so we fall back to generic code @@ -87,7 +84,7 @@ describe('EncryptedDynamoDB Error Code Preservation', () => { async () => { const models = [{ nonexistent: 'value1' }, { nonexistent: 'value2' }] - const result = await protectDynamo.bulkEncryptModels(models, badSchema) + const result = await dynamodb.bulkEncryptModels(models, badSchema) expect(result.failure).toBeDefined() expect((result.failure as EncryptedDynamoDBError).code).toBe( @@ -108,7 +105,7 @@ describe('EncryptedDynamoDB Error Code Preservation', () => { { email__source: 'invalid2' }, ] - const result = await protectDynamo.bulkDecryptModels( + const result = await dynamodb.bulkDecryptModels( malformedItems, testSchema, ) diff --git a/packages/dynamodb/src/index.ts b/packages/dynamodb/src/index.ts index 209c22ba..4751e7dd 100644 --- a/packages/dynamodb/src/index.ts +++ b/packages/dynamodb/src/index.ts @@ -22,48 +22,48 @@ export function encryptedDynamoDB( return { encryptModel>( item: T, - protectTable: EncryptedTable, + table: EncryptedTable, ) { return new EncryptModelOperation( encryptionClient, item, - protectTable, + table, options, ) }, bulkEncryptModels>( items: T[], - protectTable: EncryptedTable, + table: EncryptedTable, ) { return new BulkEncryptModelsOperation( encryptionClient, items, - protectTable, + table, options, ) }, decryptModel>( item: Record, - protectTable: EncryptedTable, + table: EncryptedTable, ) { return new DecryptModelOperation( encryptionClient, item, - protectTable, + table, options, ) }, bulkDecryptModels>( items: Record[], - protectTable: EncryptedTable, + table: EncryptedTable, ) { return new BulkDecryptModelsOperation( encryptionClient, items, - protectTable, + table, options, ) }, diff --git a/packages/dynamodb/src/operations/base-operation.ts b/packages/dynamodb/src/operations/base-operation.ts index e9ea0bb6..b293d456 100644 --- a/packages/dynamodb/src/operations/base-operation.ts +++ b/packages/dynamodb/src/operations/base-operation.ts @@ -1,5 +1,5 @@ import type { Result } from '@byteslice/result' -import type { ProtectDynamoDBError } from '../types' +import type { EncryptedDynamoDBError } from '../types' export type AuditConfig = { metadata?: Record @@ -13,7 +13,7 @@ export type DynamoDBOperationOptions = { logger?: { error: (message: string, error: Error) => void } - errorHandler?: (error: ProtectDynamoDBError) => void + errorHandler?: (error: EncryptedDynamoDBError) => void } export abstract class DynamoDBOperation { @@ -46,15 +46,15 @@ export abstract class DynamoDBOperation { /** * Execute the operation and return a Result */ - abstract execute(): Promise> + abstract execute(): Promise> /** * Make the operation thenable */ - public then, TResult2 = never>( + public then, TResult2 = never>( onfulfilled?: | (( - value: Result, + value: Result, ) => TResult1 | PromiseLike) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, diff --git a/packages/dynamodb/src/operations/bulk-decrypt-models.ts b/packages/dynamodb/src/operations/bulk-decrypt-models.ts index e416076a..77755aa2 100644 --- a/packages/dynamodb/src/operations/bulk-decrypt-models.ts +++ b/packages/dynamodb/src/operations/bulk-decrypt-models.ts @@ -18,18 +18,18 @@ export class BulkDecryptModelsOperation< > extends DynamoDBOperation[]> { private encryptionClient: EncryptionClient private items: Record[] - private protectTable: EncryptedTable + private table: EncryptedTable constructor( encryptionClient: EncryptionClient, items: Record[], - protectTable: EncryptedTable, + table: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) this.encryptionClient = encryptionClient this.items = items - this.protectTable = protectTable + this.table = table } public async execute(): Promise< @@ -38,7 +38,7 @@ export class BulkDecryptModelsOperation< return await withResult( async () => { const itemsWithEqlPayloads = this.items.map((item) => - toItemWithEqlPayloads(item, this.protectTable), + toItemWithEqlPayloads(item, this.table), ) const decryptResult = await this.encryptionClient diff --git a/packages/dynamodb/src/operations/bulk-encrypt-models.ts b/packages/dynamodb/src/operations/bulk-encrypt-models.ts index f2e77f4a..87d89780 100644 --- a/packages/dynamodb/src/operations/bulk-encrypt-models.ts +++ b/packages/dynamodb/src/operations/bulk-encrypt-models.ts @@ -16,18 +16,18 @@ export class BulkEncryptModelsOperation< > extends DynamoDBOperation { private encryptionClient: EncryptionClient private items: T[] - private protectTable: EncryptedTable + private table: EncryptedTable constructor( encryptionClient: EncryptionClient, items: T[], - protectTable: EncryptedTable, + table: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) this.encryptionClient = encryptionClient this.items = items - this.protectTable = protectTable + this.table = table } public async execute(): Promise> { @@ -36,7 +36,7 @@ export class BulkEncryptModelsOperation< const encryptResult = await this.encryptionClient .bulkEncryptModels( this.items.map((item) => deepClone(item)), - this.protectTable, + this.table, ) .audit(this.getAuditData()) @@ -51,7 +51,7 @@ export class BulkEncryptModelsOperation< } const data = encryptResult.data.map((item) => deepClone(item)) - const encryptedAttrs = Object.keys(this.protectTable.build().columns) + const encryptedAttrs = Object.keys(this.table.build().columns) return data.map( (encrypted) => toEncryptedDynamoItem(encrypted, encryptedAttrs) as T, diff --git a/packages/dynamodb/src/operations/decrypt-model.ts b/packages/dynamodb/src/operations/decrypt-model.ts index 2f0f0065..1d7a6435 100644 --- a/packages/dynamodb/src/operations/decrypt-model.ts +++ b/packages/dynamodb/src/operations/decrypt-model.ts @@ -18,18 +18,18 @@ export class DecryptModelOperation< > extends DynamoDBOperation> { private encryptionClient: EncryptionClient private item: Record - private protectTable: EncryptedTable + private table: EncryptedTable constructor( encryptionClient: EncryptionClient, item: Record, - protectTable: EncryptedTable, + table: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) this.encryptionClient = encryptionClient this.item = item - this.protectTable = protectTable + this.table = table } public async execute(): Promise< @@ -37,10 +37,7 @@ export class DecryptModelOperation< > { return await withResult( async () => { - const withEqlPayloads = toItemWithEqlPayloads( - this.item, - this.protectTable, - ) + const withEqlPayloads = toItemWithEqlPayloads(this.item, this.table) const decryptResult = await this.encryptionClient .decryptModel(withEqlPayloads as T) diff --git a/packages/dynamodb/src/operations/encrypt-model.ts b/packages/dynamodb/src/operations/encrypt-model.ts index f302e535..6e22b817 100644 --- a/packages/dynamodb/src/operations/encrypt-model.ts +++ b/packages/dynamodb/src/operations/encrypt-model.ts @@ -16,25 +16,25 @@ export class EncryptModelOperation< > extends DynamoDBOperation { private encryptionClient: EncryptionClient private item: T - private protectTable: EncryptedTable + private table: EncryptedTable constructor( encryptionClient: EncryptionClient, item: T, - protectTable: EncryptedTable, + table: EncryptedTable, options?: DynamoDBOperationOptions, ) { super(options) this.encryptionClient = encryptionClient this.item = item - this.protectTable = protectTable + this.table = table } public async execute(): Promise> { return await withResult( async () => { const encryptResult = await this.encryptionClient - .encryptModel(deepClone(this.item), this.protectTable) + .encryptModel(deepClone(this.item), this.table) .audit(this.getAuditData()) if (encryptResult.failure) { @@ -48,7 +48,7 @@ export class EncryptModelOperation< } const data = deepClone(encryptResult.data) - const encryptedAttrs = Object.keys(this.protectTable.build().columns) + const encryptedAttrs = Object.keys(this.table.build().columns) return toEncryptedDynamoItem(data, encryptedAttrs) as T }, diff --git a/packages/dynamodb/src/operations/search-terms.ts b/packages/dynamodb/src/operations/search-terms.ts index 4f28bba2..a4b43753 100644 --- a/packages/dynamodb/src/operations/search-terms.ts +++ b/packages/dynamodb/src/operations/search-terms.ts @@ -13,7 +13,7 @@ import { * @example * ```typescript * // Before (deprecated) - * const result = await protectDynamo.createSearchTerms([{ value, column, table }]) + * const result = await dynamodb.createSearchTerms([{ value, column, table }]) * const hmac = result.data[0] * * // After (new API) diff --git a/packages/dynamodb/src/types.ts b/packages/dynamodb/src/types.ts index 5e8a4e97..7e428889 100644 --- a/packages/dynamodb/src/types.ts +++ b/packages/dynamodb/src/types.ts @@ -30,22 +30,22 @@ export interface EncryptedDynamoDBError extends Error { export interface EncryptedDynamoDBInstance { encryptModel>( item: T, - protectTable: EncryptedTable, + table: EncryptedTable, ): EncryptModelOperation bulkEncryptModels>( items: T[], - protectTable: EncryptedTable, + table: EncryptedTable, ): BulkEncryptModelsOperation decryptModel>( item: Record, - protectTable: EncryptedTable, + table: EncryptedTable, ): DecryptModelOperation bulkDecryptModels>( items: Record[], - protectTable: EncryptedTable, + table: EncryptedTable, ): BulkDecryptModelsOperation /** @@ -54,7 +54,7 @@ export interface EncryptedDynamoDBInstance { * @example * ```typescript * // Before (deprecated) - * const result = await protectDynamo.createSearchTerms([{ value, column, table }]) + * const result = await dynamodb.createSearchTerms([{ value, column, table }]) * const hmac = result.data[0] * * // After (new API) diff --git a/packages/stack/README.md b/packages/stack/README.md index a7bf8b62..b999798e 100644 --- a/packages/stack/README.md +++ b/packages/stack/README.md @@ -199,7 +199,7 @@ Save these environment variables to a `.env` file in your project. ### Basic file structure The following is the basic file structure of the project. -In the `src/protect/` directory, we have the table definition in `schema.ts` and the protect client in `index.ts`. +In the `src/protect/` directory, we have the table definition in `schema.ts` and the encryption client in `index.ts`. ``` @@ -482,7 +482,7 @@ import { encryptionClient } from "./protect"; import { jsonb, pgTable, serial, InferSelectModel } from "drizzle-orm/pg-core"; import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; -const protectUsers = encryptedTable("users", { +const encryptedUsers = encryptedTable("users", { email: encryptedColumn("email"), }); @@ -501,7 +501,7 @@ const user = { // Drizzle User type works directly with model operations const encryptedResult = await encryptionClient.encryptModel( user, - protectUsers + encryptedUsers ); ``` From c8d20aa571e2a28db786f9b806b67607e46aa7e0 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Wed, 11 Feb 2026 18:49:33 -0700 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20final=20branding=20pass=20=E2=80=94?= =?UTF-8?q?=20file/dir=20renames,=20FfiEncryptionError,=20nextjs=20middlew?= =?UTF-8?q?are,=20docs=20(round=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File/directory renames: - Stack test files: *-protect.test.ts → *-encryption.test.ts (5 files) - Example directories: protect/ → encryption/ (nest, drizzle, nextjs-clerk, next-drizzle-mysql, dynamo, basic, typeorm) - Example files: protect.ts → encryption.ts, protect-entity.ts → encryption-entity.ts - Nest example: protect.module.ts → encryption.module.ts, etc. (6 files) - Doc files: drizzle-protect.md → drizzle-encryption.md, init-protect.md → init-encryption.md Source code: - FfiProtectError → FfiEncryptionError (with deprecated alias) - protectClerkMiddleware → encryptionClerkMiddleware (with deprecated alias) - protectMiddleware → encryptionMiddleware (with deprecated alias) - PROTECT_LOG_LEVEL → CS_LOG_LEVEL (with backward compat fallback) - extractProtectSchema → extractEncryptionSchema in drizzle example - docs.test.ts context key protect → encryption - protectedUser → encryptedUser in typeorm example Documentation: - All ./protect paths → ./encryption in READMEs, getting-started, schema, model-operations - Cross-references updated for renamed doc files - PROTECT_LOG_LEVEL → CS_LOG_LEVEL in env var docs Co-Authored-By: Claude Opus 4.6 --- docs/README.md | 2 +- docs/getting-started.md | 18 +++--- docs/how-to/lock-contexts-with-clerk.md | 6 +- .../{init-protect.md => init-encryption.md} | 22 ++++---- docs/reference/drizzle/DRIFT-TESTING.md | 4 +- ...izzle-protect.md => drizzle-encryption.md} | 0 docs/reference/drizzle/drizzle.md | 2 +- docs/reference/model-operations.md | 4 +- docs/reference/schema.md | 4 +- examples/basic/{protect.ts => encryption.ts} | 0 examples/basic/index.ts | 2 +- .../drizzle/src/controllers/transactions.ts | 2 +- .../src/{protect => encryption}/config.ts | 4 +- examples/dynamo/src/bulk-operations.ts | 2 +- .../src/common/{protect.ts => encryption.ts} | 0 examples/dynamo/src/encrypted-key-in-gsi.ts | 2 +- .../dynamo/src/encrypted-partition-key.ts | 2 +- examples/dynamo/src/encrypted-sort-key.ts | 2 +- examples/dynamo/src/export-to-pg.ts | 4 +- examples/dynamo/src/simple.ts | 2 +- examples/hono-supabase/src/index.ts | 2 +- examples/nest/src/app.controller.spec.ts | 2 +- examples/nest/src/app.module.ts | 2 +- examples/nest/src/app.service.ts | 4 +- .../decorators/decrypt.decorator.ts | 2 +- .../decorators/encrypt.decorator.ts | 4 +- .../encryption.constants.ts} | 0 .../encryption.module.ts} | 6 +- .../encryption.service.spec.ts} | 4 +- .../encryption.service.ts} | 2 +- .../nest/src/{protect => encryption}/index.ts | 8 +-- .../interceptors/decrypt.interceptor.ts | 4 +- .../interceptors/encrypt.interceptor.ts | 4 +- .../encryption-config.interface.ts} | 0 .../src/{protect => encryption}/schema.ts | 0 .../utils/get-encryption-service.util.ts} | 2 +- examples/nest/test/app.e2e-spec.ts | 2 +- .../next-drizzle-mysql/src/app/actions.ts | 4 +- examples/next-drizzle-mysql/src/app/page.tsx | 4 +- .../src/{protect => encryption}/index.ts | 0 .../src/{protect => encryption}/schema.ts | 0 examples/nextjs-clerk/src/app/page.tsx | 2 +- .../src/core/{protect => encryption}/index.ts | 0 examples/nextjs-clerk/src/lib/actions.ts | 4 +- examples/nextjs-clerk/src/middleware.ts | 4 +- examples/typeorm/README.md | 28 +++++----- .../typeorm/src/{protect.ts => encryption.ts} | 6 +- ...protect-entity.ts => encryption-entity.ts} | 6 +- examples/typeorm/src/index.ts | 52 ++++++++--------- packages/drizzle/README.md | 4 +- packages/drizzle/__tests__/docs.test.ts | 12 ++-- packages/dynamodb/README.md | 2 +- .../dynamodb/__tests__/error-codes.test.ts | 8 +-- packages/dynamodb/src/helpers.ts | 6 +- packages/nextjs/__tests__/nextjs.test.ts | 10 ++-- packages/nextjs/src/clerk/index.ts | 5 +- packages/nextjs/src/cts/index.ts | 2 +- packages/nextjs/src/index.ts | 5 +- packages/stack/README.md | 56 +++++++++---------- ...otect.test.ts => basic-encryption.test.ts} | 0 ...rotect.test.ts => bulk-encryption.test.ts} | 0 ...ect-ops.test.ts => encryption-ops.test.ts} | 0 packages/stack/__tests__/error-codes.test.ts | 6 +- ...rotect.test.ts => json-encryption.test.ts} | 0 ...tect.test.ts => number-encryption.test.ts} | 0 packages/stack/src/ffi/helpers/error-code.ts | 4 +- packages/stack/src/index.ts | 10 ++-- packages/utils/logger/index.ts | 3 +- 68 files changed, 192 insertions(+), 183 deletions(-) rename docs/prompts/{init-protect.md => init-encryption.md} (86%) rename docs/reference/drizzle/{drizzle-protect.md => drizzle-encryption.md} (100%) rename examples/basic/{protect.ts => encryption.ts} (100%) rename examples/drizzle/src/{protect => encryption}/config.ts (83%) rename examples/dynamo/src/common/{protect.ts => encryption.ts} (100%) rename examples/nest/src/{protect => encryption}/decorators/decrypt.decorator.ts (97%) rename examples/nest/src/{protect => encryption}/decorators/encrypt.decorator.ts (94%) rename examples/nest/src/{protect/protect.constants.ts => encryption/encryption.constants.ts} (100%) rename examples/nest/src/{protect/protect.module.ts => encryption/encryption.module.ts} (94%) rename examples/nest/src/{protect/protect.service.spec.ts => encryption/encryption.service.spec.ts} (98%) rename examples/nest/src/{protect/protect.service.ts => encryption/encryption.service.ts} (97%) rename examples/nest/src/{protect => encryption}/index.ts (60%) rename examples/nest/src/{protect => encryption}/interceptors/decrypt.interceptor.ts (94%) rename examples/nest/src/{protect => encryption}/interceptors/encrypt.interceptor.ts (94%) rename examples/nest/src/{protect/interfaces/protect-config.interface.ts => encryption/interfaces/encryption-config.interface.ts} (100%) rename examples/nest/src/{protect => encryption}/schema.ts (100%) rename examples/nest/src/{protect/utils/get-protect-service.util.ts => encryption/utils/get-encryption-service.util.ts} (91%) rename examples/next-drizzle-mysql/src/{protect => encryption}/index.ts (100%) rename examples/next-drizzle-mysql/src/{protect => encryption}/schema.ts (100%) rename examples/nextjs-clerk/src/core/{protect => encryption}/index.ts (100%) rename examples/typeorm/src/{protect.ts => encryption.ts} (83%) rename examples/typeorm/src/helpers/{protect-entity.ts => encryption-entity.ts} (97%) rename packages/stack/__tests__/{basic-protect.test.ts => basic-encryption.test.ts} (100%) rename packages/stack/__tests__/{bulk-protect.test.ts => bulk-encryption.test.ts} (100%) rename packages/stack/__tests__/{protect-ops.test.ts => encryption-ops.test.ts} (100%) rename packages/stack/__tests__/{json-protect.test.ts => json-encryption.test.ts} (100%) rename packages/stack/__tests__/{number-protect.test.ts => number-encryption.test.ts} (100%) diff --git a/docs/README.md b/docs/README.md index 19c82a48..93211337 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,7 +22,7 @@ The documentation for Stash Encryption is organized into the following sections: ### Drizzle ORM Integration - [Encryption Operators Pattern](reference/drizzle/drizzle.md) - Recommended approach with auto-encrypting operators -- [Manual Encryption Pattern](reference/drizzle/drizzle-protect.md) - Explicit control over encryption workflow +- [Manual Encryption Pattern](reference/drizzle/drizzle-encryption.md) - Explicit control over encryption workflow ## How-to guides diff --git a/docs/getting-started.md b/docs/getting-started.md index c0973362..5c9bd19f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -26,12 +26,12 @@ This getting started guide steps you through: ## Step 0: Basic file structure The following is the basic file structure of the standalone project for this getting started guide. -In the `src/protect/` directory, we have the table definition in `schema.ts` and the Stash Encryption client in `index.ts`. +In the `src/encryption/` directory, we have the table definition in `schema.ts` and the Stash Encryption client in `index.ts`. ``` 📦 ├ 📂 src - │ ├ 📂 protect + │ ├ 📂 encryption │ │ ├ 📜 index.ts │ │ └ 📜 schema.ts │ └ 📜 index.ts @@ -47,7 +47,7 @@ If you're following this getting started guide with an existing app, skip to [th If you're following this getting started guide with a clean slate, create a basic structure by running: ```bash -mkdir -p protect-example/src/protect +mkdir -p protect-example/src/encryption cd protect-example git init npm init -y @@ -98,7 +98,7 @@ Save these environment variables to a `.env` file in your project. Stash Encryption uses a schema to define the tables and columns that you want to encrypt and decrypt. -To define your tables and columns, add the following to `src/protect/schema.ts`: +To define your tables and columns, add the following to `src/encryption/schema.ts`: ```ts import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; @@ -114,7 +114,7 @@ export const orders = encryptedTable("orders", { **Searchable encryption:** -If you want to search encrypted data in your PostgreSQL database, you must declare the indexes in schema in `src/protect/schema.ts`: +If you want to search encrypted data in your PostgreSQL database, you must declare the indexes in schema in `src/encryption/schema.ts`: ```ts import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; @@ -132,7 +132,7 @@ Read more about [defining your schema](./docs/reference/schema.md). ## Step 4: Initialize the Encryption client -To import the `Encryption` function and initialize a client with your defined schema, add the following to `src/protect/index.ts`: +To import the `Encryption` function and initialize a client with your defined schema, add the following to `src/encryption/index.ts`: ```ts import { Encryption, type EncryptionClientConfig } from "@cipherstash/stack"; @@ -153,8 +153,8 @@ Stash Encryption provides the `encrypt` function on `encryptionClient` to encryp Start encrypting data by adding this to `src/index.ts`: ```typescript -import { users } from "./protect/schema"; -import { encryptionClient } from "./protect"; +import { users } from "./encryption/schema"; +import { encryptionClient } from "./encryption"; const encryptResult = await encryptionClient.encrypt("secret@squirrel.example", { column: users.email, @@ -209,7 +209,7 @@ Use the `decrypt` function to decrypt data. `decrypt` takes an encrypted data object as a parameter. ```typescript -import { encryptionClient } from "./protect"; +import { encryptionClient } from "./encryption"; const decryptResult = await encryptionClient.decrypt(ciphertext); diff --git a/docs/how-to/lock-contexts-with-clerk.md b/docs/how-to/lock-contexts-with-clerk.md index 4484bf8d..1b293f48 100644 --- a/docs/how-to/lock-contexts-with-clerk.md +++ b/docs/how-to/lock-contexts-with-clerk.md @@ -12,7 +12,7 @@ This how-to guide shows you how to use lock context if you're using [Clerk](http ## Getting started -If you're using [Clerk](https://clerk.com/) as your identity provider, use the `protectClerkMiddleware` function to automatically set the CTS token for every user session. +If you're using [Clerk](https://clerk.com/) as your identity provider, use the `encryptionClerkMiddleware` function to automatically set the CTS token for every user session. Install the `@cipherstash/nextjs` package: @@ -28,10 +28,10 @@ In your `middleware.ts` file, add the following code: ```typescript import { clerkMiddleware } from '@clerk/nextjs/server' -import { protectClerkMiddleware } from '@cipherstash/nextjs/clerk' +import { encryptionClerkMiddleware } from '@cipherstash/nextjs/clerk' export default clerkMiddleware(async (auth, req: NextRequest) => { - return protectClerkMiddleware(auth, req) + return encryptionClerkMiddleware(auth, req) }) ``` diff --git a/docs/prompts/init-protect.md b/docs/prompts/init-encryption.md similarity index 86% rename from docs/prompts/init-protect.md rename to docs/prompts/init-encryption.md index 3590dbb0..8a0117be 100644 --- a/docs/prompts/init-protect.md +++ b/docs/prompts/init-encryption.md @@ -22,35 +22,35 @@ If you detect a mono repo, you need to ask the user which application they want ## Adding scafolding for the Stash Encryption client, schemas, and example code -In the rool of the application (if the application is configred to use something like `src` then this is where these operations will occur), you need to add a `protect` directory with the following files/content. If the application uses TypeScript use the `.ts` extension, else use the `.js` extension. +In the rool of the application (if the application is configred to use something like `src` then this is where these operations will occur), you need to add a `encryption` directory with the following files/content. If the application uses TypeScript use the `.ts` extension, else use the `.js` extension. -`protect/schemas.(ts/js)` +`encryption/schemas.(ts/js)` ```js import { encryptedTable, encryptedColumn } from '@cipherstash/stack' -export const protectedExample = encryptedTable('example_table', { +export const encryptedExample = encryptedTable('example_table', { sensitiveData: encryptedColumn('sensitiveData'), } ``` -`protect/index.(ts/js)` +`encryption/index.(ts/js)` ```js import { Encryption } from '@cipherstash/stack' -import { * as protectSchemas } from './schemas' +import { * as encryptionSchemas } from './schemas' export const encryptionClient = Encryption({ - schemas: [...protectSchemas] + schemas: [...encryptionSchemas] }) ``` -`protect/example.(ts/js)` +`encryption/example.(ts/js)` ```js import { encryptionClient } from './index' -import { * as protectSchemas } from './schemas' +import { * as encryptionSchemas } from './schemas' const sensitiveData = "Let's encrypt some data." -/** +/** * There is no need to wrap any encryptionClient method in a try/catch as it will always return a Result pattern. * --- * The Result will either contain a `failure` OR a `data` key. You should ALWAYS check for the `failure` key first. @@ -59,8 +59,8 @@ const sensitiveData = "Let's encrypt some data." */ // const encryptResult = encryptionClient.encrypt(sensitiveData, { - table: protectSchemas.protectedExample - column: protectSchemas.protectedExample.sensitiveData + table: encryptionSchemas.encryptedExample + column: encryptionSchemas.encryptedExample.sensitiveData }) if (encryptResult.failure) { diff --git a/docs/reference/drizzle/DRIFT-TESTING.md b/docs/reference/drizzle/DRIFT-TESTING.md index 145f939c..c92c9405 100644 --- a/docs/reference/drizzle/DRIFT-TESTING.md +++ b/docs/reference/drizzle/DRIFT-TESTING.md @@ -28,7 +28,7 @@ packages/drizzle/ │ docs/reference/drizzle/ ├── drizzle.md # Encryption operators pattern (recommended) -├── drizzle-protect.md # Manual encryption pattern (verbose) +├── drizzle-encryption.md # Manual encryption pattern (verbose) └── DRIFT-TESTING.md # This document ``` @@ -382,7 +382,7 @@ Verify you're using the correct column names from the `transactions` schema: - `id`, `account`, `amount`, `description`, `createdAt` ### Results are encrypted/unreadable -For manual encryption pattern (`drizzle-protect.md`), ensure you call `bulkDecryptModels()`: +For manual encryption pattern (`drizzle-encryption.md`), ensure you call `bulkDecryptModels()`: ```typescript const results = await db.select().from(transactions) const decrypted = await encryptionClient.bulkDecryptModels(results) diff --git a/docs/reference/drizzle/drizzle-protect.md b/docs/reference/drizzle/drizzle-encryption.md similarity index 100% rename from docs/reference/drizzle/drizzle-protect.md rename to docs/reference/drizzle/drizzle-encryption.md diff --git a/docs/reference/drizzle/drizzle.md b/docs/reference/drizzle/drizzle.md index b9b1a398..eacc84de 100644 --- a/docs/reference/drizzle/drizzle.md +++ b/docs/reference/drizzle/drizzle.md @@ -39,7 +39,7 @@ The TypeScript schema uses camelCase property names that map to snake_case datab This is the **recommended pattern** for most use cases. -**Alternative:** See [manual encryption pattern](/reference/drizzle/drizzle-protect) for explicit control over the encryption workflow. +**Alternative:** See [manual encryption pattern](/reference/drizzle/drizzle-encryption) for explicit control over the encryption workflow. ## Setup diff --git a/docs/reference/model-operations.md b/docs/reference/model-operations.md index c603370f..4ecd21d7 100644 --- a/docs/reference/model-operations.md +++ b/docs/reference/model-operations.md @@ -24,8 +24,8 @@ These operations automatically handle the encryption of fields defined in your s The `encryptModel` method encrypts fields in your model that are defined in your schema while leaving other fields unchanged. ```typescript -import { encryptionClient } from "./protect"; -import { users } from "./protect/schema"; +import { encryptionClient } from "./encryption"; +import { users } from "./encryption/schema"; const user = { id: "1", diff --git a/docs/reference/schema.md b/docs/reference/schema.md index df4184e3..4f4a8b2d 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -21,7 +21,7 @@ Example in a single file: ``` 📦 ├ 📂 src - │ ├ 📂 protect + │ ├ 📂 encryption │ │ └ 📜 schema.ts ``` @@ -30,7 +30,7 @@ or in multiple files: ``` 📦 ├ 📂 src - │ ├ 📂 protect + │ ├ 📂 encryption │ | └ 📂 schemas │ │ └ 📜 users.ts │ │ └ 📜 posts.ts diff --git a/examples/basic/protect.ts b/examples/basic/encryption.ts similarity index 100% rename from examples/basic/protect.ts rename to examples/basic/encryption.ts diff --git a/examples/basic/index.ts b/examples/basic/index.ts index 7a380f9c..70287c75 100644 --- a/examples/basic/index.ts +++ b/examples/basic/index.ts @@ -1,6 +1,6 @@ import 'dotenv/config' import readline from 'node:readline' -import { encryptionClient, users } from './protect' +import { encryptionClient, users } from './encryption' const rl = readline.createInterface({ input: process.stdin, diff --git a/examples/drizzle/src/controllers/transactions.ts b/examples/drizzle/src/controllers/transactions.ts index 4a704bc7..d55aa4e7 100644 --- a/examples/drizzle/src/controllers/transactions.ts +++ b/examples/drizzle/src/controllers/transactions.ts @@ -6,7 +6,7 @@ import { encryptionClient, encryptionOps, transactionsSchema, -} from '../protect/config' +} from '../encryption/config' interface CreateTransactionBody { accountNumber: string diff --git a/examples/drizzle/src/protect/config.ts b/examples/drizzle/src/encryption/config.ts similarity index 83% rename from examples/drizzle/src/protect/config.ts rename to examples/drizzle/src/encryption/config.ts index b7d5f19c..5c9bd81c 100644 --- a/examples/drizzle/src/protect/config.ts +++ b/examples/drizzle/src/encryption/config.ts @@ -1,13 +1,13 @@ import 'dotenv/config' import { createEncryptionOperators, - extractProtectSchema, + extractEncryptionSchema, } from '@cipherstash/drizzle/pg' import { Encryption } from '@cipherstash/stack' import { transactions } from '../db/schema' // Extract Stash Encryption schema from Drizzle table -export const transactionsSchema = extractProtectSchema(transactions) +export const transactionsSchema = extractEncryptionSchema(transactions) // Initialize Stash Encryption client export const encryptionClient = await Encryption({ diff --git a/examples/dynamo/src/bulk-operations.ts b/examples/dynamo/src/bulk-operations.ts index eea6ebcd..d5b0044c 100644 --- a/examples/dynamo/src/bulk-operations.ts +++ b/examples/dynamo/src/bulk-operations.ts @@ -1,8 +1,8 @@ import { BatchGetCommand, BatchWriteCommand } from '@aws-sdk/lib-dynamodb' import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' +import { encryptionClient, users } from './common/encryption' import { log } from './common/log' -import { encryptionClient, users } from './common/protect' const tableName = 'UsersBulkOperations' diff --git a/examples/dynamo/src/common/protect.ts b/examples/dynamo/src/common/encryption.ts similarity index 100% rename from examples/dynamo/src/common/protect.ts rename to examples/dynamo/src/common/encryption.ts diff --git a/examples/dynamo/src/encrypted-key-in-gsi.ts b/examples/dynamo/src/encrypted-key-in-gsi.ts index 8cc9be7d..bf1792f7 100644 --- a/examples/dynamo/src/encrypted-key-in-gsi.ts +++ b/examples/dynamo/src/encrypted-key-in-gsi.ts @@ -1,8 +1,8 @@ import { PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb' import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' +import { encryptionClient, users } from './common/encryption' import { log } from './common/log' -import { encryptionClient, users } from './common/protect' const tableName = 'UsersEncryptedKeyInGSI' const indexName = 'EmailIndex' diff --git a/examples/dynamo/src/encrypted-partition-key.ts b/examples/dynamo/src/encrypted-partition-key.ts index e95f0703..2d0ccdb9 100644 --- a/examples/dynamo/src/encrypted-partition-key.ts +++ b/examples/dynamo/src/encrypted-partition-key.ts @@ -1,8 +1,8 @@ import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient } from './common/dynamo' +import { encryptionClient, users } from './common/encryption' import { log } from './common/log' -import { encryptionClient, users } from './common/protect' const tableName = 'UsersEncryptedPartitionKey' diff --git a/examples/dynamo/src/encrypted-sort-key.ts b/examples/dynamo/src/encrypted-sort-key.ts index 9c8839db..a8533494 100644 --- a/examples/dynamo/src/encrypted-sort-key.ts +++ b/examples/dynamo/src/encrypted-sort-key.ts @@ -1,8 +1,8 @@ import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' +import { encryptionClient, users } from './common/encryption' import { log } from './common/log' -import { encryptionClient, users } from './common/protect' const tableName = 'UsersEncryptedSortKey' diff --git a/examples/dynamo/src/export-to-pg.ts b/examples/dynamo/src/export-to-pg.ts index 4678ab47..d1825f0a 100644 --- a/examples/dynamo/src/export-to-pg.ts +++ b/examples/dynamo/src/export-to-pg.ts @@ -3,8 +3,8 @@ import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import pg from 'pg' // Insert data in dynamo, scan it back out, insert/copy into PG, query from PG. import { createTable, docClient, dynamoClient } from './common/dynamo' +import { encryptionClient, users } from './common/encryption' import { log } from './common/log' -import { encryptionClient, users } from './common/protect' const PgClient = pg.Client const tableName = 'UsersExportToPG' @@ -93,7 +93,7 @@ const main = async () => { throw new Error('No items found in scan result') } - // TODO: this logic belongs in Protect (or in common/protect.ts for the prototype) + // TODO: this logic belongs in Encryption (or in common/encryption.ts for the prototype) const formattedForPgInsert = scanResult.Items.reduce( (recordsToInsert, currentItem) => { const idAsText = currentItem.pk.slice('user#'.length) diff --git a/examples/dynamo/src/simple.ts b/examples/dynamo/src/simple.ts index 5dd16e54..b31f5d6e 100644 --- a/examples/dynamo/src/simple.ts +++ b/examples/dynamo/src/simple.ts @@ -1,8 +1,8 @@ import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { createTable, docClient, dynamoClient } from './common/dynamo' +import { encryptionClient, users } from './common/encryption' import { log } from './common/log' -import { encryptionClient, users } from './common/protect' const tableName = 'UsersSimple' diff --git a/examples/hono-supabase/src/index.ts b/examples/hono-supabase/src/index.ts index cceee1b5..4d4be13c 100644 --- a/examples/hono-supabase/src/index.ts +++ b/examples/hono-supabase/src/index.ts @@ -3,7 +3,7 @@ import { serve } from '@hono/node-server' import { createClient } from '@supabase/supabase-js' import { Hono } from 'hono' -// Consolidated protect and it's schemas into a single file +// Consolidated encryption and its schemas into a single file import { Encryption, type EncryptionClientConfig, diff --git a/examples/nest/src/app.controller.spec.ts b/examples/nest/src/app.controller.spec.ts index 20ad4218..f89fbd7d 100644 --- a/examples/nest/src/app.controller.spec.ts +++ b/examples/nest/src/app.controller.spec.ts @@ -2,7 +2,7 @@ import type { Decrypted, EncryptedPayload } from '@cipherstash/stack' import { Test, type TestingModule } from '@nestjs/testing' import { AppController } from './app.controller' import { AppService, type CreateUserDto, type User } from './app.service' -import { EncryptionService } from './protect' +import { EncryptionService } from './encryption' describe('AppController', () => { let appController: AppController diff --git a/examples/nest/src/app.module.ts b/examples/nest/src/app.module.ts index a8809ce8..c02c64e5 100644 --- a/examples/nest/src/app.module.ts +++ b/examples/nest/src/app.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' import { AppController } from './app.controller' import { AppService } from './app.service' -import { EncryptionModule, schemas } from './protect' +import { EncryptionModule, schemas } from './encryption' @Module({ imports: [ diff --git a/examples/nest/src/app.service.ts b/examples/nest/src/app.service.ts index 12d0f2dd..6314e292 100644 --- a/examples/nest/src/app.service.ts +++ b/examples/nest/src/app.service.ts @@ -1,7 +1,7 @@ import type { Decrypted, EncryptedPayload } from '@cipherstash/stack' import { Injectable } from '@nestjs/common' -import type { EncryptionService } from './protect' -import { users } from './protect' +import type { EncryptionService } from './encryption' +import { users } from './encryption' export type User = { id: string diff --git a/examples/nest/src/protect/decorators/decrypt.decorator.ts b/examples/nest/src/encryption/decorators/decrypt.decorator.ts similarity index 97% rename from examples/nest/src/protect/decorators/decrypt.decorator.ts rename to examples/nest/src/encryption/decorators/decrypt.decorator.ts index fed81c31..10e6af69 100644 --- a/examples/nest/src/protect/decorators/decrypt.decorator.ts +++ b/examples/nest/src/encryption/decorators/decrypt.decorator.ts @@ -1,5 +1,5 @@ import { type ExecutionContext, createParamDecorator } from '@nestjs/common' -import { getEncryptionService } from '../utils/get-protect-service.util' +import { getEncryptionService } from '../utils/get-encryption-service.util' import type { EncryptedColumn, diff --git a/examples/nest/src/protect/decorators/encrypt.decorator.ts b/examples/nest/src/encryption/decorators/encrypt.decorator.ts similarity index 94% rename from examples/nest/src/protect/decorators/encrypt.decorator.ts rename to examples/nest/src/encryption/decorators/encrypt.decorator.ts index ea08e0f6..d25d1ecf 100644 --- a/examples/nest/src/protect/decorators/encrypt.decorator.ts +++ b/examples/nest/src/encryption/decorators/encrypt.decorator.ts @@ -1,7 +1,7 @@ import { type ExecutionContext, createParamDecorator } from '@nestjs/common' -import type { EncryptionService } from '../protect.service' +import type { EncryptionService } from '../encryption.service' import { users } from '../schema' -import { getEncryptionService } from '../utils/get-protect-service.util' +import { getEncryptionService } from '../utils/get-encryption-service.util' import type { EncryptedColumn, diff --git a/examples/nest/src/protect/protect.constants.ts b/examples/nest/src/encryption/encryption.constants.ts similarity index 100% rename from examples/nest/src/protect/protect.constants.ts rename to examples/nest/src/encryption/encryption.constants.ts diff --git a/examples/nest/src/protect/protect.module.ts b/examples/nest/src/encryption/encryption.module.ts similarity index 94% rename from examples/nest/src/protect/protect.module.ts rename to examples/nest/src/encryption/encryption.module.ts index c04971f7..cb5abd0d 100644 --- a/examples/nest/src/protect/protect.module.ts +++ b/examples/nest/src/encryption/encryption.module.ts @@ -7,9 +7,9 @@ import { } from '@cipherstash/stack' import { type DynamicModule, Global, Module } from '@nestjs/common' import { ConfigModule, ConfigService } from '@nestjs/config' -import type { EncryptionConfig } from './interfaces/protect-config.interface' -import { ENCRYPTION_CLIENT, ENCRYPTION_CONFIG } from './protect.constants' -import { EncryptionService } from './protect.service' +import { ENCRYPTION_CLIENT, ENCRYPTION_CONFIG } from './encryption.constants' +import { EncryptionService } from './encryption.service' +import type { EncryptionConfig } from './interfaces/encryption-config.interface' import { users } from './schema' @Global() diff --git a/examples/nest/src/protect/protect.service.spec.ts b/examples/nest/src/encryption/encryption.service.spec.ts similarity index 98% rename from examples/nest/src/protect/protect.service.spec.ts rename to examples/nest/src/encryption/encryption.service.spec.ts index fe664dcd..2f718e40 100644 --- a/examples/nest/src/protect/protect.service.spec.ts +++ b/examples/nest/src/encryption/encryption.service.spec.ts @@ -1,7 +1,7 @@ import type { EncryptedPayload, EncryptionClient } from '@cipherstash/stack' import { Test, type TestingModule } from '@nestjs/testing' -import { ENCRYPTION_CLIENT } from './protect.constants' -import { EncryptionService } from './protect.service' +import { ENCRYPTION_CLIENT } from './encryption.constants' +import { EncryptionService } from './encryption.service' import { users } from './schema' describe('EncryptionService', () => { diff --git a/examples/nest/src/protect/protect.service.ts b/examples/nest/src/encryption/encryption.service.ts similarity index 97% rename from examples/nest/src/protect/protect.service.ts rename to examples/nest/src/encryption/encryption.service.ts index 904038da..b99bc61c 100644 --- a/examples/nest/src/protect/protect.service.ts +++ b/examples/nest/src/encryption/encryption.service.ts @@ -8,7 +8,7 @@ import type { LockContext, } from '@cipherstash/stack' import { Inject, Injectable } from '@nestjs/common' -import { ENCRYPTION_CLIENT } from './protect.constants' +import { ENCRYPTION_CLIENT } from './encryption.constants' @Injectable() export class EncryptionService { diff --git a/examples/nest/src/protect/index.ts b/examples/nest/src/encryption/index.ts similarity index 60% rename from examples/nest/src/protect/index.ts rename to examples/nest/src/encryption/index.ts index 1f9a6603..15fe5733 100644 --- a/examples/nest/src/protect/index.ts +++ b/examples/nest/src/encryption/index.ts @@ -1,6 +1,6 @@ // Main module exports -export { EncryptionModule } from './protect.module' -export { EncryptionService } from './protect.service' +export { EncryptionModule } from './encryption.module' +export { EncryptionService } from './encryption.service' // Schema exports export * from './schema' @@ -14,5 +14,5 @@ export { EncryptInterceptor } from './interceptors/encrypt.interceptor' export { DecryptInterceptor } from './interceptors/decrypt.interceptor' // Type exports -export type { EncryptionConfig } from './interfaces/protect-config.interface' -export { ENCRYPTION_CONFIG, ENCRYPTION_CLIENT } from './protect.constants' +export type { EncryptionConfig } from './interfaces/encryption-config.interface' +export { ENCRYPTION_CONFIG, ENCRYPTION_CLIENT } from './encryption.constants' diff --git a/examples/nest/src/protect/interceptors/decrypt.interceptor.ts b/examples/nest/src/encryption/interceptors/decrypt.interceptor.ts similarity index 94% rename from examples/nest/src/protect/interceptors/decrypt.interceptor.ts rename to examples/nest/src/encryption/interceptors/decrypt.interceptor.ts index a05df697..1a8841fb 100644 --- a/examples/nest/src/protect/interceptors/decrypt.interceptor.ts +++ b/examples/nest/src/encryption/interceptors/decrypt.interceptor.ts @@ -6,8 +6,8 @@ import { } from '@nestjs/common' import type { Observable } from 'rxjs' import { map } from 'rxjs/operators' -import type { EncryptionService } from '../protect.service' -import { getEncryptionService } from '../utils/get-protect-service.util' +import type { EncryptionService } from '../encryption.service' +import { getEncryptionService } from '../utils/get-encryption-service.util' import type { EncryptedColumn, diff --git a/examples/nest/src/protect/interceptors/encrypt.interceptor.ts b/examples/nest/src/encryption/interceptors/encrypt.interceptor.ts similarity index 94% rename from examples/nest/src/protect/interceptors/encrypt.interceptor.ts rename to examples/nest/src/encryption/interceptors/encrypt.interceptor.ts index 19c579d0..c8811564 100644 --- a/examples/nest/src/protect/interceptors/encrypt.interceptor.ts +++ b/examples/nest/src/encryption/interceptors/encrypt.interceptor.ts @@ -6,8 +6,8 @@ import { } from '@nestjs/common' import type { Observable } from 'rxjs' import { map } from 'rxjs/operators' -import type { EncryptionService } from '../protect.service' -import { getEncryptionService } from '../utils/get-protect-service.util' +import type { EncryptionService } from '../encryption.service' +import { getEncryptionService } from '../utils/get-encryption-service.util' import type { EncryptedColumn, diff --git a/examples/nest/src/protect/interfaces/protect-config.interface.ts b/examples/nest/src/encryption/interfaces/encryption-config.interface.ts similarity index 100% rename from examples/nest/src/protect/interfaces/protect-config.interface.ts rename to examples/nest/src/encryption/interfaces/encryption-config.interface.ts diff --git a/examples/nest/src/protect/schema.ts b/examples/nest/src/encryption/schema.ts similarity index 100% rename from examples/nest/src/protect/schema.ts rename to examples/nest/src/encryption/schema.ts diff --git a/examples/nest/src/protect/utils/get-protect-service.util.ts b/examples/nest/src/encryption/utils/get-encryption-service.util.ts similarity index 91% rename from examples/nest/src/protect/utils/get-protect-service.util.ts rename to examples/nest/src/encryption/utils/get-encryption-service.util.ts index 317fde17..079fcc36 100644 --- a/examples/nest/src/protect/utils/get-protect-service.util.ts +++ b/examples/nest/src/encryption/utils/get-encryption-service.util.ts @@ -1,6 +1,6 @@ import type { ExecutionContext } from '@nestjs/common' import type { ModuleRef } from '@nestjs/core' -import { EncryptionService } from '../protect.service' +import { EncryptionService } from '../encryption.service' export function getEncryptionService( ctx: ExecutionContext, diff --git a/examples/nest/test/app.e2e-spec.ts b/examples/nest/test/app.e2e-spec.ts index 29dc0b1d..26d502b6 100644 --- a/examples/nest/test/app.e2e-spec.ts +++ b/examples/nest/test/app.e2e-spec.ts @@ -4,7 +4,7 @@ import { Test, type TestingModule } from '@nestjs/testing' import request from 'supertest' import { AppController } from '../src/app.controller' import { AppService } from '../src/app.service' -import { EncryptionService } from '../src/protect' +import { EncryptionService } from '../src/encryption' describe('AppController (e2e)', () => { let app: INestApplication diff --git a/examples/next-drizzle-mysql/src/app/actions.ts b/examples/next-drizzle-mysql/src/app/actions.ts index 3e231016..0cfaace9 100644 --- a/examples/next-drizzle-mysql/src/app/actions.ts +++ b/examples/next-drizzle-mysql/src/app/actions.ts @@ -3,8 +3,8 @@ import type { FormData } from '@/components/form' import { db } from '@/db' import { users } from '@/db/schema' -import { encryptionClient } from '@/protect' -import { users as encryptedUsers } from '@/protect/schema' +import { encryptionClient } from '@/encryption' +import { users as encryptedUsers } from '@/encryption/schema' export async function createUser(data: FormData) { console.log(data) diff --git a/examples/next-drizzle-mysql/src/app/page.tsx b/examples/next-drizzle-mysql/src/app/page.tsx index 3bc549e8..6feeab6b 100644 --- a/examples/next-drizzle-mysql/src/app/page.tsx +++ b/examples/next-drizzle-mysql/src/app/page.tsx @@ -1,8 +1,8 @@ import { ClientForm } from '@/components/form' import { db } from '@/db' import { users } from '@/db/schema' -import { encryptionClient } from '@/protect' -import { users as encryptedUsers } from '@/protect/schema' +import { encryptionClient } from '@/encryption' +import { users as encryptedUsers } from '@/encryption/schema' type User = { id: number diff --git a/examples/next-drizzle-mysql/src/protect/index.ts b/examples/next-drizzle-mysql/src/encryption/index.ts similarity index 100% rename from examples/next-drizzle-mysql/src/protect/index.ts rename to examples/next-drizzle-mysql/src/encryption/index.ts diff --git a/examples/next-drizzle-mysql/src/protect/schema.ts b/examples/next-drizzle-mysql/src/encryption/schema.ts similarity index 100% rename from examples/next-drizzle-mysql/src/protect/schema.ts rename to examples/next-drizzle-mysql/src/encryption/schema.ts diff --git a/examples/nextjs-clerk/src/app/page.tsx b/examples/nextjs-clerk/src/app/page.tsx index 4a0fadd1..410e0345 100644 --- a/examples/nextjs-clerk/src/app/page.tsx +++ b/examples/nextjs-clerk/src/app/page.tsx @@ -1,6 +1,6 @@ import { db } from '@/core/db' import { users } from '@/core/db/schema' -import { encryptionClient, getLockContext } from '@/core/protect' +import { encryptionClient, getLockContext } from '@/core/encryption' import { getCtsToken } from '@cipherstash/nextjs' import type { EncryptedData } from '@cipherstash/stack' import { auth, currentUser } from '@clerk/nextjs/server' diff --git a/examples/nextjs-clerk/src/core/protect/index.ts b/examples/nextjs-clerk/src/core/encryption/index.ts similarity index 100% rename from examples/nextjs-clerk/src/core/protect/index.ts rename to examples/nextjs-clerk/src/core/encryption/index.ts diff --git a/examples/nextjs-clerk/src/lib/actions.ts b/examples/nextjs-clerk/src/lib/actions.ts index e8232d25..a4f4ecdb 100644 --- a/examples/nextjs-clerk/src/lib/actions.ts +++ b/examples/nextjs-clerk/src/lib/actions.ts @@ -2,8 +2,8 @@ import { db } from '@/core/db' import { users } from '@/core/db/schema' -import { users as encryptedUsers, encryptionClient } from '@/core/protect' -import { getLockContext } from '@/core/protect' +import { users as encryptedUsers, encryptionClient } from '@/core/encryption' +import { getLockContext } from '@/core/encryption' import { getCtsToken } from '@cipherstash/nextjs' import { auth } from '@clerk/nextjs/server' import { revalidatePath } from 'next/cache' diff --git a/examples/nextjs-clerk/src/middleware.ts b/examples/nextjs-clerk/src/middleware.ts index 71ad1059..b8757051 100644 --- a/examples/nextjs-clerk/src/middleware.ts +++ b/examples/nextjs-clerk/src/middleware.ts @@ -1,8 +1,8 @@ -import { protectClerkMiddleware } from '@cipherstash/nextjs/clerk' +import { encryptionClerkMiddleware } from '@cipherstash/nextjs/clerk' import { clerkMiddleware } from '@clerk/nextjs/server' export default clerkMiddleware(async (auth, req) => { - return protectClerkMiddleware(auth, req) + return encryptionClerkMiddleware(auth, req) }) export const config = { diff --git a/examples/typeorm/README.md b/examples/typeorm/README.md index 5b737a09..885d9e5d 100644 --- a/examples/typeorm/README.md +++ b/examples/typeorm/README.md @@ -71,17 +71,17 @@ export class User { ### 4. Configure Stash Encryption ```typescript -// src/protect.ts +// src/encryption.ts import { Encryption, encryptedTable, encryptedColumn } from '@cipherstash/stack' -export const protectedUser = encryptedTable('user', { +export const encryptedUser = encryptedTable('user', { email: encryptedColumn('email').equality().orderAndRange(), ssn: encryptedColumn('ssn').equality(), phone: encryptedColumn('phone').equality(), }) export const encryptionClient = await Encryption({ - schemas: [protectedUser], + schemas: [encryptedUser], }) ``` @@ -101,15 +101,15 @@ const users = await helper.bulkEncryptAndSave( { firstName: 'Jane', email: 'jane@example.com', ssn: '987-65-4321' } ], { - email: { table: protectedUser, column: protectedUser.email }, - ssn: { table: protectedUser, column: protectedUser.ssn } + email: { table: encryptedUser, column: encryptedUser.email }, + ssn: { table: encryptedUser, column: encryptedUser.ssn } } ) // 🔓 Bulk decrypt for display const decryptedUsers = await helper.bulkDecrypt(allUsers, { - email: { table: protectedUser, column: protectedUser.email }, - ssn: { table: protectedUser, column: protectedUser.ssn } + email: { table: encryptedUser, column: encryptedUser.email }, + ssn: { table: encryptedUser, column: encryptedUser.ssn } }) // 🔍 Search encrypted data @@ -117,7 +117,7 @@ const foundUser = await helper.searchEncryptedField( User, 'email', 'john@example.com', - { table: protectedUser, column: protectedUser.email } + { table: encryptedUser, column: encryptedUser.email } ) ``` @@ -156,8 +156,8 @@ email: EncryptedData | null ```typescript // Encrypt individual fields const emailResult = await encryptionClient.encrypt('user@example.com', { - table: protectedUser, - column: protectedUser.email, + table: encryptedUser, + column: encryptedUser.email, }) if (emailResult.failure) { @@ -186,8 +186,8 @@ const savedUsers = await helper.bulkEncryptAndSave( User, usersToCreate, { - email: { table: protectedUser, column: protectedUser.email }, - ssn: { table: protectedUser, column: protectedUser.ssn } + email: { table: encryptedUser, column: encryptedUser.email }, + ssn: { table: encryptedUser, column: encryptedUser.ssn } } ) ``` @@ -203,7 +203,7 @@ const newUser = { ssn: '111-22-3333' } -const encryptedModelResult = await encryptionClient.encryptModel(newUser, protectedUser) +const encryptedModelResult = await encryptionClient.encryptModel(newUser, encryptedUser) if (encryptedModelResult.failure) { throw new Error(`Model encryption failed: ${encryptedModelResult.failure.message}`) @@ -299,7 +299,7 @@ if (result.failure) { ```typescript // Use environment variables for all sensitive data export const encryptionClient = await Encryption({ - schemas: [protectedUser], + schemas: [encryptedUser], }) ``` diff --git a/examples/typeorm/src/protect.ts b/examples/typeorm/src/encryption.ts similarity index 83% rename from examples/typeorm/src/protect.ts rename to examples/typeorm/src/encryption.ts index d82c87ca..301934ec 100644 --- a/examples/typeorm/src/protect.ts +++ b/examples/typeorm/src/encryption.ts @@ -1,10 +1,10 @@ import { Encryption, encryptedColumn, encryptedTable } from '@cipherstash/stack' /** - * Define the protected schema for the User entity + * Define the encrypted schema for the User entity * This maps to the encrypted fields in your TypeORM entity */ -export const protectedUser = encryptedTable('user', { +export const encryptedUser = encryptedTable('user', { email: encryptedColumn('email').equality().orderAndRange(), ssn: encryptedColumn('ssn').equality(), phone: encryptedColumn('phone').equality(), @@ -19,7 +19,7 @@ let encryptionClient: Awaited> export async function initializeEncryptionClient() { if (!encryptionClient) { encryptionClient = await Encryption({ - schemas: [protectedUser], + schemas: [encryptedUser], }) } return encryptionClient diff --git a/examples/typeorm/src/helpers/protect-entity.ts b/examples/typeorm/src/helpers/encryption-entity.ts similarity index 97% rename from examples/typeorm/src/helpers/protect-entity.ts rename to examples/typeorm/src/helpers/encryption-entity.ts index 161d8418..cd68f3f9 100644 --- a/examples/typeorm/src/helpers/protect-entity.ts +++ b/examples/typeorm/src/helpers/encryption-entity.ts @@ -24,7 +24,7 @@ export class EncryptionEntityHelper { * const savedUsers = await helper.bulkEncryptAndSave( * User, * users, - * { email: { table: protectedUser, column: protectedUser.email } } + * { email: { table: encryptedUser, column: encryptedUser.email } } * ) * ``` */ @@ -104,7 +104,7 @@ export class EncryptionEntityHelper { * const users = await repository.find() * const decryptedUsers = await helper.bulkDecrypt( * users, - * { email_encrypted: { table: protectedUser, column: protectedUser.email } } + * { email_encrypted: { table: encryptedUser, column: encryptedUser.email } } * ) * ``` */ @@ -194,7 +194,7 @@ export class EncryptionEntityHelper { * User, * 'email', * 'john@example.com', - * { table: protectedUser, column: protectedUser.email } + * { table: encryptedUser, column: encryptedUser.email } * ) * ``` */ diff --git a/examples/typeorm/src/index.ts b/examples/typeorm/src/index.ts index bb5162ef..dca21fa2 100644 --- a/examples/typeorm/src/index.ts +++ b/examples/typeorm/src/index.ts @@ -1,9 +1,9 @@ import 'reflect-metadata' import 'dotenv/config' import { AppDataSource } from './data-source' +import { encryptedUser, initializeEncryptionClient } from './encryption' import { User } from './entity/User' -import { EncryptionEntityHelper } from './helpers/protect-entity' -import { initializeEncryptionClient, protectedUser } from './protect' +import { EncryptionEntityHelper } from './helpers/encryption-entity' async function main() { try { @@ -32,16 +32,16 @@ async function main() { // Encrypt individual fields const [emailResult, ssnResult, phoneResult] = await Promise.all([ encryptionClient.encrypt(emailToInsert, { - table: protectedUser, - column: protectedUser.email, + table: encryptedUser, + column: encryptedUser.email, }), encryptionClient.encrypt(ssnToInsert, { - table: protectedUser, - column: protectedUser.ssn, + table: encryptedUser, + column: encryptedUser.ssn, }), encryptionClient.encrypt(phoneToInsert, { - table: protectedUser, - column: protectedUser.phone, + table: encryptedUser, + column: encryptedUser.phone, }), ]) @@ -113,9 +113,9 @@ async function main() { User, usersToCreate, { - email: { table: protectedUser, column: protectedUser.email }, - ssn: { table: protectedUser, column: protectedUser.ssn }, - phone: { table: protectedUser, column: protectedUser.phone }, + email: { table: encryptedUser, column: encryptedUser.email }, + ssn: { table: encryptedUser, column: encryptedUser.ssn }, + phone: { table: encryptedUser, column: encryptedUser.phone }, }, ) @@ -132,9 +132,9 @@ async function main() { console.log(`📊 Found ${allUsers.length} users in database`) const decryptedUsers = await helper.bulkDecrypt(allUsers, { - email: { table: protectedUser, column: protectedUser.email }, - ssn: { table: protectedUser, column: protectedUser.ssn }, - phone: { table: protectedUser, column: protectedUser.phone }, + email: { table: encryptedUser, column: encryptedUser.email }, + ssn: { table: encryptedUser, column: encryptedUser.ssn }, + phone: { table: encryptedUser, column: encryptedUser.phone }, }) console.log('✅ All users decrypted using bulk operations') @@ -161,15 +161,15 @@ async function main() { User, 'email', searchEmail, - { table: protectedUser, column: protectedUser.email }, + { table: encryptedUser, column: encryptedUser.email }, ) if (foundUser) { // Decrypt the found user's data const decryptedFoundUser = await helper.bulkDecrypt([foundUser], { - email: { table: protectedUser, column: protectedUser.email }, - ssn: { table: protectedUser, column: protectedUser.ssn }, - phone: { table: protectedUser, column: protectedUser.phone }, + email: { table: encryptedUser, column: encryptedUser.email }, + ssn: { table: encryptedUser, column: encryptedUser.ssn }, + phone: { table: encryptedUser, column: encryptedUser.phone }, }) const userData = decryptedFoundUser[0] @@ -197,7 +197,7 @@ async function main() { // Encrypt the entire model const encryptedModelResult = await encryptionClient.encryptModel( newUser, - protectedUser, + encryptedUser, ) if (encryptedModelResult.failure) { @@ -206,17 +206,17 @@ async function main() { ) } - const encryptedUser = encryptedModelResult.data + const encryptedUserData = encryptedModelResult.data const finalUser = new User() - finalUser.firstName = encryptedUser.firstName as string - finalUser.lastName = encryptedUser.lastName as string - finalUser.age = encryptedUser.age as number + finalUser.firstName = encryptedUserData.firstName as string + finalUser.lastName = encryptedUserData.lastName as string + finalUser.age = encryptedUserData.age as number // biome-ignore lint/suspicious/noExplicitAny: Required for model encryption type compatibility - finalUser.email = encryptedUser.email as any + finalUser.email = encryptedUserData.email as any // biome-ignore lint/suspicious/noExplicitAny: Required for model encryption type compatibility - finalUser.ssn = encryptedUser.ssn as any + finalUser.ssn = encryptedUserData.ssn as any // biome-ignore lint/suspicious/noExplicitAny: Required for model encryption type compatibility - finalUser.phone = encryptedUser.phone as any + finalUser.phone = encryptedUserData.phone as any const savedModelUser = await AppDataSource.manager.save(finalUser) console.log( diff --git a/packages/drizzle/README.md b/packages/drizzle/README.md index d94edc8e..6c9696c1 100644 --- a/packages/drizzle/README.md +++ b/packages/drizzle/README.md @@ -115,7 +115,7 @@ export const usersTable = pgTable('users', { ### 2. Initialize Stash Encryption ```typescript -// protect/config.ts +// encryption/config.ts import { Encryption } from '@cipherstash/stack' import { extractEncryptionSchema } from '@cipherstash/drizzle/pg' import { usersTable } from '../db/schema' @@ -132,7 +132,7 @@ export const encryptionClient = await Encryption({ ### 3. Create encryption operators ```typescript -// protect/operators.ts +// encryption/operators.ts import { createEncryptionOperators } from '@cipherstash/drizzle/pg' import { encryptionClient } from './config' diff --git a/packages/drizzle/__tests__/docs.test.ts b/packages/drizzle/__tests__/docs.test.ts index 9c16aa7e..ed3771bd 100644 --- a/packages/drizzle/__tests__/docs.test.ts +++ b/packages/drizzle/__tests__/docs.test.ts @@ -120,7 +120,7 @@ describe('Documentation Drift Tests', () => { } }, 30000) - describe('drizzle.md - Protect Operators Pattern', () => { + describe('drizzle.md - Encryption Operators Pattern', () => { // Path to documentation relative to repo root const docsPath = join( __dirname, @@ -135,7 +135,7 @@ describe('Documentation Drift Tests', () => { const context: ExecutionContext = { db, transactions, - protect: encryptionOps, + encryption: encryptionOps, encryptionClient, encryptionTransactions, ...drizzleOrm, @@ -158,13 +158,13 @@ describe('Documentation Drift Tests', () => { ) }) - describe('drizzle-protect.md - Manual Encryption Pattern', () => { + describe('drizzle-encryption.md - Manual Encryption Pattern', () => { const docsPath = join( __dirname, - '../../../docs/reference/drizzle/drizzle-protect.md', + '../../../docs/reference/drizzle/drizzle-encryption.md', ) - const blocks = loadDocumentation(docsPath, 'drizzle-protect.md') + const blocks = loadDocumentation(docsPath, 'drizzle-encryption.md') it.each(blocks.map((b) => [b.section, b]))( '%s', @@ -175,7 +175,7 @@ describe('Documentation Drift Tests', () => { encryptionClient, encryptionTransactions, ...drizzleOrm, - // Note: 'protect' intentionally omitted + // Note: 'encryption' intentionally omitted } const result = await executeCodeBlock(block.code, context) diff --git a/packages/dynamodb/README.md b/packages/dynamodb/README.md index 0b0a5b6e..0e083fa4 100644 --- a/packages/dynamodb/README.md +++ b/packages/dynamodb/README.md @@ -4,7 +4,7 @@ Helpers for using CipherStash [Stash Encryption](https://github.com/cipherstash/ [![Built by CipherStash](https://raw.githubusercontent.com/cipherstash/meta/refs/heads/main/csbadge.svg)](https://cipherstash.com) [![NPM version](https://img.shields.io/npm/v/@cipherstash/protect-dynamodb.svg?style=for-the-badge&labelColor=000000)](https://www.npmjs.com/package/@cipherstash/protect-dynamodb) -[![License](https://img.shields.io/npm/l/@cipherstash/protect.svg?style=for-the-badge&labelColor=000000)](https://github.com/cipherstash/protectjs/blob/main/LICENSE.md) +[![License](https://img.shields.io/npm/l/@cipherstash/protect-dynamodb.svg?style=for-the-badge&labelColor=000000)](https://github.com/cipherstash/protectjs/blob/main/LICENSE.md) ## Installation diff --git a/packages/dynamodb/__tests__/error-codes.test.ts b/packages/dynamodb/__tests__/error-codes.test.ts index f474fde7..7574e18b 100644 --- a/packages/dynamodb/__tests__/error-codes.test.ts +++ b/packages/dynamodb/__tests__/error-codes.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config' import { Encryption, - FfiProtectError, + FfiEncryptionError, encryptedColumn, encryptedTable, } from '@cipherstash/stack' @@ -30,13 +30,13 @@ describe('EncryptedDynamoDB Error Code Preservation', () => { }) describe('handleError FFI error code extraction', () => { - it('FfiProtectError has code property accessible', () => { - const ffiError = new FfiProtectError({ + it('FfiEncryptionError has code property accessible', () => { + const ffiError = new FfiEncryptionError({ code: 'UNKNOWN_COLUMN', message: 'Test error', }) expect(ffiError.code).toBe('UNKNOWN_COLUMN') - expect(ffiError instanceof FfiProtectError).toBe(true) + expect(ffiError instanceof FfiEncryptionError).toBe(true) }) }) diff --git a/packages/dynamodb/src/helpers.ts b/packages/dynamodb/src/helpers.ts index 122798d6..20209dbe 100644 --- a/packages/dynamodb/src/helpers.ts +++ b/packages/dynamodb/src/helpers.ts @@ -4,7 +4,7 @@ import type { EncryptedTableColumn, ProtectErrorCode, } from '@cipherstash/stack' -import { FfiProtectError } from '@cipherstash/stack' +import { FfiEncryptionError } from '@cipherstash/stack' import type { EncryptedDynamoDBError } from './types' export const ciphertextAttrSuffix = '__source' export const searchTermAttrSuffix = '__hmac' @@ -34,10 +34,10 @@ export function handleError( }, ): EncryptedDynamoDBError { // Preserve FFI error code if available, otherwise use generic DynamoDB error code - // Check for FfiProtectError instance or plain error objects with code property + // Check for FfiEncryptionError instance or plain error objects with code property const errorObj = error as Record const errorCode = - error instanceof FfiProtectError + error instanceof FfiEncryptionError ? error.code : errorObj && typeof errorObj === 'object' && diff --git a/packages/nextjs/__tests__/nextjs.test.ts b/packages/nextjs/__tests__/nextjs.test.ts index c15f05b5..c63a588c 100644 --- a/packages/nextjs/__tests__/nextjs.test.ts +++ b/packages/nextjs/__tests__/nextjs.test.ts @@ -50,8 +50,8 @@ import { logger } from '../../utils/logger' import { CS_COOKIE_NAME, type CtsToken, + encryptionMiddleware, getCtsToken, - protectMiddleware, resetCtsToken, } from '../src/' @@ -115,7 +115,7 @@ describe('resetCtsToken', () => { }) }) -describe('protectMiddleware', () => { +describe('encryptionMiddleware', () => { afterEach(() => { vi.clearAllMocks() }) @@ -130,7 +130,7 @@ describe('protectMiddleware', () => { const mockOidcToken = 'valid_token' const mockReq = createMockRequest(false) - await protectMiddleware(mockOidcToken, mockReq) + await encryptionMiddleware(mockOidcToken, mockReq) expect(mockSetCtsToken).toHaveBeenCalledWith(mockOidcToken) }) @@ -138,7 +138,7 @@ describe('protectMiddleware', () => { it('should reset the cts token if oidcToken is not provided but cookie is present', async () => { const mockReq = createMockRequest(true) - await protectMiddleware('', mockReq) + await encryptionMiddleware('', mockReq) expect(logger.debug).toHaveBeenCalledWith( 'The JWT token was undefined, so the CipherStash session was reset.', @@ -149,7 +149,7 @@ describe('protectMiddleware', () => { it('should return NextResponse.next() if none of the conditions are met', async () => { const mockReq = createMockRequest(false) - const response = await protectMiddleware('', mockReq) + const response = await encryptionMiddleware('', mockReq) expect(response).toBeInstanceOf(NextResponse) expect(logger.debug).toHaveBeenCalledWith( diff --git a/packages/nextjs/src/clerk/index.ts b/packages/nextjs/src/clerk/index.ts index a48dfb53..f1da8445 100644 --- a/packages/nextjs/src/clerk/index.ts +++ b/packages/nextjs/src/clerk/index.ts @@ -5,7 +5,7 @@ import { logger } from '../../../utils/logger' import { setCtsToken } from '../cts' import { CS_COOKIE_NAME, resetCtsToken } from '../index' -export const protectClerkMiddleware = async ( +export const encryptionClerkMiddleware = async ( auth: ClerkMiddlewareAuth, req: NextRequest, ) => { @@ -40,3 +40,6 @@ export const protectClerkMiddleware = async ( return NextResponse.next() } + +/** @deprecated Use encryptionClerkMiddleware */ +export const protectClerkMiddleware = encryptionClerkMiddleware diff --git a/packages/nextjs/src/cts/index.ts b/packages/nextjs/src/cts/index.ts index d79a9b05..16c872de 100644 --- a/packages/nextjs/src/cts/index.ts +++ b/packages/nextjs/src/cts/index.ts @@ -41,7 +41,7 @@ export const fetchCtsToken = async (oidcToken: string): GetCtsTokenResponse => { if (!workspaceId) { logger.error( - 'The "CS_WORKSPACE_ID" environment variable is not set, and is required by protectClerkMiddleware. No CipherStash session will be set.', + 'The "CS_WORKSPACE_ID" environment variable is not set, and is required by encryptionClerkMiddleware. No CipherStash session will be set.', ) return { diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 9f552360..28417b52 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -74,7 +74,7 @@ export const resetCtsToken = (res?: NextResponse) => { return response } -export const protectMiddleware = async ( +export const encryptionMiddleware = async ( oidcToken: string, req: NextRequest, res?: NextResponse, @@ -123,3 +123,6 @@ export const protectMiddleware = async ( return NextResponse.next() } + +/** @deprecated Use encryptionMiddleware */ +export const protectMiddleware = encryptionMiddleware diff --git a/packages/stack/README.md b/packages/stack/README.md index b999798e..b49652b3 100644 --- a/packages/stack/README.md +++ b/packages/stack/README.md @@ -199,12 +199,12 @@ Save these environment variables to a `.env` file in your project. ### Basic file structure The following is the basic file structure of the project. -In the `src/protect/` directory, we have the table definition in `schema.ts` and the encryption client in `index.ts`. +In the `src/encryption/` directory, we have the table definition in `schema.ts` and the encryption client in `index.ts`. ``` src - protect + encryption index.ts schema.ts index.ts @@ -219,7 +219,7 @@ In the `src/protect/` directory, we have the table definition in `schema.ts` and Stash Encryption uses a schema to define the tables and columns that you want to encrypt and decrypt. -Define your tables and columns by adding this to `src/protect/schema.ts`: +Define your tables and columns by adding this to `src/encryption/schema.ts`: ```ts import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; @@ -235,7 +235,7 @@ export const orders = encryptedTable("orders", { **Searchable encryption:** -If you want to search encrypted data in your PostgreSQL database, you must declare the indexes in schema in `src/protect/schema.ts`: +If you want to search encrypted data in your PostgreSQL database, you must declare the indexes in schema in `src/encryption/schema.ts`: ```ts import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; @@ -253,7 +253,7 @@ Read more about [defining your schema](./docs/reference/schema.md). ### Initialize the encryption client -To import the `Encryption` function and initialize a client with your defined schema, add the following to `src/protect/index.ts`: +To import the `Encryption` function and initialize a client with your defined schema, add the following to `src/encryption/index.ts`: ```ts import { Encryption, type EncryptionClientConfig } from "@cipherstash/stack"; @@ -277,8 +277,8 @@ Stash Encryption provides the `encrypt` function on `encryptionClient` to encryp To start encrypting data, add the following to `src/index.ts`: ```typescript -import { users } from "./protect/schema"; -import { encryptionClient } from "./protect"; +import { users } from "./encryption/schema"; +import { encryptionClient } from "./encryption"; const encryptResult = await encryptionClient.encrypt("secret@squirrel.example", { column: users.email, @@ -323,7 +323,7 @@ Stash Encryption provides the `decrypt` function on `encryptionClient` to decryp To start decrypting data, add the following to `src/index.ts`: ```typescript -import { encryptionClient } from "./protect"; +import { encryptionClient } from "./encryption"; // encryptResult is the EQL payload from the previous step const decryptResult = await encryptionClient.decrypt(encryptResult.data); @@ -376,8 +376,8 @@ If you are working with a large data set, the model operations are significantly Use the `encryptModel` method to encrypt a model's fields that are defined in your schema: ```typescript -import { encryptionClient } from "./protect"; -import { users } from "./protect/schema"; +import { encryptionClient } from "./encryption"; +import { users } from "./encryption/schema"; // Your model with plaintext values const user = { @@ -411,8 +411,8 @@ Stash Encryption provides strong TypeScript support for model operations. You can specify your model's type to ensure end-to-end type safety: ```typescript -import { encryptionClient } from "./protect"; -import { users } from "./protect/schema"; +import { encryptionClient } from "./encryption"; +import { users } from "./encryption/schema"; // Define your model type type User = { @@ -478,7 +478,7 @@ This type safety helps catch potential issues at compile time and provides bette Example with Drizzle infered types: ```typescript -import { encryptionClient } from "./protect"; +import { encryptionClient } from "./encryption"; import { jsonb, pgTable, serial, InferSelectModel } from "drizzle-orm/pg-core"; import { encryptedTable, encryptedColumn } from "@cipherstash/stack"; @@ -510,7 +510,7 @@ const encryptedResult = await encryptionClient.encryptModel( Use the `decryptModel` method to decrypt a model's encrypted fields: ```typescript -import { encryptionClient } from "./protect"; +import { encryptionClient } from "./encryption"; const decryptedResult = await encryptionClient.decryptModel(encryptedUser); @@ -532,8 +532,8 @@ console.log("decrypted user:", decryptedUser); For better performance when working with multiple models, use the `bulkEncryptModels` and `bulkDecryptModels` methods: ```typescript -import { encryptionClient } from "./protect"; -import { users } from "./protect/schema"; +import { encryptionClient } from "./encryption"; +import { users } from "./encryption/schema"; // Array of models with plaintext values const userModels = [ @@ -586,8 +586,8 @@ Stash Encryption provides direct access to ZeroKMS bulk operations through the ` Use the `bulkEncrypt` method to encrypt multiple plaintext values at once: ```typescript -import { encryptionClient } from "./protect"; -import { users } from "./protect/schema"; +import { encryptionClient } from "./encryption"; +import { users } from "./encryption/schema"; // Array of plaintext values with optional IDs for correlation const plaintexts = [ @@ -644,7 +644,7 @@ const encryptedResult = await encryptionClient.bulkEncrypt(plaintexts, { Use the `bulkDecrypt` method to decrypt multiple encrypted values at once: ```typescript -import { encryptionClient } from "./protect"; +import { encryptionClient } from "./encryption"; // encryptedData is the result from bulkEncrypt const decryptedResult = await encryptionClient.bulkDecrypt(encryptedData); @@ -907,8 +907,8 @@ const lockContext = identifyResult.data; To encrypt data with a lock context, call the optional `withLockContext` method on the `encrypt` function and pass the lock context object as a parameter: ```typescript -import { encryptionClient } from "./protect"; -import { users } from "./protect/schema"; +import { encryptionClient } from "./encryption"; +import { users } from "./encryption/schema"; const encryptResult = await encryptionClient .encrypt("plaintext", { @@ -929,7 +929,7 @@ console.log("EQL Payload containing ciphertexts:", encryptResult.data); To decrypt data with a lock context, call the optional `withLockContext` method on the `decrypt` function and pass the lock context object as a parameter: ```typescript -import { encryptionClient } from "./protect"; +import { encryptionClient } from "./encryption"; const decryptResult = await encryptionClient .decrypt(encryptResult.data) @@ -947,8 +947,8 @@ const plaintext = decryptResult.data; All model operations support lock contexts for identity-aware encryption: ```typescript -import { encryptionClient } from "./protect"; -import { users } from "./protect/schema"; +import { encryptionClient } from "./encryption"; +import { users } from "./encryption/schema"; const myUsers = [ { @@ -1009,7 +1009,7 @@ In the [CipherStash Dashboard](https://dashboard.cipherstash.com/workspaces/_/en ```typescript import { Encryption } from "@cipherstash/stack"; -import { users } from "./protect/schema"; +import { users } from "./encryption/schema"; const encryptionClient = await Encryption({ schemas: [users], @@ -1043,9 +1043,9 @@ const encryptionClient = await Encryption({ To enable the logger, configure the following environment variable: ```bash -PROTECT_LOG_LEVEL=debug # Enable debug logging -PROTECT_LOG_LEVEL=info # Enable info logging -PROTECT_LOG_LEVEL=error # Enable error logging +CS_LOG_LEVEL=debug # Enable debug logging +CS_LOG_LEVEL=info # Enable info logging +CS_LOG_LEVEL=error # Enable error logging ``` ## CipherStash Client diff --git a/packages/stack/__tests__/basic-protect.test.ts b/packages/stack/__tests__/basic-encryption.test.ts similarity index 100% rename from packages/stack/__tests__/basic-protect.test.ts rename to packages/stack/__tests__/basic-encryption.test.ts diff --git a/packages/stack/__tests__/bulk-protect.test.ts b/packages/stack/__tests__/bulk-encryption.test.ts similarity index 100% rename from packages/stack/__tests__/bulk-protect.test.ts rename to packages/stack/__tests__/bulk-encryption.test.ts diff --git a/packages/stack/__tests__/protect-ops.test.ts b/packages/stack/__tests__/encryption-ops.test.ts similarity index 100% rename from packages/stack/__tests__/protect-ops.test.ts rename to packages/stack/__tests__/encryption-ops.test.ts diff --git a/packages/stack/__tests__/error-codes.test.ts b/packages/stack/__tests__/error-codes.test.ts index e6943670..841a499b 100644 --- a/packages/stack/__tests__/error-codes.test.ts +++ b/packages/stack/__tests__/error-codes.test.ts @@ -1,7 +1,7 @@ import 'dotenv/config' import { encryptedColumn, encryptedTable } from '@cipherstash/schema' import { beforeAll, describe, expect, it } from 'vitest' -import { Encryption, EncryptionErrorTypes, FfiProtectError } from '../src' +import { Encryption, EncryptionErrorTypes, FfiEncryptionError } from '../src' import type { EncryptionClient } from '../src' /** FFI tests require longer timeout due to client initialization */ @@ -39,9 +39,9 @@ describe('FFI Error Code Preservation', () => { }) }) - describe('FfiProtectError class', () => { + describe('FfiEncryptionError class', () => { it('constructs with code and message', () => { - const error = new FfiProtectError({ + const error = new FfiEncryptionError({ code: 'UNKNOWN_COLUMN', message: 'Test error', }) diff --git a/packages/stack/__tests__/json-protect.test.ts b/packages/stack/__tests__/json-encryption.test.ts similarity index 100% rename from packages/stack/__tests__/json-protect.test.ts rename to packages/stack/__tests__/json-encryption.test.ts diff --git a/packages/stack/__tests__/number-protect.test.ts b/packages/stack/__tests__/number-encryption.test.ts similarity index 100% rename from packages/stack/__tests__/number-protect.test.ts rename to packages/stack/__tests__/number-encryption.test.ts diff --git a/packages/stack/src/ffi/helpers/error-code.ts b/packages/stack/src/ffi/helpers/error-code.ts index 4028f33c..a73ade7d 100644 --- a/packages/stack/src/ffi/helpers/error-code.ts +++ b/packages/stack/src/ffi/helpers/error-code.ts @@ -1,5 +1,5 @@ import { - ProtectError as FfiProtectError, + ProtectError as FfiEncryptionError, type ProtectErrorCode, } from '@cipherstash/protect-ffi' @@ -8,5 +8,5 @@ import { * Used to preserve specific error codes in EncryptionError responses. */ export function getErrorCode(error: unknown): ProtectErrorCode | undefined { - return error instanceof FfiProtectError ? error.code : undefined + return error instanceof FfiEncryptionError ? error.code : undefined } diff --git a/packages/stack/src/index.ts b/packages/stack/src/index.ts index 9ad871ef..e39823fd 100644 --- a/packages/stack/src/index.ts +++ b/packages/stack/src/index.ts @@ -4,10 +4,12 @@ import { EncryptionClient } from './ffi' import type { KeysetIdentifier } from './types' // Re-export FFI error types for programmatic error handling -export { - ProtectError as FfiProtectError, - type ProtectErrorCode, -} from '@cipherstash/protect-ffi' +import { ProtectError as _FfiEncryptionError } from '@cipherstash/protect-ffi' +export type { ProtectErrorCode } from '@cipherstash/protect-ffi' + +export const FfiEncryptionError = _FfiEncryptionError +/** @deprecated Use FfiEncryptionError */ +export const FfiProtectError = _FfiEncryptionError export const EncryptionErrorTypes = { ClientInitError: 'ClientInitError', diff --git a/packages/utils/logger/index.ts b/packages/utils/logger/index.ts index 0bb555fc..69f74f94 100644 --- a/packages/utils/logger/index.ts +++ b/packages/utils/logger/index.ts @@ -11,7 +11,8 @@ function getLevelValue(level: string): number { } } -const envLogLevel = process.env.PROTECT_LOG_LEVEL || 'info' +const envLogLevel = + process.env.CS_LOG_LEVEL || process.env.PROTECT_LOG_LEVEL || 'info' const currentLevel = getLevelValue(envLogLevel) function debug(...args: unknown[]): void { From 4c01c344a3ceee199c21982a44e876e87fe9d92f Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Wed, 11 Feb 2026 19:04:00 -0700 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20final=20sweep=20=E2=80=94=20comments?= =?UTF-8?q?,=20env=20vars,=20db=20names,=20migration=20guide=20(round=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comments: - "protected table schema" → "encrypted table schema" in 6 dynamo example files + dynamodb README - "protectjs" → "CipherStash" in nextjs-clerk schema comment - eql_payload_created_by_protect → eql_payload_created_by_encryption in docs Environment variables: - PROTECT_LOG_LEVEL → CS_LOG_LEVEL in .env files and nest module - AGENTS.md env var reference updated Database/infrastructure: - protect_example → encryption_example in next-drizzle-mysql (docker-compose, drizzle config, .env.example) - protectNestedJson → encryptedNestedJson in dynamodb test Documentation: - protect-example → encryption-example in getting-started.md - jsprotect.git → protectjs.git in hono-supabase README - Updated product URLs in next-drizzle-mysql README - MIGRATION.md: updated "after" examples with new function names - Nest README: fixed src/protect/ → src/encryption/ path Co-Authored-By: Claude Opus 4.6 --- AGENTS.md | 2 +- MIGRATION.md | 26 ++++++++++++------- docs/concepts/searchable-encryption.md | 2 +- docs/getting-started.md | 4 +-- examples/dynamo/src/bulk-operations.ts | 4 +-- examples/dynamo/src/encrypted-key-in-gsi.ts | 4 +-- .../dynamo/src/encrypted-partition-key.ts | 4 +-- examples/dynamo/src/encrypted-sort-key.ts | 4 +-- examples/dynamo/src/export-to-pg.ts | 4 +-- examples/dynamo/src/simple.ts | 4 +-- examples/hono-supabase/README.md | 4 +-- examples/nest/README.md | 2 +- .../nest/src/encryption/encryption.module.ts | 2 +- examples/next-drizzle-mysql/.env.example | 2 +- examples/next-drizzle-mysql/README.md | 4 +-- .../next-drizzle-mysql/docker-compose.yml | 4 +-- examples/next-drizzle-mysql/drizzle.config.ts | 6 ++--- examples/nextjs-clerk/src/core/db/schema.ts | 2 +- packages/dynamodb/README.md | 2 +- packages/dynamodb/__tests__/dynamodb.test.ts | 6 ++--- 20 files changed, 50 insertions(+), 42 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a25033ca..7439ebe1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,7 +64,7 @@ USER_JWT= USER_2_JWT= # Logging (plaintext is never logged by design) -PROTECT_LOG_LEVEL=debug|info|error +CS_LOG_LEVEL=debug|info|error ``` If these variables are missing, tests that require live encryption will fail or be skipped; prefer filtering to specific packages and tests while developing. diff --git a/MIGRATION.md b/MIGRATION.md index 17caca88..871017cd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -17,8 +17,8 @@ npm install @cipherstash/stack If you use the DynamoDB helpers, update the peer dependency: ```bash -# @cipherstash/protect-dynamodb now expects @cipherstash/stack -npm install @cipherstash/stack @cipherstash/protect-dynamodb +# @cipherstash/dynamodb now expects @cipherstash/stack +npm install @cipherstash/stack @cipherstash/dynamodb ``` If you use the Drizzle integration, update the peer dependency: @@ -118,22 +118,24 @@ npm install @cipherstash/stack @cipherstash/drizzle ```diff -import { protect } from '@cipherstash/protect' +import { Encryption } from '@cipherstash/stack' - import { extractProtectSchema, createProtectOperators } from '@cipherstash/drizzle/pg' +-import { extractProtectSchema, createProtectOperators } from '@cipherstash/drizzle/pg' ++import { extractEncryptionSchema, createEncryptionOperators } from '@cipherstash/drizzle/pg' - const users = extractProtectSchema(usersTable) +-const users = extractProtectSchema(usersTable) ++const users = extractEncryptionSchema(usersTable) -const client = await protect({ schemas: [users] }) +const client = await Encryption({ schemas: [users] }) - const ops = createProtectOperators(client) +-const ops = createProtectOperators(client) ++const ops = createEncryptionOperators(client) ``` -> Note: `extractProtectSchema` and `createProtectOperators` retain their names in the Drizzle package. - ### DynamoDB integration ```diff -import { protect, csTable, csColumn } from '@cipherstash/protect' +import { Encryption, encryptedTable, encryptedColumn } from '@cipherstash/stack' - import { protectDynamoDB } from '@cipherstash/protect-dynamodb' +-import { protectDynamoDB } from '@cipherstash/protect-dynamodb' ++import { encryptedDynamoDB } from '@cipherstash/dynamodb' -const users = csTable('users', { - email: csColumn('email').equality(), @@ -143,7 +145,8 @@ npm install @cipherstash/stack @cipherstash/drizzle -const client = await protect({ schemas: [users] }) +const client = await Encryption({ schemas: [users] }) - const dynamo = protectDynamoDB({ protectClient: client }) +-const dynamo = protectDynamoDB({ protectClient: client }) ++const dynamo = encryptedDynamoDB({ encryptionClient: client }) ``` ## 4. Deprecated aliases @@ -183,6 +186,11 @@ ProtectColumn → EncryptedColumn ProtectValue → EncryptedValue ProtectTableColumn → EncryptedTableColumn ProtectOperation → EncryptionOperation +extractProtectSchema → extractEncryptionSchema +createProtectOperators → createEncryptionOperators +@cipherstash/protect-dynamodb → @cipherstash/dynamodb +protectDynamoDB → encryptedDynamoDB +protectClient → encryptionClient ``` > **Important**: Run the more specific replacements first (e.g., `@cipherstash/protect/identify` before `@cipherstash/protect`) to avoid partial matches. diff --git a/docs/concepts/searchable-encryption.md b/docs/concepts/searchable-encryption.md index f29eab2f..178d6bd1 100644 --- a/docs/concepts/searchable-encryption.md +++ b/docs/concepts/searchable-encryption.md @@ -98,7 +98,7 @@ Using the above approach, Stash Encryption is generating the EQL payloads and wh So does this solve the original problem of searching on encrypted data? ```sql -# SELECT * FROM users WHERE WHERE cs_unique_v2(email) = cs_unique_v2(eql_payload_created_by_protect); +# SELECT * FROM users WHERE WHERE cs_unique_v2(email) = cs_unique_v2(eql_payload_created_by_encryption); id | name | email ----+----------------+---------------------------- 1 | Alice Johnson | mBbKmsMMkbKBSN... diff --git a/docs/getting-started.md b/docs/getting-started.md index 5c9bd19f..762a7eee 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -47,8 +47,8 @@ If you're following this getting started guide with an existing app, skip to [th If you're following this getting started guide with a clean slate, create a basic structure by running: ```bash -mkdir -p protect-example/src/encryption -cd protect-example +mkdir -p encryption-example/src/encryption +cd encryption-example git init npm init -y ``` diff --git a/examples/dynamo/src/bulk-operations.ts b/examples/dynamo/src/bulk-operations.ts index d5b0044c..0741f04e 100644 --- a/examples/dynamo/src/bulk-operations.ts +++ b/examples/dynamo/src/bulk-operations.ts @@ -34,9 +34,9 @@ const main = async () => { const items = [ { - // `pk` won't be encrypted because it's not included in the `users` protected table schema. + // `pk` won't be encrypted because it's not included in the `users` encrypted table schema. pk: 'user#1', - // `email` will be encrypted because it's included in the `users` protected table schema. + // `email` will be encrypted because it's included in the `users` encrypted table schema. email: 'abc@example.com', }, { diff --git a/examples/dynamo/src/encrypted-key-in-gsi.ts b/examples/dynamo/src/encrypted-key-in-gsi.ts index bf1792f7..d1219ce1 100644 --- a/examples/dynamo/src/encrypted-key-in-gsi.ts +++ b/examples/dynamo/src/encrypted-key-in-gsi.ts @@ -49,9 +49,9 @@ const main = async () => { }) const user = { - // `pk` won't be encrypted because it's not included in the `users` protected table schema. + // `pk` won't be encrypted because it's not included in the `users` encrypted table schema. pk: 'user#1', - // `email` will be encrypted because it's included in the `users` protected table schema. + // `email` will be encrypted because it's included in the `users` encrypted table schema. email: 'abc@example.com', } diff --git a/examples/dynamo/src/encrypted-partition-key.ts b/examples/dynamo/src/encrypted-partition-key.ts index 2d0ccdb9..face0d07 100644 --- a/examples/dynamo/src/encrypted-partition-key.ts +++ b/examples/dynamo/src/encrypted-partition-key.ts @@ -32,9 +32,9 @@ const main = async () => { }) const user = { - // `email` will be encrypted because it's included in the `users` protected table schema. + // `email` will be encrypted because it's included in the `users` encrypted table schema. email: 'abc@example.com', - // `somePlaintextAttr` won't be encrypted because it's not in the protected table schema. + // `somePlaintextAttr` won't be encrypted because it's not in the encrypted table schema. somePlaintextAttr: 'abc', } diff --git a/examples/dynamo/src/encrypted-sort-key.ts b/examples/dynamo/src/encrypted-sort-key.ts index a8533494..4364b37e 100644 --- a/examples/dynamo/src/encrypted-sort-key.ts +++ b/examples/dynamo/src/encrypted-sort-key.ts @@ -41,9 +41,9 @@ const main = async () => { }) const user = { - // `pk` won't be encrypted because it's not in the protected table schema. + // `pk` won't be encrypted because it's not in the encrypted table schema. pk: 'user#1', - // `email` will be encrypted because it's included in the `users` protected table schema. + // `email` will be encrypted because it's included in the `users` encrypted table schema. email: 'abc@example.com', } diff --git a/examples/dynamo/src/export-to-pg.ts b/examples/dynamo/src/export-to-pg.ts index d1825f0a..dd1cac2a 100644 --- a/examples/dynamo/src/export-to-pg.ts +++ b/examples/dynamo/src/export-to-pg.ts @@ -36,9 +36,9 @@ const main = async () => { }) const user = { - // `pk` won't be encrypted because it's not included in the `users` protected table schema. + // `pk` won't be encrypted because it's not included in the `users` encrypted table schema. pk: 'user#1', - // `email` will be encrypted because it's included in the `users` protected table schema. + // `email` will be encrypted because it's included in the `users` encrypted table schema. email: 'abc@example.com', } diff --git a/examples/dynamo/src/simple.ts b/examples/dynamo/src/simple.ts index b31f5d6e..2e4e670e 100644 --- a/examples/dynamo/src/simple.ts +++ b/examples/dynamo/src/simple.ts @@ -33,9 +33,9 @@ const main = async () => { }) const user = { - // `pk` won't be encrypted because it's not included in the `users` protected table schema. + // `pk` won't be encrypted because it's not included in the `users` encrypted table schema. pk: 'user#1', - // `email` will be encrypted because it's included in the `users` protected table schema. + // `email` will be encrypted because it's included in the `users` encrypted table schema. email: 'abc@example.com', } diff --git a/examples/hono-supabase/README.md b/examples/hono-supabase/README.md index de7240df..7dcace9f 100644 --- a/examples/hono-supabase/README.md +++ b/examples/hono-supabase/README.md @@ -52,8 +52,8 @@ This project demonstrates how to encrypt data using [@cipherstash/stack](https:/ ### 1. Clone the repository ```bash -git clone https://github.com/cipherstash/jsprotect.git -cd jsprotect/examples/hono-supabase +git clone https://github.com/cipherstash/protectjs.git +cd protectjs/examples/hono-supabase ``` ### 2. Install dependencies diff --git a/examples/nest/README.md b/examples/nest/README.md index d1a63be6..a1b0fb9f 100644 --- a/examples/nest/README.md +++ b/examples/nest/README.md @@ -24,7 +24,7 @@ CS_CLIENT_ACCESS_KEY= ``` ### How encryption works here -- `src/protect/schema.ts` defines tables with `.equality()`, `.orderAndRange()`, `.freeTextSearch()` for searchable encryption on Postgres. +- `src/encryption/schema.ts` defines tables with `.equality()`, `.orderAndRange()`, `.freeTextSearch()` for searchable encryption on Postgres. - `EncryptionModule` initializes an `EncryptionClient` with those schemas and injects an `EncryptionService`. - `AppService` uses `encryptModel`/`decryptModel` and bulk variants to demonstrate single and bulk flows. diff --git a/examples/nest/src/encryption/encryption.module.ts b/examples/nest/src/encryption/encryption.module.ts index cb5abd0d..e3491261 100644 --- a/examples/nest/src/encryption/encryption.module.ts +++ b/examples/nest/src/encryption/encryption.module.ts @@ -37,7 +37,7 @@ export class EncryptionModule { clientKey: clientKey ?? '', clientAccessKey: clientAccessKey ?? '', logLevel: configService.get<'debug' | 'info' | 'error'>( - 'PROTECT_LOG_LEVEL', + 'CS_LOG_LEVEL', 'info', ), ...config, diff --git a/examples/next-drizzle-mysql/.env.example b/examples/next-drizzle-mysql/.env.example index f481ae25..ce3ff3f9 100644 --- a/examples/next-drizzle-mysql/.env.example +++ b/examples/next-drizzle-mysql/.env.example @@ -1,4 +1,4 @@ -DATABASE_URL=mysql://protect_example:password@127.0.0.1:3306/protect_example +DATABASE_URL=mysql://encryption_example:password@127.0.0.1:3306/encryption_example CS_CLIENT_ID= CS_CLIENT_KEY= CS_CLIENT_ACCESS_KEY= diff --git a/examples/next-drizzle-mysql/README.md b/examples/next-drizzle-mysql/README.md index acaca4e1..d758e20f 100644 --- a/examples/next-drizzle-mysql/README.md +++ b/examples/next-drizzle-mysql/README.md @@ -4,7 +4,7 @@ This example demonstrates how to build a modern web application using: - [Next.js](https://nextjs.org/) - React framework for production - [Drizzle ORM](https://orm.drizzle.team/) - TypeScript ORM for SQL databases - [MySQL](https://www.mysql.com/) - Popular open-source relational database -- [Stash Encryption](https://cipherstash.com/protect) - Data protection and encryption library +- [Stash Encryption](https://github.com/cipherstash/protectjs) - Data protection and encryption library ## Features @@ -73,5 +73,5 @@ The application will be available at `http://localhost:3000`. - [Next.js Documentation](https://nextjs.org/docs) - [Drizzle ORM Documentation](https://orm.drizzle.team/docs/overview) -- [Stash Encryption Documentation](https://cipherstash.com/protect/docs) +- [Stash Encryption Documentation](https://github.com/cipherstash/protectjs#documentation) - [MySQL Documentation](https://dev.mysql.com/doc/) diff --git a/examples/next-drizzle-mysql/docker-compose.yml b/examples/next-drizzle-mysql/docker-compose.yml index 3b40c63d..5d7e59a6 100644 --- a/examples/next-drizzle-mysql/docker-compose.yml +++ b/examples/next-drizzle-mysql/docker-compose.yml @@ -4,8 +4,8 @@ services: image: mysql:latest environment: MYSQL_ROOT_PASSWORD: password - MYSQL_DATABASE: protect_example - MYSQL_USER: protect_example + MYSQL_DATABASE: encryption_example + MYSQL_USER: encryption_example MYSQL_PASSWORD: password ports: - "3306:3306" diff --git a/examples/next-drizzle-mysql/drizzle.config.ts b/examples/next-drizzle-mysql/drizzle.config.ts index ed499c10..1d8b93a3 100644 --- a/examples/next-drizzle-mysql/drizzle.config.ts +++ b/examples/next-drizzle-mysql/drizzle.config.ts @@ -6,10 +6,10 @@ export default defineConfig({ dbCredentials: { host: '127.0.0.1', port: 3306, - user: 'protect_example', + user: 'encryption_example', password: 'password', - database: 'protect_example', + database: 'encryption_example', }, }) -// mysql://protect_example:password@127.0.0.1:3306/protect_example +// mysql://encryption_example:password@127.0.0.1:3306/encryption_example diff --git a/examples/nextjs-clerk/src/core/db/schema.ts b/examples/nextjs-clerk/src/core/db/schema.ts index 593339f8..ae8aa86b 100644 --- a/examples/nextjs-clerk/src/core/db/schema.ts +++ b/examples/nextjs-clerk/src/core/db/schema.ts @@ -1,6 +1,6 @@ import { jsonb, pgTable, serial, varchar } from 'drizzle-orm/pg-core' -// Data that is encrypted using protectjs is stored as jsonb in postgres +// Data that is encrypted using CipherStash is stored as jsonb in postgres // --- // This example does not include any searchable encrypted fields // If you want to search on encrypted fields, you will need to install EQL. diff --git a/packages/dynamodb/README.md b/packages/dynamodb/README.md index 0e083fa4..c9b5e983 100644 --- a/packages/dynamodb/README.md +++ b/packages/dynamodb/README.md @@ -26,7 +26,7 @@ import { encryptedDynamoDB } from '@cipherstash/protect-dynamodb' import { Encryption, encryptedColumn, encryptedTable } from '@cipherstash/stack' import { PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb' -// Define your protected table schema +// Define your encrypted table schema const users = encryptedTable('users', { email: encryptedColumn('email').equality(), }) diff --git a/packages/dynamodb/__tests__/dynamodb.test.ts b/packages/dynamodb/__tests__/dynamodb.test.ts index e55e9f00..011c9cc4 100644 --- a/packages/dynamodb/__tests__/dynamodb.test.ts +++ b/packages/dynamodb/__tests__/dynamodb.test.ts @@ -21,8 +21,8 @@ const schema = encryptedTable('dynamo_cipherstash_test', { protected: encryptedValue('example.protected'), deep: { protected: encryptedValue('example.deep.protected'), - protectNestedJson: encryptedValue( - 'example.deep.protectNestedJson', + encryptedNestedJson: encryptedValue( + 'example.deep.encryptedNestedJson', ).dataType('json'), }, }, @@ -76,7 +76,7 @@ describe('dynamodb helpers', () => { deep: { protected: 'deep protected', notProtected: 'deep not protected', - protectNestedJson: { + encryptedNestedJson: { hello: 'world', }, }, From 4d44ce5c1f3dcd056220cb8fb4323fc460a039c2 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Wed, 11 Feb 2026 19:07:17 -0700 Subject: [PATCH 9/9] ci: update tests --- .github/workflows/tests.yml | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9d4ac787..76637a73 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,25 +33,25 @@ jobs: - name: Create .env file in ./packages/protect/ run: | - touch ./packages/protect/.env - echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/protect/.env - echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/protect/.env - echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/protect/.env - echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/protect/.env - echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> ./packages/protect/.env - echo "SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" >> ./packages/protect/.env - echo "CS_ZEROKMS_HOST=https://ap-southeast-2.aws.zerokms.cipherstashmanaged.net" >> ./packages/protect/.env - echo "CS_CTS_HOST=https://ap-southeast-2.aws.cts.cipherstashmanaged.net" >> ./packages/protect/.env + touch ./packages/stack/.env + echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/stack/.env + echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/stack/.env + echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/stack/.env + echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/stack/.env + echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> ./packages/stack/.env + echo "SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" >> ./packages/stack/.env + echo "CS_ZEROKMS_HOST=https://ap-southeast-2.aws.zerokms.cipherstashmanaged.net" >> ./packages/stack/.env + echo "CS_CTS_HOST=https://ap-southeast-2.aws.cts.cipherstashmanaged.net" >> ./packages/stack/.env - - name: Create .env file in ./packages/protect-dynamodb/ + - name: Create .env file in ./packages/dynamodb/ run: | - touch ./packages/protect-dynamodb/.env - echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/protect-dynamodb/.env - echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/protect-dynamodb/.env - echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/protect-dynamodb/.env - echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/protect-dynamodb/.env - echo "CS_ZEROKMS_HOST=https://ap-southeast-2.aws.zerokms.cipherstashmanaged.net" >> ./packages/protect/.env - echo "CS_CTS_HOST=https://ap-southeast-2.aws.cts.cipherstashmanaged.net" >> ./packages/protect/.env + touch ./packages/dynamodb/.env + echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/dynamodb/.env + echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/dynamodb/.env + echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/dynamodb/.env + echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/dynamodb/.env + echo "CS_ZEROKMS_HOST=https://ap-southeast-2.aws.zerokms.cipherstashmanaged.net" >> ./packages/dynamodb/.env + echo "CS_CTS_HOST=https://ap-southeast-2.aws.cts.cipherstashmanaged.net" >> ./packages/dynamodb/.env - name: Create .env file in ./packages/drizzle/ run: |