@nekzus/liop 2.2.0 → 2.3.0
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 +201 -21
- package/NOTICE +5 -0
- package/README.md +61 -5
- package/dist/bin/agent.js +7 -5
- package/dist/bin/agent.js.map +1 -1
- package/dist/bridge.d.ts +2 -2
- package/dist/bridge.js +2 -1
- package/dist/{chunk-UVO7DII3.js → chunk-4LQPDN3W.js} +108 -15
- package/dist/chunk-4LQPDN3W.js.map +1 -0
- package/dist/{chunk-NJRSFFD7.js → chunk-74CISO2I.js} +60 -22
- package/dist/chunk-74CISO2I.js.map +1 -0
- package/dist/{chunk-E5QBDD5E.js → chunk-CRZBVFDV.js} +169 -22
- package/dist/chunk-CRZBVFDV.js.map +1 -0
- package/dist/chunk-GKV6M2PQ.js +208106 -0
- package/dist/chunk-GKV6M2PQ.js.map +1 -0
- package/dist/chunk-JIJPZFR2.js +51 -0
- package/dist/chunk-JIJPZFR2.js.map +1 -0
- package/dist/chunk-OIMJZ64E.js +441 -0
- package/dist/chunk-OIMJZ64E.js.map +1 -0
- package/dist/chunk-PYKRQAER.js +77 -0
- package/dist/chunk-PYKRQAER.js.map +1 -0
- package/dist/{chunk-J3WPBMJ5.js → chunk-SONUORBT.js} +95 -6
- package/dist/chunk-SONUORBT.js.map +1 -0
- package/dist/{chunk-2JLG4DET.js → chunk-UBOQZVV5.js} +261 -107
- package/dist/chunk-UBOQZVV5.js.map +1 -0
- package/dist/chunk-UCJEK75U.js +714 -0
- package/dist/chunk-UCJEK75U.js.map +1 -0
- package/dist/chunk-WSW6YJNY.js +168 -0
- package/dist/chunk-WSW6YJNY.js.map +1 -0
- package/dist/client.d.ts +3 -2
- package/dist/client.js +4 -3
- package/dist/gateway.d.ts +5 -110
- package/dist/gateway.js +5 -4
- package/dist/hybrid-CF__0YnC.d.ts +170 -0
- package/dist/{index-BlGc0iym.d.ts → index-BW95kAL4.d.ts} +63 -2
- package/dist/index-cMV0_yD9.d.ts +200 -0
- package/dist/index.d.ts +273 -18
- package/dist/index.js +266 -13
- package/dist/index.js.map +1 -1
- package/dist/mesh.d.ts +14 -0
- package/dist/mesh.js +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +5 -4
- package/dist/{types-sKeUxuky.d.ts → types-lJIxBlWF.d.ts} +71 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/workers/logic-execution.d.ts +1 -0
- package/dist/workers/logic-execution.js +17 -1
- package/dist/workers/logic-execution.js.map +1 -1
- package/package.json +53 -33
- package/dist/chunk-2JLG4DET.js.map +0 -1
- package/dist/chunk-CT6NHSYP.js +0 -30
- package/dist/chunk-CT6NHSYP.js.map +0 -1
- package/dist/chunk-E5QBDD5E.js.map +0 -1
- package/dist/chunk-HB5DXX3Q.js +0 -1976
- package/dist/chunk-HB5DXX3Q.js.map +0 -1
- package/dist/chunk-IJHTRIZC.js +0 -56
- package/dist/chunk-IJHTRIZC.js.map +0 -1
- package/dist/chunk-J3WPBMJ5.js.map +0 -1
- package/dist/chunk-NJRSFFD7.js.map +0 -1
- package/dist/chunk-PHTWUTY7.js +0 -300
- package/dist/chunk-PHTWUTY7.js.map +0 -1
- package/dist/chunk-RDWCGZ2A.js +0 -87
- package/dist/chunk-RDWCGZ2A.js.map +0 -1
- package/dist/chunk-UVO7DII3.js.map +0 -1
- package/dist/index-qM8ZH8sC.d.ts +0 -101
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import { LiopMcpRouter } from './chunk-GKV6M2PQ.js';
|
|
2
|
+
import { LIOP_SCOPES, meshPeersConnected, manifestCacheSize, protocolMetrics } from './chunk-OIMJZ64E.js';
|
|
3
|
+
import { log } from './chunk-72MNYFR6.js';
|
|
4
|
+
import * as http from 'http';
|
|
5
|
+
import * as http2 from 'http2';
|
|
6
|
+
import * as net from 'net';
|
|
7
|
+
|
|
8
|
+
// src/security/prm.ts
|
|
9
|
+
function buildProtectedResourceMetadata(issuer, audience) {
|
|
10
|
+
return {
|
|
11
|
+
resource: audience,
|
|
12
|
+
authorization_servers: [issuer],
|
|
13
|
+
scopes_supported: LIOP_SCOPES,
|
|
14
|
+
bearer_methods_supported: ["header"],
|
|
15
|
+
resource_documentation: "https://github.com/nekzus/liop"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/gateway/grpc-web.ts
|
|
20
|
+
var GRPC_WEB_CONSTANTS = {
|
|
21
|
+
FLAG_DATA: 0,
|
|
22
|
+
FLAG_TRAILERS: 128,
|
|
23
|
+
HEADER_LENGTH: 5,
|
|
24
|
+
CONTENT_TYPE_PREFIX: "application/grpc-web",
|
|
25
|
+
STATUS_OK: 0,
|
|
26
|
+
STATUS_CANCELLED: 1,
|
|
27
|
+
STATUS_UNKNOWN: 2,
|
|
28
|
+
STATUS_INVALID_ARGUMENT: 3,
|
|
29
|
+
STATUS_DEADLINE_EXCEEDED: 4,
|
|
30
|
+
STATUS_NOT_FOUND: 5,
|
|
31
|
+
STATUS_PERMISSION_DENIED: 7,
|
|
32
|
+
STATUS_UNAUTHENTICATED: 16,
|
|
33
|
+
STATUS_INTERNAL: 13
|
|
34
|
+
};
|
|
35
|
+
function isGrpcWebRequest(contentType) {
|
|
36
|
+
if (!contentType) return false;
|
|
37
|
+
const lower = contentType.toLowerCase();
|
|
38
|
+
return lower.startsWith("application/grpc-web") || lower.startsWith("application/grpc-web+proto") || lower.startsWith("application/grpc-web+json");
|
|
39
|
+
}
|
|
40
|
+
function encodeDataFrame(payload) {
|
|
41
|
+
const buf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
42
|
+
const header = Buffer.alloc(GRPC_WEB_CONSTANTS.HEADER_LENGTH);
|
|
43
|
+
header.writeUInt8(GRPC_WEB_CONSTANTS.FLAG_DATA, 0);
|
|
44
|
+
header.writeUInt32BE(buf.length, 1);
|
|
45
|
+
return Buffer.concat([header, buf]);
|
|
46
|
+
}
|
|
47
|
+
function encodeTrailerFrame(status = GRPC_WEB_CONSTANTS.STATUS_OK, message = "", customTrailers = {}) {
|
|
48
|
+
const lines = [
|
|
49
|
+
`grpc-status:${status}`,
|
|
50
|
+
`grpc-message:${encodeURIComponent(message)}`
|
|
51
|
+
];
|
|
52
|
+
for (const [key, value] of Object.entries(customTrailers)) {
|
|
53
|
+
lines.push(`${key}:${value}`);
|
|
54
|
+
}
|
|
55
|
+
const trailerText = `${lines.join("\r\n")}\r
|
|
56
|
+
`;
|
|
57
|
+
const trailerBuf = Buffer.from(trailerText, "utf-8");
|
|
58
|
+
const header = Buffer.alloc(GRPC_WEB_CONSTANTS.HEADER_LENGTH);
|
|
59
|
+
header.writeUInt8(GRPC_WEB_CONSTANTS.FLAG_TRAILERS, 0);
|
|
60
|
+
header.writeUInt32BE(trailerBuf.length, 1);
|
|
61
|
+
return Buffer.concat([header, trailerBuf]);
|
|
62
|
+
}
|
|
63
|
+
function decodeFrames(buffer) {
|
|
64
|
+
const frames = [];
|
|
65
|
+
let offset = 0;
|
|
66
|
+
while (offset + GRPC_WEB_CONSTANTS.HEADER_LENGTH <= buffer.length) {
|
|
67
|
+
const flag = buffer.readUInt8(offset);
|
|
68
|
+
const length = buffer.readUInt32BE(offset + 1);
|
|
69
|
+
const payloadStart = offset + GRPC_WEB_CONSTANTS.HEADER_LENGTH;
|
|
70
|
+
const payloadEnd = payloadStart + length;
|
|
71
|
+
if (payloadEnd > buffer.length) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const payload = buffer.subarray(payloadStart, payloadEnd);
|
|
75
|
+
frames.push({
|
|
76
|
+
isTrailer: (flag & GRPC_WEB_CONSTANTS.FLAG_TRAILERS) === GRPC_WEB_CONSTANTS.FLAG_TRAILERS,
|
|
77
|
+
payload: Buffer.from(payload)
|
|
78
|
+
});
|
|
79
|
+
offset = payloadEnd;
|
|
80
|
+
}
|
|
81
|
+
return frames;
|
|
82
|
+
}
|
|
83
|
+
async function dispatchGrpcWebRequest(req, res, handler) {
|
|
84
|
+
const chunks = [];
|
|
85
|
+
req.on("data", (chunk) => {
|
|
86
|
+
chunks.push(chunk);
|
|
87
|
+
});
|
|
88
|
+
req.on("end", async () => {
|
|
89
|
+
try {
|
|
90
|
+
const fullBody = Buffer.concat(chunks);
|
|
91
|
+
const frames = decodeFrames(fullBody);
|
|
92
|
+
const dataPayload = frames.find((f) => !f.isTrailer)?.payload ?? Buffer.alloc(0);
|
|
93
|
+
const requestPath = req.url || "/";
|
|
94
|
+
log.info(
|
|
95
|
+
`[gRPC-Web] Handling request for ${requestPath} (${dataPayload.length} bytes)`
|
|
96
|
+
);
|
|
97
|
+
const result = await handler(requestPath, dataPayload);
|
|
98
|
+
res.statusCode = 200;
|
|
99
|
+
res.setHeader("content-type", "application/grpc-web+proto");
|
|
100
|
+
res.setHeader("access-control-allow-origin", "*");
|
|
101
|
+
res.setHeader(
|
|
102
|
+
"access-control-expose-headers",
|
|
103
|
+
"grpc-status, grpc-message, content-type"
|
|
104
|
+
);
|
|
105
|
+
if (result.data && result.data.length > 0) {
|
|
106
|
+
res.write(encodeDataFrame(result.data));
|
|
107
|
+
}
|
|
108
|
+
res.write(encodeTrailerFrame(result.status, result.message || ""));
|
|
109
|
+
res.end();
|
|
110
|
+
} catch (error) {
|
|
111
|
+
log.error(`[gRPC-Web] Dispatch error: ${error}`);
|
|
112
|
+
res.statusCode = 200;
|
|
113
|
+
res.setHeader("content-type", "application/grpc-web+proto");
|
|
114
|
+
res.write(
|
|
115
|
+
encodeTrailerFrame(
|
|
116
|
+
GRPC_WEB_CONSTANTS.STATUS_INTERNAL,
|
|
117
|
+
error.message || "Internal server error"
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
res.end();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/gateway/rate-limiter.ts
|
|
126
|
+
var InMemoryRateLimiter = class {
|
|
127
|
+
windowMs;
|
|
128
|
+
maxRequests;
|
|
129
|
+
records = /* @__PURE__ */ new Map();
|
|
130
|
+
cleanupTimer;
|
|
131
|
+
constructor(options = {}) {
|
|
132
|
+
this.windowMs = options.windowMs ?? 6e4;
|
|
133
|
+
this.maxRequests = options.maxRequests ?? 120;
|
|
134
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), 3e4);
|
|
135
|
+
if (typeof this.cleanupTimer.unref === "function") {
|
|
136
|
+
this.cleanupTimer.unref();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Evaluates whether a request from the given key (IP, token, or clientId) is permitted.
|
|
141
|
+
*
|
|
142
|
+
* @param key Identifier for the client (e.g., remote IP or OAuth client ID)
|
|
143
|
+
* @returns Status containing allowed boolean, remaining quota, and time until reset
|
|
144
|
+
*/
|
|
145
|
+
check(key) {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
let record = this.records.get(key);
|
|
148
|
+
if (!record || now >= record.resetTime) {
|
|
149
|
+
record = {
|
|
150
|
+
count: 1,
|
|
151
|
+
resetTime: now + this.windowMs
|
|
152
|
+
};
|
|
153
|
+
this.records.set(key, record);
|
|
154
|
+
return {
|
|
155
|
+
allowed: true,
|
|
156
|
+
remaining: this.maxRequests - 1,
|
|
157
|
+
limit: this.maxRequests,
|
|
158
|
+
resetMs: this.windowMs
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (record.count >= this.maxRequests) {
|
|
162
|
+
return {
|
|
163
|
+
allowed: false,
|
|
164
|
+
remaining: 0,
|
|
165
|
+
limit: this.maxRequests,
|
|
166
|
+
resetMs: Math.max(0, record.resetTime - now)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
record.count++;
|
|
170
|
+
return {
|
|
171
|
+
allowed: true,
|
|
172
|
+
remaining: this.maxRequests - record.count,
|
|
173
|
+
limit: this.maxRequests,
|
|
174
|
+
resetMs: Math.max(0, record.resetTime - now)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Explicitly resets the rate limit bucket for a given key.
|
|
179
|
+
*/
|
|
180
|
+
reset(key) {
|
|
181
|
+
this.records.delete(key);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Cleans up expired rate-limiting buckets to prevent memory leaks.
|
|
185
|
+
*/
|
|
186
|
+
cleanup() {
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
for (const [key, record] of this.records.entries()) {
|
|
189
|
+
if (now >= record.resetTime) {
|
|
190
|
+
this.records.delete(key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Destroys internal timers and clears records.
|
|
196
|
+
*/
|
|
197
|
+
close() {
|
|
198
|
+
if (this.cleanupTimer) {
|
|
199
|
+
clearInterval(this.cleanupTimer);
|
|
200
|
+
this.cleanupTimer = void 0;
|
|
201
|
+
}
|
|
202
|
+
this.records.clear();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// src/gateway/hybrid.ts
|
|
207
|
+
var LiopHybridGateway = class {
|
|
208
|
+
constructor(liopServer, meshNode = null, rpcPort = 50051, rateLimiterOptions) {
|
|
209
|
+
this.liopServer = liopServer;
|
|
210
|
+
this.meshNode = meshNode;
|
|
211
|
+
this.jwtValidator = this.liopServer.jwtValidator;
|
|
212
|
+
this.oauthProvider = this.liopServer.oauthProvider;
|
|
213
|
+
this.rateLimiter = new InMemoryRateLimiter(rateLimiterOptions);
|
|
214
|
+
this.router = new LiopMcpRouter(this.liopServer, this.meshNode, rpcPort);
|
|
215
|
+
this.h2Server = http2.createServer();
|
|
216
|
+
this.setupH2Routes();
|
|
217
|
+
this.h1Server = http.createServer();
|
|
218
|
+
this.setupH1Routes();
|
|
219
|
+
this.netServer = net.createServer((socket) => {
|
|
220
|
+
socket.once("data", (buffer) => {
|
|
221
|
+
const isHttp2 = buffer.toString().startsWith("PRI * HTTP/2.0");
|
|
222
|
+
log.info(
|
|
223
|
+
`[LIOP-Gateway] Incoming L4 Connection. Protocol: ${isHttp2 ? "HTTP/2 (gRPC)" : "HTTP/1.1 (MCP)"}`
|
|
224
|
+
);
|
|
225
|
+
if (isHttp2) {
|
|
226
|
+
this.h2Server.emit("connection", socket);
|
|
227
|
+
} else {
|
|
228
|
+
this.h1Server.emit("connection", socket);
|
|
229
|
+
}
|
|
230
|
+
socket.unshift(buffer);
|
|
231
|
+
});
|
|
232
|
+
socket.on(
|
|
233
|
+
"error",
|
|
234
|
+
(err) => log.error(`[LIOP-Gateway] NetServer Socket Error: ${err.message}`)
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
this.h1Server.on(
|
|
238
|
+
"error",
|
|
239
|
+
(err) => log.error(`[LIOP-Gateway] H1 Server Error: ${err.message}`)
|
|
240
|
+
);
|
|
241
|
+
this.h2Server.on(
|
|
242
|
+
"error",
|
|
243
|
+
(err) => log.error(`[LIOP-Gateway] H2 Server Error: ${err.message}`)
|
|
244
|
+
);
|
|
245
|
+
log.info("[LIOP-Gateway] Hybrid adapter initialized.");
|
|
246
|
+
}
|
|
247
|
+
netServer;
|
|
248
|
+
h2Server;
|
|
249
|
+
h1Server;
|
|
250
|
+
router;
|
|
251
|
+
jwtValidator;
|
|
252
|
+
// biome-ignore lint/suspicious/noExplicitAny: oidc-provider is loaded in Phase C
|
|
253
|
+
oauthProvider;
|
|
254
|
+
rateLimiter;
|
|
255
|
+
isDraining = false;
|
|
256
|
+
activeRequests = 0;
|
|
257
|
+
setupH2Routes() {
|
|
258
|
+
this.h2Server.on("stream", (stream, headers) => {
|
|
259
|
+
const contentType = headers["content-type"];
|
|
260
|
+
const path = headers[":path"];
|
|
261
|
+
if (contentType === "application/grpc") {
|
|
262
|
+
this.handleGrpcStream(stream);
|
|
263
|
+
} else if (path === "/mcp") {
|
|
264
|
+
this.handleMcpH2Stream(stream, headers);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
setupH1Routes() {
|
|
269
|
+
this.h1Server.on("request", async (req, res) => {
|
|
270
|
+
this.activeRequests += 1;
|
|
271
|
+
const onDone = () => {
|
|
272
|
+
res.removeListener("finish", onDone);
|
|
273
|
+
res.removeListener("close", onDone);
|
|
274
|
+
this.activeRequests = Math.max(0, this.activeRequests - 1);
|
|
275
|
+
};
|
|
276
|
+
res.on("finish", onDone);
|
|
277
|
+
res.on("close", onDone);
|
|
278
|
+
const url = req.url || "";
|
|
279
|
+
const method = req.method;
|
|
280
|
+
if (method === "GET" && url === "/metrics") {
|
|
281
|
+
if (this.meshNode) {
|
|
282
|
+
meshPeersConnected.set({}, this.meshNode.getPeers().length);
|
|
283
|
+
}
|
|
284
|
+
if (this.router) {
|
|
285
|
+
manifestCacheSize.set({}, this.router.getManifestCacheSize());
|
|
286
|
+
}
|
|
287
|
+
res.writeHead(200, {
|
|
288
|
+
"Content-Type": "text/plain; version=0.0.4; charset=utf-8"
|
|
289
|
+
});
|
|
290
|
+
res.end(protocolMetrics.exportPrometheusText());
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (method === "GET" && url === "/healthz") {
|
|
294
|
+
if (this.isDraining) {
|
|
295
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
296
|
+
res.end(
|
|
297
|
+
JSON.stringify({
|
|
298
|
+
status: "draining",
|
|
299
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
305
|
+
res.end(
|
|
306
|
+
JSON.stringify({
|
|
307
|
+
status: "ok",
|
|
308
|
+
uptime: Math.floor(process.uptime()),
|
|
309
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
310
|
+
})
|
|
311
|
+
);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (method === "GET" && url === "/readyz") {
|
|
315
|
+
if (this.isDraining) {
|
|
316
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
317
|
+
res.end(
|
|
318
|
+
JSON.stringify({
|
|
319
|
+
status: "draining",
|
|
320
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
321
|
+
})
|
|
322
|
+
);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const meshStarted = this.meshNode ? this.meshNode.isStarted() : true;
|
|
326
|
+
if (!meshStarted) {
|
|
327
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
328
|
+
res.end(
|
|
329
|
+
JSON.stringify({
|
|
330
|
+
status: "unready",
|
|
331
|
+
meshStarted: false,
|
|
332
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
338
|
+
res.end(
|
|
339
|
+
JSON.stringify({
|
|
340
|
+
status: "ready",
|
|
341
|
+
meshStarted: true,
|
|
342
|
+
peers: this.meshNode ? this.meshNode.getPeers().length : 0,
|
|
343
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (this.isDraining && url === "/mcp") {
|
|
349
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
350
|
+
res.end(
|
|
351
|
+
JSON.stringify({
|
|
352
|
+
jsonrpc: "2.0",
|
|
353
|
+
error: {
|
|
354
|
+
code: -32e3,
|
|
355
|
+
message: "Gateway is draining connections for shutdown"
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const contentType = req.headers["content-type"];
|
|
362
|
+
if (isGrpcWebRequest(contentType)) {
|
|
363
|
+
await dispatchGrpcWebRequest(req, res, async (reqPath, payload) => {
|
|
364
|
+
log.info(
|
|
365
|
+
`[LIOP-Gateway] gRPC-Web fallback call: ${reqPath} (${payload.length} bytes)`
|
|
366
|
+
);
|
|
367
|
+
return {
|
|
368
|
+
status: GRPC_WEB_CONSTANTS.STATUS_OK,
|
|
369
|
+
message: "OK",
|
|
370
|
+
data: Buffer.from("")
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (url.startsWith("/oidc") && this.oauthProvider) {
|
|
376
|
+
const callback = typeof this.oauthProvider.callback === "function" ? this.oauthProvider.callback() : this.oauthProvider;
|
|
377
|
+
const originalUrl = req.url;
|
|
378
|
+
req.url = (originalUrl || "").slice(5) || "/";
|
|
379
|
+
try {
|
|
380
|
+
return callback(req, res);
|
|
381
|
+
} finally {
|
|
382
|
+
req.url = originalUrl;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (method === "GET" && url === "/.well-known/oauth-protected-resource") {
|
|
386
|
+
if (this.jwtValidator) {
|
|
387
|
+
const prm = buildProtectedResourceMetadata(
|
|
388
|
+
this.jwtValidator.getIssuer(),
|
|
389
|
+
this.jwtValidator.getAudience()
|
|
390
|
+
);
|
|
391
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
392
|
+
res.end(JSON.stringify(prm));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
res.writeHead(404);
|
|
396
|
+
res.end("Not Found");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (method === "GET" && (url === "/" || url === "/mcp" || url === "/health")) {
|
|
400
|
+
if (url === "/health" && req.headers.accept?.includes("application/json")) {
|
|
401
|
+
const meshInfo = this.meshNode ? {
|
|
402
|
+
peerId: this.meshNode.getPeerId()?.toString() || "",
|
|
403
|
+
multiaddrs: this.meshNode.getMultiaddrs().map((m) => m.toString())
|
|
404
|
+
} : null;
|
|
405
|
+
const issuer = this.jwtValidator?.getIssuer();
|
|
406
|
+
const baseUrl = issuer ? issuer.endsWith("/oidc") ? issuer : `${issuer}/oidc` : "";
|
|
407
|
+
const authInfoResponse = this.jwtValidator && issuer ? {
|
|
408
|
+
issuer,
|
|
409
|
+
jwks_uri: `${baseUrl}/jwks`,
|
|
410
|
+
...this.oauthProvider ? {
|
|
411
|
+
token_endpoint: `${baseUrl}/token`
|
|
412
|
+
} : {}
|
|
413
|
+
} : void 0;
|
|
414
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
415
|
+
res.end(
|
|
416
|
+
JSON.stringify({
|
|
417
|
+
status: "healthy",
|
|
418
|
+
node: this.liopServer.getServerInfo(),
|
|
419
|
+
mesh: meshInfo,
|
|
420
|
+
tools: this.liopServer.listTools().map((t) => t.name),
|
|
421
|
+
auth: authInfoResponse,
|
|
422
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
428
|
+
res.end(`
|
|
429
|
+
<body style="background:#0f172a;color:#f8fafc;font-family:sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
430
|
+
<div style="background:#1e293b;padding:40px;border-radius:16px;border:1px solid #38bdf8;text-align:center;box-shadow:0 20px 25px -5px rgba(0,0,0,0.1)">
|
|
431
|
+
<h1 style="color:#38bdf8;margin-top:0">LIOP Protocol Transformer</h1>
|
|
432
|
+
<p style="opacity:0.8;font-weight:600">L4/L7 Transcoding: JSON-RPC ↔ gRPC</p>
|
|
433
|
+
<p style="opacity:0.6;font-size:14px">Active Protections: Kyber768 + AES-256-GCM + ZK-Proof Ready</p>
|
|
434
|
+
<div style="background:#0f172a;padding:15px;border-radius:8px;margin-top:20px;border:1px dashed #334155">
|
|
435
|
+
<code style="color:#10b981">Endpoint: http://localhost:3000/mcp</code>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
</body>
|
|
439
|
+
`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (url === "/mcp" && method === "POST") {
|
|
443
|
+
let authInfo = null;
|
|
444
|
+
if (this.jwtValidator) {
|
|
445
|
+
const authHeader = req.headers.authorization;
|
|
446
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
447
|
+
res.writeHead(401, {
|
|
448
|
+
"WWW-Authenticate": 'Bearer error="invalid_token", error_description="Missing or malformed Authorization header"',
|
|
449
|
+
"Content-Type": "application/json"
|
|
450
|
+
});
|
|
451
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
authInfo = await this.jwtValidator.validate(authHeader.slice(7));
|
|
456
|
+
} catch (e) {
|
|
457
|
+
res.writeHead(401, {
|
|
458
|
+
"WWW-Authenticate": `Bearer error="invalid_token", error_description="${e.message}"`,
|
|
459
|
+
"Content-Type": "application/json"
|
|
460
|
+
});
|
|
461
|
+
res.end(JSON.stringify({ error: "Invalid token" }));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
let body = "";
|
|
466
|
+
req.on("data", (chunk) => body += chunk.toString());
|
|
467
|
+
req.on("end", async () => {
|
|
468
|
+
const clientIp = req.socket.remoteAddress || "127.0.0.1";
|
|
469
|
+
const rateLimitKey = authInfo?.clientId ? `client:${authInfo.clientId}` : `ip:${clientIp}`;
|
|
470
|
+
const rateStatus = this.rateLimiter.check(rateLimitKey);
|
|
471
|
+
if (!rateStatus.allowed) {
|
|
472
|
+
res.writeHead(429, {
|
|
473
|
+
"Content-Type": "application/json",
|
|
474
|
+
"Retry-After": Math.ceil(rateStatus.resetMs / 1e3).toString()
|
|
475
|
+
});
|
|
476
|
+
res.end(
|
|
477
|
+
JSON.stringify({
|
|
478
|
+
jsonrpc: "2.0",
|
|
479
|
+
id: null,
|
|
480
|
+
error: {
|
|
481
|
+
code: -32029,
|
|
482
|
+
message: "Rate limit exceeded. Too many requests."
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const jsonRequest = JSON.parse(body);
|
|
490
|
+
const mcpMethod = req.headers["mcp-method"];
|
|
491
|
+
const mcpName = req.headers["mcp-name"];
|
|
492
|
+
const mcpVersion = req.headers["mcp-protocol-version"];
|
|
493
|
+
if (mcpMethod && mcpMethod !== jsonRequest.method) {
|
|
494
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
495
|
+
res.end(
|
|
496
|
+
JSON.stringify({
|
|
497
|
+
jsonrpc: "2.0",
|
|
498
|
+
id: jsonRequest.id ?? null,
|
|
499
|
+
error: {
|
|
500
|
+
code: -32020,
|
|
501
|
+
message: `HeaderMismatch: 'Mcp-Method' header (${mcpMethod}) does not match JSON-RPC method (${jsonRequest.method})`
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (mcpName && jsonRequest.params?.name && mcpName !== jsonRequest.params.name) {
|
|
508
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
509
|
+
res.end(
|
|
510
|
+
JSON.stringify({
|
|
511
|
+
jsonrpc: "2.0",
|
|
512
|
+
id: jsonRequest.id ?? null,
|
|
513
|
+
error: {
|
|
514
|
+
code: -32020,
|
|
515
|
+
message: `HeaderMismatch: 'Mcp-Name' header (${mcpName}) does not match target name (${jsonRequest.params.name})`
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (mcpVersion && jsonRequest.params) {
|
|
522
|
+
if (!jsonRequest.params._meta) {
|
|
523
|
+
jsonRequest.params._meta = {};
|
|
524
|
+
}
|
|
525
|
+
if (!jsonRequest.params._meta["io.modelcontextprotocol/protocolVersion"]) {
|
|
526
|
+
jsonRequest.params._meta["io.modelcontextprotocol/protocolVersion"] = mcpVersion;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const response = await this.router.dispatch(jsonRequest, authInfo);
|
|
530
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
531
|
+
res.end(JSON.stringify(response));
|
|
532
|
+
} catch (e) {
|
|
533
|
+
log.info(
|
|
534
|
+
`[LIOP-Gateway] Error processing JSON-RPC payload: ${e.message}`
|
|
535
|
+
);
|
|
536
|
+
res.writeHead(400);
|
|
537
|
+
res.end(
|
|
538
|
+
JSON.stringify({
|
|
539
|
+
jsonrpc: "2.0",
|
|
540
|
+
error: { code: -32700, message: "Parse error" }
|
|
541
|
+
})
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
} else {
|
|
546
|
+
res.writeHead(404);
|
|
547
|
+
res.end("Not Found");
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
handleGrpcStream(stream) {
|
|
552
|
+
stream.on("data", (chunk) => {
|
|
553
|
+
const data = chunk;
|
|
554
|
+
if (data)
|
|
555
|
+
log.info(
|
|
556
|
+
`[LIOP-Gateway] Native gRPC Proxy passing ${data.length} bytes`
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
stream.respond({ ":status": 200, "content-type": "application/grpc" });
|
|
560
|
+
stream.end();
|
|
561
|
+
}
|
|
562
|
+
handleMcpH2Stream(stream, headers) {
|
|
563
|
+
let body = "";
|
|
564
|
+
stream.on("data", (chunk) => body += chunk.toString());
|
|
565
|
+
stream.on("end", async () => {
|
|
566
|
+
try {
|
|
567
|
+
let authInfo = null;
|
|
568
|
+
if (this.jwtValidator) {
|
|
569
|
+
const authHeader = headers.authorization;
|
|
570
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
571
|
+
stream.respond({
|
|
572
|
+
":status": 401,
|
|
573
|
+
"www-authenticate": 'Bearer error="invalid_token", error_description="Missing or malformed Authorization header"',
|
|
574
|
+
"content-type": "application/json"
|
|
575
|
+
});
|
|
576
|
+
stream.end(JSON.stringify({ error: "Unauthorized" }));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
authInfo = await this.jwtValidator.validate(authHeader.slice(7));
|
|
581
|
+
} catch (e) {
|
|
582
|
+
stream.respond({
|
|
583
|
+
":status": 401,
|
|
584
|
+
"www-authenticate": `Bearer error="invalid_token", error_description="${e.message}"`,
|
|
585
|
+
"content-type": "application/json"
|
|
586
|
+
});
|
|
587
|
+
stream.end(JSON.stringify({ error: "Invalid token" }));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const jsonRequest = JSON.parse(body);
|
|
592
|
+
const mcpMethod = headers["mcp-method"];
|
|
593
|
+
const mcpName = headers["mcp-name"];
|
|
594
|
+
const mcpVersion = headers["mcp-protocol-version"];
|
|
595
|
+
if (mcpMethod && mcpMethod !== jsonRequest.method) {
|
|
596
|
+
stream.respond({
|
|
597
|
+
":status": 400,
|
|
598
|
+
"content-type": "application/json"
|
|
599
|
+
});
|
|
600
|
+
stream.end(
|
|
601
|
+
JSON.stringify({
|
|
602
|
+
jsonrpc: "2.0",
|
|
603
|
+
id: jsonRequest.id ?? null,
|
|
604
|
+
error: {
|
|
605
|
+
code: -32020,
|
|
606
|
+
message: `HeaderMismatch: 'Mcp-Method' header (${mcpMethod}) does not match JSON-RPC method (${jsonRequest.method})`
|
|
607
|
+
}
|
|
608
|
+
})
|
|
609
|
+
);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (mcpName && jsonRequest.params?.name && mcpName !== jsonRequest.params.name) {
|
|
613
|
+
stream.respond({
|
|
614
|
+
":status": 400,
|
|
615
|
+
"content-type": "application/json"
|
|
616
|
+
});
|
|
617
|
+
stream.end(
|
|
618
|
+
JSON.stringify({
|
|
619
|
+
jsonrpc: "2.0",
|
|
620
|
+
id: jsonRequest.id ?? null,
|
|
621
|
+
error: {
|
|
622
|
+
code: -32020,
|
|
623
|
+
message: `HeaderMismatch: 'Mcp-Name' header (${mcpName}) does not match target name (${jsonRequest.params.name})`
|
|
624
|
+
}
|
|
625
|
+
})
|
|
626
|
+
);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (mcpVersion && jsonRequest.params) {
|
|
630
|
+
if (!jsonRequest.params._meta) {
|
|
631
|
+
jsonRequest.params._meta = {};
|
|
632
|
+
}
|
|
633
|
+
if (!jsonRequest.params._meta["io.modelcontextprotocol/protocolVersion"]) {
|
|
634
|
+
jsonRequest.params._meta["io.modelcontextprotocol/protocolVersion"] = mcpVersion;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const response = await this.router.dispatch(jsonRequest, authInfo);
|
|
638
|
+
if (response) {
|
|
639
|
+
stream.respond({
|
|
640
|
+
":status": 200,
|
|
641
|
+
"content-type": "application/json"
|
|
642
|
+
});
|
|
643
|
+
stream.end(JSON.stringify(response));
|
|
644
|
+
} else stream.close();
|
|
645
|
+
} catch (_e) {
|
|
646
|
+
stream.respond({ ":status": 400 });
|
|
647
|
+
stream.end();
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
async listen(port, host = "0.0.0.0") {
|
|
652
|
+
if (this.meshNode) {
|
|
653
|
+
await this.meshNode.start();
|
|
654
|
+
const tools = this.liopServer.listTools();
|
|
655
|
+
for (const tool of tools) {
|
|
656
|
+
await this.meshNode.announceCapability(tool.name);
|
|
657
|
+
log.info(
|
|
658
|
+
`[LIOP-Gateway] \u{1F4E1} Announced local tool to Mesh: ${tool.name}`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return new Promise((resolve, reject) => {
|
|
663
|
+
this.netServer.on("error", (err) => {
|
|
664
|
+
if (err.code === "EADDRINUSE") {
|
|
665
|
+
log.info(
|
|
666
|
+
`[LIOP-Gateway] FATAL: Port ${port} is already in use by another process.`
|
|
667
|
+
);
|
|
668
|
+
} else {
|
|
669
|
+
log.error(`[LIOP-Gateway] Binding Error: ${err.message}`);
|
|
670
|
+
}
|
|
671
|
+
reject(err);
|
|
672
|
+
});
|
|
673
|
+
this.netServer.listen(port, host, () => {
|
|
674
|
+
const addr = this.netServer.address();
|
|
675
|
+
const actualHost = typeof addr === "string" ? addr : addr?.address || host;
|
|
676
|
+
const assignedPort = typeof addr === "string" ? port : addr?.port || port;
|
|
677
|
+
log.info(
|
|
678
|
+
`[LIOP-Gateway] \u2705 Transformer Mesh Gateway READY and listening on ${actualHost}:${assignedPort}`
|
|
679
|
+
);
|
|
680
|
+
resolve(assignedPort);
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
isDrained() {
|
|
685
|
+
return this.isDraining;
|
|
686
|
+
}
|
|
687
|
+
async drain(timeoutMs = 5e3) {
|
|
688
|
+
this.isDraining = true;
|
|
689
|
+
log.info(
|
|
690
|
+
`[LIOP-Gateway] Draining gateway connections (active: ${this.activeRequests}, timeout: ${timeoutMs}ms)...`
|
|
691
|
+
);
|
|
692
|
+
const start = Date.now();
|
|
693
|
+
while (this.activeRequests > 0 && Date.now() - start < timeoutMs) {
|
|
694
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
695
|
+
}
|
|
696
|
+
await this.stop();
|
|
697
|
+
}
|
|
698
|
+
async stop() {
|
|
699
|
+
if (this.meshNode) {
|
|
700
|
+
await this.meshNode.stop();
|
|
701
|
+
}
|
|
702
|
+
this.rateLimiter.close();
|
|
703
|
+
this.netServer.close();
|
|
704
|
+
this.h2Server.close();
|
|
705
|
+
this.h1Server.close();
|
|
706
|
+
}
|
|
707
|
+
getRouter() {
|
|
708
|
+
return this.router;
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
export { GRPC_WEB_CONSTANTS, InMemoryRateLimiter, LiopHybridGateway, buildProtectedResourceMetadata, decodeFrames, dispatchGrpcWebRequest, encodeDataFrame, encodeTrailerFrame, isGrpcWebRequest };
|
|
713
|
+
//# sourceMappingURL=chunk-UCJEK75U.js.map
|
|
714
|
+
//# sourceMappingURL=chunk-UCJEK75U.js.map
|