@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.
Files changed (64) hide show
  1. package/basic-ssl.js +1 -241
  2. package/build-scope-patterns.js +1 -40
  3. package/create-webpack-dev.js +1 -186
  4. package/create-webpack-prod.js +1 -169
  5. package/executors/build/build.impl.js +1 -98
  6. package/executors/gateway/gateway-errors.js +1 -43
  7. package/executors/gateway/gateway.impl.js +1 -53
  8. package/executors/gateway/gateway.lib.js +1 -29
  9. package/executors/gateway/health-check.js +1 -66
  10. package/executors/gateway/jwks-verify.js +1 -121
  11. package/executors/gateway/main.prod-gateway.js +2 -573
  12. package/executors/gateway/main.rollout.js +11 -117
  13. package/executors/gateway/manifest.js +1 -374
  14. package/executors/gateway/metrics.js +1 -56
  15. package/executors/gateway/otel-tracing.js +1 -74
  16. package/executors/gateway/prod-gateway.lib.js +1 -22
  17. package/executors/gateway/prod-runtime/access-log.js +1 -24
  18. package/executors/gateway/prod-runtime/app.js +1 -123
  19. package/executors/gateway/prod-runtime/auth.js +1 -51
  20. package/executors/gateway/prod-runtime/cors.js +1 -40
  21. package/executors/gateway/prod-runtime/edge.js +1 -65
  22. package/executors/gateway/prod-runtime/handler.js +1 -226
  23. package/executors/gateway/prod-runtime/hooks.js +1 -42
  24. package/executors/gateway/prod-runtime/observability.js +1 -125
  25. package/executors/gateway/prod-runtime/rollout.js +1 -103
  26. package/executors/gateway/prod-runtime/routing.js +1 -40
  27. package/executors/gateway/prod-runtime/server.js +1 -79
  28. package/executors/gateway/prod-runtime/trust.js +1 -32
  29. package/executors/gateway/prod-runtime/types.js +1 -2
  30. package/executors/gateway/prod-runtime/upgrade.js +4 -116
  31. package/executors/gateway/prod-runtime/upstream.js +1 -21
  32. package/executors/gateway/providers-prod.js +1 -232
  33. package/executors/gateway/rate-limit.js +2 -75
  34. package/executors/gateway/registry-source.js +1 -131
  35. package/executors/gateway/rollout-ops.js +2 -167
  36. package/executors/gateway/runtime/auth.js +1 -64
  37. package/executors/gateway/runtime/chooser.js +12 -45
  38. package/executors/gateway/runtime/control.js +1 -128
  39. package/executors/gateway/runtime/dev-auth.js +1 -108
  40. package/executors/gateway/runtime/discovery.js +1 -123
  41. package/executors/gateway/runtime/edge.js +1 -47
  42. package/executors/gateway/runtime/handler.js +1 -183
  43. package/executors/gateway/runtime/hooks.js +1 -55
  44. package/executors/gateway/runtime/lens-children.js +1 -651
  45. package/executors/gateway/runtime/lifecycle.js +3 -842
  46. package/executors/gateway/runtime/observability.js +2 -148
  47. package/executors/gateway/runtime/pod-env.js +2 -89
  48. package/executors/gateway/runtime/proxy.js +1 -457
  49. package/executors/gateway/runtime/route-registry.js +1 -72
  50. package/executors/gateway/runtime/scope.js +1 -117
  51. package/executors/gateway/runtime/server.js +3 -487
  52. package/executors/gateway/runtime/service-keys.js +1 -49
  53. package/executors/gateway/runtime/types.js +1 -151
  54. package/executors/gateway/runtime/upgrade.js +1 -71
  55. package/executors/gateway/runtime/workspace-registry.js +1 -99
  56. package/executors/gateway/ssrf-guard.js +1 -190
  57. package/executors/serve/serve.impl.js +1 -280
  58. package/executors/trust/trust.impl.js +4 -162
  59. package/gateway.js +1 -35
  60. package/index.js +1 -16
  61. package/main.devserver.js +10 -1117
  62. package/package.json +4 -3
  63. package/tsgo-check-plugin.js +4 -364
  64. 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
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.renderChooserPage = renderChooserPage;
4
- exports.serveChooser = serveChooser;
5
- const workspace_registry_1 = require("./workspace-registry");
6
- const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[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">${action}</div>`;
31
- return e.live
32
- ? `<a class="card is-live" href="${esc(e.base)}/">\n ${inner}\n </a>`
33
- : `<div class="card is-off">\n ${inner}\n </div>`;
34
- })
35
- .join('\n');
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 · workspaces</title>
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. ${rows.filter((r) => r.live).length} live · ${rows.length} known.</p>
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
- ${cards}
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";