Skip to content

Commit 7136fd1

Browse files
committed
feat(protect): init audit logging
1 parent 18e5eec commit 7136fd1

File tree

3 files changed

+59
-28
lines changed

3 files changed

+59
-28
lines changed

packages/protect/__tests__/protect.test.ts

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import 'dotenv/config'
2-
import { describe, expect, it, vi } from 'vitest'
2+
import { describe, expect, it, beforeAll } from 'vitest'
33

44
import { LockContext, protect, csTable, csColumn } from '../src'
55

@@ -17,10 +17,16 @@ type User = {
1717
number?: number
1818
}
1919

20+
let protectClient: Awaited<ReturnType<typeof protect>>
21+
22+
beforeAll(async () => {
23+
protectClient = await protect({
24+
schemas: [users],
25+
})
26+
})
27+
2028
describe('encryption and decryption', () => {
2129
it('should encrypt and decrypt a payload', async () => {
22-
const protectClient = await protect({ schemas: [users] })
23-
2430
const email = 'hello@example.com'
2531

2632
const ciphertext = await protectClient.encrypt(email, {
@@ -35,16 +41,18 @@ describe('encryption and decryption', () => {
3541
// Verify encrypted field
3642
expect(ciphertext.data).toHaveProperty('c')
3743

38-
const plaintext = await protectClient.decrypt(ciphertext.data)
44+
const plaintext = await protectClient.decrypt(ciphertext.data).audit({
45+
metadata: {
46+
user: 'cj@cjb.io',
47+
},
48+
})
3949

4050
expect(plaintext).toEqual({
4151
data: email,
4252
})
4353
}, 30000)
4454

4555
it('should return null if plaintext is null', async () => {
46-
const protectClient = await protect({ schemas: [users] })
47-
4856
const ciphertext = await protectClient.encrypt(null, {
4957
column: users.email,
5058
table: users,
@@ -65,8 +73,6 @@ describe('encryption and decryption', () => {
6573
}, 30000)
6674

6775
it('should encrypt and decrypt a model', async () => {
68-
const protectClient = await protect({ schemas: [users] })
69-
7076
// Create a model with decrypted values
7177
const decryptedModel = {
7278
id: '1',
@@ -117,8 +123,6 @@ describe('encryption and decryption', () => {
117123
}, 30000)
118124

119125
it('should handle null values in a model', async () => {
120-
const protectClient = await protect({ schemas: [users] })
121-
122126
// Create a model with null values
123127
const decryptedModel = {
124128
id: '1',
@@ -169,8 +173,6 @@ describe('encryption and decryption', () => {
169173
}, 30000)
170174

171175
it('should handle undefined values in a model', async () => {
172-
const protectClient = await protect({ schemas: [users] })
173-
174176
// Create a model with undefined values
175177
const decryptedModel = {
176178
id: '1',
@@ -223,8 +225,6 @@ describe('encryption and decryption', () => {
223225

224226
describe('bulk encryption', () => {
225227
it('should bulk encrypt and decrypt models', async () => {
226-
const protectClient = await protect({ schemas: [users] })
227-
228228
// Create models with decrypted values
229229
const decryptedModels = [
230230
{
@@ -301,8 +301,6 @@ describe('bulk encryption', () => {
301301
}, 30000)
302302

303303
it('should return empty array if models is empty', async () => {
304-
const protectClient = await protect({ schemas: [users] })
305-
306304
// Encrypt empty array of models
307305
const encryptedModels = await protectClient.bulkEncryptModels<User>(
308306
[],
@@ -317,8 +315,6 @@ describe('bulk encryption', () => {
317315
}, 30000)
318316

319317
it('should return empty array if decrypting empty array of models', async () => {
320-
const protectClient = await protect({ schemas: [users] })
321-
322318
// Decrypt empty array of models
323319
const decryptedResult = await protectClient.bulkDecryptModels<User>([])
324320

@@ -332,7 +328,6 @@ describe('bulk encryption', () => {
332328

333329
describe('bulk encryption edge cases', () => {
334330
it('should handle mixed null and non-null values in bulk operations', async () => {
335-
const protectClient = await protect({ schemas: [users] })
336331
const decryptedModels = [
337332
{
338333
id: '1',
@@ -405,7 +400,6 @@ describe('bulk encryption edge cases', () => {
405400
}, 30000)
406401

407402
it('should handle mixed undefined and non-undefined values in bulk operations', async () => {
408-
const protectClient = await protect({ schemas: [users] })
409403
const decryptedModels = [
410404
{
411405
id: '1',
@@ -478,7 +472,6 @@ describe('bulk encryption edge cases', () => {
478472
}, 30000)
479473

480474
it('should handle empty models in bulk operations', async () => {
481-
const protectClient = await protect({ schemas: [users] })
482475
const decryptedModels = [
483476
{
484477
id: '1',
@@ -548,7 +541,6 @@ describe('bulk encryption edge cases', () => {
548541

549542
describe('error handling', () => {
550543
it('should handle invalid encrypted payloads', async () => {
551-
const protectClient = await protect({ schemas: [users] })
552544
const validModel = {
553545
id: '1',
554546
email: 'test@example.com',
@@ -582,7 +574,6 @@ describe('error handling', () => {
582574
}, 30000)
583575

584576
it('should handle missing required fields', async () => {
585-
const protectClient = await protect({ schemas: [users] })
586577
const model = {
587578
id: '1',
588579
email: null,
@@ -603,7 +594,6 @@ describe('error handling', () => {
603594

604595
describe('type safety', () => {
605596
it('should maintain type safety with complex nested objects', async () => {
606-
const protectClient = await protect({ schemas: [users] })
607597
const model = {
608598
id: '1',
609599
email: 'test@example.com',
@@ -641,7 +631,6 @@ describe('type safety', () => {
641631

642632
describe('performance', () => {
643633
it('should handle large numbers of models efficiently', async () => {
644-
const protectClient = await protect({ schemas: [users] })
645634
const largeModels = Array(10)
646635
.fill(null)
647636
.map((_, i) => ({
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export type AuditConfig = {
2+
metadata?: Record<string, unknown>
3+
}
4+
5+
export class BaseOperation<T> {
6+
protected auditMetadata?: Record<string, unknown>
7+
8+
/**
9+
* Attach audit metadata to this operation. Can be chained.
10+
* @param config Configuration for ZeroKMS audit logging
11+
* @param config.metadata Arbitrary JSON object for appending metadata to the audit log
12+
*/
13+
audit(config: AuditConfig): this {
14+
this.auditMetadata = config.metadata
15+
return this
16+
}
17+
18+
/**
19+
* Get the audit metadata for this operation.
20+
*/
21+
public getAuditMetadata(): Record<string, unknown> | undefined {
22+
return this.auditMetadata
23+
}
24+
}

packages/protect/src/ffi/operations/decrypt.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,29 @@ import { type ProtectError, ProtectErrorTypes } from '../..'
55
import { logger } from '../../../../utils/logger'
66
import type { LockContext } from '../../identify'
77
import type { Client, EncryptedPayload } from '../../types'
8+
import { BaseOperation } from './base-operation'
89

910
export class DecryptOperation
11+
extends BaseOperation<Result<string | null, ProtectError>>
1012
implements PromiseLike<Result<string | null, ProtectError>>
1113
{
1214
private client: Client
1315
private encryptedData: EncryptedPayload
1416

1517
constructor(client: Client, encryptedData: EncryptedPayload) {
18+
super()
1619
this.client = client
1720
this.encryptedData = encryptedData
1821
}
1922

2023
public withLockContext(
2124
lockContext: LockContext,
2225
): DecryptOperationWithLockContext {
23-
return new DecryptOperationWithLockContext(this, lockContext)
26+
const opWithLock = new DecryptOperationWithLockContext(this, lockContext)
27+
if (this.getAuditMetadata()) {
28+
opWithLock.audit(this.getAuditMetadata() ?? {})
29+
}
30+
return opWithLock
2431
}
2532

2633
public then<TResult1 = Result<string | null, ProtectError>, TResult2 = never>(
@@ -46,7 +53,9 @@ export class DecryptOperation
4653
return null
4754
}
4855

49-
logger.debug('Decrypting data WITHOUT a lock context')
56+
logger.debug('Decrypting data WITHOUT a lock context', {
57+
auditMetadata: this.getAuditMetadata(),
58+
})
5059
return await ffiDecrypt(this.client, this.encryptedData.c)
5160
},
5261
(error) => ({
@@ -59,23 +68,30 @@ export class DecryptOperation
5968
public getOperation(): {
6069
client: Client
6170
encryptedData: EncryptedPayload
71+
auditMetadata?: Record<string, unknown>
6272
} {
6373
return {
6474
client: this.client,
6575
encryptedData: this.encryptedData,
76+
auditMetadata: this.getAuditMetadata(),
6677
}
6778
}
6879
}
6980

7081
export class DecryptOperationWithLockContext
82+
extends BaseOperation<Result<string | null, ProtectError>>
7183
implements PromiseLike<Result<string | null, ProtectError>>
7284
{
7385
private operation: DecryptOperation
7486
private lockContext: LockContext
7587

7688
constructor(operation: DecryptOperation, lockContext: LockContext) {
89+
super()
7790
this.operation = operation
7891
this.lockContext = lockContext
92+
if (operation.getAuditMetadata()) {
93+
this.audit(operation.getAuditMetadata() ?? {})
94+
}
7995
}
8096

8197
public then<TResult1 = Result<string | null, ProtectError>, TResult2 = never>(
@@ -103,7 +119,9 @@ export class DecryptOperationWithLockContext
103119
return null
104120
}
105121

106-
logger.debug('Decrypting data WITH a lock context')
122+
logger.debug('Decrypting data WITH a lock context', {
123+
auditMetadata: this.getAuditMetadata(),
124+
})
107125

108126
const context = await this.lockContext.getLockContext()
109127

0 commit comments

Comments
 (0)