├── examples ├── dynamo │ ├── sql │ │ └── .gitkeep │ ├── .gitignore │ ├── src │ │ ├── common │ │ │ ├── log.ts │ │ │ ├── protect.ts │ │ │ └── dynamo.ts │ │ ├── simple.ts │ │ ├── encrypted-partition-key.ts │ │ ├── bulk-operations.ts │ │ └── encrypted-sort-key.ts │ ├── docker-compose.yml │ ├── package.json │ ├── README.md │ └── CHANGELOG.md ├── typeorm │ ├── .gitignore │ ├── tsconfig.json │ ├── src │ │ ├── decorators │ │ │ └── encrypted-column.ts │ │ ├── protect.ts │ │ ├── data-source.ts │ │ ├── entity │ │ │ └── User.ts │ │ └── utils │ │ │ └── encrypted-column.ts │ ├── package.json │ └── CHANGELOG.md ├── nextjs-clerk │ ├── src │ │ ├── app │ │ │ ├── favicon.ico │ │ │ ├── add-user │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── globals.css │ │ │ └── page.tsx │ │ ├── lib │ │ │ ├── utils.ts │ │ │ └── actions.ts │ │ ├── core │ │ │ ├── db │ │ │ │ ├── index.ts │ │ │ │ └── schema.ts │ │ │ └── protect │ │ │ │ └── index.ts │ │ ├── middleware.ts │ │ └── components │ │ │ ├── ui │ │ │ ├── label.tsx │ │ │ ├── toaster.tsx │ │ │ ├── input.tsx │ │ │ ├── tooltip.tsx │ │ │ └── button.tsx │ │ │ ├── UserTable.tsx │ │ │ ├── Header.tsx │ │ │ └── AddUserForm.tsx │ ├── public │ │ ├── vercel.svg │ │ ├── file.svg │ │ ├── window.svg │ │ ├── globe.svg │ │ └── next.svg │ ├── postcss.config.mjs │ ├── drizzle.config.ts │ ├── .env.example │ ├── components.json │ ├── next.config.ts │ ├── tsconfig.json │ ├── package.json │ └── tailwind.config.ts ├── nest │ ├── src │ │ ├── protect │ │ │ ├── protect.constants.ts │ │ │ ├── interfaces │ │ │ │ └── protect-config.interface.ts │ │ │ ├── schema.ts │ │ │ ├── index.ts │ │ │ ├── utils │ │ │ │ └── get-protect-service.util.ts │ │ │ ├── decorators │ │ │ │ ├── decrypt.decorator.ts │ │ │ │ └── encrypt.decorator.ts │ │ │ ├── protect.service.ts │ │ │ └── interceptors │ │ │ │ └── encrypt.interceptor.ts │ │ ├── main.ts │ │ ├── app.module.ts │ │ └── app.controller.ts │ ├── tsconfig.build.json │ ├── nest-cli.json │ ├── test │ │ └── jest-e2e.json │ ├── CHANGELOG.md │ ├── tsconfig.json │ ├── .gitignore │ ├── package.json │ └── README.md ├── next-drizzle-mysql │ ├── postcss.config.mjs │ ├── src │ │ ├── app │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── actions.ts │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── protect │ │ │ ├── schema.ts │ │ │ └── index.ts │ │ ├── db │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ └── components │ │ │ └── form.tsx │ ├── public │ │ ├── vercel.svg │ │ ├── window.svg │ │ ├── file.svg │ │ ├── globe.svg │ │ └── next.svg │ ├── .env.example │ ├── drizzle │ │ ├── 0000_brave_madrox.sql │ │ └── meta │ │ │ ├── _journal.json │ │ │ └── 0000_snapshot.json │ ├── next.config.ts │ ├── docker-compose.yml │ ├── drizzle.config.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md ├── drizzle │ ├── .env.example │ ├── drizzle │ │ ├── 0000_goofy_cannonball.sql │ │ ├── meta │ │ │ ├── _journal.json │ │ │ └── 0000_snapshot.json │ │ └── 0001_transactions.sql │ ├── src │ │ ├── db │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── routes │ │ │ └── transactions.ts │ │ ├── protect │ │ │ └── config.ts │ │ └── server.ts │ ├── environment.d.ts │ ├── drizzle.config.ts │ ├── tsconfig.json │ └── package.json ├── hono-supabase │ ├── .env.example │ ├── environment.d.ts │ ├── tsconfig.json │ ├── .gitignore │ └── package.json └── basic │ ├── protect.ts │ ├── package.json │ ├── tsconfig.json │ ├── index.ts │ └── README.md ├── .flox ├── .gitignore ├── env.json └── .gitattributes ├── .vscode └── settings.json ├── packages ├── drizzle │ ├── .npmignore │ ├── tsup.config.ts │ ├── tsconfig.json │ ├── __tests__ │ │ └── utils │ │ │ ├── markdown-parser.ts │ │ │ ├── code-executor.test.ts │ │ │ └── code-executor.ts │ ├── CHANGELOG.md │ └── package.json ├── protect │ ├── .npmignore │ ├── tsup.config.ts │ ├── src │ │ ├── client.ts │ │ ├── ffi │ │ │ └── operations │ │ │ │ ├── base-operation.ts │ │ │ │ └── search-terms.ts │ │ └── helpers │ │ │ └── index.ts │ ├── tsconfig.json │ ├── __tests__ │ │ ├── basic-protect.test.ts │ │ ├── keysets.test.ts │ │ └── search-terms.test.ts │ └── package.json ├── schema │ ├── .npmignore │ ├── tsup.config.ts │ ├── tsconfig.json │ ├── package.json │ └── CHANGELOG.md ├── protect-dynamodb │ ├── .npmignore │ ├── tsup.config.ts │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── types.ts │ │ ├── operations │ │ ├── encrypt-model.ts │ │ ├── base-operation.ts │ │ ├── decrypt-model.ts │ │ ├── bulk-decrypt-models.ts │ │ ├── search-terms.ts │ │ └── bulk-encrypt-models.ts │ │ └── index.ts ├── nextjs │ ├── tsconfig.jest.json │ ├── README.md │ ├── tsup.config.ts │ ├── tsconfig.json │ ├── src │ │ └── clerk │ │ │ └── index.ts │ ├── package.json │ └── CHANGELOG.md ├── jseql │ └── README.md └── utils │ ├── logger │ └── index.ts │ └── config │ └── index.ts ├── docs ├── images │ └── protectjs-architecture.png ├── how-to │ ├── sst-external-packages.md │ ├── nextjs-external-packages.md │ └── npm-lockfile-v3.md └── README.md ├── local ├── create-ci-table.sql ├── Dockerfile ├── docker-compose.yml └── postgres-entrypoint.sh ├── pnpm-workspace.yaml ├── .changeset ├── config.json └── README.md ├── .github ├── workflows │ ├── rebuild-docs.yml │ ├── release.yml │ └── tests.yml └── ISSUE_TEMPLATE │ └── docs-feedback.yml ├── turbo.json ├── biome.json ├── tsconfig.json ├── .gitignore ├── LICENSE.md ├── package.json └── .cursorrules /examples/dynamo/sql/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.flox/.gitignore: -------------------------------------------------------------------------------- 1 | run/ 2 | cache/ 3 | lib/ 4 | log/ 5 | !env/ 6 | -------------------------------------------------------------------------------- /.flox/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "protectjs", 3 | "version": 1 4 | } 5 | -------------------------------------------------------------------------------- /examples/dynamo/.gitignore: -------------------------------------------------------------------------------- 1 | docker 2 | sql/cipherstash-encrypt.sql 3 | -------------------------------------------------------------------------------- /.flox/.gitattributes: -------------------------------------------------------------------------------- 1 | env/manifest.lock linguist-generated=true linguist-language=JSON 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "conventionalCommits.scopes": ["protect", "examples"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/typeorm/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | build/ 5 | tmp/ 6 | temp/ -------------------------------------------------------------------------------- /packages/drizzle/.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | .turbo 3 | node_modules 4 | cipherstash.secret.toml 5 | cipherstash.toml -------------------------------------------------------------------------------- /packages/protect/.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | .turbo 3 | node_modules 4 | cipherstash.secret.toml 5 | cipherstash.toml -------------------------------------------------------------------------------- /packages/schema/.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | .turbo 3 | node_modules 4 | cipherstash.secret.toml 5 | cipherstash.toml -------------------------------------------------------------------------------- /packages/protect-dynamodb/.npmignore: -------------------------------------------------------------------------------- 1 | .env 2 | .turbo 3 | node_modules 4 | cipherstash.secret.toml 5 | cipherstash.toml -------------------------------------------------------------------------------- /docs/images/protectjs-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cipherstash/protectjs/HEAD/docs/images/protectjs-architecture.png -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cipherstash/protectjs/HEAD/examples/nextjs-clerk/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/nest/src/protect/protect.constants.ts: -------------------------------------------------------------------------------- 1 | export const PROTECT_CONFIG = 'PROTECT_CONFIG' 2 | export const PROTECT_CLIENT = 'PROTECT_CLIENT' 3 | -------------------------------------------------------------------------------- /examples/nest/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | } 4 | 5 | export default config 6 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cipherstash/protectjs/HEAD/examples/next-drizzle-mysql/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs-clerk/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/dynamo/src/common/log.ts: -------------------------------------------------------------------------------- 1 | export function log(description: string, data: unknown) { 2 | console.log(`\n${description}:\n${JSON.stringify(data, null, 2)}`) 3 | } 4 | -------------------------------------------------------------------------------- /examples/drizzle/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://[username]:[password]@localhost:6432/[database]" 2 | CS_CLIENT_ID= 3 | CS_CLIENT_KEY= 4 | CS_WORKSPACE_ID= 5 | CS_CLIENT_ACCESS_KEY= -------------------------------------------------------------------------------- /packages/nextjs/tsconfig.jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "moduleResolution": "node10", 5 | "verbatimModuleSyntax": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=mysql://protect_example:password@127.0.0.1:3306/protect_example 2 | CS_CLIENT_ID= 3 | CS_CLIENT_KEY= 4 | CS_CLIENT_ACCESS_KEY= 5 | CS_WORKSPACE_CRN= -------------------------------------------------------------------------------- /examples/nextjs-clerk/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | } 7 | 8 | export default config 9 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/drizzle/0000_brave_madrox.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `users` ( 2 | `id` int AUTO_INCREMENT NOT NULL, 3 | `name` json, 4 | `email` json, 5 | CONSTRAINT `users_id` PRIMARY KEY(`id`) 6 | ); 7 | -------------------------------------------------------------------------------- /packages/schema/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | sourcemap: true, 7 | dts: true, 8 | }) 9 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/protect/schema.ts: -------------------------------------------------------------------------------- 1 | import { csColumn, csTable } from '@cipherstash/protect' 2 | 3 | export const users = csTable('users', { 4 | email: csColumn('email'), 5 | name: csColumn('name'), 6 | }) 7 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /packages/protect-dynamodb/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | sourcemap: true, 7 | dts: true, 8 | }) 9 | -------------------------------------------------------------------------------- /examples/nest/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/nextjs/README.md: -------------------------------------------------------------------------------- 1 | # @cipherstash/protect 2 | 3 | This is the main package for the CipherStash Protect JavaScript Package. 4 | Please refer to the [main README](https://github.com/cipherstash/protectjs) for more information. -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next' 2 | 3 | const nextConfig: NextConfig = { 4 | serverExternalPackages: ['@cipherstash/protect', 'mysql2'], 5 | } 6 | 7 | export default nextConfig 8 | -------------------------------------------------------------------------------- /packages/nextjs/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/clerk/index.ts'], 5 | format: ['cjs', 'esm'], 6 | sourcemap: true, 7 | dts: true, 8 | }) 9 | -------------------------------------------------------------------------------- /examples/drizzle/drizzle/0000_goofy_cannonball.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "users" ( 2 | "id" serial PRIMARY KEY NOT NULL, 3 | "email" varchar, 4 | "email_encrypted" jsonb NOT NULL, 5 | CONSTRAINT "users_email_unique" UNIQUE("email") 6 | ); 7 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/mysql2' 2 | 3 | if (!process.env.DATABASE_URL) { 4 | throw new Error('DATABASE_URL is not set') 5 | } 6 | 7 | export const db = drizzle(process.env.DATABASE_URL) 8 | -------------------------------------------------------------------------------- /packages/jseql/README.md: -------------------------------------------------------------------------------- 1 | # @cipherstash/jseql 2 | 3 | The `@cipherstash/jseql` package has been deprecated in favor of the `@cipherstash/protect` package. 4 | Please refer to the [main README](https://github.com/cipherstash/protectjs) for more information. -------------------------------------------------------------------------------- /examples/nest/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/hono-supabase/.env.example: -------------------------------------------------------------------------------- 1 | # Credentials for your CipherStash project 2 | CS_CLIENT_ID= 3 | CS_CLIENT_KEY= 4 | CS_CLIENT_ACCESS_KEY= 5 | CS_WORKSPACE_ID= 6 | 7 | # Connection details for your Supabase project 8 | SUPABASE_URL= 9 | SUPABASE_ANON_KEY= 10 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { int, json, mysqlTable, uniqueIndex } from 'drizzle-orm/mysql-core' 2 | 3 | export const users = mysqlTable('users', { 4 | id: int().primaryKey().autoincrement(), 5 | name: json(), 6 | email: json(), 7 | }) 8 | -------------------------------------------------------------------------------- /local/create-ci-table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "protect-ci" ( 2 | id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, 3 | email eql_v2_encrypted, 4 | age eql_v2_encrypted, 5 | score eql_v2_encrypted, 6 | profile eql_v2_encrypted, 7 | created_at TIMESTAMP DEFAULT NOW() 8 | ); -------------------------------------------------------------------------------- /examples/drizzle/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { drizzle } from 'drizzle-orm/postgres-js' 3 | import postgres from 'postgres' 4 | 5 | const connectionString = process.env.DATABASE_URL 6 | const client = postgres(connectionString) 7 | export const db = drizzle(client) 8 | -------------------------------------------------------------------------------- /examples/drizzle/environment.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | DATABASE_URL: string 4 | CS_CLIENT_ID: string 5 | CS_CLIENT_KEY: string 6 | CS_WORKSPACE_CRN: string 7 | CS_CLIENT_ACCESS_KEY: string 8 | PORT?: string 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/dynamo/src/common/protect.ts: -------------------------------------------------------------------------------- 1 | import { csColumn, csTable, protect } from '@cipherstash/protect' 2 | 3 | export const users = csTable('users', { 4 | email: csColumn('email').equality(), 5 | }) 6 | 7 | export const protectClient = await protect({ 8 | schemas: [users], 9 | }) 10 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/protect/index.ts: -------------------------------------------------------------------------------- 1 | import { type ProtectClientConfig, protect } from '@cipherstash/protect' 2 | import { users } from './schema' 3 | 4 | const config: ProtectClientConfig = { 5 | schemas: [users], 6 | } 7 | 8 | export const protectClient = await protect(config) 9 | -------------------------------------------------------------------------------- /examples/hono-supabase/environment.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | SUPABASE_URL: string 4 | SUPABASE_ANON_KEY: string 5 | CS_CLIENT_ID: string 6 | CS_CLIENT_KEY: string 7 | CS_WORKSPACE_ID: string 8 | CS_CLIENT_ACCESS_KEY: string 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/nest/src/main.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { NestFactory } from '@nestjs/core' 3 | import { AppModule } from './app.module' 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule) 7 | await app.listen(process.env.PORT ?? 3000) 8 | } 9 | bootstrap() 10 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "mysql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1748545269720, 9 | "tag": "0000_brave_madrox", 10 | "breakpoints": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/drizzle/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { defineConfig } from 'drizzle-kit' 3 | 4 | export default defineConfig({ 5 | out: './drizzle', 6 | schema: './src/db/schema.ts', 7 | dialect: 'postgresql', 8 | dbCredentials: { 9 | url: process.env.DATABASE_URL, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /packages/protect/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/client.ts', 'src/identify/index.ts'], 5 | format: ['cjs', 'esm'], 6 | sourcemap: true, 7 | dts: true, 8 | target: 'es2022', 9 | tsconfig: './tsconfig.json', 10 | }) 11 | -------------------------------------------------------------------------------- /examples/hono-supabase/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "NodeNext", 5 | "strict": true, 6 | "verbatimModuleSyntax": true, 7 | "skipLibCheck": true, 8 | "types": ["node"], 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "hono/jsx" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "examples/*" 4 | 5 | catalogs: 6 | # Can be referened as catalog:repo 7 | repo: 8 | tsup: 8.4.0 9 | typescript: 5.6.3 10 | tsx: 4.19.3 11 | vitest: 3.1.3 12 | 13 | security: 14 | '@clerk/nextjs': 6.31.2 15 | next: 15.5.9 16 | vite: 6.4.1 17 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /examples/nest/src/protect/interfaces/protect-config.interface.ts: -------------------------------------------------------------------------------- 1 | import type { ProtectTable, ProtectTableColumn } from '@cipherstash/protect' 2 | 3 | export interface ProtectConfig { 4 | workspaceCrn: string 5 | clientId: string 6 | clientKey: string 7 | clientAccessKey: string 8 | logLevel?: 'debug' | 'info' | 'error' 9 | schemas?: ProtectTable[] 10 | } 11 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { defineConfig } from 'drizzle-kit' 3 | 4 | export default defineConfig({ 5 | out: './drizzle', 6 | schema: './src/db/schema.ts', 7 | dialect: 'postgresql', 8 | dbCredentials: { 9 | // biome-ignore lint/style/noNonNullAssertion: Postgres URL is required 10 | url: process.env.POSTGRES_URL!, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/basic/protect.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { 3 | type ProtectClientConfig, 4 | csColumn, 5 | csTable, 6 | protect, 7 | } from '@cipherstash/protect' 8 | 9 | export const users = csTable('users', { 10 | name: csColumn('name'), 11 | }) 12 | 13 | const config: ProtectClientConfig = { 14 | schemas: [users], 15 | } 16 | 17 | export const protectClient = await protect(config) 18 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | db: 4 | image: mysql:latest 5 | environment: 6 | MYSQL_ROOT_PASSWORD: password 7 | MYSQL_DATABASE: protect_example 8 | MYSQL_USER: protect_example 9 | MYSQL_PASSWORD: password 10 | ports: 11 | - "3306:3306" 12 | volumes: 13 | - mysql_data:/var/lib/mysql 14 | 15 | volumes: 16 | mysql_data: 17 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/.env.example: -------------------------------------------------------------------------------- 1 | # Clerk 2 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key 3 | CLERK_SECRET_KEY=your_clerk_secret_key 4 | 5 | # Postres - Try out Supabase for free https://supabase.com/ 6 | POSTGRES_URL=your_postgres_url 7 | 8 | # CipherStash Protect.js 9 | CS_WORKSPACE_ID=your_workspace_id 10 | CS_CLIENT_ID=your_client_id 11 | CS_CLIENT_KEY=your_client_secret 12 | CS_CLIENT_ACCESS_KEY=your_access_key -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/core/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/postgres-js' 2 | import postgres from 'postgres' 3 | 4 | if (!process.env.POSTGRES_URL) { 5 | throw new Error( 6 | "[ Server ] Error: Drizzle ORM - You did not supply 'POSTGRES_URL' env var.", 7 | ) 8 | } 9 | 10 | const connectionString = process.env.POSTGRES_URL 11 | const client = postgres(connectionString) 12 | export const db = drizzle(client) 13 | -------------------------------------------------------------------------------- /.github/workflows/rebuild-docs.yml: -------------------------------------------------------------------------------- 1 | name: Rebuild Docs 2 | 3 | on: 4 | push: 5 | tags: 6 | - '@cipherstash/protect@*' 7 | - '@cipherstash/drizzle@*' 8 | 9 | jobs: 10 | trigger-docs-rebuild: 11 | name: Trigger Docs Rebuild 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Send webhook 15 | env: 16 | WEBHOOK_URL: ${{ secrets.DOCS_WEBHOOK_URL }} 17 | run: | 18 | curl -X POST "$WEBHOOK_URL" 19 | -------------------------------------------------------------------------------- /examples/hono-supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # dev 2 | .yarn/ 3 | !.yarn/releases 4 | .vscode/* 5 | !.vscode/launch.json 6 | !.vscode/*.code-snippets 7 | .idea/workspace.xml 8 | .idea/usage.statistics.xml 9 | .idea/shelf 10 | 11 | # deps 12 | node_modules/ 13 | 14 | # env 15 | .env 16 | .env.production 17 | 18 | # logs 19 | logs/ 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | pnpm-debug.log* 25 | lerna-debug.log* 26 | 27 | # misc 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/app/add-user/page.tsx: -------------------------------------------------------------------------------- 1 | import AddUserForm from '@/components/AddUserForm' 2 | import Header from '@/components/Header' 3 | 4 | export default function AddUser() { 5 | return ( 6 |
7 |
8 |
9 |

Add new user

10 | 11 |
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { defineConfig } from 'drizzle-kit' 3 | export default defineConfig({ 4 | dialect: 'mysql', 5 | schema: './src/db/schema.ts', 6 | dbCredentials: { 7 | host: '127.0.0.1', 8 | port: 3306, 9 | user: 'protect_example', 10 | password: 'password', 11 | database: 'protect_example', 12 | }, 13 | }) 14 | 15 | // mysql://protect_example:password@127.0.0.1:3306/protect_example 16 | -------------------------------------------------------------------------------- /examples/drizzle/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1734712905691, 9 | "tag": "0000_goofy_cannonball", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1734712905700, 16 | "tag": "0001_transactions", 17 | "breakpoints": true 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /local/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:latest 2 | 3 | # Copy the custom entrypoint script and SQL files 4 | COPY postgres-entrypoint.sh /usr/local/bin/postgres-entrypoint.sh 5 | COPY cipherstash-encrypt-2-1-8.sql /tmp/cipherstash-encrypt-2-1-8.sql 6 | COPY create-ci-table.sql /tmp/create-ci-table.sql 7 | 8 | # Make the entrypoint script executable 9 | RUN chmod +x /usr/local/bin/postgres-entrypoint.sh 10 | 11 | # Use the custom entrypoint 12 | ENTRYPOINT ["/usr/local/bin/postgres-entrypoint.sh"] 13 | 14 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/basic-example", 3 | "private": true, 4 | "version": "1.1.14", 5 | "type": "module", 6 | "scripts": { 7 | "start": "tsx index.ts" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "", 13 | "dependencies": { 14 | "@cipherstash/protect": "workspace:*", 15 | "dotenv": "^16.4.7" 16 | }, 17 | "devDependencies": { 18 | "tsx": "catalog:repo", 19 | "typescript": "catalog:repo" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /examples/hono-supabase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hono-supabase", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "tsx watch src/index.ts" 7 | }, 8 | "dependencies": { 9 | "@cipherstash/protect": "workspace:*", 10 | "@hono/node-server": "^1.13.7", 11 | "@supabase/supabase-js": "^2.47.10", 12 | "dotenv": "^16.4.7", 13 | "hono": "^4.6.15" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^20.11.17", 17 | "tsx": "catalog:repo", 18 | "typescript": "catalog:repo" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "inputs": ["$TURBO_DEFAULT$", ".env*"] 7 | }, 8 | "release": { 9 | "dependsOn": ["^build"], 10 | "inputs": ["$TURBO_DEFAULT$", ".env*"] 11 | }, 12 | "dev": { 13 | "cache": false, 14 | "persistent": true 15 | }, 16 | "test": { 17 | "dependsOn": ["^build"], 18 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 19 | "cache": false 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/nest/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { ConfigModule } from '@nestjs/config' 3 | import { AppController } from './app.controller' 4 | import { AppService } from './app.service' 5 | import { ProtectModule, schemas } from './protect' 6 | 7 | @Module({ 8 | imports: [ 9 | ConfigModule.forRoot({ 10 | isGlobal: true, 11 | }), 12 | ProtectModule.forRoot({ 13 | schemas, 14 | }), 15 | ], 16 | controllers: [AppController], 17 | providers: [AppService], 18 | }) 19 | export class AppModule {} 20 | -------------------------------------------------------------------------------- /examples/drizzle/drizzle/0001_transactions.sql: -------------------------------------------------------------------------------- 1 | -- Drop old users table if it exists 2 | DROP TABLE IF EXISTS "users"; 3 | 4 | -- Create transactions table 5 | CREATE TABLE IF NOT EXISTS "transactions" ( 6 | "id" serial PRIMARY KEY NOT NULL, 7 | "account_number" eql_v2_encrypted, 8 | "amount" eql_v2_encrypted, 9 | "description" eql_v2_encrypted, 10 | "transaction_type" varchar(50) NOT NULL, 11 | "status" varchar(20) NOT NULL DEFAULT 'pending', 12 | "created_at" timestamp DEFAULT now() NOT NULL, 13 | "updated_at" timestamp DEFAULT now() NOT NULL 14 | ); 15 | 16 | -------------------------------------------------------------------------------- /examples/drizzle/src/routes/transactions.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { 3 | createTransaction, 4 | deleteTransaction, 5 | getTransaction, 6 | getTransactions, 7 | updateTransaction, 8 | } from '../controllers/transactions' 9 | 10 | export const transactionsRouter = Router() 11 | 12 | transactionsRouter.get('/', getTransactions) 13 | transactionsRouter.post('/', createTransaction) 14 | transactionsRouter.get('/:id', getTransaction) 15 | transactionsRouter.put('/:id', updateTransaction) 16 | transactionsRouter.delete('/:id', deleteTransaction) 17 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /packages/drizzle/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ['src/pg/index.ts'], 6 | outDir: 'dist/pg', 7 | format: ['cjs', 'esm'], 8 | sourcemap: true, 9 | dts: true, 10 | }, 11 | { 12 | entry: ['src/bin/generate-eql-migration.ts'], 13 | outDir: 'dist/bin', 14 | format: ['cjs', 'esm'], 15 | target: 'esnext', 16 | clean: true, 17 | splitting: true, 18 | minify: true, 19 | shims: true, 20 | banner: { 21 | js: '#!/usr/bin/env node', 22 | }, 23 | }, 24 | ]) 25 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next' 2 | 3 | const nextConfig: NextConfig = { 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: 'https', 8 | hostname: 'cipherstash.com', 9 | }, 10 | ], 11 | }, 12 | // serverExternalPackages does not work with workspace packages 13 | // https://github.com/vercel/next.js/issues/43433 14 | // --- 15 | // TODO: Once this is fixed upstream, we can use the workspace packages 16 | serverExternalPackages: ['@cipherstash/protect'], 17 | } 18 | 19 | export default nextConfig 20 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { protectClerkMiddleware } from '@cipherstash/nextjs/clerk' 2 | import { clerkMiddleware } from '@clerk/nextjs/server' 3 | 4 | export default clerkMiddleware(async (auth, req) => { 5 | return protectClerkMiddleware(auth, req) 6 | }) 7 | 8 | export const config = { 9 | matcher: [ 10 | // Skip Next.js internals and all static files, unless found in search params 11 | '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', 12 | // Always run for API routes 13 | '/(api|trpc)(.*)', 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /packages/protect/src/client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Client-safe exports for @cipherstash/protect 3 | * 4 | * This entry point exports types and utilities that can be used in client-side code 5 | * without requiring the @cipherstash/protect-ffi native module. 6 | * 7 | * Use this import path: `@cipherstash/protect/client` 8 | */ 9 | 10 | // Schema types and utilities - client-safe 11 | export { csTable, csColumn, csValue } from '@cipherstash/schema' 12 | export type { 13 | ProtectColumn, 14 | ProtectTable, 15 | ProtectTableColumn, 16 | ProtectValue, 17 | } from '@cipherstash/schema' 18 | export type { ProtectClient } from './ffi' 19 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /local/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: &postgres 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | environment: 7 | PGPORT: 5432 8 | POSTGRES_DB: "cipherstash" 9 | POSTGRES_USER: "cipherstash" 10 | PGUSER: "cipherstash" 11 | POSTGRES_PASSWORD: password 12 | ports: 13 | - 5432:5432 14 | deploy: 15 | resources: 16 | limits: 17 | cpus: "${CPU_LIMIT:-2}" 18 | memory: 2048mb 19 | restart: always 20 | healthcheck: 21 | test: [ "CMD-SHELL", "pg_isready" ] 22 | interval: 1s 23 | timeout: 5s 24 | retries: 10 25 | -------------------------------------------------------------------------------- /examples/drizzle/src/protect/config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { 3 | createProtectOperators, 4 | extractProtectSchema, 5 | } from '@cipherstash/drizzle/pg' 6 | import { protect } from '@cipherstash/protect' 7 | import { transactions } from '../db/schema' 8 | 9 | // Extract Protect.js schema from Drizzle table 10 | export const transactionsSchema = extractProtectSchema(transactions) 11 | 12 | // Initialize Protect.js client 13 | export const protectClient = await protect({ 14 | schemas: [transactionsSchema], 15 | }) 16 | 17 | // Create Protect operators for encrypted field queries 18 | export const protectOps = createProtectOperators(protectClient) 19 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "files": { 4 | "ignore": ["**/package.json", ".turbo/", "dist", "**/nix/store/**"] 5 | }, 6 | "formatter": { 7 | "indentStyle": "space", 8 | "indentWidth": 2, 9 | "formatWithErrors": true 10 | }, 11 | "javascript": { 12 | "formatter": { 13 | "jsxQuoteStyle": "double", 14 | "quoteStyle": "single", 15 | "semicolons": "asNeeded" 16 | }, 17 | "parser": { 18 | "unsafeParameterDecoratorsEnabled": true 19 | } 20 | }, 21 | "linter": { 22 | "rules": { 23 | "suspicious": { 24 | "noThenProperty": "off" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/nest/src/protect/schema.ts: -------------------------------------------------------------------------------- 1 | import { csColumn, csTable } from '@cipherstash/protect' 2 | 3 | export const users = csTable('users', { 4 | email_encrypted: csColumn('email_encrypted') 5 | .equality() 6 | .orderAndRange() 7 | .freeTextSearch(), 8 | phone_encrypted: csColumn('phone_encrypted').equality().orderAndRange(), 9 | ssn_encrypted: csColumn('ssn_encrypted').equality(), 10 | }) 11 | 12 | export const orders = csTable('orders', { 13 | address_encrypted: csColumn('address_encrypted').freeTextSearch(), 14 | creditCard_encrypted: csColumn('creditCard_encrypted').equality(), 15 | }) 16 | 17 | // Export all schemas for easy import 18 | export const schemas = [users, orders] 19 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from '@/components/ui/toaster' 2 | import { ClerkProvider } from '@clerk/nextjs' 3 | import type { Metadata } from 'next' 4 | import './globals.css' 5 | 6 | export const metadata: Metadata = { 7 | title: 'Protect.js + Next.js + Clerk', 8 | description: 'An example of using Protect.js with Next.js and Clerk', 9 | } 10 | 11 | export default function Layout({ children }: { children: React.ReactNode }) { 12 | return ( 13 | 14 | 15 | 16 |
{children}
17 | 18 | 19 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/typeorm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "nodenext", 6 | "moduleResolution": "nodenext", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "strictPropertyInitialization": false, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | "declaration": true, 14 | "sourceMap": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | "isolatedModules": true, 18 | "noEmit": false, 19 | "types": ["jest", "node"] 20 | }, 21 | "exclude": ["node_modules", "dist"] 22 | } 23 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/core/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { jsonb, pgTable, serial, varchar } from 'drizzle-orm/pg-core' 2 | 3 | // Data that is encrypted using protectjs is stored as jsonb in postgres 4 | // --- 5 | // This example does not include any searchable encrypted fields 6 | // If you want to search on encrypted fields, you will need to install EQL. 7 | // The EQL library ships with custom types that are used to define encrypted fields. 8 | // See https://github.com/cipherstash/encrypted-query-language 9 | // --- 10 | 11 | export const users = pgTable('users', { 12 | id: serial('id').primaryKey(), 13 | name: varchar('name').notNull(), 14 | email: jsonb('email').notNull(), 15 | role: varchar('role').notNull(), 16 | }) 17 | -------------------------------------------------------------------------------- /examples/typeorm/src/decorators/encrypted-column.ts: -------------------------------------------------------------------------------- 1 | import { Column } from 'typeorm' 2 | import { 3 | type EncryptedColumnOptions, 4 | createEncryptedColumnOptions, 5 | } from '../utils/encrypted-column' 6 | 7 | /** 8 | * Decorator for encrypted columns that automatically handles PostgreSQL composite literal transformation 9 | * 10 | * @example 11 | * ```typescript 12 | * @Entity() 13 | * export class User { 14 | * @EncryptedColumn() 15 | * email: EncryptedData | null 16 | * 17 | * @EncryptedColumn({ nullable: false }) 18 | * ssn: EncryptedData 19 | * } 20 | * ``` 21 | */ 22 | export function EncryptedColumn(options: EncryptedColumnOptions = {}) { 23 | return Column(createEncryptedColumnOptions(options)) 24 | } 25 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/typeorm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/typeorm-example", 3 | "version": "0.1.7", 4 | "private": true, 5 | "description": "Protect.js with TypeORM example", 6 | "type": "commonjs", 7 | "devDependencies": { 8 | "@types/node": "^22.13.10", 9 | "ts-node": "^10.9.2", 10 | "tsconfig-paths": "^4.2.0", 11 | "typescript": "^5.8.2" 12 | }, 13 | "dependencies": { 14 | "@cipherstash/protect": "workspace:*", 15 | "dotenv": "^16.4.7", 16 | "pg": "^8.14.1", 17 | "reflect-metadata": "^0.2.2", 18 | "typeorm": "0.3.26" 19 | }, 20 | "scripts": { 21 | "start": "ts-node -r tsconfig-paths/register src/index.ts", 22 | "typeorm": "typeorm-ts-node-commonjs" 23 | } 24 | } -------------------------------------------------------------------------------- /local/postgres-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Start PostgreSQL in the background 5 | echo "Starting PostgreSQL..." 6 | docker-entrypoint.sh postgres & 7 | 8 | # Wait for PostgreSQL to be ready 9 | echo "Waiting for PostgreSQL to be ready..." 10 | until pg_isready -U cipherstash -d cipherstash; do 11 | echo "Waiting for PostgreSQL to be ready..." 12 | sleep 2 13 | done 14 | 15 | echo "PostgreSQL is ready. Running CipherStash SQL initialization..." 16 | 17 | # Run the SQL file 18 | psql -U cipherstash -d cipherstash -f /tmp/cipherstash-encrypt-2-1-8.sql 19 | psql -U cipherstash -d cipherstash -f /tmp/create-ci-table.sql 20 | 21 | echo "CipherStash SQL initialization completed." 22 | 23 | # Wait for the PostgreSQL process 24 | wait $! 25 | -------------------------------------------------------------------------------- /examples/nest/src/protect/index.ts: -------------------------------------------------------------------------------- 1 | // Main module exports 2 | export { ProtectModule } from './protect.module' 3 | export { ProtectService } from './protect.service' 4 | 5 | // Schema exports 6 | export * from './schema' 7 | 8 | // Decorator exports 9 | export { Encrypt, EncryptModel } from './decorators/encrypt.decorator' 10 | export { Decrypt, DecryptModel } from './decorators/decrypt.decorator' 11 | 12 | // Interceptor exports 13 | export { EncryptInterceptor } from './interceptors/encrypt.interceptor' 14 | export { DecryptInterceptor } from './interceptors/decrypt.interceptor' 15 | 16 | // Type exports 17 | export type { ProtectConfig } from './interfaces/protect-config.interface' 18 | export { PROTECT_CONFIG, PROTECT_CLIENT } from './protect.constants' 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/drizzle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/how-to/sst-external-packages.md: -------------------------------------------------------------------------------- 1 | # SST and esbuild 2 | 3 | Using `@cipherstash/protect` in a serverless function deployed with [SST](https://sst.dev/)? 4 | 5 | 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): 6 | 7 | ```ts 8 | ... 9 | nodejs: { 10 | esbuild: { 11 | external: ['@cipherstash/protect'], 12 | }, 13 | install: ['@cipherstash/protect'], 14 | }, 15 | ... 16 | ``` 17 | 18 | --- 19 | 20 | ### Didn't find what you wanted? 21 | 22 | [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%sst-external-packages.md) 23 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/app/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import type { FormData } from '@/components/form' 4 | import { db } from '@/db' 5 | import { users } from '@/db/schema' 6 | import { protectClient } from '@/protect' 7 | import { users as protectedUsers } from '@/protect/schema' 8 | 9 | export async function createUser(data: FormData) { 10 | console.log(data) 11 | 12 | const result = await protectClient.encryptModel(data, protectedUsers) 13 | 14 | if (result.failure) { 15 | console.error(result.failure.message) 16 | return 17 | } 18 | 19 | console.log(result.data) 20 | 21 | await db.insert(users).values({ 22 | name: result.data.name, 23 | email: result.data.email, 24 | }) 25 | 26 | return { 27 | success: true, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/nest/src/protect/utils/get-protect-service.util.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionContext } from '@nestjs/common' 2 | import type { ModuleRef } from '@nestjs/core' 3 | import { ProtectService } from '../protect.service' 4 | 5 | export function getProtectService( 6 | ctx: ExecutionContext, 7 | ): ProtectService | null { 8 | try { 9 | const app = ctx.switchToHttp().getRequest().app 10 | if (app?.get) { 11 | return app.get(ProtectService) 12 | } 13 | 14 | // Fallback: try to get from module ref if available 15 | const moduleRef = ctx.switchToHttp().getRequest().moduleRef as ModuleRef 16 | if (moduleRef) { 17 | return moduleRef.get(ProtectService, { strict: false }) 18 | } 19 | 20 | return null 21 | } catch (error) { 22 | return null 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/nest/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # nest 2 | 3 | ## 0.0.8 4 | 5 | ### Patch Changes 6 | 7 | - @cipherstash/protect@10.2.1 8 | 9 | ## 0.0.7 10 | 11 | ### Patch Changes 12 | 13 | - Updated dependencies [de029de] 14 | - @cipherstash/protect@10.2.0 15 | 16 | ## 0.0.6 17 | 18 | ### Patch Changes 19 | 20 | - Updated dependencies [ff4421f] 21 | - @cipherstash/protect@10.1.1 22 | 23 | ## 0.0.5 24 | 25 | ### Patch Changes 26 | 27 | - Updated dependencies [6b87c17] 28 | - @cipherstash/protect@10.1.0 29 | 30 | ## 0.0.4 31 | 32 | ### Patch Changes 33 | 34 | - @cipherstash/protect@10.0.2 35 | 36 | ## 0.0.3 37 | 38 | ### Patch Changes 39 | 40 | - @cipherstash/protect@10.0.1 41 | 42 | ## 0.0.2 43 | 44 | ### Patch Changes 45 | 46 | - Updated dependencies [788dbfc] 47 | - @cipherstash/protect@10.0.0 48 | -------------------------------------------------------------------------------- /examples/nest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "nodenext", 4 | "moduleResolution": "nodenext", 5 | "resolvePackageJsonExports": true, 6 | "esModuleInterop": true, 7 | "isolatedModules": true, 8 | "declaration": true, 9 | "removeComments": true, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "allowSyntheticDefaultImports": true, 13 | "target": "ES2023", 14 | "sourceMap": true, 15 | "outDir": "./dist", 16 | "baseUrl": "./", 17 | "incremental": true, 18 | "skipLibCheck": true, 19 | "strictNullChecks": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "noImplicitAny": false, 22 | "strictBindCallApply": false, 23 | "noFallthroughCasesInSwitch": false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/core/protect/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { 3 | type CtsToken, 4 | LockContext, 5 | type ProtectClientConfig, 6 | csColumn, 7 | csTable, 8 | protect, 9 | } from '@cipherstash/protect' 10 | 11 | export const users = csTable('users', { 12 | email: csColumn('email'), 13 | }) 14 | 15 | const config: ProtectClientConfig = { 16 | schemas: [users], 17 | } 18 | 19 | export const protectClient = await protect(config) 20 | 21 | export const getLockContext = (cts_token?: CtsToken) => { 22 | if (!cts_token) { 23 | throw new Error( 24 | '[protect] A CTS token is required in order to get a lock context.', 25 | ) 26 | } 27 | 28 | const lockContext = new LockContext({ 29 | ctsToken: cts_token, 30 | }) 31 | 32 | return lockContext 33 | } 34 | -------------------------------------------------------------------------------- /packages/drizzle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | "esModuleInterop": true, 11 | 12 | // Bundler mode 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "verbatimModuleSyntax": true, 16 | "noEmit": true, 17 | 18 | // Best practices 19 | "strict": true, 20 | "skipLibCheck": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | // Some stricter flags (disabled by default) 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "noPropertyAccessFromIndexSignature": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | "esModuleInterop": true, 11 | 12 | // Bundler mode 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "verbatimModuleSyntax": true, 16 | "noEmit": true, 17 | 18 | // Best practices 19 | "strict": true, 20 | "skipLibCheck": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | // Some stricter flags (disabled by default) 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "noPropertyAccessFromIndexSignature": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/protect/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ES2022", "DOM"], 5 | "target": "ES2022", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | "esModuleInterop": true, 11 | 12 | // Bundler mode 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "verbatimModuleSyntax": true, 16 | "noEmit": true, 17 | 18 | // Best practices 19 | "strict": true, 20 | "skipLibCheck": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | // Some stricter flags (disabled by default) 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "noPropertyAccessFromIndexSignature": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | "esModuleInterop": true, 11 | 12 | // Bundler mode 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "verbatimModuleSyntax": true, 16 | "noEmit": true, 17 | 18 | // Best practices 19 | "strict": true, 20 | "skipLibCheck": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | // Some stricter flags (disabled by default) 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "noPropertyAccessFromIndexSignature": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/drizzle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drizzle-eql", 3 | "module": "index.ts", 4 | "private": true, 5 | "type": "module", 6 | "devDependencies": { 7 | "@types/express": "^4.17.21", 8 | "@types/node": "^22.10.2", 9 | "@types/pg": "^8.11.10", 10 | "dotenv": "^16.4.7", 11 | "drizzle-kit": "^0.30.5", 12 | "tsx": "catalog:repo", 13 | "typescript": "catalog:repo" 14 | }, 15 | "scripts": { 16 | "dev": "tsx src/server.ts", 17 | "start": "tsx src/server.ts", 18 | "db:generate": "drizzle-kit generate", 19 | "db:migrate": "drizzle-kit migrate" 20 | }, 21 | "dependencies": { 22 | "@cipherstash/drizzle": "workspace:*", 23 | "@cipherstash/protect": "workspace:*", 24 | "drizzle-orm": "^0.44.7", 25 | "express": "^5.2.1", 26 | "pg": "^8.16.3", 27 | "postgres": "^3.4.7" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/protect-dynamodb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | "esModuleInterop": true, 11 | 12 | // Bundler mode 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "verbatimModuleSyntax": true, 16 | "noEmit": true, 17 | 18 | // Best practices 19 | "strict": true, 20 | "skipLibCheck": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | // Some stricter flags (disabled by default) 24 | "noUnusedLocals": false, 25 | "noUnusedParameters": false, 26 | "noPropertyAccessFromIndexSignature": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": [ 26 | "examples/web/next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts", 30 | "next.config.ts" 31 | ], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { Geist, Geist_Mono } from 'next/font/google' 3 | import './globals.css' 4 | 5 | const geistSans = Geist({ 6 | variable: '--font-geist-sans', 7 | subsets: ['latin'], 8 | }) 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: '--font-geist-mono', 12 | subsets: ['latin'], 13 | }) 14 | 15 | export const metadata: Metadata = { 16 | title: 'Create Next App', 17 | description: 'Generated by create next app', 18 | } 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as LabelPrimitive from '@radix-ui/react-label' 4 | import { type VariantProps, cva } from 'class-variance-authority' 5 | import * as React from 'react' 6 | 7 | import { cn } from '@/lib/utils' 8 | 9 | const labelVariants = cva( 10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from '@/components/ui/toast' 11 | import { useToast } from '@/hooks/use-toast' 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(({ id, title, description, action, ...props }) => ( 19 | 20 |
21 | {title && {title}} 22 | {description && {description}} 23 |
24 | {action} 25 | 26 |
27 | ))} 28 | 29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /examples/typeorm/src/protect.ts: -------------------------------------------------------------------------------- 1 | import { csColumn, csTable, protect } from '@cipherstash/protect' 2 | 3 | /** 4 | * Define the protected schema for the User entity 5 | * This maps to the encrypted fields in your TypeORM entity 6 | */ 7 | export const protectedUser = csTable('user', { 8 | email: csColumn('email').equality().orderAndRange(), 9 | ssn: csColumn('ssn').equality(), 10 | phone: csColumn('phone').equality(), 11 | }) 12 | 13 | /** 14 | * Initialize the Protect client with the defined schema 15 | * This will be used throughout the application for encryption/decryption operations 16 | */ 17 | let protectClient: Awaited> 18 | 19 | export async function initializeProtectClient() { 20 | if (!protectClient) { 21 | protectClient = await protect({ 22 | schemas: [protectedUser], 23 | }) 24 | } 25 | return protectClient 26 | } 27 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | }, 19 | ) 20 | Input.displayName = 'Input' 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /examples/nest/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | /build 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | pnpm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # Tests 19 | /coverage 20 | /.nyc_output 21 | 22 | # IDEs and editors 23 | /.idea 24 | .project 25 | .classpath 26 | .c9/ 27 | *.launch 28 | .settings/ 29 | *.sublime-workspace 30 | 31 | # IDE - VSCode 32 | .vscode/* 33 | !.vscode/settings.json 34 | !.vscode/tasks.json 35 | !.vscode/launch.json 36 | !.vscode/extensions.json 37 | 38 | # dotenv environment variable files 39 | .env 40 | .env.development.local 41 | .env.test.local 42 | .env.production.local 43 | .env.local 44 | 45 | # temp directory 46 | .temp 47 | .tmp 48 | 49 | # Runtime data 50 | pids 51 | *.pid 52 | *.seed 53 | *.pid.lock 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | -------------------------------------------------------------------------------- /examples/nest/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Param, Post } from '@nestjs/common' 2 | import type { AppService } from './app.service' 3 | import type { CreateUserDto, User } from './app.service' 4 | 5 | @Controller() 6 | export class AppController { 7 | constructor(private readonly appService: AppService) {} 8 | 9 | @Get() 10 | async getHello() { 11 | return await this.appService.getHello() 12 | } 13 | 14 | @Post('users') 15 | async createUser(@Body() userData: CreateUserDto): Promise { 16 | const u = await this.appService.createUser(userData) 17 | return u 18 | } 19 | 20 | @Get('users/:id') 21 | async getUser(@Param('id') id: string, @Body() encryptedUser: User) { 22 | return await this.appService.getUser(id, encryptedUser) 23 | } 24 | 25 | @Get('users') 26 | async getUsers() { 27 | // This would typically fetch from a database 28 | // For demo purposes, return empty array 29 | return [] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-drizzle-mysql", 3 | "version": "0.2.14", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "db:generate": "drizzle-kit generate", 10 | "db:migrate": "drizzle-kit migrate" 11 | }, 12 | "dependencies": { 13 | "@cipherstash/protect": "workspace:*", 14 | "@hookform/resolvers": "^5.0.1", 15 | "drizzle-orm": "^0.44.0", 16 | "mysql2": "^3.14.1", 17 | "next": "catalog:security", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "react-hook-form": "^7.56.4", 21 | "zod": "^3.24.2" 22 | }, 23 | "devDependencies": { 24 | "@tailwindcss/postcss": "^4", 25 | "@types/node": "^20", 26 | "@types/react": "^19", 27 | "@types/react-dom": "^19", 28 | "dotenv": "^16.4.7", 29 | "drizzle-kit": "^0.30.5", 30 | "tailwindcss": "^4", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .turbo 3 | dist 4 | mise.local.toml 5 | .env 6 | 7 | # dependencies 8 | /node_modules 9 | /.pnp 10 | .pnp.* 11 | .yarn/* 12 | !.yarn/patches 13 | !.yarn/plugins 14 | !.yarn/releases 15 | !.yarn/versions 16 | 17 | # testing 18 | /coverage 19 | 20 | # next.js 21 | /.next/ 22 | /out/ 23 | 24 | # production 25 | /build 26 | 27 | # misc 28 | .DS_Store 29 | *.pem 30 | 31 | # debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | .pnpm-debug.log* 36 | 37 | # env files (can opt-in for committing if needed) 38 | .env 39 | .env.local 40 | 41 | # vercel 42 | .vercel 43 | 44 | # typescript 45 | *.tsbuildinfo 46 | next-env.d.ts 47 | 48 | # turbo 49 | .turbo 50 | 51 | node_modules 52 | .next 53 | 54 | # ffi 55 | target 56 | index.node 57 | **/node_modules 58 | **/.DS_Store 59 | npm-debug.log* 60 | cargo.log 61 | cross.log 62 | mise.local.toml 63 | !.github/.env 64 | 65 | # cipherstash 66 | cipherstash.toml 67 | cipherstash.secret.toml 68 | sql/cipherstash-*.sql 69 | -------------------------------------------------------------------------------- /examples/drizzle/src/db/schema.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { encryptedType } from '@cipherstash/drizzle/pg' 3 | import { pgTable, serial, timestamp, varchar } from 'drizzle-orm/pg-core' 4 | 5 | export const transactions = pgTable('transactions', { 6 | id: serial('id').primaryKey(), 7 | // Encrypted sensitive fields 8 | accountNumber: encryptedType('account_number', { 9 | freeTextSearch: true, 10 | equality: true, 11 | }), 12 | amount: encryptedType('amount', { 13 | dataType: 'number', 14 | equality: true, 15 | orderAndRange: true, 16 | }), 17 | description: encryptedType('description', { 18 | freeTextSearch: true, 19 | }), 20 | // Non-sensitive fields 21 | transactionType: varchar('transaction_type', { length: 50 }).notNull(), 22 | status: varchar('status', { length: 20 }).notNull().default('pending'), 23 | createdAt: timestamp('created_at').defaultNow().notNull(), 24 | updatedAt: timestamp('updated_at').defaultNow().notNull(), 25 | }) 26 | -------------------------------------------------------------------------------- /packages/utils/logger/index.ts: -------------------------------------------------------------------------------- 1 | function getLevelValue(level: string): number { 2 | switch (level) { 3 | case 'debug': 4 | return 10 5 | case 'info': 6 | return 20 7 | case 'error': 8 | return 30 9 | default: 10 | return 30 // default to error level 11 | } 12 | } 13 | 14 | const envLogLevel = process.env.PROTECT_LOG_LEVEL || 'info' 15 | const currentLevel = getLevelValue(envLogLevel) 16 | 17 | function debug(...args: unknown[]): void { 18 | if (currentLevel <= getLevelValue('debug')) { 19 | console.debug('[protect] DEBUG', ...args) 20 | } 21 | } 22 | 23 | function info(...args: unknown[]): void { 24 | if (currentLevel <= getLevelValue('info')) { 25 | console.info('[protect] INFO', ...args) 26 | } 27 | } 28 | 29 | function error(...args: unknown[]): void { 30 | if (currentLevel <= getLevelValue('error')) { 31 | console.error('[protect] ERROR', ...args) 32 | } 33 | } 34 | 35 | export const logger = { 36 | debug, 37 | info, 38 | error, 39 | } 40 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release JS 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: blacksmith-4vcpu-ubuntu-2404 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - uses: pnpm/action-setup@v4 19 | name: Install pnpm 20 | with: 21 | run_install: false 22 | 23 | - name: Install Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: 'pnpm' 28 | 29 | - name: Install dependencies 30 | run: pnpm install 31 | 32 | - name: Publish to npm 33 | id: changesets 34 | uses: changesets/action@v1 35 | with: 36 | publish: pnpm run release 37 | commitMode: 'github-api' 38 | env: 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/typeorm/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cipherstash/typeorm-example 2 | 3 | ## 0.1.7 4 | 5 | ### Patch Changes 6 | 7 | - @cipherstash/protect@10.2.1 8 | 9 | ## 0.1.6 10 | 11 | ### Patch Changes 12 | 13 | - Updated dependencies [de029de] 14 | - @cipherstash/protect@10.2.0 15 | 16 | ## 0.1.5 17 | 18 | ### Patch Changes 19 | 20 | - Updated dependencies [ff4421f] 21 | - @cipherstash/protect@10.1.1 22 | 23 | ## 0.1.4 24 | 25 | ### Patch Changes 26 | 27 | - Updated dependencies [6b87c17] 28 | - @cipherstash/protect@10.1.0 29 | 30 | ## 0.1.3 31 | 32 | ### Patch Changes 33 | 34 | - @cipherstash/protect@10.0.2 35 | 36 | ## 0.1.2 37 | 38 | ### Patch Changes 39 | 40 | - @cipherstash/protect@10.0.1 41 | 42 | ## 0.1.1 43 | 44 | ### Patch Changes 45 | 46 | - Updated dependencies [788dbfc] 47 | - @cipherstash/protect@10.0.0 48 | 49 | ## 0.1.0 50 | 51 | ### Minor Changes 52 | 53 | - c7ed7ab: Support TypeORM example with ES2022. 54 | 55 | ### Patch Changes 56 | 57 | - Updated dependencies [c7ed7ab] 58 | - Updated dependencies [211e979] 59 | - @cipherstash/protect@9.6.0 60 | -------------------------------------------------------------------------------- /docs/how-to/nextjs-external-packages.md: -------------------------------------------------------------------------------- 1 | # Next.js 2 | 3 | Using `@cipherstash/protect` with Next.js? You need to opt-out from the Server Components bundling and use native Node.js `require` instead. 4 | 5 | ## Using version 15 or later 6 | 7 | `next.config.ts` [configuration](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages): 8 | 9 | ```js 10 | const nextConfig = { 11 | ... 12 | serverExternalPackages: ['@cipherstash/protect'], 13 | } 14 | ``` 15 | 16 | ## Using version 14 17 | 18 | `next.config.mjs` [configuration](https://nextjs.org/docs/14/app/api-reference/next-config-js/serverComponentsExternalPackages): 19 | 20 | ```js 21 | const nextConfig = { 22 | ... 23 | experimental: { 24 | serverComponentsExternalPackages: ['@cipherstash/protect'], 25 | }, 26 | } 27 | ``` 28 | 29 | --- 30 | 31 | ### Didn't find what you wanted? 32 | 33 | [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%nextjs-external-packages.md) 34 | -------------------------------------------------------------------------------- /examples/drizzle/src/server.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import express, { 3 | type Request, 4 | type Response, 5 | type NextFunction, 6 | } from 'express' 7 | import { transactionsRouter } from './routes/transactions' 8 | 9 | const app = express() 10 | const PORT = process.env.PORT || 3000 11 | 12 | // Middleware 13 | app.use(express.json()) 14 | 15 | // Health check endpoint 16 | app.get('/health', (_req: Request, res: Response) => { 17 | res.status(200).json({ status: 'ok', message: 'Server is running' }) 18 | }) 19 | 20 | // Routes 21 | app.use('/transactions', transactionsRouter) 22 | 23 | // Error handling middleware 24 | app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { 25 | console.error('Error:', err) 26 | res.status(500).json({ 27 | error: 'Internal server error', 28 | message: err.message, 29 | }) 30 | }) 31 | 32 | // 404 handler 33 | app.use((_req: Request, res: Response) => { 34 | res.status(404).json({ error: 'Not found' }) 35 | }) 36 | 37 | app.listen(PORT, () => { 38 | console.log(`Server is running on http://localhost:${PORT}`) 39 | }) 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CipherStash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/dynamo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | dynamodb-local: 4 | command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" 5 | image: "amazon/dynamodb-local:latest" 6 | container_name: dynamodb-local 7 | ports: 8 | - "8000:8000" 9 | volumes: 10 | - "./docker/dynamodb:/home/dynamodblocal/data" 11 | working_dir: /home/dynamodblocal 12 | 13 | dynamodb-admin: 14 | image: aaronshaf/dynamodb-admin 15 | ports: 16 | - 8001:8001 17 | environment: 18 | DYNAMO_ENDPOINT: http://dynamodb-local:8000 19 | 20 | # used by export-to-pg example 21 | postgres: 22 | image: postgres:latest 23 | environment: 24 | PGPORT: 5432 25 | POSTGRES_DB: "cipherstash" 26 | POSTGRES_USER: "cipherstash" 27 | PGUSER: "cipherstash" 28 | POSTGRES_PASSWORD: password 29 | ports: 30 | - 5433:5432 31 | deploy: 32 | resources: 33 | limits: 34 | cpus: "${CPU_LIMIT:-2}" 35 | memory: 2048mb 36 | restart: always 37 | healthcheck: 38 | test: [ "CMD-SHELL", "pg_isready" ] 39 | interval: 1s 40 | timeout: 5s 41 | retries: 10 42 | -------------------------------------------------------------------------------- /examples/dynamo/src/common/dynamo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateTableCommand, 3 | type CreateTableCommandInput, 4 | DynamoDBClient, 5 | } from '@aws-sdk/client-dynamodb' 6 | import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb' 7 | 8 | export const dynamoClient = new DynamoDBClient({ 9 | credentials: { 10 | accessKeyId: 'fakeAccessKeyId', 11 | secretAccessKey: 'fakeSecretAccessKey', 12 | }, 13 | endpoint: 'http://localhost:8000', 14 | }) 15 | 16 | export const docClient = DynamoDBDocumentClient.from(dynamoClient) 17 | 18 | // Creates a table with provisioned throughput set to 5 RCU and 5 WCU. 19 | // Ignores `ResourceInUseException`s if the table already exists. 20 | export async function createTable( 21 | input: Omit, 22 | ) { 23 | const command = new CreateTableCommand({ 24 | ProvisionedThroughput: { 25 | ReadCapacityUnits: 5, 26 | WriteCapacityUnits: 5, 27 | }, 28 | ...input, 29 | }) 30 | 31 | try { 32 | await docClient.send(command) 33 | } catch (err) { 34 | if (err?.name !== 'ResourceInUseException') { 35 | throw err 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/schema", 3 | "version": "2.0.2", 4 | "description": "CipherStash schema builder for TypeScript", 5 | "keywords": [ 6 | "encrypted", 7 | "protect", 8 | "schema", 9 | "builder" 10 | ], 11 | "bugs": { 12 | "url": "https://github.com/cipherstash/protectjs/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/cipherstash/protectjs.git" 17 | }, 18 | "license": "MIT", 19 | "author": "CipherStash ", 20 | "type": "module", 21 | "exports": { 22 | ".": { 23 | "types": "./dist/index.d.ts", 24 | "import": "./dist/index.js", 25 | "require": "./dist/index.cjs" 26 | } 27 | }, 28 | "files": [ 29 | "dist", 30 | "cipherstash-encrypt-2-1-8.sql", 31 | "README.md" 32 | ], 33 | "scripts": { 34 | "build": "tsup", 35 | "dev": "tsup --watch", 36 | "test": "vitest run", 37 | "release": "tsup" 38 | }, 39 | "devDependencies": { 40 | "tsup": "catalog:repo", 41 | "typescript": "catalog:repo", 42 | "vitest": "catalog:repo" 43 | }, 44 | "dependencies": { 45 | "zod": "^3.24.2" 46 | }, 47 | "publishConfig": { 48 | "access": "public" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Protect.js documentation 2 | 3 | The documentation for Protect.js is organized into the following sections: 4 | 5 | - [Getting started](./getting-started.md) 6 | 7 | ## Concepts 8 | 9 | - [Searchable encryption](./concepts/searchable-encryption.md) 10 | 11 | ## Reference 12 | 13 | - [Configuration and production deployment](./reference/configuration.md) 14 | - [Searchable encryption with PostgreSQL](./reference/searchable-encryption-postgres.md) 15 | - [Protect.js schemas](./reference/schema.md) 16 | - [Model operations with bulk crypto functions](./reference/model-operations.md) 17 | 18 | ### ORMs and frameworks 19 | 20 | - [Supabase SDK](./reference/supabase-sdk.md) 21 | 22 | ### Drizzle ORM Integration 23 | 24 | - [Protect Operators Pattern](reference/drizzle/drizzle.md) - Recommended approach with auto-encrypting operators 25 | - [Manual Encryption Pattern](reference/drizzle/drizzle-protect.md) - Explicit control over encryption workflow 26 | 27 | ## How-to guides 28 | 29 | - [Lock contexts with Clerk and Next.js](./how-to/lock-contexts-with-clerk.md) 30 | - [Next.js build notes](./how-to/nextjs-external-packages.md) 31 | - [SST and serverless function notes](./how-to/sst-external-packages.md) 32 | -------------------------------------------------------------------------------- /packages/nextjs/src/clerk/index.ts: -------------------------------------------------------------------------------- 1 | import type { ClerkMiddlewareAuth } from '@clerk/nextjs/server' 2 | import type { NextRequest } from 'next/server' 3 | import { NextResponse } from 'next/server' 4 | import { logger } from '../../../utils/logger' 5 | import { setCtsToken } from '../cts' 6 | import { CS_COOKIE_NAME, resetCtsToken } from '../index' 7 | 8 | export const protectClerkMiddleware = async ( 9 | auth: ClerkMiddlewareAuth, 10 | req: NextRequest, 11 | ) => { 12 | const { userId, getToken } = await auth() 13 | const ctsSession = req.cookies.has(CS_COOKIE_NAME) 14 | 15 | if (userId && !ctsSession) { 16 | const oidcToken = await getToken() 17 | 18 | if (!oidcToken) { 19 | logger.debug( 20 | 'No Clerk token found in the request, so the CipherStash session was not set.', 21 | ) 22 | 23 | return NextResponse.next() 24 | } 25 | 26 | return await setCtsToken(oidcToken) 27 | } 28 | 29 | if (!userId && ctsSession) { 30 | logger.debug( 31 | 'No Clerk token found in the request, so the CipherStash session was reset.', 32 | ) 33 | 34 | return resetCtsToken() 35 | } 36 | 37 | logger.debug( 38 | 'No Clerk token found in the request, so the CipherStash session was not set.', 39 | ) 40 | 41 | return NextResponse.next() 42 | } 43 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/nextjs-clerk-example", 3 | "version": "0.2.15", 4 | "private": true, 5 | "scripts": { 6 | "check-types": "tsc --noEmit", 7 | "dev": "next dev --turbopack", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@cipherstash/nextjs": "workspace:*", 14 | "@cipherstash/protect": "workspace:*", 15 | "@clerk/nextjs": "catalog:security", 16 | "@radix-ui/react-label": "^2.1.1", 17 | "@radix-ui/react-select": "^2.1.4", 18 | "@radix-ui/react-slot": "^1.1.1", 19 | "@radix-ui/react-toast": "^1.2.5", 20 | "@radix-ui/react-tooltip": "^1.1.7", 21 | "class-variance-authority": "^0.7.1", 22 | "clsx": "^2.1.1", 23 | "dotenv": "^16.4.7", 24 | "drizzle-orm": "^0.38.2", 25 | "jose": "^5.9.6", 26 | "lucide-react": "^0.469.0", 27 | "next": "catalog:security", 28 | "postgres": "^3.4.5", 29 | "react": "^19", 30 | "react-dom": "^19", 31 | "tailwind-merge": "^2.5.5", 32 | "tailwindcss-animate": "^1.0.7" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^20", 36 | "@types/react": "^19", 37 | "@types/react-dom": "^19", 38 | "drizzle-kit": "^0.30.5", 39 | "postcss": "^8", 40 | "tailwindcss": "^3.4.1", 41 | "tsx": "catalog:repo", 42 | "typescript": "catalog:repo" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/protect-dynamodb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/protect-dynamodb", 3 | "version": "6.0.1", 4 | "description": "Protect.js DynamoDB Helpers", 5 | "keywords": [ 6 | "dynamodb", 7 | "cipherstash", 8 | "protect", 9 | "encrypt", 10 | "decrypt", 11 | "security" 12 | ], 13 | "bugs": { 14 | "url": "https://github.com/cipherstash/protectjs/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/cipherstash/protectjs.git" 19 | }, 20 | "license": "MIT", 21 | "author": "CipherStash ", 22 | "type": "module", 23 | "exports": { 24 | ".": { 25 | "types": "./dist/index.d.ts", 26 | "import": "./dist/index.js", 27 | "require": "./dist/index.cjs" 28 | } 29 | }, 30 | "scripts": { 31 | "build": "tsup", 32 | "dev": "tsup --watch", 33 | "test": "vitest run", 34 | "release": "tsup" 35 | }, 36 | "devDependencies": { 37 | "@cipherstash/protect": "workspace:*", 38 | "dotenv": "^16.4.7", 39 | "tsup": "catalog:repo", 40 | "tsx": "catalog:repo", 41 | "typescript": "catalog:repo", 42 | "vitest": "catalog:repo" 43 | }, 44 | "peerDependencies": { 45 | "@cipherstash/protect": "workspace:*" 46 | }, 47 | "publishConfig": { 48 | "access": "public" 49 | }, 50 | "dependencies": { 51 | "@byteslice/result": "^0.2.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/drizzle/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "a263534d-e155-4647-9ed2-eb113525c55c", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "serial", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "email": { 18 | "name": "email", 19 | "type": "varchar", 20 | "primaryKey": false, 21 | "notNull": false 22 | }, 23 | "email_encrypted": { 24 | "name": "email_encrypted", 25 | "type": "jsonb", 26 | "primaryKey": false, 27 | "notNull": true 28 | } 29 | }, 30 | "indexes": {}, 31 | "foreignKeys": {}, 32 | "compositePrimaryKeys": {}, 33 | "uniqueConstraints": { 34 | "users_email_unique": { 35 | "name": "users_email_unique", 36 | "nullsNotDistinct": false, 37 | "columns": ["email"] 38 | } 39 | } 40 | } 41 | }, 42 | "enums": {}, 43 | "schemas": {}, 44 | "sequences": {}, 45 | "_meta": { 46 | "columns": {}, 47 | "schemas": {}, 48 | "tables": {} 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/protect/__tests__/basic-protect.test.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { csColumn, csTable } from '@cipherstash/schema' 3 | import { beforeAll, describe, expect, it } from 'vitest' 4 | import { protect } from '../src' 5 | 6 | const users = csTable('users', { 7 | email: csColumn('email').freeTextSearch().equality().orderAndRange(), 8 | address: csColumn('address').freeTextSearch(), 9 | json: csColumn('json').dataType('json'), 10 | }) 11 | 12 | let protectClient: Awaited> 13 | 14 | beforeAll(async () => { 15 | protectClient = await protect({ 16 | schemas: [users], 17 | }) 18 | }) 19 | 20 | describe('encryption and decryption', () => { 21 | it('should encrypt and decrypt a payload', async () => { 22 | const email = 'hello@example.com' 23 | 24 | const ciphertext = await protectClient.encrypt(email, { 25 | column: users.email, 26 | table: users, 27 | }) 28 | 29 | if (ciphertext.failure) { 30 | throw new Error(`[protect]: ${ciphertext.failure.message}`) 31 | } 32 | 33 | // Verify encrypted field 34 | expect(ciphertext.data).toHaveProperty('c') 35 | 36 | const a = ciphertext.data 37 | 38 | const plaintext = await protectClient.decrypt(ciphertext.data) 39 | 40 | expect(plaintext).toEqual({ 41 | data: email, 42 | }) 43 | }, 30000) 44 | }) 45 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/dynamo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/dynamo-example", 3 | "private": true, 4 | "version": "0.2.15", 5 | "type": "module", 6 | "scripts": { 7 | "simple": "tsx src/simple.ts", 8 | "bulk-operations": "tsx src/bulk-operations.ts", 9 | "encrypted-partition-key": "tsx src/encrypted-partition-key.ts", 10 | "encrypted-sort-key": "tsx src/encrypted-sort-key.ts", 11 | "encrypted-key-in-gsi": "tsx src/encrypted-key-in-gsi.ts", 12 | "export-to-pg": "tsx src/export-to-pg.ts", 13 | "eql:download": "curl -sLo sql/cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/download/eql-2.0.2/cipherstash-encrypt.sql", 14 | "eql:install": "cat sql/cipherstash-encrypt.sql | docker exec -i dynamo-postgres-1 psql postgresql://cipherstash:password@postgres:5432/cipherstash -f-" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "description": "", 20 | "dependencies": { 21 | "@aws-sdk/client-dynamodb": "^3.817.0", 22 | "@aws-sdk/lib-dynamodb": "^3.817.0", 23 | "@aws-sdk/util-dynamodb": "^3.817.0", 24 | "@cipherstash/protect": "workspace:*", 25 | "@cipherstash/protect-dynamodb": "workspace:*", 26 | "pg": "^8.13.1" 27 | }, 28 | "devDependencies": { 29 | "@types/pg": "^8.11.10", 30 | "tsx": "catalog:repo", 31 | "typescript": "catalog:repo" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as TooltipPrimitive from '@radix-ui/react-tooltip' 4 | import * as React from 'react' 5 | 6 | import { cn } from '@/lib/utils' 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 19 | 28 | 29 | )) 30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 31 | 32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 33 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "mysql", 4 | "id": "03335511-a5f1-45e4-bcf7-227e326b28a5", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "int", 13 | "primaryKey": false, 14 | "notNull": true, 15 | "autoincrement": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "json", 20 | "primaryKey": false, 21 | "notNull": false, 22 | "autoincrement": false 23 | }, 24 | "email": { 25 | "name": "email", 26 | "type": "json", 27 | "primaryKey": false, 28 | "notNull": false, 29 | "autoincrement": false 30 | } 31 | }, 32 | "indexes": {}, 33 | "foreignKeys": {}, 34 | "compositePrimaryKeys": { 35 | "users_id": { 36 | "name": "users_id", 37 | "columns": ["id"] 38 | } 39 | }, 40 | "uniqueConstraints": {}, 41 | "checkConstraint": {} 42 | } 43 | }, 44 | "views": {}, 45 | "_meta": { 46 | "schemas": {}, 47 | "tables": {}, 48 | "columns": {} 49 | }, 50 | "internal": { 51 | "tables": {}, 52 | "indexes": {} 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/nextjs", 3 | "version": "4.1.0", 4 | "description": "Nextjs package for use with @cipherstash/protect", 5 | "keywords": [ 6 | "encrypted", 7 | "typescript", 8 | "eql", 9 | "nextjs" 10 | ], 11 | "bugs": { 12 | "url": "https://github.com/cipherstash/protectjs/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/cipherstash/protectjs.git" 17 | }, 18 | "license": "MIT", 19 | "author": "CipherStash ", 20 | "type": "module", 21 | "exports": { 22 | ".": { 23 | "types": "./dist/index.d.ts", 24 | "import": "./dist/index.js", 25 | "require": "./dist/index.cjs" 26 | }, 27 | "./clerk": { 28 | "types": "./dist/clerk/index.d.ts", 29 | "import": "./dist/clerk/index.js", 30 | "require": "./dist/clerk/index.cjs" 31 | } 32 | }, 33 | "scripts": { 34 | "build": "tsup", 35 | "dev": "tsup --watch", 36 | "release": "tsup" 37 | }, 38 | "devDependencies": { 39 | "@clerk/nextjs": "catalog:security", 40 | "dotenv": "^16.4.7", 41 | "tsup": "catalog:repo", 42 | "typescript": "catalog:repo", 43 | "vitest": "catalog:repo" 44 | }, 45 | "peerDependencies": { 46 | "next": "^14 || ^15" 47 | }, 48 | "publishConfig": { 49 | "access": "public" 50 | }, 51 | "optionalDependencies": { 52 | "@rollup/rollup-linux-x64-gnu": "4.24.0" 53 | }, 54 | "dependencies": { 55 | "jose": "^5.9.6" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs-feedback.yml: -------------------------------------------------------------------------------- 1 | name: Docs feedback 2 | description: Feedback to help make our docs more effective. 3 | title: "[Docs]: " 4 | labels: ["docs", "triage"] 5 | assignees: 6 | - kateandrews 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to give us feedback! 12 | - type: input 13 | id: contact 14 | attributes: 15 | label: Contact details 16 | description: How can we get in touch with you if we need more info? 17 | placeholder: you@example.com 18 | validations: 19 | required: false 20 | - type: textarea 21 | id: what-problem 22 | attributes: 23 | label: What problem were you trying to solve? 24 | placeholder: Tell us what you were looking for in our docs. 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: additional-info 29 | attributes: 30 | label: Anything else you'd like us to know? 31 | placeholder: Let us know if you've got any additional feedback. 32 | validations: 33 | required: false 34 | - type: checkboxes 35 | id: terms 36 | attributes: 37 | label: Code of conduct 38 | description: By submitting this issue, you agree to follow our [Code of conduct](https://github.com/cipherstash/protectjs/blob/main/CODE_OF_CONDUCT.md). 39 | options: 40 | - label: I agree to follow this project's Code of conduct 41 | required: true 42 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { ClientForm } from '@/components/form' 2 | import { db } from '@/db' 3 | import { users } from '@/db/schema' 4 | import { protectClient } from '@/protect' 5 | import { users as protectedUsers } from '@/protect/schema' 6 | 7 | type User = { 8 | id: number 9 | name: string 10 | email: string 11 | } 12 | 13 | export default async function Home() { 14 | const u = await db.select().from(users).limit(10) 15 | 16 | const decryptedUsers = await protectClient.bulkDecryptModels(u) 17 | 18 | if (decryptedUsers.failure) { 19 | throw new Error(decryptedUsers.failure.message) 20 | } 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {decryptedUsers.data.map((user) => ( 35 | 36 | 37 | 38 | 39 | 40 | ))} 41 | 42 |
IDNameEmail
{user.id}{user.name as string}{user.email as string}
43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /packages/protect/src/ffi/operations/base-operation.ts: -------------------------------------------------------------------------------- 1 | import type { Result } from '@byteslice/result' 2 | import type { ProtectError } from '../..' 3 | 4 | export type AuditConfig = { 5 | metadata?: Record 6 | } 7 | 8 | export type AuditData = { 9 | metadata?: Record 10 | } 11 | 12 | export abstract class ProtectOperation { 13 | protected auditMetadata?: Record 14 | 15 | /** 16 | * Attach audit metadata to this operation. Can be chained. 17 | * @param config Configuration for ZeroKMS audit logging 18 | * @param config.metadata Arbitrary JSON object for appending metadata to the audit log 19 | */ 20 | audit(config: AuditConfig): this { 21 | this.auditMetadata = config.metadata 22 | return this 23 | } 24 | 25 | /** 26 | * Get the audit data for this operation. 27 | */ 28 | public getAuditData(): AuditData { 29 | return { 30 | metadata: this.auditMetadata, 31 | } 32 | } 33 | 34 | /** 35 | * Execute the operation and return a Result 36 | */ 37 | abstract execute(): Promise> 38 | 39 | /** 40 | * Make the operation thenable 41 | */ 42 | public then, TResult2 = never>( 43 | onfulfilled?: 44 | | ((value: Result) => TResult1 | PromiseLike) 45 | | null, 46 | onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, 47 | ): Promise { 48 | return this.execute().then(onfulfilled, onrejected) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/typeorm/src/data-source.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm' 2 | import { User } from './entity/User' 3 | 4 | const originalConnectionConnectFunction = DataSource.prototype.initialize 5 | 6 | // Patch DataSource to support custom column type for Protect.js 7 | DataSource.prototype.initialize = async function (...params) { 8 | // TypeORM's supportedDataTypes is typed as ColumnType[], but we need to add our custom type. 9 | // Use 'as any' to bypass the type error for custom types. 10 | // biome-ignore lint/suspicious/noExplicitAny: Required for custom types 11 | const driver: any = this.driver 12 | if ( 13 | driver && 14 | Array.isArray(driver.supportedDataTypes) && 15 | !driver.supportedDataTypes.includes('eql_v2_encrypted') 16 | ) { 17 | driver.supportedDataTypes.push('eql_v2_encrypted') 18 | } 19 | 20 | // Execute the original functionality on top of the added code above 21 | await originalConnectionConnectFunction.call(this, ...params) 22 | 23 | return this 24 | } 25 | 26 | export const AppDataSource = new DataSource({ 27 | type: 'postgres', 28 | host: process.env.DB_HOST || 'localhost', 29 | port: Number.parseInt(process.env.DB_PORT || '5432'), 30 | username: process.env.DB_USERNAME || 'cipherstash', 31 | password: process.env.DB_PASSWORD || 'password', 32 | database: process.env.DB_DATABASE || 'cipherstash', 33 | synchronize: process.env.NODE_ENV !== 'production', // Only auto-sync in development 34 | logging: process.env.NODE_ENV === 'development', 35 | entities: [User], 36 | migrations: [], 37 | subscribers: [], 38 | }) 39 | -------------------------------------------------------------------------------- /packages/schema/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cipherstash/schema 2 | 3 | ## 2.0.2 4 | 5 | ### Patch Changes 6 | 7 | - 532ac3a: Corrected types documentation in README to match Typedoc. 8 | `int` -> `number` 9 | `text` -> `string` 10 | 11 | ## 2.0.1 12 | 13 | ### Patch Changes 14 | 15 | - ff4421f: Expanded typedoc documentation 16 | 17 | ## 2.0.0 18 | 19 | ### Major Changes 20 | 21 | - 9005484: Include EQL 2.1.8 in package distribution 22 | 23 | ## 1.1.0 24 | 25 | ### Minor Changes 26 | 27 | - d8ed4d4: Exported all types for packages looking for deeper integrations with Protect.js. 28 | 29 | ## 1.0.0 30 | 31 | ### Major Changes 32 | 33 | - 788dbfc: Added JSON and INT data type support and update FFI to v0.17.1 with x86_64 musl environment platform support. 34 | 35 | - Update @cipherstash/protect-ffi from 0.16.0 to 0.17.1 with support for x86_64 musl platforms. 36 | - Add searchableJson() method to schema for JSON field indexing (the search operations still don't work but this interface exists) 37 | - Refactor type system: EncryptedPayload → Encrypted, add JsPlaintext 38 | - Add comprehensive test suites for JSON, integer, and basic encryption 39 | - Update encryption format to use 'k' property for searchable JSON 40 | - Remove deprecated search terms tests for JSON fields 41 | - Simplify schema data types to text, int, json only 42 | - Update model helpers to handle new encryption format 43 | - Fix type safety issues in bulk operations and model encryption 44 | 45 | ## 0.1.0 46 | 47 | ### Minor Changes 48 | 49 | - d0b02ea: Released initial package for CipherStash Encrypt schemas. 50 | -------------------------------------------------------------------------------- /packages/drizzle/__tests__/utils/markdown-parser.ts: -------------------------------------------------------------------------------- 1 | export interface CodeBlock { 2 | id: string 3 | code: string 4 | section: string 5 | lineNumber: number 6 | } 7 | 8 | /** 9 | * Extract executable code blocks from markdown. 10 | * Looks for ```ts:run fenced code blocks. 11 | */ 12 | export function extractExecutableBlocks(markdown: string): CodeBlock[] { 13 | const blocks: CodeBlock[] = [] 14 | const lines = markdown.split('\n') 15 | 16 | let currentSection = 'Introduction' 17 | let inCodeBlock = false 18 | let currentCode: string[] = [] 19 | let blockStartLine = 0 20 | let blockId = 0 21 | 22 | for (let i = 0; i < lines.length; i++) { 23 | const line = lines[i] 24 | 25 | // Track section headers (## or ### level) 26 | const headerMatch = line.match(/^#{1,3}\s+(.+)$/) 27 | if (headerMatch) { 28 | currentSection = headerMatch[1].trim() 29 | } 30 | 31 | // Start of executable code block 32 | if (line.match(/^```ts:run\s*$/)) { 33 | inCodeBlock = true 34 | currentCode = [] 35 | blockStartLine = i + 2 // 1-indexed, next line is code start 36 | continue 37 | } 38 | 39 | // End of code block 40 | if (inCodeBlock && line === '```') { 41 | blocks.push({ 42 | id: `block-${blockId++}`, 43 | code: currentCode.join('\n'), 44 | section: currentSection, 45 | lineNumber: blockStartLine, 46 | }) 47 | inCodeBlock = false 48 | continue 49 | } 50 | 51 | // Accumulate code 52 | if (inCodeBlock) { 53 | currentCode.push(line) 54 | } 55 | } 56 | 57 | return blocks 58 | } 59 | -------------------------------------------------------------------------------- /examples/typeorm/src/entity/User.ts: -------------------------------------------------------------------------------- 1 | import type { EncryptedData } from '@cipherstash/protect' 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | Entity, 6 | PrimaryGeneratedColumn, 7 | UpdateDateColumn, 8 | } from 'typeorm' 9 | import { EncryptedColumn } from '../decorators/encrypted-column' 10 | 11 | @Entity() 12 | export class User { 13 | @PrimaryGeneratedColumn() 14 | id: number 15 | 16 | @Column() 17 | firstName: string 18 | 19 | @Column() 20 | lastName: string 21 | 22 | @Column() 23 | age: number 24 | 25 | /** 26 | * Encrypted email field with automatic PostgreSQL composite literal transformation 27 | * No lifecycle hooks needed - the @EncryptedColumn decorator handles everything! 28 | */ 29 | @EncryptedColumn() 30 | email: EncryptedData | null 31 | 32 | /** 33 | * Example of a non-nullable encrypted field 34 | */ 35 | @EncryptedColumn({ nullable: false }) 36 | ssn: EncryptedData 37 | 38 | /** 39 | * Optional encrypted field for phone numbers 40 | */ 41 | @EncryptedColumn() 42 | phone: EncryptedData | null 43 | 44 | @CreateDateColumn() 45 | createdAt: Date 46 | 47 | @UpdateDateColumn() 48 | updatedAt: Date 49 | 50 | // Helper method to get a plain representation of the user (for display purposes) 51 | getDisplayInfo() { 52 | return { 53 | id: this.id, 54 | firstName: this.firstName, 55 | lastName: this.lastName, 56 | age: this.age, 57 | createdAt: this.createdAt, 58 | updatedAt: this.updatedAt, 59 | // Note: email, ssn, and phone are encrypted and need to be decrypted separately 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/drizzle/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cipherstash/drizzle 2 | 3 | ## 2.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 41c4169: Update drizzle imports to use /client export path from Protect.js. 8 | 9 | ## 2.0.0 10 | 11 | ### Patch Changes 12 | 13 | - Updated dependencies [de029de] 14 | - @cipherstash/protect@10.2.0 15 | 16 | ## 1.1.1 17 | 18 | ### Patch Changes 19 | 20 | - ff4421f: Expanded typedoc documentation 21 | - Updated dependencies [ff4421f] 22 | - @cipherstash/protect@10.1.1 23 | - @cipherstash/schema@2.0.1 24 | 25 | ## 1.1.0 26 | 27 | ### Patch Changes 28 | 29 | - Updated dependencies [6b87c17] 30 | - @cipherstash/protect@10.1.0 31 | 32 | ## 1.0.0 33 | 34 | ### Minor Changes 35 | 36 | - 2edfedd: Added support for encrypted or operation. 37 | - 7b8c719: Added `generate-eql-migration` CLI command to automate EQL migration generation. 38 | 39 | This command consolidates the manual process of running `drizzle-kit generate --custom` and populating the SQL file into a single command. It uses the bundled EQL SQL from `@cipherstash/schema` for offline-friendly, version-locked installations. 40 | 41 | Usage: 42 | 43 | ```bash 44 | npx generate-eql-migration 45 | npx generate-eql-migration --name setup-eql 46 | npx generate-eql-migration --out migrations 47 | ``` 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [9005484] 52 | - @cipherstash/schema@2.0.0 53 | - @cipherstash/protect@10.0.2 54 | 55 | ## 0.2.0 56 | 57 | ### Minor Changes 58 | 59 | - ebda487: Added explicit return type to extractProtectSchem. 60 | 61 | ## 0.1.0 62 | 63 | ### Minor Changes 64 | 65 | - d8ed4d4: Released initial Drizzle ORM interface. 66 | 67 | ### Patch Changes 68 | 69 | - Updated dependencies [d8ed4d4] 70 | - @cipherstash/schema@1.1.0 71 | - @cipherstash/protect@10.0.1 72 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/lib/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { db } from '@/core/db' 4 | import { users } from '@/core/db/schema' 5 | import { protectClient, users as protectUsers } from '@/core/protect' 6 | import { getLockContext } from '@/core/protect' 7 | import { getCtsToken } from '@cipherstash/nextjs' 8 | import { auth } from '@clerk/nextjs/server' 9 | import { revalidatePath } from 'next/cache' 10 | 11 | export async function addUser(formData: FormData) { 12 | const { userId } = await auth() 13 | 14 | if (!userId) { 15 | return { error: 'You must be signed in to add a user.' } 16 | } 17 | 18 | const name = formData.get('name') as string 19 | const email = formData.get('email') as string 20 | const role = formData.get('role') as string 21 | 22 | if (!name || !email || !role) { 23 | return { error: 'All fields are required' } 24 | } 25 | 26 | const ctsToken = await getCtsToken() 27 | 28 | if (!ctsToken.success) { 29 | return { error: 'There was an error getting your session token.' } 30 | } 31 | 32 | const lockContext = getLockContext(ctsToken.ctsToken) 33 | const encryptedResult = await protectClient 34 | .encrypt(email, { 35 | column: protectUsers.email, 36 | table: protectUsers, 37 | }) 38 | .withLockContext(lockContext) 39 | 40 | if (encryptedResult.failure) { 41 | return { 42 | error: 'Failed to add the user. There was an error encrypting the email.', 43 | } 44 | } 45 | 46 | const encryptedEmail = encryptedResult.data 47 | 48 | try { 49 | await db.insert(users).values({ name, email: encryptedEmail, role }) 50 | revalidatePath('/') 51 | return { success: true } 52 | } catch (error) { 53 | console.error('Failed to add user:', error) 54 | return { error: 'Failed to add user' } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/dynamo/src/simple.ts: -------------------------------------------------------------------------------- 1 | import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' 2 | import { protectDynamoDB } from '@cipherstash/protect-dynamodb' 3 | import { createTable, docClient, dynamoClient } from './common/dynamo' 4 | import { log } from './common/log' 5 | import { protectClient, users } from './common/protect' 6 | 7 | const tableName = 'UsersSimple' 8 | 9 | type User = { 10 | pk: string 11 | email: string 12 | } 13 | 14 | const main = async () => { 15 | await createTable({ 16 | TableName: tableName, 17 | AttributeDefinitions: [ 18 | { 19 | AttributeName: 'pk', 20 | AttributeType: 'S', 21 | }, 22 | ], 23 | KeySchema: [ 24 | { 25 | AttributeName: 'pk', 26 | KeyType: 'HASH', 27 | }, 28 | ], 29 | }) 30 | 31 | const protectDynamo = protectDynamoDB({ 32 | protectClient, 33 | }) 34 | 35 | const user = { 36 | // `pk` won't be encrypted because it's not included in the `users` protected table schema. 37 | pk: 'user#1', 38 | // `email` will be encrypted because it's included in the `users` protected table schema. 39 | email: 'abc@example.com', 40 | } 41 | 42 | const encryptResult = await protectDynamo.encryptModel(user, users) 43 | 44 | log('encrypted item', encryptResult) 45 | 46 | const putCommand = new PutCommand({ 47 | TableName: tableName, 48 | Item: encryptResult, 49 | }) 50 | 51 | await dynamoClient.send(putCommand) 52 | 53 | const getCommand = new GetCommand({ 54 | TableName: tableName, 55 | Key: { pk: 'user#1' }, 56 | }) 57 | 58 | const getResult = await docClient.send(getCommand) 59 | 60 | const decryptedItem = await protectDynamo.decryptModel( 61 | getResult.Item, 62 | users, 63 | ) 64 | 65 | log('decrypted item', decryptedItem) 66 | } 67 | 68 | main() 69 | -------------------------------------------------------------------------------- /packages/protect-dynamodb/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Encrypted, 3 | ProtectClient, 4 | ProtectTable, 5 | ProtectTableColumn, 6 | SearchTerm, 7 | } from '@cipherstash/protect' 8 | import type { BulkDecryptModelsOperation } from './operations/bulk-decrypt-models' 9 | import type { BulkEncryptModelsOperation } from './operations/bulk-encrypt-models' 10 | import type { DecryptModelOperation } from './operations/decrypt-model' 11 | import type { EncryptModelOperation } from './operations/encrypt-model' 12 | import type { SearchTermsOperation } from './operations/search-terms' 13 | 14 | export interface ProtectDynamoDBConfig { 15 | protectClient: ProtectClient 16 | options?: { 17 | logger?: { 18 | error: (message: string, error: Error) => void 19 | } 20 | errorHandler?: (error: ProtectDynamoDBError) => void 21 | } 22 | } 23 | 24 | export interface ProtectDynamoDBError extends Error { 25 | code: string 26 | details?: Record 27 | } 28 | 29 | export interface ProtectDynamoDBInstance { 30 | encryptModel>( 31 | item: T, 32 | protectTable: ProtectTable, 33 | ): EncryptModelOperation 34 | 35 | bulkEncryptModels>( 36 | items: T[], 37 | protectTable: ProtectTable, 38 | ): BulkEncryptModelsOperation 39 | 40 | decryptModel>( 41 | item: Record, 42 | protectTable: ProtectTable, 43 | ): DecryptModelOperation 44 | 45 | bulkDecryptModels>( 46 | items: Record[], 47 | protectTable: ProtectTable, 48 | ): BulkDecryptModelsOperation 49 | 50 | createSearchTerms(terms: SearchTerm[]): SearchTermsOperation 51 | } 52 | -------------------------------------------------------------------------------- /packages/protect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/protect", 3 | "version": "10.2.1", 4 | "description": "CipherStash Protect for JavaScript", 5 | "keywords": [ 6 | "encrypted", 7 | "query", 8 | "language", 9 | "typescript", 10 | "ts", 11 | "protect" 12 | ], 13 | "bugs": { 14 | "url": "https://github.com/cipherstash/protectjs/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/cipherstash/protectjs.git" 19 | }, 20 | "license": "MIT", 21 | "author": "CipherStash ", 22 | "type": "module", 23 | "main": "./dist/index.cjs", 24 | "types": "./dist/index.d.ts", 25 | "exports": { 26 | ".": { 27 | "types": "./dist/index.d.ts", 28 | "import": "./dist/index.js", 29 | "require": "./dist/index.cjs" 30 | }, 31 | "./client": { 32 | "types": "./dist/client.d.ts", 33 | "import": "./dist/client.js", 34 | "require": "./dist/client.cjs" 35 | }, 36 | "./identify": { 37 | "types": "./dist/identify/index.d.ts", 38 | "import": "./dist/identify/index.js", 39 | "require": "./dist/identify/index.cjs" 40 | } 41 | }, 42 | "scripts": { 43 | "build": "tsup", 44 | "dev": "tsup --watch", 45 | "test": "vitest run", 46 | "release": "tsup" 47 | }, 48 | "devDependencies": { 49 | "@supabase/supabase-js": "^2.47.10", 50 | "dotenv": "^16.4.7", 51 | "execa": "^9.5.2", 52 | "json-schema-to-typescript": "^15.0.2", 53 | "tsup": "catalog:repo", 54 | "tsx": "catalog:repo", 55 | "typescript": "catalog:repo", 56 | "vitest": "catalog:repo" 57 | }, 58 | "publishConfig": { 59 | "access": "public" 60 | }, 61 | "dependencies": { 62 | "@byteslice/result": "^0.2.0", 63 | "@cipherstash/protect-ffi": "0.18.1", 64 | "@cipherstash/schema": "workspace:*", 65 | "zod": "^3.24.2" 66 | }, 67 | "optionalDependencies": { 68 | "@rollup/rollup-linux-x64-gnu": "4.24.0" 69 | } 70 | } -------------------------------------------------------------------------------- /packages/protect-dynamodb/src/operations/encrypt-model.ts: -------------------------------------------------------------------------------- 1 | import { type Result, withResult } from '@byteslice/result' 2 | import type { 3 | ProtectClient, 4 | ProtectTable, 5 | ProtectTableColumn, 6 | } from '@cipherstash/protect' 7 | import { deepClone, handleError, toEncryptedDynamoItem } from '../helpers' 8 | import type { ProtectDynamoDBError } from '../types' 9 | import { 10 | DynamoDBOperation, 11 | type DynamoDBOperationOptions, 12 | } from './base-operation' 13 | 14 | export class EncryptModelOperation< 15 | T extends Record, 16 | > extends DynamoDBOperation { 17 | private protectClient: ProtectClient 18 | private item: T 19 | private protectTable: ProtectTable 20 | 21 | constructor( 22 | protectClient: ProtectClient, 23 | item: T, 24 | protectTable: ProtectTable, 25 | options?: DynamoDBOperationOptions, 26 | ) { 27 | super(options) 28 | this.protectClient = protectClient 29 | this.item = item 30 | this.protectTable = protectTable 31 | } 32 | 33 | public async execute(): Promise> { 34 | return await withResult( 35 | async () => { 36 | const encryptResult = await this.protectClient 37 | .encryptModel(deepClone(this.item), this.protectTable) 38 | .audit(this.getAuditData()) 39 | 40 | if (encryptResult.failure) { 41 | throw new Error(`encryption error: ${encryptResult.failure.message}`) 42 | } 43 | 44 | const data = deepClone(encryptResult.data) 45 | const encryptedAttrs = Object.keys(this.protectTable.build().columns) 46 | 47 | return toEncryptedDynamoItem(data, encryptedAttrs) as T 48 | }, 49 | (error) => 50 | handleError(error, 'encryptModel', { 51 | logger: this.logger, 52 | errorHandler: this.errorHandler, 53 | }), 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/drizzle/__tests__/utils/code-executor.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { type ExecutionContext, executeCodeBlock } from './code-executor' 3 | 4 | describe('executeCodeBlock', () => { 5 | // ExecutionContext is now a simple index signature: { [key: string]: unknown } 6 | // Tests can define any properties needed for their specific test cases 7 | const mockContext: ExecutionContext = { 8 | db: {}, 9 | transactions: {}, 10 | } 11 | 12 | it('executes simple code and returns result', async () => { 13 | const code = 'return 1 + 1' 14 | const result = await executeCodeBlock(code, mockContext) 15 | 16 | expect(result.success).toBe(true) 17 | expect(result.result).toBe(2) 18 | }) 19 | 20 | it('provides context variables to code', async () => { 21 | const contextWithValue: ExecutionContext = { ...mockContext, testValue: 42 } 22 | const code = 'return testValue' 23 | const result = await executeCodeBlock(code, contextWithValue) 24 | 25 | expect(result.success).toBe(true) 26 | expect(result.result).toBe(42) 27 | }) 28 | 29 | it('handles async code', async () => { 30 | const code = 'return await Promise.resolve("async result")' 31 | const result = await executeCodeBlock(code, mockContext) 32 | 33 | expect(result.success).toBe(true) 34 | expect(result.result).toBe('async result') 35 | }) 36 | 37 | it('captures errors', async () => { 38 | const code = 'throw new Error("test error")' 39 | const result = await executeCodeBlock(code, mockContext) 40 | 41 | expect(result.success).toBe(false) 42 | expect(result.error).toContain('test error') 43 | }) 44 | 45 | it('handles syntax errors', async () => { 46 | const code = 'return {' // Invalid syntax 47 | const result = await executeCodeBlock(code, mockContext) 48 | 49 | expect(result.success).toBe(false) 50 | expect(result.error).toBeDefined() 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /packages/protect-dynamodb/src/operations/base-operation.ts: -------------------------------------------------------------------------------- 1 | import type { Result } from '@byteslice/result' 2 | import type { ProtectDynamoDBError } from '../types' 3 | 4 | export type AuditConfig = { 5 | metadata?: Record 6 | } 7 | 8 | export type AuditData = { 9 | metadata?: Record 10 | } 11 | 12 | export type DynamoDBOperationOptions = { 13 | logger?: { 14 | error: (message: string, error: Error) => void 15 | } 16 | errorHandler?: (error: ProtectDynamoDBError) => void 17 | } 18 | 19 | export abstract class DynamoDBOperation { 20 | protected auditMetadata?: Record 21 | protected logger?: DynamoDBOperationOptions['logger'] 22 | protected errorHandler?: DynamoDBOperationOptions['errorHandler'] 23 | 24 | constructor(options?: DynamoDBOperationOptions) { 25 | this.logger = options?.logger 26 | this.errorHandler = options?.errorHandler 27 | } 28 | 29 | /** 30 | * Attach audit metadata to this operation. Can be chained. 31 | */ 32 | audit(config: AuditConfig): this { 33 | this.auditMetadata = config.metadata 34 | return this 35 | } 36 | 37 | /** 38 | * Get the audit metadata for this operation. 39 | */ 40 | protected getAuditData(): AuditData { 41 | return { 42 | metadata: this.auditMetadata, 43 | } 44 | } 45 | 46 | /** 47 | * Execute the operation and return a Result 48 | */ 49 | abstract execute(): Promise> 50 | 51 | /** 52 | * Make the operation thenable 53 | */ 54 | public then, TResult2 = never>( 55 | onfulfilled?: 56 | | (( 57 | value: Result, 58 | ) => TResult1 | PromiseLike) 59 | | null, 60 | onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, 61 | ): Promise { 62 | return this.execute().then(onfulfilled, onrejected) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/protect-dynamodb/src/operations/decrypt-model.ts: -------------------------------------------------------------------------------- 1 | import { type Result, withResult } from '@byteslice/result' 2 | import type { 3 | Decrypted, 4 | Encrypted, 5 | ProtectClient, 6 | ProtectTable, 7 | ProtectTableColumn, 8 | } from '@cipherstash/protect' 9 | import { handleError, toItemWithEqlPayloads } from '../helpers' 10 | import type { ProtectDynamoDBError } from '../types' 11 | import { 12 | DynamoDBOperation, 13 | type DynamoDBOperationOptions, 14 | } from './base-operation' 15 | 16 | export class DecryptModelOperation< 17 | T extends Record, 18 | > extends DynamoDBOperation> { 19 | private protectClient: ProtectClient 20 | private item: Record 21 | private protectTable: ProtectTable 22 | 23 | constructor( 24 | protectClient: ProtectClient, 25 | item: Record, 26 | protectTable: ProtectTable, 27 | options?: DynamoDBOperationOptions, 28 | ) { 29 | super(options) 30 | this.protectClient = protectClient 31 | this.item = item 32 | this.protectTable = protectTable 33 | } 34 | 35 | public async execute(): Promise, ProtectDynamoDBError>> { 36 | return await withResult( 37 | async () => { 38 | const withEqlPayloads = toItemWithEqlPayloads( 39 | this.item, 40 | this.protectTable, 41 | ) 42 | 43 | const decryptResult = await this.protectClient 44 | .decryptModel(withEqlPayloads as T) 45 | .audit(this.getAuditData()) 46 | 47 | if (decryptResult.failure) { 48 | throw new Error(`[protect]: ${decryptResult.failure.message}`) 49 | } 50 | 51 | return decryptResult.data 52 | }, 53 | (error) => 54 | handleError(error, 'decryptModel', { 55 | logger: this.logger, 56 | errorHandler: this.errorHandler, 57 | }), 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/drizzle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/drizzle", 3 | "version": "2.1.0", 4 | "description": "CipherStash Protect.js Drizzle ORM integration for TypeScript", 5 | "keywords": [ 6 | "encrypted", 7 | "drizzle", 8 | "orm", 9 | "type-safe", 10 | "security", 11 | "protectjs", 12 | "postgres" 13 | ], 14 | "bugs": { 15 | "url": "https://github.com/cipherstash/protectjs/issues" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/cipherstash/protectjs.git" 20 | }, 21 | "license": "MIT", 22 | "author": "CipherStash ", 23 | "type": "module", 24 | "bin": { 25 | "generate-eql-migration": "./dist/bin/generate-eql-migration.js" 26 | }, 27 | "exports": { 28 | "./pg": { 29 | "types": "./dist/pg/index.d.ts", 30 | "import": "./dist/pg/index.js", 31 | "require": "./dist/pg/index.cjs" 32 | } 33 | }, 34 | "files": [ 35 | "dist", 36 | "README.md" 37 | ], 38 | "scripts": { 39 | "build": "tsup", 40 | "dev": "tsup --watch", 41 | "postbuild": "chmod +x ./dist/bin/generate-eql-migration.js", 42 | "test": "vitest run", 43 | "release": "tsup" 44 | }, 45 | "peerDependencies": { 46 | "@cipherstash/protect": ">=10.2.1", 47 | "@cipherstash/schema": ">=2.0.2", 48 | "@types/pg": "*", 49 | "drizzle-kit": ">=0.20", 50 | "drizzle-orm": ">=0.33", 51 | "pg": ">=8", 52 | "postgres": ">=3" 53 | }, 54 | "peerDependenciesMeta": { 55 | "@types/pg": { 56 | "optional": true 57 | }, 58 | "drizzle-kit": { 59 | "optional": true 60 | }, 61 | "pg": { 62 | "optional": true 63 | }, 64 | "postgres": { 65 | "optional": true 66 | } 67 | }, 68 | "devDependencies": { 69 | "@cipherstash/protect": "workspace:*", 70 | "@cipherstash/schema": "workspace:*", 71 | "dotenv": "^16.4.7", 72 | "fast-check": "^4.3.0", 73 | "tsup": "catalog:repo", 74 | "typescript": "catalog:repo", 75 | "vitest": "catalog:repo" 76 | }, 77 | "publishConfig": { 78 | "access": "public" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 0 0% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 0 0% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 0 0% 3.9%; 17 | --primary: 0 0% 9%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | --muted: 0 0% 96.1%; 22 | --muted-foreground: 0 0% 45.1%; 23 | --accent: 0 0% 96.1%; 24 | --accent-foreground: 0 0% 9%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 0 0% 89.8%; 28 | --input: 0 0% 89.8%; 29 | --ring: 0 0% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | } 37 | .dark { 38 | --background: 0 0% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --card: 0 0% 3.9%; 41 | --card-foreground: 0 0% 98%; 42 | --popover: 0 0% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --primary: 0 0% 98%; 45 | --primary-foreground: 0 0% 9%; 46 | --secondary: 0 0% 14.9%; 47 | --secondary-foreground: 0 0% 98%; 48 | --muted: 0 0% 14.9%; 49 | --muted-foreground: 0 0% 63.9%; 50 | --accent: 0 0% 14.9%; 51 | --accent-foreground: 0 0% 98%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 98%; 54 | --border: 0 0% 14.9%; 55 | --input: 0 0% 14.9%; 56 | --ring: 0 0% 83.1%; 57 | --chart-1: 220 70% 50%; 58 | --chart-2: 160 60% 45%; 59 | --chart-3: 30 80% 55%; 60 | --chart-4: 280 65% 60%; 61 | --chart-5: 340 75% 55%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/protect-dynamodb/src/operations/bulk-decrypt-models.ts: -------------------------------------------------------------------------------- 1 | import { type Result, withResult } from '@byteslice/result' 2 | import type { 3 | Decrypted, 4 | Encrypted, 5 | ProtectClient, 6 | ProtectTable, 7 | ProtectTableColumn, 8 | } from '@cipherstash/protect' 9 | import { handleError, toItemWithEqlPayloads } from '../helpers' 10 | import type { ProtectDynamoDBError } from '../types' 11 | import { 12 | DynamoDBOperation, 13 | type DynamoDBOperationOptions, 14 | } from './base-operation' 15 | 16 | export class BulkDecryptModelsOperation< 17 | T extends Record, 18 | > extends DynamoDBOperation[]> { 19 | private protectClient: ProtectClient 20 | private items: Record[] 21 | private protectTable: ProtectTable 22 | 23 | constructor( 24 | protectClient: ProtectClient, 25 | items: Record[], 26 | protectTable: ProtectTable, 27 | options?: DynamoDBOperationOptions, 28 | ) { 29 | super(options) 30 | this.protectClient = protectClient 31 | this.items = items 32 | this.protectTable = protectTable 33 | } 34 | 35 | public async execute(): Promise< 36 | Result[], ProtectDynamoDBError> 37 | > { 38 | return await withResult( 39 | async () => { 40 | const itemsWithEqlPayloads = this.items.map((item) => 41 | toItemWithEqlPayloads(item, this.protectTable), 42 | ) 43 | 44 | const decryptResult = await this.protectClient 45 | .bulkDecryptModels(itemsWithEqlPayloads as T[]) 46 | .audit(this.getAuditData()) 47 | 48 | if (decryptResult.failure) { 49 | throw new Error(`[protect]: ${decryptResult.failure.message}`) 50 | } 51 | 52 | return decryptResult.data 53 | }, 54 | (error) => 55 | handleError(error, 'bulkDecryptModels', { 56 | logger: this.logger, 57 | errorHandler: this.errorHandler, 58 | }), 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /docs/how-to/npm-lockfile-v3.md: -------------------------------------------------------------------------------- 1 | # Troubleshoot Linux deployments with npm lockfile v3 2 | 3 | Some npm users see deployments fail on Linux (e.g., AWS Lambda) when their `package-lock.json` was created on macOS or Windows. 4 | 5 | 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. 6 | 7 | ## Who is affected 8 | 9 | - You use `npm ci` in CI/CD 10 | - Your `package-lock.json` is version 3 and was generated on macOS/Windows 11 | - You deploy/run on Linux (Lambda, containers, EC2, etc.) 12 | 13 | ## What you might see 14 | 15 | - 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 16 | 17 | ## Fixes (pick one) 18 | 19 | ### 1) Recommended: use pnpm 20 | 21 | - This repo includes `pnpm-lock.yaml`. pnpm installs the correct native pieces for each platform. 22 | - CI: 23 | 24 | ```bash 25 | pnpm install --frozen-lockfile 26 | ``` 27 | 28 | ### 2) Generate the lockfile on Linux in CI, then run `npm ci` 29 | 30 | - Ensures the Linux build records what Linux needs. 31 | 32 | ```bash 33 | rm -f package-lock.json 34 | npm install --package-lock-only --ignore-scripts --no-audit --no-fund --platform=linux --arch=x64 35 | npm ci 36 | ``` 37 | 38 | - Alternative with environment variables: 39 | 40 | ```bash 41 | npm_config_platform=linux npm_config_arch=x64 npm install --package-lock-only --ignore-scripts --no-audit --no-fund 42 | npm ci 43 | ``` 44 | 45 | ### 3) Keep using npm but pin lockfile v2 (npm 8) 46 | 47 | - Locally: 48 | 49 | ```bash 50 | npm install --package-lock-only --lockfile-version=2 51 | ``` 52 | 53 | - CI: 54 | 55 | ```bash 56 | npm i -g npm@8 57 | npm ci 58 | ``` 59 | 60 | ## Quick tip 61 | 62 | Before packaging for deployment on Linux, you can quickly verify the native engine loads by running your app’s startup locally inside a Linux container or CI job. 63 | 64 | 65 | -------------------------------------------------------------------------------- /packages/protect-dynamodb/src/operations/search-terms.ts: -------------------------------------------------------------------------------- 1 | import { type Result, withResult } from '@byteslice/result' 2 | import type { ProtectClient, SearchTerm } from '@cipherstash/protect' 3 | import { handleError } from '../helpers' 4 | import type { ProtectDynamoDBError } from '../types' 5 | import { 6 | DynamoDBOperation, 7 | type DynamoDBOperationOptions, 8 | } from './base-operation' 9 | 10 | export class SearchTermsOperation extends DynamoDBOperation { 11 | private protectClient: ProtectClient 12 | private terms: SearchTerm[] 13 | 14 | constructor( 15 | protectClient: ProtectClient, 16 | terms: SearchTerm[], 17 | options?: DynamoDBOperationOptions, 18 | ) { 19 | super(options) 20 | this.protectClient = protectClient 21 | this.terms = terms 22 | } 23 | 24 | public async execute(): Promise> { 25 | return await withResult( 26 | async () => { 27 | const searchTermsResult = await this.protectClient 28 | .createSearchTerms(this.terms) 29 | .audit(this.getAuditData()) 30 | 31 | if (searchTermsResult.failure) { 32 | throw new Error(`[protect]: ${searchTermsResult.failure.message}`) 33 | } 34 | 35 | return searchTermsResult.data.map((term) => { 36 | if (typeof term === 'string') { 37 | throw new Error( 38 | 'expected encrypted search term to be an EncryptedPayload', 39 | ) 40 | } 41 | 42 | if (term?.k !== 'ct') { 43 | throw new Error( 44 | 'Tried to create search term with an invalid encrypted payload', 45 | ) 46 | } 47 | 48 | if (!term?.hm) { 49 | throw new Error('expected encrypted search term to have an HMAC') 50 | } 51 | 52 | return term.hm 53 | }) 54 | }, 55 | (error) => 56 | handleError(error, 'createSearchTerms', { 57 | logger: this.logger, 58 | errorHandler: this.errorHandler, 59 | }), 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/basic/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import readline from 'node:readline' 3 | import { protectClient, users } from './protect' 4 | 5 | const rl = readline.createInterface({ 6 | input: process.stdin, 7 | output: process.stdout, 8 | }) 9 | 10 | const askQuestion = (): Promise => { 11 | return new Promise((resolve) => { 12 | rl.question('\n👋Hello\n\nWhat is your name? ', (answer) => { 13 | resolve(answer) 14 | }) 15 | }) 16 | } 17 | 18 | async function main() { 19 | const input = await askQuestion() 20 | 21 | const encryptResult = await protectClient.encrypt(input, { 22 | column: users.name, 23 | table: users, 24 | }) 25 | 26 | if (encryptResult.failure) { 27 | throw new Error(`[protect]: ${encryptResult.failure.message}`) 28 | } 29 | 30 | const ciphertext = encryptResult.data 31 | 32 | console.log('Encrypting your name...') 33 | console.log('The ciphertext is:', ciphertext) 34 | 35 | const decryptResult = await protectClient.decrypt(ciphertext) 36 | 37 | if (decryptResult.failure) { 38 | throw new Error(`[protect]: ${decryptResult.failure.message}`) 39 | } 40 | 41 | const plaintext = decryptResult.data 42 | 43 | console.log('Decrypting the ciphertext...') 44 | console.log('The plaintext is:', plaintext) 45 | 46 | // Demonstrate bulk encryption 47 | console.log('\n--- Bulk Encryption Demo ---') 48 | 49 | const bulkPlaintexts = [ 50 | { id: '1', plaintext: 'Alice' }, 51 | { id: '2', plaintext: 'Bob' }, 52 | { id: '3', plaintext: 'Charlie' }, 53 | { id: '4', plaintext: null }, 54 | ] 55 | 56 | console.log( 57 | 'Bulk encrypting names:', 58 | bulkPlaintexts.map((p) => p.plaintext), 59 | ) 60 | 61 | const bulkEncryptResult = await protectClient.bulkEncrypt(bulkPlaintexts, { 62 | column: users.name, 63 | table: users, 64 | }) 65 | 66 | if (bulkEncryptResult.failure) { 67 | throw new Error(`[protect]: ${bulkEncryptResult.failure.message}`) 68 | } 69 | 70 | console.log('Bulk encrypted data:', bulkEncryptResult.data) 71 | 72 | rl.close() 73 | } 74 | 75 | main() 76 | -------------------------------------------------------------------------------- /examples/dynamo/README.md: -------------------------------------------------------------------------------- 1 | # DynamoDB Examples 2 | 3 | Examples of using Protect.js with DynamoDB. 4 | 5 | ## Prereqs 6 | - [Node.js](https://nodejs.org/en) (tested with v22.11.0) 7 | - [pnpm](https://pnpm.io/) (tested with v9.15.3) 8 | - [Docker](https://www.docker.com/) 9 | - a CipherStash account and [credentials configured](../../README.md#configuration) 10 | 11 | ## Setup 12 | 13 | Install the workspace dependencies and build Protect.js: 14 | ``` 15 | # change to the workspace root directory 16 | cd ../.. 17 | 18 | pnpm install 19 | pnpm run build 20 | ``` 21 | 22 | Switch back to the DynamoDB examples 23 | ``` 24 | cd examples/dynamo 25 | ``` 26 | 27 | Start Docker services used by the DynamoDB examples: 28 | ``` 29 | docker compose up --detach 30 | ``` 31 | 32 | Download [EQL](https://github.com/cipherstash/encrypt-query-language) and install it into the PG DB (this is optional and only necessary for running the `export-to-pg` example): 33 | ``` 34 | pnpm run eql:download 35 | pnpm run eql:install 36 | ``` 37 | 38 | ## Examples 39 | 40 | All examples run as scripts from [`package.json`](./package.json). 41 | You can run an example with the command `pnpm run [example_name]`. 42 | 43 | Each example runs against local DynamoDB in Docker. 44 | 45 | - `simple` 46 | - `pnpm run simple` 47 | - Round trip encryption/decryption through DynamoDB (no search on encrypted attributes). 48 | - `encrypted-partition-key` 49 | - `pnpm run encrypted-partition-key` 50 | - Uses an encrypted attribute as a partition key. 51 | - `encrypted-sort-key` 52 | - `pnpm run encrypted-sort-key` 53 | - Similar to the `encrypted-partition-key` example, but uses an encrypted attribute as a sort key instead. 54 | - `encrypted-key-in-gsi` 55 | - `pnpm run encrypted-key-in-gsi` 56 | - Uses an encrypted attribute as the partition key in a global secondary index. 57 | The source ciphertext is projected into the index for decryption after querying the index. 58 | - `export-to-pg` 59 | - `pnpm run export-to-pg` 60 | - Encrypts an item, puts it in Dynamo, exports it to Postgres, and decrypts a result from Postgres. 61 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | export default { 4 | darkMode: ['class'], 5 | content: [ 6 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 7 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 8 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))', 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))', 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))', 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))', 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))', 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))', 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))', 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))', 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)', 58 | }, 59 | }, 60 | }, 61 | plugins: [require('tailwindcss-animate')], 62 | } satisfies Config 63 | -------------------------------------------------------------------------------- /packages/protect-dynamodb/src/operations/bulk-encrypt-models.ts: -------------------------------------------------------------------------------- 1 | import { type Result, withResult } from '@byteslice/result' 2 | import type { 3 | ProtectClient, 4 | ProtectTable, 5 | ProtectTableColumn, 6 | } from '@cipherstash/protect' 7 | import { deepClone, handleError, toEncryptedDynamoItem } from '../helpers' 8 | import type { ProtectDynamoDBError } from '../types' 9 | import { 10 | DynamoDBOperation, 11 | type DynamoDBOperationOptions, 12 | } from './base-operation' 13 | 14 | export class BulkEncryptModelsOperation< 15 | T extends Record, 16 | > extends DynamoDBOperation { 17 | private protectClient: ProtectClient 18 | private items: T[] 19 | private protectTable: ProtectTable 20 | 21 | constructor( 22 | protectClient: ProtectClient, 23 | items: T[], 24 | protectTable: ProtectTable, 25 | options?: DynamoDBOperationOptions, 26 | ) { 27 | super(options) 28 | this.protectClient = protectClient 29 | this.items = items 30 | this.protectTable = protectTable 31 | } 32 | 33 | public async execute(): Promise> { 34 | return await withResult( 35 | async () => { 36 | const encryptResult = await this.protectClient 37 | .bulkEncryptModels( 38 | this.items.map((item) => deepClone(item)), 39 | this.protectTable, 40 | ) 41 | .audit(this.getAuditData()) 42 | 43 | if (encryptResult.failure) { 44 | throw new Error(`encryption error: ${encryptResult.failure.message}`) 45 | } 46 | 47 | const data = encryptResult.data.map((item) => deepClone(item)) 48 | const encryptedAttrs = Object.keys(this.protectTable.build().columns) 49 | 50 | return data.map( 51 | (encrypted) => toEncryptedDynamoItem(encrypted, encryptedAttrs) as T, 52 | ) 53 | }, 54 | (error) => 55 | handleError(error, 'bulkEncryptModels', { 56 | logger: this.logger, 57 | errorHandler: this.errorHandler, 58 | }), 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/UserTable.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { InfoIcon } from 'lucide-react' 4 | import type { EncryptedUser } from '../app/page' 5 | import { 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableHead, 10 | TableHeader, 11 | TableRow, 12 | } from './ui/table' 13 | 14 | import { 15 | Tooltip, 16 | TooltipContent, 17 | TooltipProvider, 18 | TooltipTrigger, 19 | } from '@/components/ui/tooltip' 20 | 21 | export default function UserTable({ 22 | users, 23 | email = 'Your user', 24 | }: { users: EncryptedUser[]; email?: string }) { 25 | return ( 26 |
27 | 28 | 29 | 30 | Name 31 | Email 32 | Role 33 | 34 | 35 | 36 | {users.map((user) => ( 37 | 38 | {user.name} 39 | 40 | 41 | {user.email} 42 | 43 | {!user.authorized && ( 44 | 45 | 46 | 47 | 48 | 49 | 50 |

51 | {email} is not authorized to decrypt this user's 52 | email. 53 |

54 |
55 |
56 |
57 | )} 58 |
59 | {user.role} 60 |
61 | ))} 62 |
63 |
64 |
65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/protectjs", 3 | "description": "CipherStash Protect for JavaScript/TypeScript", 4 | "author": "CipherStash ", 5 | "keywords": [ 6 | "encrypted", 7 | "query", 8 | "language", 9 | "typescript", 10 | "ts", 11 | "eql" 12 | ], 13 | "bugs": { 14 | "url": "https://github.com/cipherstash/protectjs/issues" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/cipherstash/protectjs.git" 19 | }, 20 | "license": "MIT", 21 | "workspaces": [ 22 | "examples/*", 23 | "packages/*" 24 | ], 25 | "scripts": { 26 | "build": "turbo build --filter './packages/*'", 27 | "build:js": "turbo build --filter './packages/protect' --filter './packages/nextjs'", 28 | "changeset": "changeset", 29 | "changeset:version": "changeset version", 30 | "changeset:publish": "changeset publish", 31 | "dev": "turbo dev --filter './packages/*'", 32 | "clean": "rimraf --glob **/.next **/.turbo **/dist **/node_modules", 33 | "code:fix": "biome check --write", 34 | "release": "pnpm run build && changeset publish", 35 | "test": "turbo test --filter './packages/*'" 36 | }, 37 | "devDependencies": { 38 | "@biomejs/biome": "^1.9.4", 39 | "@changesets/cli": "^2.29.6", 40 | "@types/node": "^22.15.12", 41 | "rimraf": "^6.1.2", 42 | "turbo": "2.1.1" 43 | }, 44 | "packageManager": "pnpm@10.14.0", 45 | "engines": { 46 | "node": ">=22" 47 | }, 48 | "pnpm": { 49 | "overrides": { 50 | "@babel/runtime": "7.26.10", 51 | "body-parser": "2.2.1", 52 | "vite": "catalog:security", 53 | "pg": "^8.16.3", 54 | "postgres": "^3.4.7", 55 | "js-yaml": "3.14.2", 56 | "test-exclude": "^7.0.1", 57 | "glob": ">=11.1.0" 58 | }, 59 | "peerDependencyRules": { 60 | "ignoreMissing": [ 61 | "@types/pg", 62 | "pg", 63 | "postgres" 64 | ], 65 | "allowedVersions": { 66 | "drizzle-orm": "*" 67 | } 68 | }, 69 | "dedupe-peer-dependents": true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/protect/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import type { KeysetIdentifier as KeysetIdentifierFfi } from '@cipherstash/protect-ffi' 2 | import type { Encrypted, KeysetIdentifier } from '../types' 3 | 4 | export type EncryptedPgComposite = { 5 | data: Encrypted 6 | } 7 | 8 | /** 9 | * Helper function to transform an encrypted payload into a PostgreSQL composite type 10 | */ 11 | export function encryptedToPgComposite(obj: Encrypted): EncryptedPgComposite { 12 | return { 13 | data: obj, 14 | } 15 | } 16 | 17 | /** 18 | * Helper function to transform a model's encrypted fields into PostgreSQL composite types 19 | */ 20 | export function modelToEncryptedPgComposites>( 21 | model: T, 22 | ): T { 23 | const result: Record = {} 24 | 25 | for (const [key, value] of Object.entries(model)) { 26 | if (isEncryptedPayload(value)) { 27 | result[key] = encryptedToPgComposite(value) 28 | } else { 29 | result[key] = value 30 | } 31 | } 32 | 33 | return result as T 34 | } 35 | 36 | /** 37 | * Helper function to transform multiple models' encrypted fields into PostgreSQL composite types 38 | */ 39 | export function bulkModelsToEncryptedPgComposites< 40 | T extends Record, 41 | >(models: T[]): T[] { 42 | return models.map((model) => modelToEncryptedPgComposites(model)) 43 | } 44 | 45 | export function toFfiKeysetIdentifier( 46 | keyset: KeysetIdentifier | undefined, 47 | ): KeysetIdentifierFfi | undefined { 48 | if (!keyset) return undefined 49 | 50 | if ('name' in keyset) { 51 | return { Name: keyset.name } 52 | } 53 | 54 | return { Uuid: keyset.id } 55 | } 56 | 57 | /** 58 | * Helper function to check if a value is an encrypted payload 59 | */ 60 | export function isEncryptedPayload(value: unknown): value is Encrypted { 61 | if (value === null) return false 62 | 63 | // TODO: this can definitely be improved 64 | if (typeof value === 'object') { 65 | const obj = value as Encrypted 66 | return ( 67 | obj !== null && 'v' in obj && ('c' in obj || 'sv' in obj) && 'i' in obj 68 | ) 69 | } 70 | 71 | return false 72 | } 73 | -------------------------------------------------------------------------------- /packages/protect/src/ffi/operations/search-terms.ts: -------------------------------------------------------------------------------- 1 | import { type Result, withResult } from '@byteslice/result' 2 | import { encryptBulk } from '@cipherstash/protect-ffi' 3 | import { type ProtectError, ProtectErrorTypes } from '../..' 4 | import { logger } from '../../../../utils/logger' 5 | import type { Client, EncryptedSearchTerm, SearchTerm } from '../../types' 6 | import { noClientError } from '../index' 7 | import { ProtectOperation } from './base-operation' 8 | 9 | export class SearchTermsOperation extends ProtectOperation< 10 | EncryptedSearchTerm[] 11 | > { 12 | private client: Client 13 | private terms: SearchTerm[] 14 | 15 | constructor(client: Client, terms: SearchTerm[]) { 16 | super() 17 | this.client = client 18 | this.terms = terms 19 | } 20 | 21 | public async execute(): Promise> { 22 | logger.debug('Creating search terms', { 23 | terms: this.terms, 24 | }) 25 | 26 | return await withResult( 27 | async () => { 28 | if (!this.client) { 29 | throw noClientError() 30 | } 31 | 32 | const { metadata } = this.getAuditData() 33 | 34 | const encryptedSearchTerms = await encryptBulk(this.client, { 35 | plaintexts: this.terms.map((term) => ({ 36 | plaintext: term.value, 37 | column: term.column.getName(), 38 | table: term.table.tableName, 39 | })), 40 | unverifiedContext: metadata, 41 | }) 42 | 43 | return this.terms.map((term, index) => { 44 | if (term.returnType === 'composite-literal') { 45 | return `(${JSON.stringify(JSON.stringify(encryptedSearchTerms[index]))})` 46 | } 47 | 48 | if (term.returnType === 'escaped-composite-literal') { 49 | return `${JSON.stringify(`(${JSON.stringify(JSON.stringify(encryptedSearchTerms[index]))})`)}` 50 | } 51 | 52 | return encryptedSearchTerms[index] 53 | }) 54 | }, 55 | (error) => ({ 56 | type: ProtectErrorTypes.EncryptionError, 57 | message: error.message, 58 | }), 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot' 2 | import { type VariantProps, cva } from 'class-variance-authority' 3 | import * as React from 'react' 4 | 5 | import { cn } from '../../lib/utils' 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 14 | destructive: 15 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 16 | outline: 17 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 18 | secondary: 19 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 20 | ghost: 'hover:bg-accent hover:text-accent-foreground', 21 | link: 'text-primary underline-offset-4 hover:underline', 22 | }, 23 | size: { 24 | default: 'h-9 px-4 py-2', 25 | sm: 'h-8 rounded-md px-3 text-xs', 26 | lg: 'h-10 rounded-md px-8', 27 | icon: 'h-9 w-9', 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: 'default', 32 | size: 'default', 33 | }, 34 | }, 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : 'button' 46 | return ( 47 | 52 | ) 53 | }, 54 | ) 55 | Button.displayName = 'Button' 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Breadcrumb, 3 | BreadcrumbItem, 4 | BreadcrumbLink, 5 | BreadcrumbList, 6 | BreadcrumbSeparator, 7 | } from '@/components/ui/breadcrumb' 8 | import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs' 9 | import Image from 'next/image' 10 | import Link from 'next/link' 11 | 12 | import { Github, KeyIcon } from 'lucide-react' 13 | import { Button } from './ui/button' 14 | 15 | export default function Header() { 16 | return ( 17 |
18 |
19 |
20 | 21 | Logo 27 | 28 |
29 | / 30 |

protect.js

31 | / 32 | 33 | 34 | 35 | Users 36 | 37 | 38 | 39 | Add a user 40 | 41 | 42 | 43 |
44 |
45 |
46 | 47 | 48 | 51 | 52 | 53 | 54 | 55 | 56 |
57 |
58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/AddUserForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useToast } from '@/hooks/use-toast' 4 | import { useRouter } from 'next/navigation' 5 | import { useState } from 'react' 6 | import { addUser } from '../lib/actions' 7 | import { Button } from './ui/button' 8 | import { Input } from './ui/input' 9 | import { Label } from './ui/label' 10 | import { 11 | Select, 12 | SelectContent, 13 | SelectItem, 14 | SelectTrigger, 15 | SelectValue, 16 | } from './ui/select' 17 | 18 | export default function AddUserForm() { 19 | const [role, setRole] = useState('') 20 | const router = useRouter() 21 | const { toast } = useToast() 22 | 23 | const handleSubmit = async (formData: FormData) => { 24 | formData.append('role', role) 25 | const result = await addUser(formData) 26 | if (result.error) { 27 | toast({ 28 | title: 'Error', 29 | description: result.error, 30 | variant: 'destructive', 31 | }) 32 | } else { 33 | toast({ 34 | title: 'Success', 35 | description: 'User added successfully', 36 | }) 37 | router.push('/') 38 | } 39 | } 40 | 41 | return ( 42 |
43 |
44 | 45 | 46 |
47 |
48 | 49 | 50 |
51 |
52 | 53 | 63 |
64 | 67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /examples/nest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest", 3 | "version": "0.0.8", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@cipherstash/protect": "workspace:*", 24 | "@nestjs/common": "^11.0.1", 25 | "@nestjs/config": "^3.2.0", 26 | "@nestjs/core": "^11.0.1", 27 | "@nestjs/platform-express": "^11.0.1", 28 | "dotenv": "^16.4.7", 29 | "reflect-metadata": "^0.2.2", 30 | "rxjs": "^7.8.1" 31 | }, 32 | "devDependencies": { 33 | "@eslint/eslintrc": "^3.2.0", 34 | "@eslint/js": "^9.18.0", 35 | "@nestjs/cli": "^11.0.0", 36 | "@nestjs/schematics": "^11.0.0", 37 | "@nestjs/testing": "^11.0.1", 38 | "@types/express": "^5.0.0", 39 | "@types/jest": "^30.0.0", 40 | "@types/node": "^22.10.7", 41 | "@types/supertest": "^6.0.2", 42 | "globals": "^16.0.0", 43 | "jest": "^30.0.0", 44 | "prettier": "^3.4.2", 45 | "source-map-support": "^0.5.21", 46 | "supertest": "^7.0.0", 47 | "ts-jest": "^29.2.5", 48 | "ts-loader": "^9.5.2", 49 | "ts-node": "^10.9.2", 50 | "tsconfig-paths": "^4.2.0", 51 | "typescript": "^5.7.3", 52 | "typescript-eslint": "^8.20.0" 53 | }, 54 | "jest": { 55 | "moduleFileExtensions": [ 56 | "js", 57 | "json", 58 | "ts" 59 | ], 60 | "rootDir": "src", 61 | "testRegex": ".*\\.spec\\.ts$", 62 | "transform": { 63 | "^.+\\.(t|j)s$": "ts-jest" 64 | }, 65 | "collectCoverageFrom": [ 66 | "**/*.(t|j)s" 67 | ], 68 | "coverageDirectory": "../coverage", 69 | "testEnvironment": "node" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/dynamo/src/encrypted-partition-key.ts: -------------------------------------------------------------------------------- 1 | import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' 2 | import { protectDynamoDB } from '@cipherstash/protect-dynamodb' 3 | import { createTable, docClient } from './common/dynamo' 4 | import { log } from './common/log' 5 | import { protectClient, users } from './common/protect' 6 | 7 | const tableName = 'UsersEncryptedPartitionKey' 8 | 9 | type User = { 10 | email: string 11 | } 12 | 13 | const main = async () => { 14 | await createTable({ 15 | TableName: tableName, 16 | AttributeDefinitions: [ 17 | { 18 | AttributeName: 'email__hmac', 19 | AttributeType: 'S', 20 | }, 21 | ], 22 | KeySchema: [ 23 | { 24 | AttributeName: 'email__hmac', 25 | KeyType: 'HASH', 26 | }, 27 | ], 28 | }) 29 | 30 | const protectDynamo = protectDynamoDB({ 31 | protectClient, 32 | }) 33 | 34 | const user = { 35 | // `email` will be encrypted because it's included in the `users` protected table schema. 36 | email: 'abc@example.com', 37 | // `somePlaintextAttr` won't be encrypted because it's not in the protected table schema. 38 | somePlaintextAttr: 'abc', 39 | } 40 | 41 | const encryptResult = await protectDynamo.encryptModel(user, users) 42 | 43 | log('encrypted item', encryptResult) 44 | 45 | const putCommand = new PutCommand({ 46 | TableName: tableName, 47 | Item: encryptResult, 48 | }) 49 | 50 | await docClient.send(putCommand) 51 | 52 | const searchTermsResult = await protectDynamo.createSearchTerms([ 53 | { 54 | value: 'abc@example.com', 55 | column: users.email, 56 | table: users, 57 | }, 58 | ]) 59 | 60 | if (searchTermsResult.failure) { 61 | throw new Error( 62 | `Failed to create search terms: ${searchTermsResult.failure.message}`, 63 | ) 64 | } 65 | 66 | const [emailHmac] = searchTermsResult.data 67 | 68 | const getCommand = new GetCommand({ 69 | TableName: tableName, 70 | Key: { email__hmac: emailHmac }, 71 | }) 72 | 73 | const getResult = await docClient.send(getCommand) 74 | 75 | const decryptedItem = await protectDynamo.decryptModel( 76 | getResult.Item, 77 | users, 78 | ) 79 | 80 | log('decrypted item', decryptedItem) 81 | } 82 | 83 | main() 84 | -------------------------------------------------------------------------------- /examples/nest/README.md: -------------------------------------------------------------------------------- 1 | # Protect.js Example with NestJS 2 | 3 | > ⚠️ **Heads-up:** This example was generated with AI with some very specific prompting to make it as useful as possible for you :) 4 | > 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) 5 | 6 | ## What this shows 7 | - Field-level encryption on 2+ properties via `encryptModel`/`decryptModel` and bulk variants 8 | - Identity-aware encryption is supported (optional `LockContext` chaining) 9 | - Result contract preserved: operations return `{ data }` or `{ failure }` 10 | 11 | ## 90-second Quickstart 12 | ```bash 13 | pnpm install 14 | cp .env.example .env 15 | pnpm start:dev 16 | ``` 17 | 18 | Environment variables (in `.env`): 19 | ```bash 20 | CS_WORKSPACE_CRN= 21 | CS_CLIENT_ID= 22 | CS_CLIENT_KEY= 23 | CS_CLIENT_ACCESS_KEY= 24 | ``` 25 | 26 | ### How encryption works here 27 | - `src/protect/schema.ts` defines tables with `.equality()`, `.orderAndRange()`, `.freeTextSearch()` for searchable encryption on Postgres. 28 | - `ProtectModule` initializes a `ProtectClient` with those schemas and injects a `ProtectService`. 29 | - `AppService` uses `encryptModel`/`decryptModel` and bulk variants to demonstrate single and bulk flows. 30 | 31 | ### Minimal API demo 32 | - `GET /` — returns a demo payload with encrypted and decrypted models and a bulk example 33 | - `POST /users` — encrypts provided fields and returns the encrypted model 34 | - `GET /users/:id` — decrypts a provided encrypted model (demo flow) 35 | 36 | ### Scripts 37 | - `pnpm start:dev` — run in watch mode 38 | - `pnpm test` / `pnpm test:e2e` 39 | 40 | ### Troubleshooting 41 | - Ensure `.env` has all required `CS_*` variables; lock-context flows require user JWTs. 42 | - Node 22+ is required; Bun is not supported. 43 | - If you integrate bundlers, externalize `@cipherstash/protect-ffi` (native module). 44 | 45 | ### References 46 | - Protect.js: see repo root `README.md` 47 | - NestJS docs: `https://docs.nestjs.com/` 48 | - Next.js external packages: `docs/how-to/nextjs-external-packages.md` 49 | - SST external packages: `docs/how-to/sst-external-packages.md` 50 | - npm lockfile v3 on Linux: `docs/how-to/npm-lockfile-v3.md` -------------------------------------------------------------------------------- /packages/protect-dynamodb/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Encrypted, 3 | ProtectTable, 4 | ProtectTableColumn, 5 | SearchTerm, 6 | } from '@cipherstash/protect' 7 | import { BulkDecryptModelsOperation } from './operations/bulk-decrypt-models' 8 | import { BulkEncryptModelsOperation } from './operations/bulk-encrypt-models' 9 | import { DecryptModelOperation } from './operations/decrypt-model' 10 | import { EncryptModelOperation } from './operations/encrypt-model' 11 | import { SearchTermsOperation } from './operations/search-terms' 12 | import type { ProtectDynamoDBConfig, ProtectDynamoDBInstance } from './types' 13 | 14 | export function protectDynamoDB( 15 | config: ProtectDynamoDBConfig, 16 | ): ProtectDynamoDBInstance { 17 | const { protectClient, options } = config 18 | 19 | return { 20 | encryptModel>( 21 | item: T, 22 | protectTable: ProtectTable, 23 | ) { 24 | return new EncryptModelOperation( 25 | protectClient, 26 | item, 27 | protectTable, 28 | options, 29 | ) 30 | }, 31 | 32 | bulkEncryptModels>( 33 | items: T[], 34 | protectTable: ProtectTable, 35 | ) { 36 | return new BulkEncryptModelsOperation( 37 | protectClient, 38 | items, 39 | protectTable, 40 | options, 41 | ) 42 | }, 43 | 44 | decryptModel>( 45 | item: Record, 46 | protectTable: ProtectTable, 47 | ) { 48 | return new DecryptModelOperation( 49 | protectClient, 50 | item, 51 | protectTable, 52 | options, 53 | ) 54 | }, 55 | 56 | bulkDecryptModels>( 57 | items: Record[], 58 | protectTable: ProtectTable, 59 | ) { 60 | return new BulkDecryptModelsOperation( 61 | protectClient, 62 | items, 63 | protectTable, 64 | options, 65 | ) 66 | }, 67 | 68 | createSearchTerms(terms: SearchTerm[]) { 69 | return new SearchTermsOperation(protectClient, terms, options) 70 | }, 71 | } 72 | } 73 | 74 | export * from './types' 75 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic example of using @cipherstash/protect 2 | 3 | This basic example demonstrates how to use the `@cipherstash/protect` package to encrypt arbitrary input. 4 | 5 | ## Installing the basic example 6 | 7 | > [!IMPORTANT] 8 | > Make sure you have installed Node.js and [pnpm](https://pnpm.io/installation) before following these steps. 9 | 10 | Clone this repo: 11 | 12 | ```bash 13 | git clone https://github.com/cipherstash/protectjs 14 | ``` 15 | 16 | Install dependencies: 17 | 18 | ```bash 19 | # Build Project.js 20 | cd protectjs 21 | pnpm build 22 | 23 | # Install deps for basic example 24 | cd examples/basic 25 | pnpm install 26 | ``` 27 | 28 | Lastly, install the CipherStash CLI: 29 | 30 | - On macOS: 31 | 32 | ```bash 33 | brew install cipherstash/tap/stash 34 | ``` 35 | 36 | - On Linux, download the binary for your platform, and put it on your `PATH`: 37 | - [Linux ARM64](https://github.com/cipherstash/cli-releases/releases/latest/download/stash-aarch64-unknown-linux-gnu) 38 | - [Linux x86_64](https://github.com/cipherstash/cli-releases/releases/latest/download/stash-x86_64-unknown-linux-gnu) 39 | 40 | 41 | ## Configuring the basic example 42 | 43 | > [!IMPORTANT] 44 | > Make sure you have [installed the CipherStash CLI](#installation) before following these steps. 45 | 46 | Set up all the configuration and credentials required for Protect.js: 47 | 48 | ```bash 49 | stash setup 50 | ``` 51 | 52 | If you have not already signed up for a CipherStash account, this will prompt you to do so along the way. 53 | 54 | At the end of `stash setup`, you will have two files in your project: 55 | 56 | - `cipherstash.toml` which contains the configuration for Protect.js 57 | - `cipherstash.secret.toml` which contains the credentials for Protect.js 58 | 59 | > [!WARNING] 60 | > Do not commit `cipherstash.secret.toml` to git, because it contains sensitive credentials. 61 | 62 | 63 | ## Using the basic example 64 | 65 | Run the example: 66 | 67 | ``` 68 | pnpm start 69 | ``` 70 | 71 | The application will log the plaintext to the console that has been encrypted using the CipherStash, decrypted, and logged the original plaintext. 72 | 73 | ## Next steps 74 | 75 | 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. 76 | -------------------------------------------------------------------------------- /packages/drizzle/__tests__/utils/code-executor.ts: -------------------------------------------------------------------------------- 1 | export interface ExecutionContext { 2 | [key: string]: unknown 3 | } 4 | 5 | export interface ExecutionResult { 6 | success: boolean 7 | result?: unknown 8 | error?: string 9 | } 10 | 11 | /** 12 | * Execute a documentation code block in a controlled context. 13 | * 14 | * ## Security Considerations 15 | * 16 | * This function uses the `Function()` constructor to execute arbitrary code. 17 | * This is equivalent to `eval()` and would normally be a serious security risk. 18 | * 19 | * **Why it's safe in this context:** 20 | * 1. **Trusted source:** Code comes only from our own documentation files in the 21 | * repository, not from user input or external sources. 22 | * 2. **Code review:** All documentation code examples go through PR review before 23 | * being merged, same as production code. 24 | * 3. **No network exposure:** Tests run in CI or local dev, never in production 25 | * environments handling user requests. 26 | * 4. **Controlled context:** Executed code only has access to explicitly provided 27 | * context variables (db, operators), not global scope or filesystem. 28 | * 29 | * **When this would NOT be safe:** 30 | * - If code came from user input (web forms, API requests) 31 | * - If code came from external/untrusted sources 32 | * - If executed in a production environment 33 | * - If the execution context included sensitive globals 34 | * 35 | * The eslint-disable comment below acknowledges we've considered the security 36 | * implications and determined this usage is appropriate for the use case. 37 | */ 38 | export async function executeCodeBlock( 39 | code: string, 40 | context: ExecutionContext, 41 | ): Promise { 42 | try { 43 | // Create an async function with access to context variables 44 | const contextKeys = Object.keys(context) 45 | const contextValues = Object.values(context) 46 | 47 | // eslint-disable-next-line @typescript-eslint/no-implied-eval 48 | const asyncFn = new Function( 49 | ...contextKeys, 50 | `return (async () => { ${code} })()`, 51 | ) 52 | 53 | const result = await asyncFn(...contextValues) 54 | 55 | return { 56 | success: true, 57 | result, 58 | } 59 | } catch (error) { 60 | return { 61 | success: false, 62 | error: error instanceof Error ? error.message : String(error), 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/dynamo/src/bulk-operations.ts: -------------------------------------------------------------------------------- 1 | import { BatchGetCommand, BatchWriteCommand } from '@aws-sdk/lib-dynamodb' 2 | import { protectDynamoDB } from '@cipherstash/protect-dynamodb' 3 | import { createTable, docClient, dynamoClient } from './common/dynamo' 4 | import { log } from './common/log' 5 | import { protectClient, users } from './common/protect' 6 | 7 | const tableName = 'UsersBulkOperations' 8 | 9 | type User = { 10 | pk: string 11 | email: string 12 | } 13 | 14 | const main = async () => { 15 | await createTable({ 16 | TableName: tableName, 17 | AttributeDefinitions: [ 18 | { 19 | AttributeName: 'pk', 20 | AttributeType: 'S', 21 | }, 22 | ], 23 | KeySchema: [ 24 | { 25 | AttributeName: 'pk', 26 | KeyType: 'HASH', 27 | }, 28 | ], 29 | }) 30 | 31 | const protectDynamo = protectDynamoDB({ 32 | protectClient, 33 | }) 34 | 35 | const items = [ 36 | { 37 | // `pk` won't be encrypted because it's not included in the `users` protected table schema. 38 | pk: 'user#1', 39 | // `email` will be encrypted because it's included in the `users` protected table schema. 40 | email: 'abc@example.com', 41 | }, 42 | { 43 | pk: 'user#2', 44 | email: 'def@example.com', 45 | }, 46 | ] 47 | 48 | const encryptResult = await protectDynamo.bulkEncryptModels(items, users) 49 | 50 | if (encryptResult.failure) { 51 | throw new Error(`Failed to encrypt items: ${encryptResult.failure.message}`) 52 | } 53 | 54 | const putRequests = encryptResult.data.map( 55 | (item: Record) => ({ 56 | PutRequest: { 57 | Item: item, 58 | }, 59 | }), 60 | ) 61 | 62 | log('encrypted items', encryptResult) 63 | 64 | const batchWriteCommand = new BatchWriteCommand({ 65 | RequestItems: { 66 | [tableName]: putRequests, 67 | }, 68 | }) 69 | 70 | await dynamoClient.send(batchWriteCommand) 71 | 72 | const batchGetCommand = new BatchGetCommand({ 73 | RequestItems: { 74 | [tableName]: { 75 | Keys: [{ pk: 'user#1' }, { pk: 'user#2' }], 76 | }, 77 | }, 78 | }) 79 | 80 | const getResult = await docClient.send(batchGetCommand) 81 | 82 | const decryptedItems = await protectDynamo.bulkDecryptModels( 83 | getResult.Responses?.[tableName], 84 | users, 85 | ) 86 | 87 | log('decrypted items', decryptedItems) 88 | } 89 | 90 | main() 91 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/components/form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { createUser } from '@/app/actions' 4 | import { zodResolver } from '@hookform/resolvers/zod' 5 | import { useTransition } from 'react' 6 | import { useForm } from 'react-hook-form' 7 | import * as z from 'zod' 8 | 9 | const formSchema = z.object({ 10 | name: z.string().min(1, 'Name is required'), 11 | email: z.string().email('Invalid email address'), 12 | }) 13 | 14 | export type FormData = z.infer 15 | 16 | export function ClientForm() { 17 | const [isPending, startTransition] = useTransition() 18 | const { 19 | register, 20 | handleSubmit, 21 | reset, 22 | formState: { errors }, 23 | } = useForm({ 24 | resolver: zodResolver(formSchema), 25 | }) 26 | 27 | const onSubmit = (data: FormData) => { 28 | startTransition(async () => { 29 | await createUser(data) 30 | reset() 31 | }) 32 | } 33 | 34 | return ( 35 |
39 |
40 |
41 | 44 | 50 | {errors.name && ( 51 |

{errors.name.message}

52 | )} 53 |
54 |
55 | 58 | 64 | {errors.email && ( 65 |

{errors.email.message}

66 | )} 67 |
68 | 75 |
76 |
77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /packages/protect/__tests__/keysets.test.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { csColumn, csTable } from '@cipherstash/schema' 3 | import { describe, expect, it } from 'vitest' 4 | import { protect } from '../src' 5 | 6 | const users = csTable('users', { 7 | email: csColumn('email'), 8 | }) 9 | 10 | describe('encryption and decryption with keyset id', () => { 11 | it('should encrypt and decrypt a payload', async () => { 12 | const protectClient = await protect({ 13 | schemas: [users], 14 | keyset: { 15 | id: '4152449b-505a-4186-93b6-d3d87eba7a47', 16 | }, 17 | }) 18 | 19 | const email = 'hello@example.com' 20 | 21 | const ciphertext = await protectClient.encrypt(email, { 22 | column: users.email, 23 | table: users, 24 | }) 25 | 26 | if (ciphertext.failure) { 27 | throw new Error(`[protect]: ${ciphertext.failure.message}`) 28 | } 29 | 30 | // Verify encrypted field 31 | expect(ciphertext.data).toHaveProperty('c') 32 | 33 | const a = ciphertext.data 34 | 35 | const plaintext = await protectClient.decrypt(ciphertext.data) 36 | 37 | expect(plaintext).toEqual({ 38 | data: email, 39 | }) 40 | }, 30000) 41 | }) 42 | 43 | describe('encryption and decryption with keyset name', () => { 44 | it('should encrypt and decrypt a payload', async () => { 45 | const protectClient = await protect({ 46 | schemas: [users], 47 | keyset: { 48 | name: 'Test', 49 | }, 50 | }) 51 | 52 | const email = 'hello@example.com' 53 | 54 | const ciphertext = await protectClient.encrypt(email, { 55 | column: users.email, 56 | table: users, 57 | }) 58 | 59 | if (ciphertext.failure) { 60 | throw new Error(`[protect]: ${ciphertext.failure.message}`) 61 | } 62 | 63 | // Verify encrypted field 64 | expect(ciphertext.data).toHaveProperty('c') 65 | 66 | const a = ciphertext.data 67 | 68 | const plaintext = await protectClient.decrypt(ciphertext.data) 69 | 70 | expect(plaintext).toEqual({ 71 | data: email, 72 | }) 73 | }, 30000) 74 | }) 75 | 76 | describe('encryption and decryption with invalid keyset id', () => { 77 | it('should throw an error', async () => { 78 | await expect( 79 | protect({ 80 | schemas: [users], 81 | keyset: { 82 | id: 'invalid-uuid', 83 | }, 84 | }), 85 | ).rejects.toThrow( 86 | '[protect]: Invalid UUID provided for keyset id. Must be a valid UUID.', 87 | ) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/README.md: -------------------------------------------------------------------------------- 1 | # Next.js + Drizzle ORM + MySQL + Protect.js Example 2 | 3 | This example demonstrates how to build a modern web application using: 4 | - [Next.js](https://nextjs.org/) - React framework for production 5 | - [Drizzle ORM](https://orm.drizzle.team/) - TypeScript ORM for SQL databases 6 | - [MySQL](https://www.mysql.com/) - Popular open-source relational database 7 | - [Protect.js](https://cipherstash.com/protect) - Data protection and encryption library 8 | 9 | ## Features 10 | 11 | - Full-stack TypeScript application 12 | - Database migrations and schema management with Drizzle 13 | - Data protection and encryption with Protect.js 14 | - Modern UI with Tailwind CSS 15 | - Form handling with React Hook Form and Zod validation 16 | - Docker-based MySQL database setup 17 | 18 | ## Prerequisites 19 | 20 | - Node.js 18+ 21 | - Docker and Docker Compose 22 | - MySQL (if running locally without Docker) 23 | 24 | ## Getting Started 25 | 26 | 1. Clone the repository and install dependencies: 27 | ```bash 28 | pnpm install 29 | ``` 30 | 31 | 2. Set up your environment variables: 32 | Copy the `.env.example` file to `.env.local`: 33 | ```bash 34 | cp .env.example .env.local 35 | ``` 36 | Then update the environment variables in `.env.local` with your Protect.js configuration values. 37 | 38 | 3. Start the MySQL database using Docker: 39 | ```bash 40 | docker compose up -d 41 | ``` 42 | 43 | 4. Run database migrations: 44 | ```bash 45 | pnpm run db:generate 46 | pnpm run db:migrate 47 | ``` 48 | 49 | 5. Start the development server: 50 | ```bash 51 | pnpm run dev 52 | ``` 53 | 54 | The application will be available at `http://localhost:3000`. 55 | 56 | ## Project Structure 57 | 58 | - `/src` - Application source code 59 | - `/drizzle` - Database migrations and schema 60 | - `/public` - Static assets 61 | - `drizzle.config.ts` - Drizzle ORM configuration 62 | - `docker-compose.yml` - Docker configuration for MySQL 63 | 64 | ## Available Scripts 65 | 66 | - `npm run dev` - Start development server 67 | - `npm run build` - Build for production 68 | - `npm run start` - Start production server 69 | - `npm run db:generate` - Generate database migrations 70 | - `npm run db:migrate` - Run database migrations 71 | 72 | ## Learn More 73 | 74 | - [Next.js Documentation](https://nextjs.org/docs) 75 | - [Drizzle ORM Documentation](https://orm.drizzle.team/docs/overview) 76 | - [Protect.js Documentation](https://cipherstash.com/protect/docs) 77 | - [MySQL Documentation](https://dev.mysql.com/doc/) 78 | -------------------------------------------------------------------------------- /examples/dynamo/src/encrypted-sort-key.ts: -------------------------------------------------------------------------------- 1 | import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' 2 | import { protectDynamoDB } from '@cipherstash/protect-dynamodb' 3 | import { createTable, docClient, dynamoClient } from './common/dynamo' 4 | import { log } from './common/log' 5 | import { protectClient, users } from './common/protect' 6 | 7 | const tableName = 'UsersEncryptedSortKey' 8 | 9 | type User = { 10 | pk: string 11 | email: string 12 | } 13 | 14 | const main = async () => { 15 | await createTable({ 16 | TableName: tableName, 17 | AttributeDefinitions: [ 18 | { 19 | AttributeName: 'pk', 20 | AttributeType: 'S', 21 | }, 22 | { 23 | AttributeName: 'email__hmac', 24 | AttributeType: 'S', 25 | }, 26 | ], 27 | KeySchema: [ 28 | { 29 | AttributeName: 'pk', 30 | KeyType: 'HASH', 31 | }, 32 | { 33 | AttributeName: 'email__hmac', 34 | KeyType: 'RANGE', 35 | }, 36 | ], 37 | }) 38 | 39 | const protectDynamo = protectDynamoDB({ 40 | protectClient, 41 | }) 42 | 43 | const user = { 44 | // `pk` won't be encrypted because it's not in the protected table schema. 45 | pk: 'user#1', 46 | // `email` will be encrypted because it's included in the `users` protected table schema. 47 | email: 'abc@example.com', 48 | } 49 | 50 | const encryptResult = await protectDynamo.encryptModel(user, users) 51 | 52 | log('encrypted item', encryptResult) 53 | 54 | const putCommand = new PutCommand({ 55 | TableName: tableName, 56 | Item: encryptResult, 57 | }) 58 | 59 | await docClient.send(putCommand) 60 | 61 | const searchTermsResult = await protectDynamo.createSearchTerms([ 62 | { 63 | value: 'abc@example.com', 64 | column: users.email, 65 | table: users, 66 | }, 67 | ]) 68 | 69 | if (searchTermsResult.failure) { 70 | throw new Error( 71 | `Failed to create search terms: ${searchTermsResult.failure.message}`, 72 | ) 73 | } 74 | 75 | const [emailHmac] = searchTermsResult.data 76 | 77 | const getCommand = new GetCommand({ 78 | TableName: tableName, 79 | Key: { pk: 'user#1', email__hmac: emailHmac }, 80 | }) 81 | 82 | const getResult = await docClient.send(getCommand) 83 | 84 | if (!getResult.Item) { 85 | throw new Error('Item not found') 86 | } 87 | 88 | const decryptedItem = await protectDynamo.decryptModel( 89 | getResult.Item, 90 | users, 91 | ) 92 | 93 | log('decrypted item', decryptedItem) 94 | } 95 | 96 | main() 97 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | ## Protect.js Cursor Rules 2 | 3 | These rules guide agents when creating or updating example apps under `examples/*` in this repository. 4 | 5 | ### Example App Prompt (for agents) 6 | 7 | - **Goals** 8 | - Show end-to-end usage of Protect.js with clear, minimal code. 9 | - Demonstrate schema, encrypt/decrypt, and (when relevant) searchable encryption on PostgreSQL. 10 | 11 | - **Hard guardrails (do not violate)** 12 | - Do not log plaintext at any time. 13 | - Preserve the Result contract: operations return `{ data }` or `{ failure }` with stable error `type` strings. 14 | - Do not change EQL payload shapes or keys (e.g., `c`). 15 | - `@cipherstash/protect-ffi` is a native Node-API module and must be externalized by bundlers (loaded via runtime `require`). 16 | - Keep both ESM and CJS exports working; do not break `require`. 17 | - Bun is not supported; use Node.js. 18 | 19 | - **Prerequisites and workflow** 20 | - Use Node.js >= 22 and pnpm 9.x. 21 | - Install/build/test: 22 | - `pnpm install` 23 | - `pnpm --filter dev|build|test` 24 | - Environment variables for examples/tests that talk to CipherStash: 25 | - `CS_WORKSPACE_CRN`, `CS_CLIENT_ID`, `CS_CLIENT_KEY`, `CS_CLIENT_ACCESS_KEY` 26 | - Optional for identity-aware encryption: `USER_JWT`, `USER_2_JWT` 27 | 28 | - **Docs to reference** 29 | - `docs/how-to/nextjs-external-packages.md` 30 | - `docs/how-to/sst-external-packages.md` 31 | - `docs/how-to/npm-lockfile-v3.md` 32 | - `docs/reference/schema.md` 33 | - `docs/concepts/searchable-encryption.md` 34 | 35 | - **Deliverables checklist for a new example** 36 | - A `protect.ts` (or equivalent) that initializes `protect({ schemas })` using `csTable`/`csColumn`. 37 | - If targeting Postgres searchable encryption, include `.freeTextSearch().equality().orderAndRange()` on appropriate columns. 38 | - A minimal script or route/handler that encrypts and decrypts at least one value. 39 | - A README covering: 40 | - Setup (env vars, install, run commands) 41 | - Notes on native module externalization if the framework builds/bundles (e.g., Next.js, SST) 42 | - How to run tests (if included) 43 | - Optional: demonstrate identity-aware encryption via `LockContext` and chaining `.withLockContext()` for both encrypt and decrypt. 44 | 45 | - **Quality bar** 46 | - Prefer bulk operations to demonstrate performance where appropriate. 47 | - Keep examples small, idiomatic, and runnable as-is with documented env vars. 48 | - Never leak secrets in code or logs; avoid any plaintext logging. 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test JS 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | branches: 9 | - "**" 10 | 11 | jobs: 12 | run-tests: 13 | name: Run Tests 14 | runs-on: blacksmith-4vcpu-ubuntu-2404 15 | 16 | steps: 17 | - name: Checkout Repo 18 | uses: actions/checkout@v3 19 | 20 | - uses: pnpm/action-setup@v4 21 | name: Install pnpm 22 | with: 23 | run_install: false 24 | 25 | - name: Install Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | cache: 'pnpm' 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | - name: Create .env file in ./packages/protect/ 35 | run: | 36 | touch ./packages/protect/.env 37 | echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/protect/.env 38 | echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/protect/.env 39 | echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/protect/.env 40 | echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/protect/.env 41 | echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> ./packages/protect/.env 42 | echo "SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" >> ./packages/protect/.env 43 | 44 | - name: Create .env file in ./packages/protect-dynamodb/ 45 | run: | 46 | touch ./packages/protect-dynamodb/.env 47 | echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/protect-dynamodb/.env 48 | echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/protect-dynamodb/.env 49 | echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/protect-dynamodb/.env 50 | echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/protect-dynamodb/.env 51 | 52 | - name: Create .env file in ./packages/drizzle/ 53 | run: | 54 | touch ./packages/drizzle/.env 55 | echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/drizzle/.env 56 | echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/drizzle/.env 57 | echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/drizzle/.env 58 | echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/drizzle/.env 59 | echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> ./packages/drizzle/.env 60 | 61 | # Run TurboRepo tests 62 | - name: Run tests 63 | run: pnpm run test 64 | -------------------------------------------------------------------------------- /examples/dynamo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cipherstash/dynamo-example 2 | 3 | ## 0.2.15 4 | 5 | ### Patch Changes 6 | 7 | - @cipherstash/protect@10.2.1 8 | - @cipherstash/protect-dynamodb@6.0.1 9 | 10 | ## 0.2.14 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [de029de] 15 | - @cipherstash/protect@10.2.0 16 | - @cipherstash/protect-dynamodb@6.0.0 17 | 18 | ## 0.2.13 19 | 20 | ### Patch Changes 21 | 22 | - Updated dependencies [ff4421f] 23 | - @cipherstash/protect@10.1.1 24 | - @cipherstash/protect-dynamodb@5.1.1 25 | 26 | ## 0.2.12 27 | 28 | ### Patch Changes 29 | 30 | - Updated dependencies [6b87c17] 31 | - @cipherstash/protect@10.1.0 32 | - @cipherstash/protect-dynamodb@6.0.0 33 | 34 | ## 0.2.11 35 | 36 | ### Patch Changes 37 | 38 | - @cipherstash/protect@10.0.2 39 | - @cipherstash/protect-dynamodb@5.0.2 40 | 41 | ## 0.2.10 42 | 43 | ### Patch Changes 44 | 45 | - @cipherstash/protect@10.0.1 46 | - @cipherstash/protect-dynamodb@5.0.1 47 | 48 | ## 0.2.9 49 | 50 | ### Patch Changes 51 | 52 | - Updated dependencies [788dbfc] 53 | - @cipherstash/protect-dynamodb@5.0.0 54 | - @cipherstash/protect@10.0.0 55 | 56 | ## 0.2.8 57 | 58 | ### Patch Changes 59 | 60 | - Updated dependencies [c7ed7ab] 61 | - Updated dependencies [211e979] 62 | - @cipherstash/protect@9.6.0 63 | - @cipherstash/protect-dynamodb@4.0.0 64 | 65 | ## 0.2.7 66 | 67 | ### Patch Changes 68 | 69 | - Updated dependencies [6f45b02] 70 | - @cipherstash/protect-dynamodb@3.0.0 71 | - @cipherstash/protect@9.5.0 72 | 73 | ## 0.2.6 74 | 75 | ### Patch Changes 76 | 77 | - @cipherstash/protect@9.4.1 78 | - @cipherstash/protect-dynamodb@2.0.1 79 | 80 | ## 0.2.5 81 | 82 | ### Patch Changes 83 | 84 | - Updated dependencies [1cc4772] 85 | - @cipherstash/protect@9.4.0 86 | - @cipherstash/protect-dynamodb@2.0.0 87 | 88 | ## 0.2.4 89 | 90 | ### Patch Changes 91 | 92 | - Updated dependencies [01fed9e] 93 | - @cipherstash/protect-dynamodb@1.0.0 94 | - @cipherstash/protect@9.3.0 95 | 96 | ## 0.2.3 97 | 98 | ### Patch Changes 99 | 100 | - Updated dependencies [2b63ee1] 101 | - Updated dependencies [e33fbaf] 102 | - @cipherstash/protect-dynamodb@0.3.0 103 | 104 | ## 0.2.2 105 | 106 | ### Patch Changes 107 | 108 | - Updated dependencies [587f222] 109 | - @cipherstash/protect@9.2.0 110 | - @cipherstash/protect-dynamodb@0.2.0 111 | 112 | ## 0.2.1 113 | 114 | ### Patch Changes 115 | 116 | - Updated dependencies [5fc0150] 117 | - @cipherstash/protect-dynamodb@0.2.0 118 | 119 | ## 0.2.0 120 | 121 | ### Minor Changes 122 | 123 | - c8468ee: Released initial version of the DynamoDB helper interface. 124 | 125 | ### Patch Changes 126 | 127 | - Updated dependencies [c8468ee] 128 | - @cipherstash/protect-dynamodb@1.0.0 129 | - @cipherstash/protect@9.1.0 130 | -------------------------------------------------------------------------------- /examples/typeorm/src/utils/encrypted-column.ts: -------------------------------------------------------------------------------- 1 | import type { EncryptedData } from '@cipherstash/protect' 2 | import type { ColumnOptions } from 'typeorm' 3 | 4 | /** 5 | * Transformer for encrypted data columns that handles PostgreSQL composite literal format 6 | * automatically. This eliminates the need for manual lifecycle hooks. 7 | */ 8 | export const encryptedDataTransformer = { 9 | /** 10 | * Transform encrypted data to PostgreSQL composite literal format for storage 11 | */ 12 | to(value: EncryptedData | null): string | null { 13 | if (value === null || value === undefined) { 14 | return null 15 | } 16 | 17 | // Convert to PostgreSQL composite literal format: (json_string) 18 | return `(${JSON.stringify(JSON.stringify(value))})` 19 | }, 20 | 21 | /** 22 | * Transform PostgreSQL composite literal format back to encrypted data object 23 | */ 24 | from(value: string | null): EncryptedData | null { 25 | if (!value || typeof value !== 'string') { 26 | return null 27 | } 28 | 29 | try { 30 | let jsonString: string = value.trim() 31 | 32 | // Remove outer parentheses if they exist 33 | if (jsonString.startsWith('(') && jsonString.endsWith(')')) { 34 | jsonString = jsonString.slice(1, -1) 35 | } 36 | 37 | // Handle PostgreSQL's double-quote escaping: "" -> " 38 | jsonString = jsonString.replace(/""/g, '"') 39 | 40 | // Remove outer quotes if they exist 41 | if (jsonString.startsWith('"') && jsonString.endsWith('"')) { 42 | jsonString = jsonString.slice(1, -1) 43 | } 44 | 45 | // Parse the JSON string 46 | return JSON.parse(jsonString) 47 | } catch (error: unknown) { 48 | console.error('Failed to parse encrypted data:', { 49 | original: value, 50 | error: error instanceof Error ? error.message : 'Unknown error', 51 | }) 52 | // Return null if parsing fails to avoid breaking the application 53 | return null 54 | } 55 | }, 56 | } 57 | 58 | /** 59 | * Enhanced column options for encrypted data with automatic transformation 60 | */ 61 | export interface EncryptedColumnOptions 62 | extends Omit { 63 | /** 64 | * Whether the column can be null. Defaults to true for encrypted columns. 65 | */ 66 | nullable?: boolean 67 | } 68 | 69 | /** 70 | * Creates column options for an encrypted column with automatic PostgreSQL transformation 71 | */ 72 | export function createEncryptedColumnOptions( 73 | options: EncryptedColumnOptions = {}, 74 | ): ColumnOptions { 75 | return { 76 | // biome-ignore lint/suspicious/noExplicitAny: TypeORM doesn't know about our custom type 77 | type: 'eql_v2_encrypted' as any, 78 | nullable: true, // Default to nullable for encrypted columns 79 | transformer: encryptedDataTransformer, 80 | ...options, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/utils/config/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | 4 | /** 5 | * A lightweight function that parses a TOML-like string 6 | * and returns the `workspace_crn` value found under `[auth]`. 7 | * 8 | * @param tomlString The contents of the TOML file as a string. 9 | * @returns The workspace_crn if found, otherwise undefined. 10 | */ 11 | function getWorkspaceCrn(tomlString: string): string | undefined { 12 | let currentSection = '' 13 | let workspaceCrn: string | undefined 14 | 15 | const lines = tomlString.split(/\r?\n/) 16 | 17 | for (const line of lines) { 18 | const trimmedLine = line.trim() 19 | 20 | if (!trimmedLine || trimmedLine.startsWith('#')) { 21 | continue 22 | } 23 | 24 | const sectionMatch = trimmedLine.match(/^\[([^\]]+)\]$/) 25 | if (sectionMatch) { 26 | currentSection = sectionMatch[1] 27 | continue 28 | } 29 | 30 | const kvMatch = trimmedLine.match(/^(\w+)\s*=\s*"([^"]+)"$/) 31 | if (kvMatch) { 32 | const [_, key, value] = kvMatch 33 | 34 | if (currentSection === 'auth' && key === 'workspace_crn') { 35 | workspaceCrn = value 36 | break 37 | } 38 | } 39 | } 40 | 41 | return workspaceCrn 42 | } 43 | 44 | /** 45 | * Extracts the workspace ID from a CRN string. 46 | * CRN format: crn:region.aws:ID 47 | * 48 | * @param crn The CRN string to extract from 49 | * @returns The workspace ID portion of the CRN 50 | */ 51 | function extractWorkspaceIdFromCrn(crn: string): string { 52 | const match = crn.match(/crn:[^:]+:([^:]+)$/) 53 | if (!match) { 54 | throw new Error('Invalid CRN format') 55 | } 56 | return match[1] 57 | } 58 | 59 | export function loadWorkSpaceId(suppliedCrn?: string): string { 60 | const configPath = path.join(process.cwd(), 'cipherstash.toml') 61 | 62 | if (suppliedCrn) { 63 | return extractWorkspaceIdFromCrn(suppliedCrn) 64 | } 65 | 66 | if (!fs.existsSync(configPath) && !process.env.CS_WORKSPACE_CRN) { 67 | throw new Error( 68 | 'You have not defined a workspace CRN in your config file, or the CS_WORKSPACE_CRN environment variable.', 69 | ) 70 | } 71 | 72 | // Environment variables take precedence over config files 73 | if (process.env.CS_WORKSPACE_CRN) { 74 | return extractWorkspaceIdFromCrn(process.env.CS_WORKSPACE_CRN) 75 | } 76 | 77 | if (!fs.existsSync(configPath)) { 78 | throw new Error( 79 | 'You have not defined a workspace CRN in your config file, or the CS_WORKSPACE_CRN environment variable.', 80 | ) 81 | } 82 | 83 | const tomlString = fs.readFileSync(configPath, 'utf8') 84 | const workspaceCrn = getWorkspaceCrn(tomlString) 85 | 86 | if (!workspaceCrn) { 87 | throw new Error( 88 | 'You have not defined a workspace CRN in your config file, or the CS_WORKSPACE_CRN environment variable.', 89 | ) 90 | } 91 | 92 | return extractWorkspaceIdFromCrn(workspaceCrn) 93 | } 94 | -------------------------------------------------------------------------------- /examples/nest/src/protect/decorators/decrypt.decorator.ts: -------------------------------------------------------------------------------- 1 | import { type ExecutionContext, createParamDecorator } from '@nestjs/common' 2 | import { getProtectService } from '../utils/get-protect-service.util' 3 | 4 | import type { 5 | ProtectColumn, 6 | ProtectTable, 7 | ProtectTableColumn, 8 | ProtectValue, 9 | } from '@cipherstash/protect' 10 | 11 | export interface DecryptOptions { 12 | table: ProtectTable 13 | column: ProtectColumn | ProtectValue 14 | lockContext?: unknown // JWT or LockContext 15 | } 16 | 17 | /** 18 | * Decorator to automatically decrypt a field or entire object 19 | * 20 | * @example 21 | * ```typescript 22 | * @Get(':id') 23 | * async getUser(@Param('id') id: string, @Decrypt('email', { table: 'users', column: 'email' }) decryptedEmail: string) { 24 | * // decryptedEmail is automatically decrypted 25 | * return { id, email: decryptedEmail }; 26 | * } 27 | * 28 | * @Get(':id') 29 | * async getUser(@Param('id') id: string, @DecryptModel('users') user: User) { 30 | * // user is automatically decrypted based on schema 31 | * return user; 32 | * } 33 | * ``` 34 | */ 35 | export const Decrypt = createParamDecorator( 36 | async (field: string, ctx: ExecutionContext) => { 37 | const request = ctx.switchToHttp().getRequest() 38 | const protectService = getProtectService(ctx) 39 | 40 | if (!protectService) { 41 | throw new Error( 42 | 'ProtectService not found. Make sure ProtectModule is imported.', 43 | ) 44 | } 45 | 46 | const value = 47 | request.body?.[field] || request.params?.[field] || request.query?.[field] 48 | if (value === undefined || value === null) { 49 | return value 50 | } 51 | 52 | // Check if value is already an encrypted payload 53 | if (typeof value === 'object' && value.c) { 54 | const result = await protectService.decrypt(value) 55 | if (result.failure) { 56 | throw new Error(`Decryption failed: ${result.failure.message}`) 57 | } 58 | return result.data 59 | } 60 | 61 | // If it's not encrypted, return as-is 62 | return value 63 | }, 64 | ) 65 | 66 | /** 67 | * Decorator to automatically decrypt an entire model based on schema 68 | */ 69 | export const DecryptModel = createParamDecorator( 70 | async (tableName: string, ctx: ExecutionContext) => { 71 | const request = ctx.switchToHttp().getRequest() 72 | const protectService = getProtectService(ctx) 73 | 74 | if (!protectService) { 75 | throw new Error( 76 | 'ProtectService not found. Make sure ProtectModule is imported.', 77 | ) 78 | } 79 | 80 | const model = request.body || request.params || request.query 81 | if (!model || typeof model !== 'object') { 82 | return model 83 | } 84 | 85 | // This would need to be enhanced to work with actual schema definitions 86 | // For now, it's a placeholder for the concept 87 | return model 88 | }, 89 | ) 90 | -------------------------------------------------------------------------------- /examples/nest/src/protect/protect.service.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Decrypted, 3 | EncryptOptions, 4 | EncryptedPayload, 5 | LockContext, 6 | ProtectClient, 7 | ProtectTable, 8 | ProtectTableColumn, 9 | } from '@cipherstash/protect' 10 | import { Inject, Injectable } from '@nestjs/common' 11 | import { PROTECT_CLIENT } from './protect.constants' 12 | 13 | @Injectable() 14 | export class ProtectService { 15 | constructor( 16 | @Inject(PROTECT_CLIENT) 17 | private readonly client: ProtectClient, 18 | ) {} 19 | 20 | async encrypt(plaintext: string, options: EncryptOptions) { 21 | return this.client.encrypt(plaintext, options) 22 | } 23 | 24 | async decrypt(encryptedPayload: EncryptedPayload) { 25 | return this.client.decrypt(encryptedPayload) 26 | } 27 | 28 | async encryptModel>( 29 | model: Decrypted, 30 | table: ProtectTable, 31 | ) { 32 | return this.client.encryptModel(model, table) 33 | } 34 | 35 | async decryptModel>(model: T) { 36 | return this.client.decryptModel(model) 37 | } 38 | 39 | async bulkEncrypt( 40 | plaintexts: Array<{ id?: string; plaintext: string | null }>, 41 | options: EncryptOptions, 42 | ) { 43 | return this.client.bulkEncrypt(plaintexts, options) 44 | } 45 | 46 | async bulkDecrypt( 47 | encryptedData: Array<{ id?: string; data: EncryptedPayload | null }>, 48 | ) { 49 | return this.client.bulkDecrypt(encryptedData) 50 | } 51 | 52 | async bulkEncryptModels>( 53 | models: Decrypted[], 54 | table: ProtectTable, 55 | ) { 56 | return this.client.bulkEncryptModels(models, table) 57 | } 58 | 59 | async bulkDecryptModels>(models: T[]) { 60 | return this.client.bulkDecryptModels(models) 61 | } 62 | 63 | // Identity-aware encryption methods 64 | async encryptWithLockContext( 65 | plaintext: string, 66 | options: EncryptOptions, 67 | lockContext: LockContext, 68 | ) { 69 | return this.client.encrypt(plaintext, options).withLockContext(lockContext) 70 | } 71 | 72 | async decryptWithLockContext( 73 | encryptedPayload: EncryptedPayload, 74 | lockContext: LockContext, 75 | ) { 76 | return this.client.decrypt(encryptedPayload).withLockContext(lockContext) 77 | } 78 | 79 | async encryptModelWithLockContext>( 80 | model: Decrypted, 81 | table: ProtectTable, 82 | lockContext: LockContext, 83 | ) { 84 | return this.client 85 | .encryptModel(model, table) 86 | .withLockContext(lockContext) 87 | } 88 | 89 | async decryptModelWithLockContext>( 90 | model: T, 91 | lockContext: LockContext, 92 | ) { 93 | return this.client.decryptModel(model).withLockContext(lockContext) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { db } from '@/core/db' 2 | import { users } from '@/core/db/schema' 3 | import { getLockContext, protectClient } from '@/core/protect' 4 | import { getCtsToken } from '@cipherstash/nextjs' 5 | import type { EncryptedData } from '@cipherstash/protect' 6 | import { auth, currentUser } from '@clerk/nextjs/server' 7 | import Header from '../components/Header' 8 | import UserTable from '../components/UserTable' 9 | 10 | export type EncryptedUser = { 11 | id: number 12 | name: string 13 | email: string | null 14 | authorized: boolean 15 | role: string 16 | } 17 | 18 | async function getUsers(): Promise { 19 | const { userId } = await auth() 20 | const token = await getCtsToken() 21 | const results = await db.select().from(users).limit(500) 22 | 23 | if (userId && token.success) { 24 | const cts_token = token.ctsToken 25 | const lockContext = getLockContext(cts_token) 26 | 27 | const promises = results.map(async (row) => { 28 | const decryptResult = await protectClient 29 | .decrypt(row.email as EncryptedData) 30 | .withLockContext(lockContext) 31 | 32 | if (decryptResult.failure) { 33 | console.error( 34 | 'Failed to decrypt the email for user', 35 | row.id, 36 | decryptResult.failure.message, 37 | ) 38 | 39 | return row.email 40 | } 41 | 42 | return decryptResult.data 43 | }) 44 | 45 | const data = (await Promise.allSettled(promises)) as PromiseSettledResult< 46 | string | null 47 | >[] 48 | 49 | return results.map((row, index) => ({ 50 | ...row, 51 | authorized: data[index].status === 'fulfilled', 52 | email: 53 | data[index].status === 'fulfilled' 54 | ? data[index].value 55 | : (row.email as { c: string }).c, 56 | })) 57 | } 58 | 59 | return results.map((row) => ({ 60 | id: row.id, 61 | name: row.name, 62 | authorized: false, 63 | email: (row.email as { c: string })?.c, 64 | role: row.role, 65 | })) 66 | } 67 | 68 | export default async function Home() { 69 | const users = await getUsers() 70 | const user = await currentUser() 71 | 72 | return ( 73 |
74 |
75 |
76 |
77 |

Users

78 | 79 | The email address of each user was encrypted with CipherStash and{' '} 80 | locked to the individual who created the user. Only that 81 | individual will be able to decrypt the email. 82 | 83 |
84 | 88 |
89 |
90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /packages/protect/__tests__/search-terms.test.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { csColumn, csTable } from '@cipherstash/schema' 3 | import { describe, expect, it } from 'vitest' 4 | import { type SearchTerm, protect } from '../src' 5 | 6 | const users = csTable('users', { 7 | email: csColumn('email').freeTextSearch().equality().orderAndRange(), 8 | address: csColumn('address').freeTextSearch(), 9 | }) 10 | 11 | describe('create search terms', () => { 12 | it('should create search terms with default return type', async () => { 13 | const protectClient = await protect({ schemas: [users] }) 14 | 15 | const searchTerms = [ 16 | { 17 | value: 'hello', 18 | column: users.email, 19 | table: users, 20 | }, 21 | { 22 | value: 'world', 23 | column: users.address, 24 | table: users, 25 | }, 26 | ] as SearchTerm[] 27 | 28 | const searchTermsResult = await protectClient.createSearchTerms(searchTerms) 29 | 30 | if (searchTermsResult.failure) { 31 | throw new Error(`[protect]: ${searchTermsResult.failure.message}`) 32 | } 33 | 34 | expect(searchTermsResult.data).toEqual( 35 | expect.arrayContaining([ 36 | expect.objectContaining({ 37 | c: expect.any(String), 38 | }), 39 | ]), 40 | ) 41 | }, 30000) 42 | 43 | it('should create search terms with composite-literal return type', async () => { 44 | const protectClient = await protect({ schemas: [users] }) 45 | 46 | const searchTerms = [ 47 | { 48 | value: 'hello', 49 | column: users.email, 50 | table: users, 51 | returnType: 'composite-literal', 52 | }, 53 | ] as SearchTerm[] 54 | 55 | const searchTermsResult = await protectClient.createSearchTerms(searchTerms) 56 | 57 | if (searchTermsResult.failure) { 58 | throw new Error(`[protect]: ${searchTermsResult.failure.message}`) 59 | } 60 | 61 | const result = searchTermsResult.data[0] as string 62 | expect(result).toMatch(/^\(.*\)$/) 63 | expect(() => JSON.parse(result.slice(1, -1))).not.toThrow() 64 | }, 30000) 65 | 66 | it('should create search terms with escaped-composite-literal return type', async () => { 67 | const protectClient = await protect({ schemas: [users] }) 68 | 69 | const searchTerms = [ 70 | { 71 | value: 'hello', 72 | column: users.email, 73 | table: users, 74 | returnType: 'escaped-composite-literal', 75 | }, 76 | ] as SearchTerm[] 77 | 78 | const searchTermsResult = await protectClient.createSearchTerms(searchTerms) 79 | 80 | if (searchTermsResult.failure) { 81 | throw new Error(`[protect]: ${searchTermsResult.failure.message}`) 82 | } 83 | 84 | const result = searchTermsResult.data[0] as string 85 | expect(result).toMatch(/^".*"$/) 86 | const unescaped = JSON.parse(result) 87 | expect(unescaped).toMatch(/^\(.*\)$/) 88 | expect(() => JSON.parse(unescaped.slice(1, -1))).not.toThrow() 89 | }, 30000) 90 | }) 91 | -------------------------------------------------------------------------------- /packages/nextjs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cipherstash/nextjs 2 | 3 | ## 4.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 1535259: Remove node api calls which are incompatible with Next.js middleware. 8 | 9 | ## 4.0.0 10 | 11 | ### Major Changes 12 | 13 | - 95c891d: Implemented CipherStash CRN in favor of workspace ID. 14 | 15 | - Replaces the environment variable `CS_WORKSPACE_ID` with `CS_WORKSPACE_CRN` 16 | - Replaces `workspace_id` with `workspace_crn` in the `cipherstash.toml` file 17 | 18 | ## 3.2.0 19 | 20 | ### Minor Changes 21 | 22 | - 9377b47: Updated versions to address Next.js CVE. 23 | 24 | ## 3.1.0 25 | 26 | ### Minor Changes 27 | 28 | - a564f21: Bumped versions of dependencies to address CWE-346. 29 | 30 | ## 3.0.0 31 | 32 | ### Major Changes 33 | 34 | - 02dc980: Support configuration from environment variables or toml config. 35 | 36 | ## 2.1.0 37 | 38 | ### Minor Changes 39 | 40 | - 5a34e76: Rebranded logging context and fixed tests. 41 | 42 | ## 2.0.0 43 | 44 | ### Major Changes 45 | 46 | - 76599e5: Rebrand jseql to protect. 47 | 48 | ## 1.2.0 49 | 50 | ### Minor Changes 51 | 52 | - 3cb97c2: Added an optional argument to getCtsToken to fetch a new CTS token. 53 | 54 | ## 1.1.0 55 | 56 | ### Minor Changes 57 | 58 | - d0f5dd9: Enforced a check for the subject claims before setting cts session. 59 | 60 | ## 1.0.0 61 | 62 | ### Major Changes 63 | 64 | - 24f0a72: Implemented better error handling for fetching CTS tokens and accessing them in the Next.js application. 65 | 66 | ## 0.12.0 67 | 68 | ### Minor Changes 69 | 70 | - 14c0279: Fixed optional response argument getting called in setCtsToken. 71 | 72 | ## 0.11.0 73 | 74 | ### Minor Changes 75 | 76 | - ebc23ba: Added support for optional next response in generic jseql middleware functions. 77 | 78 | ## 0.10.0 79 | 80 | ### Minor Changes 81 | 82 | - 7d0fac0: Implemented a generic Next.js jseql middleware. 83 | 84 | ## 0.9.0 85 | 86 | ### Minor Changes 87 | 88 | - e885975: Fixed improper use of throwing errors, and log with jseql logger. 89 | 90 | ## 0.8.0 91 | 92 | ### Minor Changes 93 | 94 | - eeaec18: Implemented typing and import synatx for es6. 95 | 96 | ## 0.7.0 97 | 98 | ### Minor Changes 99 | 100 | - 7b8ec52: Implement packageless logging framework. 101 | 102 | ## 0.6.0 103 | 104 | ### Minor Changes 105 | 106 | - 7480cfd: Fixed node:util package bundling. 107 | 108 | ## 0.5.0 109 | 110 | ### Minor Changes 111 | 112 | - c0123be: Replaced logtape with native node debuglog. 113 | 114 | ## 0.4.0 115 | 116 | ### Minor Changes 117 | 118 | - 3bb4a10: Cleared session cookies when a user has logged out. 119 | 120 | ## 0.3.0 121 | 122 | ### Minor Changes 123 | 124 | - 9a3132c: Fixed the logtape peer dependency version. 125 | 126 | ## 0.2.0 127 | 128 | ### Minor Changes 129 | 130 | - 80ee5af: Fixed bugs when implmenting the lock context with CTS v2 tokens. 131 | 132 | ## 0.1.0 133 | 134 | ### Minor Changes 135 | 136 | - fbb2bcb: Released jseql clerk middleware. 137 | -------------------------------------------------------------------------------- /examples/nest/src/protect/decorators/encrypt.decorator.ts: -------------------------------------------------------------------------------- 1 | import { type ExecutionContext, createParamDecorator } from '@nestjs/common' 2 | import type { ProtectService } from '../protect.service' 3 | import { users } from '../schema' 4 | import { getProtectService } from '../utils/get-protect-service.util' 5 | 6 | import type { 7 | ProtectColumn, 8 | ProtectTable, 9 | ProtectTableColumn, 10 | ProtectValue, 11 | } from '@cipherstash/protect' 12 | 13 | export interface EncryptOptions { 14 | table: ProtectTable 15 | column: ProtectColumn | ProtectValue 16 | lockContext?: unknown // JWT or LockContext 17 | } 18 | 19 | /** 20 | * Decorator to automatically encrypt a field or entire object 21 | * 22 | * @example 23 | * ```typescript 24 | * @Post() 25 | * async createUser(@Body() userData: CreateUserDto, @Encrypt('email', { table: 'users', column: 'email' }) encryptedEmail: string) { 26 | * // encryptedEmail is automatically encrypted 27 | * return this.userService.create({ ...userData, email: encryptedEmail }); 28 | * } 29 | * 30 | * @Post() 31 | * async createUser(@Body() @EncryptModel('users') userData: CreateUserDto) { 32 | * // userData is automatically encrypted based on schema 33 | * return this.userService.create(userData); 34 | * } 35 | * ``` 36 | */ 37 | export const Encrypt = createParamDecorator( 38 | async (field: string, ctx: ExecutionContext) => { 39 | const request = ctx.switchToHttp().getRequest() 40 | const protectService = getProtectService(ctx) 41 | 42 | if (!protectService) { 43 | throw new Error( 44 | 'ProtectService not found. Make sure ProtectModule is imported.', 45 | ) 46 | } 47 | 48 | const value = request.body?.[field] 49 | if (value === undefined || value === null) { 50 | return value 51 | } 52 | 53 | // Note: This is a simplified example. In practice, you'd need to pass actual table/column objects 54 | // from your schema definitions rather than creating them inline 55 | const result = await protectService.encrypt(value, { 56 | table: users, 57 | column: users.email_encrypted, 58 | }) 59 | 60 | if (result.failure) { 61 | throw new Error(`Encryption failed: ${result.failure.message}`) 62 | } 63 | 64 | return result.data 65 | }, 66 | ) 67 | 68 | /** 69 | * Decorator to automatically encrypt an entire model based on schema 70 | */ 71 | export const EncryptModel = createParamDecorator( 72 | async (tableName: string, ctx: ExecutionContext) => { 73 | const request = ctx.switchToHttp().getRequest() 74 | const protectService = getProtectService(ctx) 75 | 76 | if (!protectService) { 77 | throw new Error( 78 | 'ProtectService not found. Make sure ProtectModule is imported.', 79 | ) 80 | } 81 | 82 | const model = request.body 83 | if (!model || typeof model !== 'object') { 84 | return model 85 | } 86 | 87 | // This would need to be enhanced to work with actual schema definitions 88 | // For now, it's a placeholder for the concept 89 | return model 90 | }, 91 | ) 92 | -------------------------------------------------------------------------------- /examples/nest/src/protect/interceptors/encrypt.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type CallHandler, 3 | type ExecutionContext, 4 | Injectable, 5 | type NestInterceptor, 6 | } from '@nestjs/common' 7 | import type { Observable } from 'rxjs' 8 | import { map } from 'rxjs/operators' 9 | import type { ProtectService } from '../protect.service' 10 | import { getProtectService } from '../utils/get-protect-service.util' 11 | 12 | import type { 13 | ProtectColumn, 14 | ProtectTable, 15 | ProtectTableColumn, 16 | ProtectValue, 17 | } from '@cipherstash/protect' 18 | 19 | export interface EncryptInterceptorOptions { 20 | fields?: string[] 21 | table: ProtectTable 22 | column: ProtectColumn | ProtectValue 23 | lockContext?: unknown 24 | } 25 | 26 | /** 27 | * Interceptor to automatically encrypt response data 28 | * 29 | * @example 30 | * ```typescript 31 | * @UseInterceptors(new EncryptInterceptor({ 32 | * fields: ['email', 'phone'], 33 | * table: 'users', 34 | * column: 'email' 35 | * })) 36 | * @Get() 37 | * async getUsers() { 38 | * return this.userService.findAll(); // Email and phone fields will be encrypted 39 | * } 40 | * ``` 41 | */ 42 | @Injectable() 43 | export class EncryptInterceptor implements NestInterceptor { 44 | constructor(private readonly options: EncryptInterceptorOptions) {} 45 | 46 | async intercept( 47 | context: ExecutionContext, 48 | next: CallHandler, 49 | ): Promise> { 50 | const protectService = getProtectService(context) 51 | 52 | if (!protectService) { 53 | throw new Error( 54 | 'ProtectService not found. Make sure ProtectModule is imported.', 55 | ) 56 | } 57 | 58 | return next.handle().pipe( 59 | map(async (data: unknown) => { 60 | if (!data) return data 61 | 62 | if (Array.isArray(data)) { 63 | return Promise.all( 64 | data.map((item) => this.encryptItem(item, protectService)), 65 | ) 66 | } 67 | 68 | return this.encryptItem(data, protectService) 69 | }), 70 | ) 71 | } 72 | 73 | private async encryptItem( 74 | item: unknown, 75 | protectService: ProtectService, 76 | ): Promise { 77 | if (!item || typeof item !== 'object') { 78 | return item 79 | } 80 | 81 | const result = { ...item } 82 | 83 | if (this.options.fields) { 84 | for (const field of this.options.fields) { 85 | if (result[field] !== undefined && result[field] !== null) { 86 | const encryptResult = await protectService.encrypt(result[field], { 87 | table: this.options.table, 88 | column: this.options.column, 89 | }) 90 | 91 | if (encryptResult.failure) { 92 | throw new Error( 93 | `Encryption failed for field ${field}: ${encryptResult.failure.message}`, 94 | ) 95 | } 96 | 97 | result[field] = encryptResult.data 98 | } 99 | } 100 | } 101 | 102 | return result 103 | } 104 | } 105 | --------------------------------------------------------------------------------