@hazeljs/audit 0.2.0-beta.53 → 0.2.0-beta.55
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/README.md +1 -1
- package/dist/audit.module.test.d.ts +2 -0
- package/dist/audit.module.test.d.ts.map +1 -0
- package/dist/audit.module.test.js +31 -0
- package/dist/audit.service.js +1 -1
- package/dist/audit.service.test.d.ts +2 -0
- package/dist/audit.service.test.d.ts.map +1 -0
- package/dist/audit.service.test.js +168 -0
- package/dist/decorators/audit.decorator.test.d.ts +2 -0
- package/dist/decorators/audit.decorator.test.d.ts.map +1 -0
- package/dist/decorators/audit.decorator.test.js +41 -0
- package/dist/interceptors/audit.interceptor.test.d.ts +2 -0
- package/dist/interceptors/audit.interceptor.test.d.ts.map +1 -0
- package/dist/interceptors/audit.interceptor.test.js +55 -0
- package/dist/transports/console.transport.test.d.ts +2 -0
- package/dist/transports/console.transport.test.d.ts.map +1 -0
- package/dist/transports/console.transport.test.js +48 -0
- package/dist/transports/file.transport.test.d.ts +2 -0
- package/dist/transports/file.transport.test.d.ts.map +1 -0
- package/dist/transports/file.transport.test.js +125 -0
- package/dist/transports/kafka.transport.test.d.ts +2 -0
- package/dist/transports/kafka.transport.test.d.ts.map +1 -0
- package/dist/transports/kafka.transport.test.js +68 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -161,4 +161,4 @@ Apache 2.0 © [HazelJS](https://hazeljs.com)
|
|
|
161
161
|
- [Documentation](https://hazeljs.com/docs/packages/auth)
|
|
162
162
|
- [GitHub](https://github.com/hazel-js/hazeljs)
|
|
163
163
|
- [Issues](https://github.com/hazel-js/hazeljs/issues)
|
|
164
|
-
- [Discord](https://discord.
|
|
164
|
+
- [Discord](https://discord.com/channels/1448263814238965833/1448263814859456575)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.module.test.d.ts","sourceRoot":"","sources":["../src/audit.module.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/// <reference types="jest" />
|
|
4
|
+
const audit_module_1 = require("./audit.module");
|
|
5
|
+
const audit_service_1 = require("./audit.service");
|
|
6
|
+
describe('AuditModule', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
audit_service_1.AuditService.configure({});
|
|
9
|
+
});
|
|
10
|
+
it('should return module from forRoot with no options', () => {
|
|
11
|
+
const result = audit_module_1.AuditModule.forRoot();
|
|
12
|
+
expect(result).toBe(audit_module_1.AuditModule);
|
|
13
|
+
});
|
|
14
|
+
it('should configure AuditService when options provided', () => {
|
|
15
|
+
const transport = { log: jest.fn() };
|
|
16
|
+
audit_module_1.AuditModule.forRoot({ transports: [transport] });
|
|
17
|
+
const service = new audit_service_1.AuditService();
|
|
18
|
+
service.log({ action: 'test' });
|
|
19
|
+
expect(transport.log).toHaveBeenCalledWith(expect.objectContaining({ action: 'test' }));
|
|
20
|
+
});
|
|
21
|
+
it('should return options from getOptions', () => {
|
|
22
|
+
audit_module_1.AuditModule.forRoot({ includeRequestContext: true });
|
|
23
|
+
const opts = audit_module_1.AuditModule.getOptions();
|
|
24
|
+
expect(opts).toEqual({ includeRequestContext: true });
|
|
25
|
+
});
|
|
26
|
+
it('should return empty object when forRoot called with no options', () => {
|
|
27
|
+
audit_module_1.AuditModule.forRoot();
|
|
28
|
+
const opts = audit_module_1.AuditModule.getOptions();
|
|
29
|
+
expect(opts).toEqual({});
|
|
30
|
+
});
|
|
31
|
+
});
|
package/dist/audit.service.js
CHANGED
|
@@ -103,6 +103,6 @@ let AuditService = AuditService_1 = class AuditService {
|
|
|
103
103
|
exports.AuditService = AuditService;
|
|
104
104
|
AuditService.staticOptions = {};
|
|
105
105
|
exports.AuditService = AuditService = AuditService_1 = __decorate([
|
|
106
|
-
(0, core_1.
|
|
106
|
+
(0, core_1.Service)(),
|
|
107
107
|
__metadata("design:paramtypes", [Object])
|
|
108
108
|
], AuditService);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.service.test.d.ts","sourceRoot":"","sources":["../src/audit.service.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/// <reference types="jest" />
|
|
4
|
+
const core_1 = require("@hazeljs/core");
|
|
5
|
+
const audit_service_1 = require("./audit.service");
|
|
6
|
+
describe('AuditService', () => {
|
|
7
|
+
let captured;
|
|
8
|
+
let mockTransport;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
captured = [];
|
|
11
|
+
mockTransport = {
|
|
12
|
+
log: (event) => {
|
|
13
|
+
captured.push(event);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
audit_service_1.AuditService.configure({});
|
|
17
|
+
});
|
|
18
|
+
it('should use provided transports', () => {
|
|
19
|
+
const service = new audit_service_1.AuditService({ transports: [mockTransport] });
|
|
20
|
+
service.log({ action: 'user.login' });
|
|
21
|
+
expect(captured).toHaveLength(1);
|
|
22
|
+
expect(captured[0].action).toBe('user.login');
|
|
23
|
+
});
|
|
24
|
+
it('should use custom redactKeys', () => {
|
|
25
|
+
const service = new audit_service_1.AuditService({
|
|
26
|
+
transports: [mockTransport],
|
|
27
|
+
redactKeys: ['customSecret'],
|
|
28
|
+
});
|
|
29
|
+
service.log({ action: 'test', metadata: { customSecret: 'x', other: 'y' } });
|
|
30
|
+
expect(captured[0].metadata).toEqual({ customSecret: '[REDACTED]', other: 'y' });
|
|
31
|
+
});
|
|
32
|
+
it('configure merges options into new instances', () => {
|
|
33
|
+
audit_service_1.AuditService.configure({ transports: [mockTransport] });
|
|
34
|
+
const service = new audit_service_1.AuditService();
|
|
35
|
+
service.log({ action: 'configured' });
|
|
36
|
+
expect(captured[0].action).toBe('configured');
|
|
37
|
+
});
|
|
38
|
+
it('log adds timestamp when not provided', () => {
|
|
39
|
+
const service = new audit_service_1.AuditService({ transports: [mockTransport] });
|
|
40
|
+
service.log({ action: 'test' });
|
|
41
|
+
expect(captured[0].timestamp).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
it('log uses provided timestamp', () => {
|
|
44
|
+
const service = new audit_service_1.AuditService({ transports: [mockTransport] });
|
|
45
|
+
service.log({ action: 'test', timestamp: '2025-01-01T00:00:00.000Z' });
|
|
46
|
+
expect(captured[0].timestamp).toBe('2025-01-01T00:00:00.000Z');
|
|
47
|
+
});
|
|
48
|
+
it('log calls all transports', () => {
|
|
49
|
+
const second = [];
|
|
50
|
+
const service = new audit_service_1.AuditService({
|
|
51
|
+
transports: [
|
|
52
|
+
mockTransport,
|
|
53
|
+
{
|
|
54
|
+
log: (e) => {
|
|
55
|
+
second.push(e);
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
});
|
|
60
|
+
service.log({ action: 'multi' });
|
|
61
|
+
expect(captured).toHaveLength(1);
|
|
62
|
+
expect(second[0].action).toBe('multi');
|
|
63
|
+
});
|
|
64
|
+
it('log redacts default sensitive keys', () => {
|
|
65
|
+
const service = new audit_service_1.AuditService({ transports: [mockTransport] });
|
|
66
|
+
service.log({ action: 'test', metadata: { password: 'secret', safe: 'ok' } });
|
|
67
|
+
expect(captured[0].metadata).toEqual({ password: '[REDACTED]', safe: 'ok' });
|
|
68
|
+
});
|
|
69
|
+
it('log catches transport errors', () => {
|
|
70
|
+
const spy = jest.spyOn(core_1.logger, 'error').mockImplementation();
|
|
71
|
+
const service = new audit_service_1.AuditService({
|
|
72
|
+
transports: [
|
|
73
|
+
{
|
|
74
|
+
log: () => {
|
|
75
|
+
throw new Error('fail');
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
service.log({ action: 'test' });
|
|
81
|
+
expect(spy).toHaveBeenCalled();
|
|
82
|
+
spy.mockRestore();
|
|
83
|
+
});
|
|
84
|
+
it('log handles async transport rejection', async () => {
|
|
85
|
+
const spy = jest.spyOn(core_1.logger, 'error').mockImplementation();
|
|
86
|
+
const service = new audit_service_1.AuditService({
|
|
87
|
+
transports: [{ log: () => Promise.reject(new Error('async fail')) }],
|
|
88
|
+
});
|
|
89
|
+
service.log({ action: 'test' });
|
|
90
|
+
await new Promise((r) => setImmediate(r));
|
|
91
|
+
expect(spy).toHaveBeenCalled();
|
|
92
|
+
spy.mockRestore();
|
|
93
|
+
});
|
|
94
|
+
it('addTransport adds a transport at runtime', () => {
|
|
95
|
+
const extra = [];
|
|
96
|
+
const service = new audit_service_1.AuditService({ transports: [mockTransport] });
|
|
97
|
+
service.addTransport({
|
|
98
|
+
log: (e) => {
|
|
99
|
+
extra.push(e);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
service.log({ action: 'runtime' });
|
|
103
|
+
expect(captured).toHaveLength(1);
|
|
104
|
+
expect(extra).toHaveLength(1);
|
|
105
|
+
expect(extra[0].action).toBe('runtime');
|
|
106
|
+
});
|
|
107
|
+
it('actorFromContext returns undefined when no user', () => {
|
|
108
|
+
const service = new audit_service_1.AuditService({ transports: [mockTransport] });
|
|
109
|
+
const ctx = { method: 'GET', url: '/', headers: {}, params: {}, query: {}, body: undefined };
|
|
110
|
+
expect(service.actorFromContext(ctx)).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
it('actorFromContext returns actor from context.user', () => {
|
|
113
|
+
const service = new audit_service_1.AuditService({ transports: [mockTransport] });
|
|
114
|
+
const ctx = {
|
|
115
|
+
method: 'GET',
|
|
116
|
+
url: '/',
|
|
117
|
+
headers: {},
|
|
118
|
+
params: {},
|
|
119
|
+
query: {},
|
|
120
|
+
body: undefined,
|
|
121
|
+
user: { id: 1, username: 'alice', role: 'admin' },
|
|
122
|
+
};
|
|
123
|
+
expect(service.actorFromContext(ctx)).toEqual({ id: 1, username: 'alice', role: 'admin' });
|
|
124
|
+
});
|
|
125
|
+
it('eventFromContext builds event with action', () => {
|
|
126
|
+
const service = new audit_service_1.AuditService({ transports: [mockTransport] });
|
|
127
|
+
const ctx = {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
url: '/orders',
|
|
130
|
+
headers: {},
|
|
131
|
+
params: {},
|
|
132
|
+
query: {},
|
|
133
|
+
body: undefined,
|
|
134
|
+
};
|
|
135
|
+
const event = service.eventFromContext(ctx, 'success', 'order.create');
|
|
136
|
+
expect(event.action).toBe('order.create');
|
|
137
|
+
expect(event.result).toBe('success');
|
|
138
|
+
expect(event.method).toBe('POST');
|
|
139
|
+
expect(event.path).toBe('/orders');
|
|
140
|
+
});
|
|
141
|
+
it('eventFromContext uses default action', () => {
|
|
142
|
+
const service = new audit_service_1.AuditService({ transports: [mockTransport] });
|
|
143
|
+
const ctx = {
|
|
144
|
+
method: 'GET',
|
|
145
|
+
url: '/users',
|
|
146
|
+
headers: {},
|
|
147
|
+
params: {},
|
|
148
|
+
query: {},
|
|
149
|
+
body: undefined,
|
|
150
|
+
};
|
|
151
|
+
const event = service.eventFromContext(ctx);
|
|
152
|
+
expect(event.action).toBe('http.get');
|
|
153
|
+
});
|
|
154
|
+
it('eventFromContext includes actor when user present', () => {
|
|
155
|
+
const service = new audit_service_1.AuditService({ transports: [mockTransport] });
|
|
156
|
+
const ctx = {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
url: '/',
|
|
159
|
+
headers: {},
|
|
160
|
+
params: {},
|
|
161
|
+
query: {},
|
|
162
|
+
body: undefined,
|
|
163
|
+
user: { id: 'u1', username: 'bob', role: 'user' },
|
|
164
|
+
};
|
|
165
|
+
const event = service.eventFromContext(ctx, 'success');
|
|
166
|
+
expect(event.actor).toEqual({ id: 'u1', username: 'bob', role: 'user' });
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.decorator.test.d.ts","sourceRoot":"","sources":["../../src/decorators/audit.decorator.test.ts"],"names":[],"mappings":"AACA,OAAO,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/// <reference types="jest" />
|
|
4
|
+
require("reflect-metadata");
|
|
5
|
+
const audit_decorator_1 = require("./audit.decorator");
|
|
6
|
+
describe('Audit decorator', () => {
|
|
7
|
+
class TestClass {
|
|
8
|
+
create() { }
|
|
9
|
+
login() { }
|
|
10
|
+
noAudit() { }
|
|
11
|
+
}
|
|
12
|
+
// Apply decorators manually to avoid TS decorator signature mismatch in test context
|
|
13
|
+
const noopDescriptor = {
|
|
14
|
+
value: () => { },
|
|
15
|
+
writable: true,
|
|
16
|
+
enumerable: false,
|
|
17
|
+
configurable: true,
|
|
18
|
+
};
|
|
19
|
+
(0, audit_decorator_1.Audit)('order.create')(TestClass.prototype, 'create', noopDescriptor);
|
|
20
|
+
(0, audit_decorator_1.Audit)({ action: 'user.login', resource: 'User' })(TestClass.prototype, 'login', noopDescriptor);
|
|
21
|
+
const instance = new TestClass();
|
|
22
|
+
it('should set metadata when using string shorthand', () => {
|
|
23
|
+
expect((0, audit_decorator_1.hasAuditMetadata)(instance, 'create')).toBe(true);
|
|
24
|
+
expect((0, audit_decorator_1.getAuditMetadata)(instance, 'create')).toEqual({ action: 'order.create' });
|
|
25
|
+
});
|
|
26
|
+
it('should set metadata when using options object', () => {
|
|
27
|
+
expect((0, audit_decorator_1.hasAuditMetadata)(instance, 'login')).toBe(true);
|
|
28
|
+
expect((0, audit_decorator_1.getAuditMetadata)(instance, 'login')).toEqual({
|
|
29
|
+
action: 'user.login',
|
|
30
|
+
resource: 'User',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it('should return false for method without decorator', () => {
|
|
34
|
+
expect((0, audit_decorator_1.hasAuditMetadata)(instance, 'noAudit')).toBe(false);
|
|
35
|
+
expect((0, audit_decorator_1.getAuditMetadata)(instance, 'noAudit')).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
it('should return undefined for unknown property', () => {
|
|
38
|
+
expect((0, audit_decorator_1.getAuditMetadata)(instance, 'unknown')).toBeUndefined();
|
|
39
|
+
expect((0, audit_decorator_1.hasAuditMetadata)(instance, 'unknown')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.interceptor.test.d.ts","sourceRoot":"","sources":["../../src/interceptors/audit.interceptor.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/// <reference types="jest" />
|
|
4
|
+
const audit_interceptor_1 = require("./audit.interceptor");
|
|
5
|
+
const audit_service_1 = require("../audit.service");
|
|
6
|
+
describe('AuditInterceptor', () => {
|
|
7
|
+
let auditService;
|
|
8
|
+
let captured;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
captured = [];
|
|
11
|
+
auditService = new audit_service_1.AuditService({
|
|
12
|
+
transports: [
|
|
13
|
+
{
|
|
14
|
+
log: (e) => {
|
|
15
|
+
captured.push(e);
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
const emptyContext = {
|
|
22
|
+
method: 'GET',
|
|
23
|
+
url: '/test',
|
|
24
|
+
headers: {},
|
|
25
|
+
params: {},
|
|
26
|
+
query: {},
|
|
27
|
+
body: undefined,
|
|
28
|
+
};
|
|
29
|
+
it('should call next and log success event', async () => {
|
|
30
|
+
const interceptor = new audit_interceptor_1.AuditInterceptor(auditService);
|
|
31
|
+
const next = jest.fn().mockResolvedValue('ok');
|
|
32
|
+
const result = await interceptor.intercept(emptyContext, next);
|
|
33
|
+
expect(result).toBe('ok');
|
|
34
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(captured).toHaveLength(1);
|
|
36
|
+
expect(captured[0].result).toBe('success');
|
|
37
|
+
expect(captured[0].action).toBe('http.get');
|
|
38
|
+
});
|
|
39
|
+
it('should log failure and rethrow when next throws', async () => {
|
|
40
|
+
const interceptor = new audit_interceptor_1.AuditInterceptor(auditService);
|
|
41
|
+
const err = new Error('handler failed');
|
|
42
|
+
const next = jest.fn().mockRejectedValue(err);
|
|
43
|
+
await expect(interceptor.intercept(emptyContext, next)).rejects.toThrow('handler failed');
|
|
44
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(captured).toHaveLength(1);
|
|
46
|
+
expect(captured[0].result).toBe('failure');
|
|
47
|
+
});
|
|
48
|
+
it('should use http.request when method is missing', async () => {
|
|
49
|
+
const interceptor = new audit_interceptor_1.AuditInterceptor(auditService);
|
|
50
|
+
const ctx = { ...emptyContext, method: '' };
|
|
51
|
+
const next = jest.fn().mockResolvedValue(undefined);
|
|
52
|
+
await interceptor.intercept(ctx, next);
|
|
53
|
+
expect(captured[0].action).toBe('http.request');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console.transport.test.d.ts","sourceRoot":"","sources":["../../src/transports/console.transport.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/// <reference types="jest" />
|
|
4
|
+
const console_transport_1 = require("./console.transport");
|
|
5
|
+
describe('ConsoleAuditTransport', () => {
|
|
6
|
+
let writeSpy;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
writeSpy.mockRestore();
|
|
12
|
+
});
|
|
13
|
+
it('should write JSON line with _type audit', () => {
|
|
14
|
+
const transport = new console_transport_1.ConsoleAuditTransport();
|
|
15
|
+
const event = {
|
|
16
|
+
action: 'user.login',
|
|
17
|
+
timestamp: '2025-01-01T00:00:00.000Z',
|
|
18
|
+
result: 'success',
|
|
19
|
+
};
|
|
20
|
+
transport.log(event);
|
|
21
|
+
expect(writeSpy).toHaveBeenCalledTimes(1);
|
|
22
|
+
const line = writeSpy.mock.calls[0][0];
|
|
23
|
+
expect(line.endsWith('\n')).toBe(true);
|
|
24
|
+
const parsed = JSON.parse(line.trim());
|
|
25
|
+
expect(parsed._type).toBe('audit');
|
|
26
|
+
expect(parsed.action).toBe('user.login');
|
|
27
|
+
expect(parsed.timestamp).toBe(event.timestamp);
|
|
28
|
+
expect(parsed.result).toBe('success');
|
|
29
|
+
});
|
|
30
|
+
it('should include all event fields in output', () => {
|
|
31
|
+
const transport = new console_transport_1.ConsoleAuditTransport();
|
|
32
|
+
const event = {
|
|
33
|
+
action: 'order.create',
|
|
34
|
+
actor: { id: 1, username: 'alice' },
|
|
35
|
+
resource: 'Order',
|
|
36
|
+
resourceId: 'ord-123',
|
|
37
|
+
result: 'success',
|
|
38
|
+
timestamp: '2025-01-01T00:00:00.000Z',
|
|
39
|
+
metadata: { amount: 99 },
|
|
40
|
+
};
|
|
41
|
+
transport.log(event);
|
|
42
|
+
const parsed = JSON.parse(writeSpy.mock.calls[0][0].trim());
|
|
43
|
+
expect(parsed.actor).toEqual({ id: 1, username: 'alice' });
|
|
44
|
+
expect(parsed.resource).toBe('Order');
|
|
45
|
+
expect(parsed.resourceId).toBe('ord-123');
|
|
46
|
+
expect(parsed.metadata).toEqual({ amount: 99 });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file.transport.test.d.ts","sourceRoot":"","sources":["../../src/transports/file.transport.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/// <reference types="jest" />
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const file_transport_1 = require("./file.transport");
|
|
41
|
+
describe('FileAuditTransport', () => {
|
|
42
|
+
let tmpDir;
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-file-'));
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
try {
|
|
48
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ignore
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const baseEvent = {
|
|
55
|
+
action: 'test.action',
|
|
56
|
+
timestamp: '2025-01-01T00:00:00.000Z',
|
|
57
|
+
result: 'success',
|
|
58
|
+
};
|
|
59
|
+
it('should create parent directory when ensureDir is true', () => {
|
|
60
|
+
const subDir = path.join(tmpDir, 'nested', 'logs');
|
|
61
|
+
const filePath = path.join(subDir, 'audit.jsonl');
|
|
62
|
+
expect(fs.existsSync(subDir)).toBe(false);
|
|
63
|
+
new file_transport_1.FileAuditTransport({ filePath, ensureDir: true });
|
|
64
|
+
expect(fs.existsSync(subDir)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
it('should write when ensureDir is false and directory already exists', () => {
|
|
67
|
+
const filePath = path.join(tmpDir, 'audit.jsonl');
|
|
68
|
+
const transport = new file_transport_1.FileAuditTransport({ filePath, ensureDir: false });
|
|
69
|
+
transport.log(baseEvent);
|
|
70
|
+
expect(fs.readFileSync(filePath, 'utf8')).toContain('test.action');
|
|
71
|
+
});
|
|
72
|
+
it('should append one JSON line per log with _type audit', () => {
|
|
73
|
+
const filePath = path.join(tmpDir, 'audit.jsonl');
|
|
74
|
+
const transport = new file_transport_1.FileAuditTransport({ filePath });
|
|
75
|
+
transport.log(baseEvent);
|
|
76
|
+
transport.log({ ...baseEvent, action: 'second' });
|
|
77
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
78
|
+
const lines = content.trim().split('\n');
|
|
79
|
+
expect(lines).toHaveLength(2);
|
|
80
|
+
expect(JSON.parse(lines[0])._type).toBe('audit');
|
|
81
|
+
expect(JSON.parse(lines[0]).action).toBe('test.action');
|
|
82
|
+
expect(JSON.parse(lines[1]).action).toBe('second');
|
|
83
|
+
});
|
|
84
|
+
it('should use absolute path as-is', () => {
|
|
85
|
+
const filePath = path.join(tmpDir, 'abs.jsonl');
|
|
86
|
+
const transport = new file_transport_1.FileAuditTransport({ filePath });
|
|
87
|
+
transport.log(baseEvent);
|
|
88
|
+
expect(fs.readFileSync(filePath, 'utf8')).toContain('test.action');
|
|
89
|
+
});
|
|
90
|
+
it('should roll to new file when size exceeds maxSizeBytes', () => {
|
|
91
|
+
const filePath = path.join(tmpDir, 'audit.jsonl');
|
|
92
|
+
const transport = new file_transport_1.FileAuditTransport({
|
|
93
|
+
filePath,
|
|
94
|
+
maxSizeBytes: 30,
|
|
95
|
+
});
|
|
96
|
+
// First log: write a line long enough that file size >= 30
|
|
97
|
+
const bigEvent = {
|
|
98
|
+
...baseEvent,
|
|
99
|
+
metadata: { padding: 'x'.repeat(100) },
|
|
100
|
+
};
|
|
101
|
+
transport.log(bigEvent);
|
|
102
|
+
const filesAfterFirst = fs.readdirSync(tmpDir);
|
|
103
|
+
expect(filesAfterFirst).toHaveLength(1);
|
|
104
|
+
// Second log should trigger roll (current file is over 30 bytes)
|
|
105
|
+
transport.log({ ...baseEvent, action: 'after-roll' });
|
|
106
|
+
const files = fs.readdirSync(tmpDir);
|
|
107
|
+
expect(files.length).toBeGreaterThanOrEqual(2);
|
|
108
|
+
const rolledFile = files.find((f) => {
|
|
109
|
+
const content = fs.readFileSync(path.join(tmpDir, f), 'utf8');
|
|
110
|
+
return content.includes('after-roll');
|
|
111
|
+
});
|
|
112
|
+
expect(rolledFile).toBeDefined();
|
|
113
|
+
expect(rolledFile).not.toBe('audit.jsonl');
|
|
114
|
+
});
|
|
115
|
+
it('should use date suffix when rollDaily is true', () => {
|
|
116
|
+
const filePath = path.join(tmpDir, 'audit.jsonl');
|
|
117
|
+
const transport = new file_transport_1.FileAuditTransport({ filePath, rollDaily: true });
|
|
118
|
+
transport.log(baseEvent);
|
|
119
|
+
const files = fs.readdirSync(tmpDir);
|
|
120
|
+
expect(files.length).toBe(1);
|
|
121
|
+
const expectedSuffix = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
122
|
+
expect(files[0]).toContain(expectedSuffix);
|
|
123
|
+
expect(fs.readFileSync(path.join(tmpDir, files[0]), 'utf8')).toContain('test.action');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kafka.transport.test.d.ts","sourceRoot":"","sources":["../../src/transports/kafka.transport.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/// <reference types="jest" />
|
|
4
|
+
const kafka_transport_1 = require("./kafka.transport");
|
|
5
|
+
describe('KafkaAuditTransport', () => {
|
|
6
|
+
const baseEvent = {
|
|
7
|
+
action: 'order.create',
|
|
8
|
+
timestamp: '2025-01-01T00:00:00.000Z',
|
|
9
|
+
result: 'success',
|
|
10
|
+
resource: 'Order',
|
|
11
|
+
resourceId: 'ord-1',
|
|
12
|
+
};
|
|
13
|
+
it('should send JSON-serialized event by default', async () => {
|
|
14
|
+
const send = jest.fn().mockResolvedValue(undefined);
|
|
15
|
+
const sender = { send };
|
|
16
|
+
const transport = new kafka_transport_1.KafkaAuditTransport({ sender, topic: 'audit' });
|
|
17
|
+
await transport.log(baseEvent);
|
|
18
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
19
|
+
expect(send).toHaveBeenCalledWith('audit', { value: JSON.stringify(baseEvent) });
|
|
20
|
+
});
|
|
21
|
+
it('should include key when key option is provided', async () => {
|
|
22
|
+
const send = jest.fn().mockResolvedValue(undefined);
|
|
23
|
+
const sender = { send };
|
|
24
|
+
const transport = new kafka_transport_1.KafkaAuditTransport({
|
|
25
|
+
sender,
|
|
26
|
+
topic: 'audit',
|
|
27
|
+
key: (e) => e.resourceId ?? '',
|
|
28
|
+
});
|
|
29
|
+
await transport.log(baseEvent);
|
|
30
|
+
expect(send).toHaveBeenCalledWith('audit', { key: 'ord-1', value: JSON.stringify(baseEvent) });
|
|
31
|
+
});
|
|
32
|
+
it('should use custom serialize when provided', async () => {
|
|
33
|
+
const send = jest.fn().mockResolvedValue(undefined);
|
|
34
|
+
const sender = { send };
|
|
35
|
+
const transport = new kafka_transport_1.KafkaAuditTransport({
|
|
36
|
+
sender,
|
|
37
|
+
topic: 'events',
|
|
38
|
+
serialize: (e) => `custom:${e.action}`,
|
|
39
|
+
});
|
|
40
|
+
await transport.log(baseEvent);
|
|
41
|
+
expect(send).toHaveBeenCalledWith('events', { value: 'custom:order.create' });
|
|
42
|
+
});
|
|
43
|
+
it('should support async serialize', async () => {
|
|
44
|
+
const send = jest.fn().mockResolvedValue(undefined);
|
|
45
|
+
const sender = { send };
|
|
46
|
+
const transport = new kafka_transport_1.KafkaAuditTransport({
|
|
47
|
+
sender,
|
|
48
|
+
topic: 'audit',
|
|
49
|
+
serialize: (e) => Promise.resolve(Buffer.from(JSON.stringify(e))),
|
|
50
|
+
});
|
|
51
|
+
await transport.log(baseEvent);
|
|
52
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
53
|
+
const value = send.mock.calls[0][1].value;
|
|
54
|
+
expect(Buffer.isBuffer(value)).toBe(true);
|
|
55
|
+
expect(JSON.parse(value.toString())).toEqual(baseEvent);
|
|
56
|
+
});
|
|
57
|
+
it('should not include key when key function returns undefined', async () => {
|
|
58
|
+
const send = jest.fn().mockResolvedValue(undefined);
|
|
59
|
+
const sender = { send };
|
|
60
|
+
const transport = new kafka_transport_1.KafkaAuditTransport({
|
|
61
|
+
sender,
|
|
62
|
+
topic: 'audit',
|
|
63
|
+
key: () => undefined,
|
|
64
|
+
});
|
|
65
|
+
await transport.log(baseEvent);
|
|
66
|
+
expect(send).toHaveBeenCalledWith('audit', { value: JSON.stringify(baseEvent) });
|
|
67
|
+
});
|
|
68
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazeljs/audit",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.55",
|
|
4
4
|
"description": "Audit logging and event trail for HazelJS applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@hazeljs/core": ">=0.2.0-beta.0"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "f2e54f346eea552595a44607999454a9e388cb9e"
|
|
53
53
|
}
|