@asif2bd/umami-mcp 0.1.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.
- package/LICENSE +21 -0
- package/README.md +400 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.js +163 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.js +121 -0
- package/dist/config.js.map +1 -0
- package/dist/envfile.d.ts +21 -0
- package/dist/envfile.js +72 -0
- package/dist/envfile.js.map +1 -0
- package/dist/http.d.ts +7 -0
- package/dist/http.js +133 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/consent.d.ts +13 -0
- package/dist/oauth/consent.js +99 -0
- package/dist/oauth/consent.js.map +1 -0
- package/dist/oauth/router.d.ts +27 -0
- package/dist/oauth/router.js +329 -0
- package/dist/oauth/router.js.map +1 -0
- package/dist/oauth/seal.d.ts +22 -0
- package/dist/oauth/seal.js +82 -0
- package/dist/oauth/seal.js.map +1 -0
- package/dist/oauth/store.d.ts +40 -0
- package/dist/oauth/store.js +48 -0
- package/dist/oauth/store.js.map +1 -0
- package/dist/redact.d.ts +14 -0
- package/dist/redact.js +54 -0
- package/dist/redact.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +47 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/admin.d.ts +1 -0
- package/dist/tools/admin.js +99 -0
- package/dist/tools/admin.js.map +1 -0
- package/dist/tools/analytics.d.ts +1 -0
- package/dist/tools/analytics.js +141 -0
- package/dist/tools/analytics.js.map +1 -0
- package/dist/tools/common.d.ts +46 -0
- package/dist/tools/common.js +86 -0
- package/dist/tools/common.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/reports.d.ts +1 -0
- package/dist/tools/reports.js +122 -0
- package/dist/tools/reports.js.map +1 -0
- package/dist/tools/websites.d.ts +1 -0
- package/dist/tools/websites.js +194 -0
- package/dist/tools/websites.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { UmamiClient } from '../client.js';
|
|
2
|
+
import { redactUnknown } from '../redact.js';
|
|
3
|
+
import { consentPage, errorPage } from './consent.js';
|
|
4
|
+
import { MemoryStore } from './store.js';
|
|
5
|
+
import { seal, unseal, verifyPkce, safeEqual, SealError } from './seal.js';
|
|
6
|
+
const MODES = ['read', 'write', 'admin'];
|
|
7
|
+
function json(res, status, body) {
|
|
8
|
+
const text = JSON.stringify(body);
|
|
9
|
+
res.writeHead(status, {
|
|
10
|
+
'content-type': 'application/json',
|
|
11
|
+
'cache-control': 'no-store',
|
|
12
|
+
'content-length': Buffer.byteLength(text),
|
|
13
|
+
});
|
|
14
|
+
res.end(text);
|
|
15
|
+
}
|
|
16
|
+
function html(res, status, body, redirectUri) {
|
|
17
|
+
// form-action must name the redirect target as well as 'self'.
|
|
18
|
+
//
|
|
19
|
+
// Browsers enforce form-action across the redirect chain that FOLLOWS a form
|
|
20
|
+
// submission, not just the initial POST. With a bare 'self', the consent form
|
|
21
|
+
// posts fine and the server answers 302 to the client's redirect_uri -- and
|
|
22
|
+
// the browser then silently blocks that navigation. The visible symptom is a
|
|
23
|
+
// Connect button that does nothing at all, which is a miserable thing to debug.
|
|
24
|
+
let formAction = "'self'";
|
|
25
|
+
if (redirectUri) {
|
|
26
|
+
try {
|
|
27
|
+
formAction += ` ${new URL(redirectUri).origin}`;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* unparseable redirect_uri is rejected elsewhere */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
res.writeHead(status, {
|
|
34
|
+
'content-type': 'text/html; charset=utf-8',
|
|
35
|
+
'cache-control': 'no-store',
|
|
36
|
+
'content-security-policy': `default-src 'none'; style-src 'unsafe-inline'; form-action ${formAction}`,
|
|
37
|
+
'x-frame-options': 'DENY',
|
|
38
|
+
'referrer-policy': 'no-referrer',
|
|
39
|
+
});
|
|
40
|
+
res.end(body);
|
|
41
|
+
}
|
|
42
|
+
async function readBody(req, limit = 64 * 1024) {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
let size = 0;
|
|
45
|
+
for await (const chunk of req) {
|
|
46
|
+
size += chunk.length;
|
|
47
|
+
if (size > limit)
|
|
48
|
+
throw new Error('Request body too large.');
|
|
49
|
+
chunks.push(chunk);
|
|
50
|
+
}
|
|
51
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
52
|
+
}
|
|
53
|
+
async function readForm(req) {
|
|
54
|
+
const type = req.headers['content-type'] ?? '';
|
|
55
|
+
const body = await readBody(req);
|
|
56
|
+
if (type.includes('application/json')) {
|
|
57
|
+
const parsed = JSON.parse(body);
|
|
58
|
+
const params = new URLSearchParams();
|
|
59
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
60
|
+
if (v !== undefined && v !== null)
|
|
61
|
+
params.set(k, String(v));
|
|
62
|
+
}
|
|
63
|
+
return params;
|
|
64
|
+
}
|
|
65
|
+
return new URLSearchParams(body);
|
|
66
|
+
}
|
|
67
|
+
export class OAuthProvider {
|
|
68
|
+
o;
|
|
69
|
+
store = new MemoryStore();
|
|
70
|
+
constructor(o) {
|
|
71
|
+
this.o = o;
|
|
72
|
+
}
|
|
73
|
+
/** Validate an Authorization header and return the caller's identity. */
|
|
74
|
+
authenticate(req) {
|
|
75
|
+
const header = req.headers.authorization ?? '';
|
|
76
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
77
|
+
if (!match)
|
|
78
|
+
throw new SealError('Missing bearer token.');
|
|
79
|
+
return unseal(match[1].trim(), this.o.key);
|
|
80
|
+
}
|
|
81
|
+
/** Returns true when the request was an OAuth endpoint and has been handled. */
|
|
82
|
+
async handle(req, res, url) {
|
|
83
|
+
const p = url.pathname;
|
|
84
|
+
if (p === '/.well-known/oauth-authorization-server') {
|
|
85
|
+
json(res, 200, {
|
|
86
|
+
issuer: this.o.issuer,
|
|
87
|
+
authorization_endpoint: `${this.o.issuer}/authorize`,
|
|
88
|
+
token_endpoint: `${this.o.issuer}/token`,
|
|
89
|
+
registration_endpoint: `${this.o.issuer}/register`,
|
|
90
|
+
response_types_supported: ['code'],
|
|
91
|
+
grant_types_supported: ['authorization_code'],
|
|
92
|
+
code_challenge_methods_supported: ['S256'],
|
|
93
|
+
token_endpoint_auth_methods_supported: ['client_secret_post', 'none'],
|
|
94
|
+
scopes_supported: ['umami:read', 'umami:write', 'umami:admin'],
|
|
95
|
+
});
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (p === '/.well-known/oauth-protected-resource') {
|
|
99
|
+
json(res, 200, {
|
|
100
|
+
resource: this.o.issuer,
|
|
101
|
+
authorization_servers: [this.o.issuer],
|
|
102
|
+
scopes_supported: ['umami:read', 'umami:write', 'umami:admin'],
|
|
103
|
+
});
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (p === '/register' && req.method === 'POST') {
|
|
107
|
+
await this.register(req, res);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (p === '/authorize') {
|
|
111
|
+
if (req.method === 'GET')
|
|
112
|
+
return this.showConsent(res, url), true;
|
|
113
|
+
if (req.method === 'POST')
|
|
114
|
+
return await this.grant(req, res), true;
|
|
115
|
+
}
|
|
116
|
+
if (p === '/token' && req.method === 'POST') {
|
|
117
|
+
await this.token(req, res);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
async register(req, res) {
|
|
123
|
+
let body;
|
|
124
|
+
try {
|
|
125
|
+
body = JSON.parse(await readBody(req));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
json(res, 400, { error: 'invalid_client_metadata', error_description: 'Body must be JSON.' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const uris = Array.isArray(body.redirect_uris) ? body.redirect_uris : [];
|
|
132
|
+
if (uris.length === 0) {
|
|
133
|
+
json(res, 400, { error: 'invalid_redirect_uri', error_description: 'redirect_uris is required.' });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const u of uris) {
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = new URL(u);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
json(res, 400, { error: 'invalid_redirect_uri', error_description: `Not a URL: ${u}` });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Loopback may be plaintext; anything else must be HTTPS, or the code
|
|
146
|
+
// can be intercepted in transit.
|
|
147
|
+
const loopback = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
|
|
148
|
+
if (parsed.protocol !== 'https:' && !loopback) {
|
|
149
|
+
json(res, 400, {
|
|
150
|
+
error: 'invalid_redirect_uri',
|
|
151
|
+
error_description: `redirect_uri must use https (or be loopback): ${u}`,
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const client = this.store.registerClient(uris, typeof body.client_name === 'string' ? body.client_name : undefined);
|
|
157
|
+
json(res, 201, {
|
|
158
|
+
client_id: client.clientId,
|
|
159
|
+
client_secret: client.clientSecret,
|
|
160
|
+
client_id_issued_at: Math.floor(client.createdAt / 1000),
|
|
161
|
+
redirect_uris: client.redirectUris,
|
|
162
|
+
token_endpoint_auth_method: 'client_secret_post',
|
|
163
|
+
grant_types: ['authorization_code'],
|
|
164
|
+
response_types: ['code'],
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
showConsent(res, url, error) {
|
|
168
|
+
const q = url.searchParams;
|
|
169
|
+
const clientId = q.get('client_id') ?? '';
|
|
170
|
+
const redirectUri = q.get('redirect_uri') ?? '';
|
|
171
|
+
const method = q.get('code_challenge_method') ?? 'S256';
|
|
172
|
+
if (!clientId || !redirectUri) {
|
|
173
|
+
html(res, 400, errorPage('Invalid request', 'client_id and redirect_uri are required.'));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (q.get('code_challenge') && method !== 'S256') {
|
|
177
|
+
html(res, 400, errorPage('Unsupported PKCE method', 'Only S256 is accepted.'));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const client = this.store.getClient(clientId);
|
|
181
|
+
// An unregistered client_id is allowed (operators may configure a static
|
|
182
|
+
// one), but a registered client's redirect_uri must match exactly.
|
|
183
|
+
if (client && !client.redirectUris.includes(redirectUri)) {
|
|
184
|
+
html(res, 400, errorPage('Invalid redirect URI', 'It does not match this client registration.'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
html(res, 200, consentPage({
|
|
188
|
+
action: url.pathname,
|
|
189
|
+
clientName: client?.name,
|
|
190
|
+
fixedUrl: this.o.fixedUrl,
|
|
191
|
+
error,
|
|
192
|
+
hiddenFields: {
|
|
193
|
+
client_id: clientId,
|
|
194
|
+
redirect_uri: redirectUri,
|
|
195
|
+
state: q.get('state') ?? '',
|
|
196
|
+
code_challenge: q.get('code_challenge') ?? '',
|
|
197
|
+
code_challenge_method: method,
|
|
198
|
+
scope: q.get('scope') ?? '',
|
|
199
|
+
},
|
|
200
|
+
}), redirectUri);
|
|
201
|
+
}
|
|
202
|
+
/** Consent submitted: verify the credentials for real, then mint a code. */
|
|
203
|
+
async grant(req, res) {
|
|
204
|
+
let form;
|
|
205
|
+
try {
|
|
206
|
+
form = await readForm(req);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
html(res, 400, errorPage('Invalid request', redactUnknown(err)));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const clientId = form.get('client_id') ?? '';
|
|
213
|
+
const redirectUri = form.get('redirect_uri') ?? '';
|
|
214
|
+
const umamiUrl = (this.o.fixedUrl ?? form.get('umami_url') ?? '').trim().replace(/\/+$/, '');
|
|
215
|
+
const username = (form.get('umami_username') ?? '').trim();
|
|
216
|
+
const password = form.get('umami_password') ?? '';
|
|
217
|
+
const mode = (form.get('mode') ?? 'read');
|
|
218
|
+
const rerender = (message) => {
|
|
219
|
+
const back = new URL(`${this.o.issuer}/authorize`);
|
|
220
|
+
for (const k of ['client_id', 'redirect_uri', 'state', 'code_challenge', 'code_challenge_method', 'scope']) {
|
|
221
|
+
const v = form.get(k);
|
|
222
|
+
if (v)
|
|
223
|
+
back.searchParams.set(k, v);
|
|
224
|
+
}
|
|
225
|
+
this.showConsent(res, back, message);
|
|
226
|
+
};
|
|
227
|
+
if (!clientId || !redirectUri)
|
|
228
|
+
return rerender('Missing client_id or redirect_uri.');
|
|
229
|
+
if (!MODES.includes(mode))
|
|
230
|
+
return rerender('Invalid permission selection.');
|
|
231
|
+
if (!umamiUrl || !username || !password)
|
|
232
|
+
return rerender('All fields are required.');
|
|
233
|
+
let parsed;
|
|
234
|
+
try {
|
|
235
|
+
parsed = new URL(umamiUrl);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return rerender('That does not look like a valid URL.');
|
|
239
|
+
}
|
|
240
|
+
const loopback = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
|
|
241
|
+
if (parsed.protocol !== 'https:' && !loopback) {
|
|
242
|
+
return rerender('Refusing to send your password over plaintext HTTP. Use an https:// URL.');
|
|
243
|
+
}
|
|
244
|
+
// Prove the credentials work before issuing anything, so a typo fails here
|
|
245
|
+
// rather than silently producing a token that never works.
|
|
246
|
+
const probe = new UmamiClient({ ...this.o.baseConfig, url: umamiUrl, username, password, apiKey: undefined });
|
|
247
|
+
try {
|
|
248
|
+
await probe.verify();
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
return rerender(`Umami rejected those credentials: ${redactUnknown(err)}`);
|
|
252
|
+
}
|
|
253
|
+
const now = Math.floor(Date.now() / 1000);
|
|
254
|
+
const identity = {
|
|
255
|
+
url: umamiUrl,
|
|
256
|
+
username,
|
|
257
|
+
password,
|
|
258
|
+
mode,
|
|
259
|
+
iat: now,
|
|
260
|
+
exp: now + this.o.tokenTtlSeconds,
|
|
261
|
+
};
|
|
262
|
+
const code = this.store.createCode({
|
|
263
|
+
clientId,
|
|
264
|
+
redirectUri,
|
|
265
|
+
codeChallenge: form.get('code_challenge') || undefined,
|
|
266
|
+
codeChallengeMethod: form.get('code_challenge_method') || undefined,
|
|
267
|
+
scope: form.get('scope') || undefined,
|
|
268
|
+
state: form.get('state') || undefined,
|
|
269
|
+
token: seal(identity, this.o.key),
|
|
270
|
+
});
|
|
271
|
+
const location = new URL(redirectUri);
|
|
272
|
+
location.searchParams.set('code', code);
|
|
273
|
+
const state = form.get('state');
|
|
274
|
+
if (state)
|
|
275
|
+
location.searchParams.set('state', state);
|
|
276
|
+
res.writeHead(302, { location: location.toString(), 'cache-control': 'no-store' });
|
|
277
|
+
res.end();
|
|
278
|
+
}
|
|
279
|
+
async token(req, res) {
|
|
280
|
+
let form;
|
|
281
|
+
try {
|
|
282
|
+
form = await readForm(req);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
json(res, 400, { error: 'invalid_request' });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (form.get('grant_type') !== 'authorization_code') {
|
|
289
|
+
json(res, 400, { error: 'unsupported_grant_type', error_description: 'Only authorization_code is supported.' });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const code = form.get('code') ?? '';
|
|
293
|
+
const pending = this.store.consumeCode(code);
|
|
294
|
+
if (!pending) {
|
|
295
|
+
json(res, 400, { error: 'invalid_grant', error_description: 'Code is unknown, used, or expired.' });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (!safeEqual(pending.clientId, form.get('client_id') ?? '')) {
|
|
299
|
+
json(res, 400, { error: 'invalid_grant', error_description: 'Code was issued to a different client.' });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!safeEqual(pending.redirectUri, form.get('redirect_uri') ?? '')) {
|
|
303
|
+
json(res, 400, { error: 'invalid_grant', error_description: 'redirect_uri does not match the authorization request.' });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const client = this.store.getClient(pending.clientId);
|
|
307
|
+
if (client?.clientSecret) {
|
|
308
|
+
const presented = form.get('client_secret') ?? '';
|
|
309
|
+
if (!safeEqual(client.clientSecret, presented)) {
|
|
310
|
+
json(res, 401, { error: 'invalid_client' });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (pending.codeChallenge) {
|
|
315
|
+
const verifier = form.get('code_verifier') ?? '';
|
|
316
|
+
if (!verifier || !verifyPkce(verifier, pending.codeChallenge, pending.codeChallengeMethod ?? 'S256')) {
|
|
317
|
+
json(res, 400, { error: 'invalid_grant', error_description: 'PKCE verification failed.' });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
json(res, 200, {
|
|
322
|
+
access_token: pending.token,
|
|
323
|
+
token_type: 'Bearer',
|
|
324
|
+
expires_in: this.o.tokenTtlSeconds,
|
|
325
|
+
scope: pending.scope ?? 'umami:read',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../../src/oauth/router.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAuB,SAAS,EAAE,MAAM,WAAW,CAAC;AAYhG,MAAM,KAAK,GAAW,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAEjD,SAAS,IAAI,CAAC,GAAmB,EAAE,MAAc,EAAE,IAAa;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAClC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE;QACpB,cAAc,EAAE,kBAAkB;QAClC,eAAe,EAAE,UAAU;QAC3B,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC;KAC1C,CAAC,CAAC;IACH,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,SAAS,IAAI,CAAC,GAAmB,EAAE,MAAc,EAAE,IAAY,EAAE,WAAoB;IACnF,+DAA+D;IAC/D,EAAE;IACF,6EAA6E;IAC7E,8EAA8E;IAC9E,4EAA4E;IAC5E,6EAA6E;IAC7E,gFAAgF;IAChF,IAAI,UAAU,GAAG,QAAQ,CAAC;IAC1B,IAAI,WAAW,EAAE,CAAC;QAChB,IAAI,CAAC;YACH,UAAU,IAAI,IAAI,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,MAAM,EAAE,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,oDAAoD;QACtD,CAAC;IACH,CAAC;IACD,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE;QACpB,cAAc,EAAE,0BAA0B;QAC1C,eAAe,EAAE,UAAU;QAC3B,yBAAyB,EAAE,8DAA8D,UAAU,EAAE;QACrG,iBAAiB,EAAE,MAAM;QACzB,iBAAiB,EAAE,aAAa;KACjC,CAAC,CAAC;IACH,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,GAAoB,EAAE,KAAK,GAAG,EAAE,GAAG,IAAI;IAC7D,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QAC9B,IAAI,IAAK,KAAgB,CAAC,MAAM,CAAC;QACjC,IAAI,IAAI,GAAG,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7D,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,GAAoB;IAC1C,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;IAC/C,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;QACtC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC;QAC3D,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5C,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9D,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,OAAO,aAAa;IAGK;IAFpB,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC;IAEnC,YAA6B,CAAe;QAAf,MAAC,GAAD,CAAC,CAAc;IAAG,CAAC;IAEhD,yEAAyE;IACzE,YAAY,CAAC,GAAoB;QAC/B,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACrD,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,SAAS,CAAC,uBAAuB,CAAC,CAAC;QACzD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC7C,CAAC;IAED,gFAAgF;IAChF,KAAK,CAAC,MAAM,CAAC,GAAoB,EAAE,GAAmB,EAAE,GAAQ;QAC9D,MAAM,CAAC,GAAG,GAAG,CAAC,QAAQ,CAAC;QAEvB,IAAI,CAAC,KAAK,yCAAyC,EAAE,CAAC;YACpD,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE;gBACb,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM;gBACrB,sBAAsB,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,YAAY;gBACpD,cAAc,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,QAAQ;gBACxC,qBAAqB,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,WAAW;gBAClD,wBAAwB,EAAE,CAAC,MAAM,CAAC;gBAClC,qBAAqB,EAAE,CAAC,oBAAoB,CAAC;gBAC7C,gCAAgC,EAAE,CAAC,MAAM,CAAC;gBAC1C,qCAAqC,EAAE,CAAC,oBAAoB,EAAE,MAAM,CAAC;gBACrE,gBAAgB,EAAE,CAAC,YAAY,EAAE,aAAa,EAAE,aAAa,CAAC;aAC/D,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,KAAK,uCAAuC,EAAE,CAAC;YAClD,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE;gBACb,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM;gBACvB,qBAAqB,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;gBACtC,gBAAgB,EAAE,CAAC,YAAY,EAAE,aAAa,EAAE,aAAa,CAAC;aAC/D,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,KAAK,WAAW,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC/C,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC9B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,KAAK,YAAY,EAAE,CAAC;YACvB,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK;gBAAE,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC;YAClE,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;gBAAE,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC;QACrE,CAAC;QAED,IAAI,CAAC,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC5C,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC3B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAEO,KAAK,CAAC,QAAQ,CAAC,GAAoB,EAAE,GAAmB;QAC9D,IAAI,IAA6B,CAAC;QAClC,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,GAAG,CAAC,CAA4B,CAAC;QACpE,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,CAAC,CAAC;YAC9F,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,aAA0B,CAAC,CAAC,CAAC,EAAE,CAAC;QACvF,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,4BAA4B,EAAE,CAAC,CAAC;YACnG,OAAO;QACT,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,IAAI,MAAW,CAAC;YAChB,IAAI,CAAC;gBACH,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;YACtB,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,cAAc,CAAC,EAAE,EAAE,CAAC,CAAC;gBACxF,OAAO;YACT,CAAC;YACD,sEAAsE;YACtE,iCAAiC;YACjC,MAAM,QAAQ,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC7E,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC9C,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE;oBACb,KAAK,EAAE,sBAAsB;oBAC7B,iBAAiB,EAAE,iDAAiD,CAAC,EAAE;iBACxE,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,EAAE,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACpH,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE;YACb,SAAS,EAAE,MAAM,CAAC,QAAQ;YAC1B,aAAa,EAAE,MAAM,CAAC,YAAY;YAClC,mBAAmB,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC;YACxD,aAAa,EAAE,MAAM,CAAC,YAAY;YAClC,0BAA0B,EAAE,oBAAoB;YAChD,WAAW,EAAE,CAAC,oBAAoB,CAAC;YACnC,cAAc,EAAE,CAAC,MAAM,CAAC;SACzB,CAAC,CAAC;IACL,CAAC;IAEO,WAAW,CAAC,GAAmB,EAAE,GAAQ,EAAE,KAAc;QAC/D,MAAM,CAAC,GAAG,GAAG,CAAC,YAAY,CAAC;QAC3B,MAAM,QAAQ,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAC1C,MAAM,WAAW,GAAG,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,uBAAuB,CAAC,IAAI,MAAM,CAAC;QAExD,IAAI,CAAC,QAAQ,IAAI,CAAC,WAAW,EAAE,CAAC;YAC9B,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,CAAC,iBAAiB,EAAE,0CAA0C,CAAC,CAAC,CAAC;YACzF,OAAO;QACT,CAAC;QACD,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACjD,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,CAAC,yBAAyB,EAAE,wBAAwB,CAAC,CAAC,CAAC;YAC/E,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC9C,yEAAyE;QACzE,mEAAmE;QACnE,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YACzD,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,CAAC,sBAAsB,EAAE,6CAA6C,CAAC,CAAC,CAAC;YACjG,OAAO;QACT,CAAC;QAED,IAAI,CACF,GAAG,EACH,GAAG,EACH,WAAW,CAAC;YACV,MAAM,EAAE,GAAG,CAAC,QAAQ;YACpB,UAAU,EAAE,MAAM,EAAE,IAAI;YACxB,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ;YACzB,KAAK;YACL,YAAY,EAAE;gBACZ,SAAS,EAAE,QAAQ;gBACnB,YAAY,EAAE,WAAW;gBACzB,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE;gBAC3B,cAAc,EAAE,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE;gBAC7C,qBAAqB,EAAE,MAAM;gBAC7B,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE;aAC5B;SACF,CAAC,EACF,WAAW,CACZ,CAAC;IACJ,CAAC;IAED,4EAA4E;IACpE,KAAK,CAAC,KAAK,CAAC,GAAoB,EAAE,GAAmB;QAC3D,IAAI,IAAqB,CAAC;QAC1B,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,CAAC,iBAAiB,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACjE,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAC7C,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QACnD,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC7F,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;QAClD,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,MAAM,CAAS,CAAC;QAElD,MAAM,QAAQ,GAAG,CAAC,OAAe,EAAE,EAAE;YACnC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,MAAM,YAAY,CAAC,CAAC;YACnD,KAAK,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,cAAc,EAAE,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,OAAO,CAAC,EAAE,CAAC;gBAC3G,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBACtB,IAAI,CAAC;oBAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACrC,CAAC;YACD,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACvC,CAAC,CAAC;QAEF,IAAI,CAAC,QAAQ,IAAI,CAAC,WAAW;YAAE,OAAO,QAAQ,CAAC,oCAAoC,CAAC,CAAC;QACrF,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,QAAQ,CAAC,+BAA+B,CAAC,CAAC;QAC5E,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ;YAAE,OAAO,QAAQ,CAAC,0BAA0B,CAAC,CAAC;QAErF,IAAI,MAAW,CAAC;QAChB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,QAAQ,CAAC,sCAAsC,CAAC,CAAC;QAC1D,CAAC;QACD,MAAM,QAAQ,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC7E,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC9C,OAAO,QAAQ,CAAC,0EAA0E,CAAC,CAAC;QAC9F,CAAC;QAED,2EAA2E;QAC3E,2DAA2D;QAC3D,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QAC9G,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,MAAM,EAAE,CAAC;QACvB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,QAAQ,CAAC,qCAAqC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7E,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,MAAM,QAAQ,GAAmB;YAC/B,GAAG,EAAE,QAAQ;YACb,QAAQ;YACR,QAAQ;YACR,IAAI;YACJ,GAAG,EAAE,GAAG;YACR,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,eAAe;SAClC,CAAC;QAEF,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC;YACjC,QAAQ;YACR,WAAW;YACX,aAAa,EAAE,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,SAAS;YACtD,mBAAmB,EAAE,IAAI,CAAC,GAAG,CAAC,uBAAuB,CAAC,IAAI,SAAS;YACnE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,SAAS;YACrC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,SAAS;YACrC,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;SAClC,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;QACtC,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,KAAK;YAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACrD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC,CAAC;QACnF,GAAG,CAAC,GAAG,EAAE,CAAC;IACZ,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,GAAoB,EAAE,GAAmB;QAC3D,IAAI,IAAqB,CAAC;QAC1B,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;YAC7C,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,KAAK,oBAAoB,EAAE,CAAC;YACpD,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,uCAAuC,EAAE,CAAC,CAAC;YAChH,OAAO;QACT,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,oCAAoC,EAAE,CAAC,CAAC;YACpG,OAAO;QACT,CAAC;QAED,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YAC9D,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,wCAAwC,EAAE,CAAC,CAAC;YACxG,OAAO;QACT,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YACpE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,wDAAwD,EAAE,CAAC,CAAC;YACxH,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACtD,IAAI,MAAM,EAAE,YAAY,EAAE,CAAC;YACzB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;YAClD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC,EAAE,CAAC;gBAC/C,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;gBAC5C,OAAO;YACT,CAAC;QACH,CAAC;QAED,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;YAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;YACjD,IAAI,CAAC,QAAQ,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,mBAAmB,IAAI,MAAM,CAAC,EAAE,CAAC;gBACrG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,CAAC,CAAC;gBAC3F,OAAO;YACT,CAAC;QACH,CAAC;QAED,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE;YACb,YAAY,EAAE,OAAO,CAAC,KAAK;YAC3B,UAAU,EAAE,QAAQ;YACpB,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,eAAe;YAClC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,YAAY;SACrC,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface SealedIdentity {
|
|
2
|
+
url: string;
|
|
3
|
+
username?: string;
|
|
4
|
+
password?: string;
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
/** Permission tier the user consented to. */
|
|
7
|
+
mode: 'read' | 'write' | 'admin';
|
|
8
|
+
/** Unix seconds. */
|
|
9
|
+
iat: number;
|
|
10
|
+
exp: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class SealError extends Error {
|
|
13
|
+
}
|
|
14
|
+
/** Derive a 32-byte key from arbitrary operator-supplied material. */
|
|
15
|
+
export declare function deriveKey(material: string): Buffer;
|
|
16
|
+
export declare function generateKey(): string;
|
|
17
|
+
export declare function seal(identity: SealedIdentity, key: Buffer): string;
|
|
18
|
+
export declare function unseal(token: string, key: Buffer): SealedIdentity;
|
|
19
|
+
/** Constant-time compare for client secrets and PKCE verifiers. */
|
|
20
|
+
export declare function safeEqual(a: string, b: string): boolean;
|
|
21
|
+
/** RFC 7636 S256 challenge verification. */
|
|
22
|
+
export declare function verifyPkce(verifier: string, challenge: string, method?: string): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Access tokens are sealed credential envelopes, not database keys.
|
|
4
|
+
*
|
|
5
|
+
* A multi-tenant hosted deployment must let each user reach their own Umami
|
|
6
|
+
* with their own login. The obvious way is a sessions table mapping token to
|
|
7
|
+
* credentials -- which means the host is now storing a pile of other people's
|
|
8
|
+
* analytics passwords, and is one backup leak away from a very bad day.
|
|
9
|
+
*
|
|
10
|
+
* Instead the credentials travel inside the token itself, encrypted with a key
|
|
11
|
+
* only the server holds. The server keeps no session state at all: it decrypts
|
|
12
|
+
* on each request, uses the credentials, and forgets them.
|
|
13
|
+
*
|
|
14
|
+
* Trade-off, stated plainly: anyone holding the sealing key can decrypt any
|
|
15
|
+
* token they capture. The key must be persisted out of band (UMAMI_MCP_TOKEN_KEY)
|
|
16
|
+
* and treated as the most sensitive value in the deployment. Rotating it
|
|
17
|
+
* invalidates every issued token, which is the intended blast-radius control.
|
|
18
|
+
*/
|
|
19
|
+
const ALG = 'aes-256-gcm';
|
|
20
|
+
const VERSION = 'v1';
|
|
21
|
+
export class SealError extends Error {
|
|
22
|
+
}
|
|
23
|
+
/** Derive a 32-byte key from arbitrary operator-supplied material. */
|
|
24
|
+
export function deriveKey(material) {
|
|
25
|
+
if (material.length < 16) {
|
|
26
|
+
throw new SealError('UMAMI_MCP_TOKEN_KEY must be at least 16 characters; 32+ random bytes is better.');
|
|
27
|
+
}
|
|
28
|
+
return createHash('sha256').update(material, 'utf8').digest();
|
|
29
|
+
}
|
|
30
|
+
export function generateKey() {
|
|
31
|
+
return randomBytes(32).toString('base64url');
|
|
32
|
+
}
|
|
33
|
+
export function seal(identity, key) {
|
|
34
|
+
const iv = randomBytes(12);
|
|
35
|
+
const cipher = createCipheriv(ALG, key, iv);
|
|
36
|
+
const plaintext = Buffer.from(JSON.stringify(identity), 'utf8');
|
|
37
|
+
const body = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
38
|
+
const tag = cipher.getAuthTag();
|
|
39
|
+
return [VERSION, iv.toString('base64url'), body.toString('base64url'), tag.toString('base64url')].join('.');
|
|
40
|
+
}
|
|
41
|
+
export function unseal(token, key) {
|
|
42
|
+
const parts = token.split('.');
|
|
43
|
+
if (parts.length !== 4 || parts[0] !== VERSION)
|
|
44
|
+
throw new SealError('Malformed token.');
|
|
45
|
+
let identity;
|
|
46
|
+
try {
|
|
47
|
+
const iv = Buffer.from(parts[1], 'base64url');
|
|
48
|
+
const body = Buffer.from(parts[2], 'base64url');
|
|
49
|
+
const tag = Buffer.from(parts[3], 'base64url');
|
|
50
|
+
const decipher = createDecipheriv(ALG, key, iv);
|
|
51
|
+
decipher.setAuthTag(tag);
|
|
52
|
+
const plaintext = Buffer.concat([decipher.update(body), decipher.final()]);
|
|
53
|
+
identity = JSON.parse(plaintext.toString('utf8'));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Never distinguish "wrong key" from "tampered" from "malformed":
|
|
57
|
+
// that distinction is a decryption oracle.
|
|
58
|
+
throw new SealError('Invalid or expired token.');
|
|
59
|
+
}
|
|
60
|
+
if (typeof identity.exp !== 'number' || identity.exp * 1000 < Date.now()) {
|
|
61
|
+
throw new SealError('Invalid or expired token.');
|
|
62
|
+
}
|
|
63
|
+
return identity;
|
|
64
|
+
}
|
|
65
|
+
/** Constant-time compare for client secrets and PKCE verifiers. */
|
|
66
|
+
export function safeEqual(a, b) {
|
|
67
|
+
const ab = Buffer.from(a, 'utf8');
|
|
68
|
+
const bb = Buffer.from(b, 'utf8');
|
|
69
|
+
if (ab.length !== bb.length)
|
|
70
|
+
return false;
|
|
71
|
+
return timingSafeEqual(ab, bb);
|
|
72
|
+
}
|
|
73
|
+
/** RFC 7636 S256 challenge verification. */
|
|
74
|
+
export function verifyPkce(verifier, challenge, method = 'S256') {
|
|
75
|
+
if (method === 'plain')
|
|
76
|
+
return safeEqual(verifier, challenge);
|
|
77
|
+
if (method !== 'S256')
|
|
78
|
+
return false;
|
|
79
|
+
const computed = createHash('sha256').update(verifier, 'utf8').digest('base64url');
|
|
80
|
+
return safeEqual(computed, challenge);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=seal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seal.js","sourceRoot":"","sources":["../../src/oauth/seal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization codes and dynamically-registered clients live in memory only.
|
|
3
|
+
*
|
|
4
|
+
* Codes are single-use and expire in a minute, so losing them on restart costs
|
|
5
|
+
* a user one retry. Registered clients are also in memory: a restart forces
|
|
6
|
+
* re-registration, which MCP clients handle automatically. Nothing here is
|
|
7
|
+
* worth the operational weight of a database.
|
|
8
|
+
*/
|
|
9
|
+
export interface PendingAuth {
|
|
10
|
+
clientId: string;
|
|
11
|
+
redirectUri: string;
|
|
12
|
+
codeChallenge?: string;
|
|
13
|
+
codeChallengeMethod?: string;
|
|
14
|
+
scope?: string;
|
|
15
|
+
state?: string;
|
|
16
|
+
/** Sealed identity, minted at consent time. */
|
|
17
|
+
token: string;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
}
|
|
20
|
+
export interface RegisteredClient {
|
|
21
|
+
clientId: string;
|
|
22
|
+
clientSecret?: string;
|
|
23
|
+
redirectUris: string[];
|
|
24
|
+
name?: string;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
}
|
|
27
|
+
export declare class MemoryStore {
|
|
28
|
+
private codes;
|
|
29
|
+
private clients;
|
|
30
|
+
createCode(auth: Omit<PendingAuth, 'expiresAt'>): string;
|
|
31
|
+
/** Codes are single-use: consuming one removes it, replay included. */
|
|
32
|
+
consumeCode(code: string): PendingAuth | undefined;
|
|
33
|
+
registerClient(redirectUris: string[], name?: string): RegisteredClient;
|
|
34
|
+
getClient(clientId: string): RegisteredClient | undefined;
|
|
35
|
+
private sweep;
|
|
36
|
+
get stats(): {
|
|
37
|
+
codes: number;
|
|
38
|
+
clients: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
const CODE_TTL_MS = 60_000;
|
|
3
|
+
export class MemoryStore {
|
|
4
|
+
codes = new Map();
|
|
5
|
+
clients = new Map();
|
|
6
|
+
createCode(auth) {
|
|
7
|
+
this.sweep();
|
|
8
|
+
const code = randomBytes(32).toString('base64url');
|
|
9
|
+
this.codes.set(code, { ...auth, expiresAt: Date.now() + CODE_TTL_MS });
|
|
10
|
+
return code;
|
|
11
|
+
}
|
|
12
|
+
/** Codes are single-use: consuming one removes it, replay included. */
|
|
13
|
+
consumeCode(code) {
|
|
14
|
+
this.sweep();
|
|
15
|
+
const found = this.codes.get(code);
|
|
16
|
+
if (!found)
|
|
17
|
+
return undefined;
|
|
18
|
+
this.codes.delete(code);
|
|
19
|
+
if (found.expiresAt < Date.now())
|
|
20
|
+
return undefined;
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
registerClient(redirectUris, name) {
|
|
24
|
+
const client = {
|
|
25
|
+
clientId: randomBytes(16).toString('base64url'),
|
|
26
|
+
clientSecret: randomBytes(32).toString('base64url'),
|
|
27
|
+
redirectUris,
|
|
28
|
+
name,
|
|
29
|
+
createdAt: Date.now(),
|
|
30
|
+
};
|
|
31
|
+
this.clients.set(client.clientId, client);
|
|
32
|
+
return client;
|
|
33
|
+
}
|
|
34
|
+
getClient(clientId) {
|
|
35
|
+
return this.clients.get(clientId);
|
|
36
|
+
}
|
|
37
|
+
sweep() {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
for (const [code, auth] of this.codes) {
|
|
40
|
+
if (auth.expiresAt < now)
|
|
41
|
+
this.codes.delete(code);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
get stats() {
|
|
45
|
+
return { codes: this.codes.size, clients: this.clients.size };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/oauth/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA+B1C,MAAM,WAAW,GAAG,MAAM,CAAC;AAE3B,MAAM,OAAO,WAAW;IACd,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IACvC,OAAO,GAAG,IAAI,GAAG,EAA4B,CAAC;IAEtD,UAAU,CAAC,IAAoC;QAC7C,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,MAAM,IAAI,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QACnD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,GAAG,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW,EAAE,CAAC,CAAC;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,uEAAuE;IACvE,WAAW,CAAC,IAAY;QACtB,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC7B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE;YAAE,OAAO,SAAS,CAAC;QACnD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,cAAc,CAAC,YAAsB,EAAE,IAAa;QAClD,MAAM,MAAM,GAAqB;YAC/B,QAAQ,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;YAC/C,YAAY,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;YACnD,YAAY;YACZ,IAAI;YACJ,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,SAAS,CAAC,QAAgB;QACxB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IAEO,KAAK;QACX,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACtC,IAAI,IAAI,CAAC,SAAS,GAAG,GAAG;gBAAE,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpD,CAAC;IACH,CAAC;IAED,IAAI,KAAK;QACP,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IAChE,CAAC;CACF"}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defence in depth: an MCP server hands its output straight to a language
|
|
3
|
+
* model, and from there often into a chat transcript. A stack trace or an
|
|
4
|
+
* echoed request body that happens to contain a password would leak it into
|
|
5
|
+
* places it can never be recalled from. Every string leaving this server is
|
|
6
|
+
* scrubbed first.
|
|
7
|
+
*/
|
|
8
|
+
/** Register values that must never appear in output. Call once at startup. */
|
|
9
|
+
export declare function registerSecrets(...values: (string | undefined)[]): void;
|
|
10
|
+
/** Test seam. */
|
|
11
|
+
export declare function _resetSecrets(): void;
|
|
12
|
+
export declare function redact(input: string): string;
|
|
13
|
+
/** Redact anything, including Error objects and nested structures. */
|
|
14
|
+
export declare function redactUnknown(value: unknown): string;
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defence in depth: an MCP server hands its output straight to a language
|
|
3
|
+
* model, and from there often into a chat transcript. A stack trace or an
|
|
4
|
+
* echoed request body that happens to contain a password would leak it into
|
|
5
|
+
* places it can never be recalled from. Every string leaving this server is
|
|
6
|
+
* scrubbed first.
|
|
7
|
+
*/
|
|
8
|
+
let secrets = [];
|
|
9
|
+
/** Register values that must never appear in output. Call once at startup. */
|
|
10
|
+
export function registerSecrets(...values) {
|
|
11
|
+
for (const v of values) {
|
|
12
|
+
// Very short values would cause absurd over-redaction of ordinary text.
|
|
13
|
+
if (v && v.length >= 6)
|
|
14
|
+
secrets.push(v);
|
|
15
|
+
}
|
|
16
|
+
// Longest first, so an overlapping secret is fully masked.
|
|
17
|
+
secrets.sort((a, b) => b.length - a.length);
|
|
18
|
+
}
|
|
19
|
+
/** Test seam. */
|
|
20
|
+
export function _resetSecrets() {
|
|
21
|
+
secrets = [];
|
|
22
|
+
}
|
|
23
|
+
const PATTERNS = [
|
|
24
|
+
// JSON fields commonly carrying credentials.
|
|
25
|
+
[/("(?:password|token|apiKey|api_key|secret|authorization)"\s*:\s*)"[^"]*"/gi, '$1"[redacted]"'],
|
|
26
|
+
// Authorization headers in any casing.
|
|
27
|
+
[/(authorization\s*:\s*bearer\s+)[A-Za-z0-9._~+/=-]+/gi, '$1[redacted]'],
|
|
28
|
+
// Bare JWTs.
|
|
29
|
+
[/\beyJ[A-Za-z0-9._-]{20,}/g, '[redacted-jwt]'],
|
|
30
|
+
];
|
|
31
|
+
export function redact(input) {
|
|
32
|
+
let out = input;
|
|
33
|
+
for (const s of secrets) {
|
|
34
|
+
// Literal replacement; secrets are not regexes.
|
|
35
|
+
out = out.split(s).join('[redacted]');
|
|
36
|
+
}
|
|
37
|
+
for (const [re, sub] of PATTERNS)
|
|
38
|
+
out = out.replace(re, sub);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** Redact anything, including Error objects and nested structures. */
|
|
42
|
+
export function redactUnknown(value) {
|
|
43
|
+
if (value instanceof Error)
|
|
44
|
+
return redact(value.message);
|
|
45
|
+
if (typeof value === 'string')
|
|
46
|
+
return redact(value);
|
|
47
|
+
try {
|
|
48
|
+
return redact(JSON.stringify(value));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return redact(String(value));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,IAAI,OAAO,GAAa,EAAE,CAAC;AAE3B,8EAA8E;AAC9E,MAAM,UAAU,eAAe,CAAC,GAAG,MAA8B;IAC/D,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,wEAAwE;QACxE,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,2DAA2D;IAC3D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;AAC9C,CAAC;AAED,iBAAiB;AACjB,MAAM,UAAU,aAAa;IAC3B,OAAO,GAAG,EAAE,CAAC;AACf,CAAC;AAED,MAAM,QAAQ,GAAuB;IACnC,6CAA6C;IAC7C,CAAC,4EAA4E,EAAE,gBAAgB,CAAC;IAChG,uCAAuC;IACvC,CAAC,sDAAsD,EAAE,cAAc,CAAC;IACxE,aAAa;IACb,CAAC,2BAA2B,EAAE,gBAAgB,CAAC;CAChD,CAAC;AAEF,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,IAAI,GAAG,GAAG,KAAK,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,gDAAgD;QAChD,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACxC,CAAC;IACD,KAAK,MAAM,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,QAAQ;QAAE,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;IAC7D,OAAO,GAAG,CAAC;AACb,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,KAAK,YAAY,KAAK;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACzD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { UmamiClient } from './client.js';
|
|
3
|
+
import type { Config } from './config.js';
|
|
4
|
+
import { type ToolDef } from './tools/index.js';
|
|
5
|
+
export declare const SERVER_NAME = "umami-mcp";
|
|
6
|
+
export declare const SERVER_VERSION = "0.1.0";
|
|
7
|
+
export interface BuiltServer {
|
|
8
|
+
server: McpServer;
|
|
9
|
+
registered: ToolDef[];
|
|
10
|
+
withheld: ToolDef[];
|
|
11
|
+
}
|
|
12
|
+
export declare function buildServer(config: Config, client?: UmamiClient): BuiltServer;
|