@riaskov/nevo-messaging 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +241 -56
  3. package/dist/common/access-control.d.ts +15 -0
  4. package/dist/common/access-control.js +94 -0
  5. package/dist/common/base.client.d.ts +7 -2
  6. package/dist/common/base.client.js +16 -2
  7. package/dist/common/base.controller.d.ts +6 -1
  8. package/dist/common/base.controller.js +68 -4
  9. package/dist/common/constants.d.ts +4 -0
  10. package/dist/common/constants.js +7 -0
  11. package/dist/common/discovery.d.ts +8 -0
  12. package/dist/common/discovery.js +35 -0
  13. package/dist/common/error-code.d.ts +2 -1
  14. package/dist/common/error-code.js +1 -0
  15. package/dist/common/error-messages.js +2 -1
  16. package/dist/common/index.d.ts +3 -0
  17. package/dist/common/index.js +3 -0
  18. package/dist/common/service-utils.d.ts +2 -0
  19. package/dist/common/service-utils.js +8 -0
  20. package/dist/common/types.d.ts +62 -0
  21. package/dist/signal-router.utils.d.ts +3 -1
  22. package/dist/signal-router.utils.js +70 -6
  23. package/dist/transports/http/http.client-base.d.ts +13 -0
  24. package/dist/transports/http/http.client-base.js +33 -0
  25. package/dist/transports/http/http.config.d.ts +8 -0
  26. package/dist/transports/http/http.config.js +16 -0
  27. package/dist/transports/http/http.signal-router.decorator.d.ts +3 -0
  28. package/dist/transports/http/http.signal-router.decorator.js +18 -0
  29. package/dist/transports/http/http.transport.controller.d.ts +21 -0
  30. package/dist/transports/http/http.transport.controller.js +114 -0
  31. package/dist/transports/http/index.d.ts +5 -0
  32. package/dist/transports/http/index.js +21 -0
  33. package/dist/transports/http/nevo-http.client.d.ts +54 -0
  34. package/dist/transports/http/nevo-http.client.js +280 -0
  35. package/dist/transports/index.d.ts +3 -0
  36. package/dist/transports/index.js +3 -0
  37. package/dist/transports/kafka/kafka.client-base.d.ts +5 -0
  38. package/dist/transports/kafka/kafka.client-base.js +15 -0
  39. package/dist/transports/kafka/kafka.config.d.ts +7 -0
  40. package/dist/transports/kafka/kafka.config.js +48 -2
  41. package/dist/transports/kafka/kafka.signal-router.decorator.js +2 -1
  42. package/dist/transports/kafka/nevo-kafka.client.d.ts +42 -0
  43. package/dist/transports/kafka/nevo-kafka.client.js +210 -4
  44. package/dist/transports/nats/index.d.ts +4 -0
  45. package/dist/transports/nats/index.js +20 -0
  46. package/dist/transports/nats/nats.client-base.d.ts +13 -0
  47. package/dist/transports/nats/nats.client-base.js +33 -0
  48. package/dist/transports/nats/nats.config.d.ts +8 -0
  49. package/dist/transports/nats/nats.config.js +16 -0
  50. package/dist/transports/nats/nats.signal-router.decorator.d.ts +6 -0
  51. package/dist/transports/nats/nats.signal-router.decorator.js +49 -0
  52. package/dist/transports/nats/nevo-nats.client.d.ts +55 -0
  53. package/dist/transports/nats/nevo-nats.client.js +210 -0
  54. package/dist/transports/socket-io/index.d.ts +4 -0
  55. package/dist/transports/socket-io/index.js +20 -0
  56. package/dist/transports/socket-io/nevo-socket.client.d.ts +50 -0
  57. package/dist/transports/socket-io/nevo-socket.client.js +202 -0
  58. package/dist/transports/socket-io/socket.client-base.d.ts +13 -0
  59. package/dist/transports/socket-io/socket.client-base.js +33 -0
  60. package/dist/transports/socket-io/socket.config.d.ts +8 -0
  61. package/dist/transports/socket-io/socket.config.js +16 -0
  62. package/dist/transports/socket-io/socket.signal-router.decorator.d.ts +13 -0
  63. package/dist/transports/socket-io/socket.signal-router.decorator.js +109 -0
  64. package/package.json +11 -5
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Andrei Riaskóv
3
+ Copyright (c) 2026 Andrei Riaskóv <code@riaskov.com>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -4,15 +4,20 @@ A powerful microservices messaging framework for NestJS 11+ with Kafka 4+ transp
4
4
 
5
5
  ## Features
6
6
 
7
- - 🚀 **Type-safe messaging** - Full TypeScript support with auto-completion
8
- - 🔄 **Dual communication patterns** - Both request-response (query) and fire-and-forget (emit)
9
- - 🎯 **Signal-based routing** - Declarative method mapping with `@Signal` decorator
10
- - 📡 **Kafka transport** - Production-ready Apache Kafka integration
11
- - 🔧 **Auto-configuration** - Automatic topic creation and client setup
12
- - 🛡️ **Error handling** - Comprehensive error propagation and timeout management
13
- - 📊 **BigInt support** - Native handling of large integers across services
14
- - 🪝 **Lifecycle hooks** - Before/after message processing hooks
15
- - 🔍 **Debug mode** - Built-in logging for development and troubleshooting
7
+ - 🚀 **Type-safe messaging** - Full TypeScript support with auto-completion
8
+ - 🔄 **Dual communication patterns** - Both request-response (query) and fire-and-forget (emit)
9
+ - 📡 **Subscriptions** - Publish/subscribe updates without direct requests
10
+ - 📢 **Broadcast** - System-wide messages for all connected consumers
11
+ - 🎯 **Signal-based routing** - Declarative method mapping with `@Signal` decorator
12
+ - 📡 **Kafka transport** - Production-ready Apache Kafka integration
13
+ - 🧭 **Service discovery** - Heartbeat-based registry topic
14
+ - 🔐 **Access control** - Topic + method + service-level ACLs
15
+ - 🔌 **Multiple transports** - Kafka, NATS, HTTP (SSE), Socket.IO
16
+ - 🔧 **Auto-configuration** - Automatic topic creation and client setup
17
+ - 🛡️ **Error handling** - Comprehensive error propagation and timeout management
18
+ - 📊 **BigInt support** - Native handling of large integers across services
19
+ - 🪝 **Lifecycle hooks** - Before/after message processing hooks
20
+ - 🔍 **Debug mode** - Built-in logging for development and troubleshooting
16
21
 
17
22
  ## Installation
18
23
 
@@ -23,7 +28,7 @@ npm install @riaskov/nevo-messaging
23
28
  ### Peer Dependencies
24
29
 
25
30
  ```bash
26
- npm install @nestjs/common @nestjs/core @nestjs/microservices @nestjs/config @nestjs/platform-fastify kafkajs rxjs reflect-metadata
31
+ npm install @nestjs/common @nestjs/core @nestjs/microservices @nestjs/config @nestjs/platform-fastify kafkajs nats socket.io socket.io-client rxjs reflect-metadata
27
32
  ```
28
33
 
29
34
  ## Quick Start
@@ -137,12 +142,44 @@ Use for operations that need a response:
137
142
  const user = await this.query("user", "user.getById", { id: 123n })
138
143
  ```
139
144
 
140
- #### Emit Pattern (Fire-and-Forget)
141
- Use for events and notifications:
142
-
143
- ```typescript
144
- await this.emit("notifications", "user.created", { userId: 123n, email: "user@example.com" })
145
- ```
145
+ #### Emit Pattern (Fire-and-Forget)
146
+ Use for events and notifications:
147
+
148
+ ```typescript
149
+ await this.emit("notifications", "user.created", { userId: 123n, email: "user@example.com" })
150
+ ```
151
+
152
+ #### Subscription Pattern (Publish/Subscribe)
153
+ Use when you want to receive updates without requesting:
154
+
155
+ ```typescript
156
+ const sub = await this.subscribe("user", "user.updated", { ack: true }, async (msg, ctx) => {
157
+ await ctx.ack()
158
+ })
159
+
160
+ await sub.unsubscribe()
161
+ ```
162
+
163
+ Publish updates:
164
+
165
+ ```typescript
166
+ await this.publish("user", "user.updated", { userId: 123n })
167
+ ```
168
+
169
+ #### Broadcast Pattern (System-Wide)
170
+ Send to everyone connected to the broker:
171
+
172
+ ```typescript
173
+ await this.broadcast("system.status", { ok: true })
174
+ ```
175
+
176
+ Receive broadcast:
177
+
178
+ ```typescript
179
+ await this.subscribe("__broadcast", "system.status", {}, (msg) => {
180
+ console.log("System status:", msg)
181
+ })
182
+ ```
146
183
 
147
184
  ## Advanced Usage
148
185
 
@@ -258,7 +295,7 @@ export class UserController {
258
295
  }
259
296
  ```
260
297
 
261
- ### Error Handling
298
+ ### Error Handling
262
299
 
263
300
  The framework provides comprehensive error handling:
264
301
 
@@ -279,10 +316,69 @@ export class UserService extends KafkaClientBase {
279
316
 
280
317
  return user
281
318
  }
282
- }
283
- ```
284
-
285
- ## Configuration
319
+ }
320
+ ```
321
+
322
+ ### Method Suggestions (Did You Mean)
323
+
324
+ If you call a method that doesn't exist, the framework returns a helpful error:
325
+
326
+ ```
327
+ Invalid method name 'user.getByI', did you mean 'user.getById'?
328
+ ```
329
+
330
+ This works for all transports.
331
+
332
+ ### Exponential Backoff (Client-Side)
333
+
334
+ Clients apply a backoff for **in-flight requests** to avoid sending a duplicate query while the previous one is still being processed.
335
+
336
+ ```typescript
337
+ createNevoKafkaClient(["USER"], {
338
+ clientIdPrefix: "frontend",
339
+ backoff: {
340
+ enabled: true,
341
+ baseMs: 100,
342
+ maxMs: 2000,
343
+ maxAttempts: 0, // 0 = wait until slot is free
344
+ jitter: true
345
+ }
346
+ })
347
+ ```
348
+
349
+ This prevents repeated sending of the same request while the service is busy (e.g., stopped on a breakpoint).
350
+
351
+ ### Access Control (ACL)
352
+
353
+ Restrict who can read messages by topic + method + service:
354
+
355
+ ```typescript
356
+ @KafkaSignalRouter([UserService], {
357
+ accessControl: {
358
+ rules: [
359
+ { topic: "user-events", method: "*", allow: ["frontend", "coordinator"] },
360
+ { topic: "user-events", method: "user.delete", deny: ["frontend"] }
361
+ ],
362
+ logDenied: true
363
+ }
364
+ })
365
+ export class UserController {}
366
+ ```
367
+
368
+ By default, all services are allowed.
369
+
370
+ ### Service Discovery (Registry Topic)
371
+
372
+ Each client sends heartbeats to `__nevo.discovery`. You can read the registry:
373
+
374
+ ```typescript
375
+ const services = this.getDiscoveredServices()
376
+ const isUserAvailable = this.isServiceAvailable("user")
377
+ ```
378
+
379
+ Discovery is enabled by default for Kafka/NATS. HTTP and Socket.IO discovery are currently disabled.
380
+
381
+ ## Configuration
286
382
 
287
383
  ### Environment Variables
288
384
 
@@ -296,35 +392,102 @@ NODE_ENV=production
296
392
  ### Kafka Client Options
297
393
 
298
394
  ```typescript
299
- createNevoKafkaClient(["USER", "INVENTORY", "NOTIFICATIONS"], {
300
- clientIdPrefix: "order-service",
301
- groupIdPrefix: "order-consumer",
302
- sessionTimeout: 30000,
303
- allowAutoTopicCreation: true,
304
- retryAttempts: 5,
305
- brokerRetryTimeout: 2000,
306
- timeoutMs: 25000,
307
- debug: false
308
- })
309
- ```
395
+ createNevoKafkaClient(["USER", "INVENTORY", "NOTIFICATIONS"], {
396
+ clientIdPrefix: "order-service",
397
+ groupIdPrefix: "order-consumer",
398
+ sessionTimeout: 30000,
399
+ allowAutoTopicCreation: true,
400
+ retryAttempts: 5,
401
+ brokerRetryTimeout: 2000,
402
+ timeoutMs: 25000,
403
+ debug: false,
404
+ discovery: {
405
+ enabled: true,
406
+ heartbeatIntervalMs: 5000,
407
+ ttlMs: 15000
408
+ }
409
+ })
410
+ ```
310
411
 
311
412
  ### Microservice Startup Options
312
413
 
313
414
  ```typescript
314
- createKafkaMicroservice({
315
- microserviceName: "user",
316
- module: AppModule,
317
- port: 8086,
318
- host: "0.0.0.0",
319
- debug: true,
320
- onInit: async (app) => {
321
- // Custom initialization logic
322
- await app.get(DatabaseService).runMigrations()
323
- }
324
- })
325
- ```
326
-
327
- ## BigInt Support
415
+ createKafkaMicroservice({
416
+ microserviceName: "user",
417
+ module: AppModule,
418
+ port: 8086,
419
+ host: "0.0.0.0",
420
+ debug: true,
421
+ onInit: async (app) => {
422
+ // Custom initialization logic
423
+ await app.get(DatabaseService).runMigrations()
424
+ }
425
+ })
426
+ ```
427
+
428
+ ## Transports
429
+
430
+ ### Kafka (default)
431
+ Use `createKafkaMicroservice` + `KafkaSignalRouter` as before.
432
+
433
+ ### NATS
434
+ Client factory:
435
+
436
+ ```typescript
437
+ createNevoNatsClient(["USER", "COORDINATOR"], {
438
+ clientIdPrefix: "user",
439
+ servers: ["nats://127.0.0.1:4222"]
440
+ })
441
+ ```
442
+
443
+ Controller decorator:
444
+
445
+ ```typescript
446
+ @NatsSignalRouter([UserService])
447
+ export class UserController {}
448
+ ```
449
+
450
+ ### HTTP (SSE)
451
+ HTTP uses plain POST for `query/emit` and SSE for `subscribe`.
452
+
453
+ ```typescript
454
+ @HttpSignalRouter([UserService])
455
+ export class UserController {}
456
+ ```
457
+
458
+ Include transport controller to enable SSE + publish endpoints:
459
+
460
+ ```typescript
461
+ controllers: [UserController, HttpTransportController]
462
+ ```
463
+
464
+ Client:
465
+
466
+ ```typescript
467
+ createNevoHttpClient(
468
+ { coordinator: "http://127.0.0.1:8091" },
469
+ { clientIdPrefix: "user" }
470
+ )
471
+ ```
472
+
473
+ ### Socket.IO
474
+ Socket.IO server is started inside the router decorator:
475
+
476
+ ```typescript
477
+ @SocketSignalRouter([UserService], { serviceName: "user", port: 8093 })
478
+ export class UserController {}
479
+ ```
480
+
481
+ Client:
482
+
483
+ ```typescript
484
+ createNevoSocketClient(
485
+ { coordinator: "http://127.0.0.1:8094" },
486
+ { clientIdPrefix: "user" }
487
+ )
488
+ ```
489
+
490
+ ## BigInt Support
328
491
 
329
492
  The framework automatically handles BigInt serialization across service boundaries:
330
493
 
@@ -723,19 +886,27 @@ Class decorator for signal routing setup.
723
886
 
724
887
  Base class for services that need to communicate with other microservices.
725
888
 
726
- **Methods:**
727
- - `query<T>(serviceName, method, params): Promise<T>` - Request-response communication
728
- - `emit(serviceName, method, params): Promise<void>` - Fire-and-forget communication
729
- - `getAvailableServices(): string[]` - List registered services
889
+ **Methods:**
890
+ - `query<T>(serviceName, method, params): Promise<T>` - Request-response communication
891
+ - `emit(serviceName, method, params): Promise<void>` - Fire-and-forget communication
892
+ - `publish(serviceName, method, params): Promise<void>` - Publish to subscriptions
893
+ - `subscribe(serviceName, method, options, handler): Promise<Subscription>` - Subscribe to updates
894
+ - `broadcast(method, params): Promise<void>` - System-wide broadcast
895
+ - `getAvailableServices(): string[]` - List registered services
896
+ - `getDiscoveredServices(): DiscoveryEntry[]` - Service registry snapshot
897
+ - `isServiceAvailable(serviceName): boolean` - Availability check
730
898
 
731
899
  #### `NevoKafkaClient`
732
900
 
733
901
  Universal Kafka client for multi-service communication.
734
902
 
735
- **Methods:**
736
- - `query<T>(serviceName, method, params): Promise<T>` - Send query to service
737
- - `emit(serviceName, method, params): Promise<void>` - Emit event to service
738
- - `getAvailableServices(): string[]` - Get list of available services
903
+ **Methods:**
904
+ - `query<T>(serviceName, method, params): Promise<T>` - Send query to service
905
+ - `emit(serviceName, method, params): Promise<void>` - Emit event to service
906
+ - `publish(serviceName, method, params): Promise<void>` - Publish to subscriptions
907
+ - `subscribe(serviceName, method, options, handler): Promise<Subscription>` - Subscribe to updates
908
+ - `broadcast(method, params): Promise<void>` - System-wide broadcast
909
+ - `getAvailableServices(): string[]` - Get list of available services
739
910
 
740
911
  ### Functions
741
912
 
@@ -747,7 +918,21 @@ Factory function for creating Kafka client providers.
747
918
 
748
919
  Bootstrap function for starting NestJS microservices with Kafka transport.
749
920
 
750
- ## Troubleshooting
921
+ ## Examples
922
+
923
+ ### Kafka
924
+ - `examples/user` - standard Kafka microservice
925
+
926
+ ### NATS
927
+ - `examples/nats-user` - NATS request/response + publish/subscribe + broadcast
928
+
929
+ ### HTTP (SSE)
930
+ - `examples/http-user` - HTTP query/emit + SSE subscribe + broadcast + discovery
931
+
932
+ ### Socket.IO
933
+ - `examples/socket-user` - Socket.IO transport with subscribe/broadcast
934
+
935
+ ## Troubleshooting
751
936
 
752
937
  ### Common Issues
753
938
 
@@ -828,4 +1013,4 @@ MIT License - see [LICENSE](LICENSE) file for details.
828
1013
  - Examples: Check the `examples/` directory for complete working examples
829
1014
 
830
1015
  ## Aux
831
- There are many anys in core code - the simple temporary solution for changeable Nest.js microservices API.
1016
+ There are many anys in core code - the simple temporary solution for changeable Nest.js microservices API.
@@ -0,0 +1,15 @@
1
+ import { AccessControlConfig, MessageMeta } from "./types";
2
+ import { ErrorCode } from "./error-code";
3
+ export declare function extractCallerService(meta?: MessageMeta): string | undefined;
4
+ export declare function isAccessAllowed(config: AccessControlConfig | undefined, topic: string, method: string, callerService: string | undefined): boolean;
5
+ export declare function logAccessDenied(config: AccessControlConfig | undefined, details: Record<string, unknown>): void;
6
+ export declare function createAccessDeniedError(method: string, serviceName: string, callerService?: string): {
7
+ code: ErrorCode;
8
+ message: string;
9
+ details: {
10
+ method: string;
11
+ serviceName: string;
12
+ callerService: string | undefined;
13
+ };
14
+ service: string;
15
+ };
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractCallerService = extractCallerService;
4
+ exports.isAccessAllowed = isAccessAllowed;
5
+ exports.logAccessDenied = logAccessDenied;
6
+ exports.createAccessDeniedError = createAccessDeniedError;
7
+ const error_code_1 = require("./error-code");
8
+ function decodeJwtPayload(token) {
9
+ const parts = token.split(".");
10
+ if (parts.length < 2) {
11
+ return null;
12
+ }
13
+ try {
14
+ const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
15
+ const padded = payload.padEnd(payload.length + ((4 - (payload.length % 4)) % 4), "=");
16
+ const json = Buffer.from(padded, "base64").toString("utf8");
17
+ return JSON.parse(json);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function extractCallerService(meta) {
24
+ if (meta?.service) {
25
+ return meta.service;
26
+ }
27
+ const token = meta?.auth?.token;
28
+ if (!token) {
29
+ return undefined;
30
+ }
31
+ const payload = decodeJwtPayload(token);
32
+ if (!payload) {
33
+ return undefined;
34
+ }
35
+ const serviceName = (payload["serviceName"] || payload["service"] || payload["svc"] || payload["sub"]);
36
+ return serviceName;
37
+ }
38
+ function matchPattern(pattern, value) {
39
+ if (!pattern || pattern === "*") {
40
+ return true;
41
+ }
42
+ return pattern === value;
43
+ }
44
+ function listHasValue(list, value) {
45
+ if (!list || list.length === 0) {
46
+ return false;
47
+ }
48
+ if (list.includes("*")) {
49
+ return true;
50
+ }
51
+ if (!value) {
52
+ return false;
53
+ }
54
+ return list.includes(value);
55
+ }
56
+ function isAccessAllowed(config, topic, method, callerService) {
57
+ if (!config) {
58
+ return true;
59
+ }
60
+ const allowAllByDefault = config.allowAllByDefault !== false;
61
+ const rules = config.rules || [];
62
+ let matched = false;
63
+ for (const rule of rules) {
64
+ if (!matchPattern(rule.topic, topic) || !matchPattern(rule.method, method)) {
65
+ continue;
66
+ }
67
+ matched = true;
68
+ if (listHasValue(rule.deny, callerService)) {
69
+ return false;
70
+ }
71
+ if (rule.allow && rule.allow.length > 0) {
72
+ return listHasValue(rule.allow, callerService);
73
+ }
74
+ }
75
+ return matched ? allowAllByDefault : allowAllByDefault;
76
+ }
77
+ function logAccessDenied(config, details) {
78
+ if (config?.logDenied === false) {
79
+ return;
80
+ }
81
+ console.warn("[NevoMessaging][ACL] Access denied:", details);
82
+ }
83
+ function createAccessDeniedError(method, serviceName, callerService) {
84
+ return {
85
+ code: error_code_1.ErrorCode.UNAUTHORIZED,
86
+ message: "Access denied",
87
+ details: {
88
+ method,
89
+ serviceName,
90
+ callerService
91
+ },
92
+ service: serviceName
93
+ };
94
+ }
@@ -1,12 +1,17 @@
1
- import { MessagePayload, MicroserviceConfig, TransportClientOptions } from "./types";
1
+ import { MessagePayload, MicroserviceConfig, TransportClientOptions, MessageType, Subscription, SubscriptionOptions, SubscriptionContext } from "./types";
2
2
  export declare abstract class BaseMessagingClient {
3
3
  protected readonly options: TransportClientOptions;
4
4
  protected readonly microservices: Map<string, string>;
5
+ protected readonly serviceName?: string;
6
+ protected readonly authToken?: string;
5
7
  protected constructor(options?: TransportClientOptions);
6
8
  protected registerMicroservices(configs: MicroserviceConfig[]): void;
7
9
  protected query<T = any>(serviceName: string, method: string, params: any): Promise<T>;
8
10
  protected emit(serviceName: string, method: string, params: any): Promise<void>;
9
- protected createMessagePayload(method: string, params: any): MessagePayload;
11
+ protected createMessagePayload(method: string, params: any, type?: MessageType): MessagePayload;
10
12
  protected abstract _queryMicroservice<T>(clientName: string, method: string, params: any): Promise<T>;
11
13
  protected abstract _emitToMicroservice(clientName: string, method: string, params: any): Promise<void>;
14
+ protected abstract _publishToMicroservice(clientName: string, method: string, params: any): Promise<void>;
15
+ protected abstract _broadcast(method: string, params: any): Promise<void>;
16
+ protected abstract _subscribeToMicroservice<T>(clientName: string, method: string, options: SubscriptionOptions | undefined, handler: (data: T, context: SubscriptionContext) => Promise<void> | void): Promise<Subscription>;
12
17
  }
@@ -12,6 +12,8 @@ class BaseMessagingClient {
12
12
  debug: false,
13
13
  ...options
14
14
  };
15
+ this.serviceName = this.options.serviceName || this.options.clientId;
16
+ this.authToken = this.options.authToken;
15
17
  }
16
18
  registerMicroservices(configs) {
17
19
  for (const config of configs) {
@@ -38,9 +40,21 @@ class BaseMessagingClient {
38
40
  }
39
41
  return this._emitToMicroservice(clientName, method, params);
40
42
  }
41
- createMessagePayload(method, params) {
43
+ createMessagePayload(method, params, type = "emit") {
42
44
  const uuid = (0, node_crypto_1.randomUUID)();
43
- const request = { uuid, method, params };
45
+ const request = {
46
+ uuid,
47
+ method,
48
+ params,
49
+ meta: {
50
+ type,
51
+ service: this.serviceName,
52
+ ts: Date.now(),
53
+ auth: {
54
+ token: this.authToken
55
+ }
56
+ }
57
+ };
44
58
  return {
45
59
  key: uuid,
46
60
  value: JSON.stringify(request)
@@ -1,4 +1,4 @@
1
- import { AfterHook, BeforeHook, ServiceMethodHandler, ServiceMethodMapping, SystemAfterHook, SystemBeforeHook, MessageResponse } from "./types";
1
+ import { AfterHook, BeforeHook, ServiceMethodHandler, ServiceMethodMapping, SystemAfterHook, SystemBeforeHook, MessageResponse, AccessControlConfig, MessageMeta } from "./types";
2
2
  export declare abstract class BaseMessageController {
3
3
  protected readonly methodRegistry: ServiceMethodMapping;
4
4
  serviceInstances: any[];
@@ -8,14 +8,18 @@ export declare abstract class BaseMessageController {
8
8
  protected readonly systemBeforeHook: SystemBeforeHook;
9
9
  protected readonly systemAfterHook: SystemAfterHook;
10
10
  protected readonly debug: boolean;
11
+ protected readonly accessControl?: AccessControlConfig;
11
12
  protected constructor(serviceName: string, serviceInstances: any[], methodHandlers: ServiceMethodMapping, options?: {
12
13
  onBefore?: BeforeHook;
13
14
  onAfter?: AfterHook;
14
15
  debug?: boolean;
16
+ accessControl?: AccessControlConfig;
15
17
  });
16
18
  protected registerMethodHandlers(handlers: ServiceMethodMapping): void;
17
19
  protected findServiceInstance(methodName: string): any;
18
20
  protected executeHandler(handler: ServiceMethodHandler, params: unknown): Promise<unknown>;
21
+ private suggestClosestMethod;
22
+ private levenshteinDistance;
19
23
  protected formatResult(result: unknown): Promise<unknown>;
20
24
  protected createErrorResponse(uuid: string, method: string, error: any): MessageResponse;
21
25
  processMessage(data: any): Promise<MessageResponse>;
@@ -23,6 +27,7 @@ export declare abstract class BaseMessageController {
23
27
  method: string;
24
28
  uuid: string;
25
29
  params: any;
30
+ meta?: MessageMeta;
26
31
  };
27
32
  abstract handleMessage(data: any): Promise<MessageResponse>;
28
33
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseMessageController = void 0;
4
4
  const _1 = require("./");
5
+ const access_control_1 = require("./access-control");
5
6
  class BaseMessageController {
6
7
  constructor(serviceName, serviceInstances, methodHandlers, options) {
7
8
  this.methodRegistry = {};
@@ -11,6 +12,7 @@ class BaseMessageController {
11
12
  this.beforeHook = options?.onBefore;
12
13
  this.afterHook = options?.onAfter;
13
14
  this.debug = options?.debug || false;
15
+ this.accessControl = options?.accessControl;
14
16
  this.systemBeforeHook = (context) => {
15
17
  if (this.debug) {
16
18
  console.log(`[${this.constructor.name}] Received:`, {
@@ -67,6 +69,50 @@ class BaseMessageController {
67
69
  throw error;
68
70
  }
69
71
  }
72
+ suggestClosestMethod(method) {
73
+ const candidates = Object.keys(this.methodRegistry || {});
74
+ if (!candidates.length) {
75
+ return null;
76
+ }
77
+ const normalized = method.toLowerCase();
78
+ let best = null;
79
+ for (const candidate of candidates) {
80
+ const score = this.levenshteinDistance(normalized, candidate.toLowerCase());
81
+ if (!best || score < best.score) {
82
+ best = { name: candidate, score };
83
+ }
84
+ }
85
+ if (!best) {
86
+ return null;
87
+ }
88
+ const threshold = Math.max(2, Math.floor(method.length * 0.4));
89
+ return best.score <= threshold ? best.name : null;
90
+ }
91
+ levenshteinDistance(a, b) {
92
+ if (a === b) {
93
+ return 0;
94
+ }
95
+ if (!a.length) {
96
+ return b.length;
97
+ }
98
+ if (!b.length) {
99
+ return a.length;
100
+ }
101
+ const matrix = Array.from({ length: a.length + 1 }, () => []);
102
+ for (let i = 0; i <= a.length; i++) {
103
+ matrix[i][0] = i;
104
+ }
105
+ for (let j = 0; j <= b.length; j++) {
106
+ matrix[0][j] = j;
107
+ }
108
+ for (let i = 1; i <= a.length; i++) {
109
+ for (let j = 1; j <= b.length; j++) {
110
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
111
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
112
+ }
113
+ }
114
+ return matrix[a.length][b.length];
115
+ }
70
116
  async formatResult(result) {
71
117
  if (result instanceof Promise) {
72
118
  result = await result;
@@ -100,12 +146,13 @@ class BaseMessageController {
100
146
  };
101
147
  }
102
148
  async processMessage(data) {
103
- const { method, uuid, params } = this.extractMessageData(data);
149
+ const { method, uuid, params, meta } = this.extractMessageData(data);
104
150
  const baseContext = {
105
151
  method,
106
152
  serviceName: this.serviceName,
107
153
  uuid,
108
- rawData: data
154
+ rawData: data,
155
+ meta
109
156
  };
110
157
  const requestContext = {
111
158
  ...baseContext,
@@ -120,10 +167,26 @@ class BaseMessageController {
120
167
  processedParams = hookResult;
121
168
  }
122
169
  }
170
+ const callerService = (0, access_control_1.extractCallerService)(meta);
171
+ const topic = this.serviceName;
172
+ if (!(0, access_control_1.isAccessAllowed)(this.accessControl, topic, method, callerService)) {
173
+ (0, access_control_1.logAccessDenied)(this.accessControl, { topic, method, serviceName: this.serviceName, callerService });
174
+ return {
175
+ uuid,
176
+ method,
177
+ params: {
178
+ result: "error",
179
+ error: (0, access_control_1.createAccessDeniedError)(method, this.serviceName, callerService)
180
+ },
181
+ meta
182
+ };
183
+ }
123
184
  const handler = this.methodRegistry[method];
124
185
  if (!handler) {
186
+ const suggestion = this.suggestClosestMethod(method);
187
+ const message = suggestion ? `Invalid method name '${method}', did you mean '${suggestion}'?` : `Method handler not found: ${method}`;
125
188
  throw new _1.MessagingError(_1.ErrorCode.UNKNOWN, {
126
- message: `Method handler not found: ${method}`
189
+ message
127
190
  });
128
191
  }
129
192
  const result = await this.executeHandler(handler, processedParams);
@@ -131,7 +194,8 @@ class BaseMessageController {
131
194
  let response = {
132
195
  uuid,
133
196
  method,
134
- params: { result: formattedResult }
197
+ params: { result: formattedResult },
198
+ meta
135
199
  };
136
200
  const responseContext = {
137
201
  ...baseContext,