@ni-c/mcp-hub 0.9.2 → 0.11.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +619 -0
  2. package/README.md +189 -64
  3. package/dist/admin.js +232 -8
  4. package/dist/admin.js.map +1 -1
  5. package/dist/auth/address.js +89 -0
  6. package/dist/auth/address.js.map +1 -0
  7. package/dist/auth/api-tokens.js +27 -0
  8. package/dist/auth/api-tokens.js.map +1 -0
  9. package/dist/auth/cimd.js +367 -0
  10. package/dist/auth/cimd.js.map +1 -0
  11. package/dist/auth/consent-page.js +4 -7
  12. package/dist/auth/consent-page.js.map +1 -1
  13. package/dist/auth/headers.js +22 -1
  14. package/dist/auth/headers.js.map +1 -1
  15. package/dist/auth/login-page.js +4 -7
  16. package/dist/auth/login-page.js.map +1 -1
  17. package/dist/auth/oidc/adapter.js +135 -0
  18. package/dist/auth/oidc/adapter.js.map +1 -0
  19. package/dist/auth/oidc/interactions.js +187 -0
  20. package/dist/auth/oidc/interactions.js.map +1 -0
  21. package/dist/auth/oidc/mount.js +144 -0
  22. package/dist/auth/oidc/mount.js.map +1 -0
  23. package/dist/auth/oidc/provider.js +440 -0
  24. package/dist/auth/oidc/provider.js.map +1 -0
  25. package/dist/auth/oidc/quirks.js +234 -0
  26. package/dist/auth/oidc/quirks.js.map +1 -0
  27. package/dist/auth/oidc/verifier.js +121 -0
  28. package/dist/auth/oidc/verifier.js.map +1 -0
  29. package/dist/auth/page.js +22 -0
  30. package/dist/auth/page.js.map +1 -1
  31. package/dist/auth/pinned-fetch.js +129 -0
  32. package/dist/auth/pinned-fetch.js.map +1 -0
  33. package/dist/auth/protected-resource.js +41 -0
  34. package/dist/auth/protected-resource.js.map +1 -0
  35. package/dist/auth/rate-limit.js +156 -0
  36. package/dist/auth/rate-limit.js.map +1 -0
  37. package/dist/auth/redirect-uri.js +90 -0
  38. package/dist/auth/redirect-uri.js.map +1 -0
  39. package/dist/auth/registration.js +146 -0
  40. package/dist/auth/registration.js.map +1 -0
  41. package/dist/auth/session.js +43 -0
  42. package/dist/auth/session.js.map +1 -0
  43. package/dist/auth/signed-token.js +49 -0
  44. package/dist/auth/signed-token.js.map +1 -0
  45. package/dist/auth/store.js +516 -5
  46. package/dist/auth/store.js.map +1 -1
  47. package/dist/auth/text.js +51 -0
  48. package/dist/auth/text.js.map +1 -0
  49. package/dist/config.js +184 -5
  50. package/dist/config.js.map +1 -1
  51. package/dist/docker-proxy/policy.js +1 -0
  52. package/dist/docker-proxy/policy.js.map +1 -1
  53. package/dist/docker-proxy/server.js +1 -0
  54. package/dist/docker-proxy/server.js.map +1 -1
  55. package/dist/elicitation.js +0 -0
  56. package/dist/elicitation.js.map +1 -0
  57. package/dist/forward.js +0 -0
  58. package/dist/forward.js.map +1 -0
  59. package/dist/health.js +22 -3
  60. package/dist/health.js.map +1 -1
  61. package/dist/hub.js +337 -29
  62. package/dist/hub.js.map +1 -1
  63. package/dist/index.js +218 -23
  64. package/dist/index.js.map +1 -1
  65. package/dist/limits.js +13 -1
  66. package/dist/limits.js.map +1 -1
  67. package/dist/mcp-limits.js +14 -2
  68. package/dist/mcp-limits.js.map +1 -1
  69. package/dist/proxy.js +262 -34
  70. package/dist/proxy.js.map +1 -1
  71. package/dist/stdio.js +97 -7
  72. package/dist/stdio.js.map +1 -1
  73. package/dist/subscriptions.js +236 -0
  74. package/dist/subscriptions.js.map +1 -0
  75. package/dist/supervisor.js +472 -28
  76. package/dist/supervisor.js.map +1 -1
  77. package/dist/timings.js +61 -0
  78. package/dist/timings.js.map +1 -0
  79. package/dist/tool-filter.js +59 -0
  80. package/dist/tool-filter.js.map +1 -0
  81. package/dist/transports/docker.js.map +1 -1
  82. package/dist/transports/stream.js +33 -20
  83. package/dist/transports/stream.js.map +1 -1
  84. package/dist/upstream/auth.js +435 -0
  85. package/dist/upstream/auth.js.map +1 -0
  86. package/dist/upstream/login.js +94 -0
  87. package/dist/upstream/login.js.map +1 -0
  88. package/dist/upstream/provider.js +286 -0
  89. package/dist/upstream/provider.js.map +1 -0
  90. package/dist/upstream/routes.js +97 -0
  91. package/dist/upstream/routes.js.map +1 -0
  92. package/package.json +23 -7
  93. package/dist/auth/provider.js +0 -380
  94. package/dist/auth/provider.js.map +0 -1
  95. package/dist/auth/routes.js +0 -223
  96. package/dist/auth/routes.js.map +0 -1
@@ -0,0 +1,187 @@
1
+ import crypto from 'node:crypto';
2
+ import bcrypt from 'bcryptjs';
3
+ import express, { Router } from 'express';
4
+ import { renderConsentPage } from '../consent-page.js';
5
+ import { allowFormActionTo, authSecurityHeaders } from '../headers.js';
6
+ import { renderLoginPage } from '../login-page.js';
7
+ import { earlyRateLimit, LoginRateLimiter } from '../rate-limit.js';
8
+ import { isLoopbackOnly } from '../redirect-uri.js';
9
+ import { createSessionCookie, csrfToken, readSessionCookie, SESSION_COOKIE, SESSION_TTL_MS, verifyCsrfToken } from '../session.js';
10
+ import { logSafe } from '../text.js';
11
+ import { HUB_ACCOUNT_ID } from './provider.js';
12
+ /**
13
+ * The hub's own login and consent pages, driven by oidc-provider's interaction
14
+ * loop instead of by HubOAuthProvider's hand-rolled one.
15
+ *
16
+ * The pages themselves are unchanged. They post to a RELATIVE `login` and
17
+ * `consent`, and that resolves to `/interaction/<uid>/login` because the page is
18
+ * served from a path ending in a slash — which it has to be anyway, since
19
+ * oidc-provider scopes the interaction cookie to exactly that path. The
20
+ * interaction id still travels in the hidden `request` field too, and is checked
21
+ * so a form cannot be replayed against a different interaction.
22
+ *
23
+ * Two behaviours from the old flow are load-bearing and preserved here:
24
+ *
25
+ * - **Typing the password is the consent.** A first-time client is approved
26
+ * by the login itself, so the consent page is only ever shown to someone
27
+ * who is already signed in and is being asked about a NEW client.
28
+ * - **One session cookie.** `mcp_hub_session` is set alongside
29
+ * oidc-provider's own, because `hasValidSession()` is read outside the auth
30
+ * layer by the upstream OAuth callback. Without it the operator would have
31
+ * to log in twice for two different things.
32
+ */
33
+ export function createOidcInteractionRoutes(options) {
34
+ const { provider, store } = options;
35
+ const router = Router();
36
+ const rateLimiter = new LoginRateLimiter();
37
+ // The pages this router serves are the ones with a password field on them, so
38
+ // they need the anti-clickjacking and no-store headers at least as much as
39
+ // the token endpoint does. `allowFormActionTo` narrows the CSP afterwards for
40
+ // the single page that has a redirect target to allow.
41
+ router.use(authSecurityHeaders);
42
+ const secure = new URL(options.externalUrl).protocol === 'https:';
43
+ const checkPassword = (password) => {
44
+ if (options.passwordHash)
45
+ return bcrypt.compareSync(password, options.passwordHash);
46
+ const expected = Buffer.from(options.password ?? '');
47
+ const given = Buffer.from(password);
48
+ return expected.length === given.length && crypto.timingSafeEqual(expected, given);
49
+ };
50
+ /** What the page may say about who is asking, and what it must not claim. */
51
+ const identityOf = async (clientId, resource) => {
52
+ const client = await provider.Client.find(clientId);
53
+ const metadata = client?.metadata();
54
+ return {
55
+ clientName: typeof metadata?.client_name === 'string' ? metadata.client_name : undefined,
56
+ resource,
57
+ // A metadata-document client is identified by a URL nobody can forge, so
58
+ // the page can show where the self-declared name came from.
59
+ clientId: options.cimd && clientId.startsWith('https://') ? clientId : undefined,
60
+ loopbackOnly: isLoopbackOnly(metadata?.redirect_uris)
61
+ };
62
+ };
63
+ const expired = (res, status, message) => {
64
+ res.status(status).type('html').send(`<p>${message}</p>`);
65
+ };
66
+ // The two POSTs below carry a limiter and this GET did not, which is the
67
+ // asymmetry CodeQL noticed and it was right. The page is unauthenticated by
68
+ // construction — the uid is all a caller has — and rendering it costs a
69
+ // provider store lookup, so a flood is cheap to send and not free to serve.
70
+ // The same window as its siblings; a person opening a login page a hundred
71
+ // times in fifteen minutes has a different problem.
72
+ router.get('/interaction/:uid', earlyRateLimit(15 * 60_000, 100, 500), async (req, res, next) => {
73
+ try {
74
+ const details = await provider.interactionDetails(req, res);
75
+ const params = details.params;
76
+ const redirectUri = String(params.redirect_uri ?? '');
77
+ const identity = await identityOf(String(params.client_id), params.resource);
78
+ allowFormActionTo(res, redirectUri);
79
+ if (details.prompt.name === 'login') {
80
+ res.status(200).type('html').send(renderLoginPage(details.uid, redirectUri, identity));
81
+ return;
82
+ }
83
+ const session = readSessionCookie(req.headers.cookie, store.cookieSecret);
84
+ if (!session) {
85
+ expired(res, 401, 'Session expired. Close this window and connect again.');
86
+ return;
87
+ }
88
+ res
89
+ .status(200)
90
+ .type('html')
91
+ .send(renderConsentPage(details.uid, csrfToken(session, store.cookieSecret), redirectUri, identity));
92
+ }
93
+ catch {
94
+ // interactionDetails throws for an unknown or expired uid, which is not
95
+ // an error worth a stack trace: the window was left open too long.
96
+ expired(res, 400, 'Authorization request expired. Close this window and connect again.');
97
+ void next;
98
+ }
99
+ });
100
+ router.post('/interaction/:uid/login', earlyRateLimit(15 * 60_000, 100, 500), express.urlencoded({ extended: false }), async (req, res) => {
101
+ const ip = req.ip ?? 'unknown';
102
+ const { password, request } = req.body;
103
+ let details;
104
+ try {
105
+ details = await provider.interactionDetails(req, res);
106
+ }
107
+ catch {
108
+ expired(res, 400, 'Authorization request expired. Close this window and connect again.');
109
+ return;
110
+ }
111
+ if (typeof request !== 'string' || request !== details.uid) {
112
+ expired(res, 400, 'Authorization request expired. Close this window and connect again.');
113
+ return;
114
+ }
115
+ if (rateLimiter.isBlocked(ip)) {
116
+ console.warn(`mcp-hub: login rate limit exceeded from ${logSafe(ip)}`);
117
+ expired(res, 429, 'Too many attempts. Try again later.');
118
+ return;
119
+ }
120
+ const params = details.params;
121
+ const redirectUri = String(params.redirect_uri ?? '');
122
+ if (typeof password !== 'string' || !checkPassword(password)) {
123
+ rateLimiter.recordFailure(ip);
124
+ console.warn(`mcp-hub: authentication failure from ${logSafe(ip)}`);
125
+ const identity = await identityOf(String(params.client_id), params.resource);
126
+ allowFormActionTo(res, redirectUri);
127
+ res.status(401).type('html').send(renderLoginPage(details.uid, redirectUri, identity, 'Wrong password'));
128
+ return;
129
+ }
130
+ rateLimiter.reset(ip);
131
+ console.log(`mcp-hub: successful login from ${logSafe(ip)}`);
132
+ // Typing the password is the consent for the client that triggered it.
133
+ const clientId = String(params.client_id);
134
+ const client = await provider.Client.find(clientId);
135
+ const clientName = client?.metadata()?.client_name;
136
+ store.saveApproval(clientId, redirectUri, typeof clientName === 'string' ? clientName : undefined);
137
+ console.log(`mcp-hub: approved OAuth client ${logSafe(clientId)} for ${logSafe(redirectUri)}`);
138
+ res.cookie(SESSION_COOKIE, createSessionCookie(store.cookieSecret), {
139
+ httpOnly: true,
140
+ secure,
141
+ sameSite: 'lax',
142
+ maxAge: SESSION_TTL_MS,
143
+ path: '/'
144
+ });
145
+ await provider.interactionFinished(req, res, { login: { accountId: HUB_ACCOUNT_ID } }, { mergeWithLastSubmission: false });
146
+ });
147
+ router.post('/interaction/:uid/consent', earlyRateLimit(15 * 60_000, 100, 500), express.urlencoded({ extended: false }), async (req, res) => {
148
+ const { request, csrf, action } = req.body;
149
+ let details;
150
+ try {
151
+ details = await provider.interactionDetails(req, res);
152
+ }
153
+ catch {
154
+ expired(res, 400, 'Authorization request expired. Close this window and connect again.');
155
+ return;
156
+ }
157
+ if (typeof request !== 'string' || request !== details.uid) {
158
+ expired(res, 400, 'Authorization request expired. Close this window and connect again.');
159
+ return;
160
+ }
161
+ const session = readSessionCookie(req.headers.cookie, store.cookieSecret);
162
+ if (!session) {
163
+ expired(res, 401, 'Session expired. Close this window and connect again.');
164
+ return;
165
+ }
166
+ if (!verifyCsrfToken(session, csrf, store.cookieSecret)) {
167
+ console.warn(`mcp-hub: consent with an invalid CSRF token from ${logSafe(req.ip ?? 'unknown')}`);
168
+ expired(res, 403, 'This form is no longer valid. Close this window and connect again.');
169
+ return;
170
+ }
171
+ if (action !== 'approve') {
172
+ await provider.interactionFinished(req, res, { error: 'access_denied' }, { mergeWithLastSubmission: false });
173
+ return;
174
+ }
175
+ const params = details.params;
176
+ const clientId = String(params.client_id);
177
+ const client = await provider.Client.find(clientId);
178
+ const clientName = client?.metadata()?.client_name;
179
+ store.saveApproval(clientId, String(params.redirect_uri ?? ''), typeof clientName === 'string' ? clientName : undefined);
180
+ console.log(`mcp-hub: approved OAuth client ${logSafe(clientId)} for ${logSafe(String(params.redirect_uri ?? ''))}`);
181
+ // The grant itself is minted by loadExistingGrant on the resumed request,
182
+ // which is the same code path an already-approved client takes.
183
+ await provider.interactionFinished(req, res, { consent: {} }, { mergeWithLastSubmission: true });
184
+ });
185
+ return router;
186
+ }
187
+ //# sourceMappingURL=interactions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactions.js","sourceRoot":"","sources":["../../../src/auth/oidc/interactions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,144 @@
1
+ import { earlyRateLimit } from '../rate-limit.js';
2
+ import { defaultScope, stripPhantomSecret } from './quirks.js';
3
+ /**
4
+ * Every path the provider answers on, given the `routes` reconfiguration in
5
+ * provider.ts.
6
+ *
7
+ * Two of these are not endpoints and are easy to miss:
8
+ *
9
+ * - `/authorize/:uid` is the RESUME route (initialize_app.js registers it at
10
+ * `${routes.authorization}/:uid`). Without it the redirect back from the
11
+ * login page lands in the hub's 404 handler and the authorization flow
12
+ * dead-ends silently, with a plausible-looking error.
13
+ * - `/register/:id` is RFC 7592 registration management.
14
+ *
15
+ * Both `.well-known` paths are served by the provider with an identical body,
16
+ * which is exactly the alias the hub used to build by hand for ChatGPT.
17
+ */
18
+ export const OIDC_PATHS = [
19
+ '/authorize',
20
+ '/authorize/:uid',
21
+ '/token',
22
+ '/revoke',
23
+ '/register',
24
+ '/register/:id',
25
+ '/jwks',
26
+ '/.well-known/oauth-authorization-server',
27
+ '/.well-known/openid-configuration'
28
+ ];
29
+ /**
30
+ * Makes EXTERNAL_URL authoritative for every URL the provider publishes.
31
+ *
32
+ * oidc-provider builds endpoint URLs from the REQUEST (`urlFor` resolves against
33
+ * `ctx.href`), not from the issuer. Two consequences the hub cannot accept:
34
+ *
35
+ * - behind a reverse proxy the document would advertise the internal host, and
36
+ * - a request carrying `Host: evil.example` would be answered with a discovery
37
+ * document pointing at evil.example.
38
+ *
39
+ * The hub already treats EXTERNAL_URL as the single source of truth and compares
40
+ * it byte for byte, so overwriting the request's idea of its own origin is the
41
+ * faithful translation rather than a workaround.
42
+ *
43
+ * It also closes the `provider.proxy` gap. That flag is a boolean — it trusts
44
+ * any `X-Forwarded-*` — whereas Express was configured with a CIDR list. Because
45
+ * all three headers are overwritten here, the client cannot influence them, and
46
+ * `X-Forwarded-For` is replaced with the address Express already resolved
47
+ * against TRUSTED_PROXIES.
48
+ */
49
+ export function forceExternalOrigin(externalUrl) {
50
+ const url = new URL(externalUrl);
51
+ const proto = url.protocol.replace(':', '');
52
+ return (req, _res, next) => {
53
+ req.headers.host = url.host;
54
+ req.headers['x-forwarded-host'] = url.host;
55
+ req.headers['x-forwarded-proto'] = proto;
56
+ if (req.ip)
57
+ req.headers['x-forwarded-for'] = req.ip;
58
+ else
59
+ delete req.headers['x-forwarded-for'];
60
+ next();
61
+ };
62
+ }
63
+ /**
64
+ * The same budgets the hub applied before, per path.
65
+ *
66
+ * They belong here rather than at the call site because the mounted Koa app is
67
+ * never entered by the Express router: a limit that is not put in front of the
68
+ * mount simply does not exist, and nothing about the resulting configuration
69
+ * looks wrong. `/register` is the tightest — registration is open — and it also
70
+ * has to cover `/register/:id`, whose own budget is more generous because
71
+ * routine management would otherwise use up a client's ability to register.
72
+ */
73
+ export function defaultRateLimits() {
74
+ return {
75
+ '/register': [earlyRateLimit(60 * 60_000, 20, 200)],
76
+ '/register/:id': [earlyRateLimit(60 * 60_000, 60, 600)],
77
+ '/authorize': [earlyRateLimit(15 * 60_000, 100, 1_000)],
78
+ '/authorize/:uid': [earlyRateLimit(15 * 60_000, 100, 1_000)],
79
+ '/token': [earlyRateLimit(15 * 60_000, 50, 500)]
80
+ };
81
+ }
82
+ /**
83
+ * Mounts the authorization server into the hub's Express app.
84
+ *
85
+ * `app.all(path, ...)` and NOT `app.use(path, ...)`: `use` strips the prefix
86
+ * from `req.url`, while oidc-provider derives its own mount path from the
87
+ * issuer (provider.js `#mountPath = new URL(issuer).pathname`). The hub's
88
+ * issuer is the origin root, so stripping would leave the router looking at
89
+ * paths the published URLs do not match.
90
+ *
91
+ * The mounted app is also a dead end — Koa's `handleRequest` always responds,
92
+ * so anything that reaches it never returns to Express. Mounting per path
93
+ * rather than at the root is what keeps `/livez`, `/health` and every MCP route
94
+ * reachable.
95
+ */
96
+ export function mountOidcProvider(app, provider, store, options) {
97
+ const callback = provider.callback();
98
+ // Safe only in combination with forceExternalOrigin, which overwrites every
99
+ // forwarded header the client could otherwise set.
100
+ provider.proxy = true;
101
+ /**
102
+ * Errors do not cross the boundary. A throw inside the Koa app is caught by
103
+ * `ctx.onerror` and never reaches the hub's four-argument Express error
104
+ * handler, so without this an authorization-server fault would be invisible
105
+ * in the hub's logs.
106
+ */
107
+ provider.on('server_error', (_ctx, error) => {
108
+ console.error(`mcp-hub: authorization server failed: ${error.message}`);
109
+ });
110
+ /**
111
+ * RFC 8414 also defines a path-insertion form, and clients probing a specific
112
+ * resource look up `/.well-known/oauth-authorization-server/<name>/mcp` before
113
+ * falling back to the root document. oidc-provider registers only the exact
114
+ * paths, so without this the suffix form 404s — which the hub answered before.
115
+ * One authorization server covers every resource, so the answer is the same
116
+ * document.
117
+ */
118
+ for (const base of ['/.well-known/oauth-authorization-server', '/.well-known/openid-configuration']) {
119
+ app.get(`${base}/{*splat}`, ...(options.common ?? []), forceExternalOrigin(options.externalUrl), (req, res) => {
120
+ req.url = base;
121
+ callback(req, res);
122
+ });
123
+ }
124
+ // Built once: each entry holds a counter, and rebuilding per path would give
125
+ // every route a fresh, empty limiter on every iteration.
126
+ const before = options.before ?? defaultRateLimits();
127
+ for (const path of OIDC_PATHS) {
128
+ const handlers = [
129
+ ...(options.common ?? []),
130
+ ...(before[path] ?? []),
131
+ forceExternalOrigin(options.externalUrl)
132
+ ];
133
+ if (path === '/authorize')
134
+ handlers.push(defaultScope);
135
+ // Reads the body, so it has to BE the terminal handler rather than sit in
136
+ // front of one: the request stream can only be consumed once.
137
+ if (path === '/token')
138
+ handlers.push(stripPhantomSecret(store, callback));
139
+ else
140
+ handlers.push((req, res) => callback(req, res));
141
+ app.all(path, ...handlers);
142
+ }
143
+ }
144
+ //# sourceMappingURL=mount.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount.js","sourceRoot":"","sources":["../../../src/auth/oidc/mount.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE/D;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,YAAY;IACZ,iBAAiB;IACjB,QAAQ;IACR,SAAS;IACT,WAAW;IACX,eAAe;IACf,OAAO;IACP,yCAAyC;IACzC,mCAAmC;CAC3B,CAAC;AAEX;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAmB;IACrD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;IACjC,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;QACzB,GAAG,CAAC,OAAO,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QAC5B,GAAG,CAAC,OAAO,CAAC,kBAAkB,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;QAC3C,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC,GAAG,KAAK,CAAC;QACzC,IAAI,GAAG,CAAC,EAAE;YAAE,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;;YAC/C,OAAO,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;QAC3C,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO;QACL,WAAW,EAAE,CAAC,cAAc,CAAC,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QACnD,eAAe,EAAE,CAAC,cAAc,CAAC,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;QACvD,YAAY,EAAE,CAAC,cAAc,CAAC,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QACvD,iBAAiB,EAAE,CAAC,cAAc,CAAC,EAAE,GAAG,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QAC5D,QAAQ,EAAE,CAAC,cAAc,CAAC,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;KACjD,CAAC;AACJ,CAAC;AAYD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAY,EAAE,QAAkB,EAAE,KAAgB,EAAE,OAAqB;IACzG,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,EAAsD,CAAC;IACzF,4EAA4E;IAC5E,mDAAmD;IACnD,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC;IAEtB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC,IAAI,EAAE,KAAY,EAAE,EAAE;QACjD,OAAO,CAAC,KAAK,CAAC,yCAAyC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH;;;;;;;OAOG;IACH,KAAK,MAAM,IAAI,IAAI,CAAC,yCAAyC,EAAE,mCAAmC,CAAC,EAAE,CAAC;QACpG,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,WAAW,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,mBAAmB,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YAC5G,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC;YACf,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,6EAA6E;IAC7E,yDAAyD;IACzD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,iBAAiB,EAAE,CAAC;IAErD,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAqB;YACjC,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;YACzB,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACvB,mBAAmB,CAAC,OAAO,CAAC,WAAW,CAAC;SACzC,CAAC;QACF,IAAI,IAAI,KAAK,YAAY;YAAE,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACvD,0EAA0E;QAC1E,8DAA8D;QAC9D,IAAI,IAAI,KAAK,QAAQ;YAAE,QAAQ,CAAC,IAAI,CAAC,kBAAkB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC;;YACrE,QAAQ,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QACrD,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,QAAQ,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC"}