diff --git a/.changeset/shaggy-points-sink.md b/.changeset/shaggy-points-sink.md new file mode 100644 index 00000000..914baf72 --- /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 +--- + +Implemented a more configurable pattern for the Protect client. + +This release introduces a new `ProtectClientConfig` type that can be used to configure the Protect client. +This is useful if you want to configure the Protect client specific to your application, and will future proof any additional configuration options that are added in the future. + +```ts +import { protect, type ProtectClientConfig } from "@cipherstash/protect"; + +const config: ProtectClientConfig = { + schemas: [users, orders], + workspaceCrn: "your-workspace-crn", + accessKey: "your-access-key", + clientId: "your-client-id", + clientKey: "your-client-key", +} + +const protectClient = await protect(config); +``` + +The now deprecated method of passing your tables to the `protect` client is no longer supported. + +```ts +import { protect, type ProtectClientConfig } from "@cipherstash/protect"; + +// old method (no longer supported) +const protectClient = await protect(users, orders); + +// required method +const config: ProtectClientConfig = { + schemas: [users, orders], +} + +const protectClient = await protect(config); +``` + + diff --git a/README.md b/README.md index f1a72843..40d0d201 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 5e2d4828..8a37d1e3 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 12dedefc..40f0c813 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 d6935917..6a57b3ad 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 fc256419..594c3122 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 40e1fc8c..b92963dd 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 8e683cd5..790cab7c 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 0ab806d4..077f73f4 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 a5b44b1c..ff34db88 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 7340a55c..52f5e78e 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 18a4d4e6..da2ae264 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 f1a72843..40d0d201 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 ec54ebe4..0dc13fd0 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 735db68e..c7b438b7 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 9b130785..1ceade2d 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 9658d460..c9e1b65c 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", "zod": "^3.24.2" }, "optionalDependencies": { diff --git a/packages/protect/src/ffi/index.ts b/packages/protect/src/ffi/index.ts index 46fa858b..40ac4232 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: { + encryptConfig: 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.encryptConfig, + ) 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 7b5dbbce..600078fb 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({ + 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 ff53de60..2f1d7ef0 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 ff41c431..812a5d89 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 + version: 0.15.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': + resolution: {integrity: sha512-+/KxVmO1OO7v0Dt8yAXWCOj5pIHSxDwyuQle3aU+pbkk9Wp03t3imdUdJTsjNa8VKDM9YZ+J8W2xCBQTrprCYg==} 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': + resolution: {integrity: sha512-EgLW59GiasDpO1oRnquLm80AmUds8TftVvENkUp9TjIWyXk6ghC6QEYKGIQHXIS7KbjN2gfhlGPS2Vi4B+o5Hw==} 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': + resolution: {integrity: sha512-kcyaERju3cIIpEAwhyD/mtFKsuP0lLxGS6bnhIo9oNcJzNxYMKp6jlE5P+IgofnDJflDXq7lGzrWvLkZqmwGKA==} 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': + resolution: {integrity: sha512-kT91kHz91tp1H/2rybFekD63+Xqc6e0bD5lzjBajtGiekfwIxLdiO+FejlGXlL50OjMy3tucnJ1uuZcLVpeYnA==} 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': + resolution: {integrity: sha512-pS1rkyh//9T6MVHrbYXazIXcxdOSLq3hZ/x2OKfL51zyOSFB3H/w0DYV/Zu5J3rk/M/CfcjpCOB6+j3tD6WbjA==} 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': + resolution: {integrity: sha512-FJuK0GyZ90eCpnNn+uOyy/4ptCMEucEZnMKlzgzTXOb/z7n5deuqOsbxz90N4QLoHwqPUUcec3h9sTboxqFHXw==} '@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': optional: true - '@cipherstash/protect-ffi-darwin-x64@0.14.2': + '@cipherstash/protect-ffi-darwin-x64@0.15.0': optional: true - '@cipherstash/protect-ffi-linux-arm64-gnu@0.14.2': + '@cipherstash/protect-ffi-linux-arm64-gnu@0.15.0': optional: true - '@cipherstash/protect-ffi-linux-x64-gnu@0.14.2': + '@cipherstash/protect-ffi-linux-x64-gnu@0.15.0': optional: true - '@cipherstash/protect-ffi-win32-x64-msvc@0.14.2': + '@cipherstash/protect-ffi-win32-x64-msvc@0.15.0': optional: true - '@cipherstash/protect-ffi@0.14.2': + '@cipherstash/protect-ffi@0.15.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 + '@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: