@lensmcp/cluster 1.18.4 → 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.
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,232 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.staticManifestProvider = staticManifestProvider;
4
- exports.fileManifestProvider = fileManifestProvider;
5
- exports.rolloutPodProvider = rolloutPodProvider;
6
- exports.endpointsPodProvider = endpointsPodProvider;
7
- /**
8
- * Production providers — the environment-specific halves of the gateway that
9
- * the dev side fills with workspace-scan + unix-socket pools. Here:
10
- *
11
- * - manifest source: a JSON file (or a static array), watched + atomically
12
- * swapped. A registry-backed provider (Redis key-space / k8s ConfigMaps)
13
- * plugs in behind the same `ManifestProvider` interface later.
14
- * - pod resolution: `endpointsPodProvider` round-robins per-service upstream
15
- * URLs (gateway-side LB). When a manifest carries its own `upstream` (a
16
- * k8s Service URL that load-balances pods itself), the core uses that
17
- * directly and no PodProvider is needed.
18
- *
19
- * Bad input never takes the gateway down: an unparseable manifest file is
20
- * rejected and the last-good table keeps serving (versioned swap).
21
- */
22
- const node_fs_1 = require("node:fs");
23
- const manifest_1 = require("./manifest");
24
- const toSpec = (v) => Array.isArray(v) ? { rules: [], cohorts: v } : { rules: v.rules ?? [], cohorts: v.cohorts ?? [] };
25
- /** A fixed set of manifests (tests, single-shot config). */
26
- function staticManifestProvider(manifests) {
27
- return { list: () => manifests };
28
- }
29
- function parseManifests(raw) {
30
- let data;
31
- try {
32
- data = JSON.parse(raw);
33
- }
34
- catch {
35
- return undefined;
36
- }
37
- // Accept either a bare array or `{ manifests: [...] }`.
38
- const arr = Array.isArray(data) ? data : data?.manifests;
39
- if (!Array.isArray(arr))
40
- return undefined;
41
- const ok = arr.filter((m) => !!m && typeof m === 'object' && typeof m.service === 'string' && Array.isArray(m.mounts));
42
- // A file that parsed but yielded ZERO valid manifests is treated as bad
43
- // (likely a truncated/half-written deploy) — keep last-good rather than
44
- // black-hole all traffic.
45
- return ok.length > 0 ? ok : undefined;
46
- }
47
- /**
48
- * Watch a manifests JSON file; atomically swap the in-memory set on a valid
49
- * change, reject and keep last-good on a bad one. `watch` notifies subscribers
50
- * with the new set. Poll fallback covers editors/atomic-rename + platforms
51
- * where fs.watch is unreliable.
52
- */
53
- function fileManifestProvider(filePath, opts = {}) {
54
- const onError = opts.onError ?? (() => undefined);
55
- let current = [];
56
- let lastMtimeMs = -1;
57
- const subs = new Set();
58
- const reload = () => {
59
- if (!(0, node_fs_1.existsSync)(filePath))
60
- return;
61
- let mtime;
62
- try {
63
- mtime = (0, node_fs_1.statSync)(filePath).mtimeMs;
64
- }
65
- catch {
66
- return;
67
- }
68
- if (mtime === lastMtimeMs)
69
- return;
70
- let raw;
71
- try {
72
- raw = (0, node_fs_1.readFileSync)(filePath, 'utf8');
73
- }
74
- catch (e) {
75
- onError(e);
76
- return;
77
- }
78
- const parsed = parseManifests(raw);
79
- if (!parsed) {
80
- onError(new Error(`[gateway] rejecting invalid manifest file ${filePath} — keeping last-good`));
81
- return;
82
- }
83
- lastMtimeMs = mtime;
84
- current = parsed;
85
- for (const cb of subs) {
86
- try {
87
- cb(current);
88
- }
89
- catch (e) {
90
- onError(e);
91
- }
92
- }
93
- };
94
- reload(); // initial load (best-effort; empty until the file appears)
95
- let watcher;
96
- try {
97
- watcher = (0, node_fs_1.watch)(filePath, { persistent: false }, () => reload());
98
- }
99
- catch { /* dir may not exist yet — poll covers it */ }
100
- const timer = setInterval(reload, opts.pollMs ?? 1000);
101
- timer.unref?.();
102
- return {
103
- list: () => current,
104
- watch: (cb) => { subs.add(cb); return () => subs.delete(cb); },
105
- stop: () => { try {
106
- watcher?.close();
107
- }
108
- catch { /* ignore */ } clearInterval(timer); },
109
- };
110
- }
111
- /**
112
- * Rollout-aware pod provider: each service is a set of VERSION COHORTS, and
113
- * `pick` splits traffic across them by weight (smooth weighted round-robin —
114
- * a 90/10 canary interleaves, never bursts) and round-robins endpoints WITHIN
115
- * the chosen cohort. `pick(svc, {version})` pins a cohort (sticky canary /
116
- * debugging). `update` swaps weights/cohorts live — a rollout ramps
117
- * (10→25→50→100) or rolls back without redeploying the gateway. The returned
118
- * Upstream carries `version` so the gateway can surface which cohort served.
119
- */
120
- function rolloutPodProvider(table) {
121
- const svc = new Map();
122
- const build = (spec) => ({
123
- cohorts: spec.cohorts.map((c) => ({ ...c, urls: c.urls.slice() })),
124
- select: (0, manifest_1.smoothWeightedSelector)(spec.cohorts.map((c) => c.weight)),
125
- rr: spec.cohorts.map(() => -1),
126
- rules: spec.rules ?? [],
127
- });
128
- const load = (t) => {
129
- for (const [s, v] of Object.entries(t))
130
- svc.set(s, build(toSpec(v)));
131
- for (const s of [...svc.keys()])
132
- if (!t[s])
133
- svc.delete(s);
134
- };
135
- load(table);
136
- const firstNonEmpty = (st) => st.cohorts.findIndex((c) => c.urls.length > 0);
137
- return {
138
- pick(service, opts) {
139
- const st = svc.get(service);
140
- if (!st || st.cohorts.length === 0)
141
- return undefined;
142
- const isHealthy = opts?.isHealthy;
143
- // round-robin the chosen cohort, preferring healthy endpoints; undefined if none pass.
144
- const pickUrl = (ci, gate) => {
145
- const cohort = st.cohorts[ci];
146
- if (!cohort || cohort.urls.length === 0)
147
- return undefined;
148
- const n = cohort.urls.length;
149
- for (let i = 0; i < n; i++) {
150
- st.rr[ci] = (st.rr[ci] + 1) % n;
151
- const u = cohort.urls[st.rr[ci]];
152
- if (!gate || gate(u))
153
- return u;
154
- }
155
- return undefined;
156
- };
157
- const pinnedCi = opts?.version ? st.cohorts.findIndex((c) => c.version === opts.version && c.urls.length > 0) : -1;
158
- if (pinnedCi >= 0) {
159
- const url = pickUrl(pinnedCi, isHealthy) ?? pickUrl(pinnedCi); // fail-open WITHIN the pinned cohort
160
- return url !== undefined ? { url, version: st.cohorts[pinnedCi].version } : undefined;
161
- }
162
- // not pinned (or unknown version) → weighted selection (unchanged contract)
163
- let ci = st.select();
164
- if (!st.cohorts[ci] || st.cohorts[ci].urls.length === 0)
165
- ci = firstNonEmpty(st);
166
- let url = pickUrl(ci, isHealthy);
167
- // chosen cohort has no healthy endpoint → try any cohort that does
168
- if (url === undefined && isHealthy) {
169
- for (let i = 0; i < st.cohorts.length; i++) {
170
- const u = pickUrl(i, isHealthy);
171
- if (u !== undefined) {
172
- ci = i;
173
- url = u;
174
- break;
175
- }
176
- }
177
- }
178
- if (url === undefined)
179
- url = pickUrl(ci); // fail-open: everything looks down → still route
180
- return url !== undefined ? { url, version: st.cohorts[ci].version } : undefined;
181
- },
182
- endpoints() {
183
- const urls = new Set();
184
- for (const st of svc.values())
185
- for (const c of st.cohorts)
186
- for (const u of c.urls)
187
- urls.add(u);
188
- return [...urls];
189
- },
190
- // first targeting rule that matches the request's attributes (sticky-by-attribute)
191
- resolveVersion(service, attrs) {
192
- const st = svc.get(service);
193
- return st && st.rules.length ? (0, manifest_1.pickTargetedVersion)(st.rules, attrs) : undefined;
194
- },
195
- // consistent-hash a stable key to a cohort version (sticky weighted split)
196
- bucket(service, key) {
197
- const st = svc.get(service);
198
- if (!st || st.cohorts.length === 0)
199
- return undefined;
200
- const cohort = st.cohorts[(0, manifest_1.bucketByWeight)(`${key}:${service}`, st.cohorts.map((c) => c.weight))];
201
- return cohort ? { version: cohort.version } : undefined;
202
- },
203
- ensureUp: async () => true, // prod services are always-on (k8s keeps them up)
204
- drop(service, upstream) {
205
- const st = svc.get(service);
206
- if (st && 'url' in upstream)
207
- for (const c of st.cohorts)
208
- c.urls = c.urls.filter((u) => u !== upstream.url);
209
- },
210
- update: load,
211
- };
212
- }
213
- /**
214
- * Flat single-version load balancing — the common case (no rollout in
215
- * progress). A thin wrapper over `rolloutPodProvider` with one synthetic
216
- * cohort per service, so the rollout path is the only code path. The synthetic
217
- * version is STRIPPED from the result: flat LB carries no `version`, so a
218
- * response only gets `x-lensmcp-version` (and a `rollout` trace step) when a
219
- * genuine multi-cohort rollout served it.
220
- */
221
- function endpointsPodProvider(endpoints) {
222
- const toCohorts = (eps) => Object.fromEntries(Object.entries(eps).map(([s, urls]) => [s, [{ version: '_flat', weight: 1, urls }]]));
223
- const inner = rolloutPodProvider(toCohorts(endpoints));
224
- const unversion = (u) => u && 'url' in u ? { url: u.url } : u;
225
- return {
226
- ...inner,
227
- pick: (service, opts) => unversion(inner.pick(service, opts)),
228
- resolveVersion: () => undefined, // flat LB: no targeting rules
229
- bucket: () => undefined, // flat LB: single version, nothing to stick to
230
- update: (next) => inner.update(toCohorts(next)),
231
- };
232
- }
1
+ "use strict";var b=Object.defineProperty;var p=(s,o)=>b(s,"name",{value:o,configurable:!0});var w=Object.defineProperty,n=p((s,o)=>w(s,"name",{value:o,configurable:!0}),"n");Object.defineProperty(exports,"__esModule",{value:!0}),exports.staticManifestProvider=staticManifestProvider,exports.fileManifestProvider=fileManifestProvider,exports.rolloutPodProvider=rolloutPodProvider,exports.endpointsPodProvider=endpointsPodProvider;const node_fs_1=require("node:fs"),manifest_1=require("./manifest"),toSpec=n(s=>Array.isArray(s)?{rules:[],cohorts:s}:{rules:s.rules??[],cohorts:s.cohorts??[]},"toSpec");function staticManifestProvider(s){return{list:n(()=>s,"list")}}p(staticManifestProvider,"staticManifestProvider"),n(staticManifestProvider,"staticManifestProvider");function parseManifests(s){let o;try{o=JSON.parse(s)}catch{return}const a=Array.isArray(o)?o:o?.manifests;if(!Array.isArray(a))return;const f=a.filter(i=>!!i&&typeof i=="object"&&typeof i.service=="string"&&Array.isArray(i.mounts));return f.length>0?f:void 0}p(parseManifests,"parseManifests"),n(parseManifests,"parseManifests");function fileManifestProvider(s,o={}){const a=o.onError??(()=>{});let f=[],i=-1;const t=new Set,e=n(()=>{if(!(0,node_fs_1.existsSync)(s))return;let l;try{l=(0,node_fs_1.statSync)(s).mtimeMs}catch{return}if(l===i)return;let v;try{v=(0,node_fs_1.readFileSync)(s,"utf8")}catch(h){a(h);return}const d=parseManifests(v);if(!d){a(new Error(`[gateway] rejecting invalid manifest file ${s} \u2014 keeping last-good`));return}i=l,f=d;for(const h of t)try{h(f)}catch(c){a(c)}},"reload");e();let r;try{r=(0,node_fs_1.watch)(s,{persistent:!1},()=>e())}catch{}const u=setInterval(e,o.pollMs??1e3);return u.unref?.(),{list:n(()=>f,"list"),watch:n(l=>(t.add(l),()=>t.delete(l)),"watch"),stop:n(()=>{try{r?.close()}catch{}clearInterval(u)},"stop")}}p(fileManifestProvider,"fileManifestProvider"),n(fileManifestProvider,"fileManifestProvider");function rolloutPodProvider(s){const o=new Map,a=n(t=>({cohorts:t.cohorts.map(e=>({...e,urls:e.urls.slice()})),select:(0,manifest_1.smoothWeightedSelector)(t.cohorts.map(e=>e.weight)),rr:t.cohorts.map(()=>-1),rules:t.rules??[]}),"build"),f=n(t=>{for(const[e,r]of Object.entries(t))o.set(e,a(toSpec(r)));for(const e of[...o.keys()])t[e]||o.delete(e)},"load");f(s);const i=n(t=>t.cohorts.findIndex(e=>e.urls.length>0),"firstNonEmpty");return{pick(t,e){const r=o.get(t);if(!r||r.cohorts.length===0)return;const u=e?.isHealthy,l=n((c,g)=>{const y=r.cohorts[c];if(!y||y.urls.length===0)return;const P=y.urls.length;for(let m=0;m<P;m++){r.rr[c]=(r.rr[c]+1)%P;const k=y.urls[r.rr[c]];if(!g||g(k))return k}},"pickUrl"),v=e?.version?r.cohorts.findIndex(c=>c.version===e.version&&c.urls.length>0):-1;if(v>=0){const c=l(v,u)??l(v);return c!==void 0?{url:c,version:r.cohorts[v].version}:void 0}let d=r.select();(!r.cohorts[d]||r.cohorts[d].urls.length===0)&&(d=i(r));let h=l(d,u);if(h===void 0&&u)for(let c=0;c<r.cohorts.length;c++){const g=l(c,u);if(g!==void 0){d=c,h=g;break}}return h===void 0&&(h=l(d)),h!==void 0?{url:h,version:r.cohorts[d].version}:void 0},endpoints(){const t=new Set;for(const e of o.values())for(const r of e.cohorts)for(const u of r.urls)t.add(u);return[...t]},resolveVersion(t,e){const r=o.get(t);return r&&r.rules.length?(0,manifest_1.pickTargetedVersion)(r.rules,e):void 0},bucket(t,e){const r=o.get(t);if(!r||r.cohorts.length===0)return;const u=r.cohorts[(0,manifest_1.bucketByWeight)(`${e}:${t}`,r.cohorts.map(l=>l.weight))];return u?{version:u.version}:void 0},ensureUp:n(async()=>!0,"ensureUp"),drop(t,e){const r=o.get(t);if(r&&"url"in e)for(const u of r.cohorts)u.urls=u.urls.filter(l=>l!==e.url)},update:f}}p(rolloutPodProvider,"rolloutPodProvider"),n(rolloutPodProvider,"rolloutPodProvider");function endpointsPodProvider(s){const o=n(i=>Object.fromEntries(Object.entries(i).map(([t,e])=>[t,[{version:"_flat",weight:1,urls:e}]])),"toCohorts"),a=rolloutPodProvider(o(s)),f=n(i=>i&&"url"in i?{url:i.url}:i,"unversion");return{...a,pick:n((i,t)=>f(a.pick(i,t)),"pick"),resolveVersion:n(()=>{},"resolveVersion"),bucket:n(()=>{},"bucket"),update:n(i=>a.update(o(i)),"update")}}p(endpointsPodProvider,"endpointsPodProvider"),n(endpointsPodProvider,"endpointsPodProvider");
@@ -1,45 +1,4 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createRateLimiter = createRateLimiter;
4
- exports.createRedisRateLimiter = createRedisRateLimiter;
5
- function createRateLimiter(opts) {
6
- const limit = Math.max(1, opts.limit);
7
- const windowMs = Math.max(1, opts.windowMs);
8
- const now = opts.now ?? Date.now;
9
- const refillPerMs = limit / windowMs;
10
- const buckets = new Map();
11
- const refilled = (b, t) => Math.min(limit, b.tokens + (t - b.last) * refillPerMs);
12
- // Evict buckets that have refilled to full (idle) so the map stays bounded.
13
- const sweep = setInterval(() => {
14
- const t = now();
15
- for (const [k, b] of buckets)
16
- if (refilled(b, t) >= limit)
17
- buckets.delete(k);
18
- }, windowMs);
19
- sweep.unref?.();
20
- return {
21
- check(key) {
22
- const t = now();
23
- let b = buckets.get(key);
24
- if (!b) {
25
- b = { tokens: limit, last: t };
26
- buckets.set(key, b);
27
- }
28
- b.tokens = refilled(b, t);
29
- b.last = t;
30
- if (b.tokens >= 1) {
31
- b.tokens -= 1;
32
- return { allowed: true, remaining: Math.floor(b.tokens), retryAfterMs: 0 };
33
- }
34
- return { allowed: false, remaining: 0, retryAfterMs: Math.ceil((1 - b.tokens) / refillPerMs) };
35
- },
36
- stop() { clearInterval(sweep); },
37
- };
38
- }
39
- // Atomic token bucket in one round-trip. Stores fractional tokens (Redis keeps the
40
- // string form) so refill is exact across calls; only the returned remaining/retry
41
- // are integer-coerced by the Lua→RESP boundary, which is fine.
42
- const BUCKET_LUA = `
1
+ "use strict";var A=Object.defineProperty;var u=(r,e)=>A(r,"name",{value:e,configurable:!0});var R=Object.defineProperty,a=u((r,e)=>R(r,"name",{value:e,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createRateLimiter=createRateLimiter,exports.createRedisRateLimiter=createRedisRateLimiter;function createRateLimiter(r){const e=Math.max(1,r.limit),i=Math.max(1,r.windowMs),n=r.now??Date.now,c=e/i,s=new Map,m=a((l,o)=>Math.min(e,l.tokens+(o-l.last)*c),"refilled"),f=setInterval(()=>{const l=n();for(const[o,t]of s)m(t,l)>=e&&s.delete(o)},i);return f.unref?.(),{check(l){const o=n();let t=s.get(l);return t||(t={tokens:e,last:o},s.set(l,t)),t.tokens=m(t,o),t.last=o,t.tokens>=1?(t.tokens-=1,{allowed:!0,remaining:Math.floor(t.tokens),retryAfterMs:0}):{allowed:!1,remaining:0,retryAfterMs:Math.ceil((1-t.tokens)/c)}},stop(){clearInterval(f)}}}u(createRateLimiter,"createRateLimiter"),a(createRateLimiter,"createRateLimiter");const BUCKET_LUA=`
43
2
  local b = redis.call('HMGET', KEYS[1], 't', 's')
44
3
  local cap = tonumber(ARGV[1])
45
4
  local refill = tonumber(ARGV[2])
@@ -56,36 +15,4 @@ redis.call('HSET', KEYS[1], 't', tokens, 's', now)
56
15
  redis.call('PEXPIRE', KEYS[1], ttl)
57
16
  local retry = 0
58
17
  if ok == 0 then retry = math.ceil((1 - tokens) / refill) end
59
- return {ok, math.floor(tokens), retry}`;
60
- const RL_TIMEOUT = Symbol('rl-timeout');
61
- const RL_FAIL = Symbol('rl-fail');
62
- function createRedisRateLimiter(client, opts) {
63
- const limit = Math.max(1, opts.limit);
64
- const windowMs = Math.max(1, opts.windowMs);
65
- const prefix = opts.prefix ?? 'lensmcp:gw:rl:';
66
- const timeoutMs = Math.max(1, opts.timeoutMs ?? 100);
67
- const failOpen = opts.failOpen !== false;
68
- const refillPerMs = limit / windowMs;
69
- const ttlMs = Math.ceil(windowMs) + 1000; // evict idle buckets shortly after a full refill
70
- const onFail = () => failOpen
71
- ? { allowed: true, remaining: limit, retryAfterMs: 0 }
72
- : { allowed: false, remaining: 0, retryAfterMs: windowMs };
73
- return {
74
- async check(key) {
75
- let timer;
76
- // The eval branch is mapped so it can never reject (→ RL_FAIL) — that keeps a
77
- // late rejection (after a timeout already won the race) from going unhandled.
78
- const evalP = Promise.resolve(client.eval(BUCKET_LUA, 1, `${prefix}${key}`, limit, refillPerMs, ttlMs)).then((v) => v, () => RL_FAIL);
79
- const timeoutP = new Promise((res) => { timer = setTimeout(() => res(RL_TIMEOUT), timeoutMs); });
80
- const res = await Promise.race([evalP, timeoutP]);
81
- clearTimeout(timer);
82
- if (res === RL_TIMEOUT || res === RL_FAIL)
83
- return onFail();
84
- const [ok, remaining, retry] = res;
85
- return ok === 1
86
- ? { allowed: true, remaining: Number(remaining) || 0, retryAfterMs: 0 }
87
- : { allowed: false, remaining: 0, retryAfterMs: Number(retry) || windowMs };
88
- },
89
- stop() { },
90
- };
91
- }
18
+ return {ok, math.floor(tokens), retry}`,RL_TIMEOUT=Symbol("rl-timeout"),RL_FAIL=Symbol("rl-fail");function createRedisRateLimiter(r,e){const i=Math.max(1,e.limit),n=Math.max(1,e.windowMs),c=e.prefix??"lensmcp:gw:rl:",s=Math.max(1,e.timeoutMs??100),m=e.failOpen!==!1,f=i/n,l=Math.ceil(n)+1e3,o=a(()=>m?{allowed:!0,remaining:i,retryAfterMs:0}:{allowed:!1,remaining:0,retryAfterMs:n},"onFail");return{async check(t){let M;const w=Promise.resolve(r.eval(BUCKET_LUA,1,`${c}${t}`,i,f,l)).then(d=>d,()=>RL_FAIL),h=new Promise(d=>{M=setTimeout(()=>d(RL_TIMEOUT),s)}),k=await Promise.race([w,h]);if(clearTimeout(M),k===RL_TIMEOUT||k===RL_FAIL)return o();const[b,y,p]=k;return b===1?{allowed:!0,remaining:Number(y)||0,retryAfterMs:0}:{allowed:!1,remaining:0,retryAfterMs:Number(p)||n}},stop(){}}}u(createRedisRateLimiter,"createRedisRateLimiter"),a(createRedisRateLimiter,"createRedisRateLimiter");
@@ -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
- Object.defineProperty(exports, "__esModule", { value: true });
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");