@winstonsayno/mcp-gateway 5.0.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Multi-region active-active (5.2).
3
+ *
4
+ * Every region runs a full gateway. Regions replicate a small shared key-value state (last-writer-wins per key,
5
+ * ties broken by region id) and gossip which upstream servers are online, so a call whose upstream is down locally
6
+ * can be failed over to a region that has it.
7
+ *
8
+ * ```yaml
9
+ * regions:
10
+ * self: eu-west
11
+ * syncIntervalMs: 5000
12
+ * peers:
13
+ * - { id: us-east, url: https://us.gw.example.com, apiKey: ${US_ADMIN_KEY}, priority: 1 }
14
+ * ```
15
+ *
16
+ * - `GET /admin/regions` — this region, peers (health, last sync, servers online), replicated key count.
17
+ * - `POST /admin/regions/sync` — peer exchange `{ region, since, entries, servers }` → `{ region, entries, servers }`.
18
+ * - `GET|PUT|DELETE /admin/regions/kv/:key` — replicated state (`PUT` body `{ value }`).
19
+ * - `GET /admin/regions/route/:serverId` — where a call to that server should run: `local`, a peer, or none.
20
+ *
21
+ * @module features/regions
22
+ */
23
+ import { z } from 'zod';
24
+ import { registerFeature, badRequest, objectBody } from '../gateway/features.js';
25
+ export const RegionsSchema = z
26
+ .object({
27
+ self: z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, 'lowercase letters, digits and dashes'),
28
+ syncIntervalMs: z.number().int().min(250).default(5000),
29
+ /** Consecutive failed syncs before a peer is considered down. */
30
+ downAfter: z.number().int().min(1).default(3),
31
+ peers: z
32
+ .array(z.object({ id: z.string().min(1), url: z.string().url(), apiKey: z.string().optional(), priority: z.number().int().min(0).default(100) }).strict())
33
+ .default([]),
34
+ })
35
+ .strict();
36
+ /** Last-writer-wins: newer ts wins; equal ts → lexicographically larger region wins. */
37
+ export const newer = (a, b) => !b || a.ts > b.ts || (a.ts === b.ts && a.region > b.region);
38
+ export class RegionMesh {
39
+ cfg;
40
+ now;
41
+ entries = new Map();
42
+ peers = new Map();
43
+ clock = 0;
44
+ timer;
45
+ constructor(cfg, now = Date.now) {
46
+ this.cfg = cfg;
47
+ this.now = now;
48
+ this.configure(cfg);
49
+ }
50
+ get self() {
51
+ return this.cfg.self;
52
+ }
53
+ configure(cfg) {
54
+ this.cfg = cfg;
55
+ const keep = new Set(cfg.peers.map((p) => p.id));
56
+ for (const id of [...this.peers.keys()])
57
+ if (!keep.has(id))
58
+ this.peers.delete(id);
59
+ for (const p of cfg.peers) {
60
+ const cur = this.peers.get(p.id);
61
+ this.peers.set(p.id, cur ? { ...cur, url: p.url, priority: p.priority } : { id: p.id, url: p.url, priority: p.priority, status: 'unknown', failures: 0, cursor: 0, servers: [] });
62
+ }
63
+ }
64
+ tick() {
65
+ this.clock = Math.max(this.clock + 1, this.now());
66
+ return this.clock;
67
+ }
68
+ get(key) {
69
+ const e = this.entries.get(key);
70
+ return e && e.value !== null ? e.value : undefined;
71
+ }
72
+ put(key, value) {
73
+ const e = { key, value: value === undefined ? null : value, ts: this.tick(), region: this.cfg.self };
74
+ this.entries.set(key, e);
75
+ return e;
76
+ }
77
+ delete(key) {
78
+ const had = this.get(key) !== undefined;
79
+ this.put(key, null);
80
+ return had;
81
+ }
82
+ size() {
83
+ return [...this.entries.values()].filter((e) => e.value !== null).length;
84
+ }
85
+ /** Merge remote entries; returns how many changed local state. */
86
+ merge(entries) {
87
+ let n = 0;
88
+ for (const e of entries) {
89
+ if (!e || typeof e.key !== 'string' || typeof e.ts !== 'number' || typeof e.region !== 'string')
90
+ continue;
91
+ if (newer(e, this.entries.get(e.key))) {
92
+ this.entries.set(e.key, { key: e.key, value: e.value ?? null, ts: e.ts, region: e.region });
93
+ n++;
94
+ }
95
+ this.clock = Math.max(this.clock, e.ts);
96
+ }
97
+ return n;
98
+ }
99
+ /** Entries changed after `since` (any origin, so state relays through intermediate regions). */
100
+ delta(since = 0) {
101
+ return [...this.entries.values()].filter((e) => e.ts > since).sort((a, b) => a.ts - b.ts);
102
+ }
103
+ peerList() {
104
+ return [...this.peers.values()].sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
105
+ }
106
+ /** Record a peer's gossip (from an inbound or outbound sync). */
107
+ sawPeer(id, servers, cursor) {
108
+ const p = this.peers.get(id);
109
+ if (!p)
110
+ return;
111
+ p.status = 'up';
112
+ p.failures = 0;
113
+ p.lastSync = new Date(this.now()).toISOString();
114
+ delete p.lastError;
115
+ if (Array.isArray(servers))
116
+ p.servers = servers.map(String);
117
+ if (typeof cursor === 'number')
118
+ p.cursor = Math.max(p.cursor, cursor);
119
+ }
120
+ peerFailed(id, err) {
121
+ const p = this.peers.get(id);
122
+ if (!p)
123
+ return;
124
+ p.failures++;
125
+ p.lastError = err;
126
+ if (p.failures >= this.cfg.downAfter)
127
+ p.status = 'down';
128
+ }
129
+ /** Where a call to `serverId` should run. */
130
+ route(serverId, localOnline) {
131
+ if (localOnline.includes(serverId))
132
+ return { target: 'local' };
133
+ const peer = this.peerList().find((p) => p.status === 'up' && p.servers.includes(serverId));
134
+ return peer ? { target: 'peer', peer: peer.id, url: peer.url } : { target: 'none' };
135
+ }
136
+ /** One outbound sync round with every peer. */
137
+ async syncOnce(localOnline, f = fetch) {
138
+ await Promise.all(this.peerList().map(async (p) => {
139
+ const conf = this.cfg.peers.find((x) => x.id === p.id);
140
+ try {
141
+ const res = await f(`${p.url.replace(/\/$/, '')}/api/v1/admin/regions/sync`, {
142
+ method: 'POST',
143
+ headers: { 'content-type': 'application/json', ...(conf?.apiKey ? { authorization: `Bearer ${conf.apiKey}` } : {}) },
144
+ body: JSON.stringify({ region: this.cfg.self, since: p.cursor, entries: this.delta(0), servers: localOnline }),
145
+ signal: AbortSignal.timeout(Math.max(1000, this.cfg.syncIntervalMs)),
146
+ });
147
+ if (!res.ok)
148
+ throw new Error(`HTTP ${res.status}`);
149
+ const body = (await res.json());
150
+ const entries = Array.isArray(body.entries) ? body.entries : [];
151
+ this.merge(entries);
152
+ this.sawPeer(p.id, body.servers, entries.reduce((m, e) => Math.max(m, e.ts ?? 0), 0));
153
+ }
154
+ catch (e) {
155
+ this.peerFailed(p.id, e.message);
156
+ }
157
+ }));
158
+ }
159
+ start(localOnline, f = fetch) {
160
+ this.stop();
161
+ if (!this.cfg.peers.length)
162
+ return;
163
+ this.timer = setInterval(() => void this.syncOnce(localOnline(), f), this.cfg.syncIntervalMs);
164
+ this.timer.unref?.();
165
+ }
166
+ stop() {
167
+ if (this.timer)
168
+ clearInterval(this.timer);
169
+ this.timer = undefined;
170
+ }
171
+ status() {
172
+ return { self: this.cfg.self, syncIntervalMs: this.cfg.syncIntervalMs, keys: this.size(), peers: this.peerList() };
173
+ }
174
+ }
175
+ /** Resolve (validate + defaults) a `regions` section. */
176
+ export const resolveRegions = (raw) => RegionsSchema.parse(raw);
177
+ function meshFor(ctx, state) {
178
+ const raw = ctx.config().regions;
179
+ if (!raw) {
180
+ state.mesh?.stop();
181
+ state.mesh = undefined;
182
+ state.key = undefined;
183
+ return undefined;
184
+ }
185
+ const key = JSON.stringify(raw);
186
+ if (state.key !== key) {
187
+ const cfg = resolveRegions(raw);
188
+ if (state.mesh && state.mesh.self === cfg.self)
189
+ state.mesh.configure(cfg);
190
+ else
191
+ state.mesh = new RegionMesh(cfg);
192
+ state.key = key;
193
+ state.mesh.start(() => ctx.onlineServers?.() ?? []);
194
+ }
195
+ return state.mesh;
196
+ }
197
+ registerFeature({
198
+ id: 'regions',
199
+ since: '5.2.0',
200
+ summary: 'Multi-region active-active: replicated state, peer health, cross-region failover routing',
201
+ mount: (router, ctx) => {
202
+ const state = {};
203
+ ctx.onStop?.(() => state.mesh?.stop());
204
+ // Start syncing as soon as the gateway is up (config may also change later; re-checked on each request).
205
+ setImmediate(() => { try {
206
+ meshFor(ctx, state);
207
+ }
208
+ catch { /* invalid config is reported on request */ } });
209
+ const need = (res) => {
210
+ const m = meshFor(ctx, state);
211
+ if (!m)
212
+ res.status(404).json({ error: 'Not Found', message: 'multi-region is not configured (`regions:`)' });
213
+ return m;
214
+ };
215
+ router.get('/', (_req, res) => {
216
+ const m = need(res);
217
+ if (m)
218
+ res.json(m.status());
219
+ });
220
+ router.post('/sync', (req, res) => {
221
+ const m = need(res);
222
+ if (!m)
223
+ return;
224
+ const b = objectBody(req, res);
225
+ if (!b)
226
+ return;
227
+ if (typeof b.region !== 'string')
228
+ return badRequest(res, '"region" is required');
229
+ if (!m.peerList().some((p) => p.id === b.region))
230
+ return void res.status(403).json({ error: 'Forbidden', message: `unknown region "${b.region}"` });
231
+ const entries = Array.isArray(b.entries) ? b.entries : [];
232
+ m.merge(entries);
233
+ m.sawPeer(b.region, b.servers, entries.reduce((x, e) => Math.max(x, e?.ts ?? 0), 0));
234
+ res.json({ region: m.self, entries: m.delta(typeof b.since === 'number' ? b.since : 0), servers: ctx.onlineServers?.() ?? [] });
235
+ });
236
+ router.get('/kv/:key', (req, res) => {
237
+ const m = need(res);
238
+ if (!m)
239
+ return;
240
+ const v = m.get(req.params.key);
241
+ if (v === undefined)
242
+ return void res.status(404).json({ error: 'Not Found', message: 'no such key' });
243
+ res.json({ key: req.params.key, value: v });
244
+ });
245
+ router.put('/kv/:key', (req, res) => {
246
+ const m = need(res);
247
+ if (!m)
248
+ return;
249
+ const b = objectBody(req, res);
250
+ if (!b)
251
+ return;
252
+ if (!('value' in b) || b.value === null)
253
+ return badRequest(res, '"value" is required (use DELETE to remove)');
254
+ res.json(m.put(req.params.key, b.value));
255
+ });
256
+ router.delete('/kv/:key', (req, res) => {
257
+ const m = need(res);
258
+ if (m)
259
+ res.json({ deleted: m.delete(req.params.key) });
260
+ });
261
+ router.get('/route/:serverId', (req, res) => {
262
+ const m = need(res);
263
+ if (m)
264
+ res.json({ serverId: req.params.serverId, ...m.route(req.params.serverId, ctx.onlineServers?.() ?? []) });
265
+ });
266
+ },
267
+ });
268
+ //# sourceMappingURL=regions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regions.js","sourceRoot":"","sources":["../../src/features/regions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,UAAU,EAAuB,MAAM,wBAAwB,CAAC;AAEtG,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC;KAC3B,MAAM,CAAC;IACN,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,sBAAsB,EAAE,sCAAsC,CAAC;IAC7F,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IACvD,iEAAiE;IACjE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7C,KAAK,EAAE,CAAC;SACL,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;SACzJ,OAAO,CAAC,EAAE,CAAC;CACf,CAAC;KACD,MAAM,EAAE,CAAC;AA0BZ,wFAAwF;AACxF,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,CAAkB,EAAE,CAA8B,EAAW,EAAE,CACnF,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D,MAAM,OAAO,UAAU;IAMD;IAAgC;IALnC,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;IAC7C,KAAK,GAAG,IAAI,GAAG,EAAqB,CAAC;IAC9C,KAAK,GAAG,CAAC,CAAC;IACV,KAAK,CAAkC;IAE/C,YAAoB,GAAa,EAAmB,MAAoB,IAAI,CAAC,GAAG;QAA5D,QAAG,GAAH,GAAG,CAAU;QAAmB,QAAG,GAAH,GAAG,CAAyB;QAC9E,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,SAAS,CAAC,GAAa;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACjD,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAClF,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACjC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;QACpL,CAAC;IACH,CAAC;IAEO,IAAI;QACV,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAClD,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,GAAG,CAAC,GAAW;QACb,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IACrD,CAAC;IAED,GAAG,CAAC,GAAW,EAAE,KAAc;QAC7B,MAAM,CAAC,GAAoB,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACtH,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACzB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC;QACxC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACpB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,MAAM,CAAC;IAC3E,CAAC;IAED,kEAAkE;IAClE,KAAK,CAAC,OAA0B;QAC9B,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;gBAAE,SAAS;YAC1G,IAAI,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBACtC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;gBAC5F,CAAC,EAAE,CAAC;YACN,CAAC;YACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1C,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,gGAAgG;IAChG,KAAK,CAAC,KAAK,GAAG,CAAC;QACb,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;IAC5F,CAAC;IAED,QAAQ;QACN,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtG,CAAC;IAED,iEAAiE;IACjE,OAAO,CAAC,EAAU,EAAE,OAAgB,EAAE,MAAe;QACnD,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7B,IAAI,CAAC,CAAC;YAAE,OAAO;QACf,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;QACf,CAAC,CAAC,QAAQ,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QAChD,OAAO,CAAC,CAAC,SAAS,CAAC;QACnB,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,CAAC,CAAC,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC5D,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxE,CAAC;IAED,UAAU,CAAC,EAAU,EAAE,GAAW;QAChC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7B,IAAI,CAAC,CAAC;YAAE,OAAO;QACf,CAAC,CAAC,QAAQ,EAAE,CAAC;QACb,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC;QAClB,IAAI,CAAC,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS;YAAE,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC;IAC1D,CAAC;IAED,6CAA6C;IAC7C,KAAK,CAAC,QAAgB,EAAE,WAAqB;QAC3C,IAAI,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;QAC/D,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC5F,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACtF,CAAC;IAED,+CAA+C;IAC/C,KAAK,CAAC,QAAQ,CAAC,WAAqB,EAAE,IAAkB,KAAK;QAC3D,MAAM,OAAO,CAAC,GAAG,CACf,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;YAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;YACvD,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,4BAA4B,EAAE;oBAC3E,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;oBACpH,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;oBAC9G,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;iBACrE,CAAC,CAAC;gBACH,IAAI,CAAC,GAAG,CAAC,EAAE;oBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;gBACnD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAwD,CAAC;gBACvF,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBACpB,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACxF,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAG,CAAW,CAAC,OAAO,CAAC,CAAC;YAC9C,CAAC;QACH,CAAC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,WAA2B,EAAE,IAAkB,KAAK;QACxD,IAAI,CAAC,IAAI,EAAE,CAAC;QACZ,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM;YAAE,OAAO;QACnC,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC9F,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IACvB,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,KAAK;YAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;IACzB,CAAC;IAED,MAAM;QACJ,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;IACrH,CAAC;CACF;AAED,yDAAyD;AACzD,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAY,EAAY,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAEnF,SAAS,OAAO,CAAC,GAAmB,EAAE,KAA0C;IAC9E,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC;IACjC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;QACnB,KAAK,CAAC,IAAI,GAAG,SAAS,CAAC;QACvB,KAAK,CAAC,GAAG,GAAG,SAAS,CAAC;QACtB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,KAAK,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;QACtB,MAAM,GAAG,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;;YACrE,KAAK,CAAC,IAAI,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC;QACtC,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC;QAChB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC;AACpB,CAAC;AAED,eAAe,CAAC;IACd,EAAE,EAAE,SAAS;IACb,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,0FAA0F;IACnG,KAAK,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;QACrB,MAAM,KAAK,GAAwC,EAAE,CAAC;QACtD,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,yGAAyG;QACzG,YAAY,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;YAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,2CAA2C,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3G,MAAM,IAAI,GAAG,CAAC,GAA+B,EAAE,EAAE;YAC/C,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC9B,IAAI,CAAC,CAAC;gBAAE,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,6CAA6C,EAAE,CAAC,CAAC;YAC7G,OAAO,CAAC,CAAC;QACX,CAAC,CAAC;QACF,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;YAC5B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,IAAI,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAChC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,IAAI,CAAC,CAAC;gBAAE,OAAO;YACf,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC/B,IAAI,CAAC,CAAC;gBAAE,OAAO;YACf,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;gBAAE,OAAO,UAAU,CAAC,GAAG,EAAE,sBAAsB,CAAC,CAAC;YACjF,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC;gBAAE,OAAO,KAAK,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;YACpJ,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,OAA6B,CAAC,CAAC,CAAC,EAAE,CAAC;YACjF,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACjB,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACrF,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;QAClI,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAClC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,IAAI,CAAC,CAAC;gBAAE,OAAO;YACf,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,GAAI,CAAC,CAAC;YACjC,IAAI,CAAC,KAAK,SAAS;gBAAE,OAAO,KAAK,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;YACtG,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9C,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAClC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,IAAI,CAAC,CAAC;gBAAE,OAAO;YACf,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC/B,IAAI,CAAC,CAAC;gBAAE,OAAO;YACf,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI;gBAAE,OAAO,UAAU,CAAC,GAAG,EAAE,4CAA4C,CAAC,CAAC;YAC9G,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,GAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACrC,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,IAAI,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,GAAI,CAAC,EAAE,CAAC,CAAC;QAC1D,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAC1C,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,IAAI,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,QAAS,EAAE,GAAG,CAAC,aAAa,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QACpH,CAAC,CAAC,CAAC;IACL,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Feature modules (5.1): self-contained admin capabilities mounted under `/api/v1/admin/<id>`, operators only.
3
+ *
4
+ * Each module is pure logic plus a small router; the gateway hands it a {@link FeatureContext} (running config,
5
+ * tool list, a tool invoker and recent request metrics). `GET /api/v1/admin/features` lists what is mounted.
6
+ *
7
+ * @module gateway/features
8
+ */
9
+ import express, { type Request, type RequestHandler, type Router } from 'express';
10
+ import type { GatewayConfig, ToolInfo, RequestMetric, ProxyResponse } from '../utils/types.js';
11
+ export interface FeatureContext {
12
+ config: () => GatewayConfig;
13
+ tools: () => ToolInfo[];
14
+ /** Call a tool through the full invoker pipeline (policy, cache, costs, audit). */
15
+ invoke: (serverId: string, tool: string, args: Record<string, unknown>, clientId?: string) => Promise<ProxyResponse>;
16
+ /** Most recent request metrics, newest first. */
17
+ recent: (limit?: number) => RequestMetric[];
18
+ /** Base URL of this gateway when listening (for self-tests). */
19
+ baseUrl: () => string | undefined;
20
+ /** Upstream server ids currently online on this gateway (5.2). */
21
+ onlineServers?: () => string[];
22
+ /** Register a cleanup run when the gateway stops (5.2): timers, sockets. */
23
+ onStop?: (fn: () => void | Promise<void>) => void;
24
+ }
25
+ export interface FeatureModule {
26
+ /** Mount point: `/api/v1/admin/<id>`. */
27
+ id: string;
28
+ /** Version that introduced it. */
29
+ since: string;
30
+ summary: string;
31
+ mount: (router: Router, ctx: FeatureContext) => void;
32
+ }
33
+ /** Top-level config sections owned by feature modules; all hot reload (5.2+). */
34
+ export declare const FEATURE_CONFIG_KEYS: readonly ["regions"];
35
+ /** Copy the feature-owned config sections of `next` (for hot reload). */
36
+ export declare function featureSections(next: GatewayConfig): Partial<GatewayConfig>;
37
+ /** Register a feature module (idempotent by id; later registrations replace earlier ones). */
38
+ export declare function registerFeature(f: FeatureModule): void;
39
+ export declare function listFeatures(): ReadonlyArray<Pick<FeatureModule, 'id' | 'since' | 'summary'>>;
40
+ export interface FeatureRouterDeps {
41
+ authenticate: RequestHandler;
42
+ isOperator: (req: Request) => boolean;
43
+ context: FeatureContext;
44
+ features?: FeatureModule[];
45
+ }
46
+ /** Router for `/api/v1`: `GET /admin/features` and every module under `/admin/<id>`. */
47
+ export declare function createFeatureRouter(deps: FeatureRouterDeps): express.Router;
48
+ /** Parse a JSON-object body; responds 400 and returns undefined otherwise. */
49
+ export declare function objectBody(req: Request, res: express.Response): Record<string, unknown> | undefined;
50
+ export declare const badRequest: (res: express.Response, message: string) => undefined;
51
+ //# sourceMappingURL=features.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features.d.ts","sourceRoot":"","sources":["../../src/gateway/features.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,OAAO,EAAE,EAAE,KAAK,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,MAAM,EAAE,MAAM,SAAS,CAAC;AAClF,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG/F,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,aAAa,CAAC;IAC5B,KAAK,EAAE,MAAM,QAAQ,EAAE,CAAC;IACxB,mFAAmF;IACnF,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IACrH,iDAAiD;IACjD,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,aAAa,EAAE,CAAC;IAC5C,gEAAgE;IAChE,OAAO,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClC,kEAAkE;IAClE,aAAa,CAAC,EAAE,MAAM,MAAM,EAAE,CAAC;IAC/B,4EAA4E;IAC5E,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;CACnD;AAED,MAAM,WAAW,aAAa;IAC5B,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;CACtD;AAID,iFAAiF;AACjF,eAAO,MAAM,mBAAmB,sBAAoE,CAAC;AAErG,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAE3E;AAED,8FAA8F;AAC9F,wBAAgB,eAAe,CAAC,CAAC,EAAE,aAAa,GAAG,IAAI,CAItD;AAED,wBAAgB,YAAY,IAAI,aAAa,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC,CAE7F;AAED,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,cAAc,CAAC;IAC7B,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IACtC,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,wFAAwF;AACxF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAc3E;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAOnG;AAED,eAAO,MAAM,UAAU,GAAI,KAAK,OAAO,CAAC,QAAQ,EAAE,SAAS,MAAM,cAAiE,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Feature modules (5.1): self-contained admin capabilities mounted under `/api/v1/admin/<id>`, operators only.
3
+ *
4
+ * Each module is pure logic plus a small router; the gateway hands it a {@link FeatureContext} (running config,
5
+ * tool list, a tool invoker and recent request metrics). `GET /api/v1/admin/features` lists what is mounted.
6
+ *
7
+ * @module gateway/features
8
+ */
9
+ import express from 'express';
10
+ import { VERSION } from '../utils/version.js';
11
+ const registry = [];
12
+ /** Top-level config sections owned by feature modules; all hot reload (5.2+). */
13
+ export const FEATURE_CONFIG_KEYS = ['regions'];
14
+ /** Copy the feature-owned config sections of `next` (for hot reload). */
15
+ export function featureSections(next) {
16
+ return Object.fromEntries(FEATURE_CONFIG_KEYS.map((k) => [k, next[k]]));
17
+ }
18
+ /** Register a feature module (idempotent by id; later registrations replace earlier ones). */
19
+ export function registerFeature(f) {
20
+ const i = registry.findIndex((x) => x.id === f.id);
21
+ if (i >= 0)
22
+ registry[i] = f;
23
+ else
24
+ registry.push(f);
25
+ }
26
+ export function listFeatures() {
27
+ return registry.map(({ id, since, summary }) => ({ id, since, summary }));
28
+ }
29
+ /** Router for `/api/v1`: `GET /admin/features` and every module under `/admin/<id>`. */
30
+ export function createFeatureRouter(deps) {
31
+ const router = express.Router();
32
+ const operator = (req, res, next) => deps.isOperator(req) ? next() : void res.status(403).json({ error: 'Forbidden', message: 'The admin API is for operators (unscoped keys)' });
33
+ const mods = deps.features ?? registry;
34
+ router.get('/admin/features', deps.authenticate, operator, (_req, res) => {
35
+ res.json({ version: VERSION, features: mods.map(({ id, since, summary }) => ({ id, since, summary, path: `/api/v1/admin/${id}` })) });
36
+ });
37
+ for (const m of mods) {
38
+ const sub = express.Router();
39
+ m.mount(sub, deps.context);
40
+ router.use(`/admin/${m.id}`, deps.authenticate, operator, sub);
41
+ }
42
+ return router;
43
+ }
44
+ /** Parse a JSON-object body; responds 400 and returns undefined otherwise. */
45
+ export function objectBody(req, res) {
46
+ const b = req.body;
47
+ if (!b || typeof b !== 'object' || Array.isArray(b)) {
48
+ res.status(400).json({ error: 'Bad Request', message: 'Body must be a JSON object' });
49
+ return undefined;
50
+ }
51
+ return b;
52
+ }
53
+ export const badRequest = (res, message) => void res.status(400).json({ error: 'Bad Request', message });
54
+ //# sourceMappingURL=features.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features.js","sourceRoot":"","sources":["../../src/gateway/features.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@
4
4
  import type { AddressInfo } from 'net';
5
5
  import type { GatewayConfig } from '../utils/types.js';
6
6
  import { ServerRegistry } from '../registry/index.js';
7
+ import '../features/index.js';
7
8
  import { McpEndpoint } from '../mcp/endpoint.js';
8
9
  import { type ExporterStats } from '../monitor/siem.js';
9
10
  import { type StateStore } from '../state/index.js';
@@ -40,6 +41,8 @@ export declare class Gateway {
40
41
  private cors;
41
42
  private ipFilter?;
42
43
  private jsonParser;
44
+ /** 5.2: cleanups registered by feature modules. */
45
+ private readonly featureStops;
43
46
  private readonly reloadLock;
44
47
  /** Developer portal keys (3.8). */
45
48
  readonly portal: PortalStore;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/gateway/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/gateway/index.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,8 @@ import { createApiRouter, serverStateSamples } from './api.js';
14
14
  import { createOpenAIRouter } from '../bridges/openai.js';
15
15
  import { createAdminRouter } from './admin.js';
16
16
  import { createEdgeControlRouter } from './edge-control.js';
17
+ import { createFeatureRouter, featureSections } from './features.js';
18
+ import '../features/index.js';
17
19
  import { deprecate } from '../utils/deprecations.js';
18
20
  import { createA2ARouter } from '../bridges/a2a.js';
19
21
  import { ServerSupervisor } from './supervisor.js';
@@ -73,6 +75,8 @@ export class Gateway {
73
75
  cors;
74
76
  ipFilter;
75
77
  jsonParser;
78
+ /** 5.2: cleanups registered by feature modules. */
79
+ featureStops = [];
76
80
  reloadLock = new Mutex();
77
81
  /** Developer portal keys (3.8). */
78
82
  portal;
@@ -307,6 +311,26 @@ export class Gateway {
307
311
  authenticate: this.router.authenticate,
308
312
  isOperator: (req) => this.router.isOperator(req),
309
313
  }));
314
+ // 5.1: feature modules under /api/v1/admin/<id> (src/features).
315
+ this.app.use('/api/v1', createFeatureRouter({
316
+ authenticate: this.router.authenticate,
317
+ isOperator: (req) => this.router.isOperator(req),
318
+ context: {
319
+ config: () => this.config,
320
+ tools: () => this.registry.getAllTools(),
321
+ invoke: (serverId, name, args, clientId) => this.invoker.invoke({ serverId, name, kind: 'tool', method: 'tools/call', params: { name, arguments: args }, clientId: clientId ?? 'feature', via: 'rest', timeoutMs: this.registry.getServer(serverId)?.timeout }),
322
+ recent: (limit) => this.metrics.getRecent(limit),
323
+ onlineServers: () => this.registry.getAllServers().filter((s) => this.registry.getHealth(s.id)?.status === 'online').map((s) => s.id),
324
+ onStop: (fn) => void this.featureStops.push(fn),
325
+ baseUrl: () => {
326
+ const a = this.address();
327
+ if (!a)
328
+ return undefined;
329
+ const host = a.address === '::' || a.address === '0.0.0.0' ? '127.0.0.1' : a.address.includes(':') ? `[${a.address}]` : a.address;
330
+ return `http://${host}:${a.port}`;
331
+ },
332
+ },
333
+ }));
310
334
  this.app.use('/api/v1', this.router);
311
335
  this.app.use('/api/v1', this.chains.router(this.router.authenticate));
312
336
  this.app.get('/api/v1/mtls', this.router.authenticate, (req, res) => {
@@ -449,6 +473,8 @@ export class Gateway {
449
473
  }
450
474
  async _stop() {
451
475
  logger.info('Shutting down mcp-gateway...');
476
+ for (const fn of this.featureStops.splice(0))
477
+ await Promise.resolve(fn()).catch(() => { });
452
478
  const closed = this.server.listening
453
479
  ? new Promise((res, rej) => this.server.close((err) => (err ? rej(err) : res())))
454
480
  : Promise.resolve();
@@ -632,6 +658,7 @@ export class Gateway {
632
658
  // openai.path is fixed at start; other bridge settings hot reload
633
659
  openai: next.openai ? { ...next.openai, path: this.config.openai?.path } : next.openai,
634
660
  a2a: next.a2a,
661
+ ...featureSections(next),
635
662
  configDir: next.configDir ?? this.config.configDir,
636
663
  };
637
664
  if (!same(prevCatalog, next.catalog)) {