@lensmcp/cluster 1.18.3 → 1.18.6
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.d.ts +0 -1
- package/basic-ssl.js +1 -241
- package/build-scope-patterns.d.ts +0 -1
- package/build-scope-patterns.js +1 -40
- package/create-webpack-dev.d.ts +0 -1
- package/create-webpack-dev.js +1 -186
- package/create-webpack-prod.d.ts +0 -1
- package/create-webpack-prod.js +1 -169
- package/executors/build/build.impl.d.ts +0 -1
- package/executors/build/build.impl.js +1 -98
- package/executors/gateway/gateway-errors.d.ts +0 -1
- package/executors/gateway/gateway-errors.js +1 -43
- package/executors/gateway/gateway.impl.d.ts +0 -1
- package/executors/gateway/gateway.impl.js +1 -53
- package/executors/gateway/gateway.lib.d.ts +0 -1
- package/executors/gateway/gateway.lib.js +1 -29
- package/executors/gateway/health-check.d.ts +0 -1
- package/executors/gateway/health-check.js +1 -66
- package/executors/gateway/jwks-verify.d.ts +0 -1
- package/executors/gateway/jwks-verify.js +1 -121
- package/executors/gateway/main.prod-gateway.d.ts +0 -1
- package/executors/gateway/main.prod-gateway.js +2 -573
- package/executors/gateway/main.rollout.d.ts +0 -1
- package/executors/gateway/main.rollout.js +11 -117
- package/executors/gateway/manifest.d.ts +0 -1
- package/executors/gateway/manifest.js +1 -374
- package/executors/gateway/metrics.d.ts +0 -1
- package/executors/gateway/metrics.js +1 -56
- package/executors/gateway/otel-tracing.d.ts +0 -1
- package/executors/gateway/otel-tracing.js +1 -74
- package/executors/gateway/prod-gateway.lib.d.ts +0 -1
- package/executors/gateway/prod-gateway.lib.js +1 -22
- package/executors/gateway/prod-runtime/access-log.d.ts +0 -1
- package/executors/gateway/prod-runtime/access-log.js +1 -24
- package/executors/gateway/prod-runtime/app.d.ts +0 -1
- package/executors/gateway/prod-runtime/app.js +1 -123
- package/executors/gateway/prod-runtime/auth.d.ts +0 -1
- package/executors/gateway/prod-runtime/auth.js +1 -51
- package/executors/gateway/prod-runtime/cors.d.ts +0 -1
- package/executors/gateway/prod-runtime/cors.js +1 -40
- package/executors/gateway/prod-runtime/edge.d.ts +0 -1
- package/executors/gateway/prod-runtime/edge.js +1 -65
- package/executors/gateway/prod-runtime/handler.d.ts +0 -1
- package/executors/gateway/prod-runtime/handler.js +1 -226
- package/executors/gateway/prod-runtime/hooks.d.ts +0 -1
- package/executors/gateway/prod-runtime/hooks.js +1 -42
- package/executors/gateway/prod-runtime/observability.d.ts +0 -1
- package/executors/gateway/prod-runtime/observability.js +1 -125
- package/executors/gateway/prod-runtime/rollout.d.ts +0 -1
- package/executors/gateway/prod-runtime/rollout.js +1 -103
- package/executors/gateway/prod-runtime/routing.d.ts +0 -1
- package/executors/gateway/prod-runtime/routing.js +1 -40
- package/executors/gateway/prod-runtime/server.d.ts +0 -1
- package/executors/gateway/prod-runtime/server.js +1 -79
- package/executors/gateway/prod-runtime/trust.d.ts +0 -1
- package/executors/gateway/prod-runtime/trust.js +1 -32
- package/executors/gateway/prod-runtime/types.d.ts +0 -1
- package/executors/gateway/prod-runtime/types.js +1 -2
- package/executors/gateway/prod-runtime/upgrade.d.ts +0 -1
- package/executors/gateway/prod-runtime/upgrade.js +4 -116
- package/executors/gateway/prod-runtime/upstream.d.ts +0 -1
- package/executors/gateway/prod-runtime/upstream.js +1 -21
- package/executors/gateway/providers-prod.d.ts +0 -1
- package/executors/gateway/providers-prod.js +1 -232
- package/executors/gateway/rate-limit.d.ts +0 -1
- package/executors/gateway/rate-limit.js +2 -75
- package/executors/gateway/registry-source.d.ts +0 -1
- package/executors/gateway/registry-source.js +1 -131
- package/executors/gateway/rollout-ops.d.ts +0 -1
- package/executors/gateway/rollout-ops.js +2 -167
- package/executors/gateway/runtime/auth.d.ts +0 -1
- package/executors/gateway/runtime/auth.js +1 -64
- package/executors/gateway/runtime/chooser.d.ts +0 -1
- package/executors/gateway/runtime/chooser.js +12 -45
- package/executors/gateway/runtime/control.d.ts +0 -1
- package/executors/gateway/runtime/control.js +1 -128
- package/executors/gateway/runtime/dev-auth.d.ts +0 -1
- package/executors/gateway/runtime/dev-auth.js +1 -108
- package/executors/gateway/runtime/discovery.d.ts +0 -1
- package/executors/gateway/runtime/discovery.js +1 -123
- package/executors/gateway/runtime/edge.d.ts +0 -1
- package/executors/gateway/runtime/edge.js +1 -47
- package/executors/gateway/runtime/handler.d.ts +0 -1
- package/executors/gateway/runtime/handler.js +1 -183
- package/executors/gateway/runtime/hooks.d.ts +0 -1
- package/executors/gateway/runtime/hooks.js +1 -55
- package/executors/gateway/runtime/lens-children.d.ts +0 -1
- package/executors/gateway/runtime/lens-children.js +1 -651
- package/executors/gateway/runtime/lifecycle.d.ts +0 -1
- package/executors/gateway/runtime/lifecycle.js +3 -842
- package/executors/gateway/runtime/observability.d.ts +0 -1
- package/executors/gateway/runtime/observability.js +2 -148
- package/executors/gateway/runtime/pod-env.d.ts +0 -1
- package/executors/gateway/runtime/pod-env.js +2 -89
- package/executors/gateway/runtime/proxy.d.ts +0 -1
- package/executors/gateway/runtime/proxy.js +1 -457
- package/executors/gateway/runtime/route-registry.d.ts +0 -1
- package/executors/gateway/runtime/route-registry.js +1 -72
- package/executors/gateway/runtime/scope.d.ts +0 -1
- package/executors/gateway/runtime/scope.js +1 -117
- package/executors/gateway/runtime/server.d.ts +0 -1
- package/executors/gateway/runtime/server.js +3 -487
- package/executors/gateway/runtime/service-keys.d.ts +0 -1
- package/executors/gateway/runtime/service-keys.js +1 -49
- package/executors/gateway/runtime/types.d.ts +0 -1
- package/executors/gateway/runtime/types.js +1 -151
- package/executors/gateway/runtime/upgrade.d.ts +0 -1
- package/executors/gateway/runtime/upgrade.js +1 -71
- package/executors/gateway/runtime/workspace-registry.d.ts +0 -1
- package/executors/gateway/runtime/workspace-registry.js +1 -99
- package/executors/gateway/ssrf-guard.d.ts +0 -1
- package/executors/gateway/ssrf-guard.js +1 -190
- package/executors/serve/serve.impl.d.ts +0 -1
- package/executors/serve/serve.impl.js +1 -280
- package/executors/trust/trust.impl.d.ts +0 -1
- package/executors/trust/trust.impl.js +4 -162
- package/gateway.d.ts +0 -1
- package/gateway.js +1 -35
- package/index.d.ts +0 -1
- package/index.js +1 -16
- package/main.devserver.d.ts +0 -1
- package/main.devserver.js +10 -1117
- package/package.json +4 -3
- package/tsgo-check-plugin.d.ts +0 -1
- package/tsgo-check-plugin.js +4 -364
- package/typecheck-bus.d.ts +0 -1
- package/typecheck-bus.js +4 -256
- package/basic-ssl.d.ts.map +0 -1
- package/build-scope-patterns.d.ts.map +0 -1
- package/create-webpack-dev.d.ts.map +0 -1
- package/create-webpack-prod.d.ts.map +0 -1
- package/executors/build/build.impl.d.ts.map +0 -1
- package/executors/gateway/gateway-errors.d.ts.map +0 -1
- package/executors/gateway/gateway.impl.d.ts.map +0 -1
- package/executors/gateway/gateway.lib.d.ts.map +0 -1
- package/executors/gateway/health-check.d.ts.map +0 -1
- package/executors/gateway/jwks-verify.d.ts.map +0 -1
- package/executors/gateway/main.prod-gateway.d.ts.map +0 -1
- package/executors/gateway/main.rollout.d.ts.map +0 -1
- package/executors/gateway/manifest.d.ts.map +0 -1
- package/executors/gateway/metrics.d.ts.map +0 -1
- package/executors/gateway/otel-tracing.d.ts.map +0 -1
- package/executors/gateway/prod-gateway.lib.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/access-log.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/app.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/auth.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/cors.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/edge.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/handler.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/hooks.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/observability.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/rollout.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/routing.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/server.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/trust.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/types.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/upgrade.d.ts.map +0 -1
- package/executors/gateway/prod-runtime/upstream.d.ts.map +0 -1
- package/executors/gateway/providers-prod.d.ts.map +0 -1
- package/executors/gateway/rate-limit.d.ts.map +0 -1
- package/executors/gateway/registry-source.d.ts.map +0 -1
- package/executors/gateway/rollout-ops.d.ts.map +0 -1
- package/executors/gateway/runtime/auth.d.ts.map +0 -1
- package/executors/gateway/runtime/chooser.d.ts.map +0 -1
- package/executors/gateway/runtime/control.d.ts.map +0 -1
- package/executors/gateway/runtime/dev-auth.d.ts.map +0 -1
- package/executors/gateway/runtime/discovery.d.ts.map +0 -1
- package/executors/gateway/runtime/edge.d.ts.map +0 -1
- package/executors/gateway/runtime/handler.d.ts.map +0 -1
- package/executors/gateway/runtime/hooks.d.ts.map +0 -1
- package/executors/gateway/runtime/lens-children.d.ts.map +0 -1
- package/executors/gateway/runtime/lifecycle.d.ts.map +0 -1
- package/executors/gateway/runtime/observability.d.ts.map +0 -1
- package/executors/gateway/runtime/pod-env.d.ts.map +0 -1
- package/executors/gateway/runtime/proxy.d.ts.map +0 -1
- package/executors/gateway/runtime/route-registry.d.ts.map +0 -1
- package/executors/gateway/runtime/scope.d.ts.map +0 -1
- package/executors/gateway/runtime/server.d.ts.map +0 -1
- package/executors/gateway/runtime/service-keys.d.ts.map +0 -1
- package/executors/gateway/runtime/types.d.ts.map +0 -1
- package/executors/gateway/runtime/upgrade.d.ts.map +0 -1
- package/executors/gateway/runtime/workspace-registry.d.ts.map +0 -1
- package/executors/gateway/ssrf-guard.d.ts.map +0 -1
- package/executors/serve/serve.impl.d.ts.map +0 -1
- package/executors/trust/trust.impl.d.ts.map +0 -1
- package/gateway.d.ts.map +0 -1
- package/index.d.ts.map +0 -1
- package/main.devserver.d.ts.map +0 -1
- package/tsgo-check-plugin.d.ts.map +0 -1
- package/typecheck-bus.d.ts.map +0 -1
|
@@ -1,131 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.changeChannel = exports.rolloutKey = exports.manifestKey = exports.DEFAULT_REGISTRY_PREFIX = void 0;
|
|
4
|
-
exports.redisRegistrySource = redisRegistrySource;
|
|
5
|
-
exports.createRegistryProviders = createRegistryProviders;
|
|
6
|
-
/**
|
|
7
|
-
* Registry-backed gateway config — true zero-touch discovery. Instead of a
|
|
8
|
-
* mounted file, the manifests + rollout cohorts live in a shared registry
|
|
9
|
-
* (Redis here; k8s ConfigMap/Endpoints can drop in behind `RegistrySource`).
|
|
10
|
-
* A service deploy / the `lensmcp rollout --redis` CD helper writes its keys and
|
|
11
|
-
* publishes a change; the gateway re-reads and live-swaps its route table +
|
|
12
|
-
* cohorts in ~instant — no gateway redeploy, no file mounts.
|
|
13
|
-
*
|
|
14
|
-
* The cluster lib stays dependency-light: `redisRegistrySource` works against a
|
|
15
|
-
* minimal injected `RedisLike` (ioredis satisfies it; the prod entry wires the
|
|
16
|
-
* real client, tests pass a fake). See docs/design/targeted-rollout.md and
|
|
17
|
-
* docs/design/prod-gateway.md (§Distribution).
|
|
18
|
-
*/
|
|
19
|
-
const providers_prod_1 = require("./providers-prod");
|
|
20
|
-
exports.DEFAULT_REGISTRY_PREFIX = 'lensmcp:gw';
|
|
21
|
-
const manifestKey = (prefix, service) => `${prefix}:manifest:${service}`;
|
|
22
|
-
exports.manifestKey = manifestKey;
|
|
23
|
-
const rolloutKey = (prefix, service) => `${prefix}:rollout:${service}`;
|
|
24
|
-
exports.rolloutKey = rolloutKey;
|
|
25
|
-
const changeChannel = (prefix) => `${prefix}:changed`;
|
|
26
|
-
exports.changeChannel = changeChannel;
|
|
27
|
-
function parseManifest(s) {
|
|
28
|
-
try {
|
|
29
|
-
const m = JSON.parse(s);
|
|
30
|
-
return m && typeof m.service === 'string' && Array.isArray(m.mounts) ? m : undefined;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Redis-backed source. Per-service keys (so a deploy touches only its own key,
|
|
38
|
-
* no read-modify-write races): `<prefix>:manifest:<svc>` (a RouteManifest) and
|
|
39
|
-
* `<prefix>:rollout:<svc>` (a VersionCohort[] | RolloutSpec). Writers publish
|
|
40
|
-
* `<prefix>:changed` after a SET; the gateway re-reads on the message.
|
|
41
|
-
*/
|
|
42
|
-
function redisRegistrySource(client, opts = {}) {
|
|
43
|
-
const prefix = opts.prefix ?? exports.DEFAULT_REGISTRY_PREFIX;
|
|
44
|
-
let sub;
|
|
45
|
-
const rolloutPrefix = `${prefix}:rollout:`;
|
|
46
|
-
return {
|
|
47
|
-
async read() {
|
|
48
|
-
const mKeys = await client.keys(`${prefix}:manifest:*`);
|
|
49
|
-
const manifests = mKeys.length
|
|
50
|
-
? (await client.mget(...mKeys)).filter((v) => !!v).map(parseManifest).filter((m) => !!m)
|
|
51
|
-
: [];
|
|
52
|
-
const rKeys = await client.keys(`${rolloutPrefix}*`);
|
|
53
|
-
const endpoints = {};
|
|
54
|
-
if (rKeys.length) {
|
|
55
|
-
const vals = await client.mget(...rKeys);
|
|
56
|
-
rKeys.forEach((k, i) => {
|
|
57
|
-
const v = vals[i];
|
|
58
|
-
if (v)
|
|
59
|
-
try {
|
|
60
|
-
endpoints[k.slice(rolloutPrefix.length)] = JSON.parse(v);
|
|
61
|
-
}
|
|
62
|
-
catch { /* skip a bad entry, keep the rest */ }
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
return { manifests, endpoints };
|
|
66
|
-
},
|
|
67
|
-
async watch(onChange) {
|
|
68
|
-
sub = client.duplicate();
|
|
69
|
-
await sub.subscribe((0, exports.changeChannel)(prefix));
|
|
70
|
-
sub.on('message', () => onChange());
|
|
71
|
-
return async () => { try {
|
|
72
|
-
await sub?.quit();
|
|
73
|
-
}
|
|
74
|
-
catch { /* ignore */ } };
|
|
75
|
-
},
|
|
76
|
-
stop() { void sub?.quit().catch(() => undefined); },
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Compose a ManifestProvider + PodProvider over a RegistrySource. Both refresh
|
|
81
|
-
* together on every change (source.watch) and on a poll fallback. Call
|
|
82
|
-
* `start()` (awaits the first read so the route table is warm) before handing
|
|
83
|
-
* the providers to `startProdGateway`.
|
|
84
|
-
*/
|
|
85
|
-
function createRegistryProviders(source, opts = {}) {
|
|
86
|
-
const onError = opts.onError ?? (() => undefined);
|
|
87
|
-
let current = [];
|
|
88
|
-
const subs = new Set();
|
|
89
|
-
const inner = (0, providers_prod_1.rolloutPodProvider)({});
|
|
90
|
-
let unwatch;
|
|
91
|
-
let timer;
|
|
92
|
-
const refresh = async () => {
|
|
93
|
-
let snap;
|
|
94
|
-
try {
|
|
95
|
-
snap = await source.read();
|
|
96
|
-
}
|
|
97
|
-
catch (e) {
|
|
98
|
-
onError(e);
|
|
99
|
-
return;
|
|
100
|
-
} // keep last-good on a transient error
|
|
101
|
-
current = snap.manifests;
|
|
102
|
-
inner.update(snap.endpoints);
|
|
103
|
-
for (const cb of subs) {
|
|
104
|
-
try {
|
|
105
|
-
cb(current);
|
|
106
|
-
}
|
|
107
|
-
catch (e) {
|
|
108
|
-
onError(e);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
return {
|
|
113
|
-
manifests: { list: () => current, watch: (cb) => { subs.add(cb); return () => subs.delete(cb); } },
|
|
114
|
-
pods: inner,
|
|
115
|
-
async start() {
|
|
116
|
-
await refresh();
|
|
117
|
-
if (source.watch) {
|
|
118
|
-
try {
|
|
119
|
-
unwatch = await source.watch(() => void refresh());
|
|
120
|
-
}
|
|
121
|
-
catch (e) {
|
|
122
|
-
onError(e);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
timer = setInterval(() => void refresh(), opts.pollMs ?? 5000);
|
|
126
|
-
timer.unref?.();
|
|
127
|
-
},
|
|
128
|
-
stop() { unwatch?.(); if (timer)
|
|
129
|
-
clearInterval(timer); source.stop?.(); },
|
|
130
|
-
};
|
|
131
|
-
}
|
|
1
|
+
"use strict";var g=Object.defineProperty;var u=(e,t)=>g(e,"name",{value:t,configurable:!0});var y=Object.defineProperty,s=u((e,t)=>y(e,"name",{value:t,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.changeChannel=exports.rolloutKey=exports.manifestKey=exports.DEFAULT_REGISTRY_PREFIX=void 0,exports.redisRegistrySource=redisRegistrySource,exports.createRegistryProviders=createRegistryProviders;const providers_prod_1=require("./providers-prod");exports.DEFAULT_REGISTRY_PREFIX="lensmcp:gw";const manifestKey=s((e,t)=>`${e}:manifest:${t}`,"manifestKey");exports.manifestKey=manifestKey;const rolloutKey=s((e,t)=>`${e}:rollout:${t}`,"rolloutKey");exports.rolloutKey=rolloutKey;const changeChannel=s(e=>`${e}:changed`,"changeChannel");exports.changeChannel=changeChannel;function parseManifest(e){try{const t=JSON.parse(e);return t&&typeof t.service=="string"&&Array.isArray(t.mounts)?t:void 0}catch{return}}u(parseManifest,"parseManifest"),s(parseManifest,"parseManifest");function redisRegistrySource(e,t={}){const o=t.prefix??exports.DEFAULT_REGISTRY_PREFIX;let a;const i=`${o}:rollout:`;return{async read(){const c=await e.keys(`${o}:manifest:*`),d=c.length?(await e.mget(...c)).filter(r=>!!r).map(parseManifest).filter(r=>!!r):[],n=await e.keys(`${i}*`),l={};if(n.length){const r=await e.mget(...n);n.forEach((p,f)=>{const h=r[f];if(h)try{l[p.slice(i.length)]=JSON.parse(h)}catch{}})}return{manifests:d,endpoints:l}},async watch(c){return a=e.duplicate(),await a.subscribe((0,exports.changeChannel)(o)),a.on("message",()=>c()),async()=>{try{await a?.quit()}catch{}}},stop(){a?.quit().catch(()=>{})}}}u(redisRegistrySource,"redisRegistrySource"),s(redisRegistrySource,"redisRegistrySource");function createRegistryProviders(e,t={}){const o=t.onError??(()=>{});let a=[];const i=new Set,c=(0,providers_prod_1.rolloutPodProvider)({});let d,n;const l=s(async()=>{let r;try{r=await e.read()}catch(p){o(p);return}a=r.manifests,c.update(r.endpoints);for(const p of i)try{p(a)}catch(f){o(f)}},"refresh");return{manifests:{list:s(()=>a,"list"),watch:s(r=>(i.add(r),()=>i.delete(r)),"watch")},pods:c,async start(){if(await l(),e.watch)try{d=await e.watch(()=>{l()})}catch(r){o(r)}n=setInterval(()=>{l()},t.pollMs??5e3),n.unref?.()},stop(){d?.(),n&&clearInterval(n),e.stop?.()}}}u(createRegistryProviders,"createRegistryProviders"),s(createRegistryProviders,"createRegistryProviders");
|
|
@@ -1,167 +1,2 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
exports.applyRolloutOp = applyRolloutOp;
|
|
4
|
-
exports.summarizeRollout = summarizeRollout;
|
|
5
|
-
exports.parseRolloutCommand = parseRolloutCommand;
|
|
6
|
-
const RULE_OPS = new Set(['eq', 'ne', 'in', 'nin', 'prefix', 'suffix', 'contains', 'regex', 'exists', 'gt', 'lt', 'cidr']);
|
|
7
|
-
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
8
|
-
/** Normalize a service entry to a `{rules,cohorts}` spec; reject a flat url list. */
|
|
9
|
-
function asSpec(service, v) {
|
|
10
|
-
if (v === undefined)
|
|
11
|
-
return { rules: [], cohorts: [] };
|
|
12
|
-
if (Array.isArray(v)) {
|
|
13
|
-
if (v.length && typeof v[0] === 'string')
|
|
14
|
-
throw new Error(`service "${service}" is a flat endpoint list; convert it to cohorts first (e.g. add-cohort)`);
|
|
15
|
-
return { rules: [], cohorts: v };
|
|
16
|
-
}
|
|
17
|
-
return { rules: v.rules ?? [], cohorts: v.cohorts ?? [] };
|
|
18
|
-
}
|
|
19
|
-
const findCohort = (spec, version) => spec.cohorts.find((c) => c.version === version);
|
|
20
|
-
function reqWeight(w) {
|
|
21
|
-
if (!Number.isFinite(w) || w < 0)
|
|
22
|
-
throw new Error(`weight must be a number ≥ 0 (got ${w})`);
|
|
23
|
-
return w;
|
|
24
|
-
}
|
|
25
|
-
/** Apply one operation to the rollout table; returns a NEW table. Throws on invalid input. */
|
|
26
|
-
function applyRolloutOp(config, op) {
|
|
27
|
-
const next = clone(config);
|
|
28
|
-
const spec = asSpec(op.service, next[op.service]);
|
|
29
|
-
switch (op.kind) {
|
|
30
|
-
case 'add-cohort': {
|
|
31
|
-
if (findCohort(spec, op.version))
|
|
32
|
-
throw new Error(`cohort "${op.version}" already exists on "${op.service}" — use set-weight`);
|
|
33
|
-
if (!op.urls.length)
|
|
34
|
-
throw new Error('add-cohort needs at least one upstream url');
|
|
35
|
-
spec.cohorts.push({ version: op.version, weight: reqWeight(op.weight), urls: op.urls.slice() });
|
|
36
|
-
break;
|
|
37
|
-
}
|
|
38
|
-
case 'set-weight': {
|
|
39
|
-
const c = findCohort(spec, op.version);
|
|
40
|
-
if (!c)
|
|
41
|
-
throw new Error(`no cohort "${op.version}" on "${op.service}"`);
|
|
42
|
-
c.weight = reqWeight(op.weight);
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
case 'remove-cohort': {
|
|
46
|
-
if (!findCohort(spec, op.version))
|
|
47
|
-
throw new Error(`no cohort "${op.version}" on "${op.service}"`);
|
|
48
|
-
spec.cohorts = spec.cohorts.filter((c) => c.version !== op.version);
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
case 'promote': {
|
|
52
|
-
if (!findCohort(spec, op.version))
|
|
53
|
-
throw new Error(`no cohort "${op.version}" on "${op.service}"`);
|
|
54
|
-
for (const c of spec.cohorts)
|
|
55
|
-
c.weight = c.version === op.version ? 100 : 0;
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
case 'abort': {
|
|
59
|
-
const c = findCohort(spec, op.version);
|
|
60
|
-
if (!c)
|
|
61
|
-
throw new Error(`no cohort "${op.version}" on "${op.service}"`);
|
|
62
|
-
c.weight = 0;
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
case 'add-rule': {
|
|
66
|
-
validateRule(op.rule);
|
|
67
|
-
spec.rules = [...(spec.rules ?? []).filter((r) => !op.rule.name || r.name !== op.rule.name), op.rule]; // replace-by-name (idempotent)
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
case 'remove-rule': {
|
|
71
|
-
const before = (spec.rules ?? []).length;
|
|
72
|
-
spec.rules = (spec.rules ?? []).filter((r) => r.name !== op.name);
|
|
73
|
-
if (spec.rules.length === before)
|
|
74
|
-
throw new Error(`no rule named "${op.name}" on "${op.service}"`);
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
next[op.service] = spec.rules && spec.rules.length ? { rules: spec.rules, cohorts: spec.cohorts } : spec.cohorts;
|
|
79
|
-
return next;
|
|
80
|
-
}
|
|
81
|
-
function validateRule(rule) {
|
|
82
|
-
if (!rule || typeof rule.version !== 'string' || !rule.version)
|
|
83
|
-
throw new Error('rule needs a non-empty version');
|
|
84
|
-
if (!rule.when || typeof rule.when !== 'object')
|
|
85
|
-
throw new Error('rule needs a `when` condition');
|
|
86
|
-
const walk = (n) => {
|
|
87
|
-
const node = n;
|
|
88
|
-
if ('all' in node || 'any' in node || 'not' in node) {
|
|
89
|
-
for (const k of ['all', 'any'])
|
|
90
|
-
if (Array.isArray(node[k]))
|
|
91
|
-
node[k].forEach(walk);
|
|
92
|
-
if (node.not)
|
|
93
|
-
walk(node.not);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (typeof node.attr !== 'string' || !RULE_OPS.has(node.op))
|
|
97
|
-
throw new Error(`invalid rule condition (attr/op): ${JSON.stringify(node)}`);
|
|
98
|
-
};
|
|
99
|
-
walk(rule.when);
|
|
100
|
-
}
|
|
101
|
-
/** Human-readable status for one service (or all). Read-only. */
|
|
102
|
-
function summarizeRollout(config, service) {
|
|
103
|
-
const names = service ? [service] : Object.keys(config);
|
|
104
|
-
if (!names.length)
|
|
105
|
-
return '(no services in rollout config)';
|
|
106
|
-
const lines = [];
|
|
107
|
-
for (const s of names) {
|
|
108
|
-
const entry = config[s];
|
|
109
|
-
if (entry === undefined) {
|
|
110
|
-
lines.push(`${s}: (not present)`);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (Array.isArray(entry) && entry.length && typeof entry[0] === 'string') {
|
|
114
|
-
lines.push(`${s}: flat LB → ${entry.join(', ')}`);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
const spec = asSpec(s, entry);
|
|
118
|
-
const total = spec.cohorts.reduce((a, c) => a + Math.max(0, c.weight), 0) || 1;
|
|
119
|
-
lines.push(`${s}:`);
|
|
120
|
-
for (const c of spec.cohorts)
|
|
121
|
-
lines.push(` • ${c.version.padEnd(16)} weight ${String(c.weight).padStart(3)} (${(Math.max(0, c.weight) / total * 100).toFixed(0)}%) ${c.urls.length} url(s)`);
|
|
122
|
-
for (const r of spec.rules ?? [])
|
|
123
|
-
lines.push(` ▸ rule ${(r.name ?? '(unnamed)').padEnd(16)} → ${r.version} ${ruleSummary(r.when)}`);
|
|
124
|
-
}
|
|
125
|
-
return lines.join('\n');
|
|
126
|
-
}
|
|
127
|
-
function ruleSummary(n) {
|
|
128
|
-
const node = n;
|
|
129
|
-
if ('all' in node)
|
|
130
|
-
return '(' + node.all.map(ruleSummary).join(' AND ') + ')';
|
|
131
|
-
if ('any' in node)
|
|
132
|
-
return '(' + node.any.map(ruleSummary).join(' OR ') + ')';
|
|
133
|
-
if ('not' in node)
|
|
134
|
-
return 'NOT ' + ruleSummary(node.not);
|
|
135
|
-
return `${node.attr} ${node.op}${node.value !== undefined ? ' ' + JSON.stringify(node.value) : ''}`;
|
|
136
|
-
}
|
|
137
|
-
/** Parse CLI argv (after the config-file path) into an op, a status request, or throw a usage error. */
|
|
138
|
-
function parseRolloutCommand(argv) {
|
|
139
|
-
const [cmd, ...a] = argv;
|
|
140
|
-
const need = (n, usage) => { if (a.length < n)
|
|
141
|
-
throw new Error(`usage: ${cmd} ${usage}`); };
|
|
142
|
-
switch (cmd) {
|
|
143
|
-
case 'status': return { status: a[0] };
|
|
144
|
-
case 'add-cohort':
|
|
145
|
-
need(4, '<service> <version> <weight> <url> [url...]');
|
|
146
|
-
return { op: { kind: 'add-cohort', service: a[0], version: a[1], weight: Number(a[2]), urls: a.slice(3) } };
|
|
147
|
-
case 'set-weight':
|
|
148
|
-
need(3, '<service> <version> <weight>');
|
|
149
|
-
return { op: { kind: 'set-weight', service: a[0], version: a[1], weight: Number(a[2]) } };
|
|
150
|
-
case 'remove-cohort':
|
|
151
|
-
need(2, '<service> <version>');
|
|
152
|
-
return { op: { kind: 'remove-cohort', service: a[0], version: a[1] } };
|
|
153
|
-
case 'promote':
|
|
154
|
-
need(2, '<service> <version>');
|
|
155
|
-
return { op: { kind: 'promote', service: a[0], version: a[1] } };
|
|
156
|
-
case 'abort':
|
|
157
|
-
need(2, '<service> <version>');
|
|
158
|
-
return { op: { kind: 'abort', service: a[0], version: a[1] } };
|
|
159
|
-
case 'add-rule':
|
|
160
|
-
need(6, '<service> <name> <attr> <op> <value> <version>');
|
|
161
|
-
return { op: { kind: 'add-rule', service: a[0], rule: { name: a[1], when: { attr: a[2], op: a[3], value: a[4] }, version: a[5] } } };
|
|
162
|
-
case 'remove-rule':
|
|
163
|
-
need(2, '<service> <name>');
|
|
164
|
-
return { op: { kind: 'remove-rule', service: a[0], name: a[1] } };
|
|
165
|
-
default: throw new Error(`unknown command "${cmd ?? ''}". commands: status, add-cohort, set-weight, remove-cohort, promote, abort, add-rule, remove-rule`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
1
|
+
"use strict";var d=Object.defineProperty;var u=(t,r)=>d(t,"name",{value:r,configurable:!0});var h=Object.defineProperty,s=u((t,r)=>h(t,"name",{value:r,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.applyRolloutOp=applyRolloutOp,exports.summarizeRollout=summarizeRollout,exports.parseRolloutCommand=parseRolloutCommand;const RULE_OPS=new Set(["eq","ne","in","nin","prefix","suffix","contains","regex","exists","gt","lt","cidr"]),clone=s(t=>JSON.parse(JSON.stringify(t)),"clone");function asSpec(t,r){if(r===void 0)return{rules:[],cohorts:[]};if(Array.isArray(r)){if(r.length&&typeof r[0]=="string")throw new Error(`service "${t}" is a flat endpoint list; convert it to cohorts first (e.g. add-cohort)`);return{rules:[],cohorts:r}}return{rules:r.rules??[],cohorts:r.cohorts??[]}}u(asSpec,"asSpec"),s(asSpec,"asSpec");const findCohort=s((t,r)=>t.cohorts.find(o=>o.version===r),"findCohort");function reqWeight(t){if(!Number.isFinite(t)||t<0)throw new Error(`weight must be a number \u2265 0 (got ${t})`);return t}u(reqWeight,"reqWeight"),s(reqWeight,"reqWeight");function applyRolloutOp(t,r){const o=clone(t),e=asSpec(r.service,o[r.service]);switch(r.kind){case"add-cohort":{if(findCohort(e,r.version))throw new Error(`cohort "${r.version}" already exists on "${r.service}" \u2014 use set-weight`);if(!r.urls.length)throw new Error("add-cohort needs at least one upstream url");e.cohorts.push({version:r.version,weight:reqWeight(r.weight),urls:r.urls.slice()});break}case"set-weight":{const n=findCohort(e,r.version);if(!n)throw new Error(`no cohort "${r.version}" on "${r.service}"`);n.weight=reqWeight(r.weight);break}case"remove-cohort":{if(!findCohort(e,r.version))throw new Error(`no cohort "${r.version}" on "${r.service}"`);e.cohorts=e.cohorts.filter(n=>n.version!==r.version);break}case"promote":{if(!findCohort(e,r.version))throw new Error(`no cohort "${r.version}" on "${r.service}"`);for(const n of e.cohorts)n.weight=n.version===r.version?100:0;break}case"abort":{const n=findCohort(e,r.version);if(!n)throw new Error(`no cohort "${r.version}" on "${r.service}"`);n.weight=0;break}case"add-rule":{validateRule(r.rule),e.rules=[...(e.rules??[]).filter(n=>!r.rule.name||n.name!==r.rule.name),r.rule];break}case"remove-rule":{const n=(e.rules??[]).length;if(e.rules=(e.rules??[]).filter(i=>i.name!==r.name),e.rules.length===n)throw new Error(`no rule named "${r.name}" on "${r.service}"`);break}}return o[r.service]=e.rules&&e.rules.length?{rules:e.rules,cohorts:e.cohorts}:e.cohorts,o}u(applyRolloutOp,"applyRolloutOp"),s(applyRolloutOp,"applyRolloutOp");function validateRule(t){if(!t||typeof t.version!="string"||!t.version)throw new Error("rule needs a non-empty version");if(!t.when||typeof t.when!="object")throw new Error("rule needs a `when` condition");const r=s(o=>{const e=o;if("all"in e||"any"in e||"not"in e){for(const n of["all","any"])Array.isArray(e[n])&&e[n].forEach(r);e.not&&r(e.not);return}if(typeof e.attr!="string"||!RULE_OPS.has(e.op))throw new Error(`invalid rule condition (attr/op): ${JSON.stringify(e)}`)},"walk");r(t.when)}u(validateRule,"validateRule"),s(validateRule,"validateRule");function summarizeRollout(t,r){const o=r?[r]:Object.keys(t);if(!o.length)return"(no services in rollout config)";const e=[];for(const n of o){const i=t[n];if(i===void 0){e.push(`${n}: (not present)`);continue}if(Array.isArray(i)&&i.length&&typeof i[0]=="string"){e.push(`${n}: flat LB \u2192 ${i.join(", ")}`);continue}const c=asSpec(n,i),l=c.cohorts.reduce((a,v)=>a+Math.max(0,v.weight),0)||1;e.push(`${n}:`);for(const a of c.cohorts)e.push(` \u2022 ${a.version.padEnd(16)} weight ${String(a.weight).padStart(3)} (${(Math.max(0,a.weight)/l*100).toFixed(0)}%) ${a.urls.length} url(s)`);for(const a of c.rules??[])e.push(` \u25B8 rule ${(a.name??"(unnamed)").padEnd(16)} \u2192 ${a.version} ${ruleSummary(a.when)}`)}return e.join(`
|
|
2
|
+
`)}u(summarizeRollout,"summarizeRollout"),s(summarizeRollout,"summarizeRollout");function ruleSummary(t){const r=t;return"all"in r?"("+r.all.map(ruleSummary).join(" AND ")+")":"any"in r?"("+r.any.map(ruleSummary).join(" OR ")+")":"not"in r?"NOT "+ruleSummary(r.not):`${r.attr} ${r.op}${r.value!==void 0?" "+JSON.stringify(r.value):""}`}u(ruleSummary,"ruleSummary"),s(ruleSummary,"ruleSummary");function parseRolloutCommand(t){const[r,...o]=t,e=s((n,i)=>{if(o.length<n)throw new Error(`usage: ${r} ${i}`)},"need");switch(r){case"status":return{status:o[0]};case"add-cohort":return e(4,"<service> <version> <weight> <url> [url...]"),{op:{kind:"add-cohort",service:o[0],version:o[1],weight:Number(o[2]),urls:o.slice(3)}};case"set-weight":return e(3,"<service> <version> <weight>"),{op:{kind:"set-weight",service:o[0],version:o[1],weight:Number(o[2])}};case"remove-cohort":return e(2,"<service> <version>"),{op:{kind:"remove-cohort",service:o[0],version:o[1]}};case"promote":return e(2,"<service> <version>"),{op:{kind:"promote",service:o[0],version:o[1]}};case"abort":return e(2,"<service> <version>"),{op:{kind:"abort",service:o[0],version:o[1]}};case"add-rule":return e(6,"<service> <name> <attr> <op> <value> <version>"),{op:{kind:"add-rule",service:o[0],rule:{name:o[1],when:{attr:o[2],op:o[3],value:o[4]},version:o[5]}}};case"remove-rule":return e(2,"<service> <name>"),{op:{kind:"remove-rule",service:o[0],name:o[1]}};default:throw new Error(`unknown command "${r??""}". commands: status, add-cohort, set-weight, remove-cohort, promote, abort, add-rule, remove-rule`)}}u(parseRolloutCommand,"parseRolloutCommand"),s(parseRolloutCommand,"parseRolloutCommand");
|
|
@@ -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");
|
|
@@ -11,4 +11,3 @@ import { type WorkspaceEntry } from './workspace-registry';
|
|
|
11
11
|
export declare function renderChooserPage(entries: WorkspaceEntry[], currentKey: string): string;
|
|
12
12
|
/** Serve the chooser at the dashboard-host root. Reads the registry fresh so live status is current. */
|
|
13
13
|
export declare function serveChooser(res: http.ServerResponse, currentKey: string): void;
|
|
14
|
-
//# sourceMappingURL=chooser.d.ts.map
|
|
@@ -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");
|
|
@@ -18,4 +18,3 @@ export interface ControlServer {
|
|
|
18
18
|
/** Start the control listener. The socket is loopback-only (a filesystem socket), so no IP allow-listing
|
|
19
19
|
* is needed — anything that can open it is already on this machine as this user. */
|
|
20
20
|
export declare function startControlServer(deps: ControlServerDeps): ControlServer;
|
|
21
|
-
//# sourceMappingURL=control.d.ts.map
|
|
@@ -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");
|
|
@@ -13,4 +13,3 @@ export declare function verifyDevJwt(req: http.IncomingMessage, verifier?: JwtVe
|
|
|
13
13
|
export declare function stampIdentity(req: http.IncomingMessage, claims: Record<string, unknown>): void;
|
|
14
14
|
/** Build the ABAC attribute bag from the verified claims + request envelope. */
|
|
15
15
|
export declare const devAttrs: (req: http.IncomingMessage, claims: Record<string, unknown>) => Attrs;
|
|
16
|
-
//# sourceMappingURL=dev-auth.d.ts.map
|