@lensmcp/cluster 1.11.0 → 1.13.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.
Files changed (101) hide show
  1. package/executors/gateway/gateway-errors.d.ts +36 -0
  2. package/executors/gateway/gateway-errors.d.ts.map +1 -0
  3. package/executors/gateway/gateway-errors.js +43 -0
  4. package/executors/gateway/gateway.lib.d.ts +15 -167
  5. package/executors/gateway/gateway.lib.d.ts.map +1 -1
  6. package/executors/gateway/gateway.lib.js +24 -1034
  7. package/executors/gateway/main.prod-gateway.js +74 -2
  8. package/executors/gateway/prod-gateway.lib.d.ts +19 -143
  9. package/executors/gateway/prod-gateway.lib.d.ts.map +1 -1
  10. package/executors/gateway/prod-gateway.lib.js +16 -831
  11. package/executors/gateway/prod-runtime/access-log.d.ts +9 -0
  12. package/executors/gateway/prod-runtime/access-log.d.ts.map +1 -0
  13. package/executors/gateway/prod-runtime/access-log.js +24 -0
  14. package/executors/gateway/prod-runtime/app.d.ts +23 -0
  15. package/executors/gateway/prod-runtime/app.d.ts.map +1 -0
  16. package/executors/gateway/prod-runtime/app.js +123 -0
  17. package/executors/gateway/prod-runtime/auth.d.ts +30 -0
  18. package/executors/gateway/prod-runtime/auth.d.ts.map +1 -0
  19. package/executors/gateway/prod-runtime/auth.js +51 -0
  20. package/executors/gateway/prod-runtime/cors.d.ts +19 -0
  21. package/executors/gateway/prod-runtime/cors.d.ts.map +1 -0
  22. package/executors/gateway/prod-runtime/cors.js +40 -0
  23. package/executors/gateway/prod-runtime/edge.d.ts +16 -0
  24. package/executors/gateway/prod-runtime/edge.d.ts.map +1 -0
  25. package/executors/gateway/prod-runtime/edge.js +65 -0
  26. package/executors/gateway/prod-runtime/handler.d.ts +32 -0
  27. package/executors/gateway/prod-runtime/handler.d.ts.map +1 -0
  28. package/executors/gateway/prod-runtime/handler.js +226 -0
  29. package/executors/gateway/prod-runtime/hooks.d.ts +20 -0
  30. package/executors/gateway/prod-runtime/hooks.d.ts.map +1 -0
  31. package/executors/gateway/prod-runtime/hooks.js +42 -0
  32. package/executors/gateway/prod-runtime/observability.d.ts +23 -0
  33. package/executors/gateway/prod-runtime/observability.d.ts.map +1 -0
  34. package/executors/gateway/prod-runtime/observability.js +125 -0
  35. package/executors/gateway/prod-runtime/rollout.d.ts +18 -0
  36. package/executors/gateway/prod-runtime/rollout.d.ts.map +1 -0
  37. package/executors/gateway/prod-runtime/rollout.js +103 -0
  38. package/executors/gateway/prod-runtime/routing.d.ts +4 -0
  39. package/executors/gateway/prod-runtime/routing.d.ts.map +1 -0
  40. package/executors/gateway/prod-runtime/routing.js +40 -0
  41. package/executors/gateway/prod-runtime/server.d.ts +3 -0
  42. package/executors/gateway/prod-runtime/server.d.ts.map +1 -0
  43. package/executors/gateway/prod-runtime/server.js +79 -0
  44. package/executors/gateway/prod-runtime/trust.d.ts +12 -0
  45. package/executors/gateway/prod-runtime/trust.d.ts.map +1 -0
  46. package/executors/gateway/prod-runtime/trust.js +32 -0
  47. package/executors/gateway/prod-runtime/types.d.ts +241 -0
  48. package/executors/gateway/prod-runtime/types.d.ts.map +1 -0
  49. package/executors/gateway/prod-runtime/types.js +2 -0
  50. package/executors/gateway/prod-runtime/upgrade.d.ts +21 -0
  51. package/executors/gateway/prod-runtime/upgrade.d.ts.map +1 -0
  52. package/executors/gateway/prod-runtime/upgrade.js +116 -0
  53. package/executors/gateway/prod-runtime/upstream.d.ts +13 -0
  54. package/executors/gateway/prod-runtime/upstream.d.ts.map +1 -0
  55. package/executors/gateway/prod-runtime/upstream.js +21 -0
  56. package/executors/gateway/runtime/auth.d.ts +8 -0
  57. package/executors/gateway/runtime/auth.d.ts.map +1 -0
  58. package/executors/gateway/runtime/auth.js +60 -0
  59. package/executors/gateway/runtime/dev-auth.d.ts +9 -0
  60. package/executors/gateway/runtime/dev-auth.d.ts.map +1 -0
  61. package/executors/gateway/runtime/dev-auth.js +79 -0
  62. package/executors/gateway/runtime/discovery.d.ts +17 -0
  63. package/executors/gateway/runtime/discovery.d.ts.map +1 -0
  64. package/executors/gateway/runtime/discovery.js +111 -0
  65. package/executors/gateway/runtime/edge.d.ts +31 -0
  66. package/executors/gateway/runtime/edge.d.ts.map +1 -0
  67. package/executors/gateway/runtime/edge.js +47 -0
  68. package/executors/gateway/runtime/handler.d.ts +24 -0
  69. package/executors/gateway/runtime/handler.d.ts.map +1 -0
  70. package/executors/gateway/runtime/handler.js +165 -0
  71. package/executors/gateway/runtime/hooks.d.ts +23 -0
  72. package/executors/gateway/runtime/hooks.d.ts.map +1 -0
  73. package/executors/gateway/runtime/hooks.js +55 -0
  74. package/executors/gateway/runtime/lens-children.d.ts +13 -0
  75. package/executors/gateway/runtime/lens-children.d.ts.map +1 -0
  76. package/executors/gateway/runtime/lens-children.js +136 -0
  77. package/executors/gateway/runtime/lifecycle.d.ts +13 -0
  78. package/executors/gateway/runtime/lifecycle.d.ts.map +1 -0
  79. package/executors/gateway/runtime/lifecycle.js +179 -0
  80. package/executors/gateway/runtime/observability.d.ts +17 -0
  81. package/executors/gateway/runtime/observability.d.ts.map +1 -0
  82. package/executors/gateway/runtime/observability.js +107 -0
  83. package/executors/gateway/runtime/proxy.d.ts +20 -0
  84. package/executors/gateway/runtime/proxy.d.ts.map +1 -0
  85. package/executors/gateway/runtime/proxy.js +113 -0
  86. package/executors/gateway/runtime/scope.d.ts +16 -0
  87. package/executors/gateway/runtime/scope.d.ts.map +1 -0
  88. package/executors/gateway/runtime/scope.js +68 -0
  89. package/executors/gateway/runtime/server.d.ts +3 -0
  90. package/executors/gateway/runtime/server.d.ts.map +1 -0
  91. package/executors/gateway/runtime/server.js +168 -0
  92. package/executors/gateway/runtime/service-keys.d.ts +11 -0
  93. package/executors/gateway/runtime/service-keys.d.ts.map +1 -0
  94. package/executors/gateway/runtime/service-keys.js +49 -0
  95. package/executors/gateway/runtime/types.d.ts +234 -0
  96. package/executors/gateway/runtime/types.d.ts.map +1 -0
  97. package/executors/gateway/runtime/types.js +2 -0
  98. package/executors/gateway/runtime/upgrade.d.ts +22 -0
  99. package/executors/gateway/runtime/upgrade.d.ts.map +1 -0
  100. package/executors/gateway/runtime/upgrade.js +63 -0
  101. package/package.json +3 -3
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Opt-in one-JSON-line-per-request access log to stdout, shaped for Cloud
3
+ * Logging (severity + time + message). Covers EVERY response (incl. 401/404/503
4
+ * that never reached an upstream); `reply.elapsedTime` gives latency for free.
5
+ */
6
+ import type { FastifyReply, FastifyRequest } from 'fastify';
7
+ import type { ReqState } from './types';
8
+ export declare const logAccess: (request: FastifyRequest, reply: FastifyReply, st: ReqState | undefined) => void;
9
+ //# sourceMappingURL=access-log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-log.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/prod-runtime/access-log.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,eAAO,MAAM,SAAS,GAAI,SAAS,cAAc,EAAE,OAAO,YAAY,EAAE,IAAI,QAAQ,GAAG,SAAS,KAAG,IAkBlG,CAAC"}
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logAccess = void 0;
4
+ const logAccess = (request, reply, st) => {
5
+ const req = request.raw;
6
+ const status = reply.statusCode ?? 0;
7
+ const host = (req.headers.host || '').split(':')[0];
8
+ const path = (req.url || '/').split('?')[0];
9
+ const ms = Math.round(reply.elapsedTime);
10
+ try {
11
+ console.log(JSON.stringify({
12
+ severity: status >= 500 ? 'ERROR' : status >= 400 ? 'WARNING' : 'INFO',
13
+ time: new Date().toISOString(),
14
+ message: `${req.method} ${host}${path} ${status} ${ms}ms`,
15
+ method: req.method, host, path, status, ms,
16
+ ...(st ? { service: st.route.service, upstream: st.upLabel } : {}),
17
+ ...(st?.version ? { version: st.version } : {}),
18
+ ...(st?.caller ? { caller: st.caller } : {}),
19
+ ...(req.headers['x-request-id'] ? { requestId: String(req.headers['x-request-id']) } : {}),
20
+ }));
21
+ }
22
+ catch { /* never let logging break the response */ }
23
+ };
24
+ exports.logAccess = logAccess;
@@ -0,0 +1,23 @@
1
+ import type * as http from 'node:http';
2
+ import type * as net from 'node:net';
3
+ import { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify';
4
+ import type { TraceSpan } from '../otel-tracing';
5
+ import type { ProdHookContext, ProdRuntime, ReqState } from './types';
6
+ import type { Observability } from './observability';
7
+ import type { CorsLayer } from './cors';
8
+ import type { HooksLayer } from './hooks';
9
+ export interface AppDeps {
10
+ rt: ProdRuntime;
11
+ obs: Observability;
12
+ cors: CorsLayer;
13
+ hooks: HooksLayer;
14
+ handler: (request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
15
+ upgrade: (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => void;
16
+ reqState: WeakMap<FastifyRequest, ReqState>;
17
+ spanByReq: WeakMap<FastifyRequest, TraceSpan>;
18
+ ctxByReq: WeakMap<FastifyRequest, ProdHookContext>;
19
+ }
20
+ export declare function createApp(deps: AppDeps): {
21
+ buildApp: () => Promise<FastifyInstance>;
22
+ };
23
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/prod-runtime/app.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,KAAK,GAAG,MAAM,UAAU,CAAC;AACrC,OAAgB,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,SAAS,CAAC;AAKhG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,WAAW,CAAC;IAChB,GAAG,EAAE,aAAa,CAAC;IACnB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5E,OAAO,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/E,QAAQ,EAAE,OAAO,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IAC5C,SAAS,EAAE,OAAO,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;IAC9C,QAAQ,EAAE,OAAO,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;CACpD;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG;IAAE,QAAQ,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAA;CAAE,CA0FrF"}
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createApp = createApp;
4
+ const tslib_1 = require("tslib");
5
+ /**
6
+ * Builds one Fastify app per port (shared route table + providers). Streams
7
+ * every body straight through (never parses/buffers — this is a proxy), wires
8
+ * reply-from's undici pool, the operational probes (/livez /readyz /statusz
9
+ * /metricsz), and the trust / CORS / edge-finish hooks.
10
+ */
11
+ const node_crypto_1 = require("node:crypto");
12
+ const fastify_1 = tslib_1.__importDefault(require("fastify"));
13
+ const reply_from_1 = tslib_1.__importDefault(require("@fastify/reply-from"));
14
+ const edge_1 = require("./edge");
15
+ const trust_1 = require("./trust");
16
+ const access_log_1 = require("./access-log");
17
+ function createApp(deps) {
18
+ const { rt, obs, cors, hooks, handler, upgrade, reqState, spanByReq, ctxByReq } = deps;
19
+ const opts = rt.opts;
20
+ const buildApp = async () => {
21
+ const keepAlive = opts.server?.keepAliveTimeout ?? 620_000; // > GCP external ALB 600s backend keep-alive
22
+ const app = (0, fastify_1.default)({
23
+ logger: false,
24
+ keepAliveTimeout: keepAlive,
25
+ requestTimeout: opts.server?.requestTimeout ?? 0, // 0 = no inbound request timeout (long-lived streaming)
26
+ ...(opts.server?.connectionTimeout !== undefined ? { connectionTimeout: opts.server.connectionTimeout } : {}),
27
+ ...(opts.trustProxyIp ? { trustProxy: true } : {}),
28
+ ...(opts.tls ? { https: { key: opts.tls.key, cert: opts.tls.cert } } : {}),
29
+ });
30
+ // headersTimeout must stay above keepAliveTimeout, else Node may close an idle
31
+ // keep-alive socket mid-request under load. Default: just above keepAlive.
32
+ app.server.headersTimeout = opts.server?.headersTimeout ?? keepAlive + 10_000;
33
+ // Stream every body straight through — never parse/buffer (this is a proxy).
34
+ app.removeAllContentTypeParsers();
35
+ app.addContentTypeParser('*', (_req, payload, done) => done(null, payload));
36
+ await app.register(reply_from_1.default, {
37
+ disableRequestLogging: true,
38
+ undici: {
39
+ connections: opts.undici?.connections ?? 256,
40
+ pipelining: opts.undici?.pipelining ?? 1,
41
+ keepAliveTimeout: opts.undici?.keepAliveTimeout ?? 60_000,
42
+ bodyTimeout: opts.undici?.bodyTimeout ?? 0, // 0 = unlimited — never cut long-lived SSE / streaming MCP responses
43
+ headersTimeout: opts.undici?.headersTimeout ?? 60_000, // upstream must send response headers within 60s
44
+ ...(opts.undici?.allowH2 ? { allowH2: true } : {}), // multiplex many MCP streams over few h2 conns
45
+ connect: {
46
+ rejectUnauthorized: opts.undici?.rejectUnauthorized ?? false, // local-CA/self-signed by default
47
+ ...(opts.undici?.cert ? { cert: opts.undici.cert } : {}), // ── mTLS: present a client cert to upstreams ──
48
+ ...(opts.undici?.key ? { key: opts.undici.key } : {}),
49
+ ...(opts.undici?.ca ? { ca: opts.undici.ca } : {}), // trust a private CA for the server side
50
+ },
51
+ },
52
+ });
53
+ // Host-agnostic operational probes (answered before routing).
54
+ app.get('/livez', (_req, reply) => { reply.type('text/plain').send('ok'); });
55
+ app.get('/readyz', (_req, reply) => {
56
+ const ready = rt.table.routes.length > 0;
57
+ reply.code(ready ? 200 : 503).type('text/plain').send(ready ? 'ready' : 'no routes loaded');
58
+ });
59
+ // Per-service health rollup (opt-in; token-guarded). Off by default → 404 (don't leak topology).
60
+ app.get('/statusz', (request, reply) => {
61
+ // Fail closed: served ONLY when both enabled AND a token is set. Constant-time token compare.
62
+ if (!opts.statusEndpoint || !opts.statusToken) {
63
+ reply.code(404).type('text/plain').send('not found');
64
+ return;
65
+ }
66
+ const tok = request.query?.token ?? (0, edge_1.hdr)(request.raw.headers['x-status-token']) ?? '';
67
+ const a = Buffer.from(tok), b = Buffer.from(opts.statusToken);
68
+ if (a.length !== b.length || !(0, node_crypto_1.timingSafeEqual)(a, b)) {
69
+ reply.code(401).type('text/plain').send('unauthorized');
70
+ return;
71
+ }
72
+ reply.type('application/json').send(JSON.stringify({ generatedAt: Date.now(), services: obs.buildStatusSnapshot(), endpoints: opts.healthChecker?.snapshot() ?? [] }));
73
+ });
74
+ // Process self-metrics — same token gate as /statusz.
75
+ app.get('/metricsz', (request, reply) => {
76
+ if (!opts.metrics || !opts.statusToken) {
77
+ reply.code(404).type('text/plain').send('not found');
78
+ return;
79
+ }
80
+ const tok = request.query?.token ?? (0, edge_1.hdr)(request.raw.headers['x-status-token']) ?? '';
81
+ const a = Buffer.from(tok), b = Buffer.from(opts.statusToken);
82
+ if (a.length !== b.length || !(0, node_crypto_1.timingSafeEqual)(a, b)) {
83
+ reply.code(401).type('text/plain').send('unauthorized');
84
+ return;
85
+ }
86
+ reply.type('application/json').send(JSON.stringify(opts.metrics.snapshot()));
87
+ });
88
+ app.addHook('onRequest', (request, _reply, done) => { (0, trust_1.stripTrust)(request.raw); done(); });
89
+ // Edge CORS authority: stamp Access-Control-Allow-Origin on EVERY response that lacks it (gateway-generated),
90
+ // but NEVER bless a proxied upstream success cross-origin (see cors.applyResponseCors).
91
+ app.addHook('onSend', (request, reply, payload, done) => { cors.applyResponseCors(request, reply); done(null, payload); });
92
+ app.addHook('onResponse', (request, reply, done) => {
93
+ const st = reqState.get(request);
94
+ if (st) {
95
+ obs.recordEdge(st.route, Date.now() - st.startedAt, reply.statusCode ?? 0, st.caller, st.version);
96
+ obs.finish(st.trace, st.route.service, reply.statusCode ?? 0, { upstream: st.upLabel, ...(st.version ? { version: st.version } : {}), ...(st.caller ? { caller: st.caller } : {}) });
97
+ reqState.delete(request);
98
+ }
99
+ const sp = spanByReq.get(request);
100
+ if (sp) {
101
+ opts.tracing.end(sp, reply.statusCode ?? 0, { ...(st?.upLabel ? { 'upstream.url': st.upLabel } : {}), ...(st?.version ? { 'lensmcp.version': st.version } : {}) });
102
+ spanByReq.delete(request);
103
+ }
104
+ opts.metrics?.recordRequest(reply.statusCode ?? 0);
105
+ if (rt.accessLog)
106
+ (0, access_log_1.logAccess)(request, reply, st);
107
+ // Customer onResponse (terminal phase — observes the finished response).
108
+ const ctx = ctxByReq.get(request);
109
+ if (ctx) {
110
+ ctx.status = reply.statusCode ?? 0;
111
+ if (hooks.has('onResponse'))
112
+ void hooks.run('onResponse', ctx);
113
+ ctxByReq.delete(request);
114
+ }
115
+ done();
116
+ });
117
+ app.all('/', handler);
118
+ app.all('/*', handler);
119
+ app.server.on('upgrade', upgrade);
120
+ return app;
121
+ };
122
+ return { buildApp };
123
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The edge-auth layer — ONE source for the HTTP handler and the WS upgrade so
3
+ * they can't drift. `internal(...)` validates the caller's `x-api-key` on an
4
+ * `internal.<host>` route; `public(...)` runs the JWT/authz seam (verify →
5
+ * fail-closed ABAC → stamp identity). Each returns a verdict; the caller turns a
6
+ * denial into its own rejection (handler → `finish`+`sendError`; upgrade → `socket.destroy()`).
7
+ *
8
+ * NOTE: `public()` calls `stampIdentity` and `internal()` mutates `req.headers`;
9
+ * the upgrade path rebuilds its request from `req.rawHeaders` (re-adding identity
10
+ * explicitly), so those header mutations are simply ignored there — no divergence.
11
+ */
12
+ import * as http from 'node:http';
13
+ import { type AuthMode, type Route } from '../manifest';
14
+ import type { GatewayErrorReason } from '../gateway-errors';
15
+ import type { GwTrace, ProdRuntime } from './types';
16
+ import type { Observability } from './observability';
17
+ import type { RolloutLayer } from './rollout';
18
+ export interface AuthVerdict {
19
+ ok: boolean;
20
+ caller?: string;
21
+ status?: number;
22
+ reason?: GatewayErrorReason;
23
+ detail?: string;
24
+ }
25
+ export interface EdgeAuth {
26
+ internal(req: http.IncomingMessage, route: Route, trace: GwTrace | undefined): AuthVerdict;
27
+ public(req: http.IncomingMessage, route: Route, cpath: string, claims: Record<string, unknown> | undefined, fallbackIp: string, trace: GwTrace | undefined): AuthVerdict;
28
+ }
29
+ export declare function createEdgeAuth(rt: ProdRuntime, obs: Observability, rollout: RolloutLayer, authenticate: (mode: AuthMode, req: http.IncomingMessage) => void): EdgeAuth;
30
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/prod-runtime/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAA6B,KAAK,QAAQ,EAAqB,KAAK,KAAK,EAAE,MAAM,aAAa,CAAC;AAEtG,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;IAC3F,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;CAC1K;AAED,wBAAgB,cAAc,CAC5B,EAAE,EAAE,WAAW,EACf,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,YAAY,EACrB,YAAY,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,IAAI,GAChE,QAAQ,CAiCV"}
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createEdgeAuth = createEdgeAuth;
4
+ const manifest_1 = require("../manifest");
5
+ const trust_1 = require("./trust");
6
+ function createEdgeAuth(rt, obs, rollout, authenticate) {
7
+ const internal = (req, _route, trace) => {
8
+ const caller = rt.keyToProject.get(String(req.headers['x-api-key'] ?? ''));
9
+ if (!caller) {
10
+ obs.step(trace, 'auth', { mode: 'x-api-key', ok: false });
11
+ return { ok: false, status: 401, reason: 'unauthorized', detail: 'Invalid or missing x-api-key for internal route.' };
12
+ }
13
+ req.headers['x-api-key-id'] = caller;
14
+ delete req.headers['x-api-key'];
15
+ obs.step(trace, 'auth', { mode: 'x-api-key', ok: true, caller });
16
+ return { ok: true, caller };
17
+ };
18
+ const publicAuth = (req, route, cpath, claims, fallbackIp, trace) => {
19
+ const m = rt.table.manifestByService.get(route.service);
20
+ const resolved = m ? (0, manifest_1.authRuleForPath)(m, cpath, req.method ?? 'GET', false) : { mode: route.auth };
21
+ if (resolved.mode === 'jwt') {
22
+ if (rt.opts.identify) {
23
+ if (!claims) {
24
+ obs.step(trace, 'auth', { mode: 'jwt', ok: false });
25
+ return { ok: false, status: 401, reason: 'unauthorized', detail: 'Unauthorized: invalid or missing token' };
26
+ }
27
+ }
28
+ else {
29
+ try {
30
+ authenticate('jwt', req);
31
+ }
32
+ catch (e) {
33
+ obs.step(trace, 'auth', { mode: 'jwt', ok: false });
34
+ return { ok: false, status: 401, reason: 'unauthorized', detail: `Unauthorized: ${e.message}` };
35
+ }
36
+ }
37
+ // edge ABAC: a present rule MUST be satisfied by VERIFIED claims. No claims (no verifier) → DENY (fail closed).
38
+ if (resolved.rule && (!claims || !(0, manifest_1.evalRule)(resolved.rule, rollout.buildAttrs(req, fallbackIp, claims, undefined)))) {
39
+ obs.step(trace, 'authz', { ok: false });
40
+ return { ok: false, status: 403, reason: 'forbidden', detail: 'Forbidden: insufficient permission' };
41
+ }
42
+ obs.step(trace, 'auth', { mode: 'jwt', ok: true });
43
+ if (claims)
44
+ (0, trust_1.stampIdentity)(req, claims);
45
+ return { ok: true };
46
+ }
47
+ obs.step(trace, 'auth', { mode: resolved.mode });
48
+ return { ok: true };
49
+ };
50
+ return { internal, public: publicAuth };
51
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The edge CORS authority. The gateway is the CORS authority for its OWN
3
+ * responses (no-route / errors / preflights) and for credentialed first-party
4
+ * origins, but must NEVER bless a proxied upstream success cross-origin (the
5
+ * upstream owns CORS on its own app data — CWE-942).
6
+ */
7
+ import type * as http from 'node:http';
8
+ import type { FastifyReply, FastifyRequest } from 'fastify';
9
+ import type { ProdRuntime, ReqState } from './types';
10
+ export interface CorsLayer {
11
+ /** Stamp CORS on a gateway-generated reply (no-route / preflight): exact origin +
12
+ * credentials for an allowlisted origin, else a reflected origin WITHOUT credentials. */
13
+ setCors(reply: FastifyReply, req: http.IncomingMessage): void;
14
+ /** onSend: add CORS to any response that lacks it — a credentialed-allowlist origin always
15
+ * (proxied or generated); a non-allowlisted origin ONLY on a gateway-GENERATED reply. */
16
+ applyResponseCors(request: FastifyRequest, reply: FastifyReply): void;
17
+ }
18
+ export declare function createCors(rt: ProdRuntime, reqState: WeakMap<FastifyRequest, ReqState>): CorsLayer;
19
+ //# sourceMappingURL=cors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/prod-runtime/cors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAErD,MAAM,WAAW,SAAS;IACxB;8FAC0F;IAC1F,OAAO,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;IAC9D;8FAC0F;IAC1F,iBAAiB,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CACvE;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,CAAC,cAAc,EAAE,QAAQ,CAAC,GAAG,SAAS,CAqClG"}
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCors = createCors;
4
+ function createCors(rt, reqState) {
5
+ const setCors = (reply, req) => {
6
+ const origin = req.headers.origin;
7
+ // Credentialed allowlist: echo the EXACT origin + allow credentials (cookies). NEVER `*` with
8
+ // credentials (forbidden by the Fetch spec; a cross-site read hole).
9
+ if (origin && rt.corsCredentialOrigins.has(origin.toLowerCase())) {
10
+ reply.header('access-control-allow-origin', origin);
11
+ reply.header('access-control-allow-credentials', 'true');
12
+ reply.header('vary', 'Origin');
13
+ return;
14
+ }
15
+ // Otherwise — UNAUTHENTICATED gateway responses (404 / health / discovery / preflight):
16
+ // reflect the Origin for convenience but DO NOT allow credentials.
17
+ reply.header('access-control-allow-origin', String(origin ?? '*'));
18
+ reply.header('vary', 'Origin');
19
+ };
20
+ const applyResponseCors = (request, reply) => {
21
+ const origin = request.headers.origin;
22
+ if (origin && !reply.getHeader('access-control-allow-origin')) {
23
+ // Credentialed allowlist: EXACT origin + ACAC:true so a credentialed first-party app can read a
24
+ // cookie-setting routed response AND a gateway-generated 401/403/5xx passes the browser CORS check.
25
+ if (rt.corsCredentialOrigins.has(origin.toLowerCase())) {
26
+ reply.header('access-control-allow-origin', origin);
27
+ reply.header('access-control-allow-credentials', 'true');
28
+ reply.header('vary', 'Origin');
29
+ }
30
+ else if (!reqState.get(request)?.proxied) {
31
+ // A non-allowlisted origin is reflected ONLY on a gateway-GENERATED reply (error / no-route),
32
+ // NEVER on a successful PROXIED upstream response — that would be a blanket cross-origin read
33
+ // of app data (CWE-942). The upstream owns CORS on its own responses.
34
+ reply.header('access-control-allow-origin', String(origin));
35
+ reply.header('vary', 'Origin');
36
+ }
37
+ }
38
+ };
39
+ return { setCors, applyResponseCors };
40
+ }
@@ -0,0 +1,16 @@
1
+ import type * as http from 'node:http';
2
+ import { type GatewayErrorReason } from '../gateway-errors';
3
+ import type { ReplyLike } from './types';
4
+ export declare const eid: () => string;
5
+ export declare const hdr: (v: string | string[] | undefined) => string | undefined;
6
+ /**
7
+ * Send a gateway-generated edge error as the JSON envelope `{ key, status, traceId, message? }` (NOT
8
+ * `text/plain`), stamping `x-request-id` so the trace id is on the response. The SAME `{ key, status }`
9
+ * localized contract a service returns, so a browser app renders `t(key)` and surfaces the trace id.
10
+ * `detail` is the dev-facing hint (the old plain-text reason), never shown to end users.
11
+ */
12
+ export declare const sendError: (reply: ReplyLike, req: http.IncomingMessage, status: number, reason: GatewayErrorReason, detail?: string) => ReplyLike;
13
+ export declare const ENCODED_STRUCTURE: RegExp;
14
+ /** Canonical path for auth matching, or undefined if the path is unsafe (→ 400 / drop). */
15
+ export declare const canonicalPath: (rawUrl: string) => string | undefined;
16
+ //# sourceMappingURL=edge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/prod-runtime/edge.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,EAAiE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC3H,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,eAAO,MAAM,GAAG,QAAO,MAAkE,CAAC;AAE1F,eAAO,MAAM,GAAG,GAAI,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,KAAG,MAAM,GAAG,SACa,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GACpB,OAAO,SAAS,EAChB,KAAK,IAAI,CAAC,eAAe,EACzB,QAAQ,MAAM,EACd,QAAQ,kBAAkB,EAC1B,SAAS,MAAM,KACd,SAOF,CAAC;AAQF,eAAO,MAAM,iBAAiB,QAAwB,CAAC;AAIvD,2FAA2F;AAC3F,eAAO,MAAM,aAAa,GAAI,QAAQ,MAAM,KAAG,MAAM,GAAG,SAgBvD,CAAC"}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canonicalPath = exports.ENCODED_STRUCTURE = exports.sendError = exports.hdr = exports.eid = void 0;
4
+ /**
5
+ * Edge primitives for the prod gateway: id/header helpers, the localized JSON
6
+ * error envelope, and the path-canonicalization guard. Pure (no runtime state).
7
+ */
8
+ const node_crypto_1 = require("node:crypto");
9
+ const gateway_errors_1 = require("../gateway-errors");
10
+ const eid = () => Date.now().toString(36) + (0, node_crypto_1.randomBytes)(6).toString('hex');
11
+ exports.eid = eid;
12
+ const hdr = (v) => typeof v === 'string' && v !== '' ? v : Array.isArray(v) ? v[0] : undefined;
13
+ exports.hdr = hdr;
14
+ /**
15
+ * Send a gateway-generated edge error as the JSON envelope `{ key, status, traceId, message? }` (NOT
16
+ * `text/plain`), stamping `x-request-id` so the trace id is on the response. The SAME `{ key, status }`
17
+ * localized contract a service returns, so a browser app renders `t(key)` and surfaces the trace id.
18
+ * `detail` is the dev-facing hint (the old plain-text reason), never shown to end users.
19
+ */
20
+ const sendError = (reply, req, status, reason, detail) => {
21
+ const traceId = (0, gateway_errors_1.ensureRequestId)(req);
22
+ return reply
23
+ .code(status)
24
+ .header('x-request-id', traceId)
25
+ .type(gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE)
26
+ .send((0, gateway_errors_1.gatewayErrorBody)(status, reason, traceId, detail));
27
+ };
28
+ exports.sendError = sendError;
29
+ // Path-canonicalization guard (CWE-288): a reverse proxy and its backend must agree
30
+ // on the request path, or auth gets evaluated against a DIFFERENT path than the one
31
+ // served (AWS API Gateway trailing-slash bypass, IBM API Connect CVE-2025-13915,
32
+ // Kong/Envoy/NGINX normalization mismatch). We REJECT structurally-encoded paths
33
+ // (encoded dot/slash/backslash/null) outright, and return the DECODED path for auth
34
+ // matching so `/%61dmin` is matched as `/admin` — never skipped because it's encoded.
35
+ exports.ENCODED_STRUCTURE = /%2e|%2f|%5c|%00|\\/i; // encoded . / \ NUL, or a raw backslash
36
+ const NEEDS_NORMALIZE = /[%;#\\]/; // percent-encoding, matrix param, fragment, or backslash
37
+ const TRAVERSAL = /(^|\/)\.\.(?=\/|$)/; // a `..` path segment
38
+ /** Canonical path for auth matching, or undefined if the path is unsafe (→ 400 / drop). */
39
+ const canonicalPath = (rawUrl) => {
40
+ const q = rawUrl.indexOf('?');
41
+ const p = q === -1 ? rawUrl : rawUrl.slice(0, q);
42
+ if (p.indexOf('\0') !== -1)
43
+ return undefined; // raw NUL is never a valid path (mirrors the decoded-NUL reject)
44
+ // FAST PATH (the overwhelming majority of production traffic): a clean path — no percent-encoding,
45
+ // matrix params, fragment, or backslash — needs no decoding or normalization (it already equals what
46
+ // the backend resolves). Only the `..` traversal check remains. Saves a decodeURIComponent + two regex
47
+ // + a split/map/join on every request.
48
+ if (!NEEDS_NORMALIZE.test(p))
49
+ return TRAVERSAL.test(p) ? undefined : (p || '/');
50
+ // SLOW PATH: structurally-encoded → reject outright; else decode + strip matrix/fragment so
51
+ // /admin;x=1 and /admin#.css can't skip the /admin policy (CWE-288), and /%61dmin matches /admin.
52
+ if (exports.ENCODED_STRUCTURE.test(p))
53
+ return undefined;
54
+ let decoded;
55
+ try {
56
+ decoded = decodeURIComponent(p);
57
+ }
58
+ catch {
59
+ return undefined;
60
+ } // malformed %xx
61
+ if (decoded.includes('\0') || TRAVERSAL.test(decoded))
62
+ return undefined; // NUL or `..` traversal
63
+ return decoded.split('#')[0].split('/').map((s) => s.split(';')[0]).join('/') || '/';
64
+ };
65
+ exports.canonicalPath = canonicalPath;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The prod HTTP request pipeline: route → trust → auth → forward, with the
3
+ * security guards (HTTP/0.9 reject, path-canonicalization, rate-limit) BEFORE
4
+ * routing and the rollout/sticky version selection before the undici hop.
5
+ *
6
+ * Customer hook phases slot in at the analogous points, but EVERY hook call is
7
+ * guarded by `ctx` (built only when hooks are configured) — so with no hooks the
8
+ * pipeline is byte-for-byte the original (zero overhead; the pentest/perf path).
9
+ */
10
+ import type { FastifyReply, FastifyRequest } from 'fastify';
11
+ import type { TraceSpan } from '../otel-tracing';
12
+ import type { ProdHookContext, ProdRuntime, ReqState } from './types';
13
+ import type { Observability } from './observability';
14
+ import type { CorsLayer } from './cors';
15
+ import type { EdgeAuth } from './auth';
16
+ import type { UpstreamLayer } from './upstream';
17
+ import type { RolloutLayer } from './rollout';
18
+ import type { HooksLayer } from './hooks';
19
+ export interface HandlerDeps {
20
+ rt: ProdRuntime;
21
+ obs: Observability;
22
+ cors: CorsLayer;
23
+ auth: EdgeAuth;
24
+ upstream: UpstreamLayer;
25
+ rollout: RolloutLayer;
26
+ hooks: HooksLayer;
27
+ reqState: WeakMap<FastifyRequest, ReqState>;
28
+ spanByReq: WeakMap<FastifyRequest, TraceSpan>;
29
+ ctxByReq: WeakMap<FastifyRequest, ProdHookContext>;
30
+ }
31
+ export declare function createHandler(deps: HandlerDeps): (request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
32
+ //# sourceMappingURL=handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/prod-runtime/handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE1C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,WAAW,CAAC;IAChB,GAAG,EAAE,aAAa,CAAC;IACnB,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;IACtB,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC,cAAc,EAAE,QAAQ,CAAC,CAAC;IAC5C,SAAS,EAAE,OAAO,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;IAC9C,QAAQ,EAAE,OAAO,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;CACpD;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,CAmKnH"}