├── .changeset ├── README.md └── config.json ├── .github ├── ISSUE_TEMPLATE │ └── docs-feedback.yml └── workflows │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTE.md ├── LICENSE.md ├── README.md ├── biome.json ├── docker-compose.yml ├── docs ├── README.md ├── concepts │ └── searchable-encryption.md ├── getting-started.md ├── how-to │ ├── lock-contexts-with-clerk.md │ ├── nextjs-external-packages.md │ ├── searchable-encryption.md │ └── sst-external-packages.md └── reference │ ├── configuration.md │ ├── model-operations.md │ ├── schema.md │ ├── searchable-encryption-postgres.md │ └── supabase-sdk.md ├── examples ├── basic │ ├── CHANGELOG.md │ ├── README.md │ ├── index.ts │ ├── package.json │ ├── protect.ts │ └── tsconfig.json ├── drizzle │ ├── .env.example │ ├── README.md │ ├── drizzle.config.ts │ ├── drizzle │ │ ├── 0000_goofy_cannonball.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ └── _journal.json │ ├── environment.d.ts │ ├── package.json │ ├── src │ │ ├── db │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ ├── insert.ts │ │ ├── protect.ts │ │ └── select.ts │ └── tsconfig.json ├── dynamo │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── docker-compose.yml │ ├── package.json │ ├── sql │ │ └── .gitkeep │ └── src │ │ ├── bulk-operations.ts │ │ ├── common │ │ ├── dynamo.ts │ │ ├── log.ts │ │ └── protect.ts │ │ ├── encrypted-key-in-gsi.ts │ │ ├── encrypted-partition-key.ts │ │ ├── encrypted-sort-key.ts │ │ ├── export-to-pg.ts │ │ └── simple.ts ├── hono-supabase │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── environment.d.ts │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── next-drizzle-mysql │ ├── .env.example │ ├── CHANGELOG.md │ ├── README.md │ ├── docker-compose.yml │ ├── drizzle.config.ts │ ├── drizzle │ │ ├── 0000_brave_madrox.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ └── _journal.json │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ │ ├── file.svg │ │ ├── globe.svg │ │ ├── next.svg │ │ ├── vercel.svg │ │ └── window.svg │ ├── src │ │ ├── app │ │ │ ├── actions.ts │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── components │ │ │ └── form.tsx │ │ ├── db │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ └── protect │ │ │ ├── index.ts │ │ │ └── schema.ts │ └── tsconfig.json └── nextjs-clerk │ ├── .env.example │ ├── CHANGELOG.md │ ├── README.md │ ├── components.json │ ├── drizzle.config.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ ├── file.svg │ ├── globe.svg │ ├── next.svg │ ├── vercel.svg │ └── window.svg │ ├── src │ ├── app │ │ ├── add-user │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components │ │ ├── AddUserForm.tsx │ │ ├── Header.tsx │ │ ├── UserTable.tsx │ │ └── ui │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── select.tsx │ │ │ ├── table.tsx │ │ │ ├── toast.tsx │ │ │ ├── toaster.tsx │ │ │ └── tooltip.tsx │ ├── core │ │ ├── db │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ └── protect │ │ │ └── index.ts │ ├── hooks │ │ └── use-toast.ts │ ├── lib │ │ ├── actions.ts │ │ └── utils.ts │ └── middleware.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── mise.toml ├── package.json ├── packages ├── jseql │ └── README.md ├── nextjs │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── nextjs.test.ts │ ├── package.json │ ├── src │ │ ├── clerk │ │ │ └── index.ts │ │ ├── cts │ │ │ └── index.ts │ │ └── index.ts │ ├── tsconfig.jest.json │ ├── tsconfig.json │ └── tsup.config.ts ├── protect-dynamodb │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ └── dynamodb.test.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── types.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── protect │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── __tests__ │ │ ├── helpers.test.ts │ │ ├── protect.test.ts │ │ ├── search-terms.test.ts │ │ └── supabase.test.ts │ ├── package.json │ ├── src │ │ ├── ffi │ │ │ ├── index.ts │ │ │ ├── model-helpers.ts │ │ │ └── operations │ │ │ │ ├── bulk-decrypt-models.ts │ │ │ │ ├── bulk-encrypt-models.ts │ │ │ │ ├── decrypt-model.ts │ │ │ │ ├── decrypt.ts │ │ │ │ ├── encrypt-model.ts │ │ │ │ ├── encrypt.ts │ │ │ │ └── search-terms.ts │ │ ├── helpers │ │ │ └── index.ts │ │ ├── identify │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── schema │ │ │ └── index.ts │ │ └── types.ts │ ├── tsconfig.json │ └── tsup.config.ts └── utils │ ├── config │ └── index.ts │ └── logger │ └── index.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs-feedback.yml: -------------------------------------------------------------------------------- 1 | name: Docs feedback 2 | description: Feedback to help make our docs more effective. 3 | title: "[Docs]: " 4 | labels: ["docs", "triage"] 5 | assignees: 6 | - kateandrews 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to give us feedback! 12 | - type: input 13 | id: contact 14 | attributes: 15 | label: Contact details 16 | description: How can we get in touch with you if we need more info? 17 | placeholder: you@example.com 18 | validations: 19 | required: false 20 | - type: textarea 21 | id: what-problem 22 | attributes: 23 | label: What problem were you trying to solve? 24 | placeholder: Tell us what you were looking for in our docs. 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: additional-info 29 | attributes: 30 | label: Anything else you'd like us to know? 31 | placeholder: Let us know if you've got any additional feedback. 32 | validations: 33 | required: false 34 | - type: checkboxes 35 | id: terms 36 | attributes: 37 | label: Code of conduct 38 | description: By submitting this issue, you agree to follow our [Code of conduct](https://github.com/cipherstash/protectjs/blob/main/CODE_OF_CONDUCT.md). 39 | options: 40 | - label: I agree to follow this project's Code of conduct 41 | required: true 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release JS 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout Repo 16 | uses: actions/checkout@v3 17 | 18 | - uses: pnpm/action-setup@v4 19 | name: Install pnpm 20 | with: 21 | run_install: false 22 | 23 | - name: Install Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: 'pnpm' 28 | 29 | - name: Install dependencies 30 | run: pnpm install 31 | 32 | - name: Publish to npm 33 | id: changesets 34 | uses: changesets/action@v1 35 | with: 36 | publish: pnpm run release 37 | commitMode: 'github-api' 38 | env: 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test JS 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | branches: 9 | - "**" 10 | 11 | jobs: 12 | run-tests: 13 | name: Run Tests 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout Repo 18 | uses: actions/checkout@v3 19 | 20 | - uses: pnpm/action-setup@v4 21 | name: Install pnpm 22 | with: 23 | run_install: false 24 | 25 | - name: Install Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | cache: 'pnpm' 30 | 31 | - name: Install dependencies 32 | run: pnpm install 33 | 34 | - name: Create .env file in ./packages/protect/ 35 | run: | 36 | touch ./packages/protect/.env 37 | echo "CS_WORKSPACE_CRN=${{ secrets.CS_WORKSPACE_CRN }}" >> ./packages/protect/.env 38 | echo "CS_CLIENT_ID=${{ secrets.CS_CLIENT_ID }}" >> ./packages/protect/.env 39 | echo "CS_CLIENT_KEY=${{ secrets.CS_CLIENT_KEY }}" >> ./packages/protect/.env 40 | echo "CS_CLIENT_ACCESS_KEY=${{ secrets.CS_CLIENT_ACCESS_KEY }}" >> ./packages/protect/.env 41 | echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> ./packages/protect/.env 42 | echo "SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }}" >> ./packages/protect/.env 43 | 44 | # Run TurboRepo tests 45 | - name: Run tests 46 | run: pnpm run test 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .turbo 3 | dist 4 | mise.local.toml 5 | .env 6 | 7 | # dependencies 8 | /node_modules 9 | /.pnp 10 | .pnp.* 11 | .yarn/* 12 | !.yarn/patches 13 | !.yarn/plugins 14 | !.yarn/releases 15 | !.yarn/versions 16 | 17 | # testing 18 | /coverage 19 | 20 | # next.js 21 | /.next/ 22 | /out/ 23 | 24 | # production 25 | /build 26 | 27 | # misc 28 | .DS_Store 29 | *.pem 30 | 31 | # debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | .pnpm-debug.log* 36 | 37 | # env files (can opt-in for committing if needed) 38 | .env 39 | .env.local 40 | 41 | # vercel 42 | .vercel 43 | 44 | # typescript 45 | *.tsbuildinfo 46 | next-env.d.ts 47 | 48 | # turbo 49 | .turbo 50 | 51 | node_modules 52 | .next 53 | 54 | # ffi 55 | target 56 | index.node 57 | **/node_modules 58 | **/.DS_Store 59 | npm-debug.log* 60 | cargo.log 61 | cross.log 62 | mise.local.toml 63 | !.github/.env 64 | 65 | # cipherstash 66 | cipherstash.toml 67 | cipherstash.secret.toml 68 | sql/cipherstash-*.sql 69 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "conventionalCommits.scopes": ["protect", "examples"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 CipherStash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "files": { 4 | "ignore": ["**/package.json", ".turbo/", "dist"] 5 | }, 6 | "formatter": { 7 | "indentStyle": "space", 8 | "indentWidth": 2, 9 | "formatWithErrors": true 10 | }, 11 | "javascript": { 12 | "formatter": { 13 | "jsxQuoteStyle": "double", 14 | "quoteStyle": "single", 15 | "semicolons": "asNeeded" 16 | } 17 | }, 18 | "linter": { 19 | "rules": { 20 | "suspicious": { 21 | "noThenProperty": "off" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: &postgres 3 | image: postgres:latest 4 | environment: 5 | PGPORT: 5432 6 | POSTGRES_DB: "cipherstash" 7 | POSTGRES_USER: "cipherstash" 8 | PGUSER: "cipherstash" 9 | POSTGRES_PASSWORD: password 10 | ports: 11 | - 5432:5432 12 | deploy: 13 | resources: 14 | limits: 15 | cpus: "${CPU_LIMIT:-2}" 16 | memory: 2048mb 17 | restart: always 18 | healthcheck: 19 | test: [ "CMD-SHELL", "pg_isready" ] 20 | interval: 1s 21 | timeout: 5s 22 | retries: 10 23 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Protect.js documentation 2 | 3 | The documentation for Protect.js is organized into the following sections: 4 | 5 | - [Getting started](./getting-started.md) 6 | 7 | ## Concepts 8 | 9 | - [Searchable encryption](./concepts/searchable-encryption.md) 10 | 11 | ## Reference 12 | 13 | - [Configuration and production deployment](./reference/configuration.md) 14 | - [Searchable encryption with PostgreSQL](./reference/searchable-encryption-postgres.md) 15 | - [Protect.js schemas](./reference/schema.md) 16 | - [Model operations with bulk crypto functions](./reference/model-operations.md) 17 | 18 | ### ORMs and frameworks 19 | 20 | - [Supabase SDK](./reference/supabase-sdk.md) 21 | 22 | ## How-to guides 23 | 24 | - [Lock contexts with Clerk and Next.js](./how-to/lock-contexts-with-clerk.md) 25 | - [Next.js build notes](./how-to/nextjs-external-packages.md) 26 | - [SST and serverless function notes](./how-to/sst-external-packages.md) 27 | -------------------------------------------------------------------------------- /docs/how-to/lock-contexts-with-clerk.md: -------------------------------------------------------------------------------- 1 | 2 | # Locking context with Next.js and Clerk 3 | 4 | This how-to guide shows you how to use lock context if you're using [Clerk](https://clerk.com/) with Next.js. 5 | 6 | ## Table of contents 7 | 8 | - [Getting started](#getting-started) 9 | - [Retrieving the CTS token in Next.js](#retrieving-the-cts-token-in-nextjs) 10 | - [Constructing a LockContext with an existing CTS token](#constructing-a-lockcontext-with-an-existing-cts-token) 11 | - [Custom lock contexts](#custom-lock-contexts) 12 | 13 | ## Getting started 14 | 15 | If you're using [Clerk](https://clerk.com/) as your identity provider, use the `protectClerkMiddleware` function to automatically set the CTS token for every user session. 16 | 17 | Install the `@cipherstash/nextjs` package: 18 | 19 | ```bash 20 | npm install @cipherstash/nextjs 21 | # or 22 | yarn add @cipherstash/nextjs 23 | # or 24 | pnpm add @cipherstash/nextjs 25 | ``` 26 | 27 | In your `middleware.ts` file, add the following code: 28 | 29 | ```typescript 30 | import { clerkMiddleware } from '@clerk/nextjs/server' 31 | import { protectClerkMiddleware } from '@cipherstash/nextjs/clerk' 32 | 33 | export default clerkMiddleware(async (auth, req: NextRequest) => { 34 | return protectClerkMiddleware(auth, req) 35 | }) 36 | ``` 37 | 38 | ## Retrieving the CTS token in Next.js 39 | 40 | You can then use the `getCtsToken` function to retrieve the CTS token for the current user session. 41 | 42 | ```typescript 43 | import { getCtsToken } from '@cipherstash/nextjs' 44 | 45 | export default async function Page() { 46 | const ctsToken = await getCtsToken() 47 | 48 | // getCtsToken returns either 49 | // --- 50 | // { success: true, ctsToken: CtsToken } 51 | // or 52 | // { success: false, error: string } 53 | 54 | if (!ctsToken.success) { 55 | // handle error 56 | } 57 | 58 | return ( 59 |
60 |

Server side rendered page

61 |
62 | ) 63 | } 64 | ``` 65 | 66 | ## Constructing a LockContext with an existing CTS token 67 | 68 | Since the CTS token is already available, you can construct a `LockContext` object with the existing CTS token. 69 | 70 | ```typescript 71 | import { LockContext } from '@cipherstash/protect/identify' 72 | import { getCtsToken } from '@cipherstash/nextjs' 73 | 74 | export default async function Page() { 75 | const ctsToken = await getCtsToken() 76 | 77 | if (!ctsToken.success) { 78 | // handle error 79 | } 80 | 81 | const lockContext = new LockContext({ 82 | ctsToken 83 | }) 84 | 85 | return ( 86 |
87 |

Server side rendered page

88 |
89 | ) 90 | } 91 | ``` 92 | 93 | ## Custom lock contexts 94 | 95 | If you want to override the default context, you can pass a custom context to the `LockContext` constructor. 96 | 97 | ```typescript 98 | import { LockContext } from '@cipherstash/protect/identify' 99 | 100 | // protectClient from the previous steps 101 | const lc = new LockContext({ 102 | context: { 103 | identityClaim: ['sub'], // this is the default context 104 | }, 105 | }) 106 | ``` 107 | 108 | **Context and identity claim options** 109 | 110 | The context object contains an `identityClaim` property. 111 | The `identityClaim` property must be an array of strings that correspond to the Identity Claim(s) you want to lock the encryption operation to. 112 | 113 | Currently supported Identity Claims are: 114 | 115 | | Identity Claim | Description | 116 | | -------------- | ----------- | 117 | | `sub` | The user's subject identifier. | 118 | | `scopes` | The user's scopes set by your IDP policy. | 119 | 120 | --- 121 | 122 | ### Didn't find what you wanted? 123 | 124 | [Click here to let us know what was missing from our docs.](https://github.com/cipherstash/protectjs/issues/new?template=docs-feedback.yml&title=[Docs:]%20Feedback%20on%lock-contexts-with-clerk.md) 125 | -------------------------------------------------------------------------------- /docs/how-to/nextjs-external-packages.md: -------------------------------------------------------------------------------- 1 | # Next.js 2 | 3 | Using `@cipherstash/protect` with Next.js? You need to opt-out from the Server Components bundling and use native Node.js `require` instead. 4 | 5 | ## Using version 15 or later 6 | 7 | `next.config.ts` [configuration](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages): 8 | 9 | ```js 10 | const nextConfig = { 11 | ... 12 | serverExternalPackages: ['@cipherstash/protect'], 13 | } 14 | ``` 15 | 16 | ## Using version 14 17 | 18 | `next.config.mjs` [configuration](https://nextjs.org/docs/14/app/api-reference/next-config-js/serverComponentsExternalPackages): 19 | 20 | ```js 21 | const nextConfig = { 22 | ... 23 | experimental: { 24 | serverComponentsExternalPackages: ['@cipherstash/protect'], 25 | }, 26 | } 27 | ``` 28 | 29 | --- 30 | 31 | ### Didn't find what you wanted? 32 | 33 | [Click here to let us know what was missing from our docs.](https://github.com/cipherstash/protectjs/issues/new?template=docs-feedback.yml&title=[Docs:]%20Feedback%20on%nextjs-external-packages.md) 34 | -------------------------------------------------------------------------------- /docs/how-to/searchable-encryption.md: -------------------------------------------------------------------------------- 1 | # How to search encrypted data 2 | 3 | TODO: flesh this out (sorry it's not done yet!) 4 | 5 | In the meantime checkout the [EQL repo](https://github.com/cipherstash/encrypt-query-language) which is where these docs will get their inspiration from specifically for JavaScript/TypeScript implementations. 6 | -------------------------------------------------------------------------------- /docs/how-to/sst-external-packages.md: -------------------------------------------------------------------------------- 1 | # SST and esbuild 2 | 3 | Using `@cipherstash/protect` in a serverless function deployed with [SST](https://sst.dev/)? 4 | 5 | You need to configure the `nodejs.esbuild.external` and `nodejs.install` options in your `sst.config.ts` file as documented [here](https://sst.dev/docs/component/aws/function/#nodejs): 6 | 7 | ```ts 8 | ... 9 | nodejs: { 10 | esbuild: { 11 | external: ['@cipherstash/protect'], 12 | }, 13 | install: ['@cipherstash/protect'], 14 | }, 15 | ... 16 | ``` 17 | 18 | --- 19 | 20 | ### Didn't find what you wanted? 21 | 22 | [Click here to let us know what was missing from our docs.](https://github.com/cipherstash/protectjs/issues/new?template=docs-feedback.yml&title=[Docs:]%20Feedback%20on%sst-external-packages.md) 23 | -------------------------------------------------------------------------------- /docs/reference/schema.md: -------------------------------------------------------------------------------- 1 | # Protect.js schema 2 | 3 | Protect.js lets you define a schema in TypeScript with properties that map to your database columns, and define indexes and casting for each column which are used when searching on encrypted data. 4 | 5 | ## Table of contents 6 | 7 | - [Creating schema files](#creating-schema-files) 8 | - [Understanding schema files](#understanding-schema-files) 9 | - [Defining your schema](#defining-your-schema) 10 | - [Searchable encryption](#searchable-encryption) 11 | - [Available index options](#available-index-options) 12 | - [Initializing the Protect client](#initializing-the-protect-client) 13 | 14 | ## Creating schema files 15 | 16 | You can declare your Protect.js schema directly in TypeScript either in a single `schema.ts` file, or you can split your schema into multiple files. It's up to you. 17 | 18 | Example in a single file: 19 | 20 | ``` 21 | 📦 22 | ├ 📂 src 23 | │ ├ 📂 protect 24 | │ │ └ 📜 schema.ts 25 | ``` 26 | 27 | or in multiple files: 28 | 29 | ``` 30 | 📦 31 | ├ 📂 src 32 | │ ├ 📂 protect 33 | │ | └ 📂 schemas 34 | │ │ └ 📜 users.ts 35 | │ │ └ 📜 posts.ts 36 | ``` 37 | 38 | ## Understanding schema files 39 | 40 | A schema represents a mapping of your database, and which columns you want to encrypt and index. Thus, it's a collection of tables and columns represented with `csTable` and `csColumn`. 41 | 42 | The below is pseudo-code for how these mappings are defined: 43 | 44 | ```ts 45 | import { csTable, csColumn } from "@cipherstash/protect"; 46 | 47 | export const tableNameInTypeScript = csTable("tableNameInDatabase", { 48 | columnNameInTypeScript: csColumn("columnNameInDatabase"), 49 | }); 50 | ``` 51 | 52 | ## Defining your schema 53 | 54 | Now that you understand how your schema is defined, let's dive into how you can configure your schema. 55 | 56 | Start by importing the `csTable` and `csColumn` functions from `@cipherstash/protect` and create a new table with a column. 57 | 58 | ```ts 59 | import { csTable, csColumn } from "@cipherstash/protect"; 60 | 61 | export const protectedUsers = csTable("users", { 62 | email: csColumn("email"), 63 | }); 64 | ``` 65 | 66 | ### Searchable encryption 67 | 68 | If you are looking to enable searchable encryption in a PostgreSQL database, you must declaratively enable the indexes in your schema by chaining the index options to the column. 69 | 70 | ```ts 71 | import { csTable, csColumn } from "@cipherstash/protect"; 72 | 73 | export const protectedUsers = csTable("users", { 74 | email: csColumn("email").freeTextSearch().equality().orderAndRange(), 75 | }); 76 | ``` 77 | 78 | ## Available index options 79 | 80 | The following index options are available for your schema: 81 | 82 | | **Method** | **Description** | **SQL equivalent** | 83 | | ----------- | --------------- | ------------------ | 84 | | equality | Enables a exact index for equality queries. | `WHERE email = 'example@example.com'` | 85 | | freeTextSearch | Enables a match index for free text queries. | `WHERE description LIKE '%example%'` | 86 | | orderAndRange | Enables an sorting and range queries index. | `ORDER BY price ASC` | 87 | 88 | You can chain these methods to your column to configure them in any combination. 89 | 90 | ## Initializing the Protect client 91 | 92 | You will use your defined schemas to initialize the EQL client. 93 | Simply import your schemas and pass them to the `protect` function. 94 | 95 | ```ts 96 | import { protect, type ProtectClientConfig } from "@cipherstash/protect"; 97 | import { protectedUsers } from "./schemas/users"; 98 | 99 | const config: ProtectClientConfig = { 100 | schemas: [protectedUsers], // At least one csTable is required 101 | } 102 | 103 | const protectClient = await protect(config); 104 | ``` 105 | --- 106 | 107 | ### Didn't find what you wanted? 108 | 109 | [Click here to let us know what was missing from our docs.](https://github.com/cipherstash/protectjs/issues/new?template=docs-feedback.yml&title=[Docs:]%20Feedback%20on%schema.md) 110 | -------------------------------------------------------------------------------- /docs/reference/searchable-encryption-postgres.md: -------------------------------------------------------------------------------- 1 | # Searchable encryption with Protect.js and PostgreSQL 2 | 3 | This reference guide outlines the different query patterns you can use to search encrypted data with Protect.js. 4 | 5 | ## Table of contents 6 | 7 | - [Before you start](#before-you-start) 8 | - [Query examples](#query-examples) 9 | 10 | ## Before you start 11 | 12 | You will have needed to [define your schema and initialized the protect client](../../README.md#defining-your-schema), and have [installed the EQL custom types and functions](../../README.md#searchable-encryption-in-postgresql). 13 | 14 | The below examples assume you have a schema defined: 15 | 16 | ```ts 17 | import { csTable, csColumn } from '@cipherstash/protect' 18 | 19 | export const protectedUsers = csTable('users', { 20 | email: csColumn('email').equality().freeTextSearch().orderAndRange(), 21 | }) 22 | ``` 23 | 24 | > [!TIP] 25 | > To see an example using the [Drizzle ORM](https://github.com/drizzle-team/drizzle-orm) see the example [here](../../examples/drizzle/src/select.ts). 26 | 27 | ## Query examples 28 | 29 | TODO: flesh this out (sorry it's not done yet!) 30 | 31 | --- 32 | 33 | ### Didn't find what you wanted? 34 | 35 | [Click here to let us know what was missing from our docs.](https://github.com/cipherstash/protectjs/issues/new?template=docs-feedback.yml&title=[Docs:]%20Feedback%20on%searchable-encryption-postgres.md) 36 | -------------------------------------------------------------------------------- /examples/basic/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cipherstash/basic-example 2 | 3 | ## 1.1.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [c8468ee] 8 | - @cipherstash/protect@9.1.0 9 | 10 | ## 1.1.0 11 | 12 | ### Minor Changes 13 | 14 | - 1bc55a0: Implemented a more configurable pattern for the Protect client. 15 | 16 | This release introduces a new `ProtectClientConfig` type that can be used to configure the Protect client. 17 | 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. 18 | 19 | ```ts 20 | import { protect, type ProtectClientConfig } from "@cipherstash/protect"; 21 | 22 | const config: ProtectClientConfig = { 23 | schemas: [users, orders], 24 | workspaceCrn: "your-workspace-crn", 25 | accessKey: "your-access-key", 26 | clientId: "your-client-id", 27 | clientKey: "your-client-key", 28 | }; 29 | 30 | const protectClient = await protect(config); 31 | ``` 32 | 33 | The now deprecated method of passing your tables to the `protect` client is no longer supported. 34 | 35 | ```ts 36 | import { protect, type ProtectClientConfig } from "@cipherstash/protect"; 37 | 38 | // old method (no longer supported) 39 | const protectClient = await protect(users, orders); 40 | 41 | // required method 42 | const config: ProtectClientConfig = { 43 | schemas: [users, orders], 44 | }; 45 | 46 | const protectClient = await protect(config); 47 | ``` 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [1bc55a0] 52 | - @cipherstash/protect@9.0.0 53 | 54 | ## 1.0.12 55 | 56 | ### Patch Changes 57 | 58 | - Updated dependencies [a471821] 59 | - @cipherstash/protect@8.4.0 60 | 61 | ## 1.0.11 62 | 63 | ### Patch Changes 64 | 65 | - Updated dependencies [628acdc] 66 | - @cipherstash/protect@8.3.0 67 | 68 | ## 1.0.10 69 | 70 | ### Patch Changes 71 | 72 | - Updated dependencies [0883e16] 73 | - @cipherstash/protect@8.2.0 74 | 75 | ## 1.0.9 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies [95c891d] 80 | - Updated dependencies [18d3653] 81 | - @cipherstash/protect@8.1.0 82 | 83 | ## 1.0.8 84 | 85 | ### Patch Changes 86 | 87 | - Updated dependencies [8a4ea80] 88 | - @cipherstash/protect@8.0.0 89 | 90 | ## 1.0.7 91 | 92 | ### Patch Changes 93 | 94 | - Updated dependencies [2cb2d84] 95 | - @cipherstash/protect@7.0.0 96 | 97 | ## 1.0.6 98 | 99 | ### Patch Changes 100 | 101 | - Updated dependencies [a564f21] 102 | - @cipherstash/protect@6.3.0 103 | 104 | ## 1.0.5 105 | 106 | ### Patch Changes 107 | 108 | - Updated dependencies [fe4b443] 109 | - @cipherstash/protect@6.2.0 110 | 111 | ## 1.0.4 112 | 113 | ### Patch Changes 114 | 115 | - Updated dependencies [43e1acb] 116 | - @cipherstash/protect@6.1.0 117 | 118 | ## 1.0.3 119 | 120 | ### Patch Changes 121 | 122 | - Updated dependencies [f4d8334] 123 | - @cipherstash/protect@6.0.0 124 | 125 | ## 1.0.2 126 | 127 | ### Patch Changes 128 | 129 | - Updated dependencies [499c246] 130 | - @cipherstash/protect@5.2.0 131 | 132 | ## 1.0.1 133 | 134 | ### Patch Changes 135 | 136 | - Updated dependencies [5a34e76] 137 | - @cipherstash/protect@5.1.0 138 | 139 | ## 1.0.0 140 | 141 | ### Major Changes 142 | 143 | - 76599e5: Rebrand jseql to protect. 144 | 145 | ### Patch Changes 146 | 147 | - Updated dependencies [76599e5] 148 | - @cipherstash/protect@5.0.0 149 | 150 | ## 0.1.2 151 | 152 | ### Patch Changes 153 | 154 | - Updated dependencies [5c08fe5] 155 | - @cipherstash/jseql@4.0.0 156 | 157 | ## 0.1.1 158 | 159 | ### Patch Changes 160 | 161 | - Updated dependencies [e885975] 162 | - @cipherstash/jseql@3.9.0 163 | 164 | ## 0.1.0 165 | 166 | ### Minor Changes 167 | 168 | - eeaec18: Implemented typing and import synatx for es6. 169 | 170 | ### Patch Changes 171 | 172 | - Updated dependencies [eeaec18] 173 | - @cipherstash/jseql@3.8.0 174 | 175 | ## 0.0.6 176 | 177 | ### Patch Changes 178 | 179 | - Updated dependencies [7b8ec52] 180 | - @cipherstash/jseql@3.7.0 181 | 182 | ## 0.0.5 183 | 184 | ### Patch Changes 185 | 186 | - Updated dependencies [7480cfd] 187 | - @cipherstash/jseql@3.6.0 188 | 189 | ## 0.0.4 190 | 191 | ### Patch Changes 192 | 193 | - Updated dependencies [c0123be] 194 | - @cipherstash/jseql@3.5.0 195 | 196 | ## 0.0.3 197 | 198 | ### Patch Changes 199 | 200 | - Updated dependencies [9a3132c] 201 | - Updated dependencies [9a3132c] 202 | - @cipherstash/jseql@3.4.0 203 | 204 | ## 0.0.2 205 | 206 | ### Patch Changes 207 | 208 | - Updated dependencies [80ee5af] 209 | - @cipherstash/jseql@3.3.0 210 | 211 | ## 0.0.1 212 | 213 | ### Patch Changes 214 | 215 | - Updated dependencies [0526f60] 216 | - Updated dependencies [fbb2bcb] 217 | - @cipherstash/jseql@3.2.0 218 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | # Basic example of using @cipherstash/protect 2 | 3 | This basic example demonstrates how to use the `@cipherstash/protect` package to encrypt arbitrary input. 4 | 5 | ## Installing the basic example 6 | 7 | > [!IMPORTANT] 8 | > Make sure you have installed Node.js and [pnpm](https://pnpm.io/installation) before following these steps. 9 | 10 | Clone this repo: 11 | 12 | ```bash 13 | git clone https://github.com/cipherstash/protectjs 14 | ``` 15 | 16 | Install dependencies: 17 | 18 | ```bash 19 | # Build Project.js 20 | cd protectjs 21 | pnpm build 22 | 23 | # Install deps for basic example 24 | cd examples/basic 25 | pnpm install 26 | ``` 27 | 28 | Lastly, install the CipherStash CLI: 29 | 30 | - On macOS: 31 | 32 | ```bash 33 | brew install cipherstash/tap/stash 34 | ``` 35 | 36 | - On Linux, download the binary for your platform, and put it on your `PATH`: 37 | - [Linux ARM64](https://github.com/cipherstash/cli-releases/releases/latest/download/stash-aarch64-unknown-linux-gnu) 38 | - [Linux x86_64](https://github.com/cipherstash/cli-releases/releases/latest/download/stash-x86_64-unknown-linux-gnu) 39 | 40 | 41 | ## Configuring the basic example 42 | 43 | > [!IMPORTANT] 44 | > Make sure you have [installed the CipherStash CLI](#installation) before following these steps. 45 | 46 | Set up all the configuration and credentials required for Protect.js: 47 | 48 | ```bash 49 | stash setup 50 | ``` 51 | 52 | If you have not already signed up for a CipherStash account, this will prompt you to do so along the way. 53 | 54 | At the end of `stash setup`, you will have two files in your project: 55 | 56 | - `cipherstash.toml` which contains the configuration for Protect.js 57 | - `cipherstash.secret.toml` which contains the credentials for Protect.js 58 | 59 | > [!WARNING] 60 | > Do not commit `cipherstash.secret.toml` to git, because it contains sensitive credentials. 61 | 62 | 63 | ## Using the basic example 64 | 65 | Run the example: 66 | 67 | ``` 68 | pnpm start 69 | ``` 70 | 71 | The application will log the plaintext to the console that has been encrypted using the CipherStash, decrypted, and logged the original plaintext. 72 | 73 | ## Next steps 74 | 75 | Check out the [Protect.js + Next.js + Clerk example app](../nextjs-clerk) to see how to add end-user identity as an extra control when encrypting data. 76 | -------------------------------------------------------------------------------- /examples/basic/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { protectClient, users } from './protect' 3 | import readline from 'node:readline' 4 | 5 | const rl = readline.createInterface({ 6 | input: process.stdin, 7 | output: process.stdout, 8 | }) 9 | 10 | const askQuestion = (): Promise => { 11 | return new Promise((resolve) => { 12 | rl.question('\n👋Hello\n\nWhat is your name? ', (answer) => { 13 | resolve(answer) 14 | }) 15 | }) 16 | } 17 | 18 | async function main() { 19 | const input = await askQuestion() 20 | 21 | const encryptResult = await protectClient.encrypt(input, { 22 | column: users.name, 23 | table: users, 24 | }) 25 | 26 | if (encryptResult.failure) { 27 | throw new Error(`[protect]: ${encryptResult.failure.message}`) 28 | } 29 | 30 | const ciphertext = encryptResult.data 31 | 32 | console.log('Encrypting your name...') 33 | console.log('The ciphertext is:', ciphertext) 34 | 35 | const decryptResult = await protectClient.decrypt(ciphertext) 36 | 37 | if (decryptResult.failure) { 38 | throw new Error(`[protect]: ${decryptResult.failure.message}`) 39 | } 40 | 41 | const plaintext = decryptResult.data 42 | 43 | console.log('Decrypting the ciphertext...') 44 | console.log('The plaintext is:', plaintext) 45 | 46 | rl.close() 47 | } 48 | 49 | main() 50 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/basic-example", 3 | "private": true, 4 | "version": "1.1.1", 5 | "type": "module", 6 | "scripts": { 7 | "start": "tsx index.ts" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "description": "", 13 | "dependencies": { 14 | "@cipherstash/protect": "workspace:*", 15 | "dotenv": "^16.4.7" 16 | }, 17 | "devDependencies": { 18 | "tsx": "catalog:repo", 19 | "typescript": "catalog:repo" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/basic/protect.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { 3 | protect, 4 | csColumn, 5 | csTable, 6 | type ProtectClientConfig, 7 | } from '@cipherstash/protect' 8 | 9 | export const users = csTable('users', { 10 | name: csColumn('name'), 11 | }) 12 | 13 | const config: ProtectClientConfig = { 14 | schemas: [users], 15 | } 16 | 17 | export const protectClient = await protect(config) 18 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/drizzle/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://[username]:[password]@localhost:6432/[database]" 2 | CS_CLIENT_ID= 3 | CS_CLIENT_KEY= 4 | CS_WORKSPACE_ID= 5 | CS_CLIENT_ACCESS_KEY= -------------------------------------------------------------------------------- /examples/drizzle/README.md: -------------------------------------------------------------------------------- 1 | # drizzle-eql 2 | 3 | This is a example using the [drizzle-orm](https://drizzle-orm.com/). 4 | 5 | ## Prerequisites 6 | 7 | - PostgreSQL database 8 | - CipherStash credentials and account 9 | 10 | ## Setup 11 | 12 | 1. Create a PostgreSQL database and a user with read and write permissions. 13 | 2. Create a `.env` file in the root directory of the project with the following content: 14 | 15 | ```bash 16 | DATABASE_URL="postgresql://[username]:[password]@[host]:5432/[database]" 17 | CS_CLIENT_ID=[client-id] 18 | CS_CLIENT_KEY=[client-key] 19 | CS_WORKSPACE_ID=[workspace-id] 20 | CS_CLIENT_ACCESS_KEY=[access-key] 21 | ``` 22 | 23 | 3. Run the following command to install the dependencies: 24 | 25 | ```bash 26 | npm install 27 | ``` 28 | 29 | 4. Run the following command to insert a new user with an encrypted email: 30 | 31 | ```bash 32 | npx tsx src/insert.ts --email your-email@example.com 33 | ``` 34 | 35 | 5. Run the following command to select all the encrypted emails from the database: 36 | 37 | ```bash 38 | npx tsx src/select.ts 39 | ``` 40 | 41 | ## License 42 | 43 | This project is licensed under the MIT License. -------------------------------------------------------------------------------- /examples/drizzle/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { defineConfig } from 'drizzle-kit' 3 | 4 | export default defineConfig({ 5 | out: './drizzle', 6 | schema: './src/db/schema.ts', 7 | dialect: 'postgresql', 8 | dbCredentials: { 9 | url: process.env.DATABASE_URL, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/drizzle/drizzle/0000_goofy_cannonball.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "users" ( 2 | "id" serial PRIMARY KEY NOT NULL, 3 | "email" varchar, 4 | "email_encrypted" jsonb NOT NULL, 5 | CONSTRAINT "users_email_unique" UNIQUE("email") 6 | ); 7 | -------------------------------------------------------------------------------- /examples/drizzle/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "a263534d-e155-4647-9ed2-eb113525c55c", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "serial", 14 | "primaryKey": true, 15 | "notNull": true 16 | }, 17 | "email": { 18 | "name": "email", 19 | "type": "varchar", 20 | "primaryKey": false, 21 | "notNull": false 22 | }, 23 | "email_encrypted": { 24 | "name": "email_encrypted", 25 | "type": "jsonb", 26 | "primaryKey": false, 27 | "notNull": true 28 | } 29 | }, 30 | "indexes": {}, 31 | "foreignKeys": {}, 32 | "compositePrimaryKeys": {}, 33 | "uniqueConstraints": { 34 | "users_email_unique": { 35 | "name": "users_email_unique", 36 | "nullsNotDistinct": false, 37 | "columns": ["email"] 38 | } 39 | } 40 | } 41 | }, 42 | "enums": {}, 43 | "schemas": {}, 44 | "sequences": {}, 45 | "_meta": { 46 | "columns": {}, 47 | "schemas": {}, 48 | "tables": {} 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/drizzle/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1734712905691, 9 | "tag": "0000_goofy_cannonball", 10 | "breakpoints": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/drizzle/environment.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | DATABASE_URL: string 4 | CS_CLIENT_ID: string 5 | CS_CLIENT_KEY: string 6 | CS_WORKSPACE_ID: string 7 | CS_CLIENT_ACCESS_KEY: string 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/drizzle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drizzle-eql", 3 | "module": "index.ts", 4 | "private": true, 5 | "type": "module", 6 | "devDependencies": { 7 | "@types/node": "^22.10.2", 8 | "@types/pg": "^8.11.10", 9 | "dotenv": "^16.4.7", 10 | "drizzle-kit": "^0.30.5", 11 | "typescript": "catalog:repo", 12 | "tsx": "catalog:repo" 13 | }, 14 | "scripts": { 15 | "insert": "tsx src/insert.ts", 16 | "select": "tsx src/select.ts", 17 | "db:generate": "drizzle-kit generate", 18 | "db:migrate": "drizzle-kit migrate" 19 | }, 20 | "dependencies": { 21 | "@cipherstash/protect": "workspace:*", 22 | "drizzle-orm": "^0.33.0", 23 | "pg": "^8.13.1", 24 | "postgres": "^3.4.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/drizzle/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { drizzle } from 'drizzle-orm/postgres-js' 3 | import postgres from 'postgres' 4 | 5 | const connectionString = process.env.DATABASE_URL 6 | const client = postgres(connectionString) 7 | export const db = drizzle(client) 8 | -------------------------------------------------------------------------------- /examples/drizzle/src/db/schema.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { 3 | customType, 4 | jsonb, 5 | pgTable, 6 | serial, 7 | varchar, 8 | } from 'drizzle-orm/pg-core' 9 | 10 | // Custom types will be implemented in the future - this is an example for now 11 | // --- 12 | // const cs_encrypted_v2 = (name: string) => 13 | // customType<{ data: TData; driverData: string }>({ 14 | // dataType() { 15 | // return 'cs_encrypted_v2' 16 | // }, 17 | // toDriver(value: TData): string { 18 | // return JSON.stringify(value) 19 | // }, 20 | // })(name) 21 | 22 | export const users = pgTable('users', { 23 | id: serial('id').primaryKey(), 24 | email: varchar('email').unique(), 25 | email_encrypted: jsonb('email_encrypted').notNull(), 26 | }) 27 | -------------------------------------------------------------------------------- /examples/drizzle/src/insert.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { parseArgs } from 'node:util' 3 | import { db } from './db' 4 | import { users } from './db/schema' 5 | import { protectClient, users as protectUsers } from './protect' 6 | 7 | const getEmail = () => { 8 | const { values, positionals } = parseArgs({ 9 | args: process.argv, 10 | options: { 11 | email: { 12 | type: 'string', 13 | }, 14 | }, 15 | strict: true, 16 | allowPositionals: true, 17 | }) 18 | 19 | return values.email 20 | } 21 | 22 | const email = getEmail() 23 | 24 | if (!email) { 25 | throw new Error('Email is required') 26 | } 27 | 28 | const encryptedResult = await protectClient.encrypt(email, { 29 | column: protectUsers.email_encrypted, 30 | table: protectUsers, 31 | }) 32 | 33 | if (encryptedResult.failure) { 34 | throw new Error(`[protect]: ${encryptedResult.failure.message}`) 35 | } 36 | 37 | const encryptedEmail = encryptedResult.data 38 | 39 | const sql = db.insert(users).values({ 40 | email: email, 41 | email_encrypted: encryptedEmail, 42 | }) 43 | 44 | const sqlResult = sql.toSQL() 45 | console.log('[INFO] SQL statement:', sqlResult) 46 | 47 | await sql.execute() 48 | console.log( 49 | "[INFO] You've inserted a new user with an encrypted email from the plaintext", 50 | email, 51 | ) 52 | 53 | process.exit(0) 54 | -------------------------------------------------------------------------------- /examples/drizzle/src/protect.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { 3 | protect, 4 | csColumn, 5 | csTable, 6 | type ProtectClientConfig, 7 | } from '@cipherstash/protect' 8 | 9 | export const users = csTable('users', { 10 | email_encrypted: csColumn('email_encrypted') 11 | .equality() 12 | .orderAndRange() 13 | .freeTextSearch(), 14 | }) 15 | 16 | const config: ProtectClientConfig = { 17 | schemas: [users], 18 | } 19 | 20 | export const protectClient = await protect(config) 21 | -------------------------------------------------------------------------------- /examples/drizzle/src/select.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { db } from './db' 3 | import { users } from './db/schema' 4 | import { protectClient, users as protectUsers } from './protect' 5 | import { bindIfParam, sql } from 'drizzle-orm' 6 | import type { BinaryOperator, SQL, SQLWrapper } from 'drizzle-orm' 7 | import { parseArgs } from 'node:util' 8 | import type { EncryptedData } from '@cipherstash/protect' 9 | 10 | const getArgs = () => { 11 | const { values, positionals } = parseArgs({ 12 | args: process.argv, 13 | options: { 14 | filter: { 15 | type: 'string', 16 | }, 17 | op: { 18 | type: 'string', 19 | default: 'match', 20 | }, 21 | }, 22 | strict: true, 23 | allowPositionals: true, 24 | }) 25 | 26 | return values 27 | } 28 | 29 | const { filter, op } = getArgs() 30 | 31 | if (!filter) { 32 | throw new Error('filter is required') 33 | } 34 | 35 | const fnForOp: (op: string) => BinaryOperator = (op) => { 36 | switch (op) { 37 | case 'match': 38 | return csMatch 39 | case 'eq': 40 | return csEq 41 | default: 42 | throw new Error(`unknown op: ${op}`) 43 | } 44 | } 45 | 46 | const csEq: BinaryOperator = (left: SQLWrapper, right: unknown): SQL => { 47 | return sql`cs_unique_v2(${left}) = cs_unique_v2(${bindIfParam(right, left)})` 48 | } 49 | 50 | const csGt: BinaryOperator = (left: SQLWrapper, right: unknown): SQL => { 51 | return sql`cs_ore_64_8_v2(${left}) > cs_ore_64_8_v2(${bindIfParam(right, left)})` 52 | } 53 | 54 | const csLt: BinaryOperator = (left: SQLWrapper, right: unknown): SQL => { 55 | return sql`cs_ore_64_8_v2(${left}) < cs_ore_64_8_v2(${bindIfParam(right, left)})` 56 | } 57 | 58 | const csMatch: BinaryOperator = (left: SQLWrapper, right: unknown): SQL => { 59 | return sql`cs_match_v2(${left}) @> cs_match_v2(${bindIfParam(right, left)})` 60 | } 61 | 62 | const filterInput = await protectClient.encrypt(filter, { 63 | column: protectUsers.email_encrypted, 64 | table: protectUsers, 65 | }) 66 | 67 | if (filterInput.failure) { 68 | throw new Error(`[protect]: ${filterInput.failure.message}`) 69 | } 70 | 71 | const filterFn = fnForOp(op) 72 | 73 | const query = db 74 | .select({ 75 | email: users.email_encrypted, 76 | }) 77 | .from(users) 78 | .where(filterFn(users.email_encrypted, filterInput.data)) 79 | .orderBy(sql`cs_ore_64_8_v2(users.email_encrypted)`) 80 | 81 | const sqlResult = query.toSQL() 82 | console.log('[INFO] SQL statement:', sqlResult) 83 | 84 | const data = await query.execute() 85 | 86 | const emails = await Promise.all( 87 | data.map( 88 | async (row) => await protectClient.decrypt(row.email as EncryptedData), 89 | ), 90 | ) 91 | 92 | console.log('[INFO] All emails have been decrypted by CipherStash Proxy') 93 | console.log(emails) 94 | 95 | process.exit(0) 96 | -------------------------------------------------------------------------------- /examples/drizzle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/dynamo/.gitignore: -------------------------------------------------------------------------------- 1 | docker 2 | sql/cipherstash-encrypt.sql 3 | -------------------------------------------------------------------------------- /examples/dynamo/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cipherstash/dynamo-example 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [5fc0150] 8 | - @cipherstash/protect-dynamodb@0.2.0 9 | 10 | ## 0.2.0 11 | 12 | ### Minor Changes 13 | 14 | - c8468ee: Released initial version of the DynamoDB helper interface. 15 | 16 | ### Patch Changes 17 | 18 | - Updated dependencies [c8468ee] 19 | - @cipherstash/protect-dynamodb@1.0.0 20 | - @cipherstash/protect@9.1.0 21 | -------------------------------------------------------------------------------- /examples/dynamo/README.md: -------------------------------------------------------------------------------- 1 | # DynamoDB Examples 2 | 3 | Examples of using Protect.js with DynamoDB. 4 | 5 | ## Prereqs 6 | - [Node.js](https://nodejs.org/en) (tested with v22.11.0) 7 | - [pnpm](https://pnpm.io/) (tested with v9.15.3) 8 | - [Docker](https://www.docker.com/) 9 | - a CipherStash account and [credentials configured](../../README.md#configuration) 10 | 11 | ## Setup 12 | 13 | Install the workspace dependencies and build Protect.js: 14 | ``` 15 | # change to the workspace root directory 16 | cd ../.. 17 | 18 | pnpm install 19 | pnpm run build 20 | ``` 21 | 22 | Switch back to the DynamoDB examples 23 | ``` 24 | cd examples/dynamo 25 | ``` 26 | 27 | Start Docker services used by the DynamoDB examples: 28 | ``` 29 | docker compose up --detach 30 | ``` 31 | 32 | Download [EQL](https://github.com/cipherstash/encrypt-query-language) and install it into the PG DB (this is optional and only necessary for running the `export-to-pg` example): 33 | ``` 34 | pnpm run eql:download 35 | pnpm run eql:install 36 | ``` 37 | 38 | ## Examples 39 | 40 | All examples run as scripts from [`package.json`](./package.json). 41 | You can run an example with the command `pnpm run [example_name]`. 42 | 43 | Each example runs against local DynamoDB in Docker. 44 | 45 | - `simple` 46 | - `pnpm run simple` 47 | - Round trip encryption/decryption through DynamoDB (no search on encrypted attributes). 48 | - `encrypted-partition-key` 49 | - `pnpm run encrypted-partition-key` 50 | - Uses an encrypted attribute as a partition key. 51 | - `encrypted-sort-key` 52 | - `pnpm run encrypted-sort-key` 53 | - Similar to the `encrypted-partition-key` example, but uses an encrypted attribute as a sort key instead. 54 | - `encrypted-key-in-gsi` 55 | - `pnpm run encrypted-key-in-gsi` 56 | - Uses an encrypted attribute as the partition key in a global secondary index. 57 | The source ciphertext is projected into the index for decryption after querying the index. 58 | - `export-to-pg` 59 | - `pnpm run export-to-pg` 60 | - Encrypts an item, puts it in Dynamo, exports it to Postgres, and decrypts a result from Postgres. 61 | -------------------------------------------------------------------------------- /examples/dynamo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | dynamodb-local: 4 | command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" 5 | image: "amazon/dynamodb-local:latest" 6 | container_name: dynamodb-local 7 | ports: 8 | - "8000:8000" 9 | volumes: 10 | - "./docker/dynamodb:/home/dynamodblocal/data" 11 | working_dir: /home/dynamodblocal 12 | 13 | dynamodb-admin: 14 | image: aaronshaf/dynamodb-admin 15 | ports: 16 | - 8001:8001 17 | environment: 18 | DYNAMO_ENDPOINT: http://dynamodb-local:8000 19 | 20 | # used by export-to-pg example 21 | postgres: 22 | image: postgres:latest 23 | environment: 24 | PGPORT: 5432 25 | POSTGRES_DB: "cipherstash" 26 | POSTGRES_USER: "cipherstash" 27 | PGUSER: "cipherstash" 28 | POSTGRES_PASSWORD: password 29 | ports: 30 | - 5433:5432 31 | deploy: 32 | resources: 33 | limits: 34 | cpus: "${CPU_LIMIT:-2}" 35 | memory: 2048mb 36 | restart: always 37 | healthcheck: 38 | test: [ "CMD-SHELL", "pg_isready" ] 39 | interval: 1s 40 | timeout: 5s 41 | retries: 10 42 | -------------------------------------------------------------------------------- /examples/dynamo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/dynamo-example", 3 | "private": true, 4 | "version": "0.2.1", 5 | "type": "module", 6 | "scripts": { 7 | "simple": "tsx src/simple.ts", 8 | "bulk-operations": "tsx src/bulk-operations.ts", 9 | "encrypted-partition-key": "tsx src/encrypted-partition-key.ts", 10 | "encrypted-sort-key": "tsx src/encrypted-sort-key.ts", 11 | "encrypted-key-in-gsi": "tsx src/encrypted-key-in-gsi.ts", 12 | "export-to-pg": "tsx src/export-to-pg.ts", 13 | "eql:download": "curl -sLo sql/cipherstash-encrypt.sql https://github.com/cipherstash/encrypt-query-language/releases/download/eql-2.0.2/cipherstash-encrypt.sql", 14 | "eql:install": "cat sql/cipherstash-encrypt.sql | docker exec -i dynamo-postgres-1 psql postgresql://cipherstash:password@postgres:5432/cipherstash -f-" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "description": "", 20 | "dependencies": { 21 | "@aws-sdk/client-dynamodb": "^3.817.0", 22 | "@aws-sdk/lib-dynamodb": "^3.817.0", 23 | "@aws-sdk/util-dynamodb": "^3.817.0", 24 | "@cipherstash/protect": "workspace:*", 25 | "@cipherstash/protect-dynamodb": "workspace:*", 26 | "pg": "^8.13.1" 27 | }, 28 | "devDependencies": { 29 | "@types/pg": "^8.11.10", 30 | "tsx": "catalog:repo", 31 | "typescript": "catalog:repo" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/dynamo/sql/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cipherstash/protectjs/08bc22ff90e6395a1622742a207bbc3908011810/examples/dynamo/sql/.gitkeep -------------------------------------------------------------------------------- /examples/dynamo/src/bulk-operations.ts: -------------------------------------------------------------------------------- 1 | import { dynamoClient, docClient, createTable } from './common/dynamo' 2 | import { log } from './common/log' 3 | import { users, protectClient } from './common/protect' 4 | import { BatchGetCommand, BatchWriteCommand } from '@aws-sdk/lib-dynamodb' 5 | import { protectDynamoDB } from '@cipherstash/protect-dynamodb' 6 | 7 | const tableName = 'UsersBulkOperations' 8 | 9 | type User = { 10 | pk: string 11 | email: string 12 | } 13 | 14 | const main = async () => { 15 | await createTable({ 16 | TableName: tableName, 17 | AttributeDefinitions: [ 18 | { 19 | AttributeName: 'pk', 20 | AttributeType: 'S', 21 | }, 22 | ], 23 | KeySchema: [ 24 | { 25 | AttributeName: 'pk', 26 | KeyType: 'HASH', 27 | }, 28 | ], 29 | }) 30 | 31 | const protectDynamo = protectDynamoDB({ 32 | protectClient, 33 | }) 34 | 35 | const items = [ 36 | { 37 | // `pk` won't be encrypted because it's not included in the `users` protected table schema. 38 | pk: 'user#1', 39 | // `email` will be encrypted because it's included in the `users` protected table schema. 40 | email: 'abc@example.com', 41 | }, 42 | { 43 | pk: 'user#2', 44 | email: 'def@example.com', 45 | }, 46 | ] 47 | 48 | const encryptResult = await protectDynamo.bulkEncryptModels(items, users) 49 | 50 | if (encryptResult.failure) { 51 | throw new Error(`Failed to encrypt items: ${encryptResult.failure.message}`) 52 | } 53 | 54 | const putRequests = encryptResult.data.map( 55 | (item: Record) => ({ 56 | PutRequest: { 57 | Item: item, 58 | }, 59 | }), 60 | ) 61 | 62 | log('encrypted items', encryptResult) 63 | 64 | const batchWriteCommand = new BatchWriteCommand({ 65 | RequestItems: { 66 | [tableName]: putRequests, 67 | }, 68 | }) 69 | 70 | await dynamoClient.send(batchWriteCommand) 71 | 72 | const batchGetCommand = new BatchGetCommand({ 73 | RequestItems: { 74 | [tableName]: { 75 | Keys: [{ pk: 'user#1' }, { pk: 'user#2' }], 76 | }, 77 | }, 78 | }) 79 | 80 | const getResult = await docClient.send(batchGetCommand) 81 | 82 | const decryptedItems = await protectDynamo.bulkDecryptModels( 83 | getResult.Responses?.[tableName], 84 | users, 85 | ) 86 | 87 | log('decrypted items', decryptedItems) 88 | } 89 | 90 | main() 91 | -------------------------------------------------------------------------------- /examples/dynamo/src/common/dynamo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateTableCommand, 3 | DynamoDBClient, 4 | type CreateTableCommandInput, 5 | } from '@aws-sdk/client-dynamodb' 6 | import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb' 7 | 8 | export const dynamoClient = new DynamoDBClient({ 9 | credentials: { 10 | accessKeyId: 'fakeAccessKeyId', 11 | secretAccessKey: 'fakeSecretAccessKey', 12 | }, 13 | endpoint: 'http://localhost:8000', 14 | }) 15 | 16 | export const docClient = DynamoDBDocumentClient.from(dynamoClient) 17 | 18 | // Creates a table with provisioned throughput set to 5 RCU and 5 WCU. 19 | // Ignores `ResourceInUseException`s if the table already exists. 20 | export async function createTable( 21 | input: Omit, 22 | ) { 23 | const command = new CreateTableCommand({ 24 | ProvisionedThroughput: { 25 | ReadCapacityUnits: 5, 26 | WriteCapacityUnits: 5, 27 | }, 28 | ...input, 29 | }) 30 | 31 | try { 32 | await docClient.send(command) 33 | } catch (err) { 34 | if (err?.name! !== 'ResourceInUseException') { 35 | throw err 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/dynamo/src/common/log.ts: -------------------------------------------------------------------------------- 1 | export function log(description: string, data: unknown) { 2 | console.log(`\n${description}:\n${JSON.stringify(data, null, 2)}`) 3 | } 4 | -------------------------------------------------------------------------------- /examples/dynamo/src/common/protect.ts: -------------------------------------------------------------------------------- 1 | import { protect, csColumn, csTable } from '@cipherstash/protect' 2 | 3 | export const users = csTable('users', { 4 | email: csColumn('email').equality(), 5 | }) 6 | 7 | export const protectClient = await protect({ 8 | schemas: [users], 9 | }) 10 | -------------------------------------------------------------------------------- /examples/dynamo/src/encrypted-key-in-gsi.ts: -------------------------------------------------------------------------------- 1 | import { dynamoClient, docClient, createTable } from './common/dynamo' 2 | import { log } from './common/log' 3 | import { users, protectClient } from './common/protect' 4 | import { PutCommand, QueryCommand } from '@aws-sdk/lib-dynamodb' 5 | import { protectDynamoDB } from '@cipherstash/protect-dynamodb' 6 | 7 | const tableName = 'UsersEncryptedKeyInGSI' 8 | const indexName = 'EmailIndex' 9 | 10 | type User = { 11 | pk: string 12 | email: string 13 | } 14 | 15 | const main = async () => { 16 | await createTable({ 17 | TableName: tableName, 18 | AttributeDefinitions: [ 19 | { 20 | AttributeName: 'pk', 21 | AttributeType: 'S', 22 | }, 23 | { 24 | AttributeName: 'email__hmac', 25 | AttributeType: 'S', 26 | }, 27 | ], 28 | KeySchema: [ 29 | { 30 | AttributeName: 'pk', 31 | KeyType: 'HASH', 32 | }, 33 | ], 34 | GlobalSecondaryIndexes: [ 35 | { 36 | IndexName: indexName, 37 | KeySchema: [{ AttributeName: 'email__hmac', KeyType: 'HASH' }], 38 | Projection: { 39 | ProjectionType: 'INCLUDE', 40 | NonKeyAttributes: ['email__source'], 41 | }, 42 | ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }, 43 | }, 44 | ], 45 | }) 46 | 47 | const protectDynamo = protectDynamoDB({ 48 | protectClient, 49 | }) 50 | 51 | const user = { 52 | // `pk` won't be encrypted because it's not included in the `users` protected table schema. 53 | pk: 'user#1', 54 | // `email` will be encrypted because it's included in the `users` protected table schema. 55 | email: 'abc@example.com', 56 | } 57 | 58 | const encryptResult = await protectDynamo.encryptModel(user, users) 59 | 60 | log('encrypted item', encryptResult) 61 | 62 | const putCommand = new PutCommand({ 63 | TableName: tableName, 64 | Item: encryptResult, 65 | }) 66 | 67 | await dynamoClient.send(putCommand) 68 | 69 | const searchTermsResult = await protectDynamo.createSearchTerms([ 70 | { 71 | value: 'abc@example.com', 72 | column: users.email, 73 | table: users, 74 | }, 75 | ]) 76 | 77 | if (searchTermsResult.failure) { 78 | throw new Error( 79 | `Failed to create search terms: ${searchTermsResult.failure.message}`, 80 | ) 81 | } 82 | 83 | const [emailHmac] = searchTermsResult.data 84 | 85 | const queryCommand = new QueryCommand({ 86 | TableName: tableName, 87 | IndexName: indexName, 88 | KeyConditionExpression: 'email__hmac = :e', 89 | ExpressionAttributeValues: { 90 | ':e': emailHmac, 91 | }, 92 | Limit: 1, 93 | }) 94 | 95 | const queryResult = await docClient.send(queryCommand) 96 | 97 | if (!queryResult.Items?.[0]) { 98 | throw new Error('Item not found') 99 | } 100 | 101 | const decryptedItem = await protectDynamo.decryptModel( 102 | queryResult.Items[0], 103 | users, 104 | ) 105 | 106 | log('decrypted item', decryptedItem) 107 | } 108 | 109 | main() 110 | -------------------------------------------------------------------------------- /examples/dynamo/src/encrypted-partition-key.ts: -------------------------------------------------------------------------------- 1 | import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' 2 | import { createTable, docClient } from './common/dynamo' 3 | import { users, protectClient } from './common/protect' 4 | import { log } from './common/log' 5 | import { protectDynamoDB } from '@cipherstash/protect-dynamodb' 6 | 7 | const tableName = 'UsersEncryptedPartitionKey' 8 | 9 | type User = { 10 | email: string 11 | } 12 | 13 | const main = async () => { 14 | await createTable({ 15 | TableName: tableName, 16 | AttributeDefinitions: [ 17 | { 18 | AttributeName: 'email__hmac', 19 | AttributeType: 'S', 20 | }, 21 | ], 22 | KeySchema: [ 23 | { 24 | AttributeName: 'email__hmac', 25 | KeyType: 'HASH', 26 | }, 27 | ], 28 | }) 29 | 30 | const protectDynamo = protectDynamoDB({ 31 | protectClient, 32 | }) 33 | 34 | const user = { 35 | // `email` will be encrypted because it's included in the `users` protected table schema. 36 | email: 'abc@example.com', 37 | // `somePlaintextAttr` won't be encrypted because it's not in the protected table schema. 38 | somePlaintextAttr: 'abc', 39 | } 40 | 41 | const encryptResult = await protectDynamo.encryptModel(user, users) 42 | 43 | log('encrypted item', encryptResult) 44 | 45 | const putCommand = new PutCommand({ 46 | TableName: tableName, 47 | Item: encryptResult, 48 | }) 49 | 50 | await docClient.send(putCommand) 51 | 52 | const searchTermsResult = await protectDynamo.createSearchTerms([ 53 | { 54 | value: 'abc@example.com', 55 | column: users.email, 56 | table: users, 57 | }, 58 | ]) 59 | 60 | if (searchTermsResult.failure) { 61 | throw new Error( 62 | `Failed to create search terms: ${searchTermsResult.failure.message}`, 63 | ) 64 | } 65 | 66 | const [emailHmac] = searchTermsResult.data 67 | 68 | const getCommand = new GetCommand({ 69 | TableName: tableName, 70 | Key: { email__hmac: emailHmac }, 71 | }) 72 | 73 | const getResult = await docClient.send(getCommand) 74 | 75 | const decryptedItem = await protectDynamo.decryptModel( 76 | getResult.Item, 77 | users, 78 | ) 79 | 80 | log('decrypted item', decryptedItem) 81 | } 82 | 83 | main() 84 | -------------------------------------------------------------------------------- /examples/dynamo/src/encrypted-sort-key.ts: -------------------------------------------------------------------------------- 1 | import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' 2 | import { createTable, docClient, dynamoClient } from './common/dynamo' 3 | import { users, protectClient } from './common/protect' 4 | import { log } from './common/log' 5 | import { protectDynamoDB } from '@cipherstash/protect-dynamodb' 6 | 7 | const tableName = 'UsersEncryptedSortKey' 8 | 9 | type User = { 10 | pk: string 11 | email: string 12 | } 13 | 14 | const main = async () => { 15 | await createTable({ 16 | TableName: tableName, 17 | AttributeDefinitions: [ 18 | { 19 | AttributeName: 'pk', 20 | AttributeType: 'S', 21 | }, 22 | { 23 | AttributeName: 'email__hmac', 24 | AttributeType: 'S', 25 | }, 26 | ], 27 | KeySchema: [ 28 | { 29 | AttributeName: 'pk', 30 | KeyType: 'HASH', 31 | }, 32 | { 33 | AttributeName: 'email__hmac', 34 | KeyType: 'RANGE', 35 | }, 36 | ], 37 | }) 38 | 39 | const protectDynamo = protectDynamoDB({ 40 | protectClient, 41 | }) 42 | 43 | const user = { 44 | // `pk` won't be encrypted because it's not in the protected table schema. 45 | pk: 'user#1', 46 | // `email` will be encrypted because it's included in the `users` protected table schema. 47 | email: 'abc@example.com', 48 | } 49 | 50 | const encryptResult = await protectDynamo.encryptModel(user, users) 51 | 52 | log('encrypted item', encryptResult) 53 | 54 | const putCommand = new PutCommand({ 55 | TableName: tableName, 56 | Item: encryptResult, 57 | }) 58 | 59 | await docClient.send(putCommand) 60 | 61 | const searchTermsResult = await protectDynamo.createSearchTerms([ 62 | { 63 | value: 'abc@example.com', 64 | column: users.email, 65 | table: users, 66 | }, 67 | ]) 68 | 69 | if (searchTermsResult.failure) { 70 | throw new Error( 71 | `Failed to create search terms: ${searchTermsResult.failure.message}`, 72 | ) 73 | } 74 | 75 | const [emailHmac] = searchTermsResult.data 76 | 77 | const getCommand = new GetCommand({ 78 | TableName: tableName, 79 | Key: { pk: 'user#1', email__hmac: emailHmac }, 80 | }) 81 | 82 | const getResult = await docClient.send(getCommand) 83 | 84 | if (!getResult.Item) { 85 | throw new Error('Item not found') 86 | } 87 | 88 | const decryptedItem = await protectDynamo.decryptModel( 89 | getResult.Item, 90 | users, 91 | ) 92 | 93 | log('decrypted item', decryptedItem) 94 | } 95 | 96 | main() 97 | -------------------------------------------------------------------------------- /examples/dynamo/src/export-to-pg.ts: -------------------------------------------------------------------------------- 1 | // Insert data in dynamo, scan it back out, insert/copy into PG, query from PG. 2 | import { dynamoClient, docClient, createTable } from './common/dynamo' 3 | import { log } from './common/log' 4 | import { users, protectClient } from './common/protect' 5 | import { PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb' 6 | import { protectDynamoDB } from '@cipherstash/protect-dynamodb' 7 | import pg from 'pg' 8 | const PgClient = pg.Client 9 | 10 | const tableName = 'UsersExportToPG' 11 | 12 | type User = { 13 | pk: string 14 | email: string 15 | } 16 | 17 | const main = async () => { 18 | await createTable({ 19 | TableName: tableName, 20 | AttributeDefinitions: [ 21 | { 22 | AttributeName: 'pk', 23 | AttributeType: 'S', 24 | }, 25 | ], 26 | KeySchema: [ 27 | { 28 | AttributeName: 'pk', 29 | KeyType: 'HASH', 30 | }, 31 | ], 32 | }) 33 | 34 | const protectDynamo = protectDynamoDB({ 35 | protectClient, 36 | }) 37 | 38 | const user = { 39 | // `pk` won't be encrypted because it's not included in the `users` protected table schema. 40 | pk: 'user#1', 41 | // `email` will be encrypted because it's included in the `users` protected table schema. 42 | email: 'abc@example.com', 43 | } 44 | 45 | const encryptResult = await protectDynamo.encryptModel(user, users) 46 | 47 | const putCommand = new PutCommand({ 48 | TableName: tableName, 49 | Item: encryptResult, 50 | }) 51 | 52 | await dynamoClient.send(putCommand) 53 | 54 | const scanCommand = new ScanCommand({ 55 | TableName: tableName, 56 | }) 57 | 58 | // This example uses a single scan for simplicity, but this could use streams, a paginated scans, etc. 59 | const scanResult = await docClient.send(scanCommand) 60 | 61 | log('scan items (encrypted)', scanResult.Items) 62 | 63 | const pgClient = new PgClient({ 64 | port: 5433, 65 | database: 'cipherstash', 66 | user: 'cipherstash', 67 | password: 'password', 68 | }) 69 | 70 | await pgClient.connect() 71 | 72 | await pgClient.query(` 73 | CREATE TABLE IF NOT EXISTS users ( 74 | id INT PRIMARY KEY, 75 | email eql_v2_encrypted 76 | ) 77 | `) 78 | 79 | try { 80 | await pgClient.query( 81 | "SELECT eql_v2.add_encrypted_constraint('users', 'email')", 82 | ) 83 | } catch (err) { 84 | if ( 85 | (err as Error).message !== 86 | 'constraint "eql_v2_encrypted_check_email" for relation "users" already exists' 87 | ) { 88 | throw err 89 | } 90 | } 91 | 92 | if (!scanResult.Items) { 93 | throw new Error('No items found in scan result') 94 | } 95 | 96 | // TODO: this logic belongs in Protect (or in common/protect.ts for the prototype) 97 | const formattedForPgInsert = scanResult.Items.reduce( 98 | (recordsToInsert, currentItem) => { 99 | const idAsText = currentItem.pk.slice('user#'.length) 100 | 101 | const emailAsText = JSON.stringify({ 102 | c: currentItem.email__source, 103 | bf: null, 104 | hm: currentItem.email__hmac, 105 | i: { c: 'email', t: 'users' }, 106 | k: 'ct', 107 | ob: null, 108 | v: 2, 109 | }) 110 | 111 | recordsToInsert[0].push(idAsText) 112 | recordsToInsert[1].push(emailAsText) 113 | 114 | return recordsToInsert 115 | }, 116 | [[], []] as [string[], string[]], 117 | ) 118 | 119 | const insertResult = await pgClient.query( 120 | ` 121 | INSERT INTO users(id, email) 122 | SELECT * FROM UNNEST($1::int[], $2::jsonb[]) 123 | ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email 124 | RETURNING id, email::jsonb 125 | `, 126 | [formattedForPgInsert[0], formattedForPgInsert[1]], 127 | ) 128 | 129 | log('inserted rows', insertResult.rows) 130 | 131 | const decryptRowsResult = await protectClient.bulkDecryptModels( 132 | insertResult.rows, 133 | ) 134 | 135 | if (decryptRowsResult.failure) { 136 | throw new Error(decryptRowsResult.failure.message) 137 | } 138 | 139 | log('decrypted rows', decryptRowsResult.data) 140 | 141 | pgClient.end() 142 | } 143 | 144 | main() 145 | -------------------------------------------------------------------------------- /examples/dynamo/src/simple.ts: -------------------------------------------------------------------------------- 1 | import { dynamoClient, docClient, createTable } from './common/dynamo' 2 | import { log } from './common/log' 3 | import { users, protectClient } from './common/protect' 4 | import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb' 5 | import { protectDynamoDB } from '@cipherstash/protect-dynamodb' 6 | 7 | const tableName = 'UsersSimple' 8 | 9 | type User = { 10 | pk: string 11 | email: string 12 | } 13 | 14 | const main = async () => { 15 | await createTable({ 16 | TableName: tableName, 17 | AttributeDefinitions: [ 18 | { 19 | AttributeName: 'pk', 20 | AttributeType: 'S', 21 | }, 22 | ], 23 | KeySchema: [ 24 | { 25 | AttributeName: 'pk', 26 | KeyType: 'HASH', 27 | }, 28 | ], 29 | }) 30 | 31 | const protectDynamo = protectDynamoDB({ 32 | protectClient, 33 | }) 34 | 35 | const user = { 36 | // `pk` won't be encrypted because it's not included in the `users` protected table schema. 37 | pk: 'user#1', 38 | // `email` will be encrypted because it's included in the `users` protected table schema. 39 | email: 'abc@example.com', 40 | } 41 | 42 | const encryptResult = await protectDynamo.encryptModel(user, users) 43 | 44 | log('encrypted item', encryptResult) 45 | 46 | const putCommand = new PutCommand({ 47 | TableName: tableName, 48 | Item: encryptResult, 49 | }) 50 | 51 | await dynamoClient.send(putCommand) 52 | 53 | const getCommand = new GetCommand({ 54 | TableName: tableName, 55 | Key: { pk: 'user#1' }, 56 | }) 57 | 58 | const getResult = await docClient.send(getCommand) 59 | 60 | const decryptedItem = await protectDynamo.decryptModel( 61 | getResult.Item, 62 | users, 63 | ) 64 | 65 | log('decrypted item', decryptedItem) 66 | } 67 | 68 | main() 69 | -------------------------------------------------------------------------------- /examples/hono-supabase/.env.example: -------------------------------------------------------------------------------- 1 | # Credentials for your CipherStash project 2 | CS_CLIENT_ID= 3 | CS_CLIENT_KEY= 4 | CS_CLIENT_ACCESS_KEY= 5 | CS_WORKSPACE_ID= 6 | 7 | # Connection details for your Supabase project 8 | SUPABASE_URL= 9 | SUPABASE_ANON_KEY= 10 | -------------------------------------------------------------------------------- /examples/hono-supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # dev 2 | .yarn/ 3 | !.yarn/releases 4 | .vscode/* 5 | !.vscode/launch.json 6 | !.vscode/*.code-snippets 7 | .idea/workspace.xml 8 | .idea/usage.statistics.xml 9 | .idea/shelf 10 | 11 | # deps 12 | node_modules/ 13 | 14 | # env 15 | .env 16 | .env.production 17 | 18 | # logs 19 | logs/ 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | pnpm-debug.log* 25 | lerna-debug.log* 26 | 27 | # misc 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /examples/hono-supabase/environment.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | SUPABASE_URL: string 4 | SUPABASE_ANON_KEY: string 5 | CS_CLIENT_ID: string 6 | CS_CLIENT_KEY: string 7 | CS_WORKSPACE_ID: string 8 | CS_CLIENT_ACCESS_KEY: string 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/hono-supabase/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hono-supabase", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "tsx watch src/index.ts" 7 | }, 8 | "dependencies": { 9 | "@cipherstash/protect": "workspace:*", 10 | "@hono/node-server": "^1.13.7", 11 | "@supabase/supabase-js": "^2.47.10", 12 | "dotenv": "^16.4.7", 13 | "hono": "^4.6.15" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^20.11.17", 17 | "tsx": "catalog:repo", 18 | "typescript": "catalog:repo" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/hono-supabase/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { serve } from '@hono/node-server' 3 | import { createClient } from '@supabase/supabase-js' 4 | import { Hono } from 'hono' 5 | 6 | // Consolidated protect and it's schemas into a single file 7 | import { 8 | protect, 9 | csColumn, 10 | csTable, 11 | type ProtectClientConfig, 12 | } from '@cipherstash/protect' 13 | 14 | export const users = csTable('users', { 15 | email: csColumn('email'), 16 | }) 17 | 18 | const config: ProtectClientConfig = { 19 | schemas: [users], 20 | } 21 | 22 | export const protectClient = await protect(config) 23 | 24 | // Create a single supabase client for interacting with the database 25 | const supabaseUrl = process.env.SUPABASE_URL 26 | const supabaseKey = process.env.SUPABASE_ANON_KEY 27 | 28 | // This example expects the following table in your Supabase database. 29 | // The email field is the only field that will be encrypted and the required column type is jsonb. 30 | // --- 31 | // CREATE TABLE users ( 32 | // id SERIAL PRIMARY KEY, 33 | // email jsonb NOT NULL, 34 | // name VARCHAR(255) NOT NULL, 35 | // role VARCHAR(255) NOT NULL 36 | // ); 37 | export const supabase = createClient(supabaseUrl, supabaseKey) 38 | 39 | const app = new Hono() 40 | 41 | app.get('/users', async (c) => { 42 | const { data: users } = await supabase.from('users').select() 43 | 44 | if (users && users.length > 1) { 45 | const decryptedusers = await Promise.all( 46 | users.map(async (user) => { 47 | // The encrypted data is stored in the EQL format: { c: 'ciphertext' } 48 | // and the decrypt function expects the data to be in this format. 49 | const decryptResult = await protectClient.decrypt(user.email) 50 | 51 | if (decryptResult.failure) { 52 | console.error( 53 | 'Failed to decrypt the email for user', 54 | user.id, 55 | decryptResult.failure.message, 56 | ) 57 | 58 | return user 59 | } 60 | 61 | const plaintext = decryptResult.data 62 | return { ...user, email: plaintext } 63 | }), 64 | ) 65 | 66 | return c.json({ users: decryptedusers }) 67 | } 68 | 69 | return c.json({ users: [] }) 70 | }) 71 | 72 | app.post('/users', async (c) => { 73 | const { email, name } = await c.req.json() 74 | 75 | if (!email || !name) { 76 | return c.json( 77 | { message: 'Email and name are required to create a users' }, 78 | 400, 79 | ) 80 | } 81 | 82 | // The encrypt function expects the plaintext to be of type string 83 | // and the second argument to be an object with the table and column 84 | // names of the table where you are storing the data. 85 | const encryptedResult = await protectClient.encrypt(email, { 86 | column: users.email, 87 | table: users, 88 | }) 89 | 90 | if (encryptedResult.failure) { 91 | console.error( 92 | 'Failed to encrypt the email', 93 | encryptedResult.failure.message, 94 | ) 95 | return c.json({ message: 'Failed to encrypt the email' }, 500) 96 | } 97 | 98 | const encryptedEmail = encryptedResult.data 99 | 100 | console.log( 101 | 'Encrypted email that will be stored in the database:', 102 | encryptedEmail, 103 | ) 104 | 105 | const result = await supabase 106 | .from('users') 107 | .insert({ email: encryptedEmail, name, role: 'admin' }) 108 | 109 | if (result.statusText === 'Created') { 110 | return c.json({ message: 'User created successfully' }) 111 | } 112 | 113 | console.error('User creation failed:', result) 114 | return c.json({ message: 'User creation failed. Please check the logs' }, 500) 115 | }) 116 | 117 | const port = 3000 118 | console.log(`Server is running on http://localhost:${port}`) 119 | 120 | serve({ 121 | fetch: app.fetch, 122 | port, 123 | }) 124 | -------------------------------------------------------------------------------- /examples/hono-supabase/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "NodeNext", 5 | "strict": true, 6 | "verbatimModuleSyntax": true, 7 | "skipLibCheck": true, 8 | "types": ["node"], 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "hono/jsx" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=mysql://protect_example:password@127.0.0.1:3306/protect_example 2 | CS_CLIENT_ID= 3 | CS_CLIENT_KEY= 4 | CS_CLIENT_ACCESS_KEY= 5 | CS_WORKSPACE_CRN= -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # next-drizzle-mysql 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [c8468ee] 8 | - @cipherstash/protect@9.1.0 9 | 10 | ## 0.2.0 11 | 12 | ### Minor Changes 13 | 14 | - 1bc55a0: Implemented a more configurable pattern for the Protect client. 15 | 16 | This release introduces a new `ProtectClientConfig` type that can be used to configure the Protect client. 17 | 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. 18 | 19 | ```ts 20 | import { protect, type ProtectClientConfig } from "@cipherstash/protect"; 21 | 22 | const config: ProtectClientConfig = { 23 | schemas: [users, orders], 24 | workspaceCrn: "your-workspace-crn", 25 | accessKey: "your-access-key", 26 | clientId: "your-client-id", 27 | clientKey: "your-client-key", 28 | }; 29 | 30 | const protectClient = await protect(config); 31 | ``` 32 | 33 | The now deprecated method of passing your tables to the `protect` client is no longer supported. 34 | 35 | ```ts 36 | import { protect, type ProtectClientConfig } from "@cipherstash/protect"; 37 | 38 | // old method (no longer supported) 39 | const protectClient = await protect(users, orders); 40 | 41 | // required method 42 | const config: ProtectClientConfig = { 43 | schemas: [users, orders], 44 | }; 45 | 46 | const protectClient = await protect(config); 47 | ``` 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [1bc55a0] 52 | - @cipherstash/protect@9.0.0 53 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/README.md: -------------------------------------------------------------------------------- 1 | # Next.js + Drizzle ORM + MySQL + Protect.js Example 2 | 3 | This example demonstrates how to build a modern web application using: 4 | - [Next.js](https://nextjs.org/) - React framework for production 5 | - [Drizzle ORM](https://orm.drizzle.team/) - TypeScript ORM for SQL databases 6 | - [MySQL](https://www.mysql.com/) - Popular open-source relational database 7 | - [Protect.js](https://cipherstash.com/protect) - Data protection and encryption library 8 | 9 | ## Features 10 | 11 | - Full-stack TypeScript application 12 | - Database migrations and schema management with Drizzle 13 | - Data protection and encryption with Protect.js 14 | - Modern UI with Tailwind CSS 15 | - Form handling with React Hook Form and Zod validation 16 | - Docker-based MySQL database setup 17 | 18 | ## Prerequisites 19 | 20 | - Node.js 18+ 21 | - Docker and Docker Compose 22 | - MySQL (if running locally without Docker) 23 | 24 | ## Getting Started 25 | 26 | 1. Clone the repository and install dependencies: 27 | ```bash 28 | pnpm install 29 | ``` 30 | 31 | 2. Set up your environment variables: 32 | Copy the `.env.example` file to `.env.local`: 33 | ```bash 34 | cp .env.example .env.local 35 | ``` 36 | Then update the environment variables in `.env.local` with your Protect.js configuration values. 37 | 38 | 3. Start the MySQL database using Docker: 39 | ```bash 40 | docker compose up -d 41 | ``` 42 | 43 | 4. Run database migrations: 44 | ```bash 45 | pnpm run db:generate 46 | pnpm run db:migrate 47 | ``` 48 | 49 | 5. Start the development server: 50 | ```bash 51 | pnpm run dev 52 | ``` 53 | 54 | The application will be available at `http://localhost:3000`. 55 | 56 | ## Project Structure 57 | 58 | - `/src` - Application source code 59 | - `/drizzle` - Database migrations and schema 60 | - `/public` - Static assets 61 | - `drizzle.config.ts` - Drizzle ORM configuration 62 | - `docker-compose.yml` - Docker configuration for MySQL 63 | 64 | ## Available Scripts 65 | 66 | - `npm run dev` - Start development server 67 | - `npm run build` - Build for production 68 | - `npm run start` - Start production server 69 | - `npm run db:generate` - Generate database migrations 70 | - `npm run db:migrate` - Run database migrations 71 | 72 | ## Learn More 73 | 74 | - [Next.js Documentation](https://nextjs.org/docs) 75 | - [Drizzle ORM Documentation](https://orm.drizzle.team/docs/overview) 76 | - [Protect.js Documentation](https://cipherstash.com/protect/docs) 77 | - [MySQL Documentation](https://dev.mysql.com/doc/) 78 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | db: 4 | image: mysql:latest 5 | environment: 6 | MYSQL_ROOT_PASSWORD: password 7 | MYSQL_DATABASE: protect_example 8 | MYSQL_USER: protect_example 9 | MYSQL_PASSWORD: password 10 | ports: 11 | - "3306:3306" 12 | volumes: 13 | - mysql_data:/var/lib/mysql 14 | 15 | volumes: 16 | mysql_data: 17 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import { defineConfig } from 'drizzle-kit' 3 | export default defineConfig({ 4 | dialect: 'mysql', 5 | schema: './src/db/schema.ts', 6 | dbCredentials: { 7 | host: '127.0.0.1', 8 | port: 3306, 9 | user: 'protect_example', 10 | password: 'password', 11 | database: 'protect_example', 12 | }, 13 | }) 14 | 15 | // mysql://protect_example:password@127.0.0.1:3306/protect_example 16 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/drizzle/0000_brave_madrox.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `users` ( 2 | `id` int AUTO_INCREMENT NOT NULL, 3 | `name` json, 4 | `email` json, 5 | CONSTRAINT `users_id` PRIMARY KEY(`id`) 6 | ); 7 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "mysql", 4 | "id": "03335511-a5f1-45e4-bcf7-227e326b28a5", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "users": { 8 | "name": "users", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "int", 13 | "primaryKey": false, 14 | "notNull": true, 15 | "autoincrement": true 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "json", 20 | "primaryKey": false, 21 | "notNull": false, 22 | "autoincrement": false 23 | }, 24 | "email": { 25 | "name": "email", 26 | "type": "json", 27 | "primaryKey": false, 28 | "notNull": false, 29 | "autoincrement": false 30 | } 31 | }, 32 | "indexes": {}, 33 | "foreignKeys": {}, 34 | "compositePrimaryKeys": { 35 | "users_id": { 36 | "name": "users_id", 37 | "columns": [ 38 | "id" 39 | ] 40 | } 41 | }, 42 | "uniqueConstraints": {}, 43 | "checkConstraint": {} 44 | } 45 | }, 46 | "views": {}, 47 | "_meta": { 48 | "schemas": {}, 49 | "tables": {}, 50 | "columns": {} 51 | }, 52 | "internal": { 53 | "tables": {}, 54 | "indexes": {} 55 | } 56 | } -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "mysql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1748545269720, 9 | "tag": "0000_brave_madrox", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next' 2 | 3 | const nextConfig: NextConfig = { 4 | serverExternalPackages: ['@cipherstash/protect', 'mysql2'], 5 | } 6 | 7 | export default nextConfig 8 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-drizzle-mysql", 3 | "version": "0.2.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "db:generate": "drizzle-kit generate", 10 | "db:migrate": "drizzle-kit migrate" 11 | }, 12 | "dependencies": { 13 | "@cipherstash/protect": "workspace:*", 14 | "@hookform/resolvers": "^5.0.1", 15 | "drizzle-orm": "^0.44.0", 16 | "mysql2": "^3.14.1", 17 | "next": "15.3.2", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "react-hook-form": "^7.56.4", 21 | "zod": "^3.24.2" 22 | }, 23 | "devDependencies": { 24 | "@tailwindcss/postcss": "^4", 25 | "@types/node": "^20", 26 | "@types/react": "^19", 27 | "@types/react-dom": "^19", 28 | "dotenv": "^16.4.7", 29 | "drizzle-kit": "^0.30.5", 30 | "tailwindcss": "^4", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/app/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { db } from '@/db' 4 | import { users } from '@/db/schema' 5 | import type { FormData } from '@/components/form' 6 | import { protectClient } from '@/protect' 7 | import { users as protectedUsers } from '@/protect/schema' 8 | 9 | export async function createUser(data: FormData) { 10 | console.log(data) 11 | 12 | const result = await protectClient.encryptModel(data, protectedUsers) 13 | 14 | if (result.failure) { 15 | console.error(result.failure.message) 16 | return 17 | } 18 | 19 | console.log(result.data) 20 | 21 | await db.insert(users).values({ 22 | name: result.data.name, 23 | email: result.data.email, 24 | }) 25 | 26 | return { 27 | success: true, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cipherstash/protectjs/08bc22ff90e6395a1622742a207bbc3908011810/examples/next-drizzle-mysql/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { db } from '@/db' 2 | import { users } from '@/db/schema' 3 | import { users as protectedUsers } from '@/protect/schema' 4 | import { ClientForm } from '@/components/form' 5 | import { protectClient } from '@/protect' 6 | 7 | type User = { 8 | id: number 9 | name: string 10 | email: string 11 | } 12 | 13 | export default async function Home() { 14 | const u = await db.select().from(users).limit(10) 15 | 16 | const decryptedUsers = await protectClient.bulkDecryptModels(u) 17 | 18 | if (decryptedUsers.failure) { 19 | throw new Error(decryptedUsers.failure.message) 20 | } 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {decryptedUsers.data.map((user) => ( 35 | 36 | 37 | 38 | 39 | 40 | ))} 41 | 42 |
IDNameEmail
{user.id}{user.name as string}{user.email as string}
43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/components/form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useTransition } from 'react' 4 | import { useForm } from 'react-hook-form' 5 | import { zodResolver } from '@hookform/resolvers/zod' 6 | import * as z from 'zod' 7 | import { createUser } from '@/app/actions' 8 | 9 | const formSchema = z.object({ 10 | name: z.string().min(1, 'Name is required'), 11 | email: z.string().email('Invalid email address'), 12 | }) 13 | 14 | export type FormData = z.infer 15 | 16 | export function ClientForm() { 17 | const [isPending, startTransition] = useTransition() 18 | const { 19 | register, 20 | handleSubmit, 21 | reset, 22 | formState: { errors }, 23 | } = useForm({ 24 | resolver: zodResolver(formSchema), 25 | }) 26 | 27 | const onSubmit = (data: FormData) => { 28 | startTransition(async () => { 29 | await createUser(data) 30 | reset() 31 | }) 32 | } 33 | 34 | return ( 35 |
39 |
40 |
41 | 44 | 50 | {errors.name && ( 51 |

{errors.name.message}

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

{errors.email.message}

66 | )} 67 |
68 | 75 |
76 |
77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/mysql2' 2 | 3 | if (!process.env.DATABASE_URL) { 4 | throw new Error('DATABASE_URL is not set') 5 | } 6 | 7 | export const db = drizzle(process.env.DATABASE_URL) 8 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { mysqlTable, int, json, uniqueIndex } from 'drizzle-orm/mysql-core' 2 | 3 | export const users = mysqlTable('users', { 4 | id: int().primaryKey().autoincrement(), 5 | name: json(), 6 | email: json(), 7 | }) 8 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/protect/index.ts: -------------------------------------------------------------------------------- 1 | import { protect, type ProtectClientConfig } from '@cipherstash/protect' 2 | import { users } from './schema' 3 | 4 | const config: ProtectClientConfig = { 5 | schemas: [users], 6 | } 7 | 8 | export const protectClient = await protect(config) 9 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/src/protect/schema.ts: -------------------------------------------------------------------------------- 1 | import { csColumn, csTable } from '@cipherstash/protect' 2 | 3 | export const users = csTable('users', { 4 | email: csColumn('email'), 5 | name: csColumn('name'), 6 | }) 7 | -------------------------------------------------------------------------------- /examples/next-drizzle-mysql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/.env.example: -------------------------------------------------------------------------------- 1 | # Clerk 2 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key 3 | CLERK_SECRET_KEY=your_clerk_secret_key 4 | 5 | # Postres - Try out Supabase for free https://supabase.com/ 6 | POSTGRES_URL=your_postgres_url 7 | 8 | # CipherStash Protect.js 9 | CS_WORKSPACE_ID=your_workspace_id 10 | CS_CLIENT_ID=your_client_id 11 | CS_CLIENT_KEY=your_client_secret 12 | CS_CLIENT_ACCESS_KEY=your_access_key -------------------------------------------------------------------------------- /examples/nextjs-clerk/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @cipherstash/nextjs-clerk-example 2 | 3 | ## 0.2.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [c8468ee] 8 | - @cipherstash/protect@9.1.0 9 | 10 | ## 0.2.0 11 | 12 | ### Minor Changes 13 | 14 | - 1bc55a0: Implemented a more configurable pattern for the Protect client. 15 | 16 | This release introduces a new `ProtectClientConfig` type that can be used to configure the Protect client. 17 | 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. 18 | 19 | ```ts 20 | import { protect, type ProtectClientConfig } from "@cipherstash/protect"; 21 | 22 | const config: ProtectClientConfig = { 23 | schemas: [users, orders], 24 | workspaceCrn: "your-workspace-crn", 25 | accessKey: "your-access-key", 26 | clientId: "your-client-id", 27 | clientKey: "your-client-key", 28 | }; 29 | 30 | const protectClient = await protect(config); 31 | ``` 32 | 33 | The now deprecated method of passing your tables to the `protect` client is no longer supported. 34 | 35 | ```ts 36 | import { protect, type ProtectClientConfig } from "@cipherstash/protect"; 37 | 38 | // old method (no longer supported) 39 | const protectClient = await protect(users, orders); 40 | 41 | // required method 42 | const config: ProtectClientConfig = { 43 | schemas: [users, orders], 44 | }; 45 | 46 | const protectClient = await protect(config); 47 | ``` 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies [1bc55a0] 52 | - @cipherstash/protect@9.0.0 53 | 54 | ## 0.1.6 55 | 56 | ### Patch Changes 57 | 58 | - Updated dependencies [a471821] 59 | - @cipherstash/protect@8.4.0 60 | 61 | ## 0.1.5 62 | 63 | ### Patch Changes 64 | 65 | - Updated dependencies [628acdc] 66 | - @cipherstash/protect@8.3.0 67 | 68 | ## 0.1.4 69 | 70 | ### Patch Changes 71 | 72 | - Updated dependencies [0883e16] 73 | - @cipherstash/protect@8.2.0 74 | 75 | ## 0.1.3 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies [95c891d] 80 | - Updated dependencies [18d3653] 81 | - @cipherstash/nextjs@4.0.0 82 | - @cipherstash/protect@8.1.0 83 | 84 | ## 0.1.2 85 | 86 | ### Patch Changes 87 | 88 | - Updated dependencies [8a4ea80] 89 | - @cipherstash/protect@8.0.0 90 | 91 | ## 0.1.1 92 | 93 | ### Patch Changes 94 | 95 | - Updated dependencies [2cb2d84] 96 | - @cipherstash/protect@7.0.0 97 | 98 | ## 0.1.0 99 | 100 | ### Minor Changes 101 | 102 | - 9377b47: Updated versions to address Next.js CVE. 103 | 104 | ### Patch Changes 105 | 106 | - Updated dependencies [9377b47] 107 | - @cipherstash/nextjs@3.2.0 108 | 109 | ## 0.0.4 110 | 111 | ### Patch Changes 112 | 113 | - Updated dependencies [a564f21] 114 | - @cipherstash/protect@6.3.0 115 | - @cipherstash/nextjs@3.1.0 116 | 117 | ## 0.0.3 118 | 119 | ### Patch Changes 120 | 121 | - Updated dependencies [fe4b443] 122 | - @cipherstash/protect@6.2.0 123 | 124 | ## 0.0.2 125 | 126 | ### Patch Changes 127 | 128 | - Updated dependencies [43e1acb] 129 | - @cipherstash/protect@6.1.0 130 | 131 | ## 0.0.1 132 | 133 | ### Patch Changes 134 | 135 | - Updated dependencies [02dc980] 136 | - Updated dependencies [f4d8334] 137 | - @cipherstash/nextjs@3.0.0 138 | - @cipherstash/protect@6.0.0 139 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/README.md: -------------------------------------------------------------------------------- 1 | # Protect.js + Next.js + Clerk example 2 | 3 | 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. 4 | 5 | This project uses the following technologies: 6 | 7 | - [pnpm](https://pnpm.io) for package management 8 | - [Next.js](https://nextjs.org) for the application framework 9 | - [Clerk](https://clerk.com) for auth 10 | - [Supabase](https://supabase.com) for database 11 | - [Drizzle ORM](https://drizzle.org) for database access 12 | - [CipherStash](https://cipherstash.com) for data encryption 13 | 14 | ## Getting Started 15 | 16 | First, install dependencies: 17 | 18 | ```bash 19 | pnpm install 20 | ``` 21 | 22 | Second, create a `.env.local` file in the root directory with the following content: 23 | 24 | ```bash 25 | # Clerk auth 26 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 27 | CLERK_SECRET_KEY= 28 | 29 | # Supabase postgres connection string 30 | POSTGRES_URL= 31 | 32 | # CipherStash encryption and access keys 33 | CS_CLIENT_ID= 34 | CS_CLIENT_KEY= 35 | CS_CLIENT_ACCESS_KEY= 36 | CS_WORKSPACE_ID= 37 | ``` 38 | 39 | Finally, run the development server: 40 | 41 | ```bash 42 | pnpm run dev 43 | ``` 44 | 45 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 46 | 47 | ## Database 48 | 49 | The database is hosted on Supabase and has the following schema which is defined using the Drizzle ORM: 50 | 51 | ```ts 52 | // Data that is encrypted using protect.js is stored as jsonb in postgres 53 | 54 | export const users = pgTable("users", { 55 | id: serial("id").primaryKey(), 56 | name: varchar("name").notNull(), 57 | email: jsonb("email").notNull(), 58 | role: varchar("role").notNull(), 59 | }); 60 | ``` 61 | 62 | > [!NOTE] 63 | > This example does not include any searchable encrypted fields. 64 | > If you want to search on encrypted fields, you will need to install EQL. 65 | > The EQL library ships with custom types that are used to define encrypted fields. 66 | > See the [EQL documentation](https://github.com/cipherstash/encrypted-query-language) for more information. 67 | 68 | ## @cipherstash/protect 69 | 70 | All the email data is encrypted using Protect.js. 71 | The cipherstext is stored in the `email` column of the `users` table. 72 | The application is configured to only decrypt the data when the user is signed in, otherwise it will display the encrypted data. 73 | 74 | ### Npm package 75 | 76 | `@cipherstash/protect` uses custom Rust bindings to the CipherStash Client in order to perform encryptions and decryptions. 77 | We leverage the [Neon project](https://neon-rs.dev/) to provide a JavaScript API for these bindings. 78 | 79 | ### Encryption 80 | 81 | When a user is added to the database, the email address is encrypted using Protect.js. 82 | To view the encryption implementation, see the `addUser` function in [src/lib/actions.ts](src/lib/actions.ts). 83 | 84 | ### Decryption 85 | 86 | To view the decrpytion implementation, see the `getUsers` function in [src/app/page.tsx](src/app/page.tsx). 87 | 88 | ### Next.js 89 | 90 | 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. 91 | 92 | #### Using version 15 or later 93 | 94 | `next.config.ts` [configuration](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages): 95 | 96 | ```js 97 | const nextConfig = { 98 | ... 99 | serverExternalPackages: ['@cipherstash/protect'], 100 | } 101 | ``` 102 | 103 | #### Using version 14 104 | 105 | `next.config.mjs` [configuration](https://nextjs.org/docs/14/app/api-reference/next-config-js/serverComponentsExternalPackages): 106 | 107 | ```js 108 | const nextConfig = { 109 | ... 110 | experimental: { 111 | serverComponentsExternalPackages: ['@cipherstash/protect'], 112 | }, 113 | } 114 | ``` 115 | 116 | #### Workspace package issue 117 | 118 | `serverExternalPackages` does not work with workspace packages and the issues is being tracked [here](https://github.com/vercel/next.js/issues/43433). 119 | 120 | Once this is fixed upstream, this application can use the workspace package for development. 121 | For the time being, it used `@cipherstash/protect` from the npm registry. -------------------------------------------------------------------------------- /examples/nextjs-clerk/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | export default defineConfig({ 5 | out: "./drizzle", 6 | schema: "./src/db/schema.ts", 7 | dialect: "postgresql", 8 | dbCredentials: { 9 | // biome-ignore lint/style/noNonNullAssertion: Postgres URL is required 10 | url: process.env.POSTGRES_URL!, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next' 2 | 3 | const nextConfig: NextConfig = { 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: 'https', 8 | hostname: 'cipherstash.com', 9 | }, 10 | ], 11 | }, 12 | // serverExternalPackages does not work with workspace packages 13 | // https://github.com/vercel/next.js/issues/43433 14 | // --- 15 | // TODO: Once this is fixed upstream, we can use the workspace packages 16 | serverExternalPackages: ['@cipherstash/protect'], 17 | } 18 | 19 | export default nextConfig 20 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cipherstash/nextjs-clerk-example", 3 | "version": "0.2.1", 4 | "private": true, 5 | "scripts": { 6 | "check-types": "tsc --noEmit", 7 | "dev": "next dev --turbopack", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@cipherstash/nextjs": "workspace:*", 14 | "@cipherstash/protect": "workspace:*", 15 | "@clerk/nextjs": "6.9.15", 16 | "@radix-ui/react-label": "^2.1.1", 17 | "@radix-ui/react-select": "^2.1.4", 18 | "@radix-ui/react-slot": "^1.1.1", 19 | "@radix-ui/react-toast": "^1.2.5", 20 | "@radix-ui/react-tooltip": "^1.1.7", 21 | "class-variance-authority": "^0.7.1", 22 | "clsx": "^2.1.1", 23 | "dotenv": "^16.4.7", 24 | "drizzle-orm": "^0.38.2", 25 | "jose": "^5.9.6", 26 | "lucide-react": "^0.469.0", 27 | "next": "15.2.4", 28 | "postgres": "^3.4.5", 29 | "react": "^19", 30 | "react-dom": "^19", 31 | "tailwind-merge": "^2.5.5", 32 | "tailwindcss-animate": "^1.0.7" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^20", 36 | "@types/react": "^19", 37 | "@types/react-dom": "^19", 38 | "drizzle-kit": "^0.30.5", 39 | "postcss": "^8", 40 | "tailwindcss": "^3.4.1", 41 | "tsx": "catalog:repo", 42 | "typescript": "catalog:repo" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/app/add-user/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from '@/components/Header' 2 | import AddUserForm from '@/components/AddUserForm' 3 | 4 | export default function AddUser() { 5 | return ( 6 |
7 |
8 |
9 |

Add new user

10 | 11 |
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cipherstash/protectjs/08bc22ff90e6395a1622742a207bbc3908011810/examples/nextjs-clerk/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 0 0% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 0 0% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 0 0% 3.9%; 17 | --primary: 0 0% 9%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | --muted: 0 0% 96.1%; 22 | --muted-foreground: 0 0% 45.1%; 23 | --accent: 0 0% 96.1%; 24 | --accent-foreground: 0 0% 9%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 0 0% 89.8%; 28 | --input: 0 0% 89.8%; 29 | --ring: 0 0% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | } 37 | .dark { 38 | --background: 0 0% 3.9%; 39 | --foreground: 0 0% 98%; 40 | --card: 0 0% 3.9%; 41 | --card-foreground: 0 0% 98%; 42 | --popover: 0 0% 3.9%; 43 | --popover-foreground: 0 0% 98%; 44 | --primary: 0 0% 98%; 45 | --primary-foreground: 0 0% 9%; 46 | --secondary: 0 0% 14.9%; 47 | --secondary-foreground: 0 0% 98%; 48 | --muted: 0 0% 14.9%; 49 | --muted-foreground: 0 0% 63.9%; 50 | --accent: 0 0% 14.9%; 51 | --accent-foreground: 0 0% 98%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 98%; 54 | --border: 0 0% 14.9%; 55 | --input: 0 0% 14.9%; 56 | --ring: 0 0% 83.1%; 57 | --chart-1: 220 70% 50%; 58 | --chart-2: 160 60% 45%; 59 | --chart-3: 30 80% 55%; 60 | --chart-4: 280 65% 60%; 61 | --chart-5: 340 75% 55%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ClerkProvider } from '@clerk/nextjs' 2 | import type { Metadata } from 'next' 3 | import { Toaster } from '@/components/ui/toaster' 4 | import './globals.css' 5 | 6 | export const metadata: Metadata = { 7 | title: 'Protect.js + Next.js + Clerk', 8 | description: 'An example of using Protect.js with Next.js and Clerk', 9 | } 10 | 11 | export default function Layout({ children }: { children: React.ReactNode }) { 12 | return ( 13 | 14 | 15 | 16 |
{children}
17 | 18 | 19 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from '../components/Header' 2 | import UserTable from '../components/UserTable' 3 | import { users } from '@/core/db/schema' 4 | import { db } from '@/core/db' 5 | import { protectClient, getLockContext } from '@/core/protect' 6 | import { auth, currentUser } from '@clerk/nextjs/server' 7 | import { getCtsToken } from '@cipherstash/nextjs' 8 | import type { EncryptedData } from '@cipherstash/protect' 9 | 10 | export type EncryptedUser = { 11 | id: number 12 | name: string 13 | email: string | null 14 | authorized: boolean 15 | role: string 16 | } 17 | 18 | async function getUsers(): Promise { 19 | const { userId } = await auth() 20 | const token = await getCtsToken() 21 | const results = await db.select().from(users).limit(500) 22 | 23 | if (userId && token.success) { 24 | const cts_token = token.ctsToken 25 | const lockContext = getLockContext(cts_token) 26 | 27 | const promises = results.map(async (row) => { 28 | const decryptResult = await protectClient 29 | .decrypt(row.email as EncryptedData) 30 | .withLockContext(lockContext) 31 | 32 | if (decryptResult.failure) { 33 | console.error( 34 | 'Failed to decrypt the email for user', 35 | row.id, 36 | decryptResult.failure.message, 37 | ) 38 | 39 | return row.email 40 | } 41 | 42 | return decryptResult.data 43 | }) 44 | 45 | const data = (await Promise.allSettled(promises)) as PromiseSettledResult< 46 | string | null 47 | >[] 48 | 49 | return results.map((row, index) => ({ 50 | ...row, 51 | authorized: data[index].status === 'fulfilled', 52 | email: 53 | data[index].status === 'fulfilled' 54 | ? data[index].value 55 | : (row.email as { c: string }).c, 56 | })) 57 | } 58 | 59 | return results.map((row) => ({ 60 | id: row.id, 61 | name: row.name, 62 | authorized: false, 63 | email: (row.email as { c: string })?.c, 64 | role: row.role, 65 | })) 66 | } 67 | 68 | export default async function Home() { 69 | const users = await getUsers() 70 | const user = await currentUser() 71 | 72 | return ( 73 |
74 |
75 |
76 |
77 |

Users

78 | 79 | The email address of each user was encrypted with CipherStash and{' '} 80 | locked to the individual who created the user. Only that 81 | individual will be able to decrypt the email. 82 | 83 |
84 | 88 |
89 |
90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/AddUserForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | import { useRouter } from 'next/navigation' 5 | import { addUser } from '../lib/actions' 6 | import { Button } from './ui/button' 7 | import { Input } from './ui/input' 8 | import { Label } from './ui/label' 9 | import { 10 | Select, 11 | SelectContent, 12 | SelectItem, 13 | SelectTrigger, 14 | SelectValue, 15 | } from './ui/select' 16 | import { useToast } from '@/hooks/use-toast' 17 | 18 | export default function AddUserForm() { 19 | const [role, setRole] = useState('') 20 | const router = useRouter() 21 | const { toast } = useToast() 22 | 23 | const handleSubmit = async (formData: FormData) => { 24 | formData.append('role', role) 25 | const result = await addUser(formData) 26 | if (result.error) { 27 | toast({ 28 | title: 'Error', 29 | description: result.error, 30 | variant: 'destructive', 31 | }) 32 | } else { 33 | toast({ 34 | title: 'Success', 35 | description: 'User added successfully', 36 | }) 37 | router.push('/') 38 | } 39 | } 40 | 41 | return ( 42 |
43 |
44 | 45 | 46 |
47 |
48 | 49 | 50 |
51 |
52 | 53 | 63 |
64 | 67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs' 2 | import Link from 'next/link' 3 | import Image from 'next/image' 4 | import { 5 | Breadcrumb, 6 | BreadcrumbItem, 7 | BreadcrumbLink, 8 | BreadcrumbList, 9 | BreadcrumbSeparator, 10 | } from '@/components/ui/breadcrumb' 11 | 12 | import { Button } from './ui/button' 13 | import { Github, KeyIcon } from 'lucide-react' 14 | 15 | export default function Header() { 16 | return ( 17 |
18 |
19 |
20 | 21 | Logo 27 | 28 |
29 | / 30 |

protect.js

31 | / 32 | 33 | 34 | 35 | Users 36 | 37 | 38 | 39 | Add a user 40 | 41 | 42 | 43 |
44 |
45 |
46 | 47 | 48 | 51 | 52 | 53 | 54 | 55 | 56 |
57 |
58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/UserTable.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { InfoIcon } from 'lucide-react' 4 | import type { EncryptedUser } from '../app/page' 5 | import { 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableHead, 10 | TableHeader, 11 | TableRow, 12 | } from './ui/table' 13 | 14 | import { 15 | Tooltip, 16 | TooltipContent, 17 | TooltipProvider, 18 | TooltipTrigger, 19 | } from '@/components/ui/tooltip' 20 | 21 | export default function UserTable({ 22 | users, 23 | email = 'Your user', 24 | }: { users: EncryptedUser[]; email?: string }) { 25 | return ( 26 |
27 | 28 | 29 | 30 | Name 31 | Email 32 | Role 33 | 34 | 35 | 36 | {users.map((user) => ( 37 | 38 | {user.name} 39 | 40 | 41 | {user.email} 42 | 43 | {!user.authorized && ( 44 | 45 | 46 | 47 | 48 | 49 | 50 |

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

54 |
55 |
56 |
57 | )} 58 |
59 | {user.role} 60 |
61 | ))} 62 |
63 |
64 |
65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /examples/nextjs-clerk/src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>