geonix 1.23.8 → 1.30.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.md +1 -1
- package/README.md +348 -4
- package/exports.js +0 -2
- package/index.d.ts +292 -237
- package/package.json +11 -10
- package/src/Codec.js +20 -7
- package/src/Connection.js +94 -40
- package/src/Crypto.js +103 -0
- package/src/Gateway.js +146 -70
- package/src/Logger.js +90 -9
- package/src/Registry.js +127 -15
- package/src/Remote.js +15 -6
- package/src/Request.js +117 -80
- package/src/RequestOptions.js +11 -8
- package/src/Service.js +128 -92
- package/src/Stream.js +69 -15
- package/src/Util.js +192 -158
- package/src/WebServer.js +18 -10
- package/.claude/settings.local.json +0 -10
- package/PROJECT.md +0 -164
- package/REVIEW.md +0 -372
package/PROJECT.md
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
# Geonix
|
|
2
|
-
|
|
3
|
-
## What It Is
|
|
4
|
-
|
|
5
|
-
Geonix is a Node.js microservices framework built on top of [NATS](https://nats.io/). It allows you to write self-announcing services as plain JavaScript classes, with HTTP endpoints defined as specially-named class methods. A gateway component dynamically discovers services and routes HTTP/WebSocket traffic to them.
|
|
6
|
-
|
|
7
|
-
Published on npm as `geonix`, version 1.23.6. MIT licensed. Author: Davor Tarandek, Tria d.o.o.
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Core Concept
|
|
12
|
-
|
|
13
|
-
You write a service like this:
|
|
14
|
-
|
|
15
|
-
```js
|
|
16
|
-
import { Service } from "geonix";
|
|
17
|
-
|
|
18
|
-
class MyService extends Service {
|
|
19
|
-
"GET /hello"(req, res) {
|
|
20
|
-
res.send("Hello World");
|
|
21
|
-
}
|
|
22
|
-
someRpcMethod(input) {
|
|
23
|
-
return input.toUpperCase();
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
MyService.start();
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
That's it. The service:
|
|
31
|
-
- Starts an embedded Express HTTP server on a random port
|
|
32
|
-
- Announces itself on NATS every second (a "beacon")
|
|
33
|
-
- Accepts remote procedure calls over NATS
|
|
34
|
-
- Can be discovered and called by the Gateway or other services
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## Architecture
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
Client (HTTP/WS)
|
|
42
|
-
│
|
|
43
|
-
[Gateway] ── discovers services via NATS beacons
|
|
44
|
-
│
|
|
45
|
-
┌────┴────┐
|
|
46
|
-
│ Service │ ── embedded Express HTTP server
|
|
47
|
-
│ Service │ ── announces on NATS gx2.beacon every 1s
|
|
48
|
-
│ Service │
|
|
49
|
-
└─────────┘
|
|
50
|
-
│
|
|
51
|
-
[NATS] ── message bus for beacons, RPC, streaming
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Components
|
|
55
|
-
|
|
56
|
-
| File | Role |
|
|
57
|
-
|------|------|
|
|
58
|
-
| `src/Service.js` | Base class for all services. Manages HTTP endpoints, NATS RPC listeners, beacons. |
|
|
59
|
-
| `src/Gateway.js` | HTTP reverse proxy. Subscribes to beacons, builds a dynamic Express router. |
|
|
60
|
-
| `src/Connection.js` | Singleton NATS connection wrapper. Supports multiple connections with round-robin. |
|
|
61
|
-
| `src/Registry.js` | Maintains a live map of available services from beacon messages. Entries expire after 5s. |
|
|
62
|
-
| `src/Request.js` | Sends RPC calls to services over NATS. Waits up to 5 minutes for service to appear. |
|
|
63
|
-
| `src/Remote.js` | Proxy-based sugar: `Remote("ServiceName").methodName(args)` → RPC call. |
|
|
64
|
-
| `src/Stream.js` | Wraps large payloads as streamable objects. Can stream over HTTP or NATS. |
|
|
65
|
-
| `src/WebServer.js` | Shared internal Express instance for all services in the same process. |
|
|
66
|
-
| `src/Util.js` | Utilities: multipart parser, HTTP proxy, TCP server creation, ID generation, deep merge. |
|
|
67
|
-
| `src/Codec.js` | NATS JSON codec wrapper. |
|
|
68
|
-
| `src/Logger.js` | Simple timestamped console logger. |
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## Key Mechanisms
|
|
73
|
-
|
|
74
|
-
### Service Discovery
|
|
75
|
-
- Services publish a beacon on `gx2.beacon` every 1 second
|
|
76
|
-
- The `Registry` class subscribes to beacons and tracks all live services
|
|
77
|
-
- Entries expire after 5 seconds without a beacon
|
|
78
|
-
- Each service has: instance ID, name, version, method list, network addresses
|
|
79
|
-
|
|
80
|
-
### Routing (Gateway)
|
|
81
|
-
- On startup, the Gateway subscribes to all beacons
|
|
82
|
-
- Every second it checks for newly added or removed services
|
|
83
|
-
- When services change, it rebuilds the Express router with updated routes
|
|
84
|
-
- If the backend service is directly reachable via HTTP (same network), it proxies directly
|
|
85
|
-
- If not, it tunnels HTTP over NATS using a TCP-over-NATS bridge
|
|
86
|
-
|
|
87
|
-
### RPC Calls
|
|
88
|
-
- `Request("ServiceName", "methodName", [args])` resolves the service identifier from the registry
|
|
89
|
-
- It publishes to `gx2.service.<sha256(identifier)>` with a reply-to subject
|
|
90
|
-
- The service picks it up from its queue subscription and responds
|
|
91
|
-
- Load balancing is built-in: multiple instances of `ServiceName@version` use NATS queue groups
|
|
92
|
-
|
|
93
|
-
### Streaming
|
|
94
|
-
- When a payload exceeds NATS max payload size (~512KB), it's automatically converted to a `Stream` object
|
|
95
|
-
- The `Stream` object contains `{ $: "stream", id, a: [addresses] }`
|
|
96
|
-
- Consumers use `getReadable()` to retrieve the stream via HTTP or NATS
|
|
97
|
-
|
|
98
|
-
### Versioning
|
|
99
|
-
- Services can specify a version via `process.env.VERSION`, options, or `this.version`
|
|
100
|
-
- The gateway does semver-aware routing and always routes to the highest version
|
|
101
|
-
- RPC calls can target a specific version: `Request("MyService@1.2.x", "method", [])`
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## Endpoint Declaration Syntax
|
|
106
|
-
|
|
107
|
-
Methods on a Service class are matched against this pattern:
|
|
108
|
-
```
|
|
109
|
-
[options|]VERB /path
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Examples:
|
|
113
|
-
- `"GET /api/users"` — HTTP GET endpoint
|
|
114
|
-
- `"POST /upload"` — HTTP POST endpoint
|
|
115
|
-
- `"WS /chat"` — WebSocket endpoint
|
|
116
|
-
- `"SUB my.nats.subject"` — NATS subscription handler
|
|
117
|
-
- `"order=10|GET /api"` — HTTP GET with routing priority 10
|
|
118
|
-
|
|
119
|
-
---
|
|
120
|
-
|
|
121
|
-
## Transport
|
|
122
|
-
|
|
123
|
-
- **NATS** (required): service discovery, RPC, streaming fallback
|
|
124
|
-
- **HTTP**: direct service-to-service and gateway-to-service communication
|
|
125
|
-
- **TCP-over-NATS**: tunnel for HTTP traffic when direct addressing is not possible
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## Built-in Service Methods
|
|
130
|
-
|
|
131
|
-
All services expose these internal methods (callable over NATS, not over HTTP):
|
|
132
|
-
|
|
133
|
-
| Method | Returns |
|
|
134
|
-
|--------|---------|
|
|
135
|
-
| `$createConnection(streamId)` | Opens a TCP tunnel for proxying HTTP |
|
|
136
|
-
| `$getEnv()` | System info, Node.js version, `process.env`, memory/CPU usage |
|
|
137
|
-
| `$getServiceInfo()` | Service metadata (name, version, methods, addresses) |
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
## Configuration
|
|
142
|
-
|
|
143
|
-
| Env Variable | Default | Description |
|
|
144
|
-
|---|---|---|
|
|
145
|
-
| `TRANSPORT` | `nats://localhost` | NATS server URL |
|
|
146
|
-
| `PORT` | `8080` | Gateway listen port |
|
|
147
|
-
| `LOCAL_PORT` | random | Force service HTTP server to a specific port |
|
|
148
|
-
| `VERSION` | auto-generated | Service version |
|
|
149
|
-
| `NODE_ENV` | — | Set to `production` to disable debug endpoint |
|
|
150
|
-
| `TRANSPORT_DEBUG` | `false` | Enable NATS debug logging |
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
## Dependencies
|
|
155
|
-
|
|
156
|
-
| Package | Purpose |
|
|
157
|
-
|---------|---------|
|
|
158
|
-
| `express` | HTTP server |
|
|
159
|
-
| `express-ws` | WebSocket support for Express |
|
|
160
|
-
| `express-async-errors` | Async error handling for Express |
|
|
161
|
-
| `nats` | NATS client |
|
|
162
|
-
| `cookie-parser` | Cookie parsing middleware |
|
|
163
|
-
| `semver` | Semantic versioning comparisons |
|
|
164
|
-
| `ws` | WebSocket client (for gateway WS proxy) |
|
package/REVIEW.md
DELETED
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
# Code Review: Geonix
|
|
2
|
-
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
## Honest Opinion on the Idea
|
|
6
|
-
|
|
7
|
-
The core idea is clever and genuinely useful: service discovery + RPC + HTTP routing in one framework, with zero external service registry. Classes become services, methods become endpoints, and everything discovers each other automatically via NATS beacons. For small teams deploying Node.js microservices on the same NATS network, this could eliminate a lot of boilerplate.
|
|
8
|
-
|
|
9
|
-
The design does lean heavily on some opinionated tradeoffs:
|
|
10
|
-
|
|
11
|
-
- **The method-name-as-endpoint convention** (`"GET /path"`) is novel but weird. It works in JS, but it's surprising to most developers, not statically typed, and can silently misbehave when method names overlap or are substrings of each other (see bugs below). It also means your service's public interface is entangled with its internal class structure.
|
|
12
|
-
- **Using NATS as both the transport and the discovery layer** is clever but means you're fully coupled to NATS. If NATS goes down, everything stops. There's no degraded-mode operation.
|
|
13
|
-
- **The gateway rebuilding its routing table from live beacons** is interesting but creates a window during which newly deployed services are unreachable, and destroyed services may still receive traffic.
|
|
14
|
-
- **Trust model is zero**: any process on the NATS bus can call any method on any service, including methods that expose `process.env`. This is the biggest design-level concern.
|
|
15
|
-
|
|
16
|
-
Overall: a reasonable idea for a trusted internal microservices environment, but it needs significant hardening before it's safe for anything facing external networks or for multi-tenant deployments.
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## Security Issues
|
|
21
|
-
|
|
22
|
-
### S1 — `$getEnv()` exposes all environment variables to any NATS caller (CRITICAL)
|
|
23
|
-
|
|
24
|
-
**File:** `src/Service.js:312-324`
|
|
25
|
-
|
|
26
|
-
```js
|
|
27
|
-
$getEnv() {
|
|
28
|
-
return {
|
|
29
|
-
...
|
|
30
|
-
env: process.env, // all secrets: API keys, DB passwords, tokens
|
|
31
|
-
...
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
This method is accessible to any service on the NATS bus. It is **not** gated by authentication. Methods prefixed with `$` are excluded from HTTP routes (`m: fields.filter(methodName => !methodName.startsWith("$"))`), but they can be called directly over NATS via:
|
|
37
|
-
|
|
38
|
-
```js
|
|
39
|
-
await Request("MyService", "$getEnv");
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Any compromised service, or any process that can connect to NATS, can harvest credentials from all other services. There is no way to opt out of this method — it is always present on every service.
|
|
43
|
-
|
|
44
|
-
The same information is available at the debug endpoint's `/info` route (`Gateway.js:260-272`).
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
### S2 — CORS configuration allows credential theft from any origin (HIGH)
|
|
49
|
-
|
|
50
|
-
**File:** `src/Gateway.js:71-85`
|
|
51
|
-
|
|
52
|
-
```js
|
|
53
|
-
res.set("Access-Control-Allow-Credentials", "true");
|
|
54
|
-
res.set("Access-Control-Allow-Origin", origin || "*");
|
|
55
|
-
res.set("Access-Control-Allow-Methods", requestMethod || allMethods);
|
|
56
|
-
res.set("Access-Control-Allow-Headers", requestHeaders || allHeaders);
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
This reflects the request's `Origin` back as the allowed origin, which is effectively "allow all origins." Combined with `Allow-Credentials: true`, this means **any website on the internet can make authenticated cross-origin requests** (including requests with cookies or auth headers) to the gateway.
|
|
60
|
-
|
|
61
|
-
This is actually more dangerous than `Access-Control-Allow-Origin: *` because `*` does not allow credentials, but the current implementation does.
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
### S3 — Debug endpoint exposes `process.env` and internal state in non-production (HIGH)
|
|
66
|
-
|
|
67
|
-
**File:** `src/Gateway.js:13`, `src/Gateway.js:260-272`
|
|
68
|
-
|
|
69
|
-
```js
|
|
70
|
-
const DEBUG_ENDPOINT = "/lZ6jD2eC3iP0zB3jJ1yJ9pM8gG3yI3vS";
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
The debug endpoint is "protected" by a random-looking path, but that path is published in source code (visible on npm and any git host). The `/info` route sends the full `process.env`. The endpoint is disabled in `NODE_ENV=production`, but any non-production deployment with an exposed gateway leaks all secrets.
|
|
74
|
-
|
|
75
|
-
Additionally, the `/router-registry` route dumps the entire internal registry including backend addresses.
|
|
76
|
-
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
### S4 — No authentication or authorization on NATS RPC layer (HIGH)
|
|
80
|
-
|
|
81
|
-
**File:** `src/Service.js` (entire file), `src/Request.js`
|
|
82
|
-
|
|
83
|
-
The NATS bus is fully trusted. Any process that can publish to `gx2.service.<hash>` can invoke any method on any service. This includes:
|
|
84
|
-
- Internal business logic methods
|
|
85
|
-
- `$createConnection` — can open TCP tunnels into the service
|
|
86
|
-
- `$getEnv` — leaks secrets
|
|
87
|
-
- `$getServiceInfo` — leaks internal service metadata
|
|
88
|
-
|
|
89
|
-
If NATS is ever exposed to an untrusted network (misconfigured firewall, a compromised service, etc.), the entire cluster is fully compromised. There is no way to add per-method authorization at the framework level.
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
### S5 — `randomSafeId` uses `Math.random()` (MEDIUM)
|
|
94
|
-
|
|
95
|
-
**File:** `src/Util.js:389-398`
|
|
96
|
-
|
|
97
|
-
```js
|
|
98
|
-
export function randomSafeId(size = 12) {
|
|
99
|
-
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
100
|
-
let result = "";
|
|
101
|
-
for (let i = 0; i < size; i++) {
|
|
102
|
-
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
|
103
|
-
}
|
|
104
|
-
return result;
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
This is used in `tempFilename()` to generate temporary file paths. `Math.random()` is not cryptographically secure and is predictable. An attacker who can observe some outputs can predict future filenames and pre-create symlinks to redirect where uploaded files are written. `randomBytes` is already imported and used by `picoid()` — `randomSafeId` should use it too.
|
|
109
|
-
|
|
110
|
-
---
|
|
111
|
-
|
|
112
|
-
### S6 — Multipart parser ignores declared `maxFileSize` and `maxFiles` limits (MEDIUM)
|
|
113
|
-
|
|
114
|
-
**File:** `src/Util.js:255-383`, `src/Types.js:3-6`
|
|
115
|
-
|
|
116
|
-
`parseMultipartOptions` is typed to accept `maxFileSize` and `maxFiles`, but neither is checked anywhere in `parseMultipart()`. A caller relying on these options to enforce upload limits gets no protection. With the default body limit of 1GB (`express.raw({ limit: "1024mb" })`), a single request could consume all disk space.
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## Performance Issues
|
|
121
|
-
|
|
122
|
-
### P1 — RPC resolution uses a CPU-burning polling loop (HIGH)
|
|
123
|
-
|
|
124
|
-
**File:** `src/Request.js:77-84`
|
|
125
|
-
|
|
126
|
-
```js
|
|
127
|
-
const delay = 5;
|
|
128
|
-
let retries = Math.floor(registryTimeout / delay); // 60,000 iterations for 5-minute timeout
|
|
129
|
-
while (identifier == null && retries-- > 0) {
|
|
130
|
-
identifier = registry.getIdentifier(name, version, id);
|
|
131
|
-
if (!identifier) {
|
|
132
|
-
await sleep(delay);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
For every call to a service that hasn't been discovered yet, this executes up to 60,000 iterations, each calling `registry.getIdentifier()` (which iterates all registry entries) and sleeping 5ms. With a 5-minute default timeout, a failed RPC call keeps a tight polling loop running for 5 minutes.
|
|
138
|
-
|
|
139
|
-
If many concurrent requests target a missing service, this creates severe CPU pressure. The registry already emits events via `EventEmitter` — waiting on the `"added"` event would be far more efficient.
|
|
140
|
-
|
|
141
|
-
---
|
|
142
|
-
|
|
143
|
-
### P2 — Beacon frequency vs. expiry creates unnecessary NATS traffic (MEDIUM)
|
|
144
|
-
|
|
145
|
-
**File:** `src/Service.js:121-130`, `src/Registry.js:8`
|
|
146
|
-
|
|
147
|
-
```js
|
|
148
|
-
const REGISTRY_ENTRY_TIMEOUT = 5000; // entries expire after 5s
|
|
149
|
-
// but beacons are sent every 1s:
|
|
150
|
-
await sleep(1000);
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Each service sends a beacon every 1 second, but entries only expire after 5 seconds. With N services all sending beacons every second, this is N messages/second on `gx2.beacon`. With `fullBeacon: false` (short beacon), there's an additional `$getServiceInfo` RPC call per short beacon per receiving node. At scale this creates significant traffic with no value beyond faster failure detection. The interval should be configurable, or should use NATS's built-in heartbeat facilities.
|
|
154
|
-
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
### P3 — Large payload streams are double-encoded for Stream fallback (MEDIUM)
|
|
158
|
-
|
|
159
|
-
**File:** `src/Connection.js:116-120`
|
|
160
|
-
|
|
161
|
-
```js
|
|
162
|
-
if (payload.length > this.getMaxPayloadSize()) {
|
|
163
|
-
payload = encode(Stream(JSON.stringify(json)));
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
When a payload exceeds the max size, it is:
|
|
168
|
-
1. JSON-encoded by `JSON.stringify`
|
|
169
|
-
2. Wrapped in a `Stream` object
|
|
170
|
-
3. JSON-encoded again by `encode()`
|
|
171
|
-
|
|
172
|
-
The inner `JSON.stringify` + outer `encode()` (which is also JSON) means the data is encoded twice. The receiver must decode twice, which is handled correctly, but it's wasteful for large payloads where performance matters most.
|
|
173
|
-
|
|
174
|
-
---
|
|
175
|
-
|
|
176
|
-
### P4 — The endpoint ordering sort in `Service.#start` is O(n²) per endpoint (LOW)
|
|
177
|
-
|
|
178
|
-
**File:** `src/Service.js:68-77`
|
|
179
|
-
|
|
180
|
-
```js
|
|
181
|
-
fields.sort((a, b, ia = -1, ib = -1) => {
|
|
182
|
-
for (let line = 0; line < serviceSource.length; line++) {
|
|
183
|
-
ia = serviceSource[line].includes(a) ? line : ia;
|
|
184
|
-
}
|
|
185
|
-
for (let line = 0; line < serviceSource.length; line++) {
|
|
186
|
-
ib = serviceSource[line].includes(b) ? line : ib;
|
|
187
|
-
}
|
|
188
|
-
return ia - ib;
|
|
189
|
-
});
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
The sort comparator does two full O(lines) scans of the source code for every comparison. For a service with N endpoints and M source lines, this is O(N log N × M) total. This only runs once at startup, but for large services it could cause noticeable startup delay. It also has a correctness issue (see B3).
|
|
193
|
-
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
## Bugs
|
|
197
|
-
|
|
198
|
-
### B1 — WebSocket load balancing is broken: backend is fixed at router build time (HIGH)
|
|
199
|
-
|
|
200
|
-
**File:** `src/Gateway.js:394-406`
|
|
201
|
-
|
|
202
|
-
```js
|
|
203
|
-
// Outside the handler — backend selected ONCE at build time:
|
|
204
|
-
let backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length];
|
|
205
|
-
|
|
206
|
-
if (verb === "ws") {
|
|
207
|
-
router.ws(uri, (ws, req) => {
|
|
208
|
-
let target = cleanupWebsocketUrl(`ws://${backend}${req.originalUrl}`);
|
|
209
|
-
this.#proxyWebsocketOverNats(target, ws, req); // always uses the same backend
|
|
210
|
-
});
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
For HTTP routes, there is a runtime re-evaluation at line 410 (`backend = endpoint.backend[endpoint.requests++ % endpoint.backend.length]`). But for WebSocket routes, the `backend` variable is captured by closure at router build time and never updated. All WebSocket connections after the first router build will target the same backend instance, even when multiple instances exist.
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
### B2 — `Gateway.#start()` accepts connections before middleware is registered (MEDIUM)
|
|
218
|
-
|
|
219
|
-
**File:** `src/Gateway.js:59-127`
|
|
220
|
-
|
|
221
|
-
```js
|
|
222
|
-
async #start(port = 8080) {
|
|
223
|
-
await connection.waitUntilReady();
|
|
224
|
-
|
|
225
|
-
this.#port = process.env.PORT || port;
|
|
226
|
-
this.#api.listen(this.#port); // server starts accepting connections here
|
|
227
|
-
|
|
228
|
-
logger.debug(...);
|
|
229
|
-
|
|
230
|
-
// middleware added AFTER listen:
|
|
231
|
-
this.#api.use(requestLogger);
|
|
232
|
-
this.#api.use(/* CORS */);
|
|
233
|
-
this.#api.use(DEBUG_ENDPOINT, this.#debugRouter());
|
|
234
|
-
...
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
Express registers middleware in order, and while this works for middleware definitions (Express adds them to a list for subsequent requests), there is a race condition: requests arriving between `listen()` and the `use()` calls will be processed with no CORS headers, no request logging, and potentially no debug route protection. The correct pattern is to register all middleware before calling `listen()`.
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
### B3 — Endpoint ordering sort is broken for methods whose names are substrings of each other (MEDIUM)
|
|
242
|
-
|
|
243
|
-
**File:** `src/Service.js:68-77`
|
|
244
|
-
|
|
245
|
-
```js
|
|
246
|
-
ia = serviceSource[line].includes(a) ? line : ia;
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
This uses `String.includes()` instead of an exact match. If you have methods `"GET /user"` and `"GET /users"`, any line containing `"GET /users"` will also match `"GET /user"` because `"GET /user"` is a substring of `"GET /users"`. This can assign the wrong line number for endpoint ordering, causing routes to be registered in the wrong order — which matters for Express routing (more specific routes should come first).
|
|
250
|
-
|
|
251
|
-
---
|
|
252
|
-
|
|
253
|
-
### B4 — WebSocket proxy does not close the inbound socket on backend connection failure (MEDIUM)
|
|
254
|
-
|
|
255
|
-
**File:** `src/Gateway.js:310-328`
|
|
256
|
-
|
|
257
|
-
```js
|
|
258
|
-
#proxyWebsocketOverNats(target, inbound, req) {
|
|
259
|
-
try {
|
|
260
|
-
const backend = new WebSocket(target, { headers: { ...req.headers } });
|
|
261
|
-
|
|
262
|
-
backend.on("open", () => {
|
|
263
|
-
// setup relay...
|
|
264
|
-
});
|
|
265
|
-
} catch (e) {
|
|
266
|
-
logger.error(e); // error is logged, but inbound socket is never closed
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
If the `new WebSocket(target)` constructor throws synchronously, or if the `"open"` event never fires because the backend is unreachable, the `inbound` WebSocket is never closed. The client will hang indefinitely with an open connection that receives no data and never closes.
|
|
272
|
-
|
|
273
|
-
---
|
|
274
|
-
|
|
275
|
-
### B5 — `deepMerge` mutates the shared `defaultServiceOptions` object (MEDIUM)
|
|
276
|
-
|
|
277
|
-
**File:** `src/Service.js:53-57`, `src/Util.js:400-416`
|
|
278
|
-
|
|
279
|
-
```js
|
|
280
|
-
#options = defaultServiceOptions; // shares reference, does not copy
|
|
281
|
-
|
|
282
|
-
async #start(options = {}) {
|
|
283
|
-
this.#options = deepMerge(this.#options, options); // mutates defaultServiceOptions in place
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
`deepMerge` modifies its first argument in place and returns it. Because `this.#options` is initialized as a reference to the module-level `defaultServiceOptions` constant, the first `Service.start()` call with a non-default `options` argument will mutate the shared default. Subsequent services started in the same process will start with the already-mutated defaults rather than the intended defaults.
|
|
287
|
-
|
|
288
|
-
Example: if `ServiceA.start({ middleware: { json: false } })` runs first, then `ServiceB.start()` will also have `middleware.json === false`.
|
|
289
|
-
|
|
290
|
-
---
|
|
291
|
-
|
|
292
|
-
### B6 — `Connection.monitorStatus()` advances the round-robin and only monitors one connection (LOW)
|
|
293
|
-
|
|
294
|
-
**File:** `src/Connection.js:68-72`
|
|
295
|
-
|
|
296
|
-
```js
|
|
297
|
-
async monitorStatus() {
|
|
298
|
-
for await (const event of this.#getConnection().status()) {
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
`#getConnection()` increments `#connectionRoundRobin` as a side effect. When `monitorStatus()` is called right after `start()` (line 64), it permanently moves the round-robin index forward by one and monitors only one of the available connections. For `connections > 1`, the other connections' disconnections/reconnections are silently unmonitored.
|
|
302
|
-
|
|
303
|
-
---
|
|
304
|
-
|
|
305
|
-
### B7 — `createHTTPServer` uses `net` (TCP) instead of `http` (LOW)
|
|
306
|
-
|
|
307
|
-
**File:** `src/Util.js:116`
|
|
308
|
-
|
|
309
|
-
```js
|
|
310
|
-
export const createHTTPServer = (handler, start = 30000, poolSize = 20000) =>
|
|
311
|
-
createServerAtFreePort(net, handler, start, poolSize);
|
|
312
|
-
// ^^^ should be `http`, not `net`
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
`createHTTPServer` is named to suggest it creates an HTTP server but calls `createServerAtFreePort` with the `net` module (raw TCP), making it identical to `createTCPServer`. The function is not used anywhere in the codebase currently, but any future caller expecting an HTTP server will get a raw TCP server.
|
|
316
|
-
|
|
317
|
-
---
|
|
318
|
-
|
|
319
|
-
### B8 — No timeout or cleanup for orphaned streams (LOW)
|
|
320
|
-
|
|
321
|
-
**File:** `src/Stream.js:41-56`, `src/Service.js:269-310`
|
|
322
|
-
|
|
323
|
-
In `Stream()`, if the NATS control message on `gx2.stream.<id>.a` never arrives (e.g., the receiver crashed before consuming the stream), the entry persists in `activeStreams` indefinitely, leaking memory and potentially holding open file handles.
|
|
324
|
-
|
|
325
|
-
Similarly, in `$createConnection`, if the control message on `gx2.stream.<id>.c` never arrives (e.g., the gateway crashed), the TCP connection and both NATS subscriptions stay open indefinitely. There is no timeout to force cleanup of orphaned connections.
|
|
326
|
-
|
|
327
|
-
---
|
|
328
|
-
|
|
329
|
-
## Design Observations
|
|
330
|
-
|
|
331
|
-
### D1 — No way to mark methods as non-RPC-callable
|
|
332
|
-
|
|
333
|
-
All public instance methods (not starting with `$`, not `constructor` or `onStart`) are advertised via beacon and callable over NATS. There is no annotation or convention to make a method private/internal. This means any helper method you add to a service is automatically exposed as an RPC endpoint.
|
|
334
|
-
|
|
335
|
-
### D2 — Service beacon advertises its HTTP port to all services
|
|
336
|
-
|
|
337
|
-
Every beacon includes `a: getNetworkAddresses().map(address => \`\${address}:\${webserver.getPort()}\`)`. This means every service on the network learns the internal ports of every other service. In environments where services should not be directly reachable from each other (e.g., different network segments), the gateway acts as the intended chokepoint, but services inadvertently publish their direct addresses.
|
|
338
|
-
|
|
339
|
-
### D3 — Endpoint options parsed from the method name with `querystring.parse`
|
|
340
|
-
|
|
341
|
-
**File:** `src/Gateway.js:351-353`
|
|
342
|
-
|
|
343
|
-
```js
|
|
344
|
-
...(endpoint.options ? querystring.parse(endpoint.options) : {})
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
The `querystring` module is deprecated (Node.js recommends `URLSearchParams`). More importantly, the `order` option is parsed as a string and later cast with `parseInt()` — type errors are silent. Any typo in endpoint options silently gets an `order` of `NaN` which sorts unpredictably.
|
|
348
|
-
|
|
349
|
-
---
|
|
350
|
-
|
|
351
|
-
## Summary Table
|
|
352
|
-
|
|
353
|
-
| ID | Severity | Category | Issue |
|
|
354
|
-
|----|----------|----------|-------|
|
|
355
|
-
| S1 | Critical | Security | `$getEnv()` exposes all secrets to any NATS caller |
|
|
356
|
-
| S2 | High | Security | CORS reflects origin + allows credentials — any site can make authenticated requests |
|
|
357
|
-
| S3 | High | Security | Debug endpoint leaks `process.env` (path is in public source) |
|
|
358
|
-
| S4 | High | Security | Zero authentication on NATS RPC layer |
|
|
359
|
-
| S5 | Medium | Security | `randomSafeId` uses `Math.random()` for temp file names |
|
|
360
|
-
| S6 | Medium | Security | `maxFileSize`/`maxFiles` multipart options are silently ignored |
|
|
361
|
-
| P1 | High | Performance | 60,000-iteration polling loop for unresolved RPC service lookup |
|
|
362
|
-
| P2 | Medium | Performance | Beacon rate (1s) vs. expiry (5s) generates excessive NATS traffic at scale |
|
|
363
|
-
| P3 | Medium | Performance | Large payloads double-encoded on stream fallback path |
|
|
364
|
-
| P4 | Low | Performance | O(n² × lines) endpoint ordering sort at startup |
|
|
365
|
-
| B1 | High | Bug | WebSocket round-robin broken — backend fixed at router build time |
|
|
366
|
-
| B2 | Medium | Bug | Gateway listens before middleware is registered (race at startup) |
|
|
367
|
-
| B3 | Medium | Bug | Endpoint sort uses substring match — breaks ordering for similar-named routes |
|
|
368
|
-
| B4 | Medium | Bug | Inbound WebSocket never closed on backend connection failure |
|
|
369
|
-
| B5 | Medium | Bug | `deepMerge` mutates shared `defaultServiceOptions` across service instances |
|
|
370
|
-
| B6 | Low | Bug | `monitorStatus()` advances round-robin and only monitors one connection |
|
|
371
|
-
| B7 | Low | Bug | `createHTTPServer` creates a TCP server, not an HTTP server |
|
|
372
|
-
| B8 | Low | Bug | No timeout for orphaned stream/connection cleanup |
|