From 1bc55a0caf64280e787b46d06a306d551be07994 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Tue, 3 Jun 2025 11:10:58 -0600 Subject: [PATCH 1/4] feat(protect): implement protect config object --- .changeset/shaggy-points-sink.md | 43 ++++++++++++ README.md | 13 ++-- docs/getting-started.md | 11 ++-- docs/reference/configuration.md | 35 ++++++---- docs/reference/schema.md | 11 ++-- docs/reference/supabase-sdk.md | 28 ++++++-- examples/basic/protect.ts | 13 +++- examples/drizzle/src/protect.ts | 13 +++- examples/hono-supabase/src/index.ts | 13 +++- examples/next-drizzle-mysql/package.json | 2 +- .../next-drizzle-mysql/src/protect/index.ts | 8 ++- .../nextjs-clerk/src/core/protect/index.ts | 7 +- packages/protect/README.md | 13 ++-- packages/protect/__tests__/protect.test.ts | 38 +++++------ .../protect/__tests__/search-terms.test.ts | 6 +- packages/protect/__tests__/supabase.test.ts | 6 +- packages/protect/package.json | 2 +- packages/protect/src/ffi/index.ts | 31 ++++++--- packages/protect/src/index.ts | 31 +++++++-- packages/utils/config/index.ts | 6 +- pnpm-lock.yaml | 65 ++++++++----------- 21 files changed, 274 insertions(+), 121 deletions(-) create mode 100644 .changeset/shaggy-points-sink.md diff --git a/.changeset/shaggy-points-sink.md b/.changeset/shaggy-points-sink.md new file mode 100644 index 000000000..d8b199b67 --- /dev/null +++ b/.changeset/shaggy-points-sink.md @@ -0,0 +1,43 @@ +--- +"@cipherstash/protect": major +"next-drizzle-mysql": minor +"@cipherstash/nextjs-clerk-example": minor +"@cipherstash/basic-example": minor +--- + +Implement a more configurable pattern for the Protect client. + +This release introduces a new `ProtectClientConfig` type that can be used to configure the Protect client. +This is useful if you want to configure the Protect client specific to your application, and will future proof any additional configuration options that are added in the future. + +```ts +import { protect, type ProtectClientConfig } from "@cipherstash/protect"; + +const config: ProtectClientConfig = { + schemas: [users, orders], + workspaceCrn: "your-workspace-crn", + accessKey: "your-access-key", + clientId: "your-client-id", + clientKey: "your-client-key", +} + +const protectClient = await protect(config); +``` + +The now deprecated method of passing your tables to the `protect` client is no longer supported. + +```ts +import { protect, type ProtectClientConfig } from "@cipherstash/protect"; + +// old method (no longer supported) +const protectClient = await protect(users, orders); + +// required method +const config: ProtectClientConfig = { + schemas: [users, orders], +} + +const protectClient = await protect(config); +``` + + diff --git a/README.md b/README.md index f1a728432..40d0d201b 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,8 @@ At the end of `stash setup`, you will have two files in your project: > Don't commit `cipherstash.secret.toml` to git; it contains sensitive credentials. > The `stash setup` command will attempt to append to your `.gitignore` file with the `cipherstash.secret.toml` file. -Read more about [configuration via TOML file or environment variables](./docs/reference/configuration.md). +Read more about [configuration via TOML file, environment variables, or the Protect client config object](./docs/reference/configuration.md) to meet your needs. +The method you choose will depend on your use case. ### Basic file structure @@ -209,14 +210,18 @@ Read more about [defining your schema](./docs/reference/schema.md). To import the `protect` function and initialize a client with your defined schema, add the following to `src/protect/index.ts`: ```ts -import { protect } from "@cipherstash/protect"; +import { protect, type ProtectClientConfig } from "@cipherstash/protect"; import { users, orders } from "./schema"; +const config: ProtectClientConfig = { + schemas: [users, orders], +} + // Pass all your tables to the protect function to initialize the client -export const protectClient = await protect(users, orders); +export const protectClient = await protect(config); ``` -The `protect` function requires at least one `csTable` be provided. +The `protect` function requires at least one `csTable` be provided in the `schemas` array. ### Encrypt data diff --git a/docs/getting-started.md b/docs/getting-started.md index 5e2d48284..8a37d1e3b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -154,14 +154,15 @@ Read more about [defining your schema](./docs/reference/schema.md). To import the `protect` function and initialize a client with your defined schema, add the following to `src/protect/index.ts`: ```ts -import { protect } from "@cipherstash/protect"; +import { protect, type ProtectClientConfig } from "@cipherstash/protect"; import { users, orders } from "./schema"; -// Pass all your tables to the protect function to initialize the client -export const protectClient = await protect(users, orders); -``` +const config: ProtectClientConfig = { + schemas: [users, orders], +} -The `protect` function requires at least one `csTable` to be provided. +export const protectClient = await protect(config); +``` ## Step 5: Encrypt data diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 12dedefcd..40f0c813b 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -12,6 +12,7 @@ Environment variables will take precedence over configuration files and it's rec - [`[auth]` section](#auth-section) - [cipherstash.secret.toml](#cipherstashsecrettoml) - [Environment variables](#environment-variables) +- [Configuring the Protect client directly](#configuring-the-protect-client-directly) - [Deploying to production](#deploying-to-production) - [Region configuration](#region-configuration) - [File system write permissions](#file-system-write-permissions) @@ -38,7 +39,7 @@ The minimal `cipherstash.toml` file: client_id = "your-client-id" [auth] -workspace_id = "your-workspace-id" +workspace_crn = "your-workspace-crn" ``` #### `[encrypt]` section @@ -50,7 +51,7 @@ The client key must be stored in `cipherstash.secret.toml` or in an environment #### `[auth]` section -- `workspace_id` (**required**): A base-32 encoded 10-byte unique identifier. +- `workspace_crn` (**required**): The workspace CRN for your CipherStash account. - `access_key` (**not allowed**): This is explicitly disallowed and will be rejected at runtime. The access key must be stored in `cipherstash.secret.toml` or in an environment variable `CS_CLIENT_ACCESS_KEY`. @@ -92,22 +93,34 @@ The following environment variables are supported: |:----------------------:|:---------------------------------------------------------------:|:--------:|:--------------------------------------------:| | `CS_CLIENT_ID` | The client ID for your CipherStash account. | Yes | | | `CS_CLIENT_KEY` | The client key for your CipherStash account. | Yes | | -| `CS_WORKSPACE_ID` | The workspace ID for your CipherStash account. | Yes | | +| `CS_WORKSPACE_CRN` | The workspace CRN for your CipherStash account. | Yes | | | `CS_CLIENT_ACCESS_KEY` | The access key for your CipherStash account. | Yes | | -| `CS_ZEROKMS_HOST` | The host for the ZeroKMS server. | No | `https://ap-southeast-2.aws.viturhosted.net` | | `CS_CONFIG_PATH` | A temporary path to store the CipherStash client configuration. | No | `/home/{username}/.cipherstash` | -## Deploying to production +## Configuring the Protect client directly -> [!TIP] -> There are some configuration details you should take note of when deploying `@cipherstash/protect` in your production examples. +You can also configure the Protect client directly by passing a `ProtectClientConfig` object to the `protect` function during initialization. +This is useful if you want to configure the Protect client specific to your application. +An exmaple of this might be if you want to use a secret manager to store your client key and access key rather than relying on environment variables or configuration files. -### Region configuration +```ts +import { protect, type ProtectClientConfig } from "@cipherstash/protect"; -If you've created a Workspace in a region other than `ap-southeast-2`, you will need to set the `CS_ZEROKMS_HOST` environment variable to the appropriate region. +const config: ProtectClientConfig = { + schemas: [users, orders], + workspaceCrn: "your-workspace-crn", + accessKey: "your-access-key", + clientId: "your-client-id", + clientKey: "your-client-key", +} + +const protectClient = await protect(config); +``` -For example, if you are using ZeroKMS in the `eu-central-1` region, you need to set the `CS_ZEROKMS_HOST` variable to `https://eu-central-1.aws.viturhosted.net`. -This is a known usability issue that will be addressed. +## Deploying to production + +> [!TIP] +> There are some configuration details you should take note of when deploying `@cipherstash/protect` in your production examples. ### File system write permissions diff --git a/docs/reference/schema.md b/docs/reference/schema.md index d6935917b..6a57b3ad0 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -93,14 +93,15 @@ You will use your defined schemas to initialize the EQL client. Simply import your schemas and pass them to the `protect` function. ```ts -import { protect } from "@cipherstash/protect"; +import { protect, type ProtectClientConfig } from "@cipherstash/protect"; import { protectedUsers } from "./schemas/users"; -const protectClient = await protect(protectedUsers, ...); -``` - -The `protect` function requires at least one `csTable` to be passed in. +const config: ProtectClientConfig = { + schemas: [protectedUsers], // At least one csTable is required +} +const protectClient = await protect(config); +``` --- ### Didn't find what you wanted? diff --git a/docs/reference/supabase-sdk.md b/docs/reference/supabase-sdk.md index fc2564199..594c31229 100644 --- a/docs/reference/supabase-sdk.md +++ b/docs/reference/supabase-sdk.md @@ -25,14 +25,24 @@ Under the hood, the EQL payload is a JSON object that is stored as a composite t You can insert encrypted data into the table using Protect.js and the Supabase SDK. Since the `eql_v2_encrypted` column is a composite type, you'll need to use the `encryptedToPgComposite` helper to properly format the data: ```typescript -import { protect, csTable, csColumn, encryptedToPgComposite } from '@cipherstash/protect' +import { + protect, + csTable, + csColumn, + encryptedToPgComposite, + type ProtectClientConfig +} from '@cipherstash/protect' const users = csTable('users', { name: csColumn('name').freeTextSearch().equality(), email: csColumn('email').freeTextSearch().equality() }) -const protectClient = await protect(users) +const config: ProtectClientConfig = { + schemas: [users], +} + +const protectClient = await protect(config) const encryptedResult = await protectClient.encryptModel( { @@ -78,14 +88,24 @@ console.log('Decrypted user:', decryptedResult.data) When working with models that contain multiple encrypted fields, you can use the `modelToEncryptedPgComposites` helper to handle the conversion to PostgreSQL composite types: ```typescript -import { protect, csTable, csColumn, modelToEncryptedPgComposites } from '@cipherstash/protect' +import { + protect, + csTable, + csColumn, + modelToEncryptedPgComposites, + type ProtectClientConfig +} from '@cipherstash/protect' const users = csTable('users', { name: csColumn('name').freeTextSearch().equality(), email: csColumn('email').freeTextSearch().equality() }) -const protectClient = await protect(users) +const config: ProtectClientConfig = { + schemas: [users], +} + +const protectClient = await protect(config) const model = { name: 'John Doe', diff --git a/examples/basic/protect.ts b/examples/basic/protect.ts index 40e1fc8cf..b92963dd7 100644 --- a/examples/basic/protect.ts +++ b/examples/basic/protect.ts @@ -1,8 +1,17 @@ import 'dotenv/config' -import { protect, csColumn, csTable } from '@cipherstash/protect' +import { + protect, + csColumn, + csTable, + type ProtectClientConfig, +} from '@cipherstash/protect' export const users = csTable('users', { name: csColumn('name'), }) -export const protectClient = await protect(users) +const config: ProtectClientConfig = { + schemas: [users], +} + +export const protectClient = await protect(config) diff --git a/examples/drizzle/src/protect.ts b/examples/drizzle/src/protect.ts index 8e683cd50..790cab7ce 100644 --- a/examples/drizzle/src/protect.ts +++ b/examples/drizzle/src/protect.ts @@ -1,5 +1,10 @@ import 'dotenv/config' -import { protect, csColumn, csTable } from '@cipherstash/protect' +import { + protect, + csColumn, + csTable, + type ProtectClientConfig, +} from '@cipherstash/protect' export const users = csTable('users', { email_encrypted: csColumn('email_encrypted') @@ -8,4 +13,8 @@ export const users = csTable('users', { .freeTextSearch(), }) -export const protectClient = await protect(users) +const config: ProtectClientConfig = { + schemas: [users], +} + +export const protectClient = await protect(config) diff --git a/examples/hono-supabase/src/index.ts b/examples/hono-supabase/src/index.ts index 0ab806d4c..077f73f43 100644 --- a/examples/hono-supabase/src/index.ts +++ b/examples/hono-supabase/src/index.ts @@ -4,13 +4,22 @@ import { createClient } from '@supabase/supabase-js' import { Hono } from 'hono' // Consolidated protect and it's schemas into a single file -import { protect, csColumn, csTable } from '@cipherstash/protect' +import { + protect, + csColumn, + csTable, + type ProtectClientConfig, +} from '@cipherstash/protect' export const users = csTable('users', { email: csColumn('email'), }) -export const protectClient = await protect(users) +const config: ProtectClientConfig = { + schemas: [users], +} + +export const protectClient = await protect(config) // Create a single supabase client for interacting with the database const supabaseUrl = process.env.SUPABASE_URL diff --git a/examples/next-drizzle-mysql/package.json b/examples/next-drizzle-mysql/package.json index a5b44b1c9..ff34db887 100644 --- a/examples/next-drizzle-mysql/package.json +++ b/examples/next-drizzle-mysql/package.json @@ -10,7 +10,7 @@ "db:migrate": "drizzle-kit migrate" }, "dependencies": { - "@cipherstash/protect": "^8.3.0", + "@cipherstash/protect": "workspace:*", "@hookform/resolvers": "^5.0.1", "drizzle-orm": "^0.44.0", "mysql2": "^3.14.1", diff --git a/examples/next-drizzle-mysql/src/protect/index.ts b/examples/next-drizzle-mysql/src/protect/index.ts index 7340a55cb..52f5e78e0 100644 --- a/examples/next-drizzle-mysql/src/protect/index.ts +++ b/examples/next-drizzle-mysql/src/protect/index.ts @@ -1,4 +1,8 @@ -import { protect } from '@cipherstash/protect' +import { protect, type ProtectClientConfig } from '@cipherstash/protect' import { users } from './schema' -export const protectClient = await protect(users) +const config: ProtectClientConfig = { + schemas: [users], +} + +export const protectClient = await protect(config) diff --git a/examples/nextjs-clerk/src/core/protect/index.ts b/examples/nextjs-clerk/src/core/protect/index.ts index 18a4d4e6d..da2ae264e 100644 --- a/examples/nextjs-clerk/src/core/protect/index.ts +++ b/examples/nextjs-clerk/src/core/protect/index.ts @@ -5,13 +5,18 @@ import { type CtsToken, csColumn, csTable, + type ProtectClientConfig, } from '@cipherstash/protect' export const users = csTable('users', { email: csColumn('email'), }) -export const protectClient = await protect(users) +const config: ProtectClientConfig = { + schemas: [users], +} + +export const protectClient = await protect(config) export const getLockContext = (cts_token?: CtsToken) => { if (!cts_token) { diff --git a/packages/protect/README.md b/packages/protect/README.md index f1a728432..40d0d201b 100644 --- a/packages/protect/README.md +++ b/packages/protect/README.md @@ -147,7 +147,8 @@ At the end of `stash setup`, you will have two files in your project: > Don't commit `cipherstash.secret.toml` to git; it contains sensitive credentials. > The `stash setup` command will attempt to append to your `.gitignore` file with the `cipherstash.secret.toml` file. -Read more about [configuration via TOML file or environment variables](./docs/reference/configuration.md). +Read more about [configuration via TOML file, environment variables, or the Protect client config object](./docs/reference/configuration.md) to meet your needs. +The method you choose will depend on your use case. ### Basic file structure @@ -209,14 +210,18 @@ Read more about [defining your schema](./docs/reference/schema.md). To import the `protect` function and initialize a client with your defined schema, add the following to `src/protect/index.ts`: ```ts -import { protect } from "@cipherstash/protect"; +import { protect, type ProtectClientConfig } from "@cipherstash/protect"; import { users, orders } from "./schema"; +const config: ProtectClientConfig = { + schemas: [users, orders], +} + // Pass all your tables to the protect function to initialize the client -export const protectClient = await protect(users, orders); +export const protectClient = await protect(config); ``` -The `protect` function requires at least one `csTable` be provided. +The `protect` function requires at least one `csTable` be provided in the `schemas` array. ### Encrypt data diff --git a/packages/protect/__tests__/protect.test.ts b/packages/protect/__tests__/protect.test.ts index ec54ebe47..0dc13fd01 100644 --- a/packages/protect/__tests__/protect.test.ts +++ b/packages/protect/__tests__/protect.test.ts @@ -19,7 +19,7 @@ type User = { describe('encryption and decryption', () => { it('should encrypt and decrypt a payload', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const email = 'hello@example.com' @@ -40,7 +40,7 @@ describe('encryption and decryption', () => { }, 30000) it('should return null if plaintext is null', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const ciphertext = await protectClient.encrypt(null, { column: users.email, @@ -59,7 +59,7 @@ describe('encryption and decryption', () => { }, 30000) it('should encrypt and decrypt a model', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) // Create a model with decrypted values const decryptedModel = { @@ -101,7 +101,7 @@ describe('encryption and decryption', () => { }, 30000) it('should handle null values in a model', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) // Create a model with null values const decryptedModel = { @@ -143,7 +143,7 @@ describe('encryption and decryption', () => { }, 30000) it('should handle undefined values in a model', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) // Create a model with undefined values const decryptedModel = { @@ -187,7 +187,7 @@ describe('encryption and decryption', () => { describe('bulk encryption', () => { it('should bulk encrypt and decrypt models', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) // Create models with decrypted values const decryptedModels = [ @@ -249,7 +249,7 @@ describe('bulk encryption', () => { }, 30000) it('should return empty array if models is empty', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) // Encrypt empty array of models const encryptedModels = await protectClient.bulkEncryptModels( @@ -265,7 +265,7 @@ describe('bulk encryption', () => { }, 30000) it('should return empty array if decrypting empty array of models', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) // Decrypt empty array of models const decryptedResult = await protectClient.bulkDecryptModels([]) @@ -280,7 +280,7 @@ describe('bulk encryption', () => { describe('bulk encryption edge cases', () => { it('should handle mixed null and non-null values in bulk operations', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const decryptedModels = [ { id: '1', @@ -331,7 +331,7 @@ describe('bulk encryption edge cases', () => { }, 30000) it('should handle mixed undefined and non-undefined values in bulk operations', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const decryptedModels = [ { id: '1', @@ -382,7 +382,7 @@ describe('bulk encryption edge cases', () => { }, 30000) it('should handle empty models in bulk operations', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const decryptedModels = [ { id: '1', @@ -430,7 +430,7 @@ describe('bulk encryption edge cases', () => { describe('error handling', () => { it('should handle invalid encrypted payloads', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const validModel = { id: '1', email: 'test@example.com', @@ -464,7 +464,7 @@ describe('error handling', () => { }, 30000) it('should handle missing required fields', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const model = { id: '1', email: null, @@ -485,7 +485,7 @@ describe('error handling', () => { describe('type safety', () => { it('should maintain type safety with complex nested objects', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const model = { id: '1', email: 'test@example.com', @@ -523,7 +523,7 @@ describe('type safety', () => { describe('performance', () => { it('should handle large numbers of models efficiently', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const largeModels = Array(10) .fill(null) .map((_, i) => ({ @@ -566,7 +566,7 @@ describe('performance', () => { // const userJwt = '' // describe('encryption and decryption with lock context', () => { // it('should encrypt and decrypt a payload with lock context', async () => { -// const protectClient = await protect(users) +// const protectClient = await protect({ schemas: [users] }) // const lc = new LockContext() // const lockContext = await lc.identify(userJwt) @@ -598,7 +598,7 @@ describe('performance', () => { // }, 30000) // it('should encrypt and decrypt a model with lock context', async () => { -// const protectClient = await protect(users) +// const protectClient = await protect({ schemas: [users] }) // const lc = new LockContext() // const lockContext = await lc.identify(userJwt) @@ -638,7 +638,7 @@ describe('performance', () => { // }, 30000) // it('should encrypt with context and be unable to decrypt without context', async () => { -// const protectClient = await protect(users) +// const protectClient = await protect({ schemas: [users] }) // const lc = new LockContext() // const lockContext = await lc.identify(userJwt) @@ -671,7 +671,7 @@ describe('performance', () => { // }, 30000) // it('should bulk encrypt and decrypt models with lock context', async () => { -// const protectClient = await protect(users) +// const protectClient = await protect({ schemas: [users] }) // const lc = new LockContext() // const lockContext = await lc.identify(userJwt) diff --git a/packages/protect/__tests__/search-terms.test.ts b/packages/protect/__tests__/search-terms.test.ts index 735db68ef..c7b438b76 100644 --- a/packages/protect/__tests__/search-terms.test.ts +++ b/packages/protect/__tests__/search-terms.test.ts @@ -10,7 +10,7 @@ const users = csTable('users', { describe('create search terms', () => { it('should create search terms with default return type', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const searchTerms = [ { @@ -41,7 +41,7 @@ describe('create search terms', () => { }, 30000) it('should create search terms with composite-literal return type', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const searchTerms = [ { @@ -64,7 +64,7 @@ describe('create search terms', () => { }, 30000) it('should create search terms with escaped-composite-literal return type', async () => { - const protectClient = await protect(users) + const protectClient = await protect({ schemas: [users] }) const searchTerms = [ { diff --git a/packages/protect/__tests__/supabase.test.ts b/packages/protect/__tests__/supabase.test.ts index 9b1307855..1ceade2dc 100644 --- a/packages/protect/__tests__/supabase.test.ts +++ b/packages/protect/__tests__/supabase.test.ts @@ -32,7 +32,7 @@ const table = csTable('protect-ci', { describe('supabase', () => { it('should insert and select encrypted data', async () => { - const protectClient = await protect(table) + const protectClient = await protect({ schemas: [table] }) const e = 'hello world' @@ -74,7 +74,7 @@ describe('supabase', () => { }, 30000) it('should insert and select encrypted model data', async () => { - const protectClient = await protect(table) + const protectClient = await protect({ schemas: [table] }) const model = { encrypted: 'hello world', @@ -124,7 +124,7 @@ describe('supabase', () => { }, 30000) it('should insert and select bulk encrypted model data', async () => { - const protectClient = await protect(table) + const protectClient = await protect({ schemas: [table] }) const models = [ { diff --git a/packages/protect/package.json b/packages/protect/package.json index 9658d4609..94cc6eb61 100644 --- a/packages/protect/package.json +++ b/packages/protect/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@byteslice/result": "^0.2.0", - "@cipherstash/protect-ffi": "0.14.2", + "@cipherstash/protect-ffi": "0.15.0-0", "zod": "^3.24.2" }, "optionalDependencies": { diff --git a/packages/protect/src/ffi/index.ts b/packages/protect/src/ffi/index.ts index 46fa858b8..8d2f2d429 100644 --- a/packages/protect/src/ffi/index.ts +++ b/packages/protect/src/ffi/index.ts @@ -36,18 +36,23 @@ export class ProtectClient { private encryptConfig: EncryptConfig | undefined private workspaceId: string | undefined - constructor() { - const workspaceId = loadWorkSpaceId() + constructor(workspaceCrn?: string) { + const workspaceId = loadWorkSpaceId(workspaceCrn) this.workspaceId = workspaceId } - async init( - encryptConifg?: EncryptConfig, - ): Promise> { + async init(config: { + encryptConifg: EncryptConfig + workspaceCrn?: string + accessKey?: string + clientId?: string + clientKey?: string + }): Promise> { return await withResult( async () => { - const validated: EncryptConfig = - encryptConfigSchema.parse(encryptConifg) + const validated: EncryptConfig = encryptConfigSchema.parse( + config.encryptConifg, + ) logger.debug( 'Initializing the Protect.js client with the following encrypt config:', @@ -56,7 +61,17 @@ export class ProtectClient { }, ) - this.client = await newClient(JSON.stringify(validated)) + const newClientConfig = JSON.stringify({ + workspace_crn: config.workspaceCrn, + access_key: config.accessKey, + client_id: config.clientId, + client_key: config.clientKey, + }) + + this.client = await newClient( + JSON.stringify(validated), + newClientConfig, + ) this.encryptConfig = validated logger.info('Successfully initialized the Protect.js client.') diff --git a/packages/protect/src/index.ts b/packages/protect/src/index.ts index 7b5dbbceb..a468d9585 100644 --- a/packages/protect/src/index.ts +++ b/packages/protect/src/index.ts @@ -16,19 +16,40 @@ export interface ProtectError { } type AtLeastOneCsTable = [T, ...T[]] + +export type ProtectClientConfig = { + schemas: AtLeastOneCsTable> + workspaceCrn?: string + accessKey?: string + clientId?: string + clientKey?: string +} + export const protect = async ( - ...tables: AtLeastOneCsTable> + config: ProtectClientConfig, ): Promise => { - if (!tables.length) { + const { schemas } = config + + if (!schemas.length) { throw new Error( '[protect]: At least one csTable must be provided to initialize the protect client', ) } - const client = new ProtectClient() - const encryptConfig = buildEncryptConfig(...tables) + const clientConfig = { + workspaceCrn: config.workspaceCrn, + accessKey: config.accessKey, + clientId: config.clientId, + clientKey: config.clientKey, + } + + const client = new ProtectClient(clientConfig.workspaceCrn) + const encryptConfig = buildEncryptConfig(...schemas) - const result = await client.init(encryptConfig) + const result = await client.init({ + encryptConifg: encryptConfig, + ...clientConfig, + }) if (result.failure) { throw new Error(`[protect]: ${result.failure.message}`) diff --git a/packages/utils/config/index.ts b/packages/utils/config/index.ts index ff53de607..2f1d7ef07 100644 --- a/packages/utils/config/index.ts +++ b/packages/utils/config/index.ts @@ -56,9 +56,13 @@ function extractWorkspaceIdFromCrn(crn: string): string { return match[1] } -export function loadWorkSpaceId(): string { +export function loadWorkSpaceId(suppliedCrn?: string): string { const configPath = path.join(process.cwd(), 'cipherstash.toml') + if (suppliedCrn) { + return extractWorkspaceIdFromCrn(suppliedCrn) + } + if (!fs.existsSync(configPath) && !process.env.CS_WORKSPACE_CRN) { throw new Error( 'You have not defined a workspace CRN in your config file, or the CS_WORKSPACE_CRN environment variable.', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff41c4312..d4000b2a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,8 +124,8 @@ importers: examples/next-drizzle-mysql: dependencies: '@cipherstash/protect': - specifier: ^8.3.0 - version: 8.3.0 + specifier: workspace:* + version: link:../../packages/protect '@hookform/resolvers': specifier: ^5.0.1 version: 5.0.1(react-hook-form@7.56.4(react@19.0.0)) @@ -299,8 +299,8 @@ importers: specifier: ^0.2.0 version: 0.2.0 '@cipherstash/protect-ffi': - specifier: 0.14.2 - version: 0.14.2 + specifier: 0.15.0-0 + version: 0.15.0-0 zod: specifier: ^3.24.2 version: 3.24.2 @@ -463,36 +463,33 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@cipherstash/protect-ffi-darwin-arm64@0.14.2': - resolution: {integrity: sha512-cwt+nXolRYXUMYrZ/JuXs+ujKnM7y7pr7DeYciXFfGVk+vuffbO71j3CyRBIdZ+6JFlNJHrOBwkXjvsOdgAVqw==} + '@cipherstash/protect-ffi-darwin-arm64@0.15.0-0': + resolution: {integrity: sha512-c15MjWsBIWzMZOgrpHClimwtjcRhuIMrfurLiTBSqQ/hSh4yNYlVmItQTRLj2RgkBYQXkeoqqOkedaW8+j8c8A==} cpu: [arm64] os: [darwin] - '@cipherstash/protect-ffi-darwin-x64@0.14.2': - resolution: {integrity: sha512-JLJyhhQqUYWxJuJnEnNd4+3IKim0UEFTXEcq60lEp2yiHCkFNdBU2IDJOyB8UipzIJyVPWwZtkGXf2B+n7z8nA==} + '@cipherstash/protect-ffi-darwin-x64@0.15.0-0': + resolution: {integrity: sha512-s+7R+Rj0wzK6WiISVo12EKIsrpIu3/mPS+4Jv5zB5+roFB8Zj255NcyPgWKdyvhVKz3c75TpiSNp0R/LeRC2lA==} cpu: [x64] os: [darwin] - '@cipherstash/protect-ffi-linux-arm64-gnu@0.14.2': - resolution: {integrity: sha512-LtrkkXbWtqwq9McN6Cd6P0yWnbHoa7luRvqR3s2taQ71nJcezZwAP1Ur7KbBNZDkzt1yrfNOLOtpFSeZGRFSGw==} + '@cipherstash/protect-ffi-linux-arm64-gnu@0.15.0-0': + resolution: {integrity: sha512-EJ9ebv5EMHiABUyfxEQrlvZdiJXOIJubLUfdRe4flsduVIrMNW0Ol5J8VjqhRZ/hulGEdFpX1ZAaHiDa+p35ng==} cpu: [arm64] os: [linux] - '@cipherstash/protect-ffi-linux-x64-gnu@0.14.2': - resolution: {integrity: sha512-fi8MQ5BdXf+jJBJuhZyHVwPA7qzaTAn8t189pOAsSx1340rYBMITx9qbA2aruTF2ex7jDDP8fvEAPmhpJv2/eQ==} + '@cipherstash/protect-ffi-linux-x64-gnu@0.15.0-0': + resolution: {integrity: sha512-eJP5xsyicS3KOmM5rLpGillrdeKkAzxp8ehaYLK5OgVNe7/Di/ugQJUTqj53yq9T/PhqHtPvzv5IJpuf6VU6rg==} cpu: [x64] os: [linux] - '@cipherstash/protect-ffi-win32-x64-msvc@0.14.2': - resolution: {integrity: sha512-ZMjSZ5x5Hmvrl/gGSv6WJEaUQI4F6EexG9WvuNhR00lVEqxmmI8yibUTEZqnbmxDF9kYus0IucnYrN4mXdepCw==} + '@cipherstash/protect-ffi-win32-x64-msvc@0.15.0-0': + resolution: {integrity: sha512-2aqbE6oX+O+0XXhGtQfy7wzmY9Tbg5OFinUnUbaiIkwDUUiVetXWU25JsLKjWI8sNIQfS+fQjwT1uA/FuUrPRQ==} cpu: [x64] os: [win32] - '@cipherstash/protect-ffi@0.14.2': - resolution: {integrity: sha512-+y7V5gMkMTxIV4KFW2gJAwGj6EoJ17i31l0LXf8A4QDr2F5htQwy8ek53z9zTe3zRfOX0zd4RRUAZJR2EJ7r8w==} - - '@cipherstash/protect@8.3.0': - resolution: {integrity: sha512-jqw6J8nAZG7o863A+ZxgoV2VFrfSABKn0FLhvs/50GnqEdGCkdZXH7rWRJ7hPTGNBiZvh4qf8HAcjsVo08sWgw==} + '@cipherstash/protect-ffi@0.15.0-0': + resolution: {integrity: sha512-6Id5HrlV6EuD+igwbdhychpC37vsfztdpl8FJbxExP5Cc4+dTbj5UuRbS0ahLe5gMMKJQFjKVGQq7ZvN2x56ig==} '@clerk/backend@1.24.0': resolution: {integrity: sha512-DlOZ9pnCY77ngHKFZzC7ZImHBVjMf2whPLvnnBt4YXjkvuQ3m1v1tQHUXb8qqlwilptHU4/WzkOlXytez+iJ+A==} @@ -4173,38 +4170,30 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 - '@cipherstash/protect-ffi-darwin-arm64@0.14.2': + '@cipherstash/protect-ffi-darwin-arm64@0.15.0-0': optional: true - '@cipherstash/protect-ffi-darwin-x64@0.14.2': + '@cipherstash/protect-ffi-darwin-x64@0.15.0-0': optional: true - '@cipherstash/protect-ffi-linux-arm64-gnu@0.14.2': + '@cipherstash/protect-ffi-linux-arm64-gnu@0.15.0-0': optional: true - '@cipherstash/protect-ffi-linux-x64-gnu@0.14.2': + '@cipherstash/protect-ffi-linux-x64-gnu@0.15.0-0': optional: true - '@cipherstash/protect-ffi-win32-x64-msvc@0.14.2': + '@cipherstash/protect-ffi-win32-x64-msvc@0.15.0-0': optional: true - '@cipherstash/protect-ffi@0.14.2': + '@cipherstash/protect-ffi@0.15.0-0': dependencies: '@neon-rs/load': 0.1.82 optionalDependencies: - '@cipherstash/protect-ffi-darwin-arm64': 0.14.2 - '@cipherstash/protect-ffi-darwin-x64': 0.14.2 - '@cipherstash/protect-ffi-linux-arm64-gnu': 0.14.2 - '@cipherstash/protect-ffi-linux-x64-gnu': 0.14.2 - '@cipherstash/protect-ffi-win32-x64-msvc': 0.14.2 - - '@cipherstash/protect@8.3.0': - dependencies: - '@byteslice/result': 0.2.0 - '@cipherstash/protect-ffi': 0.14.2 - zod: 3.24.2 - optionalDependencies: - '@rollup/rollup-linux-x64-gnu': 4.24.0 + '@cipherstash/protect-ffi-darwin-arm64': 0.15.0-0 + '@cipherstash/protect-ffi-darwin-x64': 0.15.0-0 + '@cipherstash/protect-ffi-linux-arm64-gnu': 0.15.0-0 + '@cipherstash/protect-ffi-linux-x64-gnu': 0.15.0-0 + '@cipherstash/protect-ffi-win32-x64-msvc': 0.15.0-0 '@clerk/backend@1.24.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: From 5f7b71d37cf164d7d352672c58492753a9e43994 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Tue, 3 Jun 2025 11:11:21 -0600 Subject: [PATCH 2/4] chore: language --- .changeset/shaggy-points-sink.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/shaggy-points-sink.md b/.changeset/shaggy-points-sink.md index d8b199b67..914baf720 100644 --- a/.changeset/shaggy-points-sink.md +++ b/.changeset/shaggy-points-sink.md @@ -5,7 +5,7 @@ "@cipherstash/basic-example": minor --- -Implement a more configurable pattern for the Protect client. +Implemented a more configurable pattern for the Protect client. This release introduces a new `ProtectClientConfig` type that can be used to configure the Protect client. This is useful if you want to configure the Protect client specific to your application, and will future proof any additional configuration options that are added in the future. From f65528fb6d1e3edbe8112c886654f99b244b38a8 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Tue, 3 Jun 2025 11:15:32 -0600 Subject: [PATCH 3/4] chore(protect): spelling of encryptConfig --- packages/protect/src/ffi/index.ts | 4 ++-- packages/protect/src/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/protect/src/ffi/index.ts b/packages/protect/src/ffi/index.ts index 8d2f2d429..40ac4232a 100644 --- a/packages/protect/src/ffi/index.ts +++ b/packages/protect/src/ffi/index.ts @@ -42,7 +42,7 @@ export class ProtectClient { } async init(config: { - encryptConifg: EncryptConfig + encryptConfig: EncryptConfig workspaceCrn?: string accessKey?: string clientId?: string @@ -51,7 +51,7 @@ export class ProtectClient { return await withResult( async () => { const validated: EncryptConfig = encryptConfigSchema.parse( - config.encryptConifg, + config.encryptConfig, ) logger.debug( diff --git a/packages/protect/src/index.ts b/packages/protect/src/index.ts index a468d9585..600078fb8 100644 --- a/packages/protect/src/index.ts +++ b/packages/protect/src/index.ts @@ -47,7 +47,7 @@ export const protect = async ( const encryptConfig = buildEncryptConfig(...schemas) const result = await client.init({ - encryptConifg: encryptConfig, + encryptConfig, ...clientConfig, }) From 247f9970bfb7d15e654b0f2cd8fc6454ef86dce6 Mon Sep 17 00:00:00 2001 From: CJ Brewer Date: Tue, 3 Jun 2025 19:50:27 -0600 Subject: [PATCH 4/4] chore(protect): use protectjs-ffi 0.15.0 --- packages/protect/package.json | 2 +- pnpm-lock.yaml | 50 +++++++++++++++++------------------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/protect/package.json b/packages/protect/package.json index 94cc6eb61..c9e1b65c6 100644 --- a/packages/protect/package.json +++ b/packages/protect/package.json @@ -53,7 +53,7 @@ }, "dependencies": { "@byteslice/result": "^0.2.0", - "@cipherstash/protect-ffi": "0.15.0-0", + "@cipherstash/protect-ffi": "0.15.0", "zod": "^3.24.2" }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4000b2a0..812a5d89a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -299,8 +299,8 @@ importers: specifier: ^0.2.0 version: 0.2.0 '@cipherstash/protect-ffi': - specifier: 0.15.0-0 - version: 0.15.0-0 + specifier: 0.15.0 + version: 0.15.0 zod: specifier: ^3.24.2 version: 3.24.2 @@ -463,33 +463,33 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@cipherstash/protect-ffi-darwin-arm64@0.15.0-0': - resolution: {integrity: sha512-c15MjWsBIWzMZOgrpHClimwtjcRhuIMrfurLiTBSqQ/hSh4yNYlVmItQTRLj2RgkBYQXkeoqqOkedaW8+j8c8A==} + '@cipherstash/protect-ffi-darwin-arm64@0.15.0': + resolution: {integrity: sha512-+/KxVmO1OO7v0Dt8yAXWCOj5pIHSxDwyuQle3aU+pbkk9Wp03t3imdUdJTsjNa8VKDM9YZ+J8W2xCBQTrprCYg==} cpu: [arm64] os: [darwin] - '@cipherstash/protect-ffi-darwin-x64@0.15.0-0': - resolution: {integrity: sha512-s+7R+Rj0wzK6WiISVo12EKIsrpIu3/mPS+4Jv5zB5+roFB8Zj255NcyPgWKdyvhVKz3c75TpiSNp0R/LeRC2lA==} + '@cipherstash/protect-ffi-darwin-x64@0.15.0': + resolution: {integrity: sha512-EgLW59GiasDpO1oRnquLm80AmUds8TftVvENkUp9TjIWyXk6ghC6QEYKGIQHXIS7KbjN2gfhlGPS2Vi4B+o5Hw==} cpu: [x64] os: [darwin] - '@cipherstash/protect-ffi-linux-arm64-gnu@0.15.0-0': - resolution: {integrity: sha512-EJ9ebv5EMHiABUyfxEQrlvZdiJXOIJubLUfdRe4flsduVIrMNW0Ol5J8VjqhRZ/hulGEdFpX1ZAaHiDa+p35ng==} + '@cipherstash/protect-ffi-linux-arm64-gnu@0.15.0': + resolution: {integrity: sha512-kcyaERju3cIIpEAwhyD/mtFKsuP0lLxGS6bnhIo9oNcJzNxYMKp6jlE5P+IgofnDJflDXq7lGzrWvLkZqmwGKA==} cpu: [arm64] os: [linux] - '@cipherstash/protect-ffi-linux-x64-gnu@0.15.0-0': - resolution: {integrity: sha512-eJP5xsyicS3KOmM5rLpGillrdeKkAzxp8ehaYLK5OgVNe7/Di/ugQJUTqj53yq9T/PhqHtPvzv5IJpuf6VU6rg==} + '@cipherstash/protect-ffi-linux-x64-gnu@0.15.0': + resolution: {integrity: sha512-kT91kHz91tp1H/2rybFekD63+Xqc6e0bD5lzjBajtGiekfwIxLdiO+FejlGXlL50OjMy3tucnJ1uuZcLVpeYnA==} cpu: [x64] os: [linux] - '@cipherstash/protect-ffi-win32-x64-msvc@0.15.0-0': - resolution: {integrity: sha512-2aqbE6oX+O+0XXhGtQfy7wzmY9Tbg5OFinUnUbaiIkwDUUiVetXWU25JsLKjWI8sNIQfS+fQjwT1uA/FuUrPRQ==} + '@cipherstash/protect-ffi-win32-x64-msvc@0.15.0': + resolution: {integrity: sha512-pS1rkyh//9T6MVHrbYXazIXcxdOSLq3hZ/x2OKfL51zyOSFB3H/w0DYV/Zu5J3rk/M/CfcjpCOB6+j3tD6WbjA==} cpu: [x64] os: [win32] - '@cipherstash/protect-ffi@0.15.0-0': - resolution: {integrity: sha512-6Id5HrlV6EuD+igwbdhychpC37vsfztdpl8FJbxExP5Cc4+dTbj5UuRbS0ahLe5gMMKJQFjKVGQq7ZvN2x56ig==} + '@cipherstash/protect-ffi@0.15.0': + resolution: {integrity: sha512-FJuK0GyZ90eCpnNn+uOyy/4ptCMEucEZnMKlzgzTXOb/z7n5deuqOsbxz90N4QLoHwqPUUcec3h9sTboxqFHXw==} '@clerk/backend@1.24.0': resolution: {integrity: sha512-DlOZ9pnCY77ngHKFZzC7ZImHBVjMf2whPLvnnBt4YXjkvuQ3m1v1tQHUXb8qqlwilptHU4/WzkOlXytez+iJ+A==} @@ -4170,30 +4170,30 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 - '@cipherstash/protect-ffi-darwin-arm64@0.15.0-0': + '@cipherstash/protect-ffi-darwin-arm64@0.15.0': optional: true - '@cipherstash/protect-ffi-darwin-x64@0.15.0-0': + '@cipherstash/protect-ffi-darwin-x64@0.15.0': optional: true - '@cipherstash/protect-ffi-linux-arm64-gnu@0.15.0-0': + '@cipherstash/protect-ffi-linux-arm64-gnu@0.15.0': optional: true - '@cipherstash/protect-ffi-linux-x64-gnu@0.15.0-0': + '@cipherstash/protect-ffi-linux-x64-gnu@0.15.0': optional: true - '@cipherstash/protect-ffi-win32-x64-msvc@0.15.0-0': + '@cipherstash/protect-ffi-win32-x64-msvc@0.15.0': optional: true - '@cipherstash/protect-ffi@0.15.0-0': + '@cipherstash/protect-ffi@0.15.0': dependencies: '@neon-rs/load': 0.1.82 optionalDependencies: - '@cipherstash/protect-ffi-darwin-arm64': 0.15.0-0 - '@cipherstash/protect-ffi-darwin-x64': 0.15.0-0 - '@cipherstash/protect-ffi-linux-arm64-gnu': 0.15.0-0 - '@cipherstash/protect-ffi-linux-x64-gnu': 0.15.0-0 - '@cipherstash/protect-ffi-win32-x64-msvc': 0.15.0-0 + '@cipherstash/protect-ffi-darwin-arm64': 0.15.0 + '@cipherstash/protect-ffi-darwin-x64': 0.15.0 + '@cipherstash/protect-ffi-linux-arm64-gnu': 0.15.0 + '@cipherstash/protect-ffi-linux-x64-gnu': 0.15.0 + '@cipherstash/protect-ffi-win32-x64-msvc': 0.15.0 '@clerk/backend@1.24.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: