@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.
- package/LICENSE +1 -1
- package/README.md +241 -56
- package/dist/common/access-control.d.ts +15 -0
- package/dist/common/access-control.js +94 -0
- package/dist/common/base.client.d.ts +7 -2
- package/dist/common/base.client.js +16 -2
- package/dist/common/base.controller.d.ts +6 -1
- package/dist/common/base.controller.js +68 -4
- package/dist/common/constants.d.ts +4 -0
- package/dist/common/constants.js +7 -0
- package/dist/common/discovery.d.ts +8 -0
- package/dist/common/discovery.js +35 -0
- package/dist/common/error-code.d.ts +2 -1
- package/dist/common/error-code.js +1 -0
- package/dist/common/error-messages.js +2 -1
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.js +3 -0
- package/dist/common/service-utils.d.ts +2 -0
- package/dist/common/service-utils.js +8 -0
- package/dist/common/types.d.ts +62 -0
- package/dist/signal-router.utils.d.ts +3 -1
- package/dist/signal-router.utils.js +70 -6
- package/dist/transports/http/http.client-base.d.ts +13 -0
- package/dist/transports/http/http.client-base.js +33 -0
- package/dist/transports/http/http.config.d.ts +8 -0
- package/dist/transports/http/http.config.js +16 -0
- package/dist/transports/http/http.signal-router.decorator.d.ts +3 -0
- package/dist/transports/http/http.signal-router.decorator.js +18 -0
- package/dist/transports/http/http.transport.controller.d.ts +21 -0
- package/dist/transports/http/http.transport.controller.js +114 -0
- package/dist/transports/http/index.d.ts +5 -0
- package/dist/transports/http/index.js +21 -0
- package/dist/transports/http/nevo-http.client.d.ts +54 -0
- package/dist/transports/http/nevo-http.client.js +280 -0
- package/dist/transports/index.d.ts +3 -0
- package/dist/transports/index.js +3 -0
- package/dist/transports/kafka/kafka.client-base.d.ts +5 -0
- package/dist/transports/kafka/kafka.client-base.js +15 -0
- package/dist/transports/kafka/kafka.config.d.ts +7 -0
- package/dist/transports/kafka/kafka.config.js +48 -2
- package/dist/transports/kafka/kafka.signal-router.decorator.js +2 -1
- package/dist/transports/kafka/nevo-kafka.client.d.ts +42 -0
- package/dist/transports/kafka/nevo-kafka.client.js +210 -4
- package/dist/transports/nats/index.d.ts +4 -0
- package/dist/transports/nats/index.js +20 -0
- package/dist/transports/nats/nats.client-base.d.ts +13 -0
- package/dist/transports/nats/nats.client-base.js +33 -0
- package/dist/transports/nats/nats.config.d.ts +8 -0
- package/dist/transports/nats/nats.config.js +16 -0
- package/dist/transports/nats/nats.signal-router.decorator.d.ts +6 -0
- package/dist/transports/nats/nats.signal-router.decorator.js +49 -0
- package/dist/transports/nats/nevo-nats.client.d.ts +55 -0
- package/dist/transports/nats/nevo-nats.client.js +210 -0
- package/dist/transports/socket-io/index.d.ts +4 -0
- package/dist/transports/socket-io/index.js +20 -0
- package/dist/transports/socket-io/nevo-socket.client.d.ts +50 -0
- package/dist/transports/socket-io/nevo-socket.client.js +202 -0
- package/dist/transports/socket-io/socket.client-base.d.ts +13 -0
- package/dist/transports/socket-io/socket.client-base.js +33 -0
- package/dist/transports/socket-io/socket.config.d.ts +8 -0
- package/dist/transports/socket-io/socket.config.js +16 -0
- package/dist/transports/socket-io/socket.signal-router.decorator.d.ts +13 -0
- package/dist/transports/socket-io/socket.signal-router.decorator.js +109 -0
- package/package.json +11 -5
package/LICENSE
CHANGED
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
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
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
|
-
##
|
|
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 = {
|
|
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
|
|
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,
|