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/LICENSE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Davor Tarandek <dtarandek@tria.hr>
|
|
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
|
@@ -1,11 +1,355 @@
|
|
|
1
1
|
## Geonix
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Node.js microservices framework built on [NATS](https://nats.io/). Write self-announcing services as plain JavaScript classes with HTTP endpoints defined as specially-named methods. A gateway component automatically discovers services and routes HTTP/WebSocket traffic to them.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```sh
|
|
8
|
+
npm install geonix
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires a running NATS server. Default: `nats://localhost`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### Service
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { Service } from "geonix";
|
|
21
|
+
|
|
22
|
+
class GreetingService extends Service {
|
|
23
|
+
"GET /hello"(req, res) {
|
|
24
|
+
res.json({ message: `Hello, ${req.query.name || "World"}` });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
greet(name) {
|
|
28
|
+
return `Hello, ${name}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
GreetingService.start();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Gateway
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { Gateway } from "geonix";
|
|
39
|
+
|
|
40
|
+
Gateway.start();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That's it. The gateway discovers all running services via NATS beacons and proxies HTTP traffic to them. No configuration required for basic use.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
Client (HTTP/WS)
|
|
51
|
+
│
|
|
52
|
+
[Gateway] ── discovers services via NATS beacons
|
|
53
|
+
│
|
|
54
|
+
┌────┴────┐
|
|
55
|
+
│ Service │ ── embedded Express HTTP server
|
|
56
|
+
│ Service │ ── announces on NATS gx2.beacon every 3.5s
|
|
57
|
+
│ Service │
|
|
58
|
+
└─────────┘
|
|
59
|
+
│
|
|
60
|
+
[NATS] ── message bus for beacons, RPC, streaming
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Components
|
|
64
|
+
|
|
65
|
+
| File | Role |
|
|
66
|
+
|------|------|
|
|
67
|
+
| `Service` | Base class for all services. Manages HTTP endpoints, NATS RPC listeners, beacons. |
|
|
68
|
+
| `Gateway` | HTTP reverse proxy. Subscribes to beacons, builds a dynamic Express router. |
|
|
69
|
+
| `Connection` | Singleton NATS connection wrapper. Supports multiple connections with round-robin. |
|
|
70
|
+
| `Registry` | Maintains a live map of available services from beacon messages. Entries expire after 5s. |
|
|
71
|
+
| `Request` | Sends RPC calls to services over NATS. Waits up to 5 minutes for service to appear. |
|
|
72
|
+
| `Remote` | Proxy-based sugar: `Remote("ServiceName").methodName(args)` → RPC call. |
|
|
73
|
+
| `Stream` | Wraps large payloads as streamable objects. Can stream over HTTP or NATS. |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## HTTP Endpoints
|
|
78
|
+
|
|
79
|
+
Methods on a Service class are matched against:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
[options|]VERB /path
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Example | Meaning |
|
|
86
|
+
|---------|---------|
|
|
87
|
+
| `"GET /api/users"` | HTTP GET endpoint |
|
|
88
|
+
| `"POST /upload"` | HTTP POST endpoint |
|
|
89
|
+
| `"WS /chat"` | WebSocket endpoint |
|
|
90
|
+
| `"SUB my.nats.subject"` | NATS subscription handler |
|
|
91
|
+
| `"order=10|GET /api"` | HTTP GET with routing priority 10 |
|
|
92
|
+
|
|
93
|
+
Multiple endpoints with the same verb+path and the same semver version are automatically load-balanced (round-robin for HTTP, IP-based affinity for WebSocket).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## RPC Calls
|
|
98
|
+
|
|
99
|
+
Call a method on any running service by name:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
import { Request } from "geonix";
|
|
103
|
+
|
|
104
|
+
// Call a method
|
|
105
|
+
const result = await Request("GreetingService", "greet", ["Alice"]);
|
|
106
|
+
|
|
107
|
+
// Target a specific version
|
|
108
|
+
const result = await Request("GreetingService@1.2.x", "greet", ["Alice"]);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Or use the proxy sugar:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
import { Remote } from "geonix";
|
|
115
|
+
|
|
116
|
+
const greeting = Remote("GreetingService");
|
|
117
|
+
const result = await greeting.greet("Alice");
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
RPC calls use NATS queue groups — multiple instances of the same service share load automatically.
|
|
121
|
+
|
|
122
|
+
### RequestOptions
|
|
123
|
+
|
|
124
|
+
Pass `RequestOptions(...)` as the first argument to override per-call options:
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
import { Remote, RequestOptions } from "geonix";
|
|
128
|
+
|
|
129
|
+
const slow = Remote("HeavyService");
|
|
130
|
+
const result = await slow.compute(RequestOptions({ timeout: 60000 }), payload);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
| Option | Description | Default |
|
|
134
|
+
|--------|-------------|---------|
|
|
135
|
+
| `timeout` | Max wait for service to appear in registry (ms) | 300 000 |
|
|
136
|
+
| `httpTimeout` | Max wait for HTTP RPC response (ms) | 5 000 |
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Context Injection
|
|
141
|
+
|
|
142
|
+
Prefix the first parameter with `$` to receive a context object containing caller info:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
class AuthService extends Service {
|
|
146
|
+
whoAmI($ctx) {
|
|
147
|
+
return { caller: $ctx.caller, me: $ctx.me };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Method Visibility
|
|
155
|
+
|
|
156
|
+
| Prefix | Beacon | RPC callable | HTTP |
|
|
157
|
+
|--------|--------|--------------|------|
|
|
158
|
+
| (none) | yes | yes | yes (if endpoint pattern) |
|
|
159
|
+
| `$` | no | yes (internal use) | no |
|
|
160
|
+
| `$$` | no | no | no |
|
|
161
|
+
|
|
162
|
+
`$` methods are framework-reserved built-ins (e.g. `$getServiceInfo`). They are not advertised in beacons but can be called over NATS. `$$` methods are fully private — any remote call attempting to invoke a `$$` method is rejected with an error.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Gateway Options
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
Gateway.start({
|
|
170
|
+
// CORS — four modes:
|
|
171
|
+
cors: undefined, // off (default) — no CORS headers
|
|
172
|
+
cors: "*", // wildcard — all origins, no credentials
|
|
173
|
+
cors: ["https://app.com"], // allowlist — credentials for listed origins only
|
|
174
|
+
cors: true, // open — reflect any origin with credentials (for auth-header APIs)
|
|
175
|
+
|
|
176
|
+
// Lifecycle hooks
|
|
177
|
+
beforeRequest: (req, res) => {},
|
|
178
|
+
afterRequest: (req, res) => {},
|
|
179
|
+
|
|
180
|
+
// Per-route middleware control
|
|
181
|
+
// handlers: { before: [...], after: [...] }
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Streaming
|
|
188
|
+
|
|
189
|
+
Large payloads are automatically streamed when they exceed the NATS max payload size (~512 KB):
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
import { Stream, getReadable, streamToBuffer, streamToString, streamToJSON } from "geonix";
|
|
193
|
+
|
|
194
|
+
// Sender — return a Stream from a service method
|
|
195
|
+
return Stream(largeBuffer);
|
|
196
|
+
|
|
197
|
+
// Receiver — materialise the stream
|
|
198
|
+
const readable = await getReadable(result); // Node.js Readable
|
|
199
|
+
const buf = await streamToBuffer(result);
|
|
200
|
+
const text = await streamToString(result);
|
|
201
|
+
const obj = await streamToJSON(result);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Service Start Options
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
MyService.start({
|
|
210
|
+
name: "MyService", // override class name
|
|
211
|
+
version: "1.2.3", // override VERSION env var
|
|
212
|
+
id: "custom-id", // stable instance ID
|
|
213
|
+
|
|
214
|
+
middleware: {
|
|
215
|
+
json: true, // parse JSON bodies (default: true)
|
|
216
|
+
raw: true, // parse raw binary bodies (default: true)
|
|
217
|
+
cookies: true // parse cookies (default: true)
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
fullBeacon: true, // broadcast full service info (default: true)
|
|
221
|
+
// set false to broadcast instance ID only (heartbeat)
|
|
222
|
+
|
|
223
|
+
handlers: {
|
|
224
|
+
before: [myAuthMiddleware], // prepend to all HTTP handlers
|
|
225
|
+
after: [myLoggingMiddleware] // append to all HTTP handlers
|
|
226
|
+
// Note: for WS endpoints, `before` runs as route-scoped Express middleware
|
|
227
|
+
// before the WebSocket upgrade; `after` does not apply to WS or SUB endpoints.
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Built-in Service Methods
|
|
235
|
+
|
|
236
|
+
All services expose these internal methods (callable over NATS, not over HTTP):
|
|
237
|
+
|
|
238
|
+
| Method | Description |
|
|
239
|
+
|--------|-------------|
|
|
240
|
+
| `$createConnection(streamId)` | Opens a TCP tunnel for HTTP-over-NATS proxying |
|
|
241
|
+
| `$getEnv()` | Returns Node.js version, platform, arch, memory and CPU usage |
|
|
242
|
+
| `$getServiceInfo()` | Returns service metadata: name, version, methods, addresses |
|
|
243
|
+
| `$stop()` | Stops the service loop |
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Multipart form data
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
import { parseMultipart } from "geonix";
|
|
251
|
+
|
|
252
|
+
class UploadService extends Service {
|
|
253
|
+
"POST /upload"(req, res) {
|
|
254
|
+
const parts = await parseMultipart(req, {
|
|
255
|
+
useMemory: false, // stream parts to temp files (default)
|
|
256
|
+
// true = keep parts in memory
|
|
257
|
+
maxFiles: 10, // throw if more than N parts
|
|
258
|
+
maxFileSize: 10e6, // throw if any part exceeds N bytes
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
for (const part of parts) {
|
|
262
|
+
console.log(part.name, part.filename, part.headers);
|
|
263
|
+
// part.body is a Node.js Readable
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
res.send("ok");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
UploadService.start({ middleware: { raw: false } });
|
|
271
|
+
// Disable the raw body middleware so the request stream reaches parseMultipart intact.
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Each returned part has:
|
|
275
|
+
|
|
276
|
+
| Property | Type | Description |
|
|
277
|
+
|----------|------|-------------|
|
|
278
|
+
| `name` | `string \| undefined` | Value of `name` in `Content-Disposition`, or `undefined` |
|
|
279
|
+
| `filename` | `string \| undefined` | Value of `filename` in `Content-Disposition`, or `undefined` |
|
|
280
|
+
| `headers` | `object` | Lowercased part headers |
|
|
281
|
+
| `body` | `Readable` | Part body as a readable stream |
|
|
282
|
+
| `size` | `number` | Bytes written so far (available during parsing) |
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Configuration
|
|
287
|
+
|
|
288
|
+
| Env Variable | Default | Description |
|
|
289
|
+
|---|---|---|
|
|
290
|
+
| `GX_TRANSPORT` | `nats://localhost` | NATS server URL(s) |
|
|
291
|
+
| `GX_PORT` | `8080` | Gateway listen port |
|
|
292
|
+
| `GX_LOCAL_PORT` | random | Force service HTTP server to a specific port |
|
|
293
|
+
| `GX_VERSION` | `999.999.<seconds>` | Service version |
|
|
294
|
+
| `GX_TRANSPORT_DEBUG` | — | Set to `true` to enable NATS protocol debug logging |
|
|
295
|
+
| `GX_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
|
296
|
+
| `GX_STREAM_TIMEOUT` | `90000` | Max wait for a stream consumer to connect (ms) |
|
|
297
|
+
| `GX_INACTIVITY_TIMEOUT` | `90000` | TCP proxy (HTTP-over-NATS) inactivity timeout (ms) |
|
|
298
|
+
| `GX_SECRET` | — | Encryption key: AES-256-GCM payloads + HMAC-SHA256 subjects. Services without the same key cannot communicate. |
|
|
299
|
+
| `GX_DEBUG_ENDPOINT` | — | Mount path for the debug router (e.g. `/_debug`). Disabled when unset. |
|
|
300
|
+
|
|
301
|
+
> **Deprecation notice:** The legacy unprefixed names `TRANSPORT`, `PORT`, `LOCAL_PORT`, `VERSION`, and `TRANSPORT_DEBUG` are still read as fallbacks but will be removed in the next major version. Migrate to the `GX_` prefixed names.
|
|
302
|
+
|
|
303
|
+
### Bus Encryption (`GX_SECRET`)
|
|
304
|
+
|
|
305
|
+
When `GX_SECRET` is set, all NATS traffic is encrypted:
|
|
306
|
+
|
|
307
|
+
- **Payloads** — AES-256-GCM with a random 12-byte IV. Tampered messages are rejected.
|
|
308
|
+
- **Subjects** — each dot-separated segment is replaced with HMAC-SHA256(segment, key). `*` and `>` wildcards are preserved so NATS subscribe semantics are unchanged.
|
|
309
|
+
|
|
310
|
+
Services sharing the same `GX_SECRET` can communicate; services with a different or missing key cannot read subjects or payloads. This effectively creates a bus partition per environment.
|
|
311
|
+
|
|
312
|
+
### Debug Router (`GX_DEBUG_ENDPOINT`)
|
|
313
|
+
|
|
314
|
+
When enabled, exposes diagnostic endpoints under the configured path:
|
|
315
|
+
|
|
316
|
+
| Path | Returns |
|
|
317
|
+
|------|---------|
|
|
318
|
+
| `/services` | List of all known services |
|
|
319
|
+
| `/endpoints` | All registered HTTP endpoints |
|
|
320
|
+
| `/registry` | Raw registry entries |
|
|
321
|
+
| `/router-registry` | Gateway's internal router state |
|
|
322
|
+
| `/stats` | Request/proxy counters |
|
|
323
|
+
| `/info` | Node.js version, platform, memory, CPU |
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Versioning
|
|
328
|
+
|
|
329
|
+
Services resolve their version in this priority order:
|
|
330
|
+
|
|
331
|
+
1. `GX_VERSION` env var (or legacy `VERSION` / `version`)
|
|
332
|
+
2. `options.version` passed to `Service.start()`
|
|
333
|
+
3. `this.version` on the class instance
|
|
334
|
+
4. Development default: `999.999.<seconds-since-midnight>`
|
|
335
|
+
|
|
336
|
+
The gateway routes requests to the highest semver version. Multiple versions of the same service can coexist. RPC calls can target a specific version range: `Request("MyService@1.2.x", "method", [])`.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Dependencies
|
|
341
|
+
|
|
342
|
+
| Package | Purpose |
|
|
343
|
+
|---------|---------|
|
|
344
|
+
| `nats` | NATS client |
|
|
345
|
+
| `express` | HTTP server |
|
|
346
|
+
| `express-ws` | WebSocket support for Express |
|
|
347
|
+
| `cookie-parser` | Cookie parsing middleware |
|
|
348
|
+
| `semver` | Semantic versioning comparisons |
|
|
349
|
+
| `ws` | WebSocket client (for gateway WS proxy) |
|
|
350
|
+
|
|
351
|
+
---
|
|
8
352
|
|
|
9
353
|
## License
|
|
10
354
|
|
|
11
|
-
|
|
355
|
+
[MIT](LICENSE)
|
package/exports.js
CHANGED
|
@@ -11,6 +11,4 @@ export { Request, Subscribe, Publish } from "./src/Request.js";
|
|
|
11
11
|
export { RequestOptions } from "./src/RequestOptions.js";
|
|
12
12
|
export { picoid as randomID } from "./src/Util.js";
|
|
13
13
|
|
|
14
|
-
export { stats as StreamStats } from "./src/Stream.js";
|
|
15
|
-
|
|
16
14
|
export { parseMultipart } from "./src/Util.js";
|