nestjs-audit-trail-workspace 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +4 -0
- package/README.md +293 -0
- package/eslint.config.mjs +35 -0
- package/libs/nestjs-audit-trail/README.md +293 -0
- package/libs/nestjs-audit-trail/package.json +57 -0
- package/libs/nestjs-audit-trail/src/audit-trail-options.ts +10 -0
- package/libs/nestjs-audit-trail/src/audit-trail.module.spec.ts +41 -0
- package/libs/nestjs-audit-trail/src/audit-trail.module.ts +45 -0
- package/libs/nestjs-audit-trail/src/decorators/audit.decorator.ts +18 -0
- package/libs/nestjs-audit-trail/src/decorators/index.ts +1 -0
- package/libs/nestjs-audit-trail/src/hashing/hash-audit.spec.ts +41 -0
- package/libs/nestjs-audit-trail/src/hashing/hash-audit.ts +42 -0
- package/libs/nestjs-audit-trail/src/hashing/index.ts +2 -0
- package/libs/nestjs-audit-trail/src/hashing/serialize-deterministic.ts +31 -0
- package/libs/nestjs-audit-trail/src/index.ts +16 -0
- package/libs/nestjs-audit-trail/src/interceptor/audit.interceptor.spec.ts +87 -0
- package/libs/nestjs-audit-trail/src/interceptor/audit.interceptor.ts +74 -0
- package/libs/nestjs-audit-trail/src/interceptor/index.ts +1 -0
- package/libs/nestjs-audit-trail/src/interfaces/audit-storage.interface.ts +18 -0
- package/libs/nestjs-audit-trail/src/interfaces/index.ts +1 -0
- package/libs/nestjs-audit-trail/src/nestjs-audit-trail.module.ts +8 -0
- package/libs/nestjs-audit-trail/src/nestjs-audit-trail.service.spec.ts +18 -0
- package/libs/nestjs-audit-trail/src/nestjs-audit-trail.service.ts +4 -0
- package/libs/nestjs-audit-trail/src/services/audit.service.spec.ts +90 -0
- package/libs/nestjs-audit-trail/src/services/audit.service.ts +39 -0
- package/libs/nestjs-audit-trail/src/services/index.ts +1 -0
- package/libs/nestjs-audit-trail/src/types/audit-record.ts +29 -0
- package/libs/nestjs-audit-trail/src/types/index.ts +1 -0
- package/libs/nestjs-audit-trail/tsconfig.lib.json +13 -0
- package/nest-cli.json +16 -0
- package/package.json +83 -0
- package/test/jest-e2e.json +16 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +33 -0
package/.prettierrc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# nestjs-audit-trail
|
|
2
|
+
|
|
3
|
+
A small, storage-agnostic audit trail module for NestJS.
|
|
4
|
+
|
|
5
|
+
If you need a reliable record of who did what, when, and what changed - without coupling your app to a specific database or ORM - this is for you.
|
|
6
|
+
|
|
7
|
+
Records are SHA256-hashed and can be chained for tamper-evidence. You bring your own persistence layer.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install nestjs-audit-trail
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
1. Implement `IAuditStorage` (or use an adapter from a separate package).
|
|
18
|
+
2. Register the module and the interceptor.
|
|
19
|
+
3. Mark handlers with `@Audit({ action, entity })`.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// app.module.ts
|
|
23
|
+
import { Module } from '@nestjs/common';
|
|
24
|
+
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
25
|
+
import {
|
|
26
|
+
AuditTrailModule,
|
|
27
|
+
AuditInterceptor,
|
|
28
|
+
type IAuditStorage,
|
|
29
|
+
} from 'nestjs-audit-trail';
|
|
30
|
+
|
|
31
|
+
const myStorage: IAuditStorage = {
|
|
32
|
+
save: async (record) => {
|
|
33
|
+
// persist to your DB, queue, or object store
|
|
34
|
+
},
|
|
35
|
+
getLastHash: async () => null, // optional, for chain linking
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
@Module({
|
|
39
|
+
imports: [
|
|
40
|
+
AuditTrailModule.forRoot({
|
|
41
|
+
storage: myStorage,
|
|
42
|
+
correlationIdHeader: 'x-correlation-id', // optional
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
providers: [{ provide: APP_INTERCEPTOR, useClass: AuditInterceptor }],
|
|
46
|
+
})
|
|
47
|
+
export class AppModule {}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// user.controller.ts
|
|
52
|
+
import { Controller, Post, Body } from '@nestjs/common';
|
|
53
|
+
import { Audit } from 'nestjs-audit-trail';
|
|
54
|
+
|
|
55
|
+
@Controller('users')
|
|
56
|
+
export class UserController {
|
|
57
|
+
@Post()
|
|
58
|
+
@Audit({ action: 'CREATE', entity: 'User' })
|
|
59
|
+
create(@Body() body: CreateUserDto) {
|
|
60
|
+
return this.userService.create(body);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The interceptor reads `action` and `entity` from `@Audit`, and fills `actorId` (from `x-actor-id` or `request.user.id`), `correlationId` (from the configured header), `payloadBefore` (request body), and `payloadAfter` (handler result). Records are hashed and saved via your storage.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Storage interface
|
|
70
|
+
|
|
71
|
+
All persistence is behind this contract. The core does not implement storage.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import type { AuditRecord, IAuditStorage } from 'nestjs-audit-trail';
|
|
75
|
+
|
|
76
|
+
const adapter: IAuditStorage = {
|
|
77
|
+
async save(record: AuditRecord): Promise<void> {
|
|
78
|
+
// Persist record (e.g. insert into table, send to queue, write to S3).
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// Optional: return the hash of the last saved record for chain linking.
|
|
82
|
+
async getLastHash(): Promise<string | null> {
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`AuditRecord` is a plain type: `action`, `entity`, `actorId`, `correlationId`, `payloadBefore`, `payloadAfter`, `metadata`, `hash`, `createdAt`, `previousHash?`. No ORM decorators; you map it to your schema.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Building custom storage adapters
|
|
93
|
+
|
|
94
|
+
Adapters live in **separate packages** (e.g. `@nestjs-audit-trail/typeorm`, `@nestjs-audit-trail/prisma`). Your adapter only needs to implement `IAuditStorage`.
|
|
95
|
+
|
|
96
|
+
1. Create a new package that depends on `nestjs-audit-trail` and your persistence layer.
|
|
97
|
+
2. Implement `IAuditStorage`: in `save()`, map `AuditRecord` to your model and persist; optionally implement `getLastHash()` by reading the latest row and returning its `hash`.
|
|
98
|
+
3. Export a ready-to-use storage instance or factory (e.g. a function that takes a data source and returns `IAuditStorage`).
|
|
99
|
+
4. In the app, install the adapter and pass its storage to `AuditTrailModule.forRoot({ storage })`.
|
|
100
|
+
|
|
101
|
+
The core stays free of ORM/database imports; only NestJS and Node `crypto` are used.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Domain examples
|
|
106
|
+
|
|
107
|
+
Same library, different domains: use `@Audit` with actions and entities that match your domain. Pass a storage implementation that fits your stack (SQL, NoSQL, queue, object store).
|
|
108
|
+
|
|
109
|
+
### Healthcare (e.g. PHI / HIPAA-style audit)
|
|
110
|
+
|
|
111
|
+
Audit access and changes to patient and clinical data. Use a storage backend that meets your retention and access rules.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
@Controller('patients')
|
|
115
|
+
export class PatientController {
|
|
116
|
+
@Get(':id/records')
|
|
117
|
+
@Audit({ action: 'READ', entity: 'PatientRecord' })
|
|
118
|
+
getRecords(@Param('id') id: string) {
|
|
119
|
+
return this.patientService.getRecords(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Patch(':id')
|
|
123
|
+
@Audit({ action: 'UPDATE', entity: 'Patient' })
|
|
124
|
+
updatePatient(@Param('id') id: string, @Body() dto: UpdatePatientDto) {
|
|
125
|
+
return this.patientService.update(id, dto);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Post(':id/prescriptions')
|
|
129
|
+
@Audit({ action: 'CREATE', entity: 'Prescription' })
|
|
130
|
+
createPrescription(
|
|
131
|
+
@Param('id') id: string,
|
|
132
|
+
@Body() dto: CreatePrescriptionDto,
|
|
133
|
+
) {
|
|
134
|
+
return this.patientService.addPrescription(id, dto);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Ecommerce (orders, inventory, payments)
|
|
140
|
+
|
|
141
|
+
Audit order lifecycle, inventory changes, and payment events for support and disputes.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
@Controller('orders')
|
|
145
|
+
export class OrderController {
|
|
146
|
+
@Post()
|
|
147
|
+
@Audit({ action: 'CREATE', entity: 'Order' })
|
|
148
|
+
create(@Body() dto: CreateOrderDto) {
|
|
149
|
+
return this.orderService.create(dto);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@Patch(':id/status')
|
|
153
|
+
@Audit({ action: 'UPDATE', entity: 'Order' })
|
|
154
|
+
updateStatus(@Param('id') id: string, @Body() body: { status: string }) {
|
|
155
|
+
return this.orderService.setStatus(id, body.status);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Controller('inventory')
|
|
160
|
+
export class InventoryController {
|
|
161
|
+
@Patch(':sku')
|
|
162
|
+
@Audit({ action: 'UPDATE', entity: 'Inventory' })
|
|
163
|
+
adjust(@Param('sku') sku: string, @Body() dto: AdjustInventoryDto) {
|
|
164
|
+
return this.inventoryService.adjust(sku, dto);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Fintech (transactions, KYC, accounts)
|
|
170
|
+
|
|
171
|
+
Audit transactions, KYC updates, and account changes for regulatory and operational traceability.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
@Controller('transactions')
|
|
175
|
+
export class TransactionController {
|
|
176
|
+
@Post('transfer')
|
|
177
|
+
@Audit({ action: 'CREATE', entity: 'Transfer' })
|
|
178
|
+
transfer(@Body() dto: TransferDto) {
|
|
179
|
+
return this.transferService.execute(dto);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@Controller('accounts')
|
|
184
|
+
export class AccountController {
|
|
185
|
+
@Patch(':id')
|
|
186
|
+
@Audit({ action: 'UPDATE', entity: 'Account' })
|
|
187
|
+
updateAccount(@Param('id') id: string, @Body() dto: UpdateAccountDto) {
|
|
188
|
+
return this.accountService.update(id, dto);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@Controller('kyc')
|
|
193
|
+
export class KycController {
|
|
194
|
+
@Post('submit')
|
|
195
|
+
@Audit({ action: 'SUBMIT', entity: 'KycApplication' })
|
|
196
|
+
submit(@Body() dto: KycDto) {
|
|
197
|
+
return this.kycService.submit(dto);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@Patch(':id/approve')
|
|
201
|
+
@Audit({ action: 'APPROVE', entity: 'KycApplication' })
|
|
202
|
+
approve(@Param('id') id: string) {
|
|
203
|
+
return this.kycService.approve(id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Education (enrollments, grades, courses)
|
|
209
|
+
|
|
210
|
+
Audit enrollment and grade changes for academic integrity and admin audits.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
@Controller('enrollments')
|
|
214
|
+
export class EnrollmentController {
|
|
215
|
+
@Post()
|
|
216
|
+
@Audit({ action: 'CREATE', entity: 'Enrollment' })
|
|
217
|
+
enroll(@Body() dto: EnrollDto) {
|
|
218
|
+
return this.enrollmentService.enroll(dto);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@Delete(':id')
|
|
222
|
+
@Audit({ action: 'DELETE', entity: 'Enrollment' })
|
|
223
|
+
withdraw(@Param('id') id: string) {
|
|
224
|
+
return this.enrollmentService.withdraw(id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@Controller('grades')
|
|
229
|
+
export class GradeController {
|
|
230
|
+
@Put(':enrollmentId')
|
|
231
|
+
@Audit({ action: 'UPDATE', entity: 'Grade' })
|
|
232
|
+
setGrade(@Param('enrollmentId') id: string, @Body() dto: GradeDto) {
|
|
233
|
+
return this.gradeService.setGrade(id, dto);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@Controller('courses')
|
|
238
|
+
export class CourseController {
|
|
239
|
+
@Post()
|
|
240
|
+
@Audit({ action: 'CREATE', entity: 'Course' })
|
|
241
|
+
create(@Body() dto: CreateCourseDto) {
|
|
242
|
+
return this.courseService.create(dto);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
In every case you use the same `@Audit({ action, entity })` and one `IAuditStorage` implementation; only the semantics (and your storage backend) change per domain.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Manual recording
|
|
252
|
+
|
|
253
|
+
You can skip the interceptor and record from services:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { AuditService } from 'nestjs-audit-trail';
|
|
257
|
+
|
|
258
|
+
@Injectable()
|
|
259
|
+
export class MyService {
|
|
260
|
+
constructor(private readonly audit: AuditService) {}
|
|
261
|
+
|
|
262
|
+
async doSomething(actorId: string, correlationId: string) {
|
|
263
|
+
const before = { count: 0 };
|
|
264
|
+
// ... work ...
|
|
265
|
+
const after = { count: 1 };
|
|
266
|
+
await this.audit.record({
|
|
267
|
+
action: 'UPDATE',
|
|
268
|
+
entity: 'Counter',
|
|
269
|
+
actorId,
|
|
270
|
+
correlationId,
|
|
271
|
+
payloadBefore: before,
|
|
272
|
+
payloadAfter: after,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Configuration
|
|
281
|
+
|
|
282
|
+
| Option | Required | Default | Description |
|
|
283
|
+
| --------------------- | -------- | -------------------- | ------------------------------------ |
|
|
284
|
+
| `storage` | Yes | — | Your `IAuditStorage` implementation. |
|
|
285
|
+
| `correlationIdHeader` | No | `'x-correlation-id'` | Request header used for correlation. |
|
|
286
|
+
|
|
287
|
+
Set `x-actor-id` on the request (or ensure `request.user.id` is set) so each record has an actor. The core uses deterministic SHA256 hashing and optional chaining via `getLastHash()`; no database or ORM is required.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import eslint from '@eslint/js';
|
|
3
|
+
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
|
4
|
+
import globals from 'globals';
|
|
5
|
+
import tseslint from 'typescript-eslint';
|
|
6
|
+
|
|
7
|
+
export default tseslint.config(
|
|
8
|
+
{
|
|
9
|
+
ignores: ['eslint.config.mjs'],
|
|
10
|
+
},
|
|
11
|
+
eslint.configs.recommended,
|
|
12
|
+
...tseslint.configs.recommendedTypeChecked,
|
|
13
|
+
eslintPluginPrettierRecommended,
|
|
14
|
+
{
|
|
15
|
+
languageOptions: {
|
|
16
|
+
globals: {
|
|
17
|
+
...globals.node,
|
|
18
|
+
...globals.jest,
|
|
19
|
+
},
|
|
20
|
+
sourceType: 'commonjs',
|
|
21
|
+
parserOptions: {
|
|
22
|
+
projectService: true,
|
|
23
|
+
tsconfigRootDir: import.meta.dirname,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
rules: {
|
|
29
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
30
|
+
'@typescript-eslint/no-floating-promises': 'warn',
|
|
31
|
+
'@typescript-eslint/no-unsafe-argument': 'warn',
|
|
32
|
+
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# nestjs-audit-trail
|
|
2
|
+
|
|
3
|
+
A small, storage-agnostic audit trail module for NestJS.
|
|
4
|
+
|
|
5
|
+
If you need a reliable record of who did what, when, and what changed - without coupling your app to a specific database or ORM - this is for you.
|
|
6
|
+
|
|
7
|
+
Records are SHA256-hashed and can be chained for tamper-evidence. You bring your own persistence layer.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install nestjs-audit-trail
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
1. Implement `IAuditStorage` (or use an adapter from a separate package).
|
|
18
|
+
2. Register the module and the interceptor.
|
|
19
|
+
3. Mark handlers with `@Audit({ action, entity })`.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// app.module.ts
|
|
23
|
+
import { Module } from '@nestjs/common';
|
|
24
|
+
import { APP_INTERCEPTOR } from '@nestjs/core';
|
|
25
|
+
import {
|
|
26
|
+
AuditTrailModule,
|
|
27
|
+
AuditInterceptor,
|
|
28
|
+
type IAuditStorage,
|
|
29
|
+
} from 'nestjs-audit-trail';
|
|
30
|
+
|
|
31
|
+
const myStorage: IAuditStorage = {
|
|
32
|
+
save: async (record) => {
|
|
33
|
+
// persist to your DB, queue, or object store
|
|
34
|
+
},
|
|
35
|
+
getLastHash: async () => null, // optional, for chain linking
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
@Module({
|
|
39
|
+
imports: [
|
|
40
|
+
AuditTrailModule.forRoot({
|
|
41
|
+
storage: myStorage,
|
|
42
|
+
correlationIdHeader: 'x-correlation-id', // optional
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
providers: [{ provide: APP_INTERCEPTOR, useClass: AuditInterceptor }],
|
|
46
|
+
})
|
|
47
|
+
export class AppModule {}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// user.controller.ts
|
|
52
|
+
import { Controller, Post, Body } from '@nestjs/common';
|
|
53
|
+
import { Audit } from 'nestjs-audit-trail';
|
|
54
|
+
|
|
55
|
+
@Controller('users')
|
|
56
|
+
export class UserController {
|
|
57
|
+
@Post()
|
|
58
|
+
@Audit({ action: 'CREATE', entity: 'User' })
|
|
59
|
+
create(@Body() body: CreateUserDto) {
|
|
60
|
+
return this.userService.create(body);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The interceptor reads `action` and `entity` from `@Audit`, and fills `actorId` (from `x-actor-id` or `request.user.id`), `correlationId` (from the configured header), `payloadBefore` (request body), and `payloadAfter` (handler result). Records are hashed and saved via your storage.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Storage interface
|
|
70
|
+
|
|
71
|
+
All persistence is behind this contract. The core does not implement storage.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import type { AuditRecord, IAuditStorage } from 'nestjs-audit-trail';
|
|
75
|
+
|
|
76
|
+
const adapter: IAuditStorage = {
|
|
77
|
+
async save(record: AuditRecord): Promise<void> {
|
|
78
|
+
// Persist record (e.g. insert into table, send to queue, write to S3).
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// Optional: return the hash of the last saved record for chain linking.
|
|
82
|
+
async getLastHash(): Promise<string | null> {
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`AuditRecord` is a plain type: `action`, `entity`, `actorId`, `correlationId`, `payloadBefore`, `payloadAfter`, `metadata`, `hash`, `createdAt`, `previousHash?`. No ORM decorators; you map it to your schema.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Building custom storage adapters
|
|
93
|
+
|
|
94
|
+
Adapters live in **separate packages** (e.g. `@nestjs-audit-trail/typeorm`, `@nestjs-audit-trail/prisma`). Your adapter only needs to implement `IAuditStorage`.
|
|
95
|
+
|
|
96
|
+
1. Create a new package that depends on `nestjs-audit-trail` and your persistence layer.
|
|
97
|
+
2. Implement `IAuditStorage`: in `save()`, map `AuditRecord` to your model and persist; optionally implement `getLastHash()` by reading the latest row and returning its `hash`.
|
|
98
|
+
3. Export a ready-to-use storage instance or factory (e.g. a function that takes a data source and returns `IAuditStorage`).
|
|
99
|
+
4. In the app, install the adapter and pass its storage to `AuditTrailModule.forRoot({ storage })`.
|
|
100
|
+
|
|
101
|
+
The core stays free of ORM/database imports; only NestJS and Node `crypto` are used.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Domain examples
|
|
106
|
+
|
|
107
|
+
Same library, different domains: use `@Audit` with actions and entities that match your domain. Pass a storage implementation that fits your stack (SQL, NoSQL, queue, object store).
|
|
108
|
+
|
|
109
|
+
### Healthcare (e.g. PHI / HIPAA-style audit)
|
|
110
|
+
|
|
111
|
+
Audit access and changes to patient and clinical data. Use a storage backend that meets your retention and access rules.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
@Controller('patients')
|
|
115
|
+
export class PatientController {
|
|
116
|
+
@Get(':id/records')
|
|
117
|
+
@Audit({ action: 'READ', entity: 'PatientRecord' })
|
|
118
|
+
getRecords(@Param('id') id: string) {
|
|
119
|
+
return this.patientService.getRecords(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Patch(':id')
|
|
123
|
+
@Audit({ action: 'UPDATE', entity: 'Patient' })
|
|
124
|
+
updatePatient(@Param('id') id: string, @Body() dto: UpdatePatientDto) {
|
|
125
|
+
return this.patientService.update(id, dto);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Post(':id/prescriptions')
|
|
129
|
+
@Audit({ action: 'CREATE', entity: 'Prescription' })
|
|
130
|
+
createPrescription(
|
|
131
|
+
@Param('id') id: string,
|
|
132
|
+
@Body() dto: CreatePrescriptionDto,
|
|
133
|
+
) {
|
|
134
|
+
return this.patientService.addPrescription(id, dto);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Ecommerce (orders, inventory, payments)
|
|
140
|
+
|
|
141
|
+
Audit order lifecycle, inventory changes, and payment events for support and disputes.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
@Controller('orders')
|
|
145
|
+
export class OrderController {
|
|
146
|
+
@Post()
|
|
147
|
+
@Audit({ action: 'CREATE', entity: 'Order' })
|
|
148
|
+
create(@Body() dto: CreateOrderDto) {
|
|
149
|
+
return this.orderService.create(dto);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@Patch(':id/status')
|
|
153
|
+
@Audit({ action: 'UPDATE', entity: 'Order' })
|
|
154
|
+
updateStatus(@Param('id') id: string, @Body() body: { status: string }) {
|
|
155
|
+
return this.orderService.setStatus(id, body.status);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Controller('inventory')
|
|
160
|
+
export class InventoryController {
|
|
161
|
+
@Patch(':sku')
|
|
162
|
+
@Audit({ action: 'UPDATE', entity: 'Inventory' })
|
|
163
|
+
adjust(@Param('sku') sku: string, @Body() dto: AdjustInventoryDto) {
|
|
164
|
+
return this.inventoryService.adjust(sku, dto);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Fintech (transactions, KYC, accounts)
|
|
170
|
+
|
|
171
|
+
Audit transactions, KYC updates, and account changes for regulatory and operational traceability.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
@Controller('transactions')
|
|
175
|
+
export class TransactionController {
|
|
176
|
+
@Post('transfer')
|
|
177
|
+
@Audit({ action: 'CREATE', entity: 'Transfer' })
|
|
178
|
+
transfer(@Body() dto: TransferDto) {
|
|
179
|
+
return this.transferService.execute(dto);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@Controller('accounts')
|
|
184
|
+
export class AccountController {
|
|
185
|
+
@Patch(':id')
|
|
186
|
+
@Audit({ action: 'UPDATE', entity: 'Account' })
|
|
187
|
+
updateAccount(@Param('id') id: string, @Body() dto: UpdateAccountDto) {
|
|
188
|
+
return this.accountService.update(id, dto);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@Controller('kyc')
|
|
193
|
+
export class KycController {
|
|
194
|
+
@Post('submit')
|
|
195
|
+
@Audit({ action: 'SUBMIT', entity: 'KycApplication' })
|
|
196
|
+
submit(@Body() dto: KycDto) {
|
|
197
|
+
return this.kycService.submit(dto);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@Patch(':id/approve')
|
|
201
|
+
@Audit({ action: 'APPROVE', entity: 'KycApplication' })
|
|
202
|
+
approve(@Param('id') id: string) {
|
|
203
|
+
return this.kycService.approve(id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Education (enrollments, grades, courses)
|
|
209
|
+
|
|
210
|
+
Audit enrollment and grade changes for academic integrity and admin audits.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
@Controller('enrollments')
|
|
214
|
+
export class EnrollmentController {
|
|
215
|
+
@Post()
|
|
216
|
+
@Audit({ action: 'CREATE', entity: 'Enrollment' })
|
|
217
|
+
enroll(@Body() dto: EnrollDto) {
|
|
218
|
+
return this.enrollmentService.enroll(dto);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@Delete(':id')
|
|
222
|
+
@Audit({ action: 'DELETE', entity: 'Enrollment' })
|
|
223
|
+
withdraw(@Param('id') id: string) {
|
|
224
|
+
return this.enrollmentService.withdraw(id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@Controller('grades')
|
|
229
|
+
export class GradeController {
|
|
230
|
+
@Put(':enrollmentId')
|
|
231
|
+
@Audit({ action: 'UPDATE', entity: 'Grade' })
|
|
232
|
+
setGrade(@Param('enrollmentId') id: string, @Body() dto: GradeDto) {
|
|
233
|
+
return this.gradeService.setGrade(id, dto);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@Controller('courses')
|
|
238
|
+
export class CourseController {
|
|
239
|
+
@Post()
|
|
240
|
+
@Audit({ action: 'CREATE', entity: 'Course' })
|
|
241
|
+
create(@Body() dto: CreateCourseDto) {
|
|
242
|
+
return this.courseService.create(dto);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
In every case you use the same `@Audit({ action, entity })` and one `IAuditStorage` implementation; only the semantics (and your storage backend) change per domain.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Manual recording
|
|
252
|
+
|
|
253
|
+
You can skip the interceptor and record from services:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { AuditService } from 'nestjs-audit-trail';
|
|
257
|
+
|
|
258
|
+
@Injectable()
|
|
259
|
+
export class MyService {
|
|
260
|
+
constructor(private readonly audit: AuditService) {}
|
|
261
|
+
|
|
262
|
+
async doSomething(actorId: string, correlationId: string) {
|
|
263
|
+
const before = { count: 0 };
|
|
264
|
+
// ... work ...
|
|
265
|
+
const after = { count: 1 };
|
|
266
|
+
await this.audit.record({
|
|
267
|
+
action: 'UPDATE',
|
|
268
|
+
entity: 'Counter',
|
|
269
|
+
actorId,
|
|
270
|
+
correlationId,
|
|
271
|
+
payloadBefore: before,
|
|
272
|
+
payloadAfter: after,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Configuration
|
|
281
|
+
|
|
282
|
+
| Option | Required | Default | Description |
|
|
283
|
+
| --------------------- | -------- | -------------------- | ------------------------------------ |
|
|
284
|
+
| `storage` | Yes | — | Your `IAuditStorage` implementation. |
|
|
285
|
+
| `correlationIdHeader` | No | `'x-correlation-id'` | Request header used for correlation. |
|
|
286
|
+
|
|
287
|
+
Set `x-actor-id` on the request (or ensure `request.user.id` is set) so each record has an actor. The core uses deterministic SHA256 hashing and optional chaining via `getLastHash()`; no database or ORM is required.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nestjs-audit-trail",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight audit trail module for NestJS",
|
|
5
|
+
"author": "Luiz Gonçalves <dev.luizh@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
23
|
+
"build": "tsc -p tsconfig.lib.json --outDir ./dist",
|
|
24
|
+
"clean:meta": "node -e \"const fs=require('fs'); ['dist/tsconfig.lib.tsbuildinfo'].forEach((file)=>{ if(fs.existsSync(file)){ fs.rmSync(file, { force: true }); } });\"",
|
|
25
|
+
"verify:dist": "node -e \"const fs=require('fs'); ['dist/index.js','dist/index.d.ts'].forEach((file)=>{ if(!fs.existsSync(file)){ console.error('Missing build artifact: '+file); process.exit(1); } });\"",
|
|
26
|
+
"prepack": "npm run clean && npm run build && npm run clean:meta && npm run verify:dist",
|
|
27
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
31
|
+
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
32
|
+
"reflect-metadata": "^0.2.0",
|
|
33
|
+
"rxjs": "^7.0.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"reflect-metadata": {
|
|
37
|
+
"optional": false
|
|
38
|
+
},
|
|
39
|
+
"rxjs": {
|
|
40
|
+
"optional": false
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"nestjs",
|
|
48
|
+
"audit",
|
|
49
|
+
"audit-trail",
|
|
50
|
+
"compliance",
|
|
51
|
+
"storage-agnostic"
|
|
52
|
+
],
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/operfildoluiz/nestjs-audit-trail.git"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for AuditTrailModule.forRoot.
|
|
3
|
+
* Used by AuditInterceptor for correlationId header and future config.
|
|
4
|
+
*/
|
|
5
|
+
export interface AuditTrailOptions {
|
|
6
|
+
/** Request header name for correlation ID. Default: 'x-correlation-id'. */
|
|
7
|
+
correlationIdHeader?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const AUDIT_TRAIL_OPTIONS = 'AUDIT_TRAIL_OPTIONS';
|