@contractspec/lib.bus 0.0.0-canary-20260113162409

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Chaman Ventures, SASU
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @contractspec/lib.bus
2
+
3
+ Website: https://contractspec.io/
4
+
5
+
6
+ A lightweight, type-safe event bus for in-memory and distributed event handling.
7
+
8
+ ## Purpose
9
+
10
+ To decouple components and services by allowing them to communicate via typed events. This library provides the core `EventBus` interface and implementations for local (in-memory) and potentially remote communication.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @contractspec/lib.bus
16
+ # or
17
+ bun add @contractspec/lib.bus
18
+ ```
19
+
20
+ ## Key Concepts
21
+
22
+ - **EventBus Interface**: A standard contract for `publish` and `subscribe`.
23
+ - **Type Safety**: leveraging `@contractspec/lib.contracts` event definitions for payload validation.
24
+ - **Decoupling**: Producers and consumers don't need to know about each other.
25
+
26
+ ## Exports
27
+
28
+ - `EventBus`: The core interface.
29
+ - `InMemoryEventBus`: A simple, synchronous/asynchronous in-memory implementation.
30
+ - `NoOpEventBus`: A placeholder implementation for testing or disabling events.
31
+
32
+ ## Usage
33
+
34
+ ```tsx
35
+ import { InMemoryEventBus } from '@contractspec/lib.bus';
36
+
37
+ // 1. Create the bus
38
+ const bus = new InMemoryEventBus();
39
+
40
+ // 2. Subscribe to an event
41
+ const unsubscribe = bus.subscribe('user.created', async (payload) => {
42
+ console.log('New user:', payload.userId);
43
+ });
44
+
45
+ // 3. Publish an event
46
+ await bus.publish('user.created', { userId: '123', email: 'test@example.com' });
47
+
48
+ // 4. Cleanup
49
+ unsubscribe();
50
+ ```
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
@@ -0,0 +1,117 @@
1
+ import { EventBus } from "./eventBus.js";
2
+ import { EventMetadata } from "./metadata.js";
3
+ import { EventEnvelope, EventKey, EventSpec } from "@contractspec/lib.contracts";
4
+ import { AnySchemaModel } from "@contractspec/lib.schema";
5
+
6
+ //#region src/auditableBus.d.ts
7
+
8
+ /**
9
+ * Extended event envelope with metadata for auditing.
10
+ */
11
+ interface AuditableEventEnvelope<T = unknown> extends EventEnvelope<T> {
12
+ metadata?: EventMetadata;
13
+ }
14
+ /**
15
+ * Audit record for persisting event history.
16
+ */
17
+ interface AuditRecord {
18
+ id: string;
19
+ eventKey: string;
20
+ eventVersion: string;
21
+ payload: unknown;
22
+ metadata?: EventMetadata;
23
+ occurredAt: string;
24
+ traceId?: string;
25
+ recordedAt: Date;
26
+ }
27
+ /**
28
+ * Audit storage adapter interface.
29
+ */
30
+ interface AuditStorage {
31
+ /** Store an audit record */
32
+ store(record: AuditRecord): Promise<void>;
33
+ /** Query audit records */
34
+ query?(options: AuditQueryOptions): Promise<AuditRecord[]>;
35
+ }
36
+ /**
37
+ * Options for querying audit records.
38
+ */
39
+ interface AuditQueryOptions {
40
+ eventKey?: string;
41
+ actorId?: string;
42
+ targetId?: string;
43
+ orgId?: string;
44
+ traceId?: string;
45
+ from?: Date;
46
+ to?: Date;
47
+ limit?: number;
48
+ offset?: number;
49
+ }
50
+ /**
51
+ * Options for creating an auditable event bus.
52
+ */
53
+ interface AuditableEventBusOptions {
54
+ /** Underlying event bus for publishing */
55
+ bus: EventBus;
56
+ /** Audit storage adapter */
57
+ storage?: AuditStorage;
58
+ /** Default metadata to include with all events */
59
+ defaultMetadata?: EventMetadata;
60
+ /** Filter function to decide which events to audit */
61
+ shouldAudit?: (eventKey: string, envelope: AuditableEventEnvelope) => boolean;
62
+ /** Transform function for audit records */
63
+ transformAuditRecord?: (record: AuditRecord) => AuditRecord;
64
+ }
65
+ /**
66
+ * AuditableEventBus wraps an EventBus to automatically record events for audit trail.
67
+ */
68
+ declare class AuditableEventBus implements EventBus {
69
+ private readonly bus;
70
+ private readonly storage?;
71
+ private readonly defaultMetadata;
72
+ private readonly shouldAudit;
73
+ private readonly transformAuditRecord?;
74
+ constructor(options: AuditableEventBusOptions);
75
+ /**
76
+ * Publish an event and optionally record it for audit.
77
+ */
78
+ publish(topic: EventKey, bytes: Uint8Array): Promise<void>;
79
+ /**
80
+ * Subscribe to events from the underlying bus.
81
+ */
82
+ subscribe(topic: EventKey | string, handler: (bytes: Uint8Array) => Promise<void>): Promise<() => Promise<void>>;
83
+ /**
84
+ * Query audit records (if storage supports it).
85
+ */
86
+ queryAudit(options: AuditQueryOptions): Promise<AuditRecord[]>;
87
+ }
88
+ /**
89
+ * Create an auditable publisher that includes metadata.
90
+ */
91
+ declare function makeAuditablePublisher<T extends AnySchemaModel>(bus: AuditableEventBus | EventBus, spec: EventSpec<T>, defaultMetadata?: EventMetadata): (payload: T, options?: {
92
+ traceId?: string;
93
+ metadata?: EventMetadata;
94
+ }) => Promise<void>;
95
+ /**
96
+ * In-memory audit storage for development/testing.
97
+ */
98
+ declare class InMemoryAuditStorage implements AuditStorage {
99
+ private records;
100
+ store(record: AuditRecord): Promise<void>;
101
+ query(options: AuditQueryOptions): Promise<AuditRecord[]>;
102
+ /**
103
+ * Get all records (for testing).
104
+ */
105
+ getAll(): AuditRecord[];
106
+ /**
107
+ * Clear all records (for testing).
108
+ */
109
+ clear(): void;
110
+ }
111
+ /**
112
+ * Create an auditable event bus with in-memory storage.
113
+ */
114
+ declare function createAuditableEventBus(bus: EventBus, options?: Omit<AuditableEventBusOptions, 'bus'>): AuditableEventBus;
115
+ //#endregion
116
+ export { AuditQueryOptions, AuditRecord, AuditStorage, AuditableEventBus, AuditableEventBusOptions, AuditableEventEnvelope, InMemoryAuditStorage, createAuditableEventBus, makeAuditablePublisher };
117
+ //# sourceMappingURL=auditableBus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auditableBus.d.ts","names":[],"sources":["../src/auditableBus.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAcA;AAA2E,UAA1D,sBAA0D,CAAA,IAAA,OAAA,CAAA,SAAd,aAAc,CAAA,CAAA,CAAA,CAAA;EAC9D,QAAA,CAAA,EAAA,aAAA;;;AAMb;AAcA;AAEgB,UAhBC,WAAA,CAgBD;EAAc,EAAA,EAAA,MAAA;EAEZ,QAAA,EAAA,MAAA;EAA4B,YAAA,EAAA,MAAA;EAAR,OAAA,EAAA,OAAA;EAAO,QAAA,CAAA,EAbhC,aAagC;EAM5B,UAAA,EAAA,MAAA;EAeA,OAAA,CAAA,EAAA,MAAA;EAEV,UAAA,EAjCO,IAiCP;;;;;AAQ2C,UAnCjC,YAAA,CAmCiC;EAAW;EAMhD,KAAA,CAAA,MAAA,EAvCG,WAuCe,CAAA,EAvCD,OAuCC,CAAA,IAAA,CAAA;EAUR;EAWA,KAAA,EAAA,OAAA,EA1DL,iBA0DK,CAAA,EA1De,OA0Df,CA1DuB,WA0DvB,EAAA,CAAA;;;;;AA0Ca,UA9FnB,iBAAA,CA8FmB;EACjB,QAAA,CAAA,EAAA,MAAA;EAAd,OAAA,CAAA,EAAA,MAAA;EAOuB,QAAA,CAAA,EAAA,MAAA;EAA4B,KAAA,CAAA,EAAA,MAAA;EAAR,OAAA,CAAA,EAAA,MAAA;EAvEN,IAAA,CAAA,EAzBjC,IAyBiC;EAAQ,EAAA,CAAA,EAxB3C,IAwB2C;EAkFlC,KAAA,CAAA,EAAA,MAAA;EAAiC,MAAA,CAAA,EAAA,MAAA;;;;;AAG7B,UArGH,wBAAA,CAqGG;EAGP;EACgC,GAAA,EAvGtC,QAuGsC;EAAe;EAAA,OAAA,CAAA,EArGhD,YAqGgD;EAwB/C;EAGS,eAAA,CAAA,EA9HF,aA8HE;EAAc;EAIb,WAAA,CAAA,EAAA,CAAA,QAAA,EAAA,MAAA,EAAA,QAAA,EAhIsB,sBAgItB,EAAA,GAAA,OAAA;EAA4B;EAAR,oBAAA,CAAA,EAAA,CAAA,MAAA,EA9HT,WA8HS,EAAA,GA9HO,WA8HP;;;;AAiE3C;AACO,cA1LM,iBAAA,YAA6B,QA0LnC,CAAA;EACU,iBAAA,GAAA;EAAL,iBAAA,OAAA;EACT,iBAAA,eAAA;EAAiB,iBAAA,WAAA;;uBAlLG;;;;iBAWA,iBAAiB,aAAa;;;;mBAyC1C,oCACU,eAAe,gBAC/B,cAAc;;;;sBAOS,oBAAoB,QAAQ;;;;;iBAWxC,iCAAiC,qBAC1C,oBAAoB,gBACnB,UAAU,sBACE,0BAGP;;aACgC;MAAe;;;;cAwB/C,oBAAA,YAAgC;;gBAGvB,cAAc;iBAIb,oBAAoB,QAAQ;;;;YAkDvC;;;;;;;;;iBAeI,uBAAA,MACT,oBACK,KAAK,mCACd"}
@@ -0,0 +1,137 @@
1
+ import { decodeEvent, encodeEvent } from "./eventBus.js";
2
+ import { eventKey } from "@contractspec/lib.contracts";
3
+
4
+ //#region src/auditableBus.ts
5
+ /**
6
+ * AuditableEventBus wraps an EventBus to automatically record events for audit trail.
7
+ */
8
+ var AuditableEventBus = class {
9
+ bus;
10
+ storage;
11
+ defaultMetadata;
12
+ shouldAudit;
13
+ transformAuditRecord;
14
+ constructor(options) {
15
+ this.bus = options.bus;
16
+ this.storage = options.storage;
17
+ this.defaultMetadata = options.defaultMetadata ?? {};
18
+ this.shouldAudit = options.shouldAudit ?? (() => true);
19
+ this.transformAuditRecord = options.transformAuditRecord;
20
+ }
21
+ /**
22
+ * Publish an event and optionally record it for audit.
23
+ */
24
+ async publish(topic, bytes) {
25
+ await this.bus.publish(topic, bytes);
26
+ if (this.storage) try {
27
+ const envelope = decodeEvent(bytes);
28
+ if (this.shouldAudit(envelope.key, envelope)) {
29
+ let record = {
30
+ id: crypto.randomUUID(),
31
+ eventKey: envelope.key,
32
+ eventVersion: envelope.version,
33
+ payload: envelope.payload,
34
+ metadata: {
35
+ ...this.defaultMetadata,
36
+ ...envelope.metadata
37
+ },
38
+ occurredAt: envelope.occurredAt,
39
+ traceId: envelope.traceId,
40
+ recordedAt: /* @__PURE__ */ new Date()
41
+ };
42
+ if (this.transformAuditRecord) record = this.transformAuditRecord(record);
43
+ await this.storage.store(record);
44
+ }
45
+ } catch (error) {
46
+ console.error("Failed to record audit:", error);
47
+ }
48
+ }
49
+ /**
50
+ * Subscribe to events from the underlying bus.
51
+ */
52
+ async subscribe(topic, handler) {
53
+ return this.bus.subscribe(topic, handler);
54
+ }
55
+ /**
56
+ * Query audit records (if storage supports it).
57
+ */
58
+ async queryAudit(options) {
59
+ if (!this.storage?.query) throw new Error("Audit storage does not support querying");
60
+ return this.storage.query(options);
61
+ }
62
+ };
63
+ /**
64
+ * Create an auditable publisher that includes metadata.
65
+ */
66
+ function makeAuditablePublisher(bus, spec, defaultMetadata) {
67
+ return async (payload, options) => {
68
+ const envelope = {
69
+ id: crypto.randomUUID(),
70
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
71
+ key: spec.meta.key,
72
+ version: spec.meta.version,
73
+ payload,
74
+ traceId: options?.traceId,
75
+ metadata: {
76
+ ...defaultMetadata,
77
+ ...options?.metadata
78
+ }
79
+ };
80
+ await bus.publish(eventKey(spec.meta.key, spec.meta.version), encodeEvent(envelope));
81
+ };
82
+ }
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
+ function createAuditableEventBus(bus, options) {
128
+ return new AuditableEventBus({
129
+ bus,
130
+ storage: options?.storage ?? new InMemoryAuditStorage(),
131
+ ...options
132
+ });
133
+ }
134
+
135
+ //#endregion
136
+ export { AuditableEventBus, InMemoryAuditStorage, createAuditableEventBus, makeAuditablePublisher };
137
+ //# sourceMappingURL=auditableBus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auditableBus.js","names":[],"sources":["../src/auditableBus.ts"],"sourcesContent":["import {\n type EventEnvelope,\n type EventKey,\n eventKey,\n type EventSpec,\n} from '@contractspec/lib.contracts';\nimport type { AnySchemaModel } from '@contractspec/lib.schema';\nimport type { EventBus } from './eventBus';\nimport { encodeEvent, decodeEvent } from './eventBus';\nimport type { EventMetadata } from './metadata';\n\n/**\n * Extended event envelope with metadata for auditing.\n */\nexport interface AuditableEventEnvelope<T = unknown> extends EventEnvelope<T> {\n metadata?: EventMetadata;\n}\n\n/**\n * Audit record for persisting event history.\n */\nexport interface AuditRecord {\n id: string;\n eventKey: string;\n eventVersion: string;\n payload: unknown;\n metadata?: EventMetadata;\n occurredAt: string;\n traceId?: string;\n recordedAt: Date;\n}\n\n/**\n * Audit storage adapter interface.\n */\nexport interface AuditStorage {\n /** Store an audit record */\n store(record: AuditRecord): Promise<void>;\n /** Query audit records */\n query?(options: AuditQueryOptions): Promise<AuditRecord[]>;\n}\n\n/**\n * Options for querying audit records.\n */\nexport interface AuditQueryOptions {\n eventKey?: string;\n actorId?: string;\n targetId?: string;\n orgId?: string;\n traceId?: string;\n from?: Date;\n to?: Date;\n limit?: number;\n offset?: number;\n}\n\n/**\n * Options for creating an auditable event bus.\n */\nexport interface AuditableEventBusOptions {\n /** Underlying event bus for publishing */\n bus: EventBus;\n /** Audit storage adapter */\n storage?: AuditStorage;\n /** Default metadata to include with all events */\n defaultMetadata?: EventMetadata;\n /** Filter function to decide which events to audit */\n shouldAudit?: (eventKey: string, envelope: AuditableEventEnvelope) => boolean;\n /** Transform function for audit records */\n transformAuditRecord?: (record: AuditRecord) => AuditRecord;\n}\n\n/**\n * AuditableEventBus wraps an EventBus to automatically record events for audit trail.\n */\nexport class AuditableEventBus implements EventBus {\n private readonly bus: EventBus;\n private readonly storage?: AuditStorage;\n private readonly defaultMetadata: EventMetadata;\n private readonly shouldAudit: (\n eventKey: string,\n envelope: AuditableEventEnvelope\n ) => boolean;\n private readonly transformAuditRecord?: (record: AuditRecord) => AuditRecord;\n\n constructor(options: AuditableEventBusOptions) {\n this.bus = options.bus;\n this.storage = options.storage;\n this.defaultMetadata = options.defaultMetadata ?? {};\n this.shouldAudit = options.shouldAudit ?? (() => true);\n this.transformAuditRecord = options.transformAuditRecord;\n }\n\n /**\n * Publish an event and optionally record it for audit.\n */\n async publish(topic: EventKey, bytes: Uint8Array): Promise<void> {\n // Publish to underlying bus first\n await this.bus.publish(topic, bytes);\n\n // Record for audit if storage is configured\n if (this.storage) {\n try {\n const envelope = decodeEvent<unknown>(bytes) as AuditableEventEnvelope;\n\n if (this.shouldAudit(envelope.key, envelope)) {\n let record: AuditRecord = {\n id: crypto.randomUUID(),\n eventKey: envelope.key,\n eventVersion: envelope.version,\n payload: envelope.payload,\n metadata: {\n ...this.defaultMetadata,\n ...envelope.metadata,\n },\n occurredAt: envelope.occurredAt,\n traceId: envelope.traceId,\n recordedAt: new Date(),\n };\n\n if (this.transformAuditRecord) {\n record = this.transformAuditRecord(record);\n }\n\n await this.storage.store(record);\n }\n } catch (error) {\n // Log but don't fail publishing if audit fails\n console.error('Failed to record audit:', error);\n }\n }\n }\n\n /**\n * Subscribe to events from the underlying bus.\n */\n async subscribe(\n topic: EventKey | string,\n handler: (bytes: Uint8Array) => Promise<void>\n ): Promise<() => Promise<void>> {\n return this.bus.subscribe(topic, handler);\n }\n\n /**\n * Query audit records (if storage supports it).\n */\n async queryAudit(options: AuditQueryOptions): Promise<AuditRecord[]> {\n if (!this.storage?.query) {\n throw new Error('Audit storage does not support querying');\n }\n return this.storage.query(options);\n }\n}\n\n/**\n * Create an auditable publisher that includes metadata.\n */\nexport function makeAuditablePublisher<T extends AnySchemaModel>(\n bus: AuditableEventBus | EventBus,\n spec: EventSpec<T>,\n defaultMetadata?: EventMetadata\n) {\n return async (\n payload: T,\n options?: { traceId?: string; metadata?: EventMetadata }\n ) => {\n const envelope: AuditableEventEnvelope<T> = {\n id: crypto.randomUUID(),\n occurredAt: new Date().toISOString(),\n key: spec.meta.key,\n version: spec.meta.version,\n payload,\n traceId: options?.traceId,\n metadata: {\n ...defaultMetadata,\n ...options?.metadata,\n },\n };\n await bus.publish(\n eventKey(spec.meta.key, spec.meta.version),\n encodeEvent(envelope)\n );\n };\n}\n\n/**\n * In-memory audit storage for development/testing.\n */\nexport class InMemoryAuditStorage implements AuditStorage {\n private records: AuditRecord[] = [];\n\n async store(record: AuditRecord): Promise<void> {\n this.records.push(record);\n }\n\n async query(options: AuditQueryOptions): Promise<AuditRecord[]> {\n let results = [...this.records];\n\n if (options.eventKey) {\n results = results.filter((r) => r.eventKey === options.eventKey);\n }\n\n if (options.actorId) {\n results = results.filter((r) => r.metadata?.actorId === options.actorId);\n }\n\n if (options.targetId) {\n results = results.filter(\n (r) => r.metadata?.targetId === options.targetId\n );\n }\n\n if (options.orgId) {\n results = results.filter((r) => r.metadata?.orgId === options.orgId);\n }\n\n if (options.traceId) {\n results = results.filter((r) => r.traceId === options.traceId);\n }\n\n if (options.from) {\n const from = options.from;\n results = results.filter((r) => new Date(r.occurredAt) >= from);\n }\n\n if (options.to) {\n const to = options.to;\n results = results.filter((r) => new Date(r.occurredAt) <= to);\n }\n\n // Sort by occurredAt descending\n results.sort(\n (a, b) =>\n new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime()\n );\n\n // Apply pagination\n const offset = options.offset ?? 0;\n const limit = options.limit ?? 100;\n return results.slice(offset, offset + limit);\n }\n\n /**\n * Get all records (for testing).\n */\n getAll(): AuditRecord[] {\n return [...this.records];\n }\n\n /**\n * Clear all records (for testing).\n */\n clear(): void {\n this.records = [];\n }\n}\n\n/**\n * Create an auditable event bus with in-memory storage.\n */\nexport function createAuditableEventBus(\n bus: EventBus,\n options?: Omit<AuditableEventBusOptions, 'bus'>\n): AuditableEventBus {\n return new AuditableEventBus({\n bus,\n storage: options?.storage ?? new InMemoryAuditStorage(),\n ...options,\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { EventEnvelope, EventKey, EventSpec } from "@contractspec/lib.contracts";
2
+ import { AnySchemaModel } from "@contractspec/lib.schema";
3
+
4
+ //#region src/eventBus.d.ts
5
+ interface EventBus {
6
+ publish: (topic: EventKey, bytes: Uint8Array) => Promise<void>;
7
+ subscribe: (topic: EventKey | string,
8
+ // allow wildcard if your broker supports it
9
+ handler: (bytes: Uint8Array) => Promise<void>) => Promise<() => Promise<void>>;
10
+ }
11
+ /** Helper to encode a typed event envelope into JSON string */
12
+ declare function encodeEvent<T>(envelope: EventEnvelope<T>): Uint8Array;
13
+ /** Helper to decode JSON string into a typed event envelope */
14
+ declare function decodeEvent<T>(data: Uint8Array): EventEnvelope<T>;
15
+ /**
16
+ * Create a typed publisher function for a given event spec.
17
+ * It ensures payload conformance at compile time and builds the correct topic.
18
+ */
19
+ declare function makePublisher<T extends AnySchemaModel>(bus: EventBus, spec: EventSpec<T>): (payload: T, traceId?: string) => Promise<void>;
20
+ //#endregion
21
+ export { EventBus, decodeEvent, encodeEvent, makePublisher };
22
+ //# sourceMappingURL=eventBus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventBus.d.ts","names":[],"sources":["../src/eventBus.ts"],"sourcesContent":[],"mappings":";;;;UAQiB,QAAA;mBACE,iBAAiB,eAAe;EADlC,SAAA,EAAQ,CAAA,KAAA,EAGd,QAHc,GAAA,MAAA;EAAA;EACN,OAAA,EAAA,CAAA,KAAA,EAGE,UAHF,EAAA,GAGiB,OAHjB,CAAA,IAAA,CAAA,EAAA,GAIZ,OAJY,CAAA,GAAA,GAIE,OAJF,CAAA,IAAA,CAAA,CAAA;;;AAER,iBAMK,WANL,CAAA,CAAA,CAAA,CAAA,QAAA,EAM8B,aAN9B,CAM4C,CAN5C,CAAA,CAAA,EAMiD,UANjD;;AACyB,iBAUpB,WAVoB,CAAA,CAAA,CAAA,CAAA,IAAA,EAUC,UAVD,CAAA,EAUc,aAVd,CAU4B,CAV5B,CAAA;;;;AAKpC;AAAuD,iBAavC,aAbuC,CAAA,UAaf,cAbe,CAAA,CAAA,GAAA,EAchD,QAdgD,EAAA,IAAA,EAe/C,SAf+C,CAerC,CAfqC,CAAA,CAAA,EAAA,CAAA,OAAA,EAiB9B,CAjB8B,EAAA,OAAA,CAAA,EAAA,MAAA,EAAA,GAiBX,OAjBW,CAAA,IAAA,CAAA"}
@@ -0,0 +1,32 @@
1
+ import { eventKey } from "@contractspec/lib.contracts";
2
+
3
+ //#region src/eventBus.ts
4
+ /** Helper to encode a typed event envelope into JSON string */
5
+ function encodeEvent(envelope) {
6
+ return new TextEncoder().encode(JSON.stringify(envelope));
7
+ }
8
+ /** Helper to decode JSON string into a typed event envelope */
9
+ function decodeEvent(data) {
10
+ return JSON.parse(new TextDecoder().decode(data));
11
+ }
12
+ /**
13
+ * Create a typed publisher function for a given event spec.
14
+ * It ensures payload conformance at compile time and builds the correct topic.
15
+ */
16
+ function makePublisher(bus, spec) {
17
+ return async (payload, traceId) => {
18
+ const envelope = {
19
+ id: crypto.randomUUID(),
20
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
21
+ key: spec.meta.key,
22
+ version: spec.meta.version,
23
+ payload,
24
+ traceId
25
+ };
26
+ await bus.publish(eventKey(spec.meta.key, spec.meta.version), encodeEvent(envelope));
27
+ };
28
+ }
29
+
30
+ //#endregion
31
+ export { decodeEvent, encodeEvent, makePublisher };
32
+ //# sourceMappingURL=eventBus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eventBus.js","names":[],"sources":["../src/eventBus.ts"],"sourcesContent":["import {\n type EventEnvelope,\n type EventKey,\n eventKey,\n type EventSpec,\n} from '@contractspec/lib.contracts';\nimport type { AnySchemaModel } from '@contractspec/lib.schema';\n\nexport interface EventBus {\n publish: (topic: EventKey, bytes: Uint8Array) => Promise<void>;\n subscribe: (\n topic: EventKey | string, // allow wildcard if your broker supports it\n handler: (bytes: Uint8Array) => Promise<void>\n ) => Promise<() => Promise<void>>; // returns unsubscribe\n}\n\n/** Helper to encode a typed event envelope into JSON string */\nexport function encodeEvent<T>(envelope: EventEnvelope<T>): Uint8Array {\n return new TextEncoder().encode(JSON.stringify(envelope));\n}\n\n/** Helper to decode JSON string into a typed event envelope */\nexport function decodeEvent<T>(data: Uint8Array): EventEnvelope<T> {\n return JSON.parse(new TextDecoder().decode(data)) as EventEnvelope<T>;\n}\n\n/**\n * Create a typed publisher function for a given event spec.\n * It ensures payload conformance at compile time and builds the correct topic.\n */\nexport function makePublisher<T extends AnySchemaModel>(\n bus: EventBus,\n spec: EventSpec<T>\n) {\n return async (payload: T, traceId?: string) => {\n const envelope: EventEnvelope<T> = {\n id: crypto.randomUUID(),\n occurredAt: new Date().toISOString(),\n key: spec.meta.key,\n version: spec.meta.version,\n payload,\n traceId,\n };\n await bus.publish(\n eventKey(spec.meta.key, spec.meta.version),\n encodeEvent(envelope)\n );\n };\n}\n"],"mappings":";;;;AAiBA,SAAgB,YAAe,UAAwC;AACrE,QAAO,IAAI,aAAa,CAAC,OAAO,KAAK,UAAU,SAAS,CAAC;;;AAI3D,SAAgB,YAAe,MAAoC;AACjE,QAAO,KAAK,MAAM,IAAI,aAAa,CAAC,OAAO,KAAK,CAAC;;;;;;AAOnD,SAAgB,cACd,KACA,MACA;AACA,QAAO,OAAO,SAAY,YAAqB;EAC7C,MAAM,WAA6B;GACjC,IAAI,OAAO,YAAY;GACvB,6BAAY,IAAI,MAAM,EAAC,aAAa;GACpC,KAAK,KAAK,KAAK;GACf,SAAS,KAAK,KAAK;GACnB;GACA;GACD;AACD,QAAM,IAAI,QACR,SAAS,KAAK,KAAK,KAAK,KAAK,KAAK,QAAQ,EAC1C,YAAY,SAAS,CACtB"}
@@ -0,0 +1,83 @@
1
+ import { EventBus } from "./eventBus.js";
2
+ import { AuditableEventEnvelope } from "./auditableBus.js";
3
+ import { EventKey, EventSpec } from "@contractspec/lib.contracts";
4
+ import { AnySchemaModel } from "@contractspec/lib.schema";
5
+
6
+ //#region src/filtering.d.ts
7
+
8
+ /**
9
+ * Event filter configuration.
10
+ */
11
+ interface EventFilter {
12
+ /** Filter by event name pattern (supports * wildcard) */
13
+ eventName?: string;
14
+ /** Filter by domain prefix */
15
+ domain?: string;
16
+ /** Filter by version */
17
+ version?: string;
18
+ /** Filter by actor ID */
19
+ actorId?: string;
20
+ /** Filter by organization ID */
21
+ orgId?: string;
22
+ /** Filter by custom tags */
23
+ tags?: Record<string, string>;
24
+ /** Custom predicate function */
25
+ predicate?: (envelope: AuditableEventEnvelope) => boolean;
26
+ }
27
+ /**
28
+ * Check if an event matches a filter.
29
+ */
30
+ declare function matchesFilter(envelope: AuditableEventEnvelope, filter: EventFilter): boolean;
31
+ /**
32
+ * Create a filtered subscriber that only receives matching events.
33
+ */
34
+ declare function createFilteredSubscriber(bus: EventBus, filter: EventFilter, handler: (envelope: AuditableEventEnvelope) => Promise<void>): (topic: EventKey | string) => Promise<() => Promise<void>>;
35
+ /**
36
+ * Domain-specific event bus that filters by domain prefix.
37
+ */
38
+ declare class DomainEventBus {
39
+ private readonly bus;
40
+ private readonly domain;
41
+ constructor(bus: EventBus, domain: string);
42
+ /**
43
+ * Publish a domain event.
44
+ */
45
+ publish<T extends AnySchemaModel>(spec: EventSpec<T>, payload: T, metadata?: AuditableEventEnvelope['metadata']): Promise<void>;
46
+ /**
47
+ * Subscribe to all domain events.
48
+ */
49
+ subscribeAll(handler: (envelope: AuditableEventEnvelope) => Promise<void>): Promise<() => Promise<void>>;
50
+ /**
51
+ * Subscribe with filter.
52
+ */
53
+ subscribeFiltered(filter: Omit<EventFilter, 'domain'>, handler: (envelope: AuditableEventEnvelope) => Promise<void>): Promise<() => Promise<void>>;
54
+ }
55
+ /**
56
+ * Create a domain-scoped event bus.
57
+ */
58
+ declare function createDomainBus(bus: EventBus, domain: string): DomainEventBus;
59
+ /**
60
+ * Event router that routes events to different handlers based on filters.
61
+ */
62
+ declare class EventRouter {
63
+ private routes;
64
+ /**
65
+ * Add a route.
66
+ */
67
+ route(filter: EventFilter, handler: (envelope: AuditableEventEnvelope) => Promise<void>): this;
68
+ /**
69
+ * Route an event to matching handlers.
70
+ */
71
+ dispatch(envelope: AuditableEventEnvelope): Promise<void>;
72
+ /**
73
+ * Create a subscriber that routes events.
74
+ */
75
+ createSubscriber(bus: EventBus): (topic: EventKey | string) => Promise<() => Promise<void>>;
76
+ }
77
+ /**
78
+ * Create an event router.
79
+ */
80
+ declare function createEventRouter(): EventRouter;
81
+ //#endregion
82
+ export { DomainEventBus, EventFilter, EventRouter, createDomainBus, createEventRouter, createFilteredSubscriber, matchesFilter };
83
+ //# sourceMappingURL=filtering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filtering.d.ts","names":[],"sources":["../src/filtering.ts"],"sourcesContent":[],"mappings":";;;;;;;;;AAUA;AAoBgB,UApBC,WAAA,CAoBY;EAyDb;EACT,SAAA,CAAA,EAAA,MAAA;EACG;EACY,MAAA,CAAA,EAAA,MAAA;EAA2B;EAE1B,OAAA,CAAA,EAAA,MAAA;EAAiB;EAAA,OAAA,CAAA,EAAA,MAAA;EAAA;EAc3B,KAAA,CAAA,EAAA,MAAA;EAEa;EAOA,IAAA,CAAA,EA7FjB,MA6FiB,CAAA,MAAA,EAAA,MAAA,CAAA;EACN;EAAV,SAAA,CAAA,EAAA,CAAA,QAAA,EA5Fe,sBA4Ff,EAAA,GAAA,OAAA;;;;;AA0ByC,iBAhHnC,aAAA,CAgHmC,QAAA,EA/GvC,sBA+GuC,EAAA,MAAA,EA9GzC,WA8GyC,CAAA,EAAA,OAAA;;;;AAavC,iBApEI,wBAAA,CAoEJ,GAAA,EAnEL,QAmEK,EAAA,MAAA,EAlEF,WAkEE,EAAA,OAAA,EAAA,CAAA,QAAA,EAjEU,sBAiEV,EAAA,GAjEqC,OAiErC,CAAA,IAAA,CAAA,CAAA,EAAA,CAAA,KAAA,EA/DW,QA+DX,GAAA,MAAA,EAAA,GA/D4B,OA+D5B,CAAA,GAAA,GA/D4B,OA+D5B,CAAA,IAAA,CAAA,CAAA;;;;AAEP,cAnDQ,cAAA,CAmDR;EAAO,iBAAA,GAAA;EAmBI,iBAAA,MAAe;EAOlB,WAAA,CAAA,GAAW,EA3EE,QA2EF,EAAA,MAAA,EAAA,MAAA;EAUZ;;;EAUe,OAAA,CAAA,UAxFD,cAwFC,CAAA,CAAA,IAAA,EAvFjB,SAuFiB,CAvFP,CAuFO,CAAA,EAAA,OAAA,EAtFd,CAsFc,EAAA,QAAA,CAAA,EArFZ,sBAqFY,CAAA,UAAA,CAAA,CAAA,EApFtB,OAoFsB,CAAA,IAAA,CAAA;EAAyB;;;EAaH,YAAA,CAAA,OAAA,EAAA,CAAA,QAAA,EA1EzB,sBA0EyB,EAAA,GA1EE,OA0EF,CAAA,IAAA,CAAA,CAAA,EAzE5C,OAyE4C,CAAA,GAAA,GAzE9B,OAyE8B,CAAA,IAAA,CAAA,CAAA;EAAd;;AAanC;4BA1EY,KAAK,4CACO,2BAA2B,gBAC9C,cAAc;;;;;iBAmBH,eAAA,MAAqB,2BAA2B;;;;cAOnD,WAAA;;;;;gBAUD,iCACY,2BAA2B;;;;qBASxB,yBAAyB;;;;wBAY3C,mBACI,sBAAsB,cAAc;;;;;iBAajC,iBAAA,CAAA,GAAqB"}
@@ -0,0 +1,134 @@
1
+ import { decodeEvent } from "./eventBus.js";
2
+ import { satisfies } from "compare-versions";
3
+
4
+ //#region src/filtering.ts
5
+ /**
6
+ * Check if an event matches a filter.
7
+ */
8
+ function matchesFilter(envelope, filter) {
9
+ if (filter.eventName) {
10
+ const pattern = filter.eventName.replace(/\*/g, ".*");
11
+ if (!(/* @__PURE__ */ new RegExp(`^${pattern}$`)).test(envelope.key)) return false;
12
+ }
13
+ if (filter.domain) {
14
+ if (!envelope.key.startsWith(filter.domain + ".")) return false;
15
+ }
16
+ if (filter.version) {
17
+ if (!envelope.version || !satisfies(envelope.version, filter.version)) return false;
18
+ }
19
+ if (filter.actorId && envelope.metadata?.actorId !== filter.actorId) return false;
20
+ if (filter.orgId && envelope.metadata?.orgId !== filter.orgId) return false;
21
+ if (filter.tags) {
22
+ const eventTags = envelope.metadata?.tags ?? {};
23
+ for (const [key, value] of Object.entries(filter.tags)) if (eventTags[key] !== value) return false;
24
+ }
25
+ if (filter.predicate && !filter.predicate(envelope)) return false;
26
+ return true;
27
+ }
28
+ /**
29
+ * Create a filtered subscriber that only receives matching events.
30
+ */
31
+ function createFilteredSubscriber(bus, filter, handler) {
32
+ return async (topic) => {
33
+ return bus.subscribe(topic, async (bytes) => {
34
+ const envelope = decodeEvent(bytes);
35
+ if (matchesFilter(envelope, filter)) await handler(envelope);
36
+ });
37
+ };
38
+ }
39
+ /**
40
+ * Domain-specific event bus that filters by domain prefix.
41
+ */
42
+ var DomainEventBus = class {
43
+ constructor(bus, domain) {
44
+ this.bus = bus;
45
+ this.domain = domain;
46
+ }
47
+ /**
48
+ * Publish a domain event.
49
+ */
50
+ async publish(spec, payload, metadata) {
51
+ const eventName = spec.meta.key.startsWith(this.domain + ".") ? spec.meta.key : `${this.domain}.${spec.meta.key}`;
52
+ const envelope = {
53
+ id: crypto.randomUUID(),
54
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
55
+ key: eventName,
56
+ version: spec.meta.version,
57
+ payload,
58
+ metadata
59
+ };
60
+ const bytes = new TextEncoder().encode(JSON.stringify(envelope));
61
+ await this.bus.publish(`${eventName}.v${spec.meta.version}`, bytes);
62
+ }
63
+ /**
64
+ * Subscribe to all domain events.
65
+ */
66
+ async subscribeAll(handler) {
67
+ return this.bus.subscribe(`${this.domain}.*`, async (bytes) => {
68
+ await handler(decodeEvent(bytes));
69
+ });
70
+ }
71
+ /**
72
+ * Subscribe with filter.
73
+ */
74
+ async subscribeFiltered(filter, handler) {
75
+ const fullFilter = {
76
+ ...filter,
77
+ domain: this.domain
78
+ };
79
+ return this.bus.subscribe(`${this.domain}.*`, async (bytes) => {
80
+ const envelope = decodeEvent(bytes);
81
+ if (matchesFilter(envelope, fullFilter)) await handler(envelope);
82
+ });
83
+ }
84
+ };
85
+ /**
86
+ * Create a domain-scoped event bus.
87
+ */
88
+ function createDomainBus(bus, domain) {
89
+ return new DomainEventBus(bus, domain);
90
+ }
91
+ /**
92
+ * Event router that routes events to different handlers based on filters.
93
+ */
94
+ var EventRouter = class {
95
+ routes = [];
96
+ /**
97
+ * Add a route.
98
+ */
99
+ route(filter, handler) {
100
+ this.routes.push({
101
+ filter,
102
+ handler
103
+ });
104
+ return this;
105
+ }
106
+ /**
107
+ * Route an event to matching handlers.
108
+ */
109
+ async dispatch(envelope) {
110
+ const matchingRoutes = this.routes.filter((r) => matchesFilter(envelope, r.filter));
111
+ await Promise.all(matchingRoutes.map((r) => r.handler(envelope)));
112
+ }
113
+ /**
114
+ * Create a subscriber that routes events.
115
+ */
116
+ createSubscriber(bus) {
117
+ return async (topic) => {
118
+ return bus.subscribe(topic, async (bytes) => {
119
+ const envelope = decodeEvent(bytes);
120
+ await this.dispatch(envelope);
121
+ });
122
+ };
123
+ }
124
+ };
125
+ /**
126
+ * Create an event router.
127
+ */
128
+ function createEventRouter() {
129
+ return new EventRouter();
130
+ }
131
+
132
+ //#endregion
133
+ export { DomainEventBus, EventRouter, createDomainBus, createEventRouter, createFilteredSubscriber, matchesFilter };
134
+ //# sourceMappingURL=filtering.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filtering.js","names":[],"sources":["../src/filtering.ts"],"sourcesContent":["import type { EventKey, EventSpec } from '@contractspec/lib.contracts';\nimport type { AnySchemaModel } from '@contractspec/lib.schema';\nimport type { EventBus } from './eventBus';\nimport { decodeEvent } from './eventBus';\nimport type { AuditableEventEnvelope } from './auditableBus';\nimport { satisfies } from 'compare-versions';\n\n/**\n * Event filter configuration.\n */\nexport interface EventFilter {\n /** Filter by event name pattern (supports * wildcard) */\n eventName?: string;\n /** Filter by domain prefix */\n domain?: string;\n /** Filter by version */\n version?: string;\n /** Filter by actor ID */\n actorId?: string;\n /** Filter by organization ID */\n orgId?: string;\n /** Filter by custom tags */\n tags?: Record<string, string>;\n /** Custom predicate function */\n predicate?: (envelope: AuditableEventEnvelope) => boolean;\n}\n\n/**\n * Check if an event matches a filter.\n */\nexport function matchesFilter(\n envelope: AuditableEventEnvelope,\n filter: EventFilter\n): boolean {\n // Check event name pattern\n if (filter.eventName) {\n const pattern = filter.eventName.replace(/\\*/g, '.*');\n const regex = new RegExp(`^${pattern}$`);\n if (!regex.test(envelope.key)) {\n return false;\n }\n }\n\n // Check domain prefix\n if (filter.domain) {\n if (!envelope.key.startsWith(filter.domain + '.')) {\n return false;\n }\n }\n\n // Check version\n if (filter.version) {\n if (!envelope.version || !satisfies(envelope.version, filter.version)) {\n return false;\n }\n }\n\n // Check metadata fields\n if (filter.actorId && envelope.metadata?.actorId !== filter.actorId) {\n return false;\n }\n\n if (filter.orgId && envelope.metadata?.orgId !== filter.orgId) {\n return false;\n }\n\n // Check tags\n if (filter.tags) {\n const eventTags = envelope.metadata?.tags ?? {};\n for (const [key, value] of Object.entries(filter.tags)) {\n if (eventTags[key] !== value) {\n return false;\n }\n }\n }\n\n // Custom predicate\n if (filter.predicate && !filter.predicate(envelope)) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Create a filtered subscriber that only receives matching events.\n */\nexport function createFilteredSubscriber(\n bus: EventBus,\n filter: EventFilter,\n handler: (envelope: AuditableEventEnvelope) => Promise<void>\n) {\n return async (topic: EventKey | string) => {\n return bus.subscribe(topic, async (bytes) => {\n const envelope = decodeEvent<unknown>(bytes) as AuditableEventEnvelope;\n\n if (matchesFilter(envelope, filter)) {\n await handler(envelope);\n }\n });\n };\n}\n\n/**\n * Domain-specific event bus that filters by domain prefix.\n */\nexport class DomainEventBus {\n constructor(\n private readonly bus: EventBus,\n private readonly domain: string\n ) {}\n\n /**\n * Publish a domain event.\n */\n async publish<T extends AnySchemaModel>(\n spec: EventSpec<T>,\n payload: T,\n metadata?: AuditableEventEnvelope['metadata']\n ): Promise<void> {\n // Ensure event name starts with domain\n const eventName = spec.meta.key.startsWith(this.domain + '.')\n ? spec.meta.key\n : `${this.domain}.${spec.meta.key}`;\n\n const envelope: AuditableEventEnvelope<T> = {\n id: crypto.randomUUID(),\n occurredAt: new Date().toISOString(),\n key: eventName,\n version: spec.meta.version,\n payload,\n metadata,\n };\n\n const bytes = new TextEncoder().encode(JSON.stringify(envelope));\n await this.bus.publish(`${eventName}.v${spec.meta.version}`, bytes);\n }\n\n /**\n * Subscribe to all domain events.\n */\n async subscribeAll(\n handler: (envelope: AuditableEventEnvelope) => Promise<void>\n ): Promise<() => Promise<void>> {\n // Subscribe to wildcard pattern if supported\n return this.bus.subscribe(`${this.domain}.*`, async (bytes) => {\n const envelope = decodeEvent<unknown>(bytes) as AuditableEventEnvelope;\n await handler(envelope);\n });\n }\n\n /**\n * Subscribe with filter.\n */\n async subscribeFiltered(\n filter: Omit<EventFilter, 'domain'>,\n handler: (envelope: AuditableEventEnvelope) => Promise<void>\n ): Promise<() => Promise<void>> {\n const fullFilter: EventFilter = {\n ...filter,\n domain: this.domain,\n };\n\n return this.bus.subscribe(`${this.domain}.*`, async (bytes) => {\n const envelope = decodeEvent<unknown>(bytes) as AuditableEventEnvelope;\n\n if (matchesFilter(envelope, fullFilter)) {\n await handler(envelope);\n }\n });\n }\n}\n\n/**\n * Create a domain-scoped event bus.\n */\nexport function createDomainBus(bus: EventBus, domain: string): DomainEventBus {\n return new DomainEventBus(bus, domain);\n}\n\n/**\n * Event router that routes events to different handlers based on filters.\n */\nexport class EventRouter {\n private routes: {\n filter: EventFilter;\n handler: (envelope: AuditableEventEnvelope) => Promise<void>;\n }[] = [];\n\n /**\n * Add a route.\n */\n route(\n filter: EventFilter,\n handler: (envelope: AuditableEventEnvelope) => Promise<void>\n ): this {\n this.routes.push({ filter, handler });\n return this;\n }\n\n /**\n * Route an event to matching handlers.\n */\n async dispatch(envelope: AuditableEventEnvelope): Promise<void> {\n const matchingRoutes = this.routes.filter((r) =>\n matchesFilter(envelope, r.filter)\n );\n\n await Promise.all(matchingRoutes.map((r) => r.handler(envelope)));\n }\n\n /**\n * Create a subscriber that routes events.\n */\n createSubscriber(\n bus: EventBus\n ): (topic: EventKey | string) => Promise<() => Promise<void>> {\n return async (topic: EventKey | string) => {\n return bus.subscribe(topic, async (bytes) => {\n const envelope = decodeEvent<unknown>(bytes) as AuditableEventEnvelope;\n await this.dispatch(envelope);\n });\n };\n }\n}\n\n/**\n * Create an event router.\n */\nexport function createEventRouter(): EventRouter {\n return new EventRouter();\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { EventBus } from "./eventBus.js";
2
+ import { EventKey } from "@contractspec/lib.contracts";
3
+
4
+ //#region src/inMemoryBus.d.ts
5
+
6
+ /**
7
+ * In-memory bus for dev/test. Not for production scale.
8
+ * Subscribers receive events synchronously in the order they were published.
9
+ */
10
+ declare class InMemoryBus implements EventBus {
11
+ private listeners;
12
+ publish(topic: EventKey, payload: Uint8Array): Promise<void>;
13
+ subscribe(topic: string | RegExp, handler: (payload: Uint8Array) => Promise<void>): Promise<() => Promise<void>>;
14
+ }
15
+ //#endregion
16
+ export { InMemoryBus };
17
+ //# sourceMappingURL=inMemoryBus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inMemoryBus.d.ts","names":[],"sources":["../src/inMemoryBus.ts"],"sourcesContent":[],"mappings":";;;;;;;AAOA;;AAM0C,cAN7B,WAAA,YAAuB,QAMM,CAAA;EAAa,QAAA,SAAA;EAOnC,OAAA,CAAA,KAAA,EAPG,QAOH,EAAA,OAAA,EAPsB,UAOtB,CAAA,EAPmC,OAOnC,CAAA,IAAA,CAAA;EACG,SAAA,CAAA,KAAA,EAAA,MAAA,GADH,MACG,EAAA,OAAA,EAAA,CAAA,OAAA,EAAA,UAAA,EAAA,GAAe,OAAf,CAAA,IAAA,CAAA,CAAA,EAClB,OADkB,CAAA,GAAA,GACJ,OADI,CAAA,IAAA,CAAA,CAAA"}
@@ -0,0 +1,29 @@
1
+ //#region src/inMemoryBus.ts
2
+ /**
3
+ * In-memory bus for dev/test. Not for production scale.
4
+ * Subscribers receive events synchronously in the order they were published.
5
+ */
6
+ var InMemoryBus = class {
7
+ listeners = /* @__PURE__ */ new Map();
8
+ async publish(topic, payload) {
9
+ const handlers = this.listeners.get(topic);
10
+ if (!handlers) return;
11
+ await Promise.all([...handlers].map((h) => h(payload)));
12
+ }
13
+ async subscribe(topic, handler) {
14
+ const topicStr = String(topic);
15
+ let set = this.listeners.get(topicStr);
16
+ if (!set) {
17
+ set = /* @__PURE__ */ new Set();
18
+ this.listeners.set(topicStr, set);
19
+ }
20
+ set.add(handler);
21
+ return async () => {
22
+ set?.delete(handler);
23
+ };
24
+ }
25
+ };
26
+
27
+ //#endregion
28
+ export { InMemoryBus };
29
+ //# sourceMappingURL=inMemoryBus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inMemoryBus.js","names":[],"sources":["../src/inMemoryBus.ts"],"sourcesContent":["import type { EventKey } from '@contractspec/lib.contracts';\nimport type { EventBus } from './eventBus';\n\n/**\n * In-memory bus for dev/test. Not for production scale.\n * Subscribers receive events synchronously in the order they were published.\n */\nexport class InMemoryBus implements EventBus {\n private listeners = new Map<\n string,\n Set<(payload: Uint8Array) => Promise<void>>\n >();\n\n async publish(topic: EventKey, payload: Uint8Array): Promise<void> {\n const handlers = this.listeners.get(topic);\n if (!handlers) return;\n await Promise.all([...handlers].map((h) => h(payload)));\n }\n\n async subscribe(\n topic: string | RegExp,\n handler: (payload: Uint8Array) => Promise<void>\n ): Promise<() => Promise<void>> {\n const topicStr = String(topic);\n let set = this.listeners.get(topicStr);\n if (!set) {\n set = new Set();\n this.listeners.set(topicStr, set);\n }\n set.add(handler);\n return async () => {\n set?.delete(handler);\n };\n }\n}\n"],"mappings":";;;;;AAOA,IAAa,cAAb,MAA6C;CAC3C,AAAQ,4BAAY,IAAI,KAGrB;CAEH,MAAM,QAAQ,OAAiB,SAAoC;EACjE,MAAM,WAAW,KAAK,UAAU,IAAI,MAAM;AAC1C,MAAI,CAAC,SAAU;AACf,QAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC,KAAK,MAAM,EAAE,QAAQ,CAAC,CAAC;;CAGzD,MAAM,UACJ,OACA,SAC8B;EAC9B,MAAM,WAAW,OAAO,MAAM;EAC9B,IAAI,MAAM,KAAK,UAAU,IAAI,SAAS;AACtC,MAAI,CAAC,KAAK;AACR,yBAAM,IAAI,KAAK;AACf,QAAK,UAAU,IAAI,UAAU,IAAI;;AAEnC,MAAI,IAAI,QAAQ;AAChB,SAAO,YAAY;AACjB,QAAK,OAAO,QAAQ"}
@@ -0,0 +1,7 @@
1
+ import { EventBus, decodeEvent, encodeEvent, makePublisher } from "./eventBus.js";
2
+ import { EventMetadata, MetadataContext, createMetadataContext, createMetadataFromContext, mergeMetadata } from "./metadata.js";
3
+ import { AuditQueryOptions, AuditRecord, AuditStorage, AuditableEventBus, AuditableEventBusOptions, AuditableEventEnvelope, InMemoryAuditStorage, createAuditableEventBus, makeAuditablePublisher } from "./auditableBus.js";
4
+ import { DomainEventBus, EventFilter, EventRouter, createDomainBus, createEventRouter, createFilteredSubscriber, matchesFilter } from "./filtering.js";
5
+ import { InMemoryBus } from "./inMemoryBus.js";
6
+ import { subscribeEvent } from "./subscriber.js";
7
+ export { AuditQueryOptions, AuditRecord, AuditStorage, AuditableEventBus, AuditableEventBusOptions, AuditableEventEnvelope, DomainEventBus, EventBus, EventFilter, EventMetadata, EventRouter, InMemoryAuditStorage, InMemoryBus, MetadataContext, createAuditableEventBus, createDomainBus, createEventRouter, createFilteredSubscriber, createMetadataContext, createMetadataFromContext, decodeEvent, encodeEvent, makeAuditablePublisher, makePublisher, matchesFilter, mergeMetadata, subscribeEvent };
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import { decodeEvent, encodeEvent, makePublisher } from "./eventBus.js";
2
+ import { AuditableEventBus, InMemoryAuditStorage, createAuditableEventBus, makeAuditablePublisher } from "./auditableBus.js";
3
+ import { DomainEventBus, EventRouter, createDomainBus, createEventRouter, createFilteredSubscriber, matchesFilter } from "./filtering.js";
4
+ import { InMemoryBus } from "./inMemoryBus.js";
5
+ import { subscribeEvent } from "./subscriber.js";
6
+ import { MetadataContext, createMetadataContext, createMetadataFromContext, mergeMetadata } from "./metadata.js";
7
+
8
+ export { AuditableEventBus, DomainEventBus, EventRouter, InMemoryAuditStorage, InMemoryBus, MetadataContext, createAuditableEventBus, createDomainBus, createEventRouter, createFilteredSubscriber, createMetadataContext, createMetadataFromContext, decodeEvent, encodeEvent, makeAuditablePublisher, makePublisher, matchesFilter, mergeMetadata, subscribeEvent };
@@ -0,0 +1,82 @@
1
+ //#region src/metadata.d.ts
2
+ /**
3
+ * Event metadata that enriches events with contextual information.
4
+ */
5
+ interface EventMetadata {
6
+ /** ID of the actor (user) who triggered the event */
7
+ actorId?: string;
8
+ /** Type of actor (user, system, service) */
9
+ actorType?: 'user' | 'system' | 'service';
10
+ /** Target resource ID */
11
+ targetId?: string;
12
+ /** Target resource type */
13
+ targetType?: string;
14
+ /** Organization context */
15
+ orgId?: string;
16
+ /** Tenant context (if different from org) */
17
+ tenantId?: string;
18
+ /** Distributed trace ID */
19
+ traceId?: string;
20
+ /** Parent span ID */
21
+ spanId?: string;
22
+ /** Client IP address */
23
+ clientIp?: string;
24
+ /** Client user agent */
25
+ userAgent?: string;
26
+ /** Session ID */
27
+ sessionId?: string;
28
+ /** Request ID */
29
+ requestId?: string;
30
+ /** Source service name */
31
+ source?: string;
32
+ /** Correlation ID for related events */
33
+ correlationId?: string;
34
+ /** Custom tags for filtering/routing */
35
+ tags?: Record<string, string>;
36
+ }
37
+ /**
38
+ * Create metadata from request context.
39
+ */
40
+ declare function createMetadataFromContext(context: {
41
+ userId?: string;
42
+ orgId?: string;
43
+ sessionId?: string;
44
+ requestId?: string;
45
+ traceId?: string;
46
+ clientIp?: string;
47
+ userAgent?: string;
48
+ }): EventMetadata;
49
+ /**
50
+ * Merge metadata with overrides.
51
+ */
52
+ declare function mergeMetadata(base: EventMetadata, overrides: Partial<EventMetadata>): EventMetadata;
53
+ /**
54
+ * Create a metadata context that can be passed through operations.
55
+ */
56
+ declare class MetadataContext {
57
+ private metadata;
58
+ constructor(initial?: EventMetadata);
59
+ /**
60
+ * Get the current metadata.
61
+ */
62
+ get(): EventMetadata;
63
+ /**
64
+ * Set a metadata value.
65
+ */
66
+ set<K extends keyof EventMetadata>(key: K, value: EventMetadata[K]): this;
67
+ /**
68
+ * Add a tag.
69
+ */
70
+ tag(key: string, value: string): this;
71
+ /**
72
+ * Create a child context with the same trace.
73
+ */
74
+ child(overrides?: Partial<EventMetadata>): MetadataContext;
75
+ }
76
+ /**
77
+ * Create a new metadata context.
78
+ */
79
+ declare function createMetadataContext(initial?: EventMetadata): MetadataContext;
80
+ //#endregion
81
+ export { EventMetadata, MetadataContext, createMetadataContext, createMetadataFromContext, mergeMetadata };
82
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","names":[],"sources":["../src/metadata.ts"],"sourcesContent":[],"mappings":";;AAGA;AAoCA;AAyBgB,UA7DC,aAAA,CA6DY;EACrB;EACa,OAAA,CAAA,EAAA,MAAA;EAAR;EACV,SAAA,CAAA,EAAA,MAAA,GAAA,QAAA,GAAA,SAAA;EAAa;EAcH,QAAA,CAAA,EAAA,MAAA;EAGU;EAUd,UAAA,CAAA,EAAA,MAAA;EAOa;EAAoB,KAAA,CAAA,EAAA,MAAA;EAAU;EAAc,QAAA,CAAA,EAAA,MAAA;EAgBvC;EAAR,OAAA,CAAA,EAAA,MAAA;EAA8B;EAAe,MAAA,CAAA,EAAA,MAAA;EAahD;;;;;;;;;;;;;SAjGP;;;;;iBAMO,yBAAA;;;;;;;;IAQZ;;;;iBAiBY,aAAA,OACR,0BACK,QAAQ,iBAClB;;;;cAcU,eAAA;;wBAGU;;;;SAUd;;;;sBAOa,oBAAoB,UAAU,cAAc;;;;;;;;oBAgB/C,QAAQ,iBAAsB;;;;;iBAajC,qBAAA,WACJ,gBACT"}
@@ -0,0 +1,82 @@
1
+ //#region src/metadata.ts
2
+ /**
3
+ * Create metadata from request context.
4
+ */
5
+ function createMetadataFromContext(context) {
6
+ return {
7
+ actorId: context.userId,
8
+ actorType: context.userId ? "user" : "system",
9
+ orgId: context.orgId,
10
+ tenantId: context.orgId,
11
+ sessionId: context.sessionId,
12
+ requestId: context.requestId,
13
+ traceId: context.traceId || crypto.randomUUID(),
14
+ clientIp: context.clientIp,
15
+ userAgent: context.userAgent
16
+ };
17
+ }
18
+ /**
19
+ * Merge metadata with overrides.
20
+ */
21
+ function mergeMetadata(base, overrides) {
22
+ return {
23
+ ...base,
24
+ ...overrides,
25
+ tags: {
26
+ ...base.tags,
27
+ ...overrides.tags
28
+ }
29
+ };
30
+ }
31
+ /**
32
+ * Create a metadata context that can be passed through operations.
33
+ */
34
+ var MetadataContext = class MetadataContext {
35
+ metadata;
36
+ constructor(initial = {}) {
37
+ this.metadata = { ...initial };
38
+ if (!this.metadata.traceId) this.metadata.traceId = crypto.randomUUID();
39
+ }
40
+ /**
41
+ * Get the current metadata.
42
+ */
43
+ get() {
44
+ return { ...this.metadata };
45
+ }
46
+ /**
47
+ * Set a metadata value.
48
+ */
49
+ set(key, value) {
50
+ this.metadata[key] = value;
51
+ return this;
52
+ }
53
+ /**
54
+ * Add a tag.
55
+ */
56
+ tag(key, value) {
57
+ this.metadata.tags = {
58
+ ...this.metadata.tags,
59
+ [key]: value
60
+ };
61
+ return this;
62
+ }
63
+ /**
64
+ * Create a child context with the same trace.
65
+ */
66
+ child(overrides = {}) {
67
+ return new MetadataContext(mergeMetadata(this.metadata, {
68
+ ...overrides,
69
+ spanId: crypto.randomUUID()
70
+ }));
71
+ }
72
+ };
73
+ /**
74
+ * Create a new metadata context.
75
+ */
76
+ function createMetadataContext(initial) {
77
+ return new MetadataContext(initial);
78
+ }
79
+
80
+ //#endregion
81
+ export { MetadataContext, createMetadataContext, createMetadataFromContext, mergeMetadata };
82
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","names":[],"sources":["../src/metadata.ts"],"sourcesContent":["/**\n * Event metadata that enriches events with contextual information.\n */\nexport interface EventMetadata {\n /** ID of the actor (user) who triggered the event */\n actorId?: string;\n /** Type of actor (user, system, service) */\n actorType?: 'user' | 'system' | 'service';\n /** Target resource ID */\n targetId?: string;\n /** Target resource type */\n targetType?: string;\n /** Organization context */\n orgId?: string;\n /** Tenant context (if different from org) */\n tenantId?: string;\n /** Distributed trace ID */\n traceId?: string;\n /** Parent span ID */\n spanId?: string;\n /** Client IP address */\n clientIp?: string;\n /** Client user agent */\n userAgent?: string;\n /** Session ID */\n sessionId?: string;\n /** Request ID */\n requestId?: string;\n /** Source service name */\n source?: string;\n /** Correlation ID for related events */\n correlationId?: string;\n /** Custom tags for filtering/routing */\n tags?: Record<string, string>;\n}\n\n/**\n * Create metadata from request context.\n */\nexport function createMetadataFromContext(context: {\n userId?: string;\n orgId?: string;\n sessionId?: string;\n requestId?: string;\n traceId?: string;\n clientIp?: string;\n userAgent?: string;\n}): EventMetadata {\n return {\n actorId: context.userId,\n actorType: context.userId ? 'user' : 'system',\n orgId: context.orgId,\n tenantId: context.orgId,\n sessionId: context.sessionId,\n requestId: context.requestId,\n traceId: context.traceId || crypto.randomUUID(),\n clientIp: context.clientIp,\n userAgent: context.userAgent,\n };\n}\n\n/**\n * Merge metadata with overrides.\n */\nexport function mergeMetadata(\n base: EventMetadata,\n overrides: Partial<EventMetadata>\n): EventMetadata {\n return {\n ...base,\n ...overrides,\n tags: {\n ...base.tags,\n ...overrides.tags,\n },\n };\n}\n\n/**\n * Create a metadata context that can be passed through operations.\n */\nexport class MetadataContext {\n private metadata: EventMetadata;\n\n constructor(initial: EventMetadata = {}) {\n this.metadata = { ...initial };\n if (!this.metadata.traceId) {\n this.metadata.traceId = crypto.randomUUID();\n }\n }\n\n /**\n * Get the current metadata.\n */\n get(): EventMetadata {\n return { ...this.metadata };\n }\n\n /**\n * Set a metadata value.\n */\n set<K extends keyof EventMetadata>(key: K, value: EventMetadata[K]): this {\n this.metadata[key] = value;\n return this;\n }\n\n /**\n * Add a tag.\n */\n tag(key: string, value: string): this {\n this.metadata.tags = { ...this.metadata.tags, [key]: value };\n return this;\n }\n\n /**\n * Create a child context with the same trace.\n */\n child(overrides: Partial<EventMetadata> = {}): MetadataContext {\n return new MetadataContext(\n mergeMetadata(this.metadata, {\n ...overrides,\n spanId: crypto.randomUUID(),\n })\n );\n }\n}\n\n/**\n * Create a new metadata context.\n */\nexport function createMetadataContext(\n initial?: EventMetadata\n): MetadataContext {\n return new MetadataContext(initial);\n}\n"],"mappings":";;;;AAuCA,SAAgB,0BAA0B,SAQxB;AAChB,QAAO;EACL,SAAS,QAAQ;EACjB,WAAW,QAAQ,SAAS,SAAS;EACrC,OAAO,QAAQ;EACf,UAAU,QAAQ;EAClB,WAAW,QAAQ;EACnB,WAAW,QAAQ;EACnB,SAAS,QAAQ,WAAW,OAAO,YAAY;EAC/C,UAAU,QAAQ;EAClB,WAAW,QAAQ;EACpB;;;;;AAMH,SAAgB,cACd,MACA,WACe;AACf,QAAO;EACL,GAAG;EACH,GAAG;EACH,MAAM;GACJ,GAAG,KAAK;GACR,GAAG,UAAU;GACd;EACF;;;;;AAMH,IAAa,kBAAb,MAAa,gBAAgB;CAC3B,AAAQ;CAER,YAAY,UAAyB,EAAE,EAAE;AACvC,OAAK,WAAW,EAAE,GAAG,SAAS;AAC9B,MAAI,CAAC,KAAK,SAAS,QACjB,MAAK,SAAS,UAAU,OAAO,YAAY;;;;;CAO/C,MAAqB;AACnB,SAAO,EAAE,GAAG,KAAK,UAAU;;;;;CAM7B,IAAmC,KAAQ,OAA+B;AACxE,OAAK,SAAS,OAAO;AACrB,SAAO;;;;;CAMT,IAAI,KAAa,OAAqB;AACpC,OAAK,SAAS,OAAO;GAAE,GAAG,KAAK,SAAS;IAAO,MAAM;GAAO;AAC5D,SAAO;;;;;CAMT,MAAM,YAAoC,EAAE,EAAmB;AAC7D,SAAO,IAAI,gBACT,cAAc,KAAK,UAAU;GAC3B,GAAG;GACH,QAAQ,OAAO,YAAY;GAC5B,CAAC,CACH;;;;;;AAOL,SAAgB,sBACd,SACiB;AACjB,QAAO,IAAI,gBAAgB,QAAQ"}
@@ -0,0 +1,13 @@
1
+ import { EventBus } from "./eventBus.js";
2
+ import { EventSpec } from "@contractspec/lib.contracts";
3
+ import { AnySchemaModel } from "@contractspec/lib.schema";
4
+
5
+ //#region src/subscriber.d.ts
6
+ /** Typed subscription using your EventSpec */
7
+ declare function subscribeEvent<T extends AnySchemaModel>(bus: EventBus, spec: EventSpec<T>, handler: (payload: T, ctx: {
8
+ traceId?: string;
9
+ deliveryId?: string;
10
+ }) => Promise<void>): Promise<() => Promise<void>>;
11
+ //#endregion
12
+ export { subscribeEvent };
13
+ //# sourceMappingURL=subscriber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriber.d.ts","names":[],"sources":["../src/subscriber.ts"],"sourcesContent":[],"mappings":";;;;;;iBAKsB,yBAAyB,qBACxC,gBACC,UAAU,uBAEL;EAJS,OAAA,CAAA,EAAA,MAAA;EAAyB,UAAA,CAAA,EAAA,MAAA;CACxC,EAAA,GAKA,OALA,CAAA,IAAA,CAAA,CAAA,EAKa,OALb,CAAA,GAAA,GAKa,OALb,CAAA,IAAA,CAAA,CAAA"}
@@ -0,0 +1,19 @@
1
+ import { decodeEvent } from "./eventBus.js";
2
+
3
+ //#region src/subscriber.ts
4
+ /** Typed subscription using your EventSpec */
5
+ async function subscribeEvent(bus, spec, handler) {
6
+ const topic = `${spec.meta.key}.v${spec.meta.version}`;
7
+ return bus.subscribe(topic, async (u8) => {
8
+ const env = decodeEvent(u8);
9
+ if (env.key !== spec.meta.key || env.version !== spec.meta.version) return;
10
+ await handler(env.payload, {
11
+ traceId: env.traceId,
12
+ deliveryId: env.id
13
+ });
14
+ });
15
+ }
16
+
17
+ //#endregion
18
+ export { subscribeEvent };
19
+ //# sourceMappingURL=subscriber.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriber.js","names":[],"sources":["../src/subscriber.ts"],"sourcesContent":["import { decodeEvent, type EventBus } from './eventBus';\nimport type { EventSpec } from '@contractspec/lib.contracts';\nimport type { AnySchemaModel } from '@contractspec/lib.schema';\n\n/** Typed subscription using your EventSpec */\nexport async function subscribeEvent<T extends AnySchemaModel>(\n bus: EventBus,\n spec: EventSpec<T>,\n handler: (\n payload: T,\n ctx: { traceId?: string; deliveryId?: string }\n ) => Promise<void>\n) {\n const topic = `${spec.meta.key}.v${spec.meta.version}`;\n return bus.subscribe(topic, async (u8) => {\n const env = decodeEvent<T>(u8);\n if (env.key !== spec.meta.key || env.version !== spec.meta.version) return;\n await handler(env.payload, { traceId: env.traceId, deliveryId: env.id });\n });\n}\n"],"mappings":";;;;AAKA,eAAsB,eACpB,KACA,MACA,SAIA;CACA,MAAM,QAAQ,GAAG,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK;AAC7C,QAAO,IAAI,UAAU,OAAO,OAAO,OAAO;EACxC,MAAM,MAAM,YAAe,GAAG;AAC9B,MAAI,IAAI,QAAQ,KAAK,KAAK,OAAO,IAAI,YAAY,KAAK,KAAK,QAAS;AACpE,QAAM,QAAQ,IAAI,SAAS;GAAE,SAAS,IAAI;GAAS,YAAY,IAAI;GAAI,CAAC;GACxE"}
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@contractspec/lib.bus",
3
+ "version": "0.0.0-canary-20260113162409",
4
+ "description": "Event bus and messaging primitives",
5
+ "keywords": [
6
+ "contractspec",
7
+ "events",
8
+ "messaging",
9
+ "pubsub",
10
+ "typescript"
11
+ ],
12
+ "scripts": {
13
+ "publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
14
+ "publish:pkg:canary": "bun publish:pkg --tag canary",
15
+ "build": "bun build:types && bun build:bundle",
16
+ "build:bundle": "tsdown",
17
+ "build:types": "tsc --noEmit",
18
+ "dev": "bun build:bundle --watch",
19
+ "clean": "rimraf dist .turbo",
20
+ "lint": "bun lint:fix",
21
+ "lint:fix": "eslint src --fix",
22
+ "lint:check": "eslint src"
23
+ },
24
+ "devDependencies": {
25
+ "@contractspec/tool.tsdown": "0.0.0-canary-20260113162409",
26
+ "@contractspec/tool.typescript": "0.0.0-canary-20260113162409",
27
+ "@types/express": "^5.0.3",
28
+ "tsdown": "^0.19.0",
29
+ "typescript": "^5.9.3"
30
+ },
31
+ "dependencies": {
32
+ "@contractspec/lib.schema": "0.0.0-canary-20260113162409",
33
+ "@contractspec/lib.contracts": "0.0.0-canary-20260113162409",
34
+ "compare-versions": "^6.1.1"
35
+ },
36
+ "peerDependencies": {
37
+ "elysia": "^1.4.21",
38
+ "express": "^5.2.1",
39
+ "next": "16.1.1"
40
+ },
41
+ "type": "module",
42
+ "types": "./dist/index.d.ts",
43
+ "files": [
44
+ "dist",
45
+ "README.md"
46
+ ],
47
+ "exports": {
48
+ ".": "./dist/index.js",
49
+ "./auditableBus": "./dist/auditableBus.js",
50
+ "./eventBus": "./dist/eventBus.js",
51
+ "./filtering": "./dist/filtering.js",
52
+ "./inMemoryBus": "./dist/inMemoryBus.js",
53
+ "./metadata": "./dist/metadata.js",
54
+ "./subscriber": "./dist/subscriber.js",
55
+ "./*": "./*"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public",
59
+ "exports": {
60
+ ".": "./dist/index.js",
61
+ "./auditableBus": "./dist/auditableBus.js",
62
+ "./eventBus": "./dist/eventBus.js",
63
+ "./filtering": "./dist/filtering.js",
64
+ "./inMemoryBus": "./dist/inMemoryBus.js",
65
+ "./metadata": "./dist/metadata.js",
66
+ "./subscriber": "./dist/subscriber.js",
67
+ "./*": "./*"
68
+ },
69
+ "registry": "https://registry.npmjs.org/"
70
+ },
71
+ "license": "MIT",
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "https://github.com/lssm-tech/contractspec.git",
75
+ "directory": "packages/libs/bus"
76
+ },
77
+ "homepage": "https://contractspec.io"
78
+ }