@riaskov/nevo-messaging 1.0.1 → 1.1.2
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 +202 -67
- 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
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# Nevo Messaging
|
|
2
2
|
|
|
3
|
-
A powerful microservices messaging framework for NestJS 11+ with Kafka
|
|
3
|
+
A powerful microservices messaging framework for NestJS 11+ with multi-transport support (NATS, Kafka, Socket.IO, HTTP/SSE), designed for building scalable distributed systems with type-safe inter-service communication.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- 🚀 **Type-safe messaging** - Full TypeScript support with auto-completion
|
|
8
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
|
|
9
11
|
- 🎯 **Signal-based routing** - Declarative method mapping with `@Signal` decorator
|
|
10
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** - NATS, Kafka, Socket.IO, HTTP (SSE)
|
|
11
16
|
- 🔧 **Auto-configuration** - Automatic topic creation and client setup
|
|
12
17
|
- 🛡️ **Error handling** - Comprehensive error propagation and timeout management
|
|
13
18
|
- 📊 **BigInt support** - Native handling of large integers across services
|
|
@@ -23,23 +28,23 @@ 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
|
|
30
35
|
|
|
31
|
-
### 1. Basic Service Setup
|
|
36
|
+
### 1. Basic Service Setup (NATS)
|
|
32
37
|
|
|
33
38
|
Create a simple microservice that responds to messages:
|
|
34
39
|
|
|
35
40
|
```typescript
|
|
36
41
|
// user.service.ts
|
|
37
42
|
import { Injectable, Inject } from "@nestjs/common"
|
|
38
|
-
import {
|
|
43
|
+
import { NatsClientBase, NevoNatsClient } from "@riaskov/nevo-messaging"
|
|
39
44
|
|
|
40
45
|
@Injectable()
|
|
41
|
-
export class UserService extends
|
|
42
|
-
constructor(@Inject("
|
|
46
|
+
export class UserService extends NatsClientBase {
|
|
47
|
+
constructor(@Inject("NEVO_NATS_CLIENT") nevoClient: NevoNatsClient) {
|
|
43
48
|
super(nevoClient)
|
|
44
49
|
}
|
|
45
50
|
|
|
@@ -61,11 +66,11 @@ Map service methods to external signals using the `@Signal` decorator:
|
|
|
61
66
|
```typescript
|
|
62
67
|
// user.controller.ts
|
|
63
68
|
import { Controller, Inject } from "@nestjs/common"
|
|
64
|
-
import {
|
|
69
|
+
import { NatsSignalRouter, Signal } from "@riaskov/nevo-messaging"
|
|
65
70
|
import { UserService } from "./user.service"
|
|
66
71
|
|
|
67
72
|
@Controller()
|
|
68
|
-
@
|
|
73
|
+
@NatsSignalRouter([UserService])
|
|
69
74
|
export class UserController {
|
|
70
75
|
constructor(@Inject(UserService) private readonly userService: UserService) {}
|
|
71
76
|
|
|
@@ -79,13 +84,13 @@ export class UserController {
|
|
|
79
84
|
|
|
80
85
|
### 3. Module Configuration
|
|
81
86
|
|
|
82
|
-
Configure the module with
|
|
87
|
+
Configure the module with the NATS client:
|
|
83
88
|
|
|
84
89
|
```typescript
|
|
85
90
|
// user.module.ts
|
|
86
91
|
import { Module } from "@nestjs/common"
|
|
87
92
|
import { ConfigModule } from "@nestjs/config"
|
|
88
|
-
import {
|
|
93
|
+
import { createNevoNatsClient } from "@riaskov/nevo-messaging"
|
|
89
94
|
import { UserController } from "./user.controller"
|
|
90
95
|
import { UserService } from "./user.service"
|
|
91
96
|
|
|
@@ -94,8 +99,9 @@ import { UserService } from "./user.service"
|
|
|
94
99
|
controllers: [UserController],
|
|
95
100
|
providers: [
|
|
96
101
|
UserService,
|
|
97
|
-
|
|
98
|
-
clientIdPrefix: "user"
|
|
102
|
+
createNevoNatsClient(["COORDINATOR"], {
|
|
103
|
+
clientIdPrefix: "user",
|
|
104
|
+
servers: ["nats://127.0.0.1:4222"]
|
|
99
105
|
})
|
|
100
106
|
]
|
|
101
107
|
})
|
|
@@ -104,20 +110,20 @@ export class UserModule {}
|
|
|
104
110
|
|
|
105
111
|
### 4. Application Bootstrap
|
|
106
112
|
|
|
107
|
-
Start your
|
|
113
|
+
Start your service:
|
|
108
114
|
|
|
109
115
|
```typescript
|
|
110
116
|
// main.ts
|
|
111
|
-
import {
|
|
117
|
+
import { NestFactory } from "@nestjs/core"
|
|
112
118
|
import { AppModule } from "./app.module"
|
|
113
119
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}).then()
|
|
119
|
-
```
|
|
120
|
+
async function bootstrap() {
|
|
121
|
+
const app = await NestFactory.create(AppModule)
|
|
122
|
+
await app.listen(8086)
|
|
123
|
+
}
|
|
120
124
|
|
|
125
|
+
bootstrap()
|
|
126
|
+
```
|
|
121
127
|
## Core Concepts
|
|
122
128
|
|
|
123
129
|
### Signal Routing
|
|
@@ -144,6 +150,38 @@ Use for events and notifications:
|
|
|
144
150
|
await this.emit("notifications", "user.created", { userId: 123n, email: "user@example.com" })
|
|
145
151
|
```
|
|
146
152
|
|
|
153
|
+
#### Subscription Pattern (Publish/Subscribe)
|
|
154
|
+
Use when you want to receive updates without requesting:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const sub = await this.subscribe("user", "user.updated", { ack: true }, async (msg, ctx) => {
|
|
158
|
+
await ctx.ack()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
await sub.unsubscribe()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Publish updates:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
await this.publish("user", "user.updated", { userId: 123n })
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### Broadcast Pattern (System-Wide)
|
|
171
|
+
Send to everyone connected to the broker:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
await this.broadcast("system.status", { ok: true })
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Receive broadcast:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
await this.subscribe("__broadcast", "system.status", {}, (msg) => {
|
|
181
|
+
console.log("System status:", msg)
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
147
185
|
## Advanced Usage
|
|
148
186
|
|
|
149
187
|
### Parameter Transformation
|
|
@@ -282,6 +320,65 @@ export class UserService extends KafkaClientBase {
|
|
|
282
320
|
}
|
|
283
321
|
```
|
|
284
322
|
|
|
323
|
+
### Method Suggestions (Did You Mean)
|
|
324
|
+
|
|
325
|
+
If you call a method that doesn't exist, the framework returns a helpful error:
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
Invalid method name 'user.getByI', did you mean 'user.getById'?
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
This works for all transports.
|
|
332
|
+
|
|
333
|
+
### Exponential Backoff (Client-Side)
|
|
334
|
+
|
|
335
|
+
Clients apply a backoff for **in-flight requests** to avoid sending a duplicate query while the previous one is still being processed.
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
createNevoKafkaClient(["USER"], {
|
|
339
|
+
clientIdPrefix: "frontend",
|
|
340
|
+
backoff: {
|
|
341
|
+
enabled: true,
|
|
342
|
+
baseMs: 100,
|
|
343
|
+
maxMs: 2000,
|
|
344
|
+
maxAttempts: 0, // 0 = wait until slot is free
|
|
345
|
+
jitter: true
|
|
346
|
+
}
|
|
347
|
+
})
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
This prevents repeated sending of the same request while the service is busy (e.g., stopped on a breakpoint).
|
|
351
|
+
|
|
352
|
+
### Access Control (ACL)
|
|
353
|
+
|
|
354
|
+
Restrict who can read messages by topic + method + service:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
@KafkaSignalRouter([UserService], {
|
|
358
|
+
accessControl: {
|
|
359
|
+
rules: [
|
|
360
|
+
{ topic: "user-events", method: "*", allow: ["frontend", "coordinator"] },
|
|
361
|
+
{ topic: "user-events", method: "user.delete", deny: ["frontend"] }
|
|
362
|
+
],
|
|
363
|
+
logDenied: true
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
export class UserController {}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
By default, all services are allowed.
|
|
370
|
+
|
|
371
|
+
### Service Discovery (Registry Topic)
|
|
372
|
+
|
|
373
|
+
Each client sends heartbeats to `__nevo.discovery`. You can read the registry:
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
const services = this.getDiscoveredServices()
|
|
377
|
+
const isUserAvailable = this.isServiceAvailable("user")
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Discovery is enabled by default for Kafka/NATS. HTTP and Socket.IO discovery are available when enabled (HTTP requires `discoveryUrl`).
|
|
381
|
+
|
|
285
382
|
## Configuration
|
|
286
383
|
|
|
287
384
|
### Environment Variables
|
|
@@ -304,7 +401,12 @@ createNevoKafkaClient(["USER", "INVENTORY", "NOTIFICATIONS"], {
|
|
|
304
401
|
retryAttempts: 5,
|
|
305
402
|
brokerRetryTimeout: 2000,
|
|
306
403
|
timeoutMs: 25000,
|
|
307
|
-
debug: false
|
|
404
|
+
debug: false,
|
|
405
|
+
discovery: {
|
|
406
|
+
enabled: true,
|
|
407
|
+
heartbeatIntervalMs: 5000,
|
|
408
|
+
ttlMs: 15000
|
|
409
|
+
}
|
|
308
410
|
})
|
|
309
411
|
```
|
|
310
412
|
|
|
@@ -324,6 +426,74 @@ createKafkaMicroservice({
|
|
|
324
426
|
})
|
|
325
427
|
```
|
|
326
428
|
|
|
429
|
+
## Transports
|
|
430
|
+
|
|
431
|
+
| Transport | Patterns | Discovery | Infra | Notes |
|
|
432
|
+
| --- | --- | --- | --- | --- |
|
|
433
|
+
| NATS | query/emit/publish/subscribe/broadcast | on by default | NATS server | Lowest latency, simple ops |
|
|
434
|
+
| Kafka | query/emit/publish/subscribe/broadcast | on by default | Kafka broker | Durable log, topic setup |
|
|
435
|
+
| Socket.IO | query/emit/publish/subscribe/broadcast | optional | Socket.IO server | WebSocket-friendly apps |
|
|
436
|
+
| HTTP (SSE) | query/emit + SSE subscribe/broadcast | optional | HTTP server | Simple HTTP/SSE integration |
|
|
437
|
+
|
|
438
|
+
### NATS (priority)
|
|
439
|
+
Client factory:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
createNevoNatsClient(["USER", "COORDINATOR"], {
|
|
443
|
+
clientIdPrefix: "user",
|
|
444
|
+
servers: ["nats://127.0.0.1:4222"]
|
|
445
|
+
})
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Controller decorator:
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
@NatsSignalRouter([UserService])
|
|
452
|
+
export class UserController {}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Kafka
|
|
456
|
+
Use `createKafkaMicroservice` + `KafkaSignalRouter` as before.
|
|
457
|
+
|
|
458
|
+
### Socket.IO
|
|
459
|
+
Socket.IO server is started inside the router decorator:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
@SocketSignalRouter([UserService], { serviceName: "user", port: 8093 })
|
|
463
|
+
export class UserController {}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Client:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
createNevoSocketClient(
|
|
470
|
+
{ coordinator: "http://127.0.0.1:8094" },
|
|
471
|
+
{ clientIdPrefix: "user" }
|
|
472
|
+
)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### HTTP (SSE)
|
|
476
|
+
HTTP uses plain POST for `query/emit` and SSE for `subscribe`.
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
@HttpSignalRouter([UserService])
|
|
480
|
+
export class UserController {}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Include transport controller to enable SSE + publish endpoints:
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
controllers: [UserController, HttpTransportController]
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
Client:
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
createNevoHttpClient(
|
|
493
|
+
{ coordinator: "http://127.0.0.1:8091" },
|
|
494
|
+
{ clientIdPrefix: "user" }
|
|
495
|
+
)
|
|
496
|
+
```
|
|
327
497
|
## BigInt Support
|
|
328
498
|
|
|
329
499
|
The framework automatically handles BigInt serialization across service boundaries:
|
|
@@ -697,56 +867,21 @@ createNevoKafkaClient(["HIGH_VOLUME_SERVICE"], {
|
|
|
697
867
|
|
|
698
868
|
## API Reference
|
|
699
869
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
#### `@Signal(signalName, methodName?, paramTransformer?, resultTransformer?)`
|
|
703
|
-
|
|
704
|
-
Maps external signals to service methods.
|
|
705
|
-
|
|
706
|
-
**Parameters:**
|
|
707
|
-
- `signalName` (string): External signal identifier
|
|
708
|
-
- `methodName` (string, optional): Service method name (defaults to signalName)
|
|
709
|
-
- `paramTransformer` (function, optional): Transform incoming parameters
|
|
710
|
-
- `resultTransformer` (function, optional): Transform outgoing results
|
|
711
|
-
|
|
712
|
-
#### `@KafkaSignalRouter(serviceTypes, options?)`
|
|
713
|
-
|
|
714
|
-
Class decorator for signal routing setup.
|
|
715
|
-
|
|
716
|
-
**Parameters:**
|
|
717
|
-
- `serviceTypes` (Type<any> | Type<any>[]): Service classes to route to
|
|
718
|
-
- `options` (object, optional): Configuration options
|
|
719
|
-
|
|
720
|
-
### Classes
|
|
721
|
-
|
|
722
|
-
#### `KafkaClientBase`
|
|
723
|
-
|
|
724
|
-
Base class for services that need to communicate with other microservices.
|
|
725
|
-
|
|
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
|
|
730
|
-
|
|
731
|
-
#### `NevoKafkaClient`
|
|
732
|
-
|
|
733
|
-
Universal Kafka client for multi-service communication.
|
|
734
|
-
|
|
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
|
|
739
|
-
|
|
740
|
-
### Functions
|
|
870
|
+
See `API.md`.
|
|
741
871
|
|
|
742
|
-
|
|
872
|
+
## Examples
|
|
743
873
|
|
|
744
|
-
|
|
874
|
+
### NATS
|
|
875
|
+
- `examples/nats-user` - NATS request/response + publish/subscribe + broadcast
|
|
745
876
|
|
|
746
|
-
|
|
877
|
+
### Kafka
|
|
878
|
+
- `examples/user` - standard Kafka microservice
|
|
747
879
|
|
|
748
|
-
|
|
880
|
+
### Socket.IO
|
|
881
|
+
- `examples/socket-user` - Socket.IO transport with subscribe/broadcast
|
|
749
882
|
|
|
883
|
+
### HTTP (SSE)
|
|
884
|
+
- `examples/http-user` - HTTP query/emit + SSE subscribe + broadcast + discovery
|
|
750
885
|
## Troubleshooting
|
|
751
886
|
|
|
752
887
|
### Common Issues
|
|
@@ -828,4 +963,4 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
828
963
|
- Examples: Check the `examples/` directory for complete working examples
|
|
829
964
|
|
|
830
965
|
## Aux
|
|
831
|
-
There are many anys in core code - the simple temporary solution for changeable Nest.js microservices API.
|
|
966
|
+
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
|
}
|