diff --git a/examples/basic/README.md b/examples/basic/README.md index c25c6de6..37b96239 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -1,6 +1,6 @@ -# Basic example of using @cipherstash/protect +# Basic example of using @cipherstash/stack -This basic example demonstrates how to use the `@cipherstash/protect` package to encrypt arbitrary input. +This basic example demonstrates how to use the `@cipherstash/stack` package and the **Encryption SDK** to encrypt and decrypt arbitrary input. ## Installing the basic example @@ -16,7 +16,7 @@ git clone https://github.com/cipherstash/protectjs Install dependencies: ```bash -# Build Project.js +# Build the repo (including @cipherstash/stack) cd protectjs pnpm build @@ -43,7 +43,7 @@ Lastly, install the CipherStash CLI: > [!IMPORTANT] > Make sure you have [installed the CipherStash CLI](#installation) before following these steps. -Set up all the configuration and credentials required for Protect.js: +Set up all the configuration and credentials required for the Encryption SDK: ```bash stash setup @@ -53,8 +53,8 @@ If you have not already signed up for a CipherStash account, this will prompt yo At the end of `stash setup`, you will have two files in your project: -- `cipherstash.toml` which contains the configuration for Protect.js -- `cipherstash.secret.toml` which contains the credentials for Protect.js +- `cipherstash.toml` which contains the configuration for the Encryption SDK +- `cipherstash.secret.toml` which contains the credentials for the Encryption SDK > [!WARNING] > Do not commit `cipherstash.secret.toml` to git, because it contains sensitive credentials. @@ -68,8 +68,4 @@ Run the example: pnpm start ``` -The application will log the plaintext to the console that has been encrypted using the CipherStash, decrypted, and logged the original plaintext. - -## Next steps - -Check out the [Protect.js + Next.js + Clerk example app](../nextjs-clerk) to see how to add end-user identity as an extra control when encrypting data. +The application will prompt for a name, encrypt it with CipherStash, log the ciphertext, decrypt it, and log the original plaintext. It then runs a short bulk-encryption demo. diff --git a/examples/basic/encrypt.ts b/examples/basic/encrypt.ts new file mode 100644 index 00000000..3d41e1c5 --- /dev/null +++ b/examples/basic/encrypt.ts @@ -0,0 +1,14 @@ +import 'dotenv/config' +import { + Encryption, + encryptedTable, + encryptedColumn, +} from '@cipherstash/stack' + +export const users = encryptedTable('users', { + name: encryptedColumn('name'), +}) + +export const client = await Encryption({ + schemas: [users], +}) diff --git a/examples/basic/index.ts b/examples/basic/index.ts index 9e9e8fce..b00ad13f 100644 --- a/examples/basic/index.ts +++ b/examples/basic/index.ts @@ -1,6 +1,6 @@ import 'dotenv/config' import readline from 'node:readline' -import { protectClient, users } from './protect' +import { client, users } from './encrypt' const rl = readline.createInterface({ input: process.stdin, @@ -18,13 +18,13 @@ const askQuestion = (): Promise => { async function main() { const input = await askQuestion() - const encryptResult = await protectClient.encrypt(input, { + const encryptResult = await client.encrypt(input, { column: users.name, table: users, }) if (encryptResult.failure) { - throw new Error(`[protect]: ${encryptResult.failure.message}`) + throw new Error(`[encryption]: ${encryptResult.failure.message}`) } const ciphertext = encryptResult.data @@ -32,10 +32,10 @@ async function main() { console.log('Encrypting your name...') console.log('The ciphertext is:', ciphertext) - const decryptResult = await protectClient.decrypt(ciphertext) + const decryptResult = await client.decrypt(ciphertext) if (decryptResult.failure) { - throw new Error(`[protect]: ${decryptResult.failure.message}`) + throw new Error(`[encryption]: ${decryptResult.failure.message}`) } const plaintext = decryptResult.data @@ -50,7 +50,6 @@ async function main() { { id: '1', plaintext: 'Alice' }, { id: '2', plaintext: 'Bob' }, { id: '3', plaintext: 'Charlie' }, - { id: '4', plaintext: null }, ] console.log( @@ -58,13 +57,13 @@ async function main() { bulkPlaintexts.map((p) => p.plaintext), ) - const bulkEncryptResult = await protectClient.bulkEncrypt(bulkPlaintexts, { + const bulkEncryptResult = await client.bulkEncrypt(bulkPlaintexts, { column: users.name, table: users, }) if (bulkEncryptResult.failure) { - throw new Error(`[protect]: ${bulkEncryptResult.failure.message}`) + throw new Error(`[encryption]: ${bulkEncryptResult.failure.message}`) } console.log('Bulk encrypted data:', bulkEncryptResult.data) diff --git a/examples/basic/package.json b/examples/basic/package.json index 938dd068..e3c04a4e 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -11,7 +11,7 @@ "license": "ISC", "description": "", "dependencies": { - "@cipherstash/protect": "workspace:*", + "@cipherstash/stack": "workspace:*", "dotenv": "^16.4.7" }, "devDependencies": { diff --git a/examples/basic/protect.ts b/examples/basic/protect.ts deleted file mode 100644 index 0feb8f63..00000000 --- a/examples/basic/protect.ts +++ /dev/null @@ -1,17 +0,0 @@ -import 'dotenv/config' -import { - type ProtectClientConfig, - csColumn, - csTable, - protect, -} from '@cipherstash/protect' - -export const users = csTable('users', { - name: csColumn('name'), -}) - -const config: ProtectClientConfig = { - schemas: [users], -} - -export const protectClient = await protect(config) diff --git a/examples/drizzle/.env.example b/examples/drizzle/.env.example deleted file mode 100644 index 51f1cc33..00000000 --- a/examples/drizzle/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -DATABASE_URL="postgresql://[username]:[password]@localhost:6432/[database]" -CS_CLIENT_ID= -CS_CLIENT_KEY= -CS_WORKSPACE_ID= -CS_CLIENT_ACCESS_KEY= \ No newline at end of file diff --git a/examples/drizzle/README.md b/examples/drizzle/README.md deleted file mode 100644 index 6138e78b..00000000 --- a/examples/drizzle/README.md +++ /dev/null @@ -1,319 +0,0 @@ -# Express REST API with Drizzle ORM and Protect.js - -This example demonstrates a FinTech REST API built with Express.js, Drizzle ORM, and Protect.js. It showcases how to encrypt sensitive financial data (account numbers, amounts, transaction descriptions) while maintaining the ability to search and query encrypted fields. - -## Prerequisites - -- **Node.js**: >= 22 -- **PostgreSQL**: Database with EQL v2 functions installed -- **CipherStash account**: For encryption credentials - -## Technologies - -- [Express](https://expressjs.com/) - Web framework -- [Drizzle ORM](https://orm.drizzle.team/) - TypeScript ORM -- [Protect.js](https://github.com/cipherstash/protectjs) - End-to-end encryption -- [PostgreSQL](https://www.postgresql.org/) - Database - -## Setup - -### 1. Install Dependencies - -```bash -pnpm install -``` - -### 2. Set Up PostgreSQL with EQL v2 - -Before running migrations, you need to install the EQL v2 types and functions in your PostgreSQL database: - -```bash -# Download the EQL install script -curl -sLo cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/latest/download/cipherstash-encrypt.sql - -# Install EQL types and functions -psql -d your_database -f cipherstash-encrypt.sql -``` - -This creates the `eql_v2_encrypted` composite type and search functions needed for searchable encryption. - -### 3. Environment Variables - -Create an account on [CipherStash](https://dashboard.cipherstash.com/sign-up), create a workspace, then generate your client credentials. - -Then, create a `.env` file in the root directory: - -```bash -# Database connection -DATABASE_URL="postgresql://[username]:[password]@[host]:5432/[database]" - -# CipherStash credentials (generated in your CipherStash workspace) -CS_CLIENT_ID=[client-id] -CS_CLIENT_KEY=[client-key] -CS_WORKSPACE_CRN=[workspace-crn] -CS_CLIENT_ACCESS_KEY=[access-key] - -# Optional: Server port (default: 3000) -PORT=3000 -``` - -### 4. Run Database Migrations - -```bash -pnpm db:migrate -``` - -This creates the `transactions` table with encrypted columns. - -### 5. Start the Server - -```bash -pnpm dev -``` - -The server will start on `http://localhost:3000` (or the port specified in `PORT`). - -## API Endpoints - -### Health Check - -**GET** `/health` - -Returns server status. - -```bash -curl http://localhost:3000/health -``` - -Response: -```json -{ - "status": "ok", - "message": "Server is running" -} -``` - -### List Transactions - -**GET** `/transactions` - -Retrieves all transactions with optional filters. - -**Query Parameters:** -- `accountNumber` (string) - Search by account number (encrypted field, text search) -- `minAmount` (number) - Minimum transaction amount (encrypted field, range query) -- `maxAmount` (number) - Maximum transaction amount (encrypted field, range query) -- `status` (string) - Filter by status (non-encrypted field) - -**Example:** -```bash -# Get all transactions -curl http://localhost:3000/transactions - -# Filter by account number -curl "http://localhost:3000/transactions?accountNumber=1234" - -# Filter by amount range -curl "http://localhost:3000/transactions?minAmount=100&maxAmount=1000" - -# Filter by status -curl "http://localhost:3000/transactions?status=completed" -``` - -> [!IMPORTANT] -> For production use, you should not use GET requests to filter data. -> Instead, you should use POST requests to filter data so sensitive data is not exposed in the URL. - -**Response:** -```json -{ - "transactions": [ - { - "id": 1, - "accountNumber": "1234567890", - "amount": 500.00, - "description": "Payment for services", - "transactionType": "payment", - "status": "completed", - "createdAt": "2024-01-15T10:30:00Z", - "updatedAt": "2024-01-15T10:30:00Z" - } - ] -} -``` - -### Create Transaction - -**POST** `/transactions` - -Creates a new transaction with encrypted sensitive fields. - -**Request Body:** -```json -{ - "accountNumber": "1234567890", - "amount": 500.00, - "description": "Payment for services", - "transactionType": "payment", - "status": "pending" -} -``` - -**Example:** -```bash -curl -X POST http://localhost:3000/transactions \ - -H "Content-Type: application/json" \ - -d '{ - "accountNumber": "1234567890", - "amount": 500.00, - "description": "Payment for services", - "transactionType": "payment", - "status": "pending" - }' -``` - -**Response:** -```json -{ - "transaction": { - "id": 1, - "accountNumber": "1234567890", - "amount": 500.00, - "description": "Payment for services", - "transactionType": "payment", - "status": "pending", - "createdAt": "2024-01-15T10:30:00Z", - "updatedAt": "2024-01-15T10:30:00Z" - } -} -``` - -### Get Transaction by ID - -**GET** `/transactions/:id` - -Retrieves a single transaction by ID. - -**Example:** -```bash -curl http://localhost:3000/transactions/1 -``` - -**Response:** -```json -{ - "transaction": { - "id": 1, - "accountNumber": "1234567890", - "amount": 500.00, - "description": "Payment for services", - "transactionType": "payment", - "status": "completed", - "createdAt": "2024-01-15T10:30:00Z", - "updatedAt": "2024-01-15T10:30:00Z" - } -} -``` - -### Update Transaction - -**PUT** `/transactions/:id` - -Updates a transaction. All fields are optional. - -**Request Body:** -```json -{ - "accountNumber": "9876543210", - "amount": 750.00, - "description": "Updated description", - "transactionType": "refund", - "status": "completed" -} -``` - -**Example:** -```bash -curl -X PUT http://localhost:3000/transactions/1 \ - -H "Content-Type: application/json" \ - -d '{ - "status": "completed", - "amount": 750.00 - }' -``` - -**Response:** -```json -{ - "transaction": { - "id": 1, - "accountNumber": "1234567890", - "amount": 750.00, - "description": "Payment for services", - "transactionType": "payment", - "status": "completed", - "createdAt": "2024-01-15T10:30:00Z", - "updatedAt": "2024-01-15T11:00:00Z" - } -} -``` - -### Delete Transaction - -**DELETE** `/transactions/:id` - -Deletes a transaction. - -**Example:** -```bash -curl -X DELETE http://localhost:3000/transactions/1 -``` - -**Response:** 204 No Content - -## Database Schema - -The `transactions` table has the following structure: - -- **Encrypted fields** (using `eql_v2_encrypted` type): - - `account_number` - Account number with equality and text search - - `amount` - Transaction amount with equality, range queries, and sorting - - `description` - Transaction description with text search - -- **Non-encrypted fields**: - - `id` - Primary key (serial) - - `transaction_type` - Type of transaction (varchar) - - `status` - Transaction status (varchar, default: 'pending') - - `created_at` - Timestamp - - `updated_at` - Timestamp - -## How It Works - -### Encryption - -- Sensitive fields (`accountNumber`, `amount`, `description`) are encrypted using Protect.js before being stored in the database -- The `@cipherstash/drizzle` package provides `encryptedType` helper to define encrypted columns in Drizzle schemas -- Data is automatically encrypted when inserting/updating and decrypted when reading - -### Searchable Encryption - -- The API demonstrates searchable encryption capabilities: - - **Text search** on `accountNumber` and `description` using `ilike` operator - - **Range queries** on `amount` using `gte` and `lte` operators - - **Equality queries** on `accountNumber` and `amount` -- All encrypted field queries use Protect.js operators that automatically handle encryption - -### Type Safety - -- TypeScript types are preserved throughout the encryption/decryption process -- The `encryptedType` helper ensures decrypted values maintain their correct types - -## Notes - -- **Native Module**: Protect.js uses `@cipherstash/protect-ffi`, a native Node-API module. Express doesn't bundle code, so no special configuration is needed. If deploying to serverless platforms, ensure the native module is properly externalized. -- **Error Handling**: All Protect.js operations return a Result type (`{ data }` or `{ failure }`). The API properly handles these results and returns appropriate HTTP status codes. -- **Bulk Operations**: The API uses `bulkEncryptModels` and `bulkDecryptModels` for efficient batch operations when querying multiple transactions. - -## License - -This project is licensed under the MIT License. diff --git a/examples/drizzle/drizzle.config.ts b/examples/drizzle/drizzle.config.ts deleted file mode 100644 index 16835108..00000000 --- a/examples/drizzle/drizzle.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import 'dotenv/config' -import { defineConfig } from 'drizzle-kit' - -export default defineConfig({ - out: './drizzle', - schema: './src/db/schema.ts', - dialect: 'postgresql', - dbCredentials: { - url: process.env.DATABASE_URL, - }, -}) diff --git a/examples/drizzle/drizzle/0000_goofy_cannonball.sql b/examples/drizzle/drizzle/0000_goofy_cannonball.sql deleted file mode 100644 index 5a67974e..00000000 --- a/examples/drizzle/drizzle/0000_goofy_cannonball.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS "users" ( - "id" serial PRIMARY KEY NOT NULL, - "email" varchar, - "email_encrypted" jsonb NOT NULL, - CONSTRAINT "users_email_unique" UNIQUE("email") -); diff --git a/examples/drizzle/drizzle/0001_transactions.sql b/examples/drizzle/drizzle/0001_transactions.sql deleted file mode 100644 index 40143922..00000000 --- a/examples/drizzle/drizzle/0001_transactions.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Drop old users table if it exists -DROP TABLE IF EXISTS "users"; - --- Create transactions table -CREATE TABLE IF NOT EXISTS "transactions" ( - "id" serial PRIMARY KEY NOT NULL, - "account_number" eql_v2_encrypted, - "amount" eql_v2_encrypted, - "description" eql_v2_encrypted, - "transaction_type" varchar(50) NOT NULL, - "status" varchar(20) NOT NULL DEFAULT 'pending', - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); - diff --git a/examples/drizzle/drizzle/meta/0000_snapshot.json b/examples/drizzle/drizzle/meta/0000_snapshot.json deleted file mode 100644 index 1abd1658..00000000 --- a/examples/drizzle/drizzle/meta/0000_snapshot.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "id": "a263534d-e155-4647-9ed2-eb113525c55c", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "serial", - "primaryKey": true, - "notNull": true - }, - "email": { - "name": "email", - "type": "varchar", - "primaryKey": false, - "notNull": false - }, - "email_encrypted": { - "name": "email_encrypted", - "type": "jsonb", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - } - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/examples/drizzle/drizzle/meta/_journal.json b/examples/drizzle/drizzle/meta/_journal.json deleted file mode 100644 index b38eaec5..00000000 --- a/examples/drizzle/drizzle/meta/_journal.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "7", - "dialect": "postgresql", - "entries": [ - { - "idx": 0, - "version": "7", - "when": 1734712905691, - "tag": "0000_goofy_cannonball", - "breakpoints": true - }, - { - "idx": 1, - "version": "7", - "when": 1734712905700, - "tag": "0001_transactions", - "breakpoints": true - } - ] -} diff --git a/examples/drizzle/environment.d.ts b/examples/drizzle/environment.d.ts deleted file mode 100644 index 33e051c2..00000000 --- a/examples/drizzle/environment.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare namespace NodeJS { - interface ProcessEnv { - DATABASE_URL: string - CS_CLIENT_ID: string - CS_CLIENT_KEY: string - CS_WORKSPACE_CRN: string - CS_CLIENT_ACCESS_KEY: string - PORT?: string - } -} diff --git a/examples/drizzle/package.json b/examples/drizzle/package.json deleted file mode 100644 index 13bf1e46..00000000 --- a/examples/drizzle/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "drizzle-eql", - "module": "index.ts", - "private": true, - "type": "module", - "devDependencies": { - "@types/express": "^4.17.21", - "@types/node": "^22.10.2", - "@types/pg": "^8.11.10", - "dotenv": "^16.4.7", - "drizzle-kit": "^0.30.5", - "tsx": "catalog:repo", - "typescript": "catalog:repo" - }, - "scripts": { - "dev": "tsx src/server.ts", - "start": "tsx src/server.ts", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate" - }, - "dependencies": { - "@cipherstash/drizzle": "workspace:*", - "@cipherstash/protect": "workspace:*", - "drizzle-orm": "^0.44.7", - "express": "^5.2.1", - "pg": "^8.16.3", - "postgres": "^3.4.7" - } -} diff --git a/examples/drizzle/src/controllers/transactions.ts b/examples/drizzle/src/controllers/transactions.ts deleted file mode 100644 index 308a1daf..00000000 --- a/examples/drizzle/src/controllers/transactions.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { eq } from 'drizzle-orm' -import type { Request, Response } from 'express' -import { db } from '../db' -import { transactions } from '../db/schema' -import { - protectClient, - protectOps, - transactionsSchema, -} from '../protect/config' - -interface CreateTransactionBody { - accountNumber: string - amount: number - description?: string - transactionType: string - status?: string -} - -interface UpdateTransactionBody { - accountNumber?: string - amount?: number - description?: string - transactionType?: string - status?: string -} - -// GET /transactions - List all transactions with optional filters -export async function getTransactions(req: Request, res: Response) { - try { - const { accountNumber, minAmount, maxAmount, status } = req.query - - let query = db.select().from(transactions) - - // Build where conditions - const conditions = [] - - // Account number search (encrypted field) - if (accountNumber && typeof accountNumber === 'string') { - const accountCondition = await protectOps.like( - transactions.accountNumber, - accountNumber, - ) - conditions.push(accountCondition) - } - - // Amount range (encrypted field) - if (minAmount !== undefined || maxAmount !== undefined) { - if (minAmount !== undefined) { - const minAmountNum = Number(minAmount) - if (!Number.isNaN(minAmountNum)) { - conditions.push(protectOps.gte(transactions.amount, minAmountNum)) - } - } - if (maxAmount !== undefined) { - const maxAmountNum = Number(maxAmount) - if (!Number.isNaN(maxAmountNum)) { - conditions.push(protectOps.lte(transactions.amount, maxAmountNum)) - } - } - } - - // Status filter (non-encrypted field) - if (status && typeof status === 'string') { - conditions.push(eq(transactions.status, status)) - } - - // Apply conditions - if (conditions.length > 0) { - const condition = await protectOps.and(...conditions) - query = query.where(condition) as typeof query - } - - // Execute query - const results = await query.execute() - - // Decrypt results - const decryptedResult = await protectClient.bulkDecryptModels(results) - if (decryptedResult.failure) { - return res.status(500).json({ - error: 'Decryption failed', - message: decryptedResult.failure.message, - }) - } - - res.json({ transactions: decryptedResult.data }) - } catch (error) { - console.error('Error fetching transactions:', error) - res.status(500).json({ - error: 'Failed to fetch transactions', - message: error instanceof Error ? error.message : 'Unknown error', - }) - } -} - -// POST /transactions - Create new transaction -export async function createTransaction(req: Request, res: Response) { - try { - const body = req.body as CreateTransactionBody - - // Validate required fields - if ( - !body.accountNumber || - body.amount === undefined || - !body.transactionType - ) { - return res.status(400).json({ - error: 'Missing required fields', - message: 'accountNumber, amount, and transactionType are required', - }) - } - - // Prepare transaction data - const transactionData = { - accountNumber: body.accountNumber, - amount: body.amount, - description: body.description || '', - transactionType: body.transactionType, - status: body.status || 'pending', - } - - // Encrypt the transaction model - const encryptedResult = await protectClient.encryptModel< - typeof transactionData - >(transactionData, transactionsSchema) - - if (encryptedResult.failure) { - return res.status(500).json({ - error: 'Encryption failed', - message: encryptedResult.failure.message, - }) - } - - // Insert encrypted data - const [inserted] = await db - .insert(transactions) - .values(encryptedResult.data) - .returning() - - // Decrypt the inserted record for response - const decryptedResult = await protectClient.decryptModel(inserted) - if (decryptedResult.failure) { - return res.status(500).json({ - error: 'Decryption failed', - message: decryptedResult.failure.message, - }) - } - - res.status(201).json({ transaction: decryptedResult.data }) - } catch (error) { - console.error('Error creating transaction:', error) - res.status(500).json({ - error: 'Failed to create transaction', - message: error instanceof Error ? error.message : 'Unknown error', - }) - } -} - -// GET /transactions/:id - Get single transaction by ID -export async function getTransaction(req: Request, res: Response) { - try { - const id = Number.parseInt(req.params.id, 10) - - if (Number.isNaN(id)) { - return res.status(400).json({ error: 'Invalid transaction ID' }) - } - - const [transaction] = await db - .select() - .from(transactions) - .where(eq(transactions.id, id)) - .limit(1) - - if (!transaction) { - return res.status(404).json({ error: 'Transaction not found' }) - } - - // Decrypt the transaction - const decryptedResult = await protectClient.decryptModel(transaction) - if (decryptedResult.failure) { - return res.status(500).json({ - error: 'Decryption failed', - message: decryptedResult.failure.message, - }) - } - - res.json({ transaction: decryptedResult.data }) - } catch (error) { - console.error('Error fetching transaction:', error) - res.status(500).json({ - error: 'Failed to fetch transaction', - message: error instanceof Error ? error.message : 'Unknown error', - }) - } -} - -// PUT /transactions/:id - Update transaction -export async function updateTransaction(req: Request, res: Response) { - try { - const id = Number.parseInt(req.params.id, 10) - - if (Number.isNaN(id)) { - return res.status(400).json({ error: 'Invalid transaction ID' }) - } - - const body = req.body as UpdateTransactionBody - - // Check if transaction exists - const [existing] = await db - .select() - .from(transactions) - .where(eq(transactions.id, id)) - .limit(1) - - if (!existing) { - return res.status(404).json({ error: 'Transaction not found' }) - } - - // Build update data (only include fields that are provided) - const updateData: Partial = { - updatedAt: new Date(), - } - - if (body.transactionType !== undefined) { - updateData.transactionType = body.transactionType - } - if (body.status !== undefined) { - updateData.status = body.status - } - - // If sensitive fields are being updated, we need to encrypt them - if ( - body.accountNumber !== undefined || - body.amount !== undefined || - body.description !== undefined - ) { - // Decrypt existing transaction to get current values - const decryptedExisting = await protectClient.decryptModel(existing) - if (decryptedExisting.failure) { - return res.status(500).json({ - error: 'Decryption failed', - message: decryptedExisting.failure.message, - }) - } - - // Merge with new values - const mergedData = { - accountNumber: - body.accountNumber ?? decryptedExisting.data.accountNumber, - amount: body.amount ?? decryptedExisting.data.amount, - description: body.description ?? decryptedExisting.data.description, - transactionType: - body.transactionType ?? decryptedExisting.data.transactionType, - status: body.status ?? decryptedExisting.data.status, - } - - // Encrypt the merged data - const encryptedResult = await protectClient.encryptModel( - mergedData, - transactionsSchema, - ) - - if (encryptedResult.failure) { - return res.status(500).json({ - error: 'Encryption failed', - message: encryptedResult.failure.message, - }) - } - - // Merge encrypted fields into update data - Object.assign(updateData, { - accountNumber: encryptedResult.data.accountNumber, - amount: encryptedResult.data.amount, - description: encryptedResult.data.description, - }) - } - - // Update the transaction - const [updated] = await db - .update(transactions) - .set(updateData) - .where(eq(transactions.id, id)) - .returning() - - // Decrypt the updated record for response - const decryptedResult = await protectClient.decryptModel(updated) - if (decryptedResult.failure) { - return res.status(500).json({ - error: 'Decryption failed', - message: decryptedResult.failure.message, - }) - } - - res.json({ transaction: decryptedResult.data }) - } catch (error) { - console.error('Error updating transaction:', error) - res.status(500).json({ - error: 'Failed to update transaction', - message: error instanceof Error ? error.message : 'Unknown error', - }) - } -} - -// DELETE /transactions/:id - Delete transaction -export async function deleteTransaction(req: Request, res: Response) { - try { - const id = Number.parseInt(req.params.id, 10) - - if (Number.isNaN(id)) { - return res.status(400).json({ error: 'Invalid transaction ID' }) - } - - // Check if transaction exists - const [existing] = await db - .select() - .from(transactions) - .where(eq(transactions.id, id)) - .limit(1) - - if (!existing) { - return res.status(404).json({ error: 'Transaction not found' }) - } - - // Delete the transaction - await db.delete(transactions).where(eq(transactions.id, id)) - - res.status(204).send() - } catch (error) { - console.error('Error deleting transaction:', error) - res.status(500).json({ - error: 'Failed to delete transaction', - message: error instanceof Error ? error.message : 'Unknown error', - }) - } -} diff --git a/examples/drizzle/src/db/index.ts b/examples/drizzle/src/db/index.ts deleted file mode 100644 index 838933d4..00000000 --- a/examples/drizzle/src/db/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import 'dotenv/config' -import { drizzle } from 'drizzle-orm/postgres-js' -import postgres from 'postgres' - -const connectionString = process.env.DATABASE_URL -const client = postgres(connectionString) -export const db = drizzle(client) diff --git a/examples/drizzle/src/db/schema.ts b/examples/drizzle/src/db/schema.ts deleted file mode 100644 index 4b34e1c1..00000000 --- a/examples/drizzle/src/db/schema.ts +++ /dev/null @@ -1,25 +0,0 @@ -import 'dotenv/config' -import { encryptedType } from '@cipherstash/drizzle/pg' -import { pgTable, serial, timestamp, varchar } from 'drizzle-orm/pg-core' - -export const transactions = pgTable('transactions', { - id: serial('id').primaryKey(), - // Encrypted sensitive fields - accountNumber: encryptedType('account_number', { - freeTextSearch: true, - equality: true, - }), - amount: encryptedType('amount', { - dataType: 'number', - equality: true, - orderAndRange: true, - }), - description: encryptedType('description', { - freeTextSearch: true, - }), - // Non-sensitive fields - transactionType: varchar('transaction_type', { length: 50 }).notNull(), - status: varchar('status', { length: 20 }).notNull().default('pending'), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}) diff --git a/examples/drizzle/src/protect/config.ts b/examples/drizzle/src/protect/config.ts deleted file mode 100644 index 05482ba0..00000000 --- a/examples/drizzle/src/protect/config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import 'dotenv/config' -import { - createProtectOperators, - extractProtectSchema, -} from '@cipherstash/drizzle/pg' -import { protect } from '@cipherstash/protect' -import { transactions } from '../db/schema' - -// Extract Protect.js schema from Drizzle table -export const transactionsSchema = extractProtectSchema(transactions) - -// Initialize Protect.js client -export const protectClient = await protect({ - schemas: [transactionsSchema], -}) - -// Create Protect operators for encrypted field queries -export const protectOps = createProtectOperators(protectClient) diff --git a/examples/drizzle/src/routes/transactions.ts b/examples/drizzle/src/routes/transactions.ts deleted file mode 100644 index 4c06c321..00000000 --- a/examples/drizzle/src/routes/transactions.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Router } from 'express' -import { - createTransaction, - deleteTransaction, - getTransaction, - getTransactions, - updateTransaction, -} from '../controllers/transactions' - -export const transactionsRouter = Router() - -transactionsRouter.get('/', getTransactions) -transactionsRouter.post('/', createTransaction) -transactionsRouter.get('/:id', getTransaction) -transactionsRouter.put('/:id', updateTransaction) -transactionsRouter.delete('/:id', deleteTransaction) diff --git a/examples/drizzle/src/server.ts b/examples/drizzle/src/server.ts deleted file mode 100644 index 331b21f2..00000000 --- a/examples/drizzle/src/server.ts +++ /dev/null @@ -1,39 +0,0 @@ -import 'dotenv/config' -import express, { - type Request, - type Response, - type NextFunction, -} from 'express' -import { transactionsRouter } from './routes/transactions' - -const app = express() -const PORT = process.env.PORT || 3000 - -// Middleware -app.use(express.json()) - -// Health check endpoint -app.get('/health', (_req: Request, res: Response) => { - res.status(200).json({ status: 'ok', message: 'Server is running' }) -}) - -// Routes -app.use('/transactions', transactionsRouter) - -// Error handling middleware -app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { - console.error('Error:', err) - res.status(500).json({ - error: 'Internal server error', - message: err.message, - }) -}) - -// 404 handler -app.use((_req: Request, res: Response) => { - res.status(404).json({ error: 'Not found' }) -}) - -app.listen(PORT, () => { - console.log(`Server is running on http://localhost:${PORT}`) -}) diff --git a/examples/drizzle/tsconfig.json b/examples/drizzle/tsconfig.json deleted file mode 100644 index 238655f2..00000000 --- a/examples/drizzle/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - // Enable latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} diff --git a/examples/dynamo/.gitignore b/examples/dynamo/.gitignore deleted file mode 100644 index c99105a3..00000000 --- a/examples/dynamo/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -docker -sql/cipherstash-encrypt.sql diff --git a/examples/dynamo/CHANGELOG.md b/examples/dynamo/CHANGELOG.md deleted file mode 100644 index 05e10c00..00000000 --- a/examples/dynamo/CHANGELOG.md +++ /dev/null @@ -1,155 +0,0 @@ -# @cipherstash/dynamo-example - -## 0.2.18 - -### Patch Changes - -- Updated dependencies [db72e2c] -- Updated dependencies [e769740] - - @cipherstash/protect@10.5.0 - - @cipherstash/protect-dynamodb@9.0.0 - -## 0.2.17 - -### Patch Changes - -- Updated dependencies [9ccaf68] - - @cipherstash/protect@10.4.0 - - @cipherstash/protect-dynamodb@8.0.0 - -## 0.2.16 - -### Patch Changes - -- Updated dependencies [a1fce2b] -- Updated dependencies [622b684] - - @cipherstash/protect@10.3.0 - - @cipherstash/protect-dynamodb@7.0.0 - -## 0.2.15 - -### Patch Changes - -- @cipherstash/protect@10.2.1 -- @cipherstash/protect-dynamodb@6.0.1 - -## 0.2.14 - -### Patch Changes - -- Updated dependencies [de029de] - - @cipherstash/protect@10.2.0 - - @cipherstash/protect-dynamodb@6.0.0 - -## 0.2.13 - -### Patch Changes - -- Updated dependencies [ff4421f] - - @cipherstash/protect@10.1.1 - - @cipherstash/protect-dynamodb@5.1.1 - -## 0.2.12 - -### Patch Changes - -- Updated dependencies [6b87c17] - - @cipherstash/protect@10.1.0 - - @cipherstash/protect-dynamodb@6.0.0 - -## 0.2.11 - -### Patch Changes - -- @cipherstash/protect@10.0.2 -- @cipherstash/protect-dynamodb@5.0.2 - -## 0.2.10 - -### Patch Changes - -- @cipherstash/protect@10.0.1 -- @cipherstash/protect-dynamodb@5.0.1 - -## 0.2.9 - -### Patch Changes - -- Updated dependencies [788dbfc] - - @cipherstash/protect-dynamodb@5.0.0 - - @cipherstash/protect@10.0.0 - -## 0.2.8 - -### Patch Changes - -- Updated dependencies [c7ed7ab] -- Updated dependencies [211e979] - - @cipherstash/protect@9.6.0 - - @cipherstash/protect-dynamodb@4.0.0 - -## 0.2.7 - -### Patch Changes - -- Updated dependencies [6f45b02] - - @cipherstash/protect-dynamodb@3.0.0 - - @cipherstash/protect@9.5.0 - -## 0.2.6 - -### Patch Changes - -- @cipherstash/protect@9.4.1 -- @cipherstash/protect-dynamodb@2.0.1 - -## 0.2.5 - -### Patch Changes - -- Updated dependencies [1cc4772] - - @cipherstash/protect@9.4.0 - - @cipherstash/protect-dynamodb@2.0.0 - -## 0.2.4 - -### Patch Changes - -- Updated dependencies [01fed9e] - - @cipherstash/protect-dynamodb@1.0.0 - - @cipherstash/protect@9.3.0 - -## 0.2.3 - -### Patch Changes - -- Updated dependencies [2b63ee1] -- Updated dependencies [e33fbaf] - - @cipherstash/protect-dynamodb@0.3.0 - -## 0.2.2 - -### Patch Changes - -- Updated dependencies [587f222] - - @cipherstash/protect@9.2.0 - - @cipherstash/protect-dynamodb@0.2.0 - -## 0.2.1 - -### Patch Changes - -- Updated dependencies [5fc0150] - - @cipherstash/protect-dynamodb@0.2.0 - -## 0.2.0 - -### Minor Changes - -- c8468ee: Released initial version of the DynamoDB helper interface. - -### Patch Changes - -- Updated dependencies [c8468ee] - - @cipherstash/protect-dynamodb@1.0.0 - - @cipherstash/protect@9.1.0 diff --git a/examples/dynamo/README.md b/examples/dynamo/README.md deleted file mode 100644 index 6b5f8d63..00000000 --- a/examples/dynamo/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# DynamoDB Examples - -Examples of using Protect.js with DynamoDB. - -## Prereqs -- [Node.js](https://nodejs.org/en) (tested with v22.11.0) -- [pnpm](https://pnpm.io/) (tested with v9.15.3) -- [Docker](https://www.docker.com/) -- a CipherStash account and [credentials configured](../../README.md#configuration) - -## Setup - -Install the workspace dependencies and build Protect.js: -``` -# change to the workspace root directory -cd ../.. - -pnpm install -pnpm run build -``` - -Switch back to the DynamoDB examples -``` -cd examples/dynamo -``` - -Start Docker services used by the DynamoDB examples: -``` -docker compose up --detach -``` - -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): -``` -pnpm run eql:download -pnpm run eql:install -``` - -## Examples - -All examples run as scripts from [`package.json`](./package.json). -You can run an example with the command `pnpm run [example_name]`. - -Each example runs against local DynamoDB in Docker. - -- `simple` - - `pnpm run simple` - - Round trip encryption/decryption through DynamoDB (no search on encrypted attributes). -- `encrypted-partition-key` - - `pnpm run encrypted-partition-key` - - Uses an encrypted attribute as a partition key. -- `encrypted-sort-key` - - `pnpm run encrypted-sort-key` - - Similar to the `encrypted-partition-key` example, but uses an encrypted attribute as a sort key instead. -- `encrypted-key-in-gsi` - - `pnpm run encrypted-key-in-gsi` - - Uses an encrypted attribute as the partition key in a global secondary index. - The source ciphertext is projected into the index for decryption after querying the index. -- `export-to-pg` - - `pnpm run export-to-pg` - - Encrypts an item, puts it in Dynamo, exports it to Postgres, and decrypts a result from Postgres. diff --git a/examples/dynamo/docker-compose.yml b/examples/dynamo/docker-compose.yml deleted file mode 100644 index 2e95b4fa..00000000 --- a/examples/dynamo/docker-compose.yml +++ /dev/null @@ -1,41 +0,0 @@ - -services: - dynamodb-local: - command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" - image: "amazon/dynamodb-local:latest" - container_name: dynamodb-local - ports: - - "8000:8000" - volumes: - - "./docker/dynamodb:/home/dynamodblocal/data" - working_dir: /home/dynamodblocal - - dynamodb-admin: - image: aaronshaf/dynamodb-admin - ports: - - 8001:8001 - environment: - DYNAMO_ENDPOINT: http://dynamodb-local:8000 - - # used by export-to-pg example - postgres: - image: postgres:latest - environment: - PGPORT: 5432 - POSTGRES_DB: "cipherstash" - POSTGRES_USER: "cipherstash" - PGUSER: "cipherstash" - POSTGRES_PASSWORD: password - ports: - - 5433:5432 - deploy: - resources: - limits: - cpus: "${CPU_LIMIT:-2}" - memory: 2048mb - restart: always - healthcheck: - test: [ "CMD-SHELL", "pg_isready" ] - interval: 1s - timeout: 5s - retries: 10 diff --git a/examples/dynamo/package.json b/examples/dynamo/package.json deleted file mode 100644 index d8d7d8ca..00000000 --- a/examples/dynamo/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@cipherstash/dynamo-example", - "private": true, - "version": "0.2.18", - "type": "module", - "scripts": { - "simple": "tsx src/simple.ts", - "bulk-operations": "tsx src/bulk-operations.ts", - "encrypted-partition-key": "tsx src/encrypted-partition-key.ts", - "encrypted-sort-key": "tsx src/encrypted-sort-key.ts", - "encrypted-key-in-gsi": "tsx src/encrypted-key-in-gsi.ts", - "export-to-pg": "tsx src/export-to-pg.ts", - "eql:download": "curl -sLo sql/cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/download/eql-2.0.2/cipherstash-encrypt.sql", - "eql:install": "cat sql/cipherstash-encrypt.sql | docker exec -i dynamo-postgres-1 psql postgresql://cipherstash:password@postgres:5432/cipherstash -f-" - }, - "keywords": [], - "author": "", - "license": "ISC", - "description": "", - "dependencies": { - "@aws-sdk/client-dynamodb": "^3.817.0", - "@aws-sdk/lib-dynamodb": "^3.817.0", - "@aws-sdk/util-dynamodb": "^3.817.0", - "@cipherstash/protect": "workspace:*", - "@cipherstash/protect-dynamodb": "workspace:*", - "pg": "^8.13.1" - }, - "devDependencies": { - "@types/pg": "^8.11.10", - "tsx": "catalog:repo", - "typescript": "catalog:repo" - } -} diff --git a/examples/dynamo/sql/.gitkeep b/examples/dynamo/sql/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/dynamo/src/bulk-operations.ts b/examples/dynamo/src/bulk-operations.ts deleted file mode 100644 index 2973b679..00000000 --- a/examples/dynamo/src/bulk-operations.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { BatchGetCommand, BatchWriteCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' -import { createTable, docClient, dynamoClient } from './common/dynamo' -import { log } from './common/log' -import { protectClient, users } from './common/protect' - -const tableName = 'UsersBulkOperations' - -type User = { - pk: string - email: string -} - -const main = async () => { - await createTable({ - TableName: tableName, - AttributeDefinitions: [ - { - AttributeName: 'pk', - AttributeType: 'S', - }, - ], - KeySchema: [ - { - AttributeName: 'pk', - KeyType: 'HASH', - }, - ], - }) - - const protectDynamo = protectDynamoDB({ - protectClient, - }) - - const items = [ - { - // `pk` won't be encrypted because it's not included in the `users` protected table schema. - pk: 'user#1', - // `email` will be encrypted because it's included in the `users` protected table schema. - email: 'abc@example.com', - }, - { - pk: 'user#2', - email: 'def@example.com', - }, - ] - - const encryptResult = await protectDynamo.bulkEncryptModels(items, users) - - if (encryptResult.failure) { - throw new Error(`Failed to encrypt items: ${encryptResult.failure.message}`) - } - - const putRequests = encryptResult.data.map( - (item: Record) => ({ - PutRequest: { - Item: item, - }, - }), - ) - - log('encrypted items', encryptResult) - - const batchWriteCommand = new BatchWriteCommand({ - RequestItems: { - [tableName]: putRequests, - }, - }) - - await dynamoClient.send(batchWriteCommand) - - const batchGetCommand = new BatchGetCommand({ - RequestItems: { - [tableName]: { - Keys: [{ pk: 'user#1' }, { pk: 'user#2' }], - }, - }, - }) - - const getResult = await docClient.send(batchGetCommand) - - const decryptedItems = await protectDynamo.bulkDecryptModels( - getResult.Responses?.[tableName], - users, - ) - - log('decrypted items', decryptedItems) -} - -main() diff --git a/examples/dynamo/src/common/dynamo.ts b/examples/dynamo/src/common/dynamo.ts deleted file mode 100644 index 70a77f4f..00000000 --- a/examples/dynamo/src/common/dynamo.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - CreateTableCommand, - type CreateTableCommandInput, - DynamoDBClient, -} from '@aws-sdk/client-dynamodb' -import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb' - -export const dynamoClient = new DynamoDBClient({ - credentials: { - accessKeyId: 'fakeAccessKeyId', - secretAccessKey: 'fakeSecretAccessKey', - }, - endpoint: 'http://localhost:8000', -}) - -export const docClient = DynamoDBDocumentClient.from(dynamoClient) - -// Creates a table with provisioned throughput set to 5 RCU and 5 WCU. -// Ignores `ResourceInUseException`s if the table already exists. -export async function createTable( - input: Omit, -) { - const command = new CreateTableCommand({ - ProvisionedThroughput: { - ReadCapacityUnits: 5, - WriteCapacityUnits: 5, - }, - ...input, - }) - - try { - await docClient.send(command) - } catch (err) { - if (err?.name !== 'ResourceInUseException') { - throw err - } - } -} diff --git a/examples/dynamo/src/common/log.ts b/examples/dynamo/src/common/log.ts deleted file mode 100644 index 367d5806..00000000 --- a/examples/dynamo/src/common/log.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function log(description: string, data: unknown) { - console.log(`\n${description}:\n${JSON.stringify(data, null, 2)}`) -} diff --git a/examples/dynamo/src/common/protect.ts b/examples/dynamo/src/common/protect.ts deleted file mode 100644 index 78441f07..00000000 --- a/examples/dynamo/src/common/protect.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { csColumn, csTable, protect } from '@cipherstash/protect' - -export const users = csTable('users', { - email: csColumn('email').equality(), -}) - -export const protectClient = await protect({ - schemas: [users], -}) diff --git a/examples/dynamo/src/encrypted-key-in-gsi.ts b/examples/dynamo/src/encrypted-key-in-gsi.ts deleted file mode 100644 index 9d50d949..00000000 --- a/examples/dynamo/src/encrypted-key-in-gsi.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' -import { createTable, docClient, dynamoClient } from './common/dynamo' -import { log } from './common/log' -import { protectClient, users } from './common/protect' - -const tableName = 'UsersEncryptedKeyInGSI' -const indexName = 'EmailIndex' - -type User = { - pk: string - email: string -} - -const main = async () => { - await createTable({ - TableName: tableName, - AttributeDefinitions: [ - { - AttributeName: 'pk', - AttributeType: 'S', - }, - { - AttributeName: 'email__hmac', - AttributeType: 'S', - }, - ], - KeySchema: [ - { - AttributeName: 'pk', - KeyType: 'HASH', - }, - ], - GlobalSecondaryIndexes: [ - { - IndexName: indexName, - KeySchema: [{ AttributeName: 'email__hmac', KeyType: 'HASH' }], - Projection: { - ProjectionType: 'INCLUDE', - NonKeyAttributes: ['email__source'], - }, - ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, - }, - ], - }) - - const protectDynamo = protectDynamoDB({ - protectClient, - }) - - const user = { - // `pk` won't be encrypted because it's not included in the `users` protected table schema. - pk: 'user#1', - // `email` will be encrypted because it's included in the `users` protected table schema. - email: 'abc@example.com', - } - - const encryptResult = await protectDynamo.encryptModel(user, users) - - log('encrypted item', encryptResult) - - const putCommand = new PutCommand({ - TableName: tableName, - Item: encryptResult, - }) - - await dynamoClient.send(putCommand) - - // Use encryptQuery to create the search term for GSI query - const encryptedResult = await protectClient.encryptQuery([ - { - value: 'abc@example.com', - column: users.email, - table: users, - queryType: 'equality', - }, - ]) - - if (encryptedResult.failure) { - throw new Error( - `Failed to encrypt query: ${encryptedResult.failure.message}`, - ) - } - - // Extract the HMAC for DynamoDB key lookup - const encryptedEmail = encryptedResult.data[0] - if (!encryptedEmail) { - throw new Error('Failed to encrypt query: no result returned') - } - const emailHmac = encryptedEmail.hm - - const queryCommand = new QueryCommand({ - TableName: tableName, - IndexName: indexName, - KeyConditionExpression: 'email__hmac = :e', - ExpressionAttributeValues: { - ':e': emailHmac, - }, - Limit: 1, - }) - - const queryResult = await docClient.send(queryCommand) - - if (!queryResult.Items?.[0]) { - throw new Error('Item not found') - } - - const decryptedItem = await protectDynamo.decryptModel( - queryResult.Items[0], - users, - ) - - log('decrypted item', decryptedItem) -} - -main() diff --git a/examples/dynamo/src/encrypted-partition-key.ts b/examples/dynamo/src/encrypted-partition-key.ts deleted file mode 100644 index 183f119f..00000000 --- a/examples/dynamo/src/encrypted-partition-key.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' -import { createTable, docClient } from './common/dynamo' -import { log } from './common/log' -import { protectClient, users } from './common/protect' - -const tableName = 'UsersEncryptedPartitionKey' - -type User = { - email: string -} - -const main = async () => { - await createTable({ - TableName: tableName, - AttributeDefinitions: [ - { - AttributeName: 'email__hmac', - AttributeType: 'S', - }, - ], - KeySchema: [ - { - AttributeName: 'email__hmac', - KeyType: 'HASH', - }, - ], - }) - - const protectDynamo = protectDynamoDB({ - protectClient, - }) - - const user = { - // `email` will be encrypted because it's included in the `users` protected table schema. - email: 'abc@example.com', - // `somePlaintextAttr` won't be encrypted because it's not in the protected table schema. - somePlaintextAttr: 'abc', - } - - const encryptResult = await protectDynamo.encryptModel(user, users) - - log('encrypted item', encryptResult) - - const putCommand = new PutCommand({ - TableName: tableName, - Item: encryptResult, - }) - - await docClient.send(putCommand) - - // Use encryptQuery to create the search term for partition key lookup - const encryptedResult = await protectClient.encryptQuery([ - { - value: 'abc@example.com', - column: users.email, - table: users, - queryType: 'equality', - }, - ]) - - if (encryptedResult.failure) { - throw new Error( - `Failed to encrypt query: ${encryptedResult.failure.message}`, - ) - } - - // Extract the HMAC for DynamoDB key lookup - const encryptedEmail = encryptedResult.data[0] - if (!encryptedEmail) { - throw new Error('Failed to encrypt query: no result returned') - } - const emailHmac = encryptedEmail.hm - - const getCommand = new GetCommand({ - TableName: tableName, - Key: { email__hmac: emailHmac }, - }) - - const getResult = await docClient.send(getCommand) - - const decryptedItem = await protectDynamo.decryptModel( - getResult.Item, - users, - ) - - log('decrypted item', decryptedItem) -} - -main() diff --git a/examples/dynamo/src/encrypted-sort-key.ts b/examples/dynamo/src/encrypted-sort-key.ts deleted file mode 100644 index b868ea45..00000000 --- a/examples/dynamo/src/encrypted-sort-key.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' -import { createTable, docClient, dynamoClient } from './common/dynamo' -import { log } from './common/log' -import { protectClient, users } from './common/protect' - -const tableName = 'UsersEncryptedSortKey' - -type User = { - pk: string - email: string -} - -const main = async () => { - await createTable({ - TableName: tableName, - AttributeDefinitions: [ - { - AttributeName: 'pk', - AttributeType: 'S', - }, - { - AttributeName: 'email__hmac', - AttributeType: 'S', - }, - ], - KeySchema: [ - { - AttributeName: 'pk', - KeyType: 'HASH', - }, - { - AttributeName: 'email__hmac', - KeyType: 'RANGE', - }, - ], - }) - - const protectDynamo = protectDynamoDB({ - protectClient, - }) - - const user = { - // `pk` won't be encrypted because it's not in the protected table schema. - pk: 'user#1', - // `email` will be encrypted because it's included in the `users` protected table schema. - email: 'abc@example.com', - } - - const encryptResult = await protectDynamo.encryptModel(user, users) - - log('encrypted item', encryptResult) - - const putCommand = new PutCommand({ - TableName: tableName, - Item: encryptResult, - }) - - await docClient.send(putCommand) - - // Use encryptQuery to create the search term for sort key range query - const encryptedResult = await protectClient.encryptQuery([ - { - value: 'abc@example.com', - column: users.email, - table: users, - queryType: 'equality', - }, - ]) - - if (encryptedResult.failure) { - throw new Error( - `Failed to encrypt query: ${encryptedResult.failure.message}`, - ) - } - - // Extract the HMAC for DynamoDB key lookup - const encryptedEmail = encryptedResult.data[0] - if (!encryptedEmail) { - throw new Error('Failed to encrypt query: no result returned') - } - const emailHmac = encryptedEmail.hm - - const getCommand = new GetCommand({ - TableName: tableName, - Key: { pk: 'user#1', email__hmac: emailHmac }, - }) - - const getResult = await docClient.send(getCommand) - - if (!getResult.Item) { - throw new Error('Item not found') - } - - const decryptedItem = await protectDynamo.decryptModel( - getResult.Item, - users, - ) - - log('decrypted item', decryptedItem) -} - -main() diff --git a/examples/dynamo/src/export-to-pg.ts b/examples/dynamo/src/export-to-pg.ts deleted file mode 100644 index 7ecf5c80..00000000 --- a/examples/dynamo/src/export-to-pg.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' -import pg from 'pg' -// Insert data in dynamo, scan it back out, insert/copy into PG, query from PG. -import { createTable, docClient, dynamoClient } from './common/dynamo' -import { log } from './common/log' -import { protectClient, users } from './common/protect' -const PgClient = pg.Client - -const tableName = 'UsersExportToPG' - -type User = { - pk: string - email: string -} - -const main = async () => { - await createTable({ - TableName: tableName, - AttributeDefinitions: [ - { - AttributeName: 'pk', - AttributeType: 'S', - }, - ], - KeySchema: [ - { - AttributeName: 'pk', - KeyType: 'HASH', - }, - ], - }) - - const protectDynamo = protectDynamoDB({ - protectClient, - }) - - const user = { - // `pk` won't be encrypted because it's not included in the `users` protected table schema. - pk: 'user#1', - // `email` will be encrypted because it's included in the `users` protected table schema. - email: 'abc@example.com', - } - - const encryptResult = await protectDynamo.encryptModel(user, users) - - const putCommand = new PutCommand({ - TableName: tableName, - Item: encryptResult, - }) - - await dynamoClient.send(putCommand) - - const scanCommand = new ScanCommand({ - TableName: tableName, - }) - - // This example uses a single scan for simplicity, but this could use streams, a paginated scans, etc. - const scanResult = await docClient.send(scanCommand) - - log('scan items (encrypted)', scanResult.Items) - - const pgClient = new PgClient({ - port: 5433, - database: 'cipherstash', - user: 'cipherstash', - password: 'password', - }) - - await pgClient.connect() - - await pgClient.query(` - CREATE TABLE IF NOT EXISTS users ( - id INT PRIMARY KEY, - email eql_v2_encrypted - ) - `) - - try { - await pgClient.query( - "SELECT eql_v2.add_encrypted_constraint('users', 'email')", - ) - } catch (err) { - if ( - (err as Error).message !== - 'constraint "eql_v2_encrypted_check_email" for relation "users" already exists' - ) { - throw err - } - } - - if (!scanResult.Items) { - throw new Error('No items found in scan result') - } - - // TODO: this logic belongs in Protect (or in common/protect.ts for the prototype) - const formattedForPgInsert = scanResult.Items.reduce( - (recordsToInsert, currentItem) => { - const idAsText = currentItem.pk.slice('user#'.length) - - const emailAsText = JSON.stringify({ - c: currentItem.email__source, - bf: null, - hm: currentItem.email__hmac, - i: { c: 'email', t: 'users' }, - k: 'ct', - ob: null, - v: 2, - }) - - recordsToInsert[0].push(idAsText) - recordsToInsert[1].push(emailAsText) - - return recordsToInsert - }, - [[], []] as [string[], string[]], - ) - - const insertResult = await pgClient.query( - ` - INSERT INTO users(id, email) - SELECT * FROM UNNEST($1::int[], $2::jsonb[]) - ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email - RETURNING id, email::jsonb - `, - [formattedForPgInsert[0], formattedForPgInsert[1]], - ) - - log('inserted rows', insertResult.rows) - - const decryptRowsResult = await protectClient.bulkDecryptModels( - insertResult.rows, - ) - - if (decryptRowsResult.failure) { - throw new Error(decryptRowsResult.failure.message) - } - - log('decrypted rows', decryptRowsResult.data) - - pgClient.end() -} - -main() diff --git a/examples/dynamo/src/simple.ts b/examples/dynamo/src/simple.ts deleted file mode 100644 index 999ffa35..00000000 --- a/examples/dynamo/src/simple.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' -import { protectDynamoDB } from '@cipherstash/protect-dynamodb' -import { createTable, docClient, dynamoClient } from './common/dynamo' -import { log } from './common/log' -import { protectClient, users } from './common/protect' - -const tableName = 'UsersSimple' - -type User = { - pk: string - email: string -} - -const main = async () => { - await createTable({ - TableName: tableName, - AttributeDefinitions: [ - { - AttributeName: 'pk', - AttributeType: 'S', - }, - ], - KeySchema: [ - { - AttributeName: 'pk', - KeyType: 'HASH', - }, - ], - }) - - const protectDynamo = protectDynamoDB({ - protectClient, - }) - - const user = { - // `pk` won't be encrypted because it's not included in the `users` protected table schema. - pk: 'user#1', - // `email` will be encrypted because it's included in the `users` protected table schema. - email: 'abc@example.com', - } - - const encryptResult = await protectDynamo.encryptModel(user, users) - - log('encrypted item', encryptResult) - - const putCommand = new PutCommand({ - TableName: tableName, - Item: encryptResult, - }) - - await dynamoClient.send(putCommand) - - const getCommand = new GetCommand({ - TableName: tableName, - Key: { pk: 'user#1' }, - }) - - const getResult = await docClient.send(getCommand) - - const decryptedItem = await protectDynamo.decryptModel( - getResult.Item, - users, - ) - - log('decrypted item', decryptedItem) -} - -main() diff --git a/examples/hono-supabase/.env.example b/examples/hono-supabase/.env.example deleted file mode 100644 index f8378c2e..00000000 --- a/examples/hono-supabase/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -# Credentials for your CipherStash project -CS_CLIENT_ID= -CS_CLIENT_KEY= -CS_CLIENT_ACCESS_KEY= -CS_WORKSPACE_ID= - -# Connection details for your Supabase project -SUPABASE_URL= -SUPABASE_ANON_KEY= diff --git a/examples/hono-supabase/.gitignore b/examples/hono-supabase/.gitignore deleted file mode 100644 index 36fabb6c..00000000 --- a/examples/hono-supabase/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -# dev -.yarn/ -!.yarn/releases -.vscode/* -!.vscode/launch.json -!.vscode/*.code-snippets -.idea/workspace.xml -.idea/usage.statistics.xml -.idea/shelf - -# deps -node_modules/ - -# env -.env -.env.production - -# logs -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -# misc -.DS_Store diff --git a/examples/hono-supabase/README.md b/examples/hono-supabase/README.md deleted file mode 100644 index 0093c327..00000000 --- a/examples/hono-supabase/README.md +++ /dev/null @@ -1,162 +0,0 @@ -# CipherStash JSEQL + Supabase + Hono Example - -This project demonstrates how to encrypt data using [@cipherstash/protect](https://www.npmjs.com/package/@cipherstash/protect) before storing it in a [Supabase](https://supabase.com/) Postgres database. It uses [Hono](https://hono.dev/) to create a minimal RESTful API, showcasing how to seamlessly integrate field-level encryption into a typical web application workflow. - -## Table of Contents -- [Overview](#overview) -- [Prerequisites](#prerequisites) -- [Getting Started](#getting-started) - - [1. Clone the repository](#1-clone-the-repository) - - [2. Install dependencies](#2-install-dependencies) - - [3. Set up environment variables](#3-set-up-environment-variables) - - [4. Create the `users` table in Supabase](#4-create-the-users-table-in-supabase) - - [5. Run the application](#5-run-the-application) -- [API Endpoints](#api-endpoints) - - [GET /users](#get-users) - - [POST /users](#post-users) -- [Additional Resources](#additional-resources) -- [License](#license) - ---- - -## Overview - -**What does this example show?** -1. **Encrypting data** with [@cipherstash/protect](https://www.npmjs.com/package/@cipherstash/protect). -2. **Storing encrypted data** in a Postgres database (using Supabase). -3. **Retrieving and decrypting** that data in a minimal Hono-based REST API. - -**Why is this useful?** -- You get strong, field-level encryption on sensitive data (like user emails) before it even hits your database. -- You can keep your API and database interactions almost the same, with only minor changes to handle encryption/decryption. - ---- - -## Prerequisites - -1. **Node.js** v18+ or v20+ -2. **Supabase account** -3. A **CipherStash account** (to acquire the required JSEQL credentials) -4. A `.env` file with the following environment variables: - - `SUPABASE_URL` - - `SUPABASE_ANON_KEY` - - `CS_CLIENT_ID` - - `CS_CLIENT_KEY` - - `CS_CLIENT_ACCESS_KEY` - - `CS_WORKSPACE_ID` - ---- - -## Getting Started - -### 1. Clone the repository - -```bash -git clone https://github.com/cipherstash/jsprotect.git -cd jsprotect/examples/hono-supabase -``` - -### 2. Install dependencies - -```bash -npm install -``` - -### 3. Set up environment variables - -Create a `.env` file in the root directory (if not already present) and fill in your keys and IDs: - -```bash -SUPABASE_URL=https://your-project.supabase.co -SUPABASE_ANON_KEY=your-supabase-anon-key - -CS_CLIENT_ID=your-cs-client-id -CS_CLIENT_KEY=your-cs-client-key -CS_CLIENT_ACCESS_KEY=your-cs-client-access-key -CS_WORKSPACE_ID=your-cs-workspace-id -``` - -> **Note**: Keep these values **secret**. Never commit them to a public repo. - -### 4. Create the `users` table in Supabase - -In your Supabase project, create the following table: - -```sql -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - email jsonb NOT NULL, - name VARCHAR(255) NOT NULL, - role VARCHAR(255) NOT NULL -); -``` - -- The `email` field is of type `jsonb` to accommodate the encrypted data structure. - -### 5. Run the application - -```bash -npm run dev -``` - -This will start the server at [http://localhost:3000](http://localhost:3000). - ---- - -## API Endpoints - -### GET /users -Retrieves a list of all users from the `users` table. -- **Endpoint**: `GET http://localhost:3000/users` -- **Response**: Returns an array of users with their `email` fields **decrypted**. - -#### Example Response - -```json -{ - "users": [ - { - "id": 1, - "email": "alice@example.com", - "name": "Alice", - "role": "admin" - }, - { - "id": 2, - "email": "bob@example.com", - "name": "Bob", - "role": "admin" - } - ] -} -``` - -### POST /users -Creates a new user with an **encrypted** email field. -- **Endpoint**: `POST http://localhost:3000/users` -- **Request Body**: - ```json - { - "email": "alice@example.com", - "name": "Alice" - } - ``` -- **Response**: - ```json - { - "message": "User created successfully" - } - ``` - - If the creation fails, you’ll get: - ```json - { - "message": "User creation failed. Please check the logs" - } - ``` - -## Additional Resources - -- [@cipherstash/protect Documentation](https://github.com/cipherstash/protectjs) -- [Hono Framework](https://hono.dev/) -- [Supabase Documentation](https://supabase.com/docs) \ No newline at end of file diff --git a/examples/hono-supabase/environment.d.ts b/examples/hono-supabase/environment.d.ts deleted file mode 100644 index 370cad9d..00000000 --- a/examples/hono-supabase/environment.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare namespace NodeJS { - interface ProcessEnv { - SUPABASE_URL: string - SUPABASE_ANON_KEY: string - CS_CLIENT_ID: string - CS_CLIENT_KEY: string - CS_WORKSPACE_ID: string - CS_CLIENT_ACCESS_KEY: string - } -} diff --git a/examples/hono-supabase/package.json b/examples/hono-supabase/package.json deleted file mode 100644 index 079343c7..00000000 --- a/examples/hono-supabase/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "hono-supabase", - "private": true, - "type": "module", - "scripts": { - "dev": "tsx watch src/index.ts" - }, - "dependencies": { - "@cipherstash/protect": "workspace:*", - "@hono/node-server": "^1.13.7", - "@supabase/supabase-js": "^2.47.10", - "dotenv": "^16.4.7", - "hono": "^4.11.9" - }, - "devDependencies": { - "@types/node": "^20.11.17", - "tsx": "catalog:repo", - "typescript": "catalog:repo" - } -} diff --git a/examples/hono-supabase/src/index.ts b/examples/hono-supabase/src/index.ts deleted file mode 100644 index 8f5bef13..00000000 --- a/examples/hono-supabase/src/index.ts +++ /dev/null @@ -1,123 +0,0 @@ -import 'dotenv/config' -import { serve } from '@hono/node-server' -import { createClient } from '@supabase/supabase-js' -import { Hono } from 'hono' - -// Consolidated protect and it's schemas into a single file -import { - type ProtectClientConfig, - csColumn, - csTable, - protect, -} from '@cipherstash/protect' - -export const users = csTable('users', { - email: csColumn('email'), -}) - -const config: ProtectClientConfig = { - schemas: [users], -} - -export const protectClient = await protect(config) - -// Create a single supabase client for interacting with the database -const supabaseUrl = process.env.SUPABASE_URL -const supabaseKey = process.env.SUPABASE_ANON_KEY - -// This example expects the following table in your Supabase database. -// The email field is the only field that will be encrypted and the required column type is jsonb. -// --- -// CREATE TABLE users ( -// id SERIAL PRIMARY KEY, -// email jsonb NOT NULL, -// name VARCHAR(255) NOT NULL, -// role VARCHAR(255) NOT NULL -// ); -export const supabase = createClient(supabaseUrl, supabaseKey) - -const app = new Hono() - -app.get('/users', async (c) => { - const { data: users } = await supabase.from('users').select() - - if (users && users.length > 1) { - const decryptedusers = await Promise.all( - users.map(async (user) => { - // The encrypted data is stored in the EQL format: { c: 'ciphertext' } - // and the decrypt function expects the data to be in this format. - const decryptResult = await protectClient.decrypt(user.email) - - if (decryptResult.failure) { - console.error( - 'Failed to decrypt the email for user', - user.id, - decryptResult.failure.message, - ) - - return user - } - - const plaintext = decryptResult.data - return { ...user, email: plaintext } - }), - ) - - return c.json({ users: decryptedusers }) - } - - return c.json({ users: [] }) -}) - -app.post('/users', async (c) => { - const { email, name } = await c.req.json() - - if (!email || !name) { - return c.json( - { message: 'Email and name are required to create a users' }, - 400, - ) - } - - // The encrypt function expects the plaintext to be of type string - // and the second argument to be an object with the table and column - // names of the table where you are storing the data. - const encryptedResult = await protectClient.encrypt(email, { - column: users.email, - table: users, - }) - - if (encryptedResult.failure) { - console.error( - 'Failed to encrypt the email', - encryptedResult.failure.message, - ) - return c.json({ message: 'Failed to encrypt the email' }, 500) - } - - const encryptedEmail = encryptedResult.data - - console.log( - 'Encrypted email that will be stored in the database:', - encryptedEmail, - ) - - const result = await supabase - .from('users') - .insert({ email: encryptedEmail, name, role: 'admin' }) - - if (result.statusText === 'Created') { - return c.json({ message: 'User created successfully' }) - } - - console.error('User creation failed:', result) - return c.json({ message: 'User creation failed. Please check the logs' }, 500) -}) - -const port = 3000 -console.log(`Server is running on http://localhost:${port}`) - -serve({ - fetch: app.fetch, - port, -}) diff --git a/examples/hono-supabase/tsconfig.json b/examples/hono-supabase/tsconfig.json deleted file mode 100644 index ad959210..00000000 --- a/examples/hono-supabase/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "NodeNext", - "strict": true, - "verbatimModuleSyntax": true, - "skipLibCheck": true, - "types": ["node"], - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" - } -} diff --git a/examples/nest/.gitignore b/examples/nest/.gitignore deleted file mode 100644 index 4b56acfb..00000000 --- a/examples/nest/.gitignore +++ /dev/null @@ -1,56 +0,0 @@ -# compiled output -/dist -/node_modules -/build - -# Logs -logs -*.log -npm-debug.log* -pnpm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# OS -.DS_Store - -# Tests -/coverage -/.nyc_output - -# IDEs and editors -/.idea -.project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace - -# IDE - VSCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# temp directory -.temp -.tmp - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/examples/nest/CHANGELOG.md b/examples/nest/CHANGELOG.md deleted file mode 100644 index a694a80d..00000000 --- a/examples/nest/CHANGELOG.md +++ /dev/null @@ -1,70 +0,0 @@ -# nest - -## 0.0.11 - -### Patch Changes - -- Updated dependencies [db72e2c] -- Updated dependencies [e769740] - - @cipherstash/protect@10.5.0 - -## 0.0.10 - -### Patch Changes - -- Updated dependencies [9ccaf68] - - @cipherstash/protect@10.4.0 - -## 0.0.9 - -### Patch Changes - -- Updated dependencies [a1fce2b] -- Updated dependencies [622b684] - - @cipherstash/protect@10.3.0 - -## 0.0.8 - -### Patch Changes - -- @cipherstash/protect@10.2.1 - -## 0.0.7 - -### Patch Changes - -- Updated dependencies [de029de] - - @cipherstash/protect@10.2.0 - -## 0.0.6 - -### Patch Changes - -- Updated dependencies [ff4421f] - - @cipherstash/protect@10.1.1 - -## 0.0.5 - -### Patch Changes - -- Updated dependencies [6b87c17] - - @cipherstash/protect@10.1.0 - -## 0.0.4 - -### Patch Changes - -- @cipherstash/protect@10.0.2 - -## 0.0.3 - -### Patch Changes - -- @cipherstash/protect@10.0.1 - -## 0.0.2 - -### Patch Changes - -- Updated dependencies [788dbfc] - - @cipherstash/protect@10.0.0 diff --git a/examples/nest/README.md b/examples/nest/README.md deleted file mode 100644 index 7235ce70..00000000 --- a/examples/nest/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Protect.js Example with NestJS - -> ⚠️ **Heads-up:** This example was generated with AI with some very specific prompting to make it as useful as possible for you :) -> If you find any issues, think this example is absolutely terrible, or would like to speak with a human, book a call with the [CipherStash solutions engineering team](https://calendly.com/cipherstash-gtm/cipherstash-discovery-call?month=2025-09) - -## What this shows -- Field-level encryption on 2+ properties via `encryptModel`/`decryptModel` and bulk variants -- Identity-aware encryption is supported (optional `LockContext` chaining) -- Result contract preserved: operations return `{ data }` or `{ failure }` - -## 90-second Quickstart -```bash -pnpm install -cp .env.example .env -pnpm start:dev -``` - -Environment variables (in `.env`): -```bash -CS_WORKSPACE_CRN= -CS_CLIENT_ID= -CS_CLIENT_KEY= -CS_CLIENT_ACCESS_KEY= -``` - -### How encryption works here -- `src/protect/schema.ts` defines tables with `.equality()`, `.orderAndRange()`, `.freeTextSearch()` for searchable encryption on Postgres. -- `ProtectModule` initializes a `ProtectClient` with those schemas and injects a `ProtectService`. -- `AppService` uses `encryptModel`/`decryptModel` and bulk variants to demonstrate single and bulk flows. - -### Minimal API demo -- `GET /` — returns a demo payload with encrypted and decrypted models and a bulk example -- `POST /users` — encrypts provided fields and returns the encrypted model -- `GET /users/:id` — decrypts a provided encrypted model (demo flow) - -### Scripts -- `pnpm start:dev` — run in watch mode -- `pnpm test` / `pnpm test:e2e` - -### Troubleshooting -- Ensure `.env` has all required `CS_*` variables; lock-context flows require user JWTs. -- Node 22+ is required; Bun is not supported. -- If you integrate bundlers, externalize `@cipherstash/protect-ffi` (native module). - -### References -- Protect.js: see repo root `README.md` -- NestJS docs: `https://docs.nestjs.com/` -- Next.js external packages: `docs/how-to/nextjs-external-packages.md` -- SST external packages: `docs/how-to/sst-external-packages.md` -- npm lockfile v3 on Linux: `docs/how-to/npm-lockfile-v3.md` \ No newline at end of file diff --git a/examples/nest/nest-cli.json b/examples/nest/nest-cli.json deleted file mode 100644 index f9aa683b..00000000 --- a/examples/nest/nest-cli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } -} diff --git a/examples/nest/package.json b/examples/nest/package.json deleted file mode 100644 index e8c2cb8a..00000000 --- a/examples/nest/package.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "name": "nest", - "version": "0.0.11", - "description": "", - "author": "", - "private": true, - "license": "UNLICENSED", - "scripts": { - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" - }, - "dependencies": { - "@cipherstash/protect": "workspace:*", - "@nestjs/common": "^11.0.1", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^11.0.1", - "@nestjs/platform-express": "^11.0.1", - "dotenv": "^16.4.7", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" - }, - "devDependencies": { - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.18.0", - "@nestjs/cli": "^11.0.0", - "@nestjs/schematics": "^11.0.0", - "@nestjs/testing": "^11.0.1", - "@types/express": "^5.0.0", - "@types/jest": "^30.0.0", - "@types/node": "^22.10.7", - "@types/supertest": "^6.0.2", - "globals": "^16.0.0", - "jest": "^30.0.0", - "prettier": "^3.4.2", - "source-map-support": "^0.5.21", - "supertest": "^7.0.0", - "ts-jest": "^29.2.5", - "ts-loader": "^9.5.2", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", - "typescript": "^5.7.3", - "typescript-eslint": "^8.20.0" - }, - "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], - "coverageDirectory": "../coverage", - "testEnvironment": "node" - } -} diff --git a/examples/nest/src/app.controller.spec.ts b/examples/nest/src/app.controller.spec.ts deleted file mode 100644 index b61e370f..00000000 --- a/examples/nest/src/app.controller.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { Decrypted, EncryptedPayload } from '@cipherstash/protect' -import { Test, type TestingModule } from '@nestjs/testing' -import { AppController } from './app.controller' -import { AppService, type CreateUserDto, type User } from './app.service' -import { ProtectService } from './protect' - -describe('AppController', () => { - let appController: AppController - let appService: AppService - let protectService: ProtectService - - const mockEncryptedPayload: EncryptedPayload = { - c: 'mock-encrypted-data', - h: 'mock-header', - } - - const mockUser: User = { - id: '1', - email_encrypted: mockEncryptedPayload, - name: 'John Doe', - } - - const mockDecryptedUser: Decrypted = { - id: '1', - email_encrypted: 'john.doe@example.com', - name: 'John Doe', - } - - beforeEach(async () => { - const mockProtectService = { - encryptModel: jest.fn(), - decryptModel: jest.fn(), - bulkEncryptModels: jest.fn(), - bulkDecryptModels: jest.fn(), - } - - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [ - AppService, - { - provide: ProtectService, - useValue: mockProtectService, - }, - ], - }).compile() - - appController = app.get(AppController) - appService = app.get(AppService) - protectService = app.get(ProtectService) - }) - - describe('getHello', () => { - it('should return demo data with encrypted and decrypted users', async () => { - // Mock the service methods - jest.spyOn(appService, 'getHello').mockResolvedValue({ - encryptedUser: mockUser, - decryptedUser: mockDecryptedUser, - bulkExample: { - encrypted: [mockUser], - decrypted: [mockDecryptedUser], - }, - }) - - const result = await appController.getHello() - - expect(result).toEqual({ - encryptedUser: mockUser, - decryptedUser: mockDecryptedUser, - bulkExample: { - encrypted: [mockUser], - decrypted: [mockDecryptedUser], - }, - }) - }) - }) - - describe('createUser', () => { - it('should create a user with encrypted data', async () => { - const userData: CreateUserDto = { - email: 'test@example.com', - name: 'Test User', - } - - jest.spyOn(appService, 'createUser').mockResolvedValue(mockUser) - - const result = await appController.createUser(userData) - - expect(result).toEqual(mockUser) - expect(appService.createUser).toHaveBeenCalledWith(userData) - }) - }) - - describe('getUser', () => { - it('should get a user by id', async () => { - const userId = '1' - - jest.spyOn(appService, 'getUser').mockResolvedValue(mockDecryptedUser) - - const result = await appController.getUser(userId, mockUser) - - expect(result).toEqual(mockDecryptedUser) - expect(appService.getUser).toHaveBeenCalledWith(userId, mockUser) - }) - }) - - describe('getUsers', () => { - it('should return an empty array', async () => { - const result = await appController.getUsers() - - expect(result).toEqual([]) - }) - }) -}) diff --git a/examples/nest/src/app.controller.ts b/examples/nest/src/app.controller.ts deleted file mode 100644 index a78912c3..00000000 --- a/examples/nest/src/app.controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Body, Controller, Get, Param, Post } from '@nestjs/common' -import type { AppService } from './app.service' -import type { CreateUserDto, User } from './app.service' - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - async getHello() { - return await this.appService.getHello() - } - - @Post('users') - async createUser(@Body() userData: CreateUserDto): Promise { - const u = await this.appService.createUser(userData) - return u - } - - @Get('users/:id') - async getUser(@Param('id') id: string, @Body() encryptedUser: User) { - return await this.appService.getUser(id, encryptedUser) - } - - @Get('users') - async getUsers() { - // This would typically fetch from a database - // For demo purposes, return empty array - return [] - } -} diff --git a/examples/nest/src/app.module.ts b/examples/nest/src/app.module.ts deleted file mode 100644 index 58b0d1b7..00000000 --- a/examples/nest/src/app.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Module } from '@nestjs/common' -import { ConfigModule } from '@nestjs/config' -import { AppController } from './app.controller' -import { AppService } from './app.service' -import { ProtectModule, schemas } from './protect' - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - }), - ProtectModule.forRoot({ - schemas, - }), - ], - controllers: [AppController], - providers: [AppService], -}) -export class AppModule {} diff --git a/examples/nest/src/app.service.ts b/examples/nest/src/app.service.ts deleted file mode 100644 index e67dad28..00000000 --- a/examples/nest/src/app.service.ts +++ /dev/null @@ -1,149 +0,0 @@ -import type { Decrypted, EncryptedPayload } from '@cipherstash/protect' -import { Injectable } from '@nestjs/common' -import type { ProtectService } from './protect' -import { users } from './protect' - -export type User = { - id: string - email_encrypted: EncryptedPayload | string - phone_encrypted?: EncryptedPayload | string - ssn_encrypted?: EncryptedPayload | string - name: string -} - -export type CreateUserDto = { - email: string - phone?: string - ssn?: string - name: string -} - -@Injectable() -export class AppService { - constructor(private readonly protectService: ProtectService) {} - - async getHello(): Promise<{ - encryptedUser: User - decryptedUser: Decrypted - bulkExample: { - encrypted: User[] - decrypted: Decrypted[] - } - }> { - // Example 1: Single model encryption/decryption - const userData: CreateUserDto = { - email: 'john.doe@example.com', - phone: '+1-555-123-4567', - ssn: '123-45-6789', - name: 'John Doe', - } - - const encryptedResult = await this.protectService.encryptModel( - { - id: '1', - email_encrypted: userData.email, - phone_encrypted: userData.phone, - ssn_encrypted: userData.ssn, - name: userData.name, - }, - users, - ) - - if (encryptedResult.failure) { - throw new Error(`Encryption failed: ${encryptedResult.failure.message}`) - } - - const decryptedResult = await this.protectService.decryptModel( - encryptedResult.data, - ) - - if (decryptedResult.failure) { - throw new Error(`Decryption failed: ${decryptedResult.failure.message}`) - } - - // Example 2: Bulk operations for better performance - const bulkUsers: CreateUserDto[] = [ - { - email: 'alice@example.com', - phone: '+1-555-111-1111', - name: 'Alice Smith', - }, - { - email: 'bob@example.com', - phone: '+1-555-222-2222', - name: 'Bob Johnson', - }, - ] - - const bulkEncryptedResult = - await this.protectService.bulkEncryptModels( - bulkUsers.map((user, index) => ({ - id: (index + 2).toString(), - email_encrypted: user.email, - phone_encrypted: user.phone, - name: user.name, - })), - users, - ) - - if (bulkEncryptedResult.failure) { - throw new Error( - `Bulk encryption failed: ${bulkEncryptedResult.failure.message}`, - ) - } - - const bulkDecryptedResult = - await this.protectService.bulkDecryptModels( - bulkEncryptedResult.data, - ) - - if (bulkDecryptedResult.failure) { - throw new Error( - `Bulk decryption failed: ${bulkDecryptedResult.failure.message}`, - ) - } - - return { - encryptedUser: encryptedResult.data, - decryptedUser: decryptedResult.data, - bulkExample: { - encrypted: bulkEncryptedResult.data, - decrypted: bulkDecryptedResult.data, - }, - } - } - - async createUser(userData: CreateUserDto): Promise { - const encryptedResult = await this.protectService.encryptModel( - { - id: Date.now().toString(), - email_encrypted: userData.email, - phone_encrypted: userData.phone, - ssn_encrypted: userData.ssn, - name: userData.name, - }, - users, - ) - - if (encryptedResult.failure) { - throw new Error( - `User creation failed: ${encryptedResult.failure.message}`, - ) - } - - return encryptedResult.data - } - - async getUser(id: string, encryptedUser: User): Promise> { - const decryptedResult = - await this.protectService.decryptModel(encryptedUser) - - if (decryptedResult.failure) { - throw new Error( - `User retrieval failed: ${decryptedResult.failure.message}`, - ) - } - - return decryptedResult.data - } -} diff --git a/examples/nest/src/main.ts b/examples/nest/src/main.ts deleted file mode 100644 index e99d5a4b..00000000 --- a/examples/nest/src/main.ts +++ /dev/null @@ -1,9 +0,0 @@ -import 'dotenv/config' -import { NestFactory } from '@nestjs/core' -import { AppModule } from './app.module' - -async function bootstrap() { - const app = await NestFactory.create(AppModule) - await app.listen(process.env.PORT ?? 3000) -} -bootstrap() diff --git a/examples/nest/src/protect/decorators/decrypt.decorator.ts b/examples/nest/src/protect/decorators/decrypt.decorator.ts deleted file mode 100644 index c6572fe5..00000000 --- a/examples/nest/src/protect/decorators/decrypt.decorator.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { type ExecutionContext, createParamDecorator } from '@nestjs/common' -import { getProtectService } from '../utils/get-protect-service.util' - -import type { - ProtectColumn, - ProtectTable, - ProtectTableColumn, - ProtectValue, -} from '@cipherstash/protect' - -export interface DecryptOptions { - table: ProtectTable - column: ProtectColumn | ProtectValue - lockContext?: unknown // JWT or LockContext -} - -/** - * Decorator to automatically decrypt a field or entire object - * - * @example - * ```typescript - * @Get(':id') - * async getUser(@Param('id') id: string, @Decrypt('email', { table: 'users', column: 'email' }) decryptedEmail: string) { - * // decryptedEmail is automatically decrypted - * return { id, email: decryptedEmail }; - * } - * - * @Get(':id') - * async getUser(@Param('id') id: string, @DecryptModel('users') user: User) { - * // user is automatically decrypted based on schema - * return user; - * } - * ``` - */ -export const Decrypt = createParamDecorator( - async (field: string, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest() - const protectService = getProtectService(ctx) - - if (!protectService) { - throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', - ) - } - - const value = - request.body?.[field] || request.params?.[field] || request.query?.[field] - if (value === undefined || value === null) { - return value - } - - // Check if value is already an encrypted payload - if (typeof value === 'object' && value.c) { - const result = await protectService.decrypt(value) - if (result.failure) { - throw new Error(`Decryption failed: ${result.failure.message}`) - } - return result.data - } - - // If it's not encrypted, return as-is - return value - }, -) - -/** - * Decorator to automatically decrypt an entire model based on schema - */ -export const DecryptModel = createParamDecorator( - async (tableName: string, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest() - const protectService = getProtectService(ctx) - - if (!protectService) { - throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', - ) - } - - const model = request.body || request.params || request.query - if (!model || typeof model !== 'object') { - return model - } - - // This would need to be enhanced to work with actual schema definitions - // For now, it's a placeholder for the concept - return model - }, -) diff --git a/examples/nest/src/protect/decorators/encrypt.decorator.ts b/examples/nest/src/protect/decorators/encrypt.decorator.ts deleted file mode 100644 index 9abdb352..00000000 --- a/examples/nest/src/protect/decorators/encrypt.decorator.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { type ExecutionContext, createParamDecorator } from '@nestjs/common' -import type { ProtectService } from '../protect.service' -import { users } from '../schema' -import { getProtectService } from '../utils/get-protect-service.util' - -import type { - ProtectColumn, - ProtectTable, - ProtectTableColumn, - ProtectValue, -} from '@cipherstash/protect' - -export interface EncryptOptions { - table: ProtectTable - column: ProtectColumn | ProtectValue - lockContext?: unknown // JWT or LockContext -} - -/** - * Decorator to automatically encrypt a field or entire object - * - * @example - * ```typescript - * @Post() - * async createUser(@Body() userData: CreateUserDto, @Encrypt('email', { table: 'users', column: 'email' }) encryptedEmail: string) { - * // encryptedEmail is automatically encrypted - * return this.userService.create({ ...userData, email: encryptedEmail }); - * } - * - * @Post() - * async createUser(@Body() @EncryptModel('users') userData: CreateUserDto) { - * // userData is automatically encrypted based on schema - * return this.userService.create(userData); - * } - * ``` - */ -export const Encrypt = createParamDecorator( - async (field: string, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest() - const protectService = getProtectService(ctx) - - if (!protectService) { - throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', - ) - } - - const value = request.body?.[field] - if (value === undefined || value === null) { - return value - } - - // Note: This is a simplified example. In practice, you'd need to pass actual table/column objects - // from your schema definitions rather than creating them inline - const result = await protectService.encrypt(value, { - table: users, - column: users.email_encrypted, - }) - - if (result.failure) { - throw new Error(`Encryption failed: ${result.failure.message}`) - } - - return result.data - }, -) - -/** - * Decorator to automatically encrypt an entire model based on schema - */ -export const EncryptModel = createParamDecorator( - async (tableName: string, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest() - const protectService = getProtectService(ctx) - - if (!protectService) { - throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', - ) - } - - const model = request.body - if (!model || typeof model !== 'object') { - return model - } - - // This would need to be enhanced to work with actual schema definitions - // For now, it's a placeholder for the concept - return model - }, -) diff --git a/examples/nest/src/protect/index.ts b/examples/nest/src/protect/index.ts deleted file mode 100644 index d9b04bf0..00000000 --- a/examples/nest/src/protect/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Main module exports -export { ProtectModule } from './protect.module' -export { ProtectService } from './protect.service' - -// Schema exports -export * from './schema' - -// Decorator exports -export { Encrypt, EncryptModel } from './decorators/encrypt.decorator' -export { Decrypt, DecryptModel } from './decorators/decrypt.decorator' - -// Interceptor exports -export { EncryptInterceptor } from './interceptors/encrypt.interceptor' -export { DecryptInterceptor } from './interceptors/decrypt.interceptor' - -// Type exports -export type { ProtectConfig } from './interfaces/protect-config.interface' -export { PROTECT_CONFIG, PROTECT_CLIENT } from './protect.constants' diff --git a/examples/nest/src/protect/interceptors/decrypt.interceptor.ts b/examples/nest/src/protect/interceptors/decrypt.interceptor.ts deleted file mode 100644 index 1078c3c8..00000000 --- a/examples/nest/src/protect/interceptors/decrypt.interceptor.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - type CallHandler, - type ExecutionContext, - Injectable, - type NestInterceptor, -} from '@nestjs/common' -import type { Observable } from 'rxjs' -import { map } from 'rxjs/operators' -import type { ProtectService } from '../protect.service' -import { getProtectService } from '../utils/get-protect-service.util' - -import type { - ProtectColumn, - ProtectTable, - ProtectTableColumn, - ProtectValue, -} from '@cipherstash/protect' - -export interface DecryptInterceptorOptions { - fields?: string[] - table: ProtectTable - column: ProtectColumn | ProtectValue - lockContext?: unknown -} - -/** - * Interceptor to automatically decrypt response data - * - * @example - * ```typescript - * @UseInterceptors(new DecryptInterceptor({ - * fields: ['email', 'phone'], - * table: 'users', - * column: 'email' - * })) - * @Get() - * async getUsers() { - * return this.userService.findAll(); // Email and phone fields will be decrypted - * } - * ``` - */ -@Injectable() -export class DecryptInterceptor implements NestInterceptor { - constructor(private readonly options: DecryptInterceptorOptions) {} - - async intercept( - context: ExecutionContext, - next: CallHandler, - ): Promise> { - const protectService = getProtectService(context) - - if (!protectService) { - throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', - ) - } - - return next.handle().pipe( - map(async (data: unknown) => { - if (!data) return data - - if (Array.isArray(data)) { - return Promise.all( - data.map((item) => this.decryptItem(item, protectService)), - ) - } - - return this.decryptItem(data, protectService) - }), - ) - } - - private async decryptItem( - item: unknown, - protectService: ProtectService, - ): Promise { - if (!item || typeof item !== 'object') { - return item - } - - const result = { ...item } - - if (this.options.fields) { - for (const field of this.options.fields) { - if (result[field] !== undefined && result[field] !== null) { - // Check if the field contains an encrypted payload - if (typeof result[field] === 'object' && result[field].c) { - const decryptResult = await protectService.decrypt(result[field]) - - if (decryptResult.failure) { - throw new Error( - `Decryption failed for field ${field}: ${decryptResult.failure.message}`, - ) - } - - result[field] = decryptResult.data - } - } - } - } - - return result - } -} diff --git a/examples/nest/src/protect/interceptors/encrypt.interceptor.ts b/examples/nest/src/protect/interceptors/encrypt.interceptor.ts deleted file mode 100644 index d1a2fa6c..00000000 --- a/examples/nest/src/protect/interceptors/encrypt.interceptor.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - type CallHandler, - type ExecutionContext, - Injectable, - type NestInterceptor, -} from '@nestjs/common' -import type { Observable } from 'rxjs' -import { map } from 'rxjs/operators' -import type { ProtectService } from '../protect.service' -import { getProtectService } from '../utils/get-protect-service.util' - -import type { - ProtectColumn, - ProtectTable, - ProtectTableColumn, - ProtectValue, -} from '@cipherstash/protect' - -export interface EncryptInterceptorOptions { - fields?: string[] - table: ProtectTable - column: ProtectColumn | ProtectValue - lockContext?: unknown -} - -/** - * Interceptor to automatically encrypt response data - * - * @example - * ```typescript - * @UseInterceptors(new EncryptInterceptor({ - * fields: ['email', 'phone'], - * table: 'users', - * column: 'email' - * })) - * @Get() - * async getUsers() { - * return this.userService.findAll(); // Email and phone fields will be encrypted - * } - * ``` - */ -@Injectable() -export class EncryptInterceptor implements NestInterceptor { - constructor(private readonly options: EncryptInterceptorOptions) {} - - async intercept( - context: ExecutionContext, - next: CallHandler, - ): Promise> { - const protectService = getProtectService(context) - - if (!protectService) { - throw new Error( - 'ProtectService not found. Make sure ProtectModule is imported.', - ) - } - - return next.handle().pipe( - map(async (data: unknown) => { - if (!data) return data - - if (Array.isArray(data)) { - return Promise.all( - data.map((item) => this.encryptItem(item, protectService)), - ) - } - - return this.encryptItem(data, protectService) - }), - ) - } - - private async encryptItem( - item: unknown, - protectService: ProtectService, - ): Promise { - if (!item || typeof item !== 'object') { - return item - } - - const result = { ...item } - - if (this.options.fields) { - for (const field of this.options.fields) { - if (result[field] !== undefined && result[field] !== null) { - const encryptResult = await protectService.encrypt(result[field], { - table: this.options.table, - column: this.options.column, - }) - - if (encryptResult.failure) { - throw new Error( - `Encryption failed for field ${field}: ${encryptResult.failure.message}`, - ) - } - - result[field] = encryptResult.data - } - } - } - - return result - } -} diff --git a/examples/nest/src/protect/interfaces/protect-config.interface.ts b/examples/nest/src/protect/interfaces/protect-config.interface.ts deleted file mode 100644 index 2a7ef944..00000000 --- a/examples/nest/src/protect/interfaces/protect-config.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ProtectTable, ProtectTableColumn } from '@cipherstash/protect' - -export interface ProtectConfig { - workspaceCrn: string - clientId: string - clientKey: string - clientAccessKey: string - logLevel?: 'debug' | 'info' | 'error' - schemas?: ProtectTable[] -} diff --git a/examples/nest/src/protect/protect.constants.ts b/examples/nest/src/protect/protect.constants.ts deleted file mode 100644 index f1c70b35..00000000 --- a/examples/nest/src/protect/protect.constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const PROTECT_CONFIG = 'PROTECT_CONFIG' -export const PROTECT_CLIENT = 'PROTECT_CLIENT' diff --git a/examples/nest/src/protect/protect.module.ts b/examples/nest/src/protect/protect.module.ts deleted file mode 100644 index 6aca610f..00000000 --- a/examples/nest/src/protect/protect.module.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { - type ProtectClient, - type ProtectClientConfig, - type ProtectTable, - type ProtectTableColumn, - protect, -} from '@cipherstash/protect' -import { type DynamicModule, Global, Module } from '@nestjs/common' -import { ConfigModule, ConfigService } from '@nestjs/config' -import type { ProtectConfig } from './interfaces/protect-config.interface' -import { PROTECT_CLIENT, PROTECT_CONFIG } from './protect.constants' -import { ProtectService } from './protect.service' -import { users } from './schema' - -@Global() -@Module({}) -// biome-ignore lint/complexity/noStaticOnlyClass: NestJS module -export class ProtectModule { - static forRoot(config?: Partial): DynamicModule { - return { - module: ProtectModule, - imports: [ConfigModule], - providers: [ - { - provide: PROTECT_CONFIG, - useFactory: (configService: ConfigService): ProtectConfig => { - const workspaceCrn = configService.get('CS_WORKSPACE_CRN') - const clientId = configService.get('CS_CLIENT_ID') - const clientKey = configService.get('CS_CLIENT_KEY') - const clientAccessKey = configService.get( - 'CS_CLIENT_ACCESS_KEY', - ) - - const defaultConfig: ProtectConfig = { - workspaceCrn: workspaceCrn ?? '', - clientId: clientId ?? '', - clientKey: clientKey ?? '', - clientAccessKey: clientAccessKey ?? '', - logLevel: configService.get<'debug' | 'info' | 'error'>( - 'PROTECT_LOG_LEVEL', - 'info', - ), - ...config, - } - - // Validate required configuration - if (!defaultConfig.workspaceCrn) { - throw new Error('CS_WORKSPACE_CRN is required') - } - if (!defaultConfig.clientId) { - throw new Error('CS_CLIENT_ID is required') - } - if (!defaultConfig.clientKey) { - throw new Error('CS_CLIENT_KEY is required') - } - if (!defaultConfig.clientAccessKey) { - throw new Error('CS_CLIENT_ACCESS_KEY is required') - } - - return defaultConfig - }, - inject: [ConfigService], - }, - { - provide: PROTECT_CLIENT, - useFactory: async (config: ProtectConfig): Promise => { - const protectConfig: ProtectClientConfig = { - schemas: (config.schemas && config.schemas.length > 0 - ? config.schemas - : [users]) as [ - ProtectTable, - ...ProtectTable[], - ], - } - - return await protect(protectConfig) - }, - inject: [PROTECT_CONFIG], - }, - ProtectService, - ], - exports: [ProtectService, PROTECT_CLIENT], - } - } - - static forRootAsync(options: { - useFactory: (...args: unknown[]) => Promise | ProtectConfig - inject?: unknown[] - }): DynamicModule { - return { - module: ProtectModule, - imports: [ConfigModule], - providers: [ - { - provide: PROTECT_CONFIG, - useFactory: options.useFactory, - inject: options.inject || [], - }, - { - provide: PROTECT_CLIENT, - useFactory: async (config: ProtectConfig): Promise => { - const protectConfig: ProtectClientConfig = { - schemas: (config.schemas && config.schemas.length > 0 - ? config.schemas - : [users]) as [ - ProtectTable, - ...ProtectTable[], - ], - } - - return await protect(protectConfig) - }, - inject: [PROTECT_CONFIG], - }, - ProtectService, - ], - exports: [ProtectService, PROTECT_CLIENT], - } - } -} diff --git a/examples/nest/src/protect/protect.service.spec.ts b/examples/nest/src/protect/protect.service.spec.ts deleted file mode 100644 index fb4a4d96..00000000 --- a/examples/nest/src/protect/protect.service.spec.ts +++ /dev/null @@ -1,219 +0,0 @@ -import type { EncryptedPayload, ProtectClient } from '@cipherstash/protect' -import { Test, type TestingModule } from '@nestjs/testing' -import { PROTECT_CLIENT } from './protect.constants' -import { ProtectService } from './protect.service' -import { users } from './schema' - -describe('ProtectService', () => { - let service: ProtectService - let mockClient: jest.Mocked - - const mockEncryptedPayload: EncryptedPayload = { - c: 'mock-encrypted-data', - h: 'mock-header', - } - - beforeEach(async () => { - mockClient = { - encrypt: jest.fn(), - decrypt: jest.fn(), - encryptModel: jest.fn(), - decryptModel: jest.fn(), - bulkEncrypt: jest.fn(), - bulkDecrypt: jest.fn(), - bulkEncryptModels: jest.fn(), - bulkDecryptModels: jest.fn(), - } as jest.Mocked - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ProtectService, - { - provide: PROTECT_CLIENT, - useValue: mockClient, - }, - ], - }).compile() - - service = module.get(ProtectService) - }) - - describe('encrypt', () => { - it('should encrypt plaintext', async () => { - const plaintext = 'test@example.com' - const options = { table: users, column: users.email_encrypted } - const expectedResult = { data: mockEncryptedPayload } - - mockClient.encrypt.mockResolvedValue(expectedResult) - - const result = await service.encrypt(plaintext, options) - - expect(result).toEqual(expectedResult) - expect(mockClient.encrypt).toHaveBeenCalledWith(plaintext, options) - }) - - it('should handle encryption failure', async () => { - const plaintext = 'test@example.com' - const options = { table: users, column: users.email_encrypted } - const expectedResult = { - failure: { type: 'EncryptionError', message: 'Failed to encrypt' }, - } - - mockClient.encrypt.mockResolvedValue(expectedResult) - - const result = await service.encrypt(plaintext, options) - - expect(result).toEqual(expectedResult) - }) - }) - - describe('decrypt', () => { - it('should decrypt encrypted payload', async () => { - const expectedResult = { data: 'test@example.com' } - - mockClient.decrypt.mockResolvedValue(expectedResult) - - const result = await service.decrypt(mockEncryptedPayload) - - expect(result).toEqual(expectedResult) - expect(mockClient.decrypt).toHaveBeenCalledWith(mockEncryptedPayload) - }) - }) - - describe('encryptModel', () => { - it('should encrypt a model', async () => { - const model = { - id: '1', - email_encrypted: 'test@example.com', - name: 'John Doe', - } - const expectedResult = { - data: { - id: '1', - email_encrypted: mockEncryptedPayload, - name: 'John Doe', - }, - } - - mockClient.encryptModel.mockResolvedValue(expectedResult) - - const result = await service.encryptModel(model, users) - - expect(result).toEqual(expectedResult) - expect(mockClient.encryptModel).toHaveBeenCalledWith(model, users) - }) - }) - - describe('decryptModel', () => { - it('should decrypt a model', async () => { - const encryptedModel = { - id: '1', - email_encrypted: mockEncryptedPayload, - name: 'John Doe', - } - const expectedResult = { - data: { - id: '1', - email_encrypted: 'test@example.com', - name: 'John Doe', - }, - } - - mockClient.decryptModel.mockResolvedValue(expectedResult) - - const result = await service.decryptModel(encryptedModel) - - expect(result).toEqual(expectedResult) - expect(mockClient.decryptModel).toHaveBeenCalledWith(encryptedModel) - }) - }) - - describe('bulkEncrypt', () => { - it('should bulk encrypt plaintexts', async () => { - const plaintexts = [ - { id: '1', plaintext: 'test1@example.com' }, - { id: '2', plaintext: 'test2@example.com' }, - ] - const options = { table: users, column: users.email_encrypted } - const expectedResult = { - data: [ - { id: '1', data: mockEncryptedPayload }, - { id: '2', data: mockEncryptedPayload }, - ], - } - - mockClient.bulkEncrypt.mockResolvedValue(expectedResult) - - const result = await service.bulkEncrypt(plaintexts, options) - - expect(result).toEqual(expectedResult) - expect(mockClient.bulkEncrypt).toHaveBeenCalledWith(plaintexts, options) - }) - }) - - describe('bulkDecrypt', () => { - it('should bulk decrypt encrypted data', async () => { - const encryptedData = [ - { id: '1', data: mockEncryptedPayload }, - { id: '2', data: mockEncryptedPayload }, - ] - const expectedResult = { - data: [ - { id: '1', data: 'test1@example.com' }, - { id: '2', data: 'test2@example.com' }, - ], - } - - mockClient.bulkDecrypt.mockResolvedValue(expectedResult) - - const result = await service.bulkDecrypt(encryptedData) - - expect(result).toEqual(expectedResult) - expect(mockClient.bulkDecrypt).toHaveBeenCalledWith(encryptedData) - }) - }) - - describe('bulkEncryptModels', () => { - it('should bulk encrypt models', async () => { - const models = [ - { id: '1', email_encrypted: 'test1@example.com', name: 'User 1' }, - { id: '2', email_encrypted: 'test2@example.com', name: 'User 2' }, - ] - const expectedResult = { - data: [ - { id: '1', email_encrypted: mockEncryptedPayload, name: 'User 1' }, - { id: '2', email_encrypted: mockEncryptedPayload, name: 'User 2' }, - ], - } - - mockClient.bulkEncryptModels.mockResolvedValue(expectedResult) - - const result = await service.bulkEncryptModels(models, users) - - expect(result).toEqual(expectedResult) - expect(mockClient.bulkEncryptModels).toHaveBeenCalledWith(models, users) - }) - }) - - describe('bulkDecryptModels', () => { - it('should bulk decrypt models', async () => { - const encryptedModels = [ - { id: '1', email_encrypted: mockEncryptedPayload, name: 'User 1' }, - { id: '2', email_encrypted: mockEncryptedPayload, name: 'User 2' }, - ] - const expectedResult = { - data: [ - { id: '1', email_encrypted: 'test1@example.com', name: 'User 1' }, - { id: '2', email_encrypted: 'test2@example.com', name: 'User 2' }, - ], - } - - mockClient.bulkDecryptModels.mockResolvedValue(expectedResult) - - const result = await service.bulkDecryptModels(encryptedModels) - - expect(result).toEqual(expectedResult) - expect(mockClient.bulkDecryptModels).toHaveBeenCalledWith(encryptedModels) - }) - }) -}) diff --git a/examples/nest/src/protect/protect.service.ts b/examples/nest/src/protect/protect.service.ts deleted file mode 100644 index be3a8a12..00000000 --- a/examples/nest/src/protect/protect.service.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { - Decrypted, - EncryptOptions, - EncryptedPayload, - LockContext, - ProtectClient, - ProtectTable, - ProtectTableColumn, -} from '@cipherstash/protect' -import { Inject, Injectable } from '@nestjs/common' -import { PROTECT_CLIENT } from './protect.constants' - -@Injectable() -export class ProtectService { - constructor( - @Inject(PROTECT_CLIENT) - private readonly client: ProtectClient, - ) {} - - async encrypt(plaintext: string, options: EncryptOptions) { - return this.client.encrypt(plaintext, options) - } - - async decrypt(encryptedPayload: EncryptedPayload) { - return this.client.decrypt(encryptedPayload) - } - - async encryptModel>( - model: Decrypted, - table: ProtectTable, - ) { - return this.client.encryptModel(model, table) - } - - async decryptModel>(model: T) { - return this.client.decryptModel(model) - } - - async bulkEncrypt( - plaintexts: Array<{ id?: string; plaintext: string | null }>, - options: EncryptOptions, - ) { - return this.client.bulkEncrypt(plaintexts, options) - } - - async bulkDecrypt( - encryptedData: Array<{ id?: string; data: EncryptedPayload | null }>, - ) { - return this.client.bulkDecrypt(encryptedData) - } - - async bulkEncryptModels>( - models: Decrypted[], - table: ProtectTable, - ) { - return this.client.bulkEncryptModels(models, table) - } - - async bulkDecryptModels>(models: T[]) { - return this.client.bulkDecryptModels(models) - } - - // Identity-aware encryption methods - async encryptWithLockContext( - plaintext: string, - options: EncryptOptions, - lockContext: LockContext, - ) { - return this.client.encrypt(plaintext, options).withLockContext(lockContext) - } - - async decryptWithLockContext( - encryptedPayload: EncryptedPayload, - lockContext: LockContext, - ) { - return this.client.decrypt(encryptedPayload).withLockContext(lockContext) - } - - async encryptModelWithLockContext>( - model: Decrypted, - table: ProtectTable, - lockContext: LockContext, - ) { - return this.client - .encryptModel(model, table) - .withLockContext(lockContext) - } - - async decryptModelWithLockContext>( - model: T, - lockContext: LockContext, - ) { - return this.client.decryptModel(model).withLockContext(lockContext) - } -} diff --git a/examples/nest/src/protect/schema.ts b/examples/nest/src/protect/schema.ts deleted file mode 100644 index bf104f79..00000000 --- a/examples/nest/src/protect/schema.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { csColumn, csTable } from '@cipherstash/protect' - -export const users = csTable('users', { - email_encrypted: csColumn('email_encrypted') - .equality() - .orderAndRange() - .freeTextSearch(), - phone_encrypted: csColumn('phone_encrypted').equality().orderAndRange(), - ssn_encrypted: csColumn('ssn_encrypted').equality(), -}) - -export const orders = csTable('orders', { - address_encrypted: csColumn('address_encrypted').freeTextSearch(), - creditCard_encrypted: csColumn('creditCard_encrypted').equality(), -}) - -// Export all schemas for easy import -export const schemas = [users, orders] diff --git a/examples/nest/src/protect/utils/get-protect-service.util.ts b/examples/nest/src/protect/utils/get-protect-service.util.ts deleted file mode 100644 index 22bd5a03..00000000 --- a/examples/nest/src/protect/utils/get-protect-service.util.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { ExecutionContext } from '@nestjs/common' -import type { ModuleRef } from '@nestjs/core' -import { ProtectService } from '../protect.service' - -export function getProtectService( - ctx: ExecutionContext, -): ProtectService | null { - try { - const app = ctx.switchToHttp().getRequest().app - if (app?.get) { - return app.get(ProtectService) - } - - // Fallback: try to get from module ref if available - const moduleRef = ctx.switchToHttp().getRequest().moduleRef as ModuleRef - if (moduleRef) { - return moduleRef.get(ProtectService, { strict: false }) - } - - return null - } catch (error) { - return null - } -} diff --git a/examples/nest/test/app.e2e-spec.ts b/examples/nest/test/app.e2e-spec.ts deleted file mode 100644 index 948e3e0d..00000000 --- a/examples/nest/test/app.e2e-spec.ts +++ /dev/null @@ -1,162 +0,0 @@ -import type { EncryptedPayload } from '@cipherstash/protect' -import type { INestApplication } from '@nestjs/common' -import { Test, type TestingModule } from '@nestjs/testing' -import request from 'supertest' -import { AppController } from '../src/app.controller' -import { AppService } from '../src/app.service' -import { ProtectService } from '../src/protect' - -describe('AppController (e2e)', () => { - let app: INestApplication - let protectService: ProtectService - - const mockEncryptedPayload: EncryptedPayload = { - c: 'mock-encrypted-data', - h: 'mock-header', - } - - beforeAll(async () => { - const moduleFixture: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [ - AppService, - { - provide: ProtectService, - useValue: { - encryptModel: jest.fn().mockImplementation((model) => - Promise.resolve({ - data: { - id: model.id || '1', - email_encrypted: mockEncryptedPayload, - name: model.name || 'Default User', - }, - }), - ), - decryptModel: jest.fn().mockResolvedValue({ - data: { - id: '1', - email_encrypted: 'john.doe@example.com', - name: 'John Doe', - }, - }), - bulkEncryptModels: jest.fn().mockResolvedValue({ - data: [ - { - id: '2', - email_encrypted: mockEncryptedPayload, - name: 'Alice Smith', - }, - { - id: '3', - email_encrypted: mockEncryptedPayload, - name: 'Bob Johnson', - }, - ], - }), - bulkDecryptModels: jest.fn().mockResolvedValue({ - data: [ - { - id: '2', - email_encrypted: 'alice@example.com', - name: 'Alice Smith', - }, - { - id: '3', - email_encrypted: 'bob@example.com', - name: 'Bob Johnson', - }, - ], - }), - }, - }, - ], - }).compile() - - app = moduleFixture.createNestApplication() - protectService = moduleFixture.get(ProtectService) - await app.init() - }) - - afterAll(async () => { - await app.close() - }) - - describe('/ (GET)', () => { - it('should return demo data with encrypted and decrypted users', () => { - return request(app.getHttpServer()) - .get('/') - .expect(200) - .expect((res) => { - expect(res.body).toHaveProperty('encryptedUser') - expect(res.body).toHaveProperty('decryptedUser') - expect(res.body).toHaveProperty('bulkExample') - expect(res.body.bulkExample).toHaveProperty('encrypted') - expect(res.body.bulkExample).toHaveProperty('decrypted') - }) - }) - }) - - describe('/users (POST)', () => { - it('should create a user with encrypted data', () => { - const userData = { - email: 'test@example.com', - name: 'Test User', - } - - return request(app.getHttpServer()) - .post('/users') - .send(userData) - .expect(201) - .expect((res) => { - expect(res.body).toHaveProperty('id') - expect(res.body).toHaveProperty('email_encrypted') - expect(res.body).toHaveProperty('name') - expect(res.body.name).toBe('Test User') - }) - }) - - it('should handle invalid user data', () => { - const invalidUserData = { - // Missing required fields - } - - // Since we don't have validation pipes set up, this will succeed with default values - return request(app.getHttpServer()) - .post('/users') - .send(invalidUserData) - .expect(201) - .expect((res) => { - expect(res.body).toHaveProperty('id') - expect(res.body).toHaveProperty('email_encrypted') - expect(res.body).toHaveProperty('name') - }) - }) - }) - - describe('/users/:id (GET)', () => { - it('should get a user by id', () => { - const userId = '1' - const encryptedUser = { - id: '1', - email_encrypted: mockEncryptedPayload, - name: 'John Doe', - } - - return request(app.getHttpServer()) - .get(`/users/${userId}`) - .send(encryptedUser) - .expect(200) - .expect((res) => { - expect(res.body).toHaveProperty('id') - expect(res.body).toHaveProperty('email_encrypted') - expect(res.body).toHaveProperty('name') - }) - }) - }) - - describe('/users (GET)', () => { - it('should return an empty array', () => { - return request(app.getHttpServer()).get('/users').expect(200).expect([]) - }) - }) -}) diff --git a/examples/nest/test/jest-e2e.json b/examples/nest/test/jest-e2e.json deleted file mode 100644 index e9d912f3..00000000 --- a/examples/nest/test/jest-e2e.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - } -} diff --git a/examples/nest/tsconfig.build.json b/examples/nest/tsconfig.build.json deleted file mode 100644 index 64f86c6b..00000000 --- a/examples/nest/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] -} diff --git a/examples/nest/tsconfig.json b/examples/nest/tsconfig.json deleted file mode 100644 index aba29b0e..00000000 --- a/examples/nest/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "module": "nodenext", - "moduleResolution": "nodenext", - "resolvePackageJsonExports": true, - "esModuleInterop": true, - "isolatedModules": true, - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2023", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, - "forceConsistentCasingInFileNames": true, - "noImplicitAny": false, - "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false - } -} diff --git a/examples/next-drizzle-mysql/.env.example b/examples/next-drizzle-mysql/.env.example deleted file mode 100644 index f481ae25..00000000 --- a/examples/next-drizzle-mysql/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -DATABASE_URL=mysql://protect_example:password@127.0.0.1:3306/protect_example -CS_CLIENT_ID= -CS_CLIENT_KEY= -CS_CLIENT_ACCESS_KEY= -CS_WORKSPACE_CRN= \ No newline at end of file diff --git a/examples/next-drizzle-mysql/CHANGELOG.md b/examples/next-drizzle-mysql/CHANGELOG.md deleted file mode 100644 index c41201e9..00000000 --- a/examples/next-drizzle-mysql/CHANGELOG.md +++ /dev/null @@ -1,163 +0,0 @@ -# next-drizzle-mysql - -## 0.2.17 - -### Patch Changes - -- Updated dependencies [db72e2c] -- Updated dependencies [e769740] - - @cipherstash/protect@10.5.0 - -## 0.2.16 - -### Patch Changes - -- Updated dependencies [9ccaf68] - - @cipherstash/protect@10.4.0 - -## 0.2.15 - -### Patch Changes - -- Updated dependencies [a1fce2b] -- Updated dependencies [622b684] - - @cipherstash/protect@10.3.0 - -## 0.2.14 - -### Patch Changes - -- @cipherstash/protect@10.2.1 - -## 0.2.13 - -### Patch Changes - -- Updated dependencies [de029de] - - @cipherstash/protect@10.2.0 - -## 0.2.12 - -### Patch Changes - -- Updated dependencies [ff4421f] - - @cipherstash/protect@10.1.1 - -## 0.2.11 - -### Patch Changes - -- Updated dependencies [6b87c17] - - @cipherstash/protect@10.1.0 - -## 0.2.10 - -### Patch Changes - -- @cipherstash/protect@10.0.2 - -## 0.2.9 - -### Patch Changes - -- @cipherstash/protect@10.0.1 - -## 0.2.8 - -### Patch Changes - -- Updated dependencies [788dbfc] - - @cipherstash/protect@10.0.0 - -## 0.2.7 - -### Patch Changes - -- Updated dependencies [c7ed7ab] -- Updated dependencies [211e979] - - @cipherstash/protect@9.6.0 - -## 0.2.6 - -### Patch Changes - -- Updated dependencies [6f45b02] - - @cipherstash/protect@9.5.0 - -## 0.2.5 - -### Patch Changes - -- @cipherstash/protect@9.4.1 - -## 0.2.4 - -### Patch Changes - -- Updated dependencies [1cc4772] - - @cipherstash/protect@9.4.0 - -## 0.2.3 - -### Patch Changes - -- Updated dependencies [01fed9e] - - @cipherstash/protect@9.3.0 - -## 0.2.2 - -### Patch Changes - -- Updated dependencies [587f222] - - @cipherstash/protect@9.2.0 - -## 0.2.1 - -### Patch Changes - -- Updated dependencies [c8468ee] - - @cipherstash/protect@9.1.0 - -## 0.2.0 - -### Minor Changes - -- 1bc55a0: Implemented a more configurable pattern for the Protect client. - - This release introduces a new `ProtectClientConfig` type that can be used to configure the Protect client. - This is useful if you want to configure the Protect client specific to your application, and will future proof any additional configuration options that are added in the future. - - ```ts - import { protect, type ProtectClientConfig } from "@cipherstash/protect"; - - const config: ProtectClientConfig = { - schemas: [users, orders], - workspaceCrn: "your-workspace-crn", - accessKey: "your-access-key", - clientId: "your-client-id", - clientKey: "your-client-key", - }; - - const protectClient = await protect(config); - ``` - - The now deprecated method of passing your tables to the `protect` client is no longer supported. - - ```ts - import { protect, type ProtectClientConfig } from "@cipherstash/protect"; - - // old method (no longer supported) - const protectClient = await protect(users, orders); - - // required method - const config: ProtectClientConfig = { - schemas: [users, orders], - }; - - const protectClient = await protect(config); - ``` - -### Patch Changes - -- Updated dependencies [1bc55a0] - - @cipherstash/protect@9.0.0 diff --git a/examples/next-drizzle-mysql/README.md b/examples/next-drizzle-mysql/README.md deleted file mode 100644 index d11fe529..00000000 --- a/examples/next-drizzle-mysql/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Next.js + Drizzle ORM + MySQL + Protect.js Example - -This example demonstrates how to build a modern web application using: -- [Next.js](https://nextjs.org/) - React framework for production -- [Drizzle ORM](https://orm.drizzle.team/) - TypeScript ORM for SQL databases -- [MySQL](https://www.mysql.com/) - Popular open-source relational database -- [Protect.js](https://cipherstash.com/protect) - Data protection and encryption library - -## Features - -- Full-stack TypeScript application -- Database migrations and schema management with Drizzle -- Data protection and encryption with Protect.js -- Modern UI with Tailwind CSS -- Form handling with React Hook Form and Zod validation -- Docker-based MySQL database setup - -## Prerequisites - -- Node.js 18+ -- Docker and Docker Compose -- MySQL (if running locally without Docker) - -## Getting Started - -1. Clone the repository and install dependencies: - ```bash - pnpm install - ``` - -2. Set up your environment variables: - Copy the `.env.example` file to `.env.local`: - ```bash - cp .env.example .env.local - ``` - Then update the environment variables in `.env.local` with your Protect.js configuration values. - -3. Start the MySQL database using Docker: - ```bash - docker compose up -d - ``` - -4. Run database migrations: - ```bash - pnpm run db:generate - pnpm run db:migrate - ``` - -5. Start the development server: - ```bash - pnpm run dev - ``` - -The application will be available at `http://localhost:3000`. - -## Project Structure - -- `/src` - Application source code -- `/drizzle` - Database migrations and schema -- `/public` - Static assets -- `drizzle.config.ts` - Drizzle ORM configuration -- `docker-compose.yml` - Docker configuration for MySQL - -## Available Scripts - -- `npm run dev` - Start development server -- `npm run build` - Build for production -- `npm run start` - Start production server -- `npm run db:generate` - Generate database migrations -- `npm run db:migrate` - Run database migrations - -## Learn More - -- [Next.js Documentation](https://nextjs.org/docs) -- [Drizzle ORM Documentation](https://orm.drizzle.team/docs/overview) -- [Protect.js Documentation](https://cipherstash.com/protect/docs) -- [MySQL Documentation](https://dev.mysql.com/doc/) diff --git a/examples/next-drizzle-mysql/docker-compose.yml b/examples/next-drizzle-mysql/docker-compose.yml deleted file mode 100644 index 3b40c63d..00000000 --- a/examples/next-drizzle-mysql/docker-compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: '3.8' -services: - db: - image: mysql:latest - environment: - MYSQL_ROOT_PASSWORD: password - MYSQL_DATABASE: protect_example - MYSQL_USER: protect_example - MYSQL_PASSWORD: password - ports: - - "3306:3306" - volumes: - - mysql_data:/var/lib/mysql - -volumes: - mysql_data: diff --git a/examples/next-drizzle-mysql/drizzle.config.ts b/examples/next-drizzle-mysql/drizzle.config.ts deleted file mode 100644 index ed499c10..00000000 --- a/examples/next-drizzle-mysql/drizzle.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import 'dotenv/config' -import { defineConfig } from 'drizzle-kit' -export default defineConfig({ - dialect: 'mysql', - schema: './src/db/schema.ts', - dbCredentials: { - host: '127.0.0.1', - port: 3306, - user: 'protect_example', - password: 'password', - database: 'protect_example', - }, -}) - -// mysql://protect_example:password@127.0.0.1:3306/protect_example diff --git a/examples/next-drizzle-mysql/drizzle/0000_brave_madrox.sql b/examples/next-drizzle-mysql/drizzle/0000_brave_madrox.sql deleted file mode 100644 index 7e8f651b..00000000 --- a/examples/next-drizzle-mysql/drizzle/0000_brave_madrox.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE `users` ( - `id` int AUTO_INCREMENT NOT NULL, - `name` json, - `email` json, - CONSTRAINT `users_id` PRIMARY KEY(`id`) -); diff --git a/examples/next-drizzle-mysql/drizzle/meta/0000_snapshot.json b/examples/next-drizzle-mysql/drizzle/meta/0000_snapshot.json deleted file mode 100644 index 9f32d367..00000000 --- a/examples/next-drizzle-mysql/drizzle/meta/0000_snapshot.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "version": "5", - "dialect": "mysql", - "id": "03335511-a5f1-45e4-bcf7-227e326b28a5", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "users": { - "name": "users", - "columns": { - "id": { - "name": "id", - "type": "int", - "primaryKey": false, - "notNull": true, - "autoincrement": true - }, - "name": { - "name": "name", - "type": "json", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "email": { - "name": "email", - "type": "json", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": { - "users_id": { - "name": "users_id", - "columns": ["id"] - } - }, - "uniqueConstraints": {}, - "checkConstraint": {} - } - }, - "views": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "tables": {}, - "indexes": {} - } -} diff --git a/examples/next-drizzle-mysql/drizzle/meta/_journal.json b/examples/next-drizzle-mysql/drizzle/meta/_journal.json deleted file mode 100644 index 1e308553..00000000 --- a/examples/next-drizzle-mysql/drizzle/meta/_journal.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": "7", - "dialect": "mysql", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1748545269720, - "tag": "0000_brave_madrox", - "breakpoints": true - } - ] -} diff --git a/examples/next-drizzle-mysql/next.config.ts b/examples/next-drizzle-mysql/next.config.ts deleted file mode 100644 index 0786faa6..00000000 --- a/examples/next-drizzle-mysql/next.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NextConfig } from 'next' - -const nextConfig: NextConfig = { - serverExternalPackages: ['@cipherstash/protect', 'mysql2'], -} - -export default nextConfig diff --git a/examples/next-drizzle-mysql/package.json b/examples/next-drizzle-mysql/package.json deleted file mode 100644 index d13483d7..00000000 --- a/examples/next-drizzle-mysql/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "next-drizzle-mysql", - "version": "0.2.17", - "private": true, - "scripts": { - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate" - }, - "dependencies": { - "@cipherstash/protect": "workspace:*", - "@hookform/resolvers": "^5.0.1", - "drizzle-orm": "^0.44.0", - "mysql2": "^3.14.1", - "next": "catalog:security", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-hook-form": "^7.56.4", - "zod": "^3.24.2" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "dotenv": "^16.4.7", - "drizzle-kit": "^0.30.5", - "tailwindcss": "^4", - "typescript": "^5" - } -} diff --git a/examples/next-drizzle-mysql/postcss.config.mjs b/examples/next-drizzle-mysql/postcss.config.mjs deleted file mode 100644 index 86e8e3c4..00000000 --- a/examples/next-drizzle-mysql/postcss.config.mjs +++ /dev/null @@ -1,5 +0,0 @@ -const config = { - plugins: ['@tailwindcss/postcss'], -} - -export default config diff --git a/examples/next-drizzle-mysql/public/file.svg b/examples/next-drizzle-mysql/public/file.svg deleted file mode 100644 index 004145cd..00000000 --- a/examples/next-drizzle-mysql/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/next-drizzle-mysql/public/globe.svg b/examples/next-drizzle-mysql/public/globe.svg deleted file mode 100644 index 567f17b0..00000000 --- a/examples/next-drizzle-mysql/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/next-drizzle-mysql/public/next.svg b/examples/next-drizzle-mysql/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/examples/next-drizzle-mysql/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/next-drizzle-mysql/public/vercel.svg b/examples/next-drizzle-mysql/public/vercel.svg deleted file mode 100644 index 77053960..00000000 --- a/examples/next-drizzle-mysql/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/next-drizzle-mysql/public/window.svg b/examples/next-drizzle-mysql/public/window.svg deleted file mode 100644 index b2b2a44f..00000000 --- a/examples/next-drizzle-mysql/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/next-drizzle-mysql/src/app/actions.ts b/examples/next-drizzle-mysql/src/app/actions.ts deleted file mode 100644 index 7ef7e663..00000000 --- a/examples/next-drizzle-mysql/src/app/actions.ts +++ /dev/null @@ -1,29 +0,0 @@ -'use server' - -import type { FormData } from '@/components/form' -import { db } from '@/db' -import { users } from '@/db/schema' -import { protectClient } from '@/protect' -import { users as protectedUsers } from '@/protect/schema' - -export async function createUser(data: FormData) { - console.log(data) - - const result = await protectClient.encryptModel(data, protectedUsers) - - if (result.failure) { - console.error(result.failure.message) - return - } - - console.log(result.data) - - await db.insert(users).values({ - name: result.data.name, - email: result.data.email, - }) - - return { - success: true, - } -} diff --git a/examples/next-drizzle-mysql/src/app/favicon.ico b/examples/next-drizzle-mysql/src/app/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/examples/next-drizzle-mysql/src/app/favicon.ico and /dev/null differ diff --git a/examples/next-drizzle-mysql/src/app/globals.css b/examples/next-drizzle-mysql/src/app/globals.css deleted file mode 100644 index a2dc41ec..00000000 --- a/examples/next-drizzle-mysql/src/app/globals.css +++ /dev/null @@ -1,26 +0,0 @@ -@import "tailwindcss"; - -:root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/examples/next-drizzle-mysql/src/app/layout.tsx b/examples/next-drizzle-mysql/src/app/layout.tsx deleted file mode 100644 index 8005856e..00000000 --- a/examples/next-drizzle-mysql/src/app/layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { Metadata } from 'next' -import { Geist, Geist_Mono } from 'next/font/google' -import './globals.css' - -const geistSans = Geist({ - variable: '--font-geist-sans', - subsets: ['latin'], -}) - -const geistMono = Geist_Mono({ - variable: '--font-geist-mono', - subsets: ['latin'], -}) - -export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -} - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode -}>) { - return ( - - - {children} - - - ) -} diff --git a/examples/next-drizzle-mysql/src/app/page.tsx b/examples/next-drizzle-mysql/src/app/page.tsx deleted file mode 100644 index 6194c61b..00000000 --- a/examples/next-drizzle-mysql/src/app/page.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { ClientForm } from '@/components/form' -import { db } from '@/db' -import { users } from '@/db/schema' -import { protectClient } from '@/protect' -import { users as protectedUsers } from '@/protect/schema' - -type User = { - id: number - name: string - email: string -} - -export default async function Home() { - const u = await db.select().from(users).limit(10) - - const decryptedUsers = await protectClient.bulkDecryptModels(u) - - if (decryptedUsers.failure) { - throw new Error(decryptedUsers.failure.message) - } - - return ( -
- - - - - - - - - - - {decryptedUsers.data.map((user) => ( - - - - - - ))} - -
IDNameEmail
{user.id}{user.name as string}{user.email as string}
-
- ) -} diff --git a/examples/next-drizzle-mysql/src/components/form.tsx b/examples/next-drizzle-mysql/src/components/form.tsx deleted file mode 100644 index c26923bd..00000000 --- a/examples/next-drizzle-mysql/src/components/form.tsx +++ /dev/null @@ -1,78 +0,0 @@ -'use client' - -import { createUser } from '@/app/actions' -import { zodResolver } from '@hookform/resolvers/zod' -import { useTransition } from 'react' -import { useForm } from 'react-hook-form' -import * as z from 'zod' - -const formSchema = z.object({ - name: z.string().min(1, 'Name is required'), - email: z.string().email('Invalid email address'), -}) - -export type FormData = z.infer - -export function ClientForm() { - const [isPending, startTransition] = useTransition() - const { - register, - handleSubmit, - reset, - formState: { errors }, - } = useForm({ - resolver: zodResolver(formSchema), - }) - - const onSubmit = (data: FormData) => { - startTransition(async () => { - await createUser(data) - reset() - }) - } - - return ( -
-
-
- - - {errors.name && ( -

{errors.name.message}

- )} -
-
- - - {errors.email && ( -

{errors.email.message}

- )} -
- -
-
- ) -} diff --git a/examples/next-drizzle-mysql/src/db/index.ts b/examples/next-drizzle-mysql/src/db/index.ts deleted file mode 100644 index 89c86289..00000000 --- a/examples/next-drizzle-mysql/src/db/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { drizzle } from 'drizzle-orm/mysql2' - -if (!process.env.DATABASE_URL) { - throw new Error('DATABASE_URL is not set') -} - -export const db = drizzle(process.env.DATABASE_URL) diff --git a/examples/next-drizzle-mysql/src/db/schema.ts b/examples/next-drizzle-mysql/src/db/schema.ts deleted file mode 100644 index d405603e..00000000 --- a/examples/next-drizzle-mysql/src/db/schema.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { int, json, mysqlTable, uniqueIndex } from 'drizzle-orm/mysql-core' - -export const users = mysqlTable('users', { - id: int().primaryKey().autoincrement(), - name: json(), - email: json(), -}) diff --git a/examples/next-drizzle-mysql/src/protect/index.ts b/examples/next-drizzle-mysql/src/protect/index.ts deleted file mode 100644 index 339a8cb6..00000000 --- a/examples/next-drizzle-mysql/src/protect/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type ProtectClientConfig, protect } from '@cipherstash/protect' -import { users } from './schema' - -const config: ProtectClientConfig = { - schemas: [users], -} - -export const protectClient = await protect(config) diff --git a/examples/next-drizzle-mysql/src/protect/schema.ts b/examples/next-drizzle-mysql/src/protect/schema.ts deleted file mode 100644 index a9591051..00000000 --- a/examples/next-drizzle-mysql/src/protect/schema.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { csColumn, csTable } from '@cipherstash/protect' - -export const users = csTable('users', { - email: csColumn('email'), - name: csColumn('name'), -}) diff --git a/examples/next-drizzle-mysql/tsconfig.json b/examples/next-drizzle-mysql/tsconfig.json deleted file mode 100644 index c1334095..00000000 --- a/examples/next-drizzle-mysql/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/examples/nextjs-clerk/.env.example b/examples/nextjs-clerk/.env.example deleted file mode 100644 index dbb3cfa2..00000000 --- a/examples/nextjs-clerk/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# Clerk -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key -CLERK_SECRET_KEY=your_clerk_secret_key - -# Postres - Try out Supabase for free https://supabase.com/ -POSTGRES_URL=your_postgres_url - -# CipherStash Protect.js -CS_WORKSPACE_ID=your_workspace_id -CS_CLIENT_ID=your_client_id -CS_CLIENT_KEY=your_client_secret -CS_CLIENT_ACCESS_KEY=your_access_key \ No newline at end of file diff --git a/examples/nextjs-clerk/CHANGELOG.md b/examples/nextjs-clerk/CHANGELOG.md deleted file mode 100644 index 5e47ebf1..00000000 --- a/examples/nextjs-clerk/CHANGELOG.md +++ /dev/null @@ -1,256 +0,0 @@ -# @cipherstash/nextjs-clerk-example - -## 0.2.18 - -### Patch Changes - -- Updated dependencies [db72e2c] -- Updated dependencies [e769740] - - @cipherstash/protect@10.5.0 - -## 0.2.17 - -### Patch Changes - -- Updated dependencies [9ccaf68] - - @cipherstash/protect@10.4.0 - -## 0.2.16 - -### Patch Changes - -- Updated dependencies [a1fce2b] -- Updated dependencies [622b684] - - @cipherstash/protect@10.3.0 - -## 0.2.15 - -### Patch Changes - -- @cipherstash/protect@10.2.1 - -## 0.2.14 - -### Patch Changes - -- Updated dependencies [1535259] - - @cipherstash/nextjs@4.1.0 - -## 0.2.13 - -### Patch Changes - -- Updated dependencies [de029de] - - @cipherstash/protect@10.2.0 - -## 0.2.12 - -### Patch Changes - -- Updated dependencies [ff4421f] - - @cipherstash/protect@10.1.1 - -## 0.2.11 - -### Patch Changes - -- Updated dependencies [6b87c17] - - @cipherstash/protect@10.1.0 - -## 0.2.10 - -### Patch Changes - -- @cipherstash/protect@10.0.2 - -## 0.2.9 - -### Patch Changes - -- @cipherstash/protect@10.0.1 - -## 0.2.8 - -### Patch Changes - -- Updated dependencies [788dbfc] - - @cipherstash/protect@10.0.0 - -## 0.2.7 - -### Patch Changes - -- Updated dependencies [c7ed7ab] -- Updated dependencies [211e979] - - @cipherstash/protect@9.6.0 - -## 0.2.6 - -### Patch Changes - -- Updated dependencies [6f45b02] - - @cipherstash/protect@9.5.0 - -## 0.2.5 - -### Patch Changes - -- @cipherstash/protect@9.4.1 - -## 0.2.4 - -### Patch Changes - -- Updated dependencies [1cc4772] - - @cipherstash/protect@9.4.0 - -## 0.2.3 - -### Patch Changes - -- Updated dependencies [01fed9e] - - @cipherstash/protect@9.3.0 - -## 0.2.2 - -### Patch Changes - -- Updated dependencies [587f222] - - @cipherstash/protect@9.2.0 - -## 0.2.1 - -### Patch Changes - -- Updated dependencies [c8468ee] - - @cipherstash/protect@9.1.0 - -## 0.2.0 - -### Minor Changes - -- 1bc55a0: Implemented a more configurable pattern for the Protect client. - - This release introduces a new `ProtectClientConfig` type that can be used to configure the Protect client. - This is useful if you want to configure the Protect client specific to your application, and will future proof any additional configuration options that are added in the future. - - ```ts - import { protect, type ProtectClientConfig } from "@cipherstash/protect"; - - const config: ProtectClientConfig = { - schemas: [users, orders], - workspaceCrn: "your-workspace-crn", - accessKey: "your-access-key", - clientId: "your-client-id", - clientKey: "your-client-key", - }; - - const protectClient = await protect(config); - ``` - - The now deprecated method of passing your tables to the `protect` client is no longer supported. - - ```ts - import { protect, type ProtectClientConfig } from "@cipherstash/protect"; - - // old method (no longer supported) - const protectClient = await protect(users, orders); - - // required method - const config: ProtectClientConfig = { - schemas: [users, orders], - }; - - const protectClient = await protect(config); - ``` - -### Patch Changes - -- Updated dependencies [1bc55a0] - - @cipherstash/protect@9.0.0 - -## 0.1.6 - -### Patch Changes - -- Updated dependencies [a471821] - - @cipherstash/protect@8.4.0 - -## 0.1.5 - -### Patch Changes - -- Updated dependencies [628acdc] - - @cipherstash/protect@8.3.0 - -## 0.1.4 - -### Patch Changes - -- Updated dependencies [0883e16] - - @cipherstash/protect@8.2.0 - -## 0.1.3 - -### Patch Changes - -- Updated dependencies [95c891d] -- Updated dependencies [18d3653] - - @cipherstash/nextjs@4.0.0 - - @cipherstash/protect@8.1.0 - -## 0.1.2 - -### Patch Changes - -- Updated dependencies [8a4ea80] - - @cipherstash/protect@8.0.0 - -## 0.1.1 - -### Patch Changes - -- Updated dependencies [2cb2d84] - - @cipherstash/protect@7.0.0 - -## 0.1.0 - -### Minor Changes - -- 9377b47: Updated versions to address Next.js CVE. - -### Patch Changes - -- Updated dependencies [9377b47] - - @cipherstash/nextjs@3.2.0 - -## 0.0.4 - -### Patch Changes - -- Updated dependencies [a564f21] - - @cipherstash/protect@6.3.0 - - @cipherstash/nextjs@3.1.0 - -## 0.0.3 - -### Patch Changes - -- Updated dependencies [fe4b443] - - @cipherstash/protect@6.2.0 - -## 0.0.2 - -### Patch Changes - -- Updated dependencies [43e1acb] - - @cipherstash/protect@6.1.0 - -## 0.0.1 - -### Patch Changes - -- Updated dependencies [02dc980] -- Updated dependencies [f4d8334] - - @cipherstash/nextjs@3.0.0 - - @cipherstash/protect@6.0.0 diff --git a/examples/nextjs-clerk/README.md b/examples/nextjs-clerk/README.md deleted file mode 100644 index 273c900b..00000000 --- a/examples/nextjs-clerk/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# Protect.js + Next.js + Clerk example - -This example demonstrates how to use Protect.js with Next.js. It also demonstrates how to use Lock Contexts to ensure that only the intended users can access sensitive data, by using Clerk for authentication. - -This project uses the following technologies: - -- [pnpm](https://pnpm.io) for package management -- [Next.js](https://nextjs.org) for the application framework -- [Clerk](https://clerk.com) for auth -- [Supabase](https://supabase.com) for database -- [Drizzle ORM](https://drizzle.org) for database access -- [CipherStash](https://cipherstash.com) for data encryption - -## Getting Started - -First, install dependencies: - -```bash -pnpm install -``` - -Second, create a `.env.local` file in the root directory with the following content: - -```bash -# Clerk auth -NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= -CLERK_SECRET_KEY= - -# Supabase postgres connection string -POSTGRES_URL= - -# CipherStash encryption and access keys -CS_CLIENT_ID= -CS_CLIENT_KEY= -CS_CLIENT_ACCESS_KEY= -CS_WORKSPACE_ID= -``` - -Finally, run the development server: - -```bash -pnpm run dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -## Database - -The database is hosted on Supabase and has the following schema which is defined using the Drizzle ORM: - -```ts -// Data that is encrypted using protect.js is stored as jsonb in postgres - -export const users = pgTable("users", { - id: serial("id").primaryKey(), - name: varchar("name").notNull(), - email: jsonb("email").notNull(), - role: varchar("role").notNull(), -}); -``` - -> [!NOTE] -> This example does not include any searchable encrypted fields. -> If you want to search on encrypted fields, you will need to install EQL. -> The EQL library ships with custom types that are used to define encrypted fields. -> See the [EQL documentation](https://github.com/cipherstash/encrypted-query-language) for more information. - -## @cipherstash/protect - -All the email data is encrypted using Protect.js. -The cipherstext is stored in the `email` column of the `users` table. -The application is configured to only decrypt the data when the user is signed in, otherwise it will display the encrypted data. - -### Npm package - -`@cipherstash/protect` uses custom Rust bindings to the CipherStash Client in order to perform encryptions and decryptions. -We leverage the [Neon project](https://neon-rs.dev/) to provide a JavaScript API for these bindings. - -### Encryption - -When a user is added to the database, the email address is encrypted using Protect.js. -To view the encryption implementation, see the `addUser` function in [src/lib/actions.ts](src/lib/actions.ts). - -### Decryption - -To view the decrpytion implementation, see the `getUsers` function in [src/app/page.tsx](src/app/page.tsx). - -### Next.js - -Since `@cipherstash/protect` is a native Node.js module, you need to opt-out from the Server Components bundling and use native Node.js `require` instead. - -#### Using version 15 or later - -`next.config.ts` [configuration](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages): - -```js -const nextConfig = { - ... - serverExternalPackages: ['@cipherstash/protect'], -} -``` - -#### Using version 14 - -`next.config.mjs` [configuration](https://nextjs.org/docs/14/app/api-reference/next-config-js/serverComponentsExternalPackages): - -```js -const nextConfig = { - ... - experimental: { - serverComponentsExternalPackages: ['@cipherstash/protect'], - }, -} -``` - -#### Workspace package issue - -`serverExternalPackages` does not work with workspace packages and the issues is being tracked [here](https://github.com/vercel/next.js/issues/43433). - -Once this is fixed upstream, this application can use the workspace package for development. -For the time being, it used `@cipherstash/protect` from the npm registry. \ No newline at end of file diff --git a/examples/nextjs-clerk/components.json b/examples/nextjs-clerk/components.json deleted file mode 100644 index 405a08cd..00000000 --- a/examples/nextjs-clerk/components.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} diff --git a/examples/nextjs-clerk/drizzle.config.ts b/examples/nextjs-clerk/drizzle.config.ts deleted file mode 100644 index 68860aeb..00000000 --- a/examples/nextjs-clerk/drizzle.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import 'dotenv/config' -import { defineConfig } from 'drizzle-kit' - -export default defineConfig({ - out: './drizzle', - schema: './src/db/schema.ts', - dialect: 'postgresql', - dbCredentials: { - // biome-ignore lint/style/noNonNullAssertion: Postgres URL is required - url: process.env.POSTGRES_URL!, - }, -}) diff --git a/examples/nextjs-clerk/next.config.ts b/examples/nextjs-clerk/next.config.ts deleted file mode 100644 index 69f6bd42..00000000 --- a/examples/nextjs-clerk/next.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { NextConfig } from 'next' - -const nextConfig: NextConfig = { - images: { - remotePatterns: [ - { - protocol: 'https', - hostname: 'cipherstash.com', - }, - ], - }, - // serverExternalPackages does not work with workspace packages - // https://github.com/vercel/next.js/issues/43433 - // --- - // TODO: Once this is fixed upstream, we can use the workspace packages - serverExternalPackages: ['@cipherstash/protect'], -} - -export default nextConfig diff --git a/examples/nextjs-clerk/package.json b/examples/nextjs-clerk/package.json deleted file mode 100644 index 6c31b881..00000000 --- a/examples/nextjs-clerk/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@cipherstash/nextjs-clerk-example", - "version": "0.2.18", - "private": true, - "scripts": { - "check-types": "tsc --noEmit", - "dev": "next dev --turbopack", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@cipherstash/nextjs": "workspace:*", - "@cipherstash/protect": "workspace:*", - "@clerk/nextjs": "catalog:security", - "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-select": "^2.1.4", - "@radix-ui/react-slot": "^1.1.1", - "@radix-ui/react-toast": "^1.2.5", - "@radix-ui/react-tooltip": "^1.1.7", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "dotenv": "^16.4.7", - "drizzle-orm": "^0.38.2", - "jose": "^5.9.6", - "lucide-react": "^0.469.0", - "next": "catalog:security", - "postgres": "^3.4.5", - "react": "^19", - "react-dom": "^19", - "tailwind-merge": "^2.5.5", - "tailwindcss-animate": "^1.0.7" - }, - "devDependencies": { - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", - "drizzle-kit": "^0.30.5", - "postcss": "^8", - "tailwindcss": "^3.4.1", - "tsx": "catalog:repo", - "typescript": "catalog:repo" - } -} diff --git a/examples/nextjs-clerk/postcss.config.mjs b/examples/nextjs-clerk/postcss.config.mjs deleted file mode 100644 index 0dc456ad..00000000 --- a/examples/nextjs-clerk/postcss.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('postcss-load-config').Config} */ -const config = { - plugins: { - tailwindcss: {}, - }, -} - -export default config diff --git a/examples/nextjs-clerk/public/file.svg b/examples/nextjs-clerk/public/file.svg deleted file mode 100644 index 004145cd..00000000 --- a/examples/nextjs-clerk/public/file.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/nextjs-clerk/public/globe.svg b/examples/nextjs-clerk/public/globe.svg deleted file mode 100644 index 567f17b0..00000000 --- a/examples/nextjs-clerk/public/globe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/nextjs-clerk/public/next.svg b/examples/nextjs-clerk/public/next.svg deleted file mode 100644 index 5174b28c..00000000 --- a/examples/nextjs-clerk/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/nextjs-clerk/public/vercel.svg b/examples/nextjs-clerk/public/vercel.svg deleted file mode 100644 index 77053960..00000000 --- a/examples/nextjs-clerk/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/nextjs-clerk/public/window.svg b/examples/nextjs-clerk/public/window.svg deleted file mode 100644 index b2b2a44f..00000000 --- a/examples/nextjs-clerk/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/examples/nextjs-clerk/src/app/add-user/page.tsx b/examples/nextjs-clerk/src/app/add-user/page.tsx deleted file mode 100644 index 9ab22f9d..00000000 --- a/examples/nextjs-clerk/src/app/add-user/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import AddUserForm from '@/components/AddUserForm' -import Header from '@/components/Header' - -export default function AddUser() { - return ( -
-
-
-

Add new user

- -
-
- ) -} diff --git a/examples/nextjs-clerk/src/app/favicon.ico b/examples/nextjs-clerk/src/app/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/examples/nextjs-clerk/src/app/favicon.ico and /dev/null differ diff --git a/examples/nextjs-clerk/src/app/globals.css b/examples/nextjs-clerk/src/app/globals.css deleted file mode 100644 index a23ac26b..00000000 --- a/examples/nextjs-clerk/src/app/globals.css +++ /dev/null @@ -1,72 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -body { - font-family: Arial, Helvetica, sans-serif; -} - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - } - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/examples/nextjs-clerk/src/app/layout.tsx b/examples/nextjs-clerk/src/app/layout.tsx deleted file mode 100644 index b835533a..00000000 --- a/examples/nextjs-clerk/src/app/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Toaster } from '@/components/ui/toaster' -import { ClerkProvider } from '@clerk/nextjs' -import type { Metadata } from 'next' -import './globals.css' - -export const metadata: Metadata = { - title: 'Protect.js + Next.js + Clerk', - description: 'An example of using Protect.js with Next.js and Clerk', -} - -export default function Layout({ children }: { children: React.ReactNode }) { - return ( - - - -
{children}
- - - -
- ) -} diff --git a/examples/nextjs-clerk/src/app/page.tsx b/examples/nextjs-clerk/src/app/page.tsx deleted file mode 100644 index 0008947c..00000000 --- a/examples/nextjs-clerk/src/app/page.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { db } from '@/core/db' -import { users } from '@/core/db/schema' -import { getLockContext, protectClient } from '@/core/protect' -import { getCtsToken } from '@cipherstash/nextjs' -import type { EncryptedData } from '@cipherstash/protect' -import { auth, currentUser } from '@clerk/nextjs/server' -import Header from '../components/Header' -import UserTable from '../components/UserTable' - -export type EncryptedUser = { - id: number - name: string - email: string | null - authorized: boolean - role: string -} - -async function getUsers(): Promise { - const { userId } = await auth() - const token = await getCtsToken() - const results = await db.select().from(users).limit(500) - - if (userId && token.success) { - const cts_token = token.ctsToken - const lockContext = getLockContext(cts_token) - - const promises = results.map(async (row) => { - const decryptResult = await protectClient - .decrypt(row.email as EncryptedData) - .withLockContext(lockContext) - - if (decryptResult.failure) { - console.error( - 'Failed to decrypt the email for user', - row.id, - decryptResult.failure.message, - ) - - return row.email - } - - return decryptResult.data - }) - - const data = (await Promise.allSettled(promises)) as PromiseSettledResult< - string | null - >[] - - return results.map((row, index) => ({ - ...row, - authorized: data[index].status === 'fulfilled', - email: - data[index].status === 'fulfilled' - ? data[index].value - : (row.email as { c: string }).c, - })) - } - - return results.map((row) => ({ - id: row.id, - name: row.name, - authorized: false, - email: (row.email as { c: string })?.c, - role: row.role, - })) -} - -export default async function Home() { - const users = await getUsers() - const user = await currentUser() - - return ( -
-
-
-
-

Users

- - The email address of each user was encrypted with CipherStash and{' '} - locked to the individual who created the user. Only that - individual will be able to decrypt the email. - -
- -
-
- ) -} diff --git a/examples/nextjs-clerk/src/components/AddUserForm.tsx b/examples/nextjs-clerk/src/components/AddUserForm.tsx deleted file mode 100644 index c645a633..00000000 --- a/examples/nextjs-clerk/src/components/AddUserForm.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client' - -import { useToast } from '@/hooks/use-toast' -import { useRouter } from 'next/navigation' -import { useState } from 'react' -import { addUser } from '../lib/actions' -import { Button } from './ui/button' -import { Input } from './ui/input' -import { Label } from './ui/label' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from './ui/select' - -export default function AddUserForm() { - const [role, setRole] = useState('') - const router = useRouter() - const { toast } = useToast() - - const handleSubmit = async (formData: FormData) => { - formData.append('role', role) - const result = await addUser(formData) - if (result.error) { - toast({ - title: 'Error', - description: result.error, - variant: 'destructive', - }) - } else { - toast({ - title: 'Success', - description: 'User added successfully', - }) - router.push('/') - } - } - - return ( -
-
- - -
-
- - -
-
- - -
- -
- ) -} diff --git a/examples/nextjs-clerk/src/components/Header.tsx b/examples/nextjs-clerk/src/components/Header.tsx deleted file mode 100644 index 3163b87a..00000000 --- a/examples/nextjs-clerk/src/components/Header.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbSeparator, -} from '@/components/ui/breadcrumb' -import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs' -import Image from 'next/image' -import Link from 'next/link' - -import { Github, KeyIcon } from 'lucide-react' -import { Button } from './ui/button' - -export default function Header() { - return ( -
-
-
- - Logo - -
- / -

protect.js

- / - - - - Users - - - - Add a user - - - -
-
-
- - - - - - - - -
-
-
- ) -} diff --git a/examples/nextjs-clerk/src/components/UserTable.tsx b/examples/nextjs-clerk/src/components/UserTable.tsx deleted file mode 100644 index 97c46e1a..00000000 --- a/examples/nextjs-clerk/src/components/UserTable.tsx +++ /dev/null @@ -1,66 +0,0 @@ -'use client' - -import { InfoIcon } from 'lucide-react' -import type { EncryptedUser } from '../app/page' -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from './ui/table' - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip' - -export default function UserTable({ - users, - email = 'Your user', -}: { users: EncryptedUser[]; email?: string }) { - return ( -
- - - - Name - Email - Role - - - - {users.map((user) => ( - - {user.name} - - - {user.email} - - {!user.authorized && ( - - - - - - -

- {email} is not authorized to decrypt this user's - email. -

-
-
-
- )} -
- {user.role} -
- ))} -
-
-
- ) -} diff --git a/examples/nextjs-clerk/src/components/ui/breadcrumb.tsx b/examples/nextjs-clerk/src/components/ui/breadcrumb.tsx deleted file mode 100644 index 1dcf2a56..00000000 --- a/examples/nextjs-clerk/src/components/ui/breadcrumb.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Slot } from '@radix-ui/react-slot' -import { ChevronRight, MoreHorizontal } from 'lucide-react' -import * as React from 'react' - -import { cn } from '@/lib/utils' - -const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<'nav'> & { - separator?: React.ReactNode - } ->(({ ...props }, ref) =>