Skip to content

Commit 38555cc

Browse files
committed
feat(protect): init raw bulk interfaces
1 parent 9008990 commit 38555cc

File tree

7 files changed

+704
-2
lines changed

7 files changed

+704
-2
lines changed

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,46 @@ const decryptedUsers = decryptedResult.data;
528528
The model encryption methods provide a higher-level interface that's particularly useful when working with ORMs or when you need to encrypt multiple fields in an object.
529529
They automatically handle the mapping between your model's structure and the encrypted fields defined in your schema.
530530

531+
#### Bulk encryption operations
532+
533+
For encrypting multiple individual values (rather than entire models), use the `bulkEncrypt` method:
534+
535+
```typescript
536+
import { protectClient } from "./protect";
537+
import { users } from "./protect/schema";
538+
539+
// Array of payloads with IDs and plaintext values
540+
const plaintexts = [
541+
{ id: "1", plaintext: "user1@example.com" },
542+
{ id: "2", plaintext: "user2@example.com" },
543+
{ id: "3", plaintext: null },
544+
];
545+
546+
// Encrypt multiple values at once
547+
const encryptedResult = await protectClient.bulkEncrypt(plaintexts, {
548+
column: users.email,
549+
table: users,
550+
});
551+
552+
if (encryptedResult.failure) {
553+
// Handle the failure
554+
}
555+
556+
const encryptedData = encryptedResult.data;
557+
// Returns: [
558+
// { id: "1", c: "encrypted_value_1" },
559+
// { id: "2", c: "encrypted_value_2" },
560+
// { id: "3", c: null }
561+
// ]
562+
563+
// You can then decrypt individual values using the decrypt method
564+
const decryptedResult = await protectClient.decrypt(encryptedData[0].c);
565+
```
566+
567+
The `bulkEncrypt` method is useful when you need to encrypt multiple values for the same column and table, but don't want to encrypt entire model objects. Each encrypted value maintains its own unique key while benefiting from ZeroKMS's bulk operation performance.
568+
569+
The model encryption methods provide a higher-level interface that's particularly useful when working with ORMs or when you need to encrypt multiple fields in an object.
570+
531571
### Store encrypted data in a database
532572

533573
Encrypted data can be stored in any database that supports JSONB, noting that searchable encryption is only supported in PostgreSQL at the moment.
@@ -719,6 +759,40 @@ const bulkDecryptedResult = await protectClient
719759
.withLockContext(lockContext);
720760
```
721761

762+
### Bulk encryption operations with lock context
763+
764+
Bulk encryption operations also support lock contexts for identity-aware encryption:
765+
766+
```typescript
767+
import { protectClient } from "./protect";
768+
import { users } from "./protect/schema";
769+
770+
const plaintexts = [
771+
{ id: "1", plaintext: "user1@example.com" },
772+
{ id: "2", plaintext: "user2@example.com" },
773+
{ id: "3", plaintext: null },
774+
];
775+
776+
// Bulk encrypt with lock context
777+
const encryptedResult = await protectClient
778+
.bulkEncrypt(plaintexts, {
779+
column: users.email,
780+
table: users,
781+
})
782+
.withLockContext(lockContext);
783+
784+
if (encryptedResult.failure) {
785+
// Handle the failure
786+
}
787+
788+
const encryptedData = encryptedResult.data;
789+
790+
// Decrypt individual values with lock context
791+
const decryptedResult = await protectClient
792+
.decrypt(encryptedData[0].c)
793+
.withLockContext(lockContext);
794+
```
795+
722796
## Supported data types
723797

724798
Protect.js currently supports encrypting and decrypting text.

examples/basic/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,32 @@ async function main() {
4343
console.log('Decrypting the ciphertext...')
4444
console.log('The plaintext is:', plaintext)
4545

46+
// Demonstrate bulk encryption
47+
console.log('\n--- Bulk Encryption Demo ---')
48+
49+
const bulkPlaintexts = [
50+
{ id: '1', plaintext: 'Alice' },
51+
{ id: '2', plaintext: 'Bob' },
52+
{ id: '3', plaintext: 'Charlie' },
53+
{ id: '4', plaintext: null },
54+
]
55+
56+
console.log(
57+
'Bulk encrypting names:',
58+
bulkPlaintexts.map((p) => p.plaintext),
59+
)
60+
61+
const bulkEncryptResult = await protectClient.bulkEncrypt(bulkPlaintexts, {
62+
column: users.name,
63+
table: users,
64+
})
65+
66+
if (bulkEncryptResult.failure) {
67+
throw new Error(`[protect]: ${bulkEncryptResult.failure.message}`)
68+
}
69+
70+
console.log('Bulk encrypted data:', bulkEncryptResult.data)
71+
4672
rl.close()
4773
}
4874

packages/protect/__tests__/protect.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,96 @@ describe('performance', () => {
665665
}, 60000)
666666
})
667667

668+
describe('bulk encryption operations', () => {
669+
it('should bulk encrypt payloads with IDs', async () => {
670+
const plaintexts = [
671+
{ id: '1', plaintext: 'hello@example.com' },
672+
{ id: '2', plaintext: 'world@example.com' },
673+
{ id: '3', plaintext: null },
674+
]
675+
676+
const encryptedData = await protectClient.bulkEncrypt(plaintexts, {
677+
column: users.email,
678+
table: users,
679+
})
680+
681+
if (encryptedData.failure) {
682+
throw new Error(`[protect]: ${encryptedData.failure.message}`)
683+
}
684+
685+
// Verify encrypted data structure
686+
expect(encryptedData.data).toHaveLength(3)
687+
expect(encryptedData.data[0]).toHaveProperty('id', '1')
688+
expect(encryptedData.data[0]).toHaveProperty('c')
689+
expect(encryptedData.data[1]).toHaveProperty('id', '2')
690+
expect(encryptedData.data[1]).toHaveProperty('c')
691+
expect(encryptedData.data[2]).toHaveProperty('id', '3')
692+
expect(encryptedData.data[2]).toHaveProperty('c', null)
693+
694+
// Verify encrypted values are different from plaintext
695+
expect(encryptedData.data[0].c).not.toBe('hello@example.com')
696+
expect(encryptedData.data[1].c).not.toBe('world@example.com')
697+
}, 30000)
698+
699+
it('should bulk encrypt simple arrays', async () => {
700+
const plaintexts = ['hello@example.com', 'world@example.com', null]
701+
702+
const encryptedData = await protectClient.bulkEncrypt(plaintexts, {
703+
column: users.email,
704+
table: users,
705+
})
706+
707+
if (encryptedData.failure) {
708+
throw new Error(`[protect]: ${encryptedData.failure.message}`)
709+
}
710+
711+
// Verify encrypted data structure
712+
expect(encryptedData.data).toHaveLength(3)
713+
expect(encryptedData.data[0]).toHaveProperty('c')
714+
expect(encryptedData.data[1]).toHaveProperty('c')
715+
expect(encryptedData.data[2]).toBeNull()
716+
717+
// Verify encrypted values are different from plaintext
718+
expect(encryptedData.data[0]).not.toBe('hello@example.com')
719+
expect(encryptedData.data[1]).not.toBe('world@example.com')
720+
}, 30000)
721+
722+
it('should return empty array if plaintexts is empty', async () => {
723+
const encryptedData = await protectClient.bulkEncrypt([], {
724+
column: users.email,
725+
table: users,
726+
})
727+
728+
if (encryptedData.failure) {
729+
throw new Error(`[protect]: ${encryptedData.failure.message}`)
730+
}
731+
732+
expect(encryptedData.data).toEqual([])
733+
}, 30000)
734+
735+
it('should handle mixed null and non-null values', async () => {
736+
const plaintexts = [
737+
{ id: '1', plaintext: 'test1' },
738+
{ id: '2', plaintext: null },
739+
{ id: '3', plaintext: 'test3' },
740+
]
741+
742+
const encryptedData = await protectClient.bulkEncrypt(plaintexts, {
743+
column: users.email,
744+
table: users,
745+
})
746+
747+
if (encryptedData.failure) {
748+
throw new Error(`[protect]: ${encryptedData.failure.message}`)
749+
}
750+
751+
expect(encryptedData.data).toHaveLength(3)
752+
expect(encryptedData.data[0].c).not.toBeNull()
753+
expect(encryptedData.data[1].c).toBeNull()
754+
expect(encryptedData.data[2].c).not.toBeNull()
755+
}, 30000)
756+
})
757+
668758
// ------------------------
669759
// TODO get LockContext working in CI.
670760
// To manually test locally, uncomment the following lines and provide a valid JWT in the userJwt variable.

packages/protect/src/ffi/index.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import { BulkDecryptModelsOperation } from './operations/bulk-decrypt-models'
1818
import { EncryptOperation } from './operations/encrypt'
1919
import { DecryptOperation } from './operations/decrypt'
2020
import { SearchTermsOperation } from './operations/search-terms'
21+
import {
22+
BulkEncryptOperation,
23+
type BulkEncryptPayload,
24+
} from './operations/bulk-encrypt'
2125
import {
2226
type EncryptConfig,
2327
encryptConfigSchema,
@@ -76,9 +80,9 @@ export class ProtectClient {
7680
logger.info('Successfully initialized the Protect.js client.')
7781
return this
7882
},
79-
(error) => ({
83+
(error: unknown) => ({
8084
type: ProtectErrorTypes.ClientInitError,
81-
message: error.message,
85+
message: (error as Error).message,
8286
}),
8387
)
8488
}
@@ -153,6 +157,19 @@ export class ProtectClient {
153157
return new BulkDecryptModelsOperation(this.client, input)
154158
}
155159

160+
/**
161+
* Bulk encryption - returns a thenable object.
162+
* Usage:
163+
* await eqlClient.bulkEncrypt(plaintexts, { column, table })
164+
* await eqlClient.bulkEncrypt(plaintexts, { column, table }).withLockContext(lockContext)
165+
*/
166+
bulkEncrypt(
167+
plaintexts: BulkEncryptPayload,
168+
opts: EncryptOptions,
169+
): BulkEncryptOperation {
170+
return new BulkEncryptOperation(this.client, plaintexts, opts)
171+
}
172+
156173
/**
157174
* Create search terms to use in a query searching encrypted data
158175
* Usage:

0 commit comments

Comments
 (0)