@contractspec/lib.bus 1.56.1 → 1.58.0
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/dist/auditableBus.d.ts +76 -81
- package/dist/auditableBus.d.ts.map +1 -1
- package/dist/auditableBus.js +149 -132
- package/dist/browser/auditableBus.js +153 -0
- package/dist/browser/eventBus.js +28 -0
- package/dist/browser/filtering.js +147 -0
- package/dist/browser/inMemoryBus.js +25 -0
- package/dist/browser/index.js +373 -0
- package/dist/browser/metadata.js +60 -0
- package/dist/browser/subscriber.js +37 -0
- package/dist/eventBus.d.ts +9 -14
- package/dist/eventBus.d.ts.map +1 -1
- package/dist/eventBus.js +23 -26
- package/dist/filtering.d.ts +53 -58
- package/dist/filtering.d.ts.map +1 -1
- package/dist/filtering.js +139 -125
- package/dist/inMemoryBus.d.ts +6 -11
- package/dist/inMemoryBus.d.ts.map +1 -1
- package/dist/inMemoryBus.js +25 -28
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +374 -8
- package/dist/metadata.d.ts +60 -63
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +55 -76
- package/dist/node/auditableBus.js +153 -0
- package/dist/node/eventBus.js +28 -0
- package/dist/node/filtering.js +147 -0
- package/dist/node/inMemoryBus.js +25 -0
- package/dist/node/index.js +373 -0
- package/dist/node/metadata.js +60 -0
- package/dist/node/subscriber.js +37 -0
- package/dist/subscriber.d.ts +6 -10
- package/dist/subscriber.d.ts.map +1 -1
- package/dist/subscriber.js +35 -16
- package/package.json +69 -28
- package/dist/auditableBus.js.map +0 -1
- package/dist/eventBus.js.map +0 -1
- package/dist/filtering.js.map +0 -1
- package/dist/inMemoryBus.js.map +0 -1
- package/dist/metadata.js.map +0 -1
- package/dist/subscriber.js.map +0 -1
package/dist/auditableBus.d.ts
CHANGED
|
@@ -1,117 +1,112 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
//#region src/auditableBus.d.ts
|
|
7
|
-
|
|
1
|
+
import { type EventEnvelope, type EventKey, type EventSpec } from '@contractspec/lib.contracts';
|
|
2
|
+
import type { AnySchemaModel } from '@contractspec/lib.schema';
|
|
3
|
+
import type { EventBus } from './eventBus';
|
|
4
|
+
import type { EventMetadata } from './metadata';
|
|
8
5
|
/**
|
|
9
6
|
* Extended event envelope with metadata for auditing.
|
|
10
7
|
*/
|
|
11
|
-
interface AuditableEventEnvelope<T = unknown> extends EventEnvelope<T> {
|
|
12
|
-
|
|
8
|
+
export interface AuditableEventEnvelope<T = unknown> extends EventEnvelope<T> {
|
|
9
|
+
metadata?: EventMetadata;
|
|
13
10
|
}
|
|
14
11
|
/**
|
|
15
12
|
* Audit record for persisting event history.
|
|
16
13
|
*/
|
|
17
|
-
interface AuditRecord {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
export interface AuditRecord {
|
|
15
|
+
id: string;
|
|
16
|
+
eventKey: string;
|
|
17
|
+
eventVersion: string;
|
|
18
|
+
payload: unknown;
|
|
19
|
+
metadata?: EventMetadata;
|
|
20
|
+
occurredAt: string;
|
|
21
|
+
traceId?: string;
|
|
22
|
+
recordedAt: Date;
|
|
26
23
|
}
|
|
27
24
|
/**
|
|
28
25
|
* Audit storage adapter interface.
|
|
29
26
|
*/
|
|
30
|
-
interface AuditStorage {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
export interface AuditStorage {
|
|
28
|
+
/** Store an audit record */
|
|
29
|
+
store(record: AuditRecord): Promise<void>;
|
|
30
|
+
/** Query audit records */
|
|
31
|
+
query?(options: AuditQueryOptions): Promise<AuditRecord[]>;
|
|
35
32
|
}
|
|
36
33
|
/**
|
|
37
34
|
* Options for querying audit records.
|
|
38
35
|
*/
|
|
39
|
-
interface AuditQueryOptions {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
export interface AuditQueryOptions {
|
|
37
|
+
eventKey?: string;
|
|
38
|
+
actorId?: string;
|
|
39
|
+
targetId?: string;
|
|
40
|
+
orgId?: string;
|
|
41
|
+
traceId?: string;
|
|
42
|
+
from?: Date;
|
|
43
|
+
to?: Date;
|
|
44
|
+
limit?: number;
|
|
45
|
+
offset?: number;
|
|
49
46
|
}
|
|
50
47
|
/**
|
|
51
48
|
* Options for creating an auditable event bus.
|
|
52
49
|
*/
|
|
53
|
-
interface AuditableEventBusOptions {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
export interface AuditableEventBusOptions {
|
|
51
|
+
/** Underlying event bus for publishing */
|
|
52
|
+
bus: EventBus;
|
|
53
|
+
/** Audit storage adapter */
|
|
54
|
+
storage?: AuditStorage;
|
|
55
|
+
/** Default metadata to include with all events */
|
|
56
|
+
defaultMetadata?: EventMetadata;
|
|
57
|
+
/** Filter function to decide which events to audit */
|
|
58
|
+
shouldAudit?: (eventKey: string, envelope: AuditableEventEnvelope) => boolean;
|
|
59
|
+
/** Transform function for audit records */
|
|
60
|
+
transformAuditRecord?: (record: AuditRecord) => AuditRecord;
|
|
64
61
|
}
|
|
65
62
|
/**
|
|
66
63
|
* AuditableEventBus wraps an EventBus to automatically record events for audit trail.
|
|
67
64
|
*/
|
|
68
|
-
declare class AuditableEventBus implements EventBus {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
65
|
+
export declare class AuditableEventBus implements EventBus {
|
|
66
|
+
private readonly bus;
|
|
67
|
+
private readonly storage?;
|
|
68
|
+
private readonly defaultMetadata;
|
|
69
|
+
private readonly shouldAudit;
|
|
70
|
+
private readonly transformAuditRecord?;
|
|
71
|
+
constructor(options: AuditableEventBusOptions);
|
|
72
|
+
/**
|
|
73
|
+
* Publish an event and optionally record it for audit.
|
|
74
|
+
*/
|
|
75
|
+
publish(topic: EventKey, bytes: Uint8Array): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Subscribe to events from the underlying bus.
|
|
78
|
+
*/
|
|
79
|
+
subscribe(topic: EventKey | string, handler: (bytes: Uint8Array) => Promise<void>): Promise<() => Promise<void>>;
|
|
80
|
+
/**
|
|
81
|
+
* Query audit records (if storage supports it).
|
|
82
|
+
*/
|
|
83
|
+
queryAudit(options: AuditQueryOptions): Promise<AuditRecord[]>;
|
|
87
84
|
}
|
|
88
85
|
/**
|
|
89
86
|
* Create an auditable publisher that includes metadata.
|
|
90
87
|
*/
|
|
91
|
-
declare function makeAuditablePublisher<T extends AnySchemaModel>(bus: AuditableEventBus | EventBus, spec: EventSpec<T>, defaultMetadata?: EventMetadata): (payload: T, options?: {
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
export declare function makeAuditablePublisher<T extends AnySchemaModel>(bus: AuditableEventBus | EventBus, spec: EventSpec<T>, defaultMetadata?: EventMetadata): (payload: T, options?: {
|
|
89
|
+
traceId?: string;
|
|
90
|
+
metadata?: EventMetadata;
|
|
94
91
|
}) => Promise<void>;
|
|
95
92
|
/**
|
|
96
93
|
* In-memory audit storage for development/testing.
|
|
97
94
|
*/
|
|
98
|
-
declare class InMemoryAuditStorage implements AuditStorage {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
95
|
+
export declare class InMemoryAuditStorage implements AuditStorage {
|
|
96
|
+
private records;
|
|
97
|
+
store(record: AuditRecord): Promise<void>;
|
|
98
|
+
query(options: AuditQueryOptions): Promise<AuditRecord[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Get all records (for testing).
|
|
101
|
+
*/
|
|
102
|
+
getAll(): AuditRecord[];
|
|
103
|
+
/**
|
|
104
|
+
* Clear all records (for testing).
|
|
105
|
+
*/
|
|
106
|
+
clear(): void;
|
|
110
107
|
}
|
|
111
108
|
/**
|
|
112
109
|
* Create an auditable event bus with in-memory storage.
|
|
113
110
|
*/
|
|
114
|
-
declare function createAuditableEventBus(bus: EventBus, options?: Omit<AuditableEventBusOptions, 'bus'>): AuditableEventBus;
|
|
115
|
-
//#endregion
|
|
116
|
-
export { AuditQueryOptions, AuditRecord, AuditStorage, AuditableEventBus, AuditableEventBusOptions, AuditableEventEnvelope, InMemoryAuditStorage, createAuditableEventBus, makeAuditablePublisher };
|
|
111
|
+
export declare function createAuditableEventBus(bus: EventBus, options?: Omit<AuditableEventBusOptions, 'bus'>): AuditableEventBus;
|
|
117
112
|
//# sourceMappingURL=auditableBus.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auditableBus.d.ts","
|
|
1
|
+
{"version":3,"file":"auditableBus.d.ts","sourceRoot":"","sources":["../src/auditableBus.ts"],"names":[],"mappings":"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"}
|
package/dist/auditableBus.js
CHANGED
|
@@ -1,137 +1,154 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// @bun
|
|
2
|
+
// src/eventBus.ts
|
|
3
|
+
import {
|
|
4
|
+
eventKey
|
|
5
|
+
} from "@contractspec/lib.contracts";
|
|
6
|
+
function encodeEvent(envelope) {
|
|
7
|
+
return new TextEncoder().encode(JSON.stringify(envelope));
|
|
8
|
+
}
|
|
9
|
+
function decodeEvent(data) {
|
|
10
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
11
|
+
}
|
|
12
|
+
function makePublisher(bus, spec) {
|
|
13
|
+
return async (payload, traceId) => {
|
|
14
|
+
const envelope = {
|
|
15
|
+
id: crypto.randomUUID(),
|
|
16
|
+
occurredAt: new Date().toISOString(),
|
|
17
|
+
key: spec.meta.key,
|
|
18
|
+
version: spec.meta.version,
|
|
19
|
+
payload,
|
|
20
|
+
traceId
|
|
21
|
+
};
|
|
22
|
+
await bus.publish(eventKey(spec.meta.key, spec.meta.version), encodeEvent(envelope));
|
|
23
|
+
};
|
|
24
|
+
}
|
|
3
25
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return this.storage.query(options);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
/**
|
|
64
|
-
* Create an auditable publisher that includes metadata.
|
|
65
|
-
*/
|
|
26
|
+
// src/auditableBus.ts
|
|
27
|
+
import {
|
|
28
|
+
eventKey as eventKey2
|
|
29
|
+
} from "@contractspec/lib.contracts";
|
|
30
|
+
class AuditableEventBus {
|
|
31
|
+
bus;
|
|
32
|
+
storage;
|
|
33
|
+
defaultMetadata;
|
|
34
|
+
shouldAudit;
|
|
35
|
+
transformAuditRecord;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.bus = options.bus;
|
|
38
|
+
this.storage = options.storage;
|
|
39
|
+
this.defaultMetadata = options.defaultMetadata ?? {};
|
|
40
|
+
this.shouldAudit = options.shouldAudit ?? (() => true);
|
|
41
|
+
this.transformAuditRecord = options.transformAuditRecord;
|
|
42
|
+
}
|
|
43
|
+
async publish(topic, bytes) {
|
|
44
|
+
await this.bus.publish(topic, bytes);
|
|
45
|
+
if (this.storage) {
|
|
46
|
+
try {
|
|
47
|
+
const envelope = decodeEvent(bytes);
|
|
48
|
+
if (this.shouldAudit(envelope.key, envelope)) {
|
|
49
|
+
let record = {
|
|
50
|
+
id: crypto.randomUUID(),
|
|
51
|
+
eventKey: envelope.key,
|
|
52
|
+
eventVersion: envelope.version,
|
|
53
|
+
payload: envelope.payload,
|
|
54
|
+
metadata: {
|
|
55
|
+
...this.defaultMetadata,
|
|
56
|
+
...envelope.metadata
|
|
57
|
+
},
|
|
58
|
+
occurredAt: envelope.occurredAt,
|
|
59
|
+
traceId: envelope.traceId,
|
|
60
|
+
recordedAt: new Date
|
|
61
|
+
};
|
|
62
|
+
if (this.transformAuditRecord) {
|
|
63
|
+
record = this.transformAuditRecord(record);
|
|
64
|
+
}
|
|
65
|
+
await this.storage.store(record);
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error("Failed to record audit:", error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async subscribe(topic, handler) {
|
|
73
|
+
return this.bus.subscribe(topic, handler);
|
|
74
|
+
}
|
|
75
|
+
async queryAudit(options) {
|
|
76
|
+
if (!this.storage?.query) {
|
|
77
|
+
throw new Error("Audit storage does not support querying");
|
|
78
|
+
}
|
|
79
|
+
return this.storage.query(options);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
66
82
|
function makeAuditablePublisher(bus, spec, defaultMetadata) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
return async (payload, options) => {
|
|
84
|
+
const envelope = {
|
|
85
|
+
id: crypto.randomUUID(),
|
|
86
|
+
occurredAt: new Date().toISOString(),
|
|
87
|
+
key: spec.meta.key,
|
|
88
|
+
version: spec.meta.version,
|
|
89
|
+
payload,
|
|
90
|
+
traceId: options?.traceId,
|
|
91
|
+
metadata: {
|
|
92
|
+
...defaultMetadata,
|
|
93
|
+
...options?.metadata
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
await bus.publish(eventKey2(spec.meta.key, spec.meta.version), encodeEvent(envelope));
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class InMemoryAuditStorage {
|
|
101
|
+
records = [];
|
|
102
|
+
async store(record) {
|
|
103
|
+
this.records.push(record);
|
|
104
|
+
}
|
|
105
|
+
async query(options) {
|
|
106
|
+
let results = [...this.records];
|
|
107
|
+
if (options.eventKey) {
|
|
108
|
+
results = results.filter((r) => r.eventKey === options.eventKey);
|
|
109
|
+
}
|
|
110
|
+
if (options.actorId) {
|
|
111
|
+
results = results.filter((r) => r.metadata?.actorId === options.actorId);
|
|
112
|
+
}
|
|
113
|
+
if (options.targetId) {
|
|
114
|
+
results = results.filter((r) => r.metadata?.targetId === options.targetId);
|
|
115
|
+
}
|
|
116
|
+
if (options.orgId) {
|
|
117
|
+
results = results.filter((r) => r.metadata?.orgId === options.orgId);
|
|
118
|
+
}
|
|
119
|
+
if (options.traceId) {
|
|
120
|
+
results = results.filter((r) => r.traceId === options.traceId);
|
|
121
|
+
}
|
|
122
|
+
if (options.from) {
|
|
123
|
+
const from = options.from;
|
|
124
|
+
results = results.filter((r) => new Date(r.occurredAt) >= from);
|
|
125
|
+
}
|
|
126
|
+
if (options.to) {
|
|
127
|
+
const to = options.to;
|
|
128
|
+
results = results.filter((r) => new Date(r.occurredAt) <= to);
|
|
129
|
+
}
|
|
130
|
+
results.sort((a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime());
|
|
131
|
+
const offset = options.offset ?? 0;
|
|
132
|
+
const limit = options.limit ?? 100;
|
|
133
|
+
return results.slice(offset, offset + limit);
|
|
134
|
+
}
|
|
135
|
+
getAll() {
|
|
136
|
+
return [...this.records];
|
|
137
|
+
}
|
|
138
|
+
clear() {
|
|
139
|
+
this.records = [];
|
|
140
|
+
}
|
|
82
141
|
}
|
|
83
|
-
/**
|
|
84
|
-
* In-memory audit storage for development/testing.
|
|
85
|
-
*/
|
|
86
|
-
var InMemoryAuditStorage = class {
|
|
87
|
-
records = [];
|
|
88
|
-
async store(record) {
|
|
89
|
-
this.records.push(record);
|
|
90
|
-
}
|
|
91
|
-
async query(options) {
|
|
92
|
-
let results = [...this.records];
|
|
93
|
-
if (options.eventKey) results = results.filter((r) => r.eventKey === options.eventKey);
|
|
94
|
-
if (options.actorId) results = results.filter((r) => r.metadata?.actorId === options.actorId);
|
|
95
|
-
if (options.targetId) results = results.filter((r) => r.metadata?.targetId === options.targetId);
|
|
96
|
-
if (options.orgId) results = results.filter((r) => r.metadata?.orgId === options.orgId);
|
|
97
|
-
if (options.traceId) results = results.filter((r) => r.traceId === options.traceId);
|
|
98
|
-
if (options.from) {
|
|
99
|
-
const from = options.from;
|
|
100
|
-
results = results.filter((r) => new Date(r.occurredAt) >= from);
|
|
101
|
-
}
|
|
102
|
-
if (options.to) {
|
|
103
|
-
const to = options.to;
|
|
104
|
-
results = results.filter((r) => new Date(r.occurredAt) <= to);
|
|
105
|
-
}
|
|
106
|
-
results.sort((a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime());
|
|
107
|
-
const offset = options.offset ?? 0;
|
|
108
|
-
const limit = options.limit ?? 100;
|
|
109
|
-
return results.slice(offset, offset + limit);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Get all records (for testing).
|
|
113
|
-
*/
|
|
114
|
-
getAll() {
|
|
115
|
-
return [...this.records];
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Clear all records (for testing).
|
|
119
|
-
*/
|
|
120
|
-
clear() {
|
|
121
|
-
this.records = [];
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
/**
|
|
125
|
-
* Create an auditable event bus with in-memory storage.
|
|
126
|
-
*/
|
|
127
142
|
function createAuditableEventBus(bus, options) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
return new AuditableEventBus({
|
|
144
|
+
bus,
|
|
145
|
+
storage: options?.storage ?? new InMemoryAuditStorage,
|
|
146
|
+
...options
|
|
147
|
+
});
|
|
133
148
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
export {
|
|
150
|
+
makeAuditablePublisher,
|
|
151
|
+
createAuditableEventBus,
|
|
152
|
+
InMemoryAuditStorage,
|
|
153
|
+
AuditableEventBus
|
|
154
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// src/eventBus.ts
|
|
2
|
+
import {
|
|
3
|
+
eventKey
|
|
4
|
+
} from "@contractspec/lib.contracts";
|
|
5
|
+
function encodeEvent(envelope) {
|
|
6
|
+
return new TextEncoder().encode(JSON.stringify(envelope));
|
|
7
|
+
}
|
|
8
|
+
function decodeEvent(data) {
|
|
9
|
+
return JSON.parse(new TextDecoder().decode(data));
|
|
10
|
+
}
|
|
11
|
+
function makePublisher(bus, spec) {
|
|
12
|
+
return async (payload, traceId) => {
|
|
13
|
+
const envelope = {
|
|
14
|
+
id: crypto.randomUUID(),
|
|
15
|
+
occurredAt: new Date().toISOString(),
|
|
16
|
+
key: spec.meta.key,
|
|
17
|
+
version: spec.meta.version,
|
|
18
|
+
payload,
|
|
19
|
+
traceId
|
|
20
|
+
};
|
|
21
|
+
await bus.publish(eventKey(spec.meta.key, spec.meta.version), encodeEvent(envelope));
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/auditableBus.ts
|
|
26
|
+
import {
|
|
27
|
+
eventKey as eventKey2
|
|
28
|
+
} from "@contractspec/lib.contracts";
|
|
29
|
+
class AuditableEventBus {
|
|
30
|
+
bus;
|
|
31
|
+
storage;
|
|
32
|
+
defaultMetadata;
|
|
33
|
+
shouldAudit;
|
|
34
|
+
transformAuditRecord;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.bus = options.bus;
|
|
37
|
+
this.storage = options.storage;
|
|
38
|
+
this.defaultMetadata = options.defaultMetadata ?? {};
|
|
39
|
+
this.shouldAudit = options.shouldAudit ?? (() => true);
|
|
40
|
+
this.transformAuditRecord = options.transformAuditRecord;
|
|
41
|
+
}
|
|
42
|
+
async publish(topic, bytes) {
|
|
43
|
+
await this.bus.publish(topic, bytes);
|
|
44
|
+
if (this.storage) {
|
|
45
|
+
try {
|
|
46
|
+
const envelope = decodeEvent(bytes);
|
|
47
|
+
if (this.shouldAudit(envelope.key, envelope)) {
|
|
48
|
+
let record = {
|
|
49
|
+
id: crypto.randomUUID(),
|
|
50
|
+
eventKey: envelope.key,
|
|
51
|
+
eventVersion: envelope.version,
|
|
52
|
+
payload: envelope.payload,
|
|
53
|
+
metadata: {
|
|
54
|
+
...this.defaultMetadata,
|
|
55
|
+
...envelope.metadata
|
|
56
|
+
},
|
|
57
|
+
occurredAt: envelope.occurredAt,
|
|
58
|
+
traceId: envelope.traceId,
|
|
59
|
+
recordedAt: new Date
|
|
60
|
+
};
|
|
61
|
+
if (this.transformAuditRecord) {
|
|
62
|
+
record = this.transformAuditRecord(record);
|
|
63
|
+
}
|
|
64
|
+
await this.storage.store(record);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Failed to record audit:", error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async subscribe(topic, handler) {
|
|
72
|
+
return this.bus.subscribe(topic, handler);
|
|
73
|
+
}
|
|
74
|
+
async queryAudit(options) {
|
|
75
|
+
if (!this.storage?.query) {
|
|
76
|
+
throw new Error("Audit storage does not support querying");
|
|
77
|
+
}
|
|
78
|
+
return this.storage.query(options);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function makeAuditablePublisher(bus, spec, defaultMetadata) {
|
|
82
|
+
return async (payload, options) => {
|
|
83
|
+
const envelope = {
|
|
84
|
+
id: crypto.randomUUID(),
|
|
85
|
+
occurredAt: new Date().toISOString(),
|
|
86
|
+
key: spec.meta.key,
|
|
87
|
+
version: spec.meta.version,
|
|
88
|
+
payload,
|
|
89
|
+
traceId: options?.traceId,
|
|
90
|
+
metadata: {
|
|
91
|
+
...defaultMetadata,
|
|
92
|
+
...options?.metadata
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
await bus.publish(eventKey2(spec.meta.key, spec.meta.version), encodeEvent(envelope));
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
class InMemoryAuditStorage {
|
|
100
|
+
records = [];
|
|
101
|
+
async store(record) {
|
|
102
|
+
this.records.push(record);
|
|
103
|
+
}
|
|
104
|
+
async query(options) {
|
|
105
|
+
let results = [...this.records];
|
|
106
|
+
if (options.eventKey) {
|
|
107
|
+
results = results.filter((r) => r.eventKey === options.eventKey);
|
|
108
|
+
}
|
|
109
|
+
if (options.actorId) {
|
|
110
|
+
results = results.filter((r) => r.metadata?.actorId === options.actorId);
|
|
111
|
+
}
|
|
112
|
+
if (options.targetId) {
|
|
113
|
+
results = results.filter((r) => r.metadata?.targetId === options.targetId);
|
|
114
|
+
}
|
|
115
|
+
if (options.orgId) {
|
|
116
|
+
results = results.filter((r) => r.metadata?.orgId === options.orgId);
|
|
117
|
+
}
|
|
118
|
+
if (options.traceId) {
|
|
119
|
+
results = results.filter((r) => r.traceId === options.traceId);
|
|
120
|
+
}
|
|
121
|
+
if (options.from) {
|
|
122
|
+
const from = options.from;
|
|
123
|
+
results = results.filter((r) => new Date(r.occurredAt) >= from);
|
|
124
|
+
}
|
|
125
|
+
if (options.to) {
|
|
126
|
+
const to = options.to;
|
|
127
|
+
results = results.filter((r) => new Date(r.occurredAt) <= to);
|
|
128
|
+
}
|
|
129
|
+
results.sort((a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime());
|
|
130
|
+
const offset = options.offset ?? 0;
|
|
131
|
+
const limit = options.limit ?? 100;
|
|
132
|
+
return results.slice(offset, offset + limit);
|
|
133
|
+
}
|
|
134
|
+
getAll() {
|
|
135
|
+
return [...this.records];
|
|
136
|
+
}
|
|
137
|
+
clear() {
|
|
138
|
+
this.records = [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function createAuditableEventBus(bus, options) {
|
|
142
|
+
return new AuditableEventBus({
|
|
143
|
+
bus,
|
|
144
|
+
storage: options?.storage ?? new InMemoryAuditStorage,
|
|
145
|
+
...options
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
export {
|
|
149
|
+
makeAuditablePublisher,
|
|
150
|
+
createAuditableEventBus,
|
|
151
|
+
InMemoryAuditStorage,
|
|
152
|
+
AuditableEventBus
|
|
153
|
+
};
|