@lensmcp/cluster 1.18.4 → 1.18.7
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/basic-ssl.js +1 -241
- package/build-scope-patterns.js +1 -40
- package/create-webpack-dev.js +1 -186
- package/create-webpack-prod.js +1 -169
- package/executors/build/build.impl.js +1 -98
- package/executors/gateway/gateway-errors.js +1 -43
- package/executors/gateway/gateway.impl.js +1 -53
- package/executors/gateway/gateway.lib.js +1 -29
- package/executors/gateway/health-check.js +1 -66
- package/executors/gateway/jwks-verify.js +1 -121
- package/executors/gateway/main.prod-gateway.js +2 -573
- package/executors/gateway/main.rollout.js +11 -117
- package/executors/gateway/manifest.js +1 -374
- package/executors/gateway/metrics.js +1 -56
- package/executors/gateway/otel-tracing.js +1 -74
- package/executors/gateway/prod-gateway.lib.js +1 -22
- package/executors/gateway/prod-runtime/access-log.js +1 -24
- package/executors/gateway/prod-runtime/app.js +1 -123
- package/executors/gateway/prod-runtime/auth.js +1 -51
- package/executors/gateway/prod-runtime/cors.js +1 -40
- package/executors/gateway/prod-runtime/edge.js +1 -65
- package/executors/gateway/prod-runtime/handler.js +1 -226
- package/executors/gateway/prod-runtime/hooks.js +1 -42
- package/executors/gateway/prod-runtime/observability.js +1 -125
- package/executors/gateway/prod-runtime/rollout.js +1 -103
- package/executors/gateway/prod-runtime/routing.js +1 -40
- package/executors/gateway/prod-runtime/server.js +1 -79
- package/executors/gateway/prod-runtime/trust.js +1 -32
- package/executors/gateway/prod-runtime/types.js +1 -2
- package/executors/gateway/prod-runtime/upgrade.js +4 -116
- package/executors/gateway/prod-runtime/upstream.js +1 -21
- package/executors/gateway/providers-prod.js +1 -232
- package/executors/gateway/rate-limit.js +2 -75
- package/executors/gateway/registry-source.js +1 -131
- package/executors/gateway/rollout-ops.js +2 -167
- package/executors/gateway/runtime/auth.js +1 -64
- package/executors/gateway/runtime/chooser.js +12 -45
- package/executors/gateway/runtime/control.js +1 -128
- package/executors/gateway/runtime/dev-auth.js +1 -108
- package/executors/gateway/runtime/discovery.js +1 -123
- package/executors/gateway/runtime/edge.js +1 -47
- package/executors/gateway/runtime/handler.js +1 -183
- package/executors/gateway/runtime/hooks.js +1 -55
- package/executors/gateway/runtime/lens-children.js +1 -651
- package/executors/gateway/runtime/lifecycle.js +3 -842
- package/executors/gateway/runtime/observability.js +2 -148
- package/executors/gateway/runtime/pod-env.js +2 -89
- package/executors/gateway/runtime/proxy.js +1 -457
- package/executors/gateway/runtime/route-registry.js +1 -72
- package/executors/gateway/runtime/scope.js +1 -117
- package/executors/gateway/runtime/server.js +3 -487
- package/executors/gateway/runtime/service-keys.js +1 -49
- package/executors/gateway/runtime/types.js +1 -151
- package/executors/gateway/runtime/upgrade.js +1 -71
- package/executors/gateway/runtime/workspace-registry.js +1 -99
- package/executors/gateway/ssrf-guard.js +1 -190
- package/executors/serve/serve.impl.js +1 -280
- package/executors/trust/trust.impl.js +4 -162
- package/gateway.js +1 -35
- package/index.js +1 -16
- package/main.devserver.js +10 -1117
- package/package.json +4 -3
- package/tsgo-check-plugin.js +4 -364
- package/typecheck-bus.js +4 -256
|
@@ -1,64 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createEdgeAuth = createEdgeAuth;
|
|
4
|
-
/**
|
|
5
|
-
* The edge-auth layer (the JWT seam). Two boundaries, ONE source so the HTTP
|
|
6
|
-
* handler and the WS upgrade can't drift:
|
|
7
|
-
* - `internal(...)`: validate the CALLER's `x-api-key` on an `internal.<host>`
|
|
8
|
-
* route, stamp `x-api-key-id` (audit) + `__lensmcpCaller`.
|
|
9
|
-
* - `public(...)`: verify the end-user JWT, enforce per-route rules, inject
|
|
10
|
-
* trusted identity — mirroring the prod gateway. Only enforced when the
|
|
11
|
-
* service declares `cluster.auth` (opt-in; un-migrated services stay open).
|
|
12
|
-
* Each returns a verdict; the caller turns a denial into its own rejection
|
|
13
|
-
* (the handler → `finishTrace` + `sendError`; the upgrade → `socket.destroy()`).
|
|
14
|
-
*/
|
|
15
|
-
const manifest_1 = require("../manifest");
|
|
16
|
-
const dev_auth_1 = require("./dev-auth");
|
|
17
|
-
function createEdgeAuth(rt, obs, verifiers) {
|
|
18
|
-
const { traceStep } = obs;
|
|
19
|
-
const internal = (req, _route, trace) => {
|
|
20
|
-
// service-to-service: validate the CALLER's api key at the boundary
|
|
21
|
-
const presented = String(req.headers['x-api-key'] ?? '');
|
|
22
|
-
const caller = rt.keyToProject.get(presented);
|
|
23
|
-
if (!caller) {
|
|
24
|
-
traceStep(trace, 'auth', { mode: 'x-api-key', ok: false });
|
|
25
|
-
return { ok: false, status: 401, reason: 'unauthorized', detail: 'Invalid or missing x-api-key for internal route.' };
|
|
26
|
-
}
|
|
27
|
-
req.headers['x-api-key-id'] = caller; // audit + lens attribution
|
|
28
|
-
req.__lensmcpCaller = caller;
|
|
29
|
-
delete req.headers['x-api-key'];
|
|
30
|
-
traceStep(trace, 'auth', { mode: 'x-api-key', ok: true, caller });
|
|
31
|
-
return { ok: true, caller };
|
|
32
|
-
};
|
|
33
|
-
const publicAuth = (req, route, trace) => {
|
|
34
|
-
const policy = route.auth;
|
|
35
|
-
if (policy) {
|
|
36
|
-
const url0 = (req.url ?? '/').split('?')[0];
|
|
37
|
-
const method = (req.method ?? 'GET').toUpperCase();
|
|
38
|
-
const resolved = (0, manifest_1.authRuleFor)(policy, url0, method);
|
|
39
|
-
if (resolved.mode === 'jwt') {
|
|
40
|
-
// The verifier for THIS request's host (per-apex IdP resolution in a shared
|
|
41
|
-
// multi-workspace daemon; the host is the ROUTED host — trusted exactly the
|
|
42
|
-
// way routing itself trusts it, never taken from the token).
|
|
43
|
-
const reqHost = String(req.headers.host ?? '').split(':')[0].toLowerCase();
|
|
44
|
-
const claims = (0, dev_auth_1.verifyDevJwt)(req, verifiers?.for(reqHost));
|
|
45
|
-
if (!claims) {
|
|
46
|
-
traceStep(trace, 'auth', { mode: 'jwt', ok: false });
|
|
47
|
-
return { ok: false, status: 401, reason: 'unauthorized', detail: 'Unauthorized: invalid or missing token' };
|
|
48
|
-
}
|
|
49
|
-
if (resolved.rule && !(0, manifest_1.evalRule)(resolved.rule, (0, dev_auth_1.devAttrs)(req, claims))) {
|
|
50
|
-
traceStep(trace, 'authz', { ok: false });
|
|
51
|
-
return { ok: false, status: 403, reason: 'forbidden', detail: 'Forbidden: insufficient permission' };
|
|
52
|
-
}
|
|
53
|
-
(0, dev_auth_1.stampIdentity)(req, claims);
|
|
54
|
-
traceStep(trace, 'auth', { mode: 'jwt', ok: true });
|
|
55
|
-
return { ok: true, claims };
|
|
56
|
-
}
|
|
57
|
-
traceStep(trace, 'auth', { mode: resolved.mode });
|
|
58
|
-
return { ok: true };
|
|
59
|
-
}
|
|
60
|
-
traceStep(trace, 'auth', { mode: 'public', unconfigured: true });
|
|
61
|
-
return { ok: true };
|
|
62
|
-
};
|
|
63
|
-
return { internal, public: publicAuth };
|
|
64
|
-
}
|
|
1
|
+
"use strict";var m=Object.defineProperty;var l=(o,i)=>m(o,"name",{value:i,configurable:!0});var f=Object.defineProperty,n=l((o,i)=>f(o,"name",{value:i,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createEdgeAuth=createEdgeAuth;const manifest_1=require("../manifest"),dev_auth_1=require("./dev-auth");function createEdgeAuth(o,i,h){const{traceStep:r}=i;return{internal:n((e,c,t)=>{const u=String(e.headers["x-api-key"]??""),a=o.keyToProject.get(u);return a?(e.headers["x-api-key-id"]=a,e.__lensmcpCaller=a,delete e.headers["x-api-key"],r(t,"auth",{mode:"x-api-key",ok:!0,caller:a}),{ok:!0,caller:a}):(r(t,"auth",{mode:"x-api-key",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Invalid or missing x-api-key for internal route."})},"internal"),public:n((e,c,t)=>{const u=c.auth;if(u){const a=(e.url??"/").split("?")[0],p=(e.method??"GET").toUpperCase(),s=(0,manifest_1.authRuleFor)(u,a,p);if(s.mode==="jwt"){const k=String(e.headers.host??"").split(":")[0].toLowerCase(),d=(0,dev_auth_1.verifyDevJwt)(e,h?.for(k));return d?s.rule&&!(0,manifest_1.evalRule)(s.rule,(0,dev_auth_1.devAttrs)(e,d))?(r(t,"authz",{ok:!1}),{ok:!1,status:403,reason:"forbidden",detail:"Forbidden: insufficient permission"}):((0,dev_auth_1.stampIdentity)(e,d),r(t,"auth",{mode:"jwt",ok:!0}),{ok:!0,claims:d}):(r(t,"auth",{mode:"jwt",ok:!1}),{ok:!1,status:401,reason:"unauthorized",detail:"Unauthorized: invalid or missing token"})}return r(t,"auth",{mode:s.mode}),{ok:!0}}return r(t,"auth",{mode:"public",unconfigured:!0}),{ok:!0}},"publicAuth")}}l(createEdgeAuth,"createEdgeAuth"),n(createEdgeAuth,"createEdgeAuth");
|
|
@@ -1,42 +1,16 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
exports.renderChooserPage = renderChooserPage;
|
|
4
|
-
exports.serveChooser = serveChooser;
|
|
5
|
-
const workspace_registry_1 = require("./workspace-registry");
|
|
6
|
-
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
7
|
-
/** Render the chooser HTML from the registry entries (+ which key is running in THIS gateway). */
|
|
8
|
-
function renderChooserPage(entries, currentKey) {
|
|
9
|
-
const rows = entries
|
|
10
|
-
.map((e) => ({ ...e, live: e.key === currentKey || (0, workspace_registry_1.isWorkspaceLive)(e) }))
|
|
11
|
-
.sort((a, b) => Number(b.live) - Number(a.live) || a.key.localeCompare(b.key));
|
|
12
|
-
const cards = rows.length === 0
|
|
13
|
-
? `<p class="empty">No workspaces registered yet. Start a gateway in a workspace (<code>lensmcp gateway start</code>) and it will appear here.</p>`
|
|
14
|
-
: rows
|
|
15
|
-
.map((e) => {
|
|
16
|
-
const here = e.key === currentKey;
|
|
17
|
-
const badge = here
|
|
18
|
-
? `<span class="badge here">running here</span>`
|
|
19
|
-
: e.live
|
|
20
|
-
? `<span class="badge live">live</span>`
|
|
21
|
-
: `<span class="badge off">offline</span>`;
|
|
22
|
-
// The whole card is the anchor for a live workspace (no nested <a>); a `<div>` when offline.
|
|
23
|
-
const action = e.live
|
|
24
|
-
? `<span class="open">Open dashboard →</span>`
|
|
25
|
-
: `<span class="hint">start its gateway to open</span>`;
|
|
26
|
-
const inner = `<div class="card-main">
|
|
27
|
-
<div class="key">${esc(e.key)} ${badge}</div>
|
|
1
|
+
"use strict";var l=Object.defineProperty;var n=(a,o)=>l(a,"name",{value:o,configurable:!0});var p=Object.defineProperty,t=n((a,o)=>p(a,"name",{value:o,configurable:!0}),"t");Object.defineProperty(exports,"__esModule",{value:!0}),exports.renderChooserPage=renderChooserPage,exports.serveChooser=serveChooser;const workspace_registry_1=require("./workspace-registry"),esc=t(a=>a.replace(/[&<>"']/g,o=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[o]),"esc");function renderChooserPage(a,o){const r=a.map(e=>({...e,live:e.key===o||(0,workspace_registry_1.isWorkspaceLive)(e)})).sort((e,s)=>Number(s.live)-Number(e.live)||e.key.localeCompare(s.key)),c=r.length===0?'<p class="empty">No workspaces registered yet. Start a gateway in a workspace (<code>lensmcp gateway start</code>) and it will appear here.</p>':r.map(e=>{const s=e.key===o?'<span class="badge here">running here</span>':e.live?'<span class="badge live">live</span>':'<span class="badge off">offline</span>',d=e.live?'<span class="open">Open dashboard \u2192</span>':'<span class="hint">start its gateway to open</span>',i=`<div class="card-main">
|
|
2
|
+
<div class="key">${esc(e.key)} ${s}</div>
|
|
28
3
|
<div class="root">${esc(e.root)}</div>
|
|
29
4
|
</div>
|
|
30
|
-
<div class="card-action">${
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return `<!doctype html>
|
|
5
|
+
<div class="card-action">${d}</div>`;return e.live?`<a class="card is-live" href="${esc(e.base)}/">
|
|
6
|
+
${i}
|
|
7
|
+
</a>`:`<div class="card is-off">
|
|
8
|
+
${i}
|
|
9
|
+
</div>`}).join(`
|
|
10
|
+
`);return`<!doctype html>
|
|
37
11
|
<html lang="en"><head>
|
|
38
12
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
39
|
-
<title>LensMCP
|
|
13
|
+
<title>LensMCP \xB7 workspaces</title>
|
|
40
14
|
<style>
|
|
41
15
|
:root { color-scheme: dark; }
|
|
42
16
|
* { box-sizing: border-box; }
|
|
@@ -68,17 +42,10 @@ function renderChooserPage(entries, currentKey) {
|
|
|
68
42
|
<body><div class="wrap">
|
|
69
43
|
<header>
|
|
70
44
|
<h1>LensMCP workspaces</h1>
|
|
71
|
-
<p class="sub">Pick a workspace to open its dashboard. ${
|
|
45
|
+
<p class="sub">Pick a workspace to open its dashboard. ${r.filter(e=>e.live).length} live \xB7 ${r.length} known.</p>
|
|
72
46
|
</header>
|
|
73
47
|
<div class="list">
|
|
74
|
-
${
|
|
48
|
+
${c}
|
|
75
49
|
</div>
|
|
76
50
|
<footer>Served by the gateway on <code>lensmcp.local</code>. This list updates as gateways start and stop.</footer>
|
|
77
|
-
</div></body></html
|
|
78
|
-
}
|
|
79
|
-
/** Serve the chooser at the dashboard-host root. Reads the registry fresh so live status is current. */
|
|
80
|
-
function serveChooser(res, currentKey) {
|
|
81
|
-
const html = renderChooserPage((0, workspace_registry_1.readWorkspaces)(), currentKey);
|
|
82
|
-
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
83
|
-
res.end(html);
|
|
84
|
-
}
|
|
51
|
+
</div></body></html>`}n(renderChooserPage,"renderChooserPage"),t(renderChooserPage,"renderChooserPage");function serveChooser(a,o){const r=renderChooserPage((0,workspace_registry_1.readWorkspaces)(),o);a.writeHead(200,{"content-type":"text/html; charset=utf-8","cache-control":"no-store"}),a.end(r)}n(serveChooser,"serveChooser"),t(serveChooser,"serveChooser");
|
|
@@ -1,128 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.controlSocketPath = controlSocketPath;
|
|
4
|
-
exports.startControlServer = startControlServer;
|
|
5
|
-
const tslib_1 = require("tslib");
|
|
6
|
-
/**
|
|
7
|
-
* The daemon CONTROL PLANE (planning/multi-workspace-gateway.md P3, model A). A UNIX-socket HTTP listener
|
|
8
|
-
* at `~/.lensmcp/control.sock` — loopback-only by nature (a filesystem socket, no TCP), so a second
|
|
9
|
-
* workspace's `gateway start` (P4) REGISTERS its fragment into the running `:443` daemon instead of
|
|
10
|
-
* fighting for the port. The daemon reconstructs the fragment via `discoverRoutes` on the second
|
|
11
|
-
* workspace's root (so it owns the pool controllers + spawns those devservers itself — model A), merges
|
|
12
|
-
* the routes live, and reaps them on unregister.
|
|
13
|
-
*
|
|
14
|
-
* POST /register { wsKey, root, projects } → discover + register + rebuild
|
|
15
|
-
* POST /unregister { wsKey } → unregister + reap its services + rebuild
|
|
16
|
-
* GET /list → the registered workspaces
|
|
17
|
-
*/
|
|
18
|
-
const fs = tslib_1.__importStar(require("node:fs"));
|
|
19
|
-
const http = tslib_1.__importStar(require("node:http"));
|
|
20
|
-
const os = tslib_1.__importStar(require("node:os"));
|
|
21
|
-
const path = tslib_1.__importStar(require("node:path"));
|
|
22
|
-
const discovery_1 = require("./discovery");
|
|
23
|
-
/** `~/.lensmcp/control.sock` — the well-known daemon control socket (override the home via `LENSMCP_HOME`). */
|
|
24
|
-
function controlSocketPath() {
|
|
25
|
-
const home = process.env['LENSMCP_HOME'] || os.homedir();
|
|
26
|
-
return path.join(home, '.lensmcp', 'control.sock');
|
|
27
|
-
}
|
|
28
|
-
function readJsonBody(req) {
|
|
29
|
-
return new Promise((resolve, reject) => {
|
|
30
|
-
let raw = '';
|
|
31
|
-
req.on('data', (c) => {
|
|
32
|
-
raw += c;
|
|
33
|
-
if (raw.length > 1_000_000)
|
|
34
|
-
reject(new Error('body too large')); // bound the loopback input
|
|
35
|
-
});
|
|
36
|
-
req.on('end', () => {
|
|
37
|
-
try {
|
|
38
|
-
resolve(raw ? JSON.parse(raw) : {});
|
|
39
|
-
}
|
|
40
|
-
catch (e) {
|
|
41
|
-
reject(e);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
req.on('error', reject);
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
/** Start the control listener. The socket is loopback-only (a filesystem socket), so no IP allow-listing
|
|
48
|
-
* is needed — anything that can open it is already on this machine as this user. */
|
|
49
|
-
function startControlServer(deps) {
|
|
50
|
-
const { rt } = deps;
|
|
51
|
-
const socketPath = controlSocketPath();
|
|
52
|
-
fs.mkdirSync(path.dirname(socketPath), { recursive: true });
|
|
53
|
-
try {
|
|
54
|
-
fs.unlinkSync(socketPath);
|
|
55
|
-
}
|
|
56
|
-
catch { /* no stale socket to clear */ }
|
|
57
|
-
const send = (res, code, body) => {
|
|
58
|
-
res.writeHead(code, { 'content-type': 'application/json' });
|
|
59
|
-
res.end(JSON.stringify(body));
|
|
60
|
-
};
|
|
61
|
-
const server = http.createServer((req, res) => {
|
|
62
|
-
void (async () => {
|
|
63
|
-
try {
|
|
64
|
-
const url = (req.url ?? '').split('?')[0];
|
|
65
|
-
if (req.method === 'GET' && url === '/list') {
|
|
66
|
-
return send(res, 200, {
|
|
67
|
-
workspaces: rt.registry.keys().map((key) => {
|
|
68
|
-
const f = rt.registry.get(key);
|
|
69
|
-
return { key, root: f?.root, routes: f?.routes.length ?? 0, services: f?.services.length ?? 0 };
|
|
70
|
-
}),
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
// A RICHER status than /list: the daemon's own identity + every registered workspace (flagged if it
|
|
74
|
-
// OWNS the daemon) + per-service live state (down|starting|up). The IDE plugin + `gateway status --json`
|
|
75
|
-
// consume this to render the active gateway, its workspaces, and each service's health at a glance.
|
|
76
|
-
if (req.method === 'GET' && url === '/status') {
|
|
77
|
-
return send(res, 200, {
|
|
78
|
-
daemon: { pid: process.pid, wsKey: rt.wsKey },
|
|
79
|
-
workspaces: rt.registry.keys().map((key) => {
|
|
80
|
-
const f = rt.registry.get(key);
|
|
81
|
-
return { key, root: f?.root, routes: f?.routes.length ?? 0, services: f?.services.length ?? 0, isDaemon: key === rt.wsKey };
|
|
82
|
-
}),
|
|
83
|
-
services: rt.registry.allServices().map((s) => ({ project: s.project, wsKey: s.wsKey, host: s.decl.host, state: s.state })),
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
if (req.method === 'POST' && url === '/register') {
|
|
87
|
-
const body = (await readJsonBody(req));
|
|
88
|
-
if (!body.wsKey || !body.root)
|
|
89
|
-
return send(res, 400, { error: 'wsKey and root are required' });
|
|
90
|
-
const { routes, services } = (0, discovery_1.discoverRoutes)(body.root, body.projects ?? {}, undefined, body.wsKey);
|
|
91
|
-
const fragment = { wsKey: body.wsKey, root: body.root, routes, services };
|
|
92
|
-
rt.registry.register(fragment);
|
|
93
|
-
await deps.onRegister?.(fragment); // hosts the dashboard + APPENDS its route to the fragment BEFORE the rebuild
|
|
94
|
-
rt.rebuildRoutes();
|
|
95
|
-
return send(res, 200, { ok: true, wsKey: body.wsKey, routes: routes.length, services: services.length, workspaces: rt.registry.keys() });
|
|
96
|
-
}
|
|
97
|
-
if (req.method === 'POST' && url === '/unregister') {
|
|
98
|
-
const body = (await readJsonBody(req));
|
|
99
|
-
if (!body.wsKey)
|
|
100
|
-
return send(res, 400, { error: 'wsKey is required' });
|
|
101
|
-
const dropped = rt.registry.unregister(body.wsKey);
|
|
102
|
-
if (dropped) {
|
|
103
|
-
for (const svc of dropped.services)
|
|
104
|
-
deps.killService(svc, 'workspace unregistered');
|
|
105
|
-
deps.onUnregister?.(dropped);
|
|
106
|
-
}
|
|
107
|
-
rt.rebuildRoutes();
|
|
108
|
-
return send(res, 200, { ok: true, removed: !!dropped, workspaces: rt.registry.keys() });
|
|
109
|
-
}
|
|
110
|
-
send(res, 404, { error: 'not found' });
|
|
111
|
-
}
|
|
112
|
-
catch (e) {
|
|
113
|
-
send(res, 500, { error: e.message });
|
|
114
|
-
}
|
|
115
|
-
})();
|
|
116
|
-
});
|
|
117
|
-
server.listen(socketPath);
|
|
118
|
-
return {
|
|
119
|
-
socketPath,
|
|
120
|
-
close: () => {
|
|
121
|
-
server.close();
|
|
122
|
-
try {
|
|
123
|
-
fs.unlinkSync(socketPath);
|
|
124
|
-
}
|
|
125
|
-
catch { /* already gone */ }
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
}
|
|
1
|
+
"use strict";var h=Object.defineProperty;var d=(o,r)=>h(o,"name",{value:r,configurable:!0});var p=Object.defineProperty,y=d((o,r)=>p(o,"name",{value:r,configurable:!0}),"y");Object.defineProperty(exports,"__esModule",{value:!0}),exports.controlSocketPath=controlSocketPath,exports.startControlServer=startControlServer;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery");function controlSocketPath(){const o=process.env.LENSMCP_HOME||os.homedir();return path.join(o,".lensmcp","control.sock")}d(controlSocketPath,"controlSocketPath"),y(controlSocketPath,"controlSocketPath");function readJsonBody(o){return new Promise((r,c)=>{let s="";o.on("data",u=>{s+=u,s.length>1e6&&c(new Error("body too large"))}),o.on("end",()=>{try{r(s?JSON.parse(s):{})}catch(u){c(u)}}),o.on("error",c)})}d(readJsonBody,"readJsonBody"),y(readJsonBody,"readJsonBody");function startControlServer(o){const{rt:r}=o,c=controlSocketPath();fs.mkdirSync(path.dirname(c),{recursive:!0});try{fs.unlinkSync(c)}catch{}const s=y((n,i,a)=>{n.writeHead(i,{"content-type":"application/json"}),n.end(JSON.stringify(a))},"send"),u=http.createServer((n,i)=>{(async()=>{try{const a=(n.url??"").split("?")[0];if(n.method==="GET"&&a==="/list")return s(i,200,{workspaces:r.registry.keys().map(e=>{const t=r.registry.get(e);return{key:e,root:t?.root,routes:t?.routes.length??0,services:t?.services.length??0}})});if(n.method==="GET"&&a==="/status")return s(i,200,{daemon:{pid:process.pid,wsKey:r.wsKey},workspaces:r.registry.keys().map(e=>{const t=r.registry.get(e);return{key:e,root:t?.root,routes:t?.routes.length??0,services:t?.services.length??0,isDaemon:e===r.wsKey}}),services:r.registry.allServices().map(e=>({project:e.project,wsKey:e.wsKey,host:e.decl.host,state:e.state}))});if(n.method==="POST"&&a==="/register"){const e=await readJsonBody(n);if(!e.wsKey||!e.root)return s(i,400,{error:"wsKey and root are required"});const{routes:t,services:l}=(0,discovery_1.discoverRoutes)(e.root,e.projects??{},void 0,e.wsKey),g={wsKey:e.wsKey,root:e.root,routes:t,services:l};return r.registry.register(g),await o.onRegister?.(g),r.rebuildRoutes(),s(i,200,{ok:!0,wsKey:e.wsKey,routes:t.length,services:l.length,workspaces:r.registry.keys()})}if(n.method==="POST"&&a==="/unregister"){const e=await readJsonBody(n);if(!e.wsKey)return s(i,400,{error:"wsKey is required"});const t=r.registry.unregister(e.wsKey);if(t){for(const l of t.services)o.killService(l,"workspace unregistered");o.onUnregister?.(t)}return r.rebuildRoutes(),s(i,200,{ok:!0,removed:!!t,workspaces:r.registry.keys()})}s(i,404,{error:"not found"})}catch(a){s(i,500,{error:a.message})}})()});return u.listen(c),{socketPath:c,close:y(()=>{u.close();try{fs.unlinkSync(c)}catch{}},"close")}}d(startControlServer,"startControlServer"),y(startControlServer,"startControlServer");
|
|
@@ -1,108 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.devAttrs = void 0;
|
|
4
|
-
exports.verifyDevJwt = verifyDevJwt;
|
|
5
|
-
exports.stampIdentity = stampIdentity;
|
|
6
|
-
/**
|
|
7
|
-
* Dev edge auth: verify the end-user token, then stamp the trusted identity headers the services read
|
|
8
|
-
* (via GatewayTrustGuard). It MIRRORS the prod gateway's posture — a real JWKS/RS256 verifier when one is
|
|
9
|
-
* wired (see `../jwks-verify` + the composition in `server.ts`), an HS256 shared-secret path as the legacy
|
|
10
|
-
* fallback. Pure functions (the verifier is injected, so unit tests supply their own JWKS).
|
|
11
|
-
*
|
|
12
|
-
* SECURITY (the invariant this file must hold): a `jwt` route MUST NOT accept a token whose signature the
|
|
13
|
-
* edge cannot verify. When a JWKS verifier is wired it is the SOLE acceptor — every alg goes through it, so
|
|
14
|
-
* an unknown `kid`, a bad signature, `alg:none`, an HS256-confusion token, and an expired token ALL fail
|
|
15
|
-
* closed (rejected). Without a verifier, an asymmetric (RS256/ES*) token cannot be checked (we don't hold
|
|
16
|
-
* the issuer's public keys), so it also fails closed — unless an operator EXPLICITLY opts into the old
|
|
17
|
-
* insecure decode-only path (`LENSMCP_GW_DEV_JWT_INSECURE_DECODE=1`) for local bring-up. The HS256
|
|
18
|
-
* shared-secret path stays for services using that convention (and is itself bypassed once JWKS is wired).
|
|
19
|
-
*/
|
|
20
|
-
const node_crypto_1 = require("node:crypto");
|
|
21
|
-
/** The compact JWS from the `Authorization: Bearer …` header, or undefined. */
|
|
22
|
-
function bearerToken(req) {
|
|
23
|
-
const raw = Array.isArray(req.headers['authorization']) ? req.headers['authorization'][0] : req.headers['authorization'];
|
|
24
|
-
const auth = String(raw ?? '');
|
|
25
|
-
return auth.startsWith('Bearer ') ? auth.slice(7) : undefined;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Verify the end-user JWT and return its claims, or undefined.
|
|
29
|
-
*
|
|
30
|
-
* @param verifier when provided (the RS256/JWKS verifier wired by the composition root), it is the ONLY
|
|
31
|
-
* acceptor — the token is verified against the issuer's published keys by `kid`, so a rogue key / unknown
|
|
32
|
-
* kid / bad signature / `alg:none` / HS256-confusion / expired token all return undefined (fail closed).
|
|
33
|
-
*/
|
|
34
|
-
function verifyDevJwt(req, verifier) {
|
|
35
|
-
const token = bearerToken(req);
|
|
36
|
-
if (!token)
|
|
37
|
-
return undefined;
|
|
38
|
-
const [h, p, s] = token.split('.');
|
|
39
|
-
if (!h || !p || !s)
|
|
40
|
-
return undefined;
|
|
41
|
-
// Preferred: a real JWKS/RS256 verifier (mirrors the prod gateway's `jwks ? jwks.verify(tok) : hs256`).
|
|
42
|
-
// When wired it is the SOLE acceptor, so every forgery vector below is closed at once.
|
|
43
|
-
if (verifier)
|
|
44
|
-
return verifier.verify(token);
|
|
45
|
-
// ── No verifier wired (legacy dev) ──────────────────────────────────────────────────────────────────
|
|
46
|
-
let alg;
|
|
47
|
-
try {
|
|
48
|
-
alg = JSON.parse(Buffer.from(h, 'base64url').toString())['alg'];
|
|
49
|
-
}
|
|
50
|
-
catch {
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
if (typeof alg === 'string' && alg !== 'HS256') {
|
|
54
|
-
// An asymmetric IdP (RS256 + JWKS) does NOT use the HS256 dev shared secret. Without the issuer's public
|
|
55
|
-
// keys we CANNOT verify the signature, so — unlike the old code, which decode-accepted any RS256 token
|
|
56
|
-
// (a full edge auth bypass: a rogue key or a forged `alg:none` sailed through) — we FAIL CLOSED. Wire a
|
|
57
|
-
// JWKS verifier (LENSMCP_GW_JWKS_URL, or the auto-derived auth-host JWKS) to accept RS256 tokens.
|
|
58
|
-
if (process.env['LENSMCP_GW_DEV_JWT_INSECURE_DECODE'] !== '1')
|
|
59
|
-
return undefined; // fail closed
|
|
60
|
-
// Explicit, dev-only opt-in to the legacy decode + exp-check (NO signature check) — bring-up escape hatch.
|
|
61
|
-
try {
|
|
62
|
-
const rsClaims = JSON.parse(Buffer.from(p, 'base64url').toString());
|
|
63
|
-
if (typeof rsClaims['exp'] === 'number' && Date.now() / 1000 > rsClaims['exp'])
|
|
64
|
-
return undefined;
|
|
65
|
-
return rsClaims;
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// HS256 shared-secret convention (only reachable when NO JWKS verifier is wired).
|
|
72
|
-
const secret = process.env['AUTH_JWT_SECRET'] ?? 'tetros-dev-secret';
|
|
73
|
-
const expected = (0, node_crypto_1.createHmac)('sha256', secret).update(`${h}.${p}`).digest('base64url');
|
|
74
|
-
const a = Buffer.from(s);
|
|
75
|
-
const b = Buffer.from(expected);
|
|
76
|
-
if (a.length !== b.length || !(0, node_crypto_1.timingSafeEqual)(a, b))
|
|
77
|
-
return undefined;
|
|
78
|
-
try {
|
|
79
|
-
const claims = JSON.parse(Buffer.from(p, 'base64url').toString());
|
|
80
|
-
if (typeof claims['exp'] === 'number' && Date.now() / 1000 > claims['exp'])
|
|
81
|
-
return undefined;
|
|
82
|
-
return claims;
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
/** Stamp verified identity onto the forwarded request — tenant from the `tid` CLAIM, never a client header. */
|
|
89
|
-
function stampIdentity(req, claims) {
|
|
90
|
-
const set = (k, v) => { if (v !== undefined && v !== null && v !== '')
|
|
91
|
-
req.headers[k] = String(v); };
|
|
92
|
-
set('x-user-id', claims['sub']);
|
|
93
|
-
set('x-tenant-id', claims['tid']);
|
|
94
|
-
const roles = claims['roles'];
|
|
95
|
-
if (Array.isArray(roles) && roles.length)
|
|
96
|
-
req.headers['x-user-roles'] = roles.map(String).join(',');
|
|
97
|
-
const perms = claims['perms'];
|
|
98
|
-
if (Array.isArray(perms) && perms.length)
|
|
99
|
-
req.headers['x-user-permissions'] = perms.map(String).join(',');
|
|
100
|
-
}
|
|
101
|
-
/** Build the ABAC attribute bag from the verified claims + request envelope. */
|
|
102
|
-
const devAttrs = (req, claims) => ({
|
|
103
|
-
...claims,
|
|
104
|
-
ip: req.socket.remoteAddress ?? '',
|
|
105
|
-
method: (req.method ?? 'GET').toUpperCase(),
|
|
106
|
-
path: (req.url ?? '/').split('?')[0],
|
|
107
|
-
});
|
|
108
|
-
exports.devAttrs = devAttrs;
|
|
1
|
+
"use strict";var y=Object.defineProperty;var a=(e,r)=>y(e,"name",{value:r,configurable:!0});var h=Object.defineProperty,u=a((e,r)=>h(e,"name",{value:r,configurable:!0}),"u");Object.defineProperty(exports,"__esModule",{value:!0}),exports.devAttrs=void 0,exports.verifyDevJwt=verifyDevJwt,exports.stampIdentity=stampIdentity;const node_crypto_1=require("node:crypto");function bearerToken(e){const r=Array.isArray(e.headers.authorization)?e.headers.authorization[0]:e.headers.authorization,t=String(r??"");return t.startsWith("Bearer ")?t.slice(7):void 0}a(bearerToken,"bearerToken"),u(bearerToken,"bearerToken");function verifyDevJwt(e,r){const t=bearerToken(e);if(!t)return;const[n,s,f]=t.split(".");if(!n||!s||!f)return;if(r)return r.verify(t);let o;try{o=JSON.parse(Buffer.from(n,"base64url").toString()).alg}catch{return}if(typeof o=="string"&&o!=="HS256"){if(process.env.LENSMCP_GW_DEV_JWT_INSECURE_DECODE!=="1")return;try{const i=JSON.parse(Buffer.from(s,"base64url").toString());return typeof i.exp=="number"&&Date.now()/1e3>i.exp?void 0:i}catch{return}}const d=process.env.AUTH_JWT_SECRET??"tetros-dev-secret",l=(0,node_crypto_1.createHmac)("sha256",d).update(`${n}.${s}`).digest("base64url"),p=Buffer.from(f),c=Buffer.from(l);if(!(p.length!==c.length||!(0,node_crypto_1.timingSafeEqual)(p,c)))try{const i=JSON.parse(Buffer.from(s,"base64url").toString());return typeof i.exp=="number"&&Date.now()/1e3>i.exp?void 0:i}catch{return}}a(verifyDevJwt,"verifyDevJwt"),u(verifyDevJwt,"verifyDevJwt");function stampIdentity(e,r){const t=u((f,o)=>{o!=null&&o!==""&&(e.headers[f]=String(o))},"set");t("x-user-id",r.sub),t("x-tenant-id",r.tid);const n=r.roles;Array.isArray(n)&&n.length&&(e.headers["x-user-roles"]=n.map(String).join(","));const s=r.perms;Array.isArray(s)&&s.length&&(e.headers["x-user-permissions"]=s.map(String).join(","))}a(stampIdentity,"stampIdentity"),u(stampIdentity,"stampIdentity");const devAttrs=u((e,r)=>({...r,ip:e.socket.remoteAddress??"",method:(e.method??"GET").toUpperCase(),path:(e.url??"/").split("?")[0]}),"devAttrs");exports.devAttrs=devAttrs;
|
|
@@ -1,123 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.pickSock = pickSock;
|
|
4
|
-
exports.lensPortFromUpstream = lensPortFromUpstream;
|
|
5
|
-
exports.discoverRoutes = discoverRoutes;
|
|
6
|
-
const tslib_1 = require("tslib");
|
|
7
|
-
/**
|
|
8
|
-
* Route discovery + socket-pool round-robin. Pure (fs reads only): reads every
|
|
9
|
-
* project's `cluster` (or legacy `davnx`) declaration and builds the route table.
|
|
10
|
-
*/
|
|
11
|
-
const fs = tslib_1.__importStar(require("node:fs"));
|
|
12
|
-
const os = tslib_1.__importStar(require("node:os"));
|
|
13
|
-
const path = tslib_1.__importStar(require("node:path"));
|
|
14
|
-
const manifest_1 = require("../manifest");
|
|
15
|
-
const scope_1 = require("./scope");
|
|
16
|
-
/** Round-robin over the pool's sockets, rescanning the dir when the cache ages out. */
|
|
17
|
-
function pickSock(pool, scanTtlMs = 1500) {
|
|
18
|
-
const now = Date.now();
|
|
19
|
-
if (now - pool.scannedAt > scanTtlMs) {
|
|
20
|
-
try {
|
|
21
|
-
pool.socks = fs.readdirSync(pool.dir)
|
|
22
|
-
.filter((f) => /^child-\d+\.sock$/.test(f))
|
|
23
|
-
.map((f) => path.join(pool.dir, f));
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
pool.socks = [];
|
|
27
|
-
}
|
|
28
|
-
pool.scannedAt = now;
|
|
29
|
-
}
|
|
30
|
-
if (pool.socks.length === 0)
|
|
31
|
-
return undefined;
|
|
32
|
-
pool.idx = (pool.idx + 1) % pool.socks.length;
|
|
33
|
-
return pool.socks[pool.idx];
|
|
34
|
-
}
|
|
35
|
-
/** Extract the port from an `upstream` URL like `http://localhost:5173`. */
|
|
36
|
-
function lensPortFromUpstream(upstream) {
|
|
37
|
-
if (!upstream)
|
|
38
|
-
return undefined;
|
|
39
|
-
const m = /:(\d+)/.exec(upstream);
|
|
40
|
-
return m ? Number(m[1]) : undefined;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Read every project's `cluster` (or legacy `davnx`) declaration and build
|
|
44
|
-
* the route table: host routes first (plus auto `internal.<host>` for pod
|
|
45
|
-
* services), the `default` catch-all last.
|
|
46
|
-
*/
|
|
47
|
-
function discoverRoutes(rootDir, projects, sockBaseDir = os.tmpdir(),
|
|
48
|
-
// The workspace key namespaces the pod sock dir (planning/multi-workspace-gateway.md) — MUST match
|
|
49
|
-
// `main.devserver.ts`'s `SOCK_DIR` derivation so the gateway scans the same dir the devserver writes.
|
|
50
|
-
// Empty → the legacy un-namespaced path (single-workspace / pre-multi-workspace).
|
|
51
|
-
wsKey = '') {
|
|
52
|
-
const sockDirFor = (service) => wsKey ? path.join(sockBaseDir, wsKey, `${service}-devserver`) : path.join(sockBaseDir, `${service}-devserver`);
|
|
53
|
-
const routes = [];
|
|
54
|
-
const services = [];
|
|
55
|
-
const hosts = []; // every declared host — the default's apex is derived from these
|
|
56
|
-
let defaultRoute;
|
|
57
|
-
for (const [name, p] of Object.entries(projects)) {
|
|
58
|
-
let decl;
|
|
59
|
-
try {
|
|
60
|
-
const raw = JSON.parse(fs.readFileSync(path.join(rootDir, p.root, 'project.json'), 'utf8'));
|
|
61
|
-
decl = raw.cluster ?? raw.davnx;
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (!decl)
|
|
67
|
-
continue;
|
|
68
|
-
let pool;
|
|
69
|
-
let svc;
|
|
70
|
-
let target;
|
|
71
|
-
let lens;
|
|
72
|
-
if (decl.service) {
|
|
73
|
-
// `lens` applies to FRONTEND apps only; a pod service is already
|
|
74
|
-
// instrumented by the serve executor's node-instrumentation graft.
|
|
75
|
-
if (decl.lens)
|
|
76
|
-
console.warn(`[gateway] ${name}: cluster.lens is ignored for pod (service) decls — backends are instrumented by serve-hmr.`);
|
|
77
|
-
pool = { dir: sockDirFor(decl.service), socks: [], idx: -1, scannedAt: 0 };
|
|
78
|
-
svc = { project: name, decl, root: rootDir, wsKey, pool, state: 'down', lastUsed: Date.now(), inflight: 0, lastScaleAt: 0 };
|
|
79
|
-
services.push(svc);
|
|
80
|
-
}
|
|
81
|
-
else if (decl.lens) {
|
|
82
|
-
// Lens-mode frontend: the gateway SPAWNS this app's Vite + instrumentation
|
|
83
|
-
// (deterministically on `port`) and upstreams to it. One LensFrontend per
|
|
84
|
-
// project is SHARED across its host/default routes (so the spawn fires once).
|
|
85
|
-
const port = decl.port ?? lensPortFromUpstream(decl.upstream) ?? 5173;
|
|
86
|
-
target = `http://localhost:${port}`;
|
|
87
|
-
lens = { project: name, projectRoot: path.join(rootDir, p.root), port };
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
target = decl.upstream ?? (decl.port ? `http://localhost:${decl.port}` : undefined);
|
|
91
|
-
if (!target) {
|
|
92
|
-
console.warn(`[gateway] ${name}: no service/upstream/port — skipped.`);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const base = { project: name, prependPrefix: decl.prependPrefix, target, pool, svc, prefix: decl.path, auth: decl.auth, lens };
|
|
97
|
-
if (decl.host) {
|
|
98
|
-
routes.push({ ...base, host: decl.host });
|
|
99
|
-
hosts.push(decl.host);
|
|
100
|
-
// internal.<host>: east-west traffic, same pods, NO middleware.
|
|
101
|
-
if (pool && decl.internalHost !== false) {
|
|
102
|
-
routes.push({
|
|
103
|
-
...base,
|
|
104
|
-
host: typeof decl.internalHost === 'string' ? decl.internalHost : `internal.${decl.host}`,
|
|
105
|
-
internal: true,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
if (decl.default)
|
|
110
|
-
defaultRoute = { ...base, prefix: undefined };
|
|
111
|
-
}
|
|
112
|
-
if (defaultRoute) {
|
|
113
|
-
// Stamp the apex so a shared multi-workspace gateway routes an unclaimed host to THIS workspace's
|
|
114
|
-
// default (see matchRoute). Harmless for a single workspace — one default ignores its apex.
|
|
115
|
-
defaultRoute.apex = (0, scope_1.baseDomainOf)(hosts);
|
|
116
|
-
routes.push(defaultRoute);
|
|
117
|
-
}
|
|
118
|
-
// specificity sort (shared with prod): host+path beats host-only beats
|
|
119
|
-
// catch-all — so a path-mounted service (api.x/channels) wins over the
|
|
120
|
-
// host's owner (api.x → api).
|
|
121
|
-
(0, manifest_1.sortBySpecificity)(routes);
|
|
122
|
-
return { routes, services };
|
|
123
|
-
}
|
|
1
|
+
"use strict";var S=Object.defineProperty;var p=(e,r)=>S(e,"name",{value:r,configurable:!0});var k=Object.defineProperty,a=p((e,r)=>k(e,"name",{value:r,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.pickSock=pickSock,exports.lensPortFromUpstream=lensPortFromUpstream,exports.discoverRoutes=discoverRoutes;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),manifest_1=require("../manifest"),scope_1=require("./scope");function pickSock(e,r=1500){const i=Date.now();if(i-e.scannedAt>r){try{e.socks=fs.readdirSync(e.dir).filter(s=>/^child-\d+\.sock$/.test(s)).map(s=>path.join(e.dir,s))}catch{e.socks=[]}e.scannedAt=i}if(e.socks.length!==0)return e.idx=(e.idx+1)%e.socks.length,e.socks[e.idx]}p(pickSock,"pickSock"),a(pickSock,"pickSock");function lensPortFromUpstream(e){if(!e)return;const r=/:(\d+)/.exec(e);return r?Number(r[1]):void 0}p(lensPortFromUpstream,"lensPortFromUpstream"),a(lensPortFromUpstream,"lensPortFromUpstream");function discoverRoutes(e,r,i=os.tmpdir(),s=""){const y=a(t=>s?path.join(i,s,`${t}-devserver`):path.join(i,`${t}-devserver`),"sockDirFor"),n=[],v=[],m=[];let l;for(const[t,x]of Object.entries(r)){let o;try{const c=JSON.parse(fs.readFileSync(path.join(e,x.root,"project.json"),"utf8"));o=c.cluster??c.davnx}catch{continue}if(!o)continue;let d,f,u,j;if(o.service)o.lens&&console.warn(`[gateway] ${t}: cluster.lens is ignored for pod (service) decls \u2014 backends are instrumented by serve-hmr.`),d={dir:y(o.service),socks:[],idx:-1,scannedAt:0},f={project:t,decl:o,root:e,wsKey:s,pool:d,state:"down",lastUsed:Date.now(),inflight:0,lastScaleAt:0},v.push(f);else if(o.lens){const c=o.port??lensPortFromUpstream(o.upstream)??5173;u=`http://localhost:${c}`,j={project:t,projectRoot:path.join(e,x.root),port:c}}else if(u=o.upstream??(o.port?`http://localhost:${o.port}`:void 0),!u){console.warn(`[gateway] ${t}: no service/upstream/port \u2014 skipped.`);continue}const h={project:t,prependPrefix:o.prependPrefix,target:u,pool:d,svc:f,prefix:o.path,auth:o.auth,lens:j};o.host&&(n.push({...h,host:o.host}),m.push(o.host),d&&o.internalHost!==!1&&n.push({...h,host:typeof o.internalHost=="string"?o.internalHost:`internal.${o.host}`,internal:!0})),o.default&&(l={...h,prefix:void 0})}return l&&(l.apex=(0,scope_1.baseDomainOf)(m),n.push(l)),(0,manifest_1.sortBySpecificity)(n),{routes:n,services:v}}p(discoverRoutes,"discoverRoutes"),a(discoverRoutes,"discoverRoutes");
|
|
@@ -1,47 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PREFLIGHT_METHODS_ROUTED = exports.PREFLIGHT_METHODS_NOROUTE = exports.corsPreflight = exports.cors = exports.GATEWAY_OWNED_HEADERS = exports.sendError = void 0;
|
|
4
|
-
const gateway_errors_1 = require("../gateway-errors");
|
|
5
|
-
/**
|
|
6
|
-
* Send a gateway-generated edge error as the JSON envelope `{ key, status, traceId, message? }` (NOT
|
|
7
|
-
* `text/plain`) + an `x-request-id` header — the SAME localized contract a service returns, so a browser
|
|
8
|
-
* app renders `t(key)` and surfaces the trace id. Mirrors the prod gateway's `sendError`.
|
|
9
|
-
*/
|
|
10
|
-
const sendError = (res, req, status, reason, detail) => {
|
|
11
|
-
const traceId = (0, gateway_errors_1.ensureRequestId)(req);
|
|
12
|
-
if (!res.headersSent)
|
|
13
|
-
res.writeHead(status, { 'content-type': gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE, 'x-request-id': traceId });
|
|
14
|
-
res.end((0, gateway_errors_1.gatewayErrorBody)(status, reason, traceId, detail));
|
|
15
|
-
};
|
|
16
|
-
exports.sendError = sendError;
|
|
17
|
-
/**
|
|
18
|
-
* The gateway OWNS these headers — strip any client-supplied copy at ingress so identity + trust can
|
|
19
|
-
* only originate from the gateway (mirrors the prod gateway's stripTrust). Applied at the very top of
|
|
20
|
-
* BOTH the HTTP handler and the WS upgrade.
|
|
21
|
-
*/
|
|
22
|
-
exports.GATEWAY_OWNED_HEADERS = ['x-internal-token', 'x-api-key-id', 'x-user-id', 'x-tenant-id', 'x-user-roles', 'x-user-permissions'];
|
|
23
|
-
/** Reflect the request Origin back with credentials — the gateway is the edge CORS authority. */
|
|
24
|
-
const cors = (req, res) => {
|
|
25
|
-
const origin = String(req.headers.origin ?? '*');
|
|
26
|
-
res.setHeader('access-control-allow-origin', origin);
|
|
27
|
-
res.setHeader('access-control-allow-credentials', 'true');
|
|
28
|
-
res.setHeader('vary', 'Origin');
|
|
29
|
-
};
|
|
30
|
-
exports.cors = cors;
|
|
31
|
-
/**
|
|
32
|
-
* Answer a CORS preflight (OPTIONS) at the edge: reflect CORS + 204 with the allowed `methods` and the
|
|
33
|
-
* requested headers echoed back. `methods` differs by caller — the no-route host-root uses the narrow
|
|
34
|
-
* `GET,HEAD,OPTIONS`; a routed path uses the wide verb list.
|
|
35
|
-
*/
|
|
36
|
-
const corsPreflight = (req, res, methods) => {
|
|
37
|
-
(0, exports.cors)(req, res);
|
|
38
|
-
res.writeHead(204, {
|
|
39
|
-
'access-control-allow-methods': methods,
|
|
40
|
-
'access-control-allow-headers': String(req.headers['access-control-request-headers'] ?? '*'),
|
|
41
|
-
});
|
|
42
|
-
res.end();
|
|
43
|
-
};
|
|
44
|
-
exports.corsPreflight = corsPreflight;
|
|
45
|
-
/** Allowed-methods lists for the two preflight sites. */
|
|
46
|
-
exports.PREFLIGHT_METHODS_NOROUTE = 'GET,HEAD,OPTIONS';
|
|
47
|
-
exports.PREFLIGHT_METHODS_ROUTED = 'GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS';
|
|
1
|
+
"use strict";var i=Object.defineProperty;var a=(r,e)=>i(r,"name",{value:e,configurable:!0});var c=Object.defineProperty,s=a((r,e)=>c(r,"name",{value:e,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.PREFLIGHT_METHODS_ROUTED=exports.PREFLIGHT_METHODS_NOROUTE=exports.corsPreflight=exports.cors=exports.GATEWAY_OWNED_HEADERS=exports.sendError=void 0;const gateway_errors_1=require("../gateway-errors"),sendError=s((r,e,o,n,E)=>{const t=(0,gateway_errors_1.ensureRequestId)(e);r.headersSent||r.writeHead(o,{"content-type":gateway_errors_1.GATEWAY_ERROR_CONTENT_TYPE,"x-request-id":t}),r.end((0,gateway_errors_1.gatewayErrorBody)(o,n,t,E))},"sendError");exports.sendError=sendError,exports.GATEWAY_OWNED_HEADERS=["x-internal-token","x-api-key-id","x-user-id","x-tenant-id","x-user-roles","x-user-permissions"];const cors=s((r,e)=>{const o=String(r.headers.origin??"*");e.setHeader("access-control-allow-origin",o),e.setHeader("access-control-allow-credentials","true"),e.setHeader("vary","Origin")},"cors");exports.cors=cors;const corsPreflight=s((r,e,o)=>{(0,exports.cors)(r,e),e.writeHead(204,{"access-control-allow-methods":o,"access-control-allow-headers":String(r.headers["access-control-request-headers"]??"*")}),e.end()},"corsPreflight");exports.corsPreflight=corsPreflight,exports.PREFLIGHT_METHODS_NOROUTE="GET,HEAD,OPTIONS",exports.PREFLIGHT_METHODS_ROUTED="GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS";
|